From 3f9926e3534dd6a02c7d2b515ad71009678e000f Mon Sep 17 00:00:00 2001 From: Paul Spooren Date: Mon, 21 Oct 2024 16:57:57 +0200 Subject: [PATCH] feat(cli): Automatically generate man page Instead of using a static one, generate it via clap_mangen. To generate the manpage run `rosenpass --generate-manpage `. Right now clap does not support flattening of generated manpages, meaning that each subcommand is explained in its own file. To add extra sections to the main file `rosenpass.1`, it's rewritten after the initial creation. Once clap support flattened Man pages, the `generate_to` call can be removed and all subcommand are added to the `rosenpass.1` file. This implementation allows downstream manpage generation to stay unchanged even after switching from multiple manpages to a flattened one. Signed-off-by: Paul Spooren --- .github/workflows/qc.yaml | 2 - Cargo.lock | 27 +++++++++ Cargo.toml | 2 + doc/rosenpass.1 | 114 -------------------------------------- rosenpass/Cargo.toml | 2 + rosenpass/build.rs | 52 ----------------- rosenpass/src/cli.rs | 43 +++++++------- rosenpass/src/main.rs | 56 +++++++++++++++++++ 8 files changed, 107 insertions(+), 191 deletions(-) delete mode 100644 doc/rosenpass.1 delete mode 100644 rosenpass/build.rs diff --git a/.github/workflows/qc.yaml b/.github/workflows/qc.yaml index e0bcc49..14861af 100644 --- a/.github/workflows/qc.yaml +++ b/.github/workflows/qc.yaml @@ -62,8 +62,6 @@ jobs: - name: Install mandoc run: sudo apt-get install -y mandoc - uses: actions/checkout@v3 - - name: Check rosenpass.1 - run: doc/check.sh doc/rosenpass.1 - name: Check rp.1 run: doc/check.sh doc/rp.1 diff --git a/Cargo.lock b/Cargo.lock index 2eea01c..6a03014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,6 +401,15 @@ dependencies = [ "strsim 0.11.1", ] +[[package]] +name = "clap_complete" +version = "4.5.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e" +dependencies = [ + "clap 4.5.20", +] + [[package]] name = "clap_derive" version = "4.5.18" @@ -428,6 +437,16 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "clap_mangen" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +dependencies = [ + "clap 4.5.20", + "roff", +] + [[package]] name = "cmake" version = "0.1.51" @@ -1801,12 +1820,20 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "rosenpass" version = "0.3.0-dev" dependencies = [ "anyhow", "clap 4.5.20", + "clap_complete", + "clap_mangen", "command-fds", "criterion", "derive_builder 0.20.2", diff --git a/Cargo.toml b/Cargo.toml index 6dc890b..3c6c061 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,8 @@ rand = "0.8.5" typenum = "1.17.0" log = { version = "0.4.22" } clap = { version = "4.5.20", features = ["derive"] } +clap_mangen = "0.2.23" +clap_complete = "4.5.29" serde = { version = "1.0.210", features = ["derive"] } arbitrary = { version = "1.3.2", features = ["derive"] } anyhow = { version = "1.0.89", features = ["backtrace", "std"] } diff --git a/doc/rosenpass.1 b/doc/rosenpass.1 deleted file mode 100644 index 000b6f5..0000000 --- a/doc/rosenpass.1 +++ /dev/null @@ -1,114 +0,0 @@ -.Dd $Mdocdate$ -.Dt ROSENPASS 1 -.Os -.Sh NAME -.Nm rosenpass -.Nd builds post-quantum-secure VPNs -.Sh SYNOPSIS -.Nm -.Op COMMAND -.Op Ar OPTIONS ... -.Op Ar ARGS ... -.Sh DESCRIPTION -.Nm -performs cryptographic key exchanges that are secure against quantum-computers -and then outputs the keys. -These keys can then be passed to various services, such as wireguard or other -vpn services, as pre-shared-keys to achieve security against attackers with -quantum computers. -.Pp -This is a research project and quantum computers are not thought to become -practical in fewer than ten years. -If you are not specifically tasked with developing post-quantum secure systems, -you probably do not need this tool. -.Ss COMMANDS -.Bl -tag -width Ds -.It Ar gen-keys --secret-key --public-key -Generate a keypair to use in the exchange command later. -Send the public-key file to your communication partner and keep the private-key -file secret! -.It Ar exchange private-key public-key [ OPTIONS ] PEERS -Start a process to exchange keys with the specified peers. -You should specify at least one peer. -.Pp -Its -.Ar OPTIONS -are as follows: -.Bl -tag -width Ds -.It Ar listen [:] -Instructs -.Nm -to listen on the specified interface and port. -By default, -.Nm -will listen on all interfaces and select a random port. -.It Ar verbose -Extra logging. -.El -.El -.Ss PEER -Each -.Ar PEER -is defined as follows: -.Qq peer public-key [endpoint [:]] [preshared-key ] [outfile ] [wireguard ] -.Pp -Providing a -.Ar PEER -instructs -.Nm -to exchange keys with the given peer and write the resulting PSK into the given -output file. -You must either specify the outfile or wireguard output option. -.Pp -The parameters of -.Ar PEER -are as follows: -.Bl -tag -width Ds -.It Ar endpoint [:] -Specifies the address where the peer can be reached. -This will be automatically updated after the first successful key exchange with -the peer. -If this is unspecified, the peer must initiate the connection. -.It Ar preshared-key -You may specify a pre-shared key which will be mixed into the final secret. -.It Ar outfile -You may specify a file to write the exchanged keys to. -If this option is specified, -.Nm -will write a notification to standard out every time the key is updated. -.It Ar wireguard -This allows you to directly specify a wireguard peer to deploy the -pre-shared-key to. -You may specify extra parameters you would pass to -.Qq wg set -besides the preshared-key parameter which is used by -.Nm . -This makes it possible to add peers entirely from -.Nm . -.El -.Sh EXIT STATUS -.Ex -std -.Sh SEE ALSO -.Xr rp 1 , -.Xr wg 1 -.Rs -.%A Karolin Varner -.%A Benjamin Lipp -.%A Wanja Zaeske -.%A Lisa Schmidt -.%D 2023 -.%T Rosenpass -.%U https://rosenpass.eu/whitepaper.pdf -.Re -.Sh STANDARDS -This tool is the reference implementation of the Rosenpass protocol, as -specified within the whitepaper referenced above. -.Sh AUTHORS -Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske, -Marei Peischl, Stephan Ajuvo, and Lisa Schmidt. -.Pp -This manual page was written by -.An Clara Engler -.Sh BUGS -The bugs are tracked at -.Lk https://github.com/rosenpass/rosenpass/issues . diff --git a/rosenpass/Cargo.toml b/rosenpass/Cargo.toml index de76f9d..59eb07c 100644 --- a/rosenpass/Cargo.toml +++ b/rosenpass/Cargo.toml @@ -47,6 +47,8 @@ env_logger = { workspace = true } serde = { workspace = true } toml = { workspace = true } clap = { workspace = true } +clap_complete = { workspace = true } +clap_mangen = { workspace = true } mio = { workspace = true } rand = { workspace = true } zerocopy = { workspace = true } diff --git a/rosenpass/build.rs b/rosenpass/build.rs deleted file mode 100644 index 3ea3b4c..0000000 --- a/rosenpass/build.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::bail; -use anyhow::Result; -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::process::Command; - -/// Invokes a troff compiler to compile a manual page -fn render_man(compiler: &str, man: &str) -> Result { - let out = Command::new(compiler).args(["-Tascii", man]).output()?; - if !out.status.success() { - bail!("{} returned an error", compiler); - } - - Ok(String::from_utf8(out.stdout)?) -} - -/// Generates the manual page -fn generate_man() -> String { - // This function is purposely stupid and redundant - - let man = render_man("mandoc", "./doc/rosenpass.1"); - if let Ok(man) = man { - return man; - } - - let man = render_man("groff", "./doc/rosenpass.1"); - if let Ok(man) = man { - return man; - } - - "Cannot render manual page. Please visit https://rosenpass.eu/docs/manuals/\n".into() -} - -fn man() { - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - let man = generate_man(); - let path = out_dir.join("rosenpass.1.ascii"); - - let mut file = File::create(&path).unwrap(); - file.write_all(man.as_bytes()).unwrap(); - - println!("cargo:rustc-env=ROSENPASS_MAN={}", path.display()); -} - -fn main() { - // For now, rerun the build script on every time, as the build script - // is not very expensive right now. - println!("cargo:rerun-if-changed=./"); - man(); -} diff --git a/rosenpass/src/cli.rs b/rosenpass/src/cli.rs index b698c79..8f1a7d5 100644 --- a/rosenpass/src/cli.rs +++ b/rosenpass/src/cli.rs @@ -41,7 +41,7 @@ pub enum BrokerInterface { /// struct holding all CLI arguments for `clap` crate to parse #[derive(Parser, Debug)] -#[command(author, version, about, long_about)] +#[command(author, version, about, long_about, arg_required_else_help = true)] pub struct CliArgs { /// lowest log level to show – log messages at higher levels will be omitted #[arg(long = "log-level", value_name = "LOG_LEVEL", group = "log-level")] @@ -80,7 +80,15 @@ pub struct CliArgs { psk_broker_spawn: bool, #[command(subcommand)] - pub command: CliCommand, + pub command: Option, + + /// Generate man page + #[clap(long, value_name = "out_dir")] + pub generate_manpage: Option, + + /// Generate completion file for a shell + #[clap(long, value_name = "shell")] + pub print_completions: Option, } impl CliArgs { @@ -218,10 +226,6 @@ pub enum CliCommand { /// Validate a configuration Validate { config_files: Vec }, - - /// Show the rosenpass manpage - // TODO make this the default, but only after the manpage has been adjusted once the CLI stabilizes - Man, } impl CliArgs { @@ -236,16 +240,7 @@ impl CliArgs { ) -> anyhow::Result<()> { use CliCommand::*; match &self.command { - Man => { - let man_cmd = std::process::Command::new("man") - .args(["1", "rosenpass"]) - .status(); - - if !(man_cmd.is_ok() && man_cmd.unwrap().success()) { - println!(include_str!(env!("ROSENPASS_MAN"))); - } - } - GenConfig { config_file, force } => { + Some(GenConfig { config_file, force }) => { ensure!( *force || !config_file.exists(), "config file {config_file:?} already exists" @@ -255,7 +250,7 @@ impl CliArgs { } // Deprecated - use gen-keys instead - Keygen { args } => { + Some(Keygen { args }) => { log::warn!("The 'keygen' command is deprecated. Please use the 'gen-keys' command instead."); let mut public_key: Option = None; @@ -288,12 +283,12 @@ impl CliArgs { generate_and_save_keypair(secret_key.unwrap(), public_key.unwrap())?; } - GenKeys { + Some(GenKeys { config_file, public_key, secret_key, force, - } => { + }) => { // figure out where the key file is specified, in the config file or directly as flag? let (pkf, skf) = match (config_file, public_key, secret_key) { (Some(config_file), _, _) => { @@ -337,7 +332,7 @@ impl CliArgs { generate_and_save_keypair(skf, pkf)?; } - ExchangeConfig { config_file } => { + Some(ExchangeConfig { config_file }) => { ensure!( config_file.exists(), "config file '{config_file:?}' does not exist" @@ -351,11 +346,11 @@ impl CliArgs { Self::event_loop(config, broker_interface, test_helpers)?; } - Exchange { + Some(Exchange { first_arg, rest_of_args, config_file, - } => { + }) => { let mut rest_of_args = rest_of_args.clone(); rest_of_args.insert(0, first_arg.clone()); let args = rest_of_args; @@ -372,7 +367,7 @@ impl CliArgs { Self::event_loop(config, broker_interface, test_helpers)?; } - Validate { config_files } => { + Some(Validate { config_files }) => { for file in config_files { match config::Rosenpass::load(file) { Ok(config) => { @@ -386,6 +381,8 @@ impl CliArgs { } } } + + &None => {} // calp print help if no command is given } Ok(()) diff --git a/rosenpass/src/main.rs b/rosenpass/src/main.rs index 375fd9d..93b41f5 100644 --- a/rosenpass/src/main.rs +++ b/rosenpass/src/main.rs @@ -1,13 +1,51 @@ +use clap::CommandFactory; use clap::Parser; +use clap_mangen::roff::{roman, Roff}; use log::error; use rosenpass::cli::CliArgs; use std::process::exit; +fn print_custom_man_section(section: &str, text: &str, file: &mut std::fs::File) { + let mut roff = Roff::default(); + roff.control("SH", [section]); + roff.text([roman(text)]); + let _ = roff.to_writer(file); +} + /// Catches errors, prints them through the logger, then exits pub fn main() { // parse CLI arguments let args = CliArgs::parse(); + if let Some(shell) = args.print_completions { + let mut cmd = CliArgs::command(); + clap_complete::generate(shell, &mut cmd, "rosenpass", &mut std::io::stdout()); + return; + } + + if let Some(out_dir) = args.generate_manpage { + std::fs::create_dir_all(&out_dir).expect("Failed to create man pages directory"); + + let cmd = CliArgs::command(); + let man = clap_mangen::Man::new(cmd.clone()); + let _ = clap_mangen::generate_to(cmd, &out_dir); + + let file_path = out_dir.join("rosenpass.1"); + let mut file = std::fs::File::create(file_path).expect("Failed to create man page file"); + + let _ = man.render_title(&mut file); + let _ = man.render_name_section(&mut file); + let _ = man.render_synopsis_section(&mut file); + let _ = man.render_subcommands_section(&mut file); + let _ = man.render_options_section(&mut file); + print_custom_man_section("EXIT STATUS", EXIT_STATUS_MAN, &mut file); + print_custom_man_section("SEE ALSO", SEE_ALSO_MAN, &mut file); + print_custom_man_section("STANDARDS", STANDARDS_MAN, &mut file); + print_custom_man_section("AUTHORS", AUTHORS_MAN, &mut file); + print_custom_man_section("BUGS", BUGS_MAN, &mut file); + return; + } + { use rosenpass_secret_memory as SM; #[cfg(feature = "experiment_memfd_secret")] @@ -43,3 +81,21 @@ pub fn main() { } } } +static EXIT_STATUS_MAN: &str = r" +The rosenpass utility exits 0 on success, and >0 if an error occurs."; + +static SEE_ALSO_MAN: &str = r" +rp(1), wg(1) + +Karolin Varner, Benjamin Lipp, Wanja Zaeske, and Lisa Schmidt, Rosenpass, https://rosenpass.eu/whitepaper.pdf, 2023."; + +static STANDARDS_MAN: &str = r" +This tool is the reference implementation of the Rosenpass protocol, as +specified within the whitepaper referenced above."; + +static AUTHORS_MAN: &str = r" +Rosenpass was created by Karolin Varner, Benjamin Lipp, Wanja Zaeske, Marei +Peischl, Stephan Ajuvo, and Lisa Schmidt."; + +static BUGS_MAN: &str = r" +The bugs are tracked at https://github.com/rosenpass/rosenpass/issues.";