Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for netrc files as a secondary fallback tier #395

Merged
merged 10 commits into from
Jan 3, 2024
1 change: 1 addition & 0 deletions crates/rattler_networking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

pub mod file;
pub mod keyring;

pub mod netrc;
100 changes: 100 additions & 0 deletions crates/rattler_networking/src/authentication_storage/backends/netrc.rs
Original file line number Diff line number Diff line change
@@ -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<String, Machine>,
}

/// 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<Self, (PathBuf, NetRcStorageError)> {
// 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<Self, NetRcStorageError> {
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<Option<String>, 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<Option<Authentication>> {
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)),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use url::Url;

use super::{
authentication::Authentication,
backends::{file::FileStorage, keyring::KeyringAuthenticationStorage},
backends::{file::FileStorage, keyring::KeyringAuthenticationStorage, netrc::NetRcStorage},
StorageBackend,
};

Expand All @@ -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
}
Expand Down