From 88b243bca17ab549342738bce98a1c678f98e754 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 23 Jan 2020 10:54:09 +0100 Subject: Implement configuration handling This patch implements basic configuration handling that reads a configuration file and stores the parsed data in the ExecCtx and RunCtx structs. It supports three configuration items: - model (previously only --model) - no_cache (previously only NITROCLI_NO_CACHE) - verbosity (previously only --verbose) --- CHANGELOG.md | 7 ++++++ Cargo.lock | 31 +++++++++++++++++++++++++ Cargo.toml | 7 ++++++ src/args.rs | 15 +++++++++++- src/commands.rs | 6 ++--- src/config.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 56 +++++++++++++++++++++++++++------------------ src/pinentry.rs | 2 +- src/tests/mod.rs | 5 +++- 9 files changed, 171 insertions(+), 28 deletions(-) create mode 100644 src/config.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a72c6f9..5c22351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +Unreleased +---------- +- Added support for configuration files + - Added `toml` dependency in version `0.5.6` + - Added `serde` dependency in version `1.0.114` + + 0.3.4 ----- - Changed default OTP format from `hex` to `base32` diff --git a/Cargo.lock b/Cargo.lock index 81f4c8e..6893b95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,7 +99,9 @@ dependencies = [ "nitrokey-test", "nitrokey-test-state", "regex", + "serde", "structopt", + "toml", ] [[package]] @@ -211,6 +213,26 @@ version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +[[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "structopt" version = "0.3.13" @@ -275,6 +297,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + [[package]] name = "unicode-segmentation" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index fcc1a6d..23602ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,10 +58,17 @@ version = "0.2" [dependencies.nitrokey] version = "0.7.1" +[dependencies.serde] +version = "1.0" +features = ["derive"] + [dependencies.structopt] version = "0.3.7" default-features = false +[dependencies.toml] +version = "0.5.6" + [dev-dependencies.nitrokey-test] version = "0.4" diff --git a/src/args.rs b/src/args.rs index 0f548e4..7f2dc31 100644 --- a/src/args.rs +++ b/src/args.rs @@ -18,7 +18,7 @@ // ************************************************************************* /// Provides access to a Nitrokey device -#[derive(structopt::StructOpt)] +#[derive(Debug, structopt::StructOpt)] #[structopt(name = "nitrocli")] pub struct Args { /// Increases the log level (can be supplied multiple times) @@ -57,6 +57,19 @@ impl From for nitrokey::Model { } } +impl<'de> serde::Deserialize<'de> for DeviceModel { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + use std::str::FromStr as _; + + let s = String::deserialize(deserializer)?; + DeviceModel::from_str(&s).map_err(D::Error::custom) + } +} + Command! { /// A top-level command for nitrocli. Command, [ diff --git a/src/commands.rs b/src/commands.rs index 1d59af5..090d532 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -39,7 +39,7 @@ use crate::ExecCtx; /// Set `libnitrokey`'s log level based on the execution context's verbosity. fn set_log_level(ctx: &mut ExecCtx<'_>) { - let log_lvl = match ctx.verbosity { + let log_lvl = match ctx.config.verbosity { // The error log level is what libnitrokey uses by default. As such, // there is no harm in us setting that as well when the user did not // ask for higher verbosity. @@ -63,7 +63,7 @@ where set_log_level(ctx); - let device = match ctx.model { + let device = match ctx.config.model { Some(model) => manager.connect_model(model.into()).with_context(|| { anyhow::anyhow!("Nitrokey {} device not found", model.as_user_facing_str()) })?, @@ -83,7 +83,7 @@ where set_log_level(ctx); - if let Some(model) = ctx.model { + if let Some(model) = ctx.config.model { if model != args::DeviceModel::Storage { anyhow::bail!("This command is only available on the Nitrokey Storage"); } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..850d217 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,70 @@ +// config.rs + +// ************************************************************************* +// * Copyright (C) 2020 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see . * +// ************************************************************************* + +use std::fs; +use std::path; + +use crate::args; + +use anyhow::Context as _; + +/// The configuration for nitrocli, usually read from configuration +/// files and environment variables. +#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Deserialize)] +pub struct Config { + /// The model to connect to. + pub model: Option, + /// Whether to bypass the cache for all secrets or not. + #[serde(default)] + pub no_cache: bool, + /// The log level. + #[serde(default)] + pub verbosity: u8, +} + +impl Config { + pub fn load() -> anyhow::Result { + load_user_config().map(|o| o.unwrap_or_default()) + } + + pub fn update(&mut self, args: &args::Args) { + if args.model.is_some() { + self.model = args.model; + } + if args.verbose > 0 { + self.verbosity = args.verbose; + } + } +} + +fn load_user_config() -> anyhow::Result> { + let path = path::Path::new("config.toml"); + if path.is_file() { + read_config_file(&path).map(Some) + } else { + Ok(None) + } +} + +pub fn read_config_file(path: &path::Path) -> anyhow::Result { + let s = fs::read_to_string(path) + .with_context(|| format!("Failed to read configuration file '{}'", path.display()))?; + toml::from_str(&s) + .with_context(|| format!("Failed to parse configuration file '{}'", path.display())) +} diff --git a/src/main.rs b/src/main.rs index a8c0a46..3535a91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,7 @@ mod arg_util; mod args; mod commands; +mod config; mod pinentry; #[cfg(test)] mod tests; @@ -108,8 +109,6 @@ where /// the command execution. #[allow(missing_debug_implementations)] pub struct ExecCtx<'io> { - /// The Nitrokey model to use. - pub model: Option, /// See `RunCtx::stdout`. pub stdout: &'io mut dyn io::Write, /// See `RunCtx::stderr`. @@ -124,10 +123,8 @@ pub struct ExecCtx<'io> { pub new_user_pin: Option, /// See `RunCtx::password`. pub password: Option, - /// See `RunCtx::no_cache`. - pub no_cache: bool, - /// The verbosity level to use for logging. - pub verbosity: u64, + /// See `RunCtx::config`. + pub config: config::Config, } impl<'io> Stdio for ExecCtx<'io> { @@ -142,8 +139,9 @@ fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec) -> anyhow::Result<( match args::Args::from_iter_safe(args.iter()) { Ok(args) => { + let mut config = ctx.config; + config.update(&args); let mut ctx = ExecCtx { - model: args.model, stdout: ctx.stdout, stderr: ctx.stderr, admin_pin: ctx.admin_pin.take(), @@ -151,8 +149,7 @@ fn handle_arguments(ctx: &mut RunCtx<'_>, args: Vec) -> anyhow::Result<( new_admin_pin: ctx.new_admin_pin.take(), new_user_pin: ctx.new_user_pin.take(), password: ctx.password.take(), - no_cache: ctx.no_cache, - verbosity: args.verbose.into(), + config, }; args.cmd.execute(&mut ctx) } @@ -187,8 +184,9 @@ pub(crate) struct RunCtx<'io> { pub new_user_pin: Option, /// A password used by some commands, if provided through an environment variable. pub password: Option, - /// Whether to bypass the cache for all secrets or not. - pub no_cache: bool, + /// The configuration, usually read from configuration files and environment + /// variables. + pub config: config::Config, } fn run<'ctx, 'io: 'ctx>(ctx: &'ctx mut RunCtx<'io>, args: Vec) -> i32 { @@ -206,19 +204,33 @@ fn main() { let mut stdout = io::stdout(); let mut stderr = io::stderr(); - let args = env::args().collect::>(); - let ctx = &mut RunCtx { - stdout: &mut stdout, - stderr: &mut stderr, - admin_pin: env::var_os(NITROCLI_ADMIN_PIN), - user_pin: env::var_os(NITROCLI_USER_PIN), - new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN), - new_user_pin: env::var_os(NITROCLI_NEW_USER_PIN), - password: env::var_os(NITROCLI_PASSWORD), - no_cache: env::var_os(NITROCLI_NO_CACHE).is_some(), + + let rc = match config::Config::load() { + Ok(mut config) => { + if env::var_os(NITROCLI_NO_CACHE).is_some() { + config.no_cache = true; + } + + let args = env::args().collect::>(); + let ctx = &mut RunCtx { + stdout: &mut stdout, + stderr: &mut stderr, + admin_pin: env::var_os(NITROCLI_ADMIN_PIN), + user_pin: env::var_os(NITROCLI_USER_PIN), + new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN), + new_user_pin: env::var_os(NITROCLI_NEW_USER_PIN), + password: env::var_os(NITROCLI_PASSWORD), + config, + }; + + run(ctx, args) + } + Err(err) => { + let _ = writeln!(stderr, "{:?}", err); + 1 + } }; - let rc = run(ctx, args); // We exit the process the hard way below. The problem is that because // of this, buffered IO may not be flushed. So make sure to explicitly // flush before exiting. Note that stderr is unbuffered, alleviating diff --git a/src/pinentry.rs b/src/pinentry.rs index 510d7b0..f538a47 100644 --- a/src/pinentry.rs +++ b/src/pinentry.rs @@ -238,7 +238,7 @@ where { let cache_id = entry .cache_id() - .and_then(|id| if ctx.no_cache { None } else { Some(id) }) + .and_then(|id| if ctx.config.no_cache { None } else { Some(id) }) // "X" is a sentinel value indicating that no caching is desired. .unwrap_or_else(|| "X".into()) .into(); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index f26ab80..9477964 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -115,7 +115,10 @@ impl Nitrocli { new_admin_pin: self.new_admin_pin.clone(), new_user_pin: self.new_user_pin.clone(), password: self.password.clone(), - no_cache: true, + config: crate::config::Config { + no_cache: true, + ..Default::default() + }, }; (f(ctx, args), stdout, stderr) -- cgit v1.2.3