From e45d6c25ba5b23cd098d2f3455b335577e7b1f83 Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Sat, 14 Sep 2019 11:02:22 -0700 Subject: [PATCH 1/9] Rename binaries (#71) --- src/bin/{krilld.rs => krill.rs} | 0 src/bin/{krill_admin.rs => krillc.rs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/bin/{krilld.rs => krill.rs} (100%) rename src/bin/{krill_admin.rs => krillc.rs} (100%) diff --git a/src/bin/krilld.rs b/src/bin/krill.rs similarity index 100% rename from src/bin/krilld.rs rename to src/bin/krill.rs diff --git a/src/bin/krill_admin.rs b/src/bin/krillc.rs similarity index 100% rename from src/bin/krill_admin.rs rename to src/bin/krillc.rs From 643da3fb9c9be0ff2789a60b1e4844f20b154abf Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Mon, 16 Sep 2019 12:19:02 -0700 Subject: [PATCH 2/9] Introduce KRILL_USE_TA env variable for easier testing without the need for a local config file. --- src/daemon/config.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/daemon/config.rs b/src/daemon/config.rs index 0a2513a0..1f909ad3 100644 --- a/src/daemon/config.rs +++ b/src/daemon/config.rs @@ -2,6 +2,7 @@ use std::fs::File; use std::io; use std::io::Read; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::env; use std::path::PathBuf; use std::str::FromStr; @@ -32,7 +33,12 @@ impl ConfigDefaults { 3000 } fn use_ta() -> bool { - false + + if let Ok(_) = env::var("KRILL_USE_TA") { + true + } else { + false + } } fn use_ssl() -> SslChoice { SslChoice::Test @@ -59,7 +65,6 @@ impl ConfigDefaults { PathBuf::from("./krill.log") } fn auth_token() -> Token { - use std::env; match env::var("KRILL_AUTH_TOKEN") { Ok(token) => Token::from(token), @@ -229,7 +234,7 @@ impl Config { let config_file = matches .value_of("config") - .unwrap_or("./daemon/defaults/krill.conf"); + .unwrap_or("./defaults/krill.conf"); let c = Self::read_config(config_file)?; c.init_logging()?; From b408b6adb8e93b7439ab18830c8b58c213e6a593 Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Mon, 16 Sep 2019 12:34:06 -0700 Subject: [PATCH 3/9] Simplifying CLI, providing better help and allow env variables for defaults. (#71) --- src/cli/client.rs | 9 +- src/cli/options.rs | 1216 ++++++++++++++++++-------------- src/commons/remote/rfc6492.rs | 6 +- src/commons/util/httpclient.rs | 81 ++- 4 files changed, 771 insertions(+), 541 deletions(-) diff --git a/src/cli/client.rs b/src/cli/client.rs index e6a3b3c6..fcf8b21f 100644 --- a/src/cli/client.rs +++ b/src/cli/client.rs @@ -1,10 +1,11 @@ -use std::io; +use std::{env, io}; use serde::de::DeserializeOwned; use serde::Serialize; use rpki::uri; +use crate::cli::options::KRILL_CLI_API_ENV; use crate::cli::options::{CaCommand, Command, Options, PublishersCommand, Rfc8181Command}; use crate::cli::report::{ApiResponse, ReportError}; use crate::commons::api::{ @@ -43,6 +44,11 @@ impl KrillClient { server: options.server, token: options.token, }; + + if options.api { + env::set_var(KRILL_CLI_API_ENV, "1") // this is safe here, because the CLI will exit + } + match options.command { Command::Health => client.health(), Command::CertAuth(cmd) => client.certauth(cmd), @@ -79,7 +85,6 @@ impl KrillClient { httpclient::post_json(&uri, parent, Some(&self.token))?; Ok(ApiResponse::Empty) } - CaCommand::AddChild(handle, req) => { let uri = format!("api/v1/cas/{}/children", handle); let uri = self.resolve_uri(&uri); diff --git a/src/cli/options.rs b/src/cli/options.rs index c8b5c4fe..1540d1b2 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -1,8 +1,10 @@ +use std::convert::TryFrom; +use std::env; use std::io; use std::path::PathBuf; use std::str::{from_utf8_unchecked, FromStr}; -use clap::{App, Arg, SubCommand}; +use clap::{App, Arg, ArgMatches, SubCommand}; use rpki::uri; @@ -12,9 +14,88 @@ use crate::commons::api::{ ChildAuthRequest, Handle, ParentCaContact, ResSetErr, ResourceSet, RouteAuthorizationUpdates, Token, UpdateChildRequest, }; +use crate::commons::remote::id::IdCert; use crate::commons::remote::rfc8183; use crate::commons::util::file; +const KRILL_CLI_SERVER_ARG: &str = "server"; +const KRILL_CLI_SERVER_ENV: &str = "KRILL_CLI_SERVER"; + +const KRILL_CLI_TOKEN_ARG: &str = "admintoken"; +const KRILL_CLI_TOKEN_ENV: &str = "KRILL_CLI_ADMIN_TOKEN"; + +const KRILL_CLI_FORMAT_ARG: &str = "format"; +const KRILL_CLI_FORMAT_ENV: &str = "KRILL_CLI_FORMAT"; + +const KRILL_CLI_API_ARG: &str = "api"; +pub const KRILL_CLI_API_ENV: &str = "KRILL_CLI_API"; + +const KRILL_CLI_MY_CA_ARG: &str = "ca"; +const KRILL_CLI_MY_CA_ENV: &str = "KRILL_CLI_MY_CA"; + +const KRILL_CLI_MY_CA_TOKEN_ARG: &str = "catoken"; +const KRILL_CLI_MY_CA_TOKEN_ENV: &str = "KRILL_CLI_MY_CA_TOKEN"; + +struct GeneralArgs { + server: uri::Https, + token: Token, + format: ReportFormat, + api: bool, +} + +impl GeneralArgs { + fn from_matches(matches: &ArgMatches) -> Result { + let server = { + let mut server = match env::var(KRILL_CLI_SERVER_ENV) { + Ok(server_str) => Some(uri::Https::try_from(server_str)?), + Err(_) => None, + }; + + if let Some(server_str) = matches.value_of(KRILL_CLI_SERVER_ARG) { + server = Some(uri::Https::from_str(server_str)?); + } + + server.ok_or_else(|| { + Error::missing_arg_with_env(KRILL_CLI_SERVER_ARG, KRILL_CLI_SERVER_ENV) + })? + }; + + let token = { + let mut token = env::var(KRILL_CLI_TOKEN_ENV).ok().map(Token::from); + + if let Some(token_str) = matches.value_of(KRILL_CLI_TOKEN_ARG) { + token = Some(Token::from(token_str)); + } + + token.ok_or_else(|| { + Error::missing_arg_with_env(KRILL_CLI_TOKEN_ARG, KRILL_CLI_TOKEN_ENV) + })? + }; + + let format = { + let mut format = match env::var(KRILL_CLI_FORMAT_ENV) { + Ok(fmt_str) => Some(ReportFormat::from_str(&fmt_str)?), + Err(_) => None, + }; + + if let Some(fmt_str) = matches.value_of(KRILL_CLI_FORMAT_ARG) { + format = Some(ReportFormat::from_str(fmt_str)?); + } + + format.unwrap_or_else(|| ReportFormat::Text) + }; + + let api = env::var(KRILL_CLI_API_ENV).is_ok() || matches.is_present(KRILL_CLI_API_ARG); + + Ok(GeneralArgs { + server, + token, + format, + api, + }) + } +} + /// This type holds all the necessary data to connect to a Krill daemon, and /// authenticate, and perform a specific action. Note that this is extracted /// from the bin/krillc.rs, so that we can use this in integration testing @@ -23,10 +104,21 @@ pub struct Options { pub server: uri::Https, pub token: Token, pub format: ReportFormat, + pub api: bool, pub command: Command, } impl Options { + fn make(general: GeneralArgs, command: Command) -> Self { + Options { + server: general.server, + token: general.token, + format: general.format, + api: general.api, + command, + } + } + pub fn format(&self) -> ReportFormat { self.format } @@ -37,572 +129,607 @@ impl Options { server, token: Token::from(token), format, + api: false, command, } } - /// Creates a new Options from command line args (useful for cli) - #[allow(clippy::cognitive_complexity)] // there are just many options - pub fn from_args() -> Result { - let matches = App::new("Krill admin client") - .version("0.1.0") - .arg(Arg::with_name("server") + fn add_general_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("server") .short("s") .long("server") .value_name("URI") - .help("Specify the full URI to the krill server.") - .required(true)) - .arg(Arg::with_name("token") + .help("The full URI to the krill server. Or set env: KRILL_CLI_SERVER") + .required(false), + ) + .arg( + Arg::with_name("admintoken") .short("t") - .long("token") + .long("admintoken") .value_name("token-string") - .help("Specify the value of an admin token.") - .required(true)) - .arg(Arg::with_name("format") + .help("The admin token. Or set env: KRILL_CLI_ADMIN_TOKEN") + .required(false), + ) + .arg( + Arg::with_name("format") .short("f") .long("format") .value_name("type") - .help( - "Specify the report format (none|json|text|xml). If \ - left unspecified the format will match the \ - corresponding server api response type.") + .help("Report format: none|json|text (default) |xml. Or set env: KRILL_CLI_FORMAT") + .required(false), + ) + .arg( + Arg::with_name("api") + .long("api") + .help("Only show the API call and exit. Or set env: KRILL_CLI_API=1") + .required(false), + ) + } + + fn add_my_ca_arg<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("ca") + .value_name("name") + .short("c") + .long("ca") + .help("The name of the CA you wish to control. Or set env: KRILL_CLI_MY_CA") + .required(false), + ) + } + + fn add_child_arg<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("child") + .value_name("name") + .long("child") + .help("The name of the child CA you wish to control.") + .required(false), + ) + } + + fn add_child_resource_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("asn") + .short("a") + .long("asn") + .value_name("AS resources") + .help("The delegated AS resources: e.g. AS1, AS3-4") + .required(false), + ) + .arg( + Arg::with_name("ipv4") + .short("4") + .long("ipv4") + .value_name("IPv4 resources") + .help("The delegated IPv4 resources: e.g. 192.168.0.0/16") + .required(false), + ) + .arg( + Arg::with_name("ipv6") + .short("6") + .long("ipv6") + .value_name("IPv6 resources") + .help("The delegated IPv6 resources: e.g. 2001:db8::/32") + .required(false), + ) + } + + fn add_child_embedded_rfc6492_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("embedded") + .long("embedded") + .help("Add a child that exists in this Krill instance. Note that an id cert can \ + still be added later to allow this child to connect remotely. It's really how the \ + child configures its parent that determines how it will connect") .required(false) - ) - .subcommand(SubCommand::with_name("health") - .about("Perform a health check. Exits with exit code 0 if \ - all is well, exit code 1 in case of any issues") - ) + ) + .arg( + Arg::with_name("rfc8183") + .long("rfc8183") + .help("Add a child using an RFC8183 Child Request XML file. This will return \ + an RFC8183 Parent Response XML (on stdout)") + .value_name("") + .required(false) + ) + } - .subcommand(SubCommand::with_name("cas") - .about("Manage CAs") - .subcommand(SubCommand::with_name("list") - .about("Show current CAs") - ) - .subcommand(SubCommand::with_name("show") - .about("Show CA details") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("handle") - .help("The handle (name) for the CA") - .required(true) - ) - ) - .subcommand(SubCommand::with_name("rfc8183_child_request") - .about("The RFC8183 Child Request for a CA") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("handle") - .help("The handle (name) for the CA") - .required(true) - ) - ) + fn add_parent_embedded_rfc6492_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("parent") + .long("parent") + .short("p") + .value_name("name") + .help("The local by which your ca refers to this parent.") + .required(true), + ).arg( + Arg::with_name("embedded") + .long("embedded") + .help("Add a parent that exists in this Krill instance.") + .required(false), + ) + .arg( + Arg::with_name("rfc8183") + .long("rfc8183") + .help("Add a parent using an RFC8183 Parent Response XML file.") + .value_name("") + .required(false), + ) + } - .subcommand(SubCommand::with_name("keyroll") - .about("Perform a manual key roll for a CA") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("handle") - .help("The handle (name) for the CA") - .required(true) - ) - .subcommand(SubCommand::with_name("init") - .about("Initialise a key roll for all active keys") - ) - .subcommand(SubCommand::with_name("activate") - .about("Activate all new keys now (RFC say to do this >24H after init)") - ) - ) + fn make_cas_list_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let sub = + SubCommand::with_name("list").about("List the current CAs in this Krill instance"); - .subcommand(SubCommand::with_name("add") - .about("Add a new CA") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("child-handle") - .help("The handle (name) for the child CA") - .required(true) - ) - .arg(Arg::with_name("token") - .short("ct") - .long("token") - .value_name("token-string") - .help("The auth token to control the CA") - .required(true) - ) - ) - .subcommand(SubCommand::with_name("update") - .about("Update an existing CA") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("ca handle") - .help("The handle (name) for the CA") - .required(true) - ) - .subcommand(SubCommand::with_name("add-parent") - .about("Add a parent to a CA (only embedded for now)") + let sub = Self::add_general_args(sub); - .arg(Arg::with_name("parent") - .short("p") - .long("parent") - .value_name("parent ca handle") - .help("The local handle (name) for the parent CA") - .required(true) - ) + app.subcommand(sub) + } - .subcommand(SubCommand::with_name("embedded") - .about("Add an embedded parent") - ) + fn make_cas_show_ca_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = + SubCommand::with_name("show").about("Show details of a CA in this Krill instance."); - .subcommand(SubCommand::with_name("rfc6492") - .about("Add an RFC6492 parent") - .arg(Arg::with_name("xml") - .short("x") - .long("xml") - .value_name("response.xml") - .help("The RFC8183 response xml file") - .required(true) - ) - ) + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + app.subcommand(sub) + } - ) - ) + fn make_cas_add_ca_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("add").about("Add a new CA to this Krill instance."); - .subcommand(SubCommand::with_name("children") - .about("Manage children of this CA") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("ca handle") - .help("The handle (name) for the CA") - .required(true) - ) + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); - .subcommand(SubCommand::with_name("add") - .about("Add a child to the embedded CA") + sub = sub.arg( + Arg::with_name("catoken") + .help("The token for your CA. Or set: KRILL_MY_CA_TOKEN") + .value_name("token-string") + .long("catoken") + .required(true), + ); - .arg(Arg::with_name("asn") - .short("a") - .long("asn") - .value_name("AS resources") - .help("The delegated AS resources: e.g. AS1, AS3-4") - .required(false) - ) - .arg(Arg::with_name("ipv4") - .short("4") - .long("ipv4") - .value_name("IPv4 resources") - .help("The delegated IPv4 resources: e.g. 192.168.0.0/16") - .required(false) - ) - .arg(Arg::with_name("ipv6") - .short("6") - .long("ipv6") - .value_name("IPv6 resources") - .help("The delegated IPv6 resources: e.g. 2001:db8::/32") - .required(false) - ) + app.subcommand(sub) + } - .subcommand(SubCommand::with_name("embedded") - .about("Add an embedded child") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("child-handle") - .help("The handle of the child") - .required(true) - ) - ) + fn make_cas_children_add_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("add").about("Add a child to a CA in Krill."); - .subcommand(SubCommand::with_name("rfc6492") - .about("Add an RFC 6492 child") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("child-handle") - .help("Override the handle in the XML") - .required(false) - ) - .arg(Arg::with_name("xml") - .short("x") - .long("xml") - .value_name("FILE") - .help("RFC 8183 Child Request XML") - .required(true) - ) - ) - ) - .subcommand(SubCommand::with_name("update") - .about("Update details for a child") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("child-handle") - .help("Override the handle in the XML") - .required(false) - ) - .arg(Arg::with_name("xml") - .short("x") - .long("xml") - .value_name("FILE") - .help("Update child certificate from RFC 8183 Child Request XML") - .required(false) - ) - .arg(Arg::with_name("asn") - .short("a") - .long("asn") - .value_name("AS resources") - .help("Update the delegated AS resources: e.g. AS1, AS3-4") - .required(false) - ) - .arg(Arg::with_name("ipv4") - .short("4") - .long("ipv4") - .value_name("IPv4 resources") - .help("Update the delegated IPv4 resources: e.g. 192.168.0.0/16") - .required(false) - ) - .arg(Arg::with_name("ipv6") - .short("6") - .long("ipv6") - .value_name("IPv6 resources") - .help("Update the delegated IPv6 resources: e.g. 2001:db8::/32") - .required(false) - ) - .arg(Arg::with_name("force") - .short("f") - .long("force") - .takes_value(false) - .help("Force resource shrink now.") - .required(false) - ) - ) - ) + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + sub = Self::add_child_arg(sub); + sub = Self::add_child_resource_args(sub); + sub = Self::add_child_embedded_rfc6492_args(sub); - .subcommand(SubCommand::with_name("roas") - .about("Manage route authorizations by this CA") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("ca handle") - .help("The handle (name) for the CA") - .required(true) - ) + app.subcommand(sub) + } - .subcommand(SubCommand::with_name("list") - .about("List current authorisations and ROA objects") - ) - .subcommand(SubCommand::with_name("update") - .about("Update the authorizations for this CA") - .arg(Arg::with_name("delta") - .short("d") - .long("delta") - .value_name("FILE") - .help("path to file containing deltas") - .required(true) - ) - ) - ) - ) + fn make_cas_children_update_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = + SubCommand::with_name("update").about("Update an existing child of a CA in Krill."); - .subcommand(SubCommand::with_name("publishers") - .about("Manage publishers") - .subcommand(SubCommand::with_name("list") - .about("List all current publishers") - ) - .subcommand(SubCommand::with_name("add") - .about("Add an API using publisher.") - .arg(Arg::with_name("token") - .short("t") - .long("token") - .value_name("text") - .help("Specify a token string.") - .required(true) - ) - .arg(Arg::with_name("uri") - .short("u") - .long("uri") - .value_name("rsync uri") - .help("Rsync base uri for publisher. Must be covered by server, and not overlap with existing publishers.") - .required(true) - ) - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("name") - .help("A unique name for this publisher. Must be a-Z0-9 without spaces.") - .required(true) - ) - ) - .subcommand(SubCommand::with_name("details") - .about("Show details for a publisher.") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("publisher handle") - .help("The publisher handle from RFC8181") - .required(true) - ) - ) - .subcommand(SubCommand::with_name("deactivate") - .about("Removes a known publisher") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("publisher handle") - .help("The publisher handle from RFC8181") - .required(true) - ) - ) - ) - .subcommand(SubCommand::with_name("rfc8181") - .about("Manage RFC8181 clients") - .subcommand(SubCommand::with_name("list") - .about("List all current clients with details") - ) - .subcommand(SubCommand::with_name("add") - .about("Add RFC8181 client (assumes handle is in the XML)") - .arg(Arg::with_name("xml") - .short("x") - .long("xml") - .value_name("FILE") - .help("Specify a file containing an RFC8183 \ - publisher request. (See: https://tools.ietf.org/html/rfc8183#section-5.2.3)") - .required(true) - ) - ) - .subcommand(SubCommand::with_name("repo-res") - .about("Show the RFC8181 repository response xml") - .arg(Arg::with_name("handle") - .short("h") - .long("handle") - .value_name("publisher handle") - .help("The publisher handle from RFC8181") - .required(true) - ) - ) - ) - .get_matches(); + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + sub = Self::add_child_arg(sub); + sub = Self::add_child_resource_args(sub); + sub = sub.arg( + Arg::with_name("idcert") + .long("idcert") + .help("The child's updated ID certificate") + .value_name("DER encoded certificate") + .required(false), + ); - let mut command = Command::NotSet; + app.subcommand(sub) + } - if let Some(_m) = matches.subcommand_matches("health") { - command = Command::Health; + fn make_cas_children_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("children").about("Manage children for a CA in Krill."); + + sub = Self::make_cas_children_add_sc(sub); + sub = Self::make_cas_children_update_sc(sub); + + app.subcommand(sub) + } + + fn make_cas_parents_myid_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("myid").about("Show this CA's RFC8183 Request XML"); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + + app.subcommand(sub) + } + + fn make_cas_parents_add_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("add").about("Add a parent to this CA."); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + sub = Self::add_parent_embedded_rfc6492_args(sub); + + app.subcommand(sub) + } + + fn make_cas_parents_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("parents").about("Manage parents for a CA in Krill."); + + sub = Self::make_cas_parents_myid_sc(sub); + sub = Self::make_cas_parents_add_sc(sub); + + app.subcommand(sub) + } + + fn make_cas_keyroll_init_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = + SubCommand::with_name("init").about("Initialise roll for all keys held by this CA"); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + + app.subcommand(sub) + } + + fn make_cas_keyroll_activate_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = + SubCommand::with_name("activate").about("Finish roll for all keys held by this CA"); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + + app.subcommand(sub) + } + + fn make_cas_keyroll_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("keyroll").about("Perform a manual key-roll in Krill."); + + sub = Self::make_cas_keyroll_init_sc(sub); + sub = Self::make_cas_keyroll_activate_sc(sub); + + app.subcommand(sub) + } + + fn make_cas_routes_list_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("list").about("Show current authorizations."); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + + app.subcommand(sub) + } + + fn make_cas_routes_update_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("update").about("Update authorizations."); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + + sub = sub.arg( + Arg::with_name("delta") + .long("delta") + .help(concat!( + "Provide a delta file using the following format:\n", + "# Some comment\n", + " # Indented comment\n", + "\n", // empty line + "A: 192.168.0.0/16 => 64496 # inline comment\n", + "A: 192.168.1.0/24 => 64496\n", + "R: 192.168.3.0/24 => 64496\n", + )) + .value_name("") + .required(true), + ); + + app.subcommand(sub) + } + + fn make_cas_routes_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("roas").about("Manage ROAs for your CA."); + + sub = Self::make_cas_routes_list_sc(sub); + sub = Self::make_cas_routes_update_sc(sub); + + app.subcommand(sub) + } + + fn make_cas_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("cas").about("Manage Certification Authorities"); + + sub = Self::make_cas_list_sc(sub); + sub = Self::make_cas_show_ca_sc(sub); + sub = Self::make_cas_add_ca_sc(sub); + sub = Self::make_cas_children_sc(sub); + sub = Self::make_cas_parents_sc(sub); + sub = Self::make_cas_keyroll_sc(sub); + sub = Self::make_cas_routes_sc(sub); + + app.subcommand(sub) + } + + fn make_matches<'a>() -> ArgMatches<'a> { + let mut app = App::new("Krill Client").version("0.1.1"); + + app = Self::make_cas_sc(app); + + app.get_matches() + } + + //---------------------- Parsing + + fn parse_my_ca(matches: &ArgMatches) -> Result { + let my_ca = { + let mut my_ca = env::var(KRILL_CLI_MY_CA_ENV).ok().map(Handle::from); + + if let Some(my_ca_str) = matches.value_of(KRILL_CLI_MY_CA_ARG) { + my_ca = Some(Handle::from(my_ca_str)); + } + + my_ca.ok_or_else(|| { + Error::missing_arg_with_env(KRILL_CLI_MY_CA_ARG, KRILL_CLI_MY_CA_ENV) + })? + }; + + Ok(my_ca) + } + + fn parse_my_ca_token(matches: &ArgMatches) -> Result { + let my_ca_token = { + let mut my_ca_token = env::var(KRILL_CLI_MY_CA_TOKEN_ENV).ok().map(Token::from); + + if let Some(my_ca_token_str) = matches.value_of(KRILL_CLI_MY_CA_TOKEN_ARG) { + my_ca_token = Some(Token::from(my_ca_token_str)); + } + + my_ca_token.ok_or_else(|| { + Error::missing_arg_with_env(KRILL_CLI_MY_CA_TOKEN_ARG, KRILL_CLI_MY_CA_TOKEN_ENV) + })? + }; + + Ok(my_ca_token) + } + + fn parse_resource_args(matches: &ArgMatches) -> Result, Error> { + let asn = matches.value_of("asn"); + let v4 = matches.value_of("ipv4"); + let v6 = matches.value_of("ipv6"); + + if asn.is_some() || v4.is_some() || v6.is_some() { + let asn = asn.unwrap_or_else(|| ""); + let v4 = v4.unwrap_or_else(|| ""); + let v6 = v6.unwrap_or_else(|| ""); + + Ok(Some(ResourceSet::from_strs(asn, v4, v6)?)) + } else { + Ok(None) } + } + fn parse_matches_cas_list(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let command = Command::CertAuth(CaCommand::List); + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_add(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + let token = Self::parse_my_ca_token(matches)?; + + let init = CertAuthInit::new(my_ca, token, CertAuthPubMode::Embedded); + + let command = Command::CertAuth(CaCommand::Init(init)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_show(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let command = Command::CertAuth(CaCommand::Show(my_ca)); + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_children_add(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let child = matches.value_of("child").map(Handle::from).unwrap(); + let resources = + Self::parse_resource_args(matches)?.ok_or_else(|| Error::MissingResources)?; + + let auth_request = { + if matches.is_present("embedded") { + ChildAuthRequest::Embedded + } else if let Some(path) = matches.value_of("rfc8183") { + let xml = PathBuf::from(path); + let bytes = file::read(&xml)?; + let cr = rfc8183::ChildRequest::validate(bytes.as_ref())?; + ChildAuthRequest::Rfc8183(cr) + } else { + return Err(Error::MissingChildAuth); + } + }; + + let child_request = AddChildRequest::new(child, resources, auth_request); + + let command = Command::CertAuth(CaCommand::AddChild(my_ca, child_request)); + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_children_update(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let child = matches.value_of("child").map(Handle::from).unwrap(); + + let id_cert = { + if let Some(path) = matches.value_of("idcert") { + let bytes = file::read(&PathBuf::from(path))?; + let id_cert = IdCert::decode(bytes).map_err(|_| Error::InvalidChildIdCert)?; + Some(id_cert) + } else { + None + } + }; + let resources = Self::parse_resource_args(matches)?; + + let update = UpdateChildRequest::force(id_cert, resources); + + let command = Command::CertAuth(CaCommand::UpdateChild(my_ca, child, update)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_children(matches: &ArgMatches) -> Result { + if let Some(m) = matches.subcommand_matches("add") { + Self::parse_matches_cas_children_add(m) + } else if let Some(m) = matches.subcommand_matches("update") { + Self::parse_matches_cas_children_update(m) + } else { + Err(Error::UnrecognisedSubCommand) + } + } + + fn parse_matches_cas_parents_myid(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let command = Command::CertAuth(CaCommand::ChildRequest(my_ca)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_parents_add(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let parent = matches.value_of("parent").map(Handle::from).unwrap(); + + let parent_req = { + if matches.is_present("embedded") { + AddParentRequest::new(parent, ParentCaContact::Embedded) + } else if let Some(path) = matches.value_of("rfc8183") { + let xml = PathBuf::from(path); + let bytes = file::read(&xml)?; + let res = rfc8183::ParentResponse::validate(bytes.as_ref())?; + + AddParentRequest::new(parent, ParentCaContact::for_rfc6492(res)) + } else { + return Err(Error::MissingChildAuth); + } + }; + + let command = Command::CertAuth(CaCommand::AddParent(my_ca, parent_req)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_parents(matches: &ArgMatches) -> Result { + if let Some(m) = matches.subcommand_matches("myid") { + Self::parse_matches_cas_parents_myid(m) + } else if let Some(m) = matches.subcommand_matches("add") { + Self::parse_matches_cas_parents_add(m) + } else { + Err(Error::UnrecognisedSubCommand) + } + } + + fn parse_matches_cas_keyroll_init(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let command = Command::CertAuth(CaCommand::KeyRollInit(my_ca)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_keyroll_activate(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let command = Command::CertAuth(CaCommand::KeyRollActivate(my_ca)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_keyroll(matches: &ArgMatches) -> Result { + if let Some(m) = matches.subcommand_matches("init") { + Self::parse_matches_cas_keyroll_init(m) + } else if let Some(m) = matches.subcommand_matches("activate") { + Self::parse_matches_cas_keyroll_activate(m) + } else { + Err(Error::UnrecognisedSubCommand) + } + } + + fn parse_matches_cas_routes_list(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let command = Command::CertAuth(CaCommand::RouteAuthorizationsList(my_ca)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_routes_update(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let updates = { + let path = matches.value_of("delta").map(PathBuf::from).unwrap(); + let bytes = file::read(&path)?; + let updates_str = unsafe { from_utf8_unchecked(&bytes) }; + RouteAuthorizationUpdates::from_str(updates_str)? + }; + + let command = Command::CertAuth(CaCommand::RouteAuthorizationsUpdate(my_ca, updates)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_routes(matches: &ArgMatches) -> Result { + if let Some(m) = matches.subcommand_matches("list") { + Self::parse_matches_cas_routes_list(m) + } else if let Some(m) = matches.subcommand_matches("update") { + Self::parse_matches_cas_routes_update(m) + } else { + Err(Error::UnrecognisedSubCommand) + } + } + + fn parse_matches_cas(matches: &ArgMatches) -> Result { + if let Some(m) = matches.subcommand_matches("list") { + Self::parse_matches_cas_list(m) + } else if let Some(m) = matches.subcommand_matches("add") { + Self::parse_matches_cas_add(m) + } else if let Some(m) = matches.subcommand_matches("show") { + Self::parse_matches_cas_show(m) + } else if let Some(m) = matches.subcommand_matches("children") { + Self::parse_matches_cas_children(m) + } else if let Some(m) = matches.subcommand_matches("parents") { + Self::parse_matches_cas_parents(m) + } else if let Some(m) = matches.subcommand_matches("keyroll") { + Self::parse_matches_cas_keyroll(m) + } else if let Some(m) = matches.subcommand_matches("roas") { + Self::parse_matches_cas_routes(m) + } else { + Err(Error::UnrecognisedSubCommand) + } + } + + fn parse_matches(matches: ArgMatches) -> Result { if let Some(m) = matches.subcommand_matches("cas") { - if let Some(m) = m.subcommand_matches("add") { - let handle_str = m.value_of("handle").unwrap(); - - // TODO: Issue #83 Allow '\' and '/' as well as per RFC 8183 - if !handle_str - .bytes() - .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') - { - return Err(Error::InvalidHandle); - } - - let handle = Handle::from(handle_str); - let token = Token::from(m.value_of("token").unwrap()); - let pub_mode = CertAuthPubMode::Embedded; - - let init = CertAuthInit::new(handle, token, pub_mode); - command = Command::CertAuth(CaCommand::Init(init)); - } - if let Some(m) = m.subcommand_matches("rfc8183_child_request") { - let handle = Handle::from(m.value_of("handle").unwrap()); - command = Command::CertAuth(CaCommand::ChildRequest(handle)); - } - if let Some(_m) = m.subcommand_matches("list") { - command = Command::CertAuth(CaCommand::List); - } - if let Some(m) = m.subcommand_matches("keyroll") { - let handle = Handle::from(m.value_of("handle").unwrap()); - if let Some(_m) = m.subcommand_matches("init") { - command = Command::CertAuth(CaCommand::KeyRollInit(handle)); - } else if let Some(_m) = m.subcommand_matches("activate") { - command = Command::CertAuth(CaCommand::KeyRollActivate(handle)); - } - } - - if let Some(m) = m.subcommand_matches("show") { - let handle = Handle::from(m.value_of("handle").unwrap()); - command = Command::CertAuth(CaCommand::Show(handle)); - } - - if let Some(m) = m.subcommand_matches("children") { - let ca = Handle::from(m.value_of("handle").unwrap()); - - if let Some(m) = m.subcommand_matches("add") { - let asn = m.value_of("asn").unwrap_or(""); - let ipv4 = m.value_of("ipv4").unwrap_or(""); - let ipv6 = m.value_of("ipv6").unwrap_or(""); - - if let Some(m) = m.subcommand_matches("embedded") { - let handle = Handle::from(m.value_of("handle").unwrap()); - let res = ResourceSet::from_strs(asn, ipv4, ipv6).unwrap(); - let auth = ChildAuthRequest::Embedded; - - let req = AddChildRequest::new(handle, res, auth); - command = Command::CertAuth(CaCommand::AddChild(ca, req)) - } else if let Some(m) = m.subcommand_matches("rfc6492") { - let xml_path = m.value_of("xml").unwrap(); - let xml = PathBuf::from(xml_path); - let bytes = file::read(&xml)?; - let cr = rfc8183::ChildRequest::validate(bytes.as_ref())?; - - let handle = { - if let Some(handle) = m.value_of("handle") { - Handle::from(handle) - } else { - cr.child_handle().clone() - } - }; - - let res = ResourceSet::from_strs(asn, ipv4, ipv6)?; - - let auth = ChildAuthRequest::Rfc8183(cr); - - let req = AddChildRequest::new(handle, res, auth); - command = Command::CertAuth(CaCommand::AddChild(ca, req)) - } - } else if let Some(m) = m.subcommand_matches("update") { - let handle = Handle::from(m.value_of("handle").unwrap()); - let cert = match m.value_of("xml") { - Some(xml_path) => { - let xml = PathBuf::from(xml_path); - let bytes = file::read(&xml)?; - let cr = rfc8183::ChildRequest::validate(bytes.as_ref())?; - let (_, _, cert) = cr.unwrap(); - Some(cert) - } - None => None, - }; - - let asn = m.value_of("asn").unwrap_or(""); - let ipv4 = m.value_of("ipv4").unwrap_or(""); - let ipv6 = m.value_of("ipv6").unwrap_or(""); - let resources = ResourceSet::from_strs(asn, ipv4, ipv6)?; - - let resources = if resources.is_empty() { - None - } else { - Some(resources) - }; - - let req = if m.is_present("force") { - UpdateChildRequest::force(cert, resources) - } else { - UpdateChildRequest::graceful(cert, resources) - }; - - command = Command::CertAuth(CaCommand::UpdateChild(ca, handle, req)) - } - } - - if let Some(m) = m.subcommand_matches("update") { - let handle = Handle::from(m.value_of("handle").unwrap()); - - if let Some(m) = m.subcommand_matches("add-parent") { - let parent = Handle::from(m.value_of("parent").unwrap()); - - if let Some(_m) = m.subcommand_matches("embedded") { - let contact = ParentCaContact::Embedded; - let req = AddParentRequest::new(parent, contact); - - command = Command::CertAuth(CaCommand::AddParent(handle, req)) - } else if let Some(m) = m.subcommand_matches("rfc6492") { - let xml_path = m.value_of("xml").unwrap(); - let xml = PathBuf::from(xml_path); - let bytes = file::read(&xml)?; - let pr = rfc8183::ParentResponse::validate(bytes.as_ref())?; - - let contact = ParentCaContact::Rfc6492(pr); - let req = AddParentRequest::new(parent, contact); - - command = Command::CertAuth(CaCommand::AddParent(handle, req)); - } - } - } - - if let Some(m) = m.subcommand_matches("roas") { - let ca = Handle::from(m.value_of("handle").unwrap()); - - if let Some(_m) = m.subcommand_matches("list") { - command = Command::CertAuth(CaCommand::RouteAuthorizationsList(ca)); - } else if let Some(m) = m.subcommand_matches("update") { - let file = PathBuf::from(m.value_of("delta").unwrap()); - let bytes = file::read(&file)?; - let str = unsafe { from_utf8_unchecked(bytes.as_ref()) }; - let delta = RouteAuthorizationUpdates::from_str(str)?; - command = Command::CertAuth(CaCommand::RouteAuthorizationsUpdate(ca, delta)); - } - } + Self::parse_matches_cas(m) + } else { + Err(Error::UnrecognisedSubCommand) } + } - if let Some(m) = matches.subcommand_matches("publishers") { - if let Some(_m) = m.subcommand_matches("list") { - command = Command::Publishers(PublishersCommand::List) - } - if let Some(m) = m.subcommand_matches("add") { - let handle = Handle::from(m.value_of("handle").unwrap()); - let base_uri = uri::Rsync::from_str(m.value_of("uri").unwrap())?; - let token = Token::from(m.value_of("token").unwrap()); - - let add = AddPublisher { - handle, - base_uri, - token, - }; - command = Command::Publishers(PublishersCommand::Add(add)); - } - if let Some(m) = m.subcommand_matches("details") { - let handle = m.value_of("handle").unwrap(); - let details = PublishersCommand::Details(handle.to_string()); - command = Command::Publishers(details); - } - if let Some(m) = m.subcommand_matches("deactivate") { - let handle = m.value_of("handle").unwrap().to_string(); - command = Command::Publishers(PublishersCommand::Deactivate(handle)) - } - } - - if let Some(m) = matches.subcommand_matches("rfc8181") { - if let Some(_m) = m.subcommand_matches("list") { - command = Command::Rfc8181(Rfc8181Command::List) - } - if let Some(m) = m.subcommand_matches("add") { - let xml_path = m.value_of("xml").unwrap(); - let xml = PathBuf::from(xml_path); - - command = Command::Rfc8181(Rfc8181Command::Add(AddRfc8181Client { xml })) - } - if let Some(m) = m.subcommand_matches("repo-res") { - let handle = Handle::from(m.value_of("handle").unwrap()); - command = Command::Rfc8181(Rfc8181Command::RepoRes(handle)); - } - } - - let server = matches.value_of("server").unwrap(); // required - let server = uri::Https::from_str(server).map_err(Error::UriError)?; - - let token = Token::from(matches.value_of("token").unwrap()); - - let mut format = ReportFormat::Default; - if let Some(fmt) = matches.value_of("format") { - format = ReportFormat::from_str(fmt)?; - } - - Ok(Options { - server, - token, - format, - command, - }) + pub fn from_args() -> Result { + let matches = Self::make_matches(); + Self::parse_matches(matches) } } @@ -628,7 +755,7 @@ pub enum CaCommand { // Add a parent to this CA AddParent(Handle, AddParentRequest), - // Add a child to this CA + // Children AddChild(Handle, AddChildRequest), UpdateChild(Handle, Handle, UpdateChildRequest), @@ -702,6 +829,31 @@ pub enum Error { #[display(fmt = "The publisher handle may only contain -_A-Za-z0-9, (\\ /) see issue #83")] InvalidHandle, + + #[display( + fmt = "Missing argument: --{}, alternatively you may use env var: {}", + _0, + _1 + )] + MissingArgWithEnv(String, String), + + #[display(fmt = "You must specify resources when adding a CA (--asn, --ipv4, --ipv6)")] + MissingResources, + + #[display(fmt = "You must specify either --embedded or --rfc8183 when adding a child")] + MissingChildAuth, + + #[display(fmt = "Invalid ID cert for child.")] + InvalidChildIdCert, + + #[display(fmt = "Unrecognised sub-command. Use 'help'.")] + UnrecognisedSubCommand, +} + +impl Error { + fn missing_arg_with_env(arg: &str, env_var: &str) -> Self { + Error::MissingArgWithEnv(arg.to_string(), env_var.to_string()) + } } impl From for Error { diff --git a/src/commons/remote/rfc6492.rs b/src/commons/remote/rfc6492.rs index b9ad4237..024641b7 100644 --- a/src/commons/remote/rfc6492.rs +++ b/src/commons/remote/rfc6492.rs @@ -1040,10 +1040,8 @@ mod tests { let ncc_id_cer = include_bytes!("../../../test-resources/remote/ncc-id.der"); let ncc_id_cer = IdCert::decode(ncc_id_cer.as_ref()).unwrap(); - msg.validate_at( - &ncc_id_cer, - Time::utc(2019, 9, 14, 0, 0, 0) - ).unwrap(); + msg.validate_at(&ncc_id_cer, Time::utc(2019, 9, 14, 0, 0, 0)) + .unwrap(); } #[test] diff --git a/src/commons/util/httpclient.rs b/src/commons/util/httpclient.rs index 74f54453..25ba9339 100644 --- a/src/commons/util/httpclient.rs +++ b/src/commons/util/httpclient.rs @@ -1,6 +1,7 @@ //! Some helper functions for HTTP calls use std::io::Read; use std::time::Duration; +use std::{env, fmt}; use bytes::Bytes; use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue, CONTENT_TYPE, USER_AGENT}; @@ -8,14 +9,74 @@ use reqwest::{Client, Response, StatusCode}; use serde::de::DeserializeOwned; use serde::Serialize; +use crate::cli::options::KRILL_CLI_API_ENV; use crate::commons::api::{ErrorResponse, Token}; const JSON_CONTENT: &str = "application/json"; +fn report_get(uri: &str, content_type: Option<&str>, token: Option<&Token>) { + if env::var(KRILL_CLI_API_ENV).is_ok() { + println!("GET: {}", uri); + if let Some(content_type) = content_type { + println!("Headers: content-type: {}", content_type); + } + if let Some(token) = token { + println!("Headers: Bearer: {}", token); + } + std::process::exit(0); + } +} + +enum PostBody<'a> { + String(&'a String), + Bytes(&'a Vec), +} + +impl<'a> fmt::Display for PostBody<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PostBody::String(string) => write!(f, "{}", string), + PostBody::Bytes(bytes) => { + let base64 = base64::encode(bytes); + write!(f, " {}", base64) + } + } + } +} + +fn report_post(uri: &str, content_type: Option<&str>, token: Option<&Token>, body: PostBody) { + if env::var(KRILL_CLI_API_ENV).is_ok() { + println!("POST: {}", uri); + if let Some(content_type) = content_type { + println!("Headers: content-type: {}", content_type); + } + if let Some(token) = token { + println!("Headers: Bearer: {}", token); + } + println!("Body: {}", body); + std::process::exit(0); + } +} + +fn report_delete(uri: &str, content_type: Option<&str>, token: Option<&Token>) { + if env::var(KRILL_CLI_API_ENV).is_ok() { + println!("DELETE: {}", uri); + if let Some(content_type) = content_type { + println!("Headers: content-type: {}", content_type); + } + if let Some(token) = token { + println!("Headers: Bearer: {}", token); + } + std::process::exit(0); + } +} + /// Performs a GET request that expects a json response that can be /// deserialized into the an owned value of the expected type. Returns an error /// if nothing is returned. pub fn get_json(uri: &str, token: Option<&Token>) -> Result { + report_get(uri, None, token); + let headers = headers(Some(JSON_CONTENT), token)?; let res = client(uri)?.get(uri).headers(headers).send()?; process_json_response(res) @@ -24,6 +85,8 @@ pub fn get_json(uri: &str, token: Option<&Token>) -> Result /// Performs a get request and expects a response that can be turned /// into a string (in particular, not a binary response). pub fn get_text(uri: &str, content_type: &str, token: Option<&Token>) -> Result { + report_get(uri, Some(content_type), token); + let headers = headers(Some(content_type), token)?; let res = client(uri)?.get(uri).headers(headers).send()?; match opt_text_response(res)? { @@ -35,6 +98,8 @@ pub fn get_text(uri: &str, content_type: &str, token: Option<&Token>) -> Result< /// Checks that there is a 200 OK response at the given URI. Discards the /// response body. pub fn get_ok(uri: &str, token: Option<&Token>) -> Result<(), Error> { + report_get(uri, None, token); + let headers = headers(None, token)?; let res = client(uri)?.get(uri).headers(headers).send()?; opt_text_response(res)?; // Will return nice errors with possible body. @@ -44,8 +109,11 @@ pub fn get_ok(uri: &str, token: Option<&Token>) -> Result<(), Error> { /// Performs a POST of data that can be serialized into json, and expects /// a 200 OK response, without a body. pub fn post_json(uri: &str, data: impl Serialize, token: Option<&Token>) -> Result<(), Error> { - let headers = headers(Some(JSON_CONTENT), token)?; let body = serde_json::to_string(&data)?; + report_post(uri, None, token, PostBody::String(&body)); + + let headers = headers(Some(JSON_CONTENT), token)?; + let res = client(uri)?.post(uri).headers(headers).body(body).send()?; if let Some(res) = opt_text_response(res)? { Err(Error::UnexpectedResponse(res)) @@ -62,14 +130,18 @@ pub fn post_json_with_response( data: impl Serialize, token: Option<&Token>, ) -> Result { - let headers = headers(Some(JSON_CONTENT), token)?; let body = serde_json::to_string(&data)?; + report_post(uri, None, token, PostBody::String(&body)); + + let headers = headers(Some(JSON_CONTENT), token)?; let res = client(uri)?.post(uri).headers(headers).body(body).send()?; process_json_response(res) } /// Performs a POST with no data to the given URI and expects and empty 200 OK response. pub fn post_empty(uri: &str, token: Option<&Token>) -> Result<(), Error> { + report_post(uri, None, token, PostBody::String(&"".to_string())); + let headers = headers(Some(JSON_CONTENT), token)?; let res = client(uri)?.post(uri).headers(headers).send()?; if let Some(res) = opt_text_response(res)? { @@ -84,9 +156,10 @@ pub fn post_empty(uri: &str, token: Option<&Token>) -> Result<(), Error> { /// Note: Bytes may be empty if the post was successful, but the response was /// empty. pub fn post_binary(uri: &str, data: &Bytes, content_type: &str) -> Result { - let headers = headers(Some(content_type), None)?; let body = data.to_vec(); + report_post(uri, None, None, PostBody::Bytes(&body)); + let headers = headers(Some(content_type), None)?; let mut res = client(uri)?.post(uri).headers(headers).body(body).send()?; match res.status() { @@ -111,6 +184,8 @@ pub fn post_binary(uri: &str, data: &Bytes, content_type: &str) -> Result) -> Result<(), Error> { + report_delete(uri, None, token); + let headers = headers(None, token)?; client(uri)?.delete(uri).headers(headers).send()?; Ok(()) From fb24a844d1405d25484de657354538534f610c74 Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Tue, 17 Sep 2019 08:01:06 -0700 Subject: [PATCH 4/9] The --child argument must be required. --- src/cli/options.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/options.rs b/src/cli/options.rs index 1540d1b2..ddc2949c 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -184,7 +184,7 @@ impl Options { .value_name("name") .long("child") .help("The name of the child CA you wish to control.") - .required(false), + .required(true), ) } From ece9d6826ede003452b09a63fd1d092a77550d06 Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Thu, 26 Sep 2019 06:17:10 +0200 Subject: [PATCH 5/9] Add 'extra-debug' feature. --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index bbf1e186..979b28c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,7 @@ xml-rs = "0.8.0" [build-dependencies] ignore = "^0.4" + +[features] +default = [] +extra-debug = [ "rpki/extra-debug" ] \ No newline at end of file From c9caad473d349c4ab7a285764273d0143c4c84d6 Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Thu, 26 Sep 2019 06:18:13 +0200 Subject: [PATCH 6/9] Simplify if statement. --- src/daemon/config.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/daemon/config.rs b/src/daemon/config.rs index 1f909ad3..499d5f54 100644 --- a/src/daemon/config.rs +++ b/src/daemon/config.rs @@ -33,12 +33,7 @@ impl ConfigDefaults { 3000 } fn use_ta() -> bool { - - if let Ok(_) = env::var("KRILL_USE_TA") { - true - } else { - false - } + env::var("KRILL_USE_TA").is_ok() } fn use_ssl() -> SslChoice { SslChoice::Test From 5c741d0ca1a4f9842e072dcdb98d5632e348d5fd Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Thu, 26 Sep 2019 06:18:59 +0200 Subject: [PATCH 7/9] Updating interop-test, still failing - may be at RIPE NCC. (Issue: #3) --- src/commons/remote/rfc6492.rs | 2 +- test-resources/remote/ncc-response.ber | Bin 32341 -> 32341 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/remote/rfc6492.rs b/src/commons/remote/rfc6492.rs index 024641b7..d306ec19 100644 --- a/src/commons/remote/rfc6492.rs +++ b/src/commons/remote/rfc6492.rs @@ -1040,7 +1040,7 @@ mod tests { let ncc_id_cer = include_bytes!("../../../test-resources/remote/ncc-id.der"); let ncc_id_cer = IdCert::decode(ncc_id_cer.as_ref()).unwrap(); - msg.validate_at(&ncc_id_cer, Time::utc(2019, 9, 14, 0, 0, 0)) + msg.validate_at(&ncc_id_cer, Time::utc(2019, 9, 27, 0, 0, 0)) .unwrap(); } diff --git a/test-resources/remote/ncc-response.ber b/test-resources/remote/ncc-response.ber index bf18013a1a205dc99eb2c1b25a4a2630960bb512..92ab4d02269a0ff5009a150a98301b5232cec30e 100644 GIT binary patch literal 32341 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 literal 32341 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 From 176362ecd96a0b847e3b4317e059bcee57085a9b Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Thu, 26 Sep 2019 06:20:42 +0200 Subject: [PATCH 8/9] Fix error message and code when adding duplicate child, or parent. (fixes: #75) --- src/commons/api/mod.rs | 7 ++++++- src/daemon/endpoints.rs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commons/api/mod.rs b/src/commons/api/mod.rs index 8c16c7c5..5eafa868 100644 --- a/src/commons/api/mod.rs +++ b/src/commons/api/mod.rs @@ -296,6 +296,9 @@ pub enum ErrorCode { #[display(fmt = "Child cannot have resources not held by parent")] ChildOverclaims, + #[display(fmt = "Parent with name exists")] + DuplicateParent, + // 3000s General server errors #[display(fmt = "Cannot update internal state, issue with work_dir?")] Persistence, @@ -348,6 +351,7 @@ impl From for ErrorCode { 2301 => ErrorCode::DuplicateChild, 2302 => ErrorCode::ChildNeedsResources, 2303 => ErrorCode::ChildOverclaims, + 2304 => ErrorCode::DuplicateParent, // 3000s -> Server issues, bugs or operational issues 3001 => ErrorCode::Persistence, @@ -391,6 +395,7 @@ impl Into for ErrorCode { ErrorCode::DuplicateChild => 2301, ErrorCode::ChildNeedsResources => 2302, ErrorCode::ChildOverclaims => 2303, + ErrorCode::DuplicateParent => 2304, // server errors ErrorCode::Persistence => 3001, @@ -438,7 +443,7 @@ mod tests { test_code(n) } - for n in 2301..2304 { + for n in 2301..2305 { test_code(n) } diff --git a/src/daemon/endpoints.rs b/src/daemon/endpoints.rs index 12920714..6b2fd994 100644 --- a/src/daemon/endpoints.rs +++ b/src/daemon/endpoints.rs @@ -535,6 +535,7 @@ impl ErrorToStatus for ca::ServerError { fn status(&self) -> StatusCode { match self { ca::ServerError::CertAuth(e) => e.status(), + ca::ServerError::DuplicateCa(_) => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -620,6 +621,7 @@ impl ToErrorCode for ca::ServerError { fn code(&self) -> ErrorCode { match self { ca::ServerError::CertAuth(e) => e.code(), + ca::ServerError::DuplicateCa(_) => ErrorCode::DuplicateChild, _ => ErrorCode::CaServerError, } } @@ -631,6 +633,7 @@ impl ToErrorCode for ca::Error { ca::Error::DuplicateChild(_) => ErrorCode::DuplicateChild, ca::Error::MustHaveResources => ErrorCode::ChildNeedsResources, ca::Error::MissingResources => ErrorCode::ChildOverclaims, + ca::Error::DuplicateParent(_) => ErrorCode::DuplicateParent, _ => ErrorCode::CaServerError, } } From 11d8c9a301a642f9f433bbec498bed2fe78a479c Mon Sep 17 00:00:00 2001 From: Tim Bruijnzeels Date: Thu, 26 Sep 2019 06:42:03 +0200 Subject: [PATCH 9/9] Give proper error response when updating unknown child. --- src/commons/api/mod.rs | 7 ++++++- src/daemon/endpoints.rs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commons/api/mod.rs b/src/commons/api/mod.rs index 5eafa868..5f6541ef 100644 --- a/src/commons/api/mod.rs +++ b/src/commons/api/mod.rs @@ -299,6 +299,9 @@ pub enum ErrorCode { #[display(fmt = "Parent with name exists")] DuplicateParent, + #[display(fmt = "Child unknown")] + UnknownChild, + // 3000s General server errors #[display(fmt = "Cannot update internal state, issue with work_dir?")] Persistence, @@ -352,6 +355,7 @@ impl From for ErrorCode { 2302 => ErrorCode::ChildNeedsResources, 2303 => ErrorCode::ChildOverclaims, 2304 => ErrorCode::DuplicateParent, + 2305 => ErrorCode::UnknownChild, // 3000s -> Server issues, bugs or operational issues 3001 => ErrorCode::Persistence, @@ -396,6 +400,7 @@ impl Into for ErrorCode { ErrorCode::ChildNeedsResources => 2302, ErrorCode::ChildOverclaims => 2303, ErrorCode::DuplicateParent => 2304, + ErrorCode::UnknownChild => 2305, // server errors ErrorCode::Persistence => 3001, @@ -443,7 +448,7 @@ mod tests { test_code(n) } - for n in 2301..2305 { + for n in 2301..2306 { test_code(n) } diff --git a/src/daemon/endpoints.rs b/src/daemon/endpoints.rs index 6b2fd994..c47ca548 100644 --- a/src/daemon/endpoints.rs +++ b/src/daemon/endpoints.rs @@ -536,6 +536,7 @@ impl ErrorToStatus for ca::ServerError { match self { ca::ServerError::CertAuth(e) => e.status(), ca::ServerError::DuplicateCa(_) => StatusCode::BAD_REQUEST, + ca::ServerError::UnknownCa(_) => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -622,6 +623,7 @@ impl ToErrorCode for ca::ServerError { match self { ca::ServerError::CertAuth(e) => e.code(), ca::ServerError::DuplicateCa(_) => ErrorCode::DuplicateChild, + ca::ServerError::UnknownCa(_) => ErrorCode::UnknownChild, _ => ErrorCode::CaServerError, } } @@ -631,6 +633,7 @@ impl ToErrorCode for ca::Error { fn code(&self) -> ErrorCode { match self { ca::Error::DuplicateChild(_) => ErrorCode::DuplicateChild, + ca::Error::UnknownChild(_) => ErrorCode::UnknownChild, ca::Error::MustHaveResources => ErrorCode::ChildNeedsResources, ca::Error::MissingResources => ErrorCode::ChildOverclaims, ca::Error::DuplicateParent(_) => ErrorCode::DuplicateParent,