homestar_runtime/cli/
init.rs

1use inquire::{Confirm, CustomType, Select, Text};
2use miette::{bail, miette, Result};
3use rand::Rng;
4use serde::de::IntoDeserializer;
5use serde_with::{base64::Standard, formats::Padded, DeserializeAs, SerializeAs};
6use std::{
7    fmt::Display,
8    fs::File,
9    io::{empty, stdout, IsTerminal, Write},
10    path::PathBuf,
11    str::FromStr,
12};
13
14use crate::{
15    settings::{KeyType, PubkeyConfig},
16    ExistingKeyPath, NetworkBuilder, NodeBuilder, RNGSeed, Settings, SettingsBuilder,
17};
18
19use super::InitArgs;
20
21/// Where to write the resulting configuration.
22#[derive(Debug)]
23pub enum OutputMode {
24    /// Write to standard output.
25    StdOut,
26    /// Write to a file.
27    File {
28        /// The path to write to.
29        path: PathBuf,
30    },
31}
32
33#[derive(Debug)]
34enum PubkeyConfigOption {
35    GenerateFromSeed,
36    FromFile,
37}
38
39/// The arguments for configuring the key
40#[derive(Debug)]
41pub enum KeyArg {
42    /// Load the key from an existing file
43    File {
44        /// The path of the file
45        path: Option<PathBuf>,
46    },
47    /// Generate the key from a seed
48    Seed {
49        /// The base64 encoded 32 byte seed to use for key generation
50        seed: Option<String>,
51    },
52}
53
54#[derive(Debug, Clone)]
55struct PubkeySeed([u8; 32]);
56
57impl Display for PubkeySeed {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        serde_with::base64::Base64::<Standard, Padded>::serialize_as(&self.0, f)
60    }
61}
62
63impl FromStr for PubkeySeed {
64    type Err = serde::de::value::Error;
65
66    fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
67        Ok(Self(
68            serde_with::base64::Base64::<Standard, Padded>::deserialize_as(s.into_deserializer())?,
69        ))
70    }
71}
72
73impl Display for PubkeyConfigOption {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            PubkeyConfigOption::GenerateFromSeed => write!(f, "Generate from seed"),
77            PubkeyConfigOption::FromFile => write!(f, "From file"),
78        }
79    }
80}
81
82/// Handle the `init` command.
83pub fn handle_init_command(init_args: InitArgs) -> Result<()> {
84    let output_path = init_args.output_path.clone().unwrap_or_else(Settings::path);
85    let output_mode = if init_args.dry_run {
86        OutputMode::StdOut
87    } else {
88        OutputMode::File {
89            path: output_path.clone(),
90        }
91    };
92
93    let key_arg = init_args
94        .key_file
95        .map(|key_file| KeyArg::File { path: key_file })
96        .or_else(|| {
97            init_args
98                .key_seed
99                .map(|key_seed| KeyArg::Seed { seed: key_seed })
100        });
101
102    // Run non-interactively if the input device is not a TTY
103    // or if the `--no-input` flag is passed.
104    let no_input = init_args.no_input || !stdout().is_terminal();
105
106    let mut settings_builder = SettingsBuilder::default();
107    let mut node_builder = NodeBuilder::default();
108    let mut network_builder = NetworkBuilder::default();
109
110    let mut writer = handle_quiet(init_args.quiet)?;
111    let key_type = handle_key_type(init_args.key_type, no_input, &mut writer)?;
112    let keypair_config = handle_key(key_arg, key_type, output_path, no_input, &mut writer)?;
113
114    let network = network_builder
115        .keypair_config(keypair_config)
116        .build()
117        .expect("to build network");
118
119    let node = node_builder
120        .network(network)
121        .build()
122        .expect("to build node");
123
124    let settings = settings_builder
125        .node(node)
126        .build()
127        .expect("to builder settings");
128
129    let settings_toml = toml::to_string_pretty(&settings).expect("to serialize settings");
130
131    handle_output_mode(output_mode, no_input, init_args.force, &mut writer)?
132        .write_all(settings_toml.as_bytes())
133        .expect("to write settings file");
134
135    Ok(())
136}
137
138fn handle_quiet(quiet: bool) -> Result<Box<dyn Write>> {
139    if quiet {
140        Ok(Box::new(empty()))
141    } else {
142        Ok(Box::new(stdout()))
143    }
144}
145
146fn handle_output_mode(
147    output_mode: OutputMode,
148    no_input: bool,
149    force: bool,
150    writer: &mut Box<dyn Write>,
151) -> Result<Box<dyn Write>> {
152    match output_mode {
153        OutputMode::StdOut => Ok(Box::new(stdout())),
154        OutputMode::File { path } if force => {
155            if let Some(parent) = path.parent() {
156                std::fs::create_dir_all(parent).expect("to create parent directory");
157            }
158
159            let settings_file = File::options()
160                .read(true)
161                .write(true)
162                .create(true)
163                .truncate(true)
164                .open(&path)
165                .expect("to open settings file");
166
167            writeln!(writer, "Writing settings to {:?}", path).expect("to write");
168
169            Ok(Box::new(settings_file))
170        }
171        OutputMode::File { path } => {
172            if let Some(parent) = path.parent() {
173                std::fs::create_dir_all(parent).expect("to create parent directory");
174            }
175
176            let settings_file = File::options()
177                .read(true)
178                .write(true)
179                .create_new(true)
180                .open(&path);
181
182            // This seemingly convoluted match is to avoid the risk of a
183            // TOCTOU race condition, where another process creates the file
184            // in between this one checking for its existence and opening it.
185            let settings_file = match settings_file {
186                Ok(file) => file,
187                Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
188                    if no_input {
189                        bail!("Aborting... settings file already exists at {:?}. Pass `--force` to overwrite it", path);
190                    }
191
192                    let should_overwrite = Confirm::new(&format!(
193                        "Settings file already exists at {:?}, overwrite?",
194                        path
195                    ))
196                    .with_default(false)
197                    .prompt()
198                    .map_err(|e| miette!(e))?;
199
200                    if !should_overwrite {
201                        bail!("Aborting... not overwriting existing settings file");
202                    }
203
204                    File::options()
205                        .read(true)
206                        .write(true)
207                        .create_new(false)
208                        .open(&path)
209                        .expect("to open settings file")
210                }
211                err => err.expect("to open settings file"),
212            };
213
214            writeln!(writer, "Writing settings to {:?}", path).expect("to write");
215
216            Ok(Box::new(settings_file))
217        }
218    }
219}
220
221fn handle_key_type(
222    key_type: Option<KeyType>,
223    no_input: bool,
224    _writer: &mut Box<dyn Write>,
225) -> Result<KeyType> {
226    match key_type {
227        Some(key_type) => Ok(key_type),
228        None => {
229            if no_input {
230                bail!("Aborting... cannot prompt for key type in non-interactive mode. Pass `--key-type <KEY_TYPE>` to set it.");
231            }
232
233            let options = vec![KeyType::Ed25519, KeyType::Secp256k1];
234
235            let key_type = Select::new("Select key type", options)
236                .prompt()
237                .map_err(|e| miette!(e))?;
238
239            Ok(key_type)
240        }
241    }
242}
243
244fn handle_key(
245    key_arg: Option<KeyArg>,
246    key_type: KeyType,
247    output_path: PathBuf,
248    no_input: bool,
249    writer: &mut Box<dyn Write>,
250) -> Result<PubkeyConfig> {
251    let config = match key_arg {
252        None => {
253            if no_input {
254                bail!("Aborting... cannot prompt for key in non-interactive mode. Pass `--key-file <KEY_FILE>` or `--key-seed [<KEY_SEED>]` to configure the key.");
255            }
256
257            let options = vec![
258                PubkeyConfigOption::GenerateFromSeed,
259                PubkeyConfigOption::FromFile,
260            ];
261
262            let pubkey_config_choice =
263                Select::new("How would you like to configure the key?", options)
264                    .prompt()
265                    .map_err(|e| miette!(e))?;
266
267            match pubkey_config_choice {
268                PubkeyConfigOption::GenerateFromSeed => {
269                    let seed = CustomType::<PubkeySeed>::new("Enter the seed for the key")
270                        .with_default(PubkeySeed(rand::thread_rng().gen::<[u8; 32]>()))
271                        .with_default_value_formatter(&|_| "random".to_string())
272                        .with_error_message("Please type a base64 encoding of 32 bytes")
273                        .with_help_message("Base64 encoded 32 bytes")
274                        .prompt()
275                        .map_err(|e| miette!(e))?;
276
277                    PubkeyConfig::GenerateFromSeed(RNGSeed::new(key_type, seed.0))
278                }
279                PubkeyConfigOption::FromFile => {
280                    let default_path = if let Some(parent) = output_path.parent() {
281                        parent.join("homestar.pem")
282                    } else {
283                        Settings::path().join("homestar.pem")
284                    };
285
286                    let path = Text::new("Enter the path for the key")
287                        .with_default(&default_path.display().to_string())
288                        .prompt()
289                        .map_err(|e| miette!(e))?
290                        .into();
291
292                    generate_key_file(&path, &key_type, writer)?;
293
294                    PubkeyConfig::Existing(ExistingKeyPath::new(key_type, path))
295                }
296            }
297        }
298        Some(KeyArg::File { path }) => {
299            let path = path.unwrap_or_else(|| {
300                if let Some(parent) = output_path.parent() {
301                    parent.join("homestar.pem")
302                } else {
303                    Settings::path().join("homestar.pem")
304                }
305            });
306
307            generate_key_file(&path, &key_type, writer)?;
308
309            PubkeyConfig::Existing(ExistingKeyPath::new(key_type, path))
310        }
311        Some(KeyArg::Seed { seed: None }) => {
312            let seed = rand::thread_rng().gen::<[u8; 32]>();
313
314            PubkeyConfig::GenerateFromSeed(RNGSeed::new(key_type, seed))
315        }
316        Some(KeyArg::Seed { seed: Some(seed) }) => {
317            let Ok(seed) = PubkeySeed::from_str(&seed) else {
318                bail!("Invalid seed: expected a base64 encoding of 32 bytes")
319            };
320
321            PubkeyConfig::GenerateFromSeed(RNGSeed::new(key_type, seed.0))
322        }
323    };
324
325    config
326        .keypair()
327        .map_err(|e| miette!(format!("Failed to load key: {}", e)))?;
328
329    Ok(config)
330}
331
332fn generate_key_file(
333    path: &PathBuf,
334    key_type: &KeyType,
335    writer: &mut Box<dyn Write>,
336) -> Result<()> {
337    if let Some(parent) = path.parent() {
338        std::fs::create_dir_all(parent).expect("to create parent directory");
339    }
340
341    let key_file = File::options()
342        .read(true)
343        .write(true)
344        .create_new(true)
345        .open(path);
346
347    match key_file {
348        // file did not exist, generate the key
349        Ok(mut file) => {
350            let key = match *key_type {
351                KeyType::Ed25519 => ed25519_compact::KeyPair::generate().sk.to_pem(),
352                KeyType::Secp256k1 => {
353                    std::fs::remove_file(path).expect("to delete key file");
354
355                    bail!("Aborting... generating secp256k1 keys is not yet supported, please provide an existing key file, or choose another key type.")
356                }
357            };
358
359            file.write_all(key.as_bytes())
360                .expect("to write to key file");
361
362            writeln!(writer, "Writing key file to {:?}", path).expect("to write");
363        }
364        // file did exist, do nothing and use existing key
365        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
366            writeln!(writer, "Using existing key file {:?}", path).expect("to write");
367        }
368        err => {
369            err.expect("to open key file");
370        }
371    };
372
373    Ok(())
374}