diff --git a/crates/rattler_networking/Cargo.toml b/crates/rattler_networking/Cargo.toml index 5c85dfca2..d631719da 100644 --- a/crates/rattler_networking/Cargo.toml +++ b/crates/rattler_networking/Cargo.toml @@ -24,6 +24,7 @@ itertools = "0.11.0" keyring = "2.0.5" lazy_static = "1.4.0" libc = "0.2.148" +netrc-rs = "0.1.2" once_cell = "1.18.0" reqwest = { version = "0.11.22", default-features = false } retry-policies = { version = "0.2.0", default-features = false } diff --git a/crates/rattler_networking/src/authentication_storage/backends/mod.rs b/crates/rattler_networking/src/authentication_storage/backends/mod.rs index 2d509b202..a8617f8aa 100644 --- a/crates/rattler_networking/src/authentication_storage/backends/mod.rs +++ b/crates/rattler_networking/src/authentication_storage/backends/mod.rs @@ -2,3 +2,5 @@ pub mod file; pub mod keyring; + +pub mod netrc; diff --git a/crates/rattler_networking/src/authentication_storage/backends/netrc.rs b/crates/rattler_networking/src/authentication_storage/backends/netrc.rs new file mode 100644 index 000000000..6ba39bd1c --- /dev/null +++ b/crates/rattler_networking/src/authentication_storage/backends/netrc.rs @@ -0,0 +1,100 @@ +//! Read authentication credentials from `.netrc` files. + +use crate::{authentication_storage::StorageBackend, Authentication}; +use netrc_rs::{Machine, Netrc}; +use std::{collections::HashMap, env, io::ErrorKind, path::Path, path::PathBuf}; + +/// A struct that implements storage and access of authentication +/// information backed by a on-disk JSON file +#[derive(Debug, Clone, Default)] +pub struct NetRcStorage { + /// The netrc file contents + machines: HashMap, +} + +/// An error that can occur when accessing the fallback storage +#[derive(thiserror::Error, Debug)] +pub enum NetRcStorageError { + /// An IO error occurred when accessing the fallback storage + #[error(transparent)] + IOError(#[from] std::io::Error), + + /// An error occurred when parsing the netrc file + #[error("could not parse .netc file: {0}")] + ParseError(netrc_rs::Error), +} + +impl NetRcStorage { + /// Create a new fallback storage by retrieving the netrc file from the user environment. + /// This uses the same environment variable as curl and will read the file from $NETRC + /// falling back to `~/.netrc`. + /// + /// If reading the file fails or parsing the file fails, this will return an error. However, + /// if the file does not exist an empty storage will be returned. + /// + /// When an error is returned the path to the file that the was read from is returned as well. + pub fn from_env() -> Result { + // Get the path to the netrc file + let path = match env::var("NETRC") { + Ok(val) => PathBuf::from(val), + Err(_) => match dirs::home_dir() { + Some(mut path) => { + path.push(".netrc"); + path + } + None => PathBuf::from(".netrc"), + }, + }; + + match Self::from_path(&path) { + Ok(storage) => Ok(storage), + Err(NetRcStorageError::IOError(err)) if err.kind() == ErrorKind::NotFound => { + Ok(Self::default()) + } + Err(err) => Err((path, err)), + } + } + + /// Constructs a new [`NetRcStorage`] by reading the `.netrc` file at the given path. Returns + /// an error if reading from the file failed or if parsing the file failed. + pub fn from_path(path: &Path) -> Result { + let content = std::fs::read_to_string(path)?; + let netrc = Netrc::parse(content, false).map_err(NetRcStorageError::ParseError)?; + let machines = netrc + .machines + .into_iter() + .map(|m| (m.name.clone(), m)) + .filter_map(|(name, value)| name.map(|n| (n, value))) + .collect(); + Ok(Self { machines }) + } + + /// Retrieve the authentication information for the given host + pub fn get_password(&self, host: &str) -> Result, NetRcStorageError> { + match self.machines.get(host) { + Some(machine) => Ok(machine.password.clone()), + None => Ok(None), + } + } +} + +impl StorageBackend for NetRcStorage { + fn store(&self, _host: &str, _authentication: &Authentication) -> anyhow::Result<()> { + anyhow::bail!("NetRcStorage does not support storing credentials") + } + + fn delete(&self, _host: &str) -> anyhow::Result<()> { + anyhow::bail!("NetRcStorage does not support deleting credentials") + } + + fn get(&self, host: &str) -> anyhow::Result> { + match self.get_password(host) { + Ok(Some(password)) => Ok(Some(Authentication::BasicHTTP { + username: host.to_string(), + password, + })), + Ok(None) => Ok(None), + Err(err) => Err(anyhow::Error::new(err)), + } + } +} diff --git a/crates/rattler_networking/src/authentication_storage/storage.rs b/crates/rattler_networking/src/authentication_storage/storage.rs index c6c4d7f0d..c4c8d0147 100644 --- a/crates/rattler_networking/src/authentication_storage/storage.rs +++ b/crates/rattler_networking/src/authentication_storage/storage.rs @@ -10,7 +10,7 @@ use url::Url; use super::{ authentication::Authentication, - backends::{file::FileStorage, keyring::KeyringAuthenticationStorage}, + backends::{file::FileStorage, keyring::KeyringAuthenticationStorage, netrc::NetRcStorage}, StorageBackend, }; @@ -31,6 +31,12 @@ impl Default for AuthenticationStorage { storage.add_backend(Arc::from(KeyringAuthenticationStorage::default())); storage.add_backend(Arc::from(FileStorage::default())); + storage.add_backend(Arc::from(NetRcStorage::from_env().unwrap_or_else( + |(path, err)| { + tracing::warn!("error reading netrc file from {}: {}", path.display(), err); + NetRcStorage::default() + }, + ))); storage }