1
//! The `hss` subcommand.
2

            
3
use anyhow::anyhow;
4
use arti_client::TorClientConfig;
5
use clap::{ArgMatches, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
6
use tor_hsservice::{HsId, HsNickname, OnionService};
7

            
8
use crate::{ArtiConfig, Result, TorClient};
9

            
10
/// The hss subcommands the arti CLI will be augmented with.
11
#[derive(Parser, Debug)]
12
pub(crate) enum HssSubcommands {
13
    /// Run state management commands for an Arti hidden service.
14
    Hss(Hss),
15
}
16

            
17
#[derive(Debug, Parser)]
18
pub(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)]
29
pub(crate) enum HssSubcommand {
30
    /// Print the .onion address of a hidden service
31
    OnionName(OnionNameArgs),
32
}
33

            
34
/// The arguments of the [`OnionName`](HssSubcommand::OnionName) subcommand.
35
#[derive(Debug, Clone, Args)]
36
pub(crate) struct OnionNameArgs {
37
    /// Whether to generate the key if it is missing
38
    #[arg(
39
        long,
40
48
        default_value_t = GenerateKey::No,
41
        value_enum
42
    )]
43
    generate: GenerateKey,
44
}
45

            
46
/// Whether to generate the key if missing.
47
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
48
enum GenerateKey {
49
    /// Do not generate the key.
50
    #[default]
51
    No,
52
    /// Generate the key if it's missing.
53
    IfNeeded,
54
}
55

            
56
/// A type of key
57
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
58
enum KeyType {
59
    /// The identity key of the service
60
    OnionName,
61
}
62

            
63
/// The arguments shared by all [`HssSubcommand`]s.
64
#[derive(Debug, Clone, Args)]
65
pub(crate) struct CommonArgs {
66
    /// The nickname of the service
67
    #[arg(short, long)]
68
    nickname: HsNickname,
69
}
70

            
71
/// Run the `hss` subcommand.
72
18
pub(crate) fn run(
73
18
    hss_matches: &ArgMatches,
74
18
    config: &ArtiConfig,
75
18
    client_config: &TorClientConfig,
76
18
) -> Result<()> {
77
18
    let hss = Hss::from_arg_matches(hss_matches).expect("Could not parse hss subcommand");
78
18

            
79
18
    match hss.command {
80
18
        HssSubcommand::OnionName(args) => run_onion_name(&hss.common, &args, config, client_config),
81
18
    }
82
18
}
83

            
84
/// Create the OnionService configured with `nickname`.
85
18
fn create_svc(
86
18
    nickname: &HsNickname,
87
18
    config: &ArtiConfig,
88
18
    client_config: &TorClientConfig,
89
18
) -> Result<OnionService> {
90
18
    let Some(svc_config) = config
91
18
        .onion_services
92
18
        .iter()
93
33
        .find(|(n, _)| *n == nickname)
94
20
        .map(|(_, cfg)| cfg.svc_cfg.clone())
95
    else {
96
6
        return Err(anyhow!("Service {nickname} is not configured"));
97
    };
98

            
99
    // TODO: PreferredRuntime was arbitrarily chosen and is entirely unused
100
    // (we have to specify a concrete type for the runtime when calling
101
    // TorClient::create_onion_service).
102
    //
103
    // Maybe this suggests TorClient is not the right place for
104
    // create_onion_service()
105
    Ok(
106
12
        TorClient::<tor_rtcompat::PreferredRuntime>::create_onion_service(
107
12
            client_config,
108
12
            svc_config,
109
12
        )?,
110
    )
111
18
}
112

            
113
/// Display the onion address, if any, of the specified service.
114
12
fn display_onion_name(nickname: &HsNickname, hsid: Option<HsId>) -> Result<()> {
115
    // TODO: instead of the printlns here, we should have a formatter type that
116
    // decides how to display the output
117
12
    if let Some(onion) = hsid {
118
6
        println!("{onion}");
119
6
    } else {
120
6
        return Err(anyhow!(
121
6
            "Service {nickname} does not exist, or does not have an K_hsid yet"
122
6
        ));
123
    }
124

            
125
6
    Ok(())
126
12
}
127

            
128
/// Run the `hss onion-name` subcommand.
129
18
fn onion_name(
130
18
    args: &CommonArgs,
131
18
    config: &ArtiConfig,
132
18
    client_config: &TorClientConfig,
133
18
) -> Result<()> {
134
18
    let onion_svc = create_svc(&args.nickname, config, client_config)?;
135
12
    let hsid = onion_svc.onion_name();
136
12
    display_onion_name(&args.nickname, hsid)?;
137

            
138
6
    Ok(())
139
18
}
140

            
141
/// Run the `hss onion-name` subcommand.
142
fn get_or_generate_onion_name(
143
    args: &CommonArgs,
144
    config: &ArtiConfig,
145
    client_config: &TorClientConfig,
146
) -> Result<()> {
147
    let svc = create_svc(&args.nickname, config, client_config)?;
148
    let hsid = svc.onion_name();
149
    match hsid {
150
        Some(hsid) => display_onion_name(&args.nickname, Some(hsid)),
151
        None => {
152
            let selector = Default::default();
153
            let hsid = svc.generate_identity_key(selector)?;
154
            display_onion_name(&args.nickname, Some(hsid))
155
        }
156
    }
157
}
158

            
159
/// Run the `hss onion-name` subcommand.
160
18
fn run_onion_name(
161
18
    args: &CommonArgs,
162
18
    get_key_args: &OnionNameArgs,
163
18
    config: &ArtiConfig,
164
18
    client_config: &TorClientConfig,
165
18
) -> Result<()> {
166
18
    match get_key_args.generate {
167
18
        GenerateKey::No => onion_name(args, config, client_config),
168
        GenerateKey::IfNeeded => get_or_generate_onion_name(args, config, client_config),
169
    }
170
18
}