diff options
| -rw-r--r-- | CHANGELOG.md | 7 | ||||
| -rw-r--r-- | Cargo.lock | 31 | ||||
| -rw-r--r-- | Cargo.toml | 7 | ||||
| -rw-r--r-- | src/args.rs | 15 | ||||
| -rw-r--r-- | src/commands.rs | 6 | ||||
| -rw-r--r-- | src/config.rs | 70 | ||||
| -rw-r--r-- | src/main.rs | 56 | ||||
| -rw-r--r-- | src/pinentry.rs | 2 | ||||
| -rw-r--r-- | src/tests/mod.rs | 5 | 
9 files changed, 171 insertions, 28 deletions
| 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` @@ -99,7 +99,9 @@ dependencies = [   "nitrokey-test",   "nitrokey-test-state",   "regex", + "serde",   "structopt", + "toml",  ]  [[package]] @@ -212,6 +214,26 @@ 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"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -276,6 +298,15 @@ dependencies = [  ]  [[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"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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<DeviceModel> for nitrokey::Model {    }  } +impl<'de> serde::Deserialize<'de> for DeviceModel { +  fn deserialize<D>(deserializer: D) -> Result<DeviceModel, D::Error> +  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 <http://www.gnu.org/licenses/>. * +// ************************************************************************* + +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<args::DeviceModel>, +  /// 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<Self> { +    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<Option<Config>> { +  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<Config> { +  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<args::DeviceModel>,    /// 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<ffi::OsString>,    /// See `RunCtx::password`.    pub password: Option<ffi::OsString>, -  /// 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<String>) -> 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<String>) -> 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<ffi::OsString>,    /// A password used by some commands, if provided through an environment variable.    pub password: Option<ffi::OsString>, -  /// 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<String>) -> i32 { @@ -206,19 +204,33 @@ fn main() {    let mut stdout = io::stdout();    let mut stderr = io::stderr(); -  let args = env::args().collect::<Vec<_>>(); -  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::<Vec<_>>(); +      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) | 
