arti/subcommands/
hss.rs

1//! The `hss` subcommand.
2
3use anyhow::anyhow;
4use arti_client::TorClientConfig;
5use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
6use tor_hsservice::{HsId, HsNickname, OnionService};
7
8use crate::{ArtiConfig, Result, TorClient};
9
10/// The hss subcommands the arti CLI will be augmented with.
11#[derive(Parser, Debug)]
12pub(crate) enum HssSubcommands {
13    /// Run state management commands for an Arti hidden service.
14    Hss(Hss),
15}
16
17#[derive(Debug, Parser)]
18pub(crate) struct Hss {
19    /// Arguments shared by all hss subcommands.
20    #[command(flatten)]
21    common: CommonArgs,
22
23    /// Return the identity key for the specified service.
24    #[command(subcommand)]
25    command: HssSubcommand,
26}
27
28#[derive(Subcommand, Debug, Clone)]
29pub(crate) enum HssSubcommand {
30    /// Print the .onion address of a hidden service
31    OnionAddress(OnionAddressArgs),
32
33    /// (Deprecated) Print the .onion address of a hidden service
34    #[command(hide = true)] // This hides the command from the help message
35    OnionName(OnionAddressArgs),
36}
37
38/// The arguments of the [`OnionAddress`](HssSubcommand::OnionAddress) subcommand.
39#[derive(Debug, Clone, Args)]
40pub(crate) struct OnionAddressArgs {
41    /// Whether to generate the key if it is missing
42    #[arg(
43        long,
44        default_value_t = GenerateKey::No,
45        value_enum
46    )]
47    generate: GenerateKey,
48}
49
50/// Whether to generate the key if missing.
51#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
52enum GenerateKey {
53    /// Do not generate the key.
54    #[default]
55    No,
56    /// Generate the key if it's missing.
57    IfNeeded,
58}
59
60/// A type of key
61#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
62enum KeyType {
63    /// The identity key of the service
64    OnionAddress,
65}
66
67/// The arguments shared by all [`HssSubcommand`]s.
68#[derive(Debug, Clone, Args)]
69pub(crate) struct CommonArgs {
70    /// The nickname of the service
71    #[arg(short, long)]
72    nickname: HsNickname,
73}
74
75/// Run the `hss` subcommand.
76pub(crate) fn run(
77    hss_matches: &ArgMatches,
78    config: &ArtiConfig,
79    client_config: &TorClientConfig,
80) -> Result<()> {
81    let hss = Hss::from_arg_matches(hss_matches).expect("Could not parse hss subcommand");
82
83    match hss.command {
84        HssSubcommand::OnionAddress(args) => {
85            run_onion_address(&hss.common, &args, config, client_config)
86        }
87        HssSubcommand::OnionName(args) => {
88            eprintln!(
89                "warning: using deprecated command 'onion-name', (hint: use 'onion-address' instead)"
90            );
91            run_onion_address(&hss.common, &args, config, client_config)
92        }
93    }
94}
95
96/// Create the OnionService configured with `nickname`.
97fn create_svc(
98    nickname: &HsNickname,
99    config: &ArtiConfig,
100    client_config: &TorClientConfig,
101) -> Result<OnionService> {
102    let Some(svc_config) = config
103        .onion_services
104        .iter()
105        .find(|(n, _)| *n == nickname)
106        .map(|(_, cfg)| cfg.svc_cfg.clone())
107    else {
108        return Err(anyhow!("Service {nickname} is not configured"));
109    };
110
111    // TODO: PreferredRuntime was arbitrarily chosen and is entirely unused
112    // (we have to specify a concrete type for the runtime when calling
113    // TorClient::create_onion_service).
114    //
115    // Maybe this suggests TorClient is not the right place for
116    // create_onion_service()
117    Ok(
118        TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
119            client_config,
120            svc_config,
121        )?,
122    )
123}
124
125/// Display the onion address, if any, of the specified service.
126fn display_onion_address(nickname: &HsNickname, hsid: Option<HsId>) -> Result<()> {
127    // TODO: instead of the printlns here, we should have a formatter type that
128    // decides how to display the output
129    if let Some(onion) = hsid {
130        println!("{onion}");
131    } else {
132        return Err(anyhow!(
133            "Service {nickname} does not exist, or does not have an K_hsid yet"
134        ));
135    }
136
137    Ok(())
138}
139
140/// Run the `hss onion-address` subcommand.
141fn onion_address(
142    args: &CommonArgs,
143    config: &ArtiConfig,
144    client_config: &TorClientConfig,
145) -> Result<()> {
146    let onion_svc = create_svc(&args.nickname, config, client_config)?;
147    let hsid = onion_svc.onion_address();
148    display_onion_address(&args.nickname, hsid)?;
149
150    Ok(())
151}
152
153/// Run the `hss onion-address` subcommand.
154fn get_or_generate_onion_address(
155    args: &CommonArgs,
156    config: &ArtiConfig,
157    client_config: &TorClientConfig,
158) -> Result<()> {
159    let svc = create_svc(&args.nickname, config, client_config)?;
160    let hsid = svc.onion_address();
161    match hsid {
162        Some(hsid) => display_onion_address(&args.nickname, Some(hsid)),
163        None => {
164            let selector = Default::default();
165            let hsid = svc.generate_identity_key(selector)?;
166            display_onion_address(&args.nickname, Some(hsid))
167        }
168    }
169}
170
171/// Run the `hss onion-address` subcommand.
172fn run_onion_address(
173    args: &CommonArgs,
174    get_key_args: &OnionAddressArgs,
175    config: &ArtiConfig,
176    client_config: &TorClientConfig,
177) -> Result<()> {
178    match get_key_args.generate {
179        GenerateKey::No => onion_address(args, config, client_config),
180        GenerateKey::IfNeeded => get_or_generate_onion_address(args, config, client_config),
181    }
182}