use crate::{
authentication_storage::{AuthenticationStorageError, StorageBackend},
Authentication,
};
use netrc_rs::{Machine, Netrc};
use std::{collections::HashMap, env, io::ErrorKind, path::Path, path::PathBuf};
#[derive(Debug, Clone, Default)]
pub struct NetRcStorage {
machines: HashMap<String, Machine>,
}
#[derive(thiserror::Error, Debug)]
pub enum NetRcStorageError {
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error("could not parse .netrc file: {0}")]
ParseError(netrc_rs::Error),
#[error("{0}")]
NotSupportedError(String),
}
impl NetRcStorage {
pub fn from_env() -> Result<Self, (PathBuf, NetRcStorageError)> {
let path = match env::var("NETRC") {
Ok(val) => PathBuf::from(val),
Err(_) => match dirs::home_dir() {
Some(mut path) => {
#[cfg(windows)]
path.push("_netrc");
#[cfg(not(windows))]
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)),
}
}
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 })
}
pub fn get_password(&self, host: &str) -> Result<Option<Authentication>, NetRcStorageError> {
match self.machines.get(host) {
Some(machine) => Ok(Some(Authentication::BasicHTTP {
username: machine.login.clone().unwrap_or_default(),
password: machine.password.clone().unwrap_or_default(),
})),
None => Ok(None),
}
}
}
impl StorageBackend for NetRcStorage {
fn store(
&self,
_host: &str,
_authentication: &Authentication,
) -> Result<(), AuthenticationStorageError> {
Err(NetRcStorageError::NotSupportedError(
"NetRcStorage does not support storing credentials".to_string(),
))?
}
fn delete(&self, _host: &str) -> Result<(), AuthenticationStorageError> {
Err(NetRcStorageError::NotSupportedError(
"NetRcStorage does not support deleting credentials".to_string(),
))?
}
fn get(&self, host: &str) -> Result<Option<Authentication>, AuthenticationStorageError> {
match self.get_password(host) {
Ok(Some(auth)) => Ok(Some(auth)),
Ok(None) => Ok(None),
Err(err) => Err(err.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_file_storage() {
let file = tempdir().unwrap();
let path = file.path().join(".testnetrc");
let mut netrc = std::fs::File::create(&path).unwrap();
netrc
.write_all(b"machine mainmachine\nlogin test\npassword password\n")
.unwrap();
netrc.flush().unwrap();
let storage = NetRcStorage::from_path(path.as_path()).unwrap();
assert_eq!(
storage.get("mainmachine").unwrap(),
Some(Authentication::BasicHTTP {
username: "test".to_string(),
password: "password".to_string(),
})
);
assert_eq!(storage.get("test_unknown").unwrap(), None);
}
#[test]
fn test_file_storage_from_env() {
let file = tempdir().unwrap();
let path = file.path().join(".testnetrc2");
let mut netrc = std::fs::File::create(&path).unwrap();
netrc
.write_all(b"machine supermachine\nlogin test2\npassword password2\n")
.unwrap();
netrc.flush().unwrap();
let old_netrc = env::var("NETRC");
env::set_var("NETRC", path.as_os_str());
let storage = NetRcStorage::from_env().unwrap();
assert_eq!(
storage.get("supermachine").unwrap(),
Some(Authentication::BasicHTTP {
username: "test2".to_string(),
password: "password2".to_string(),
})
);
assert_eq!(storage.get("test_unknown").unwrap(), None);
if let Ok(netrc) = old_netrc {
env::set_var("NETRC", netrc);
} else {
env::remove_var("NETRC");
}
}
}