diff options
Diffstat (limited to 'src')
-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 |
6 files changed, 126 insertions, 28 deletions
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) |