use anyhow::{anyhow, Result};
use reqwest::IntoUrl;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use url::Url;
use super::{
authentication::Authentication,
backends::{file::FileStorage, keyring::KeyringAuthenticationStorage, netrc::NetRcStorage},
StorageBackend,
};
#[derive(Debug, Clone)]
pub struct AuthenticationStorage {
backends: Vec<Arc<dyn StorageBackend + Send + Sync>>,
cache: Arc<Mutex<HashMap<String, Option<Authentication>>>>,
}
impl Default for AuthenticationStorage {
fn default() -> Self {
let mut storage = Self::new();
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
}
}
impl AuthenticationStorage {
pub fn new() -> Self {
Self {
backends: vec![],
cache: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn add_backend(&mut self, backend: Arc<dyn StorageBackend + Send + Sync>) {
self.backends.push(backend);
}
pub fn store(&self, host: &str, authentication: &Authentication) -> Result<()> {
{
let mut cache = self.cache.lock().unwrap();
cache.insert(host.to_string(), Some(authentication.clone()));
}
for backend in &self.backends {
if let Err(e) = backend.store(host, authentication) {
tracing::warn!("Error storing credentials in backend: {}", e);
} else {
return Ok(());
}
}
Err(anyhow!("All backends failed to store credentials"))
}
pub fn get(&self, host: &str) -> Result<Option<Authentication>> {
{
let cache = self.cache.lock().unwrap();
if let Some(auth) = cache.get(host) {
return Ok(auth.clone());
}
}
for backend in &self.backends {
match backend.get(host) {
Ok(Some(auth)) => {
let mut cache = self.cache.lock().unwrap();
cache.insert(host.to_string(), Some(auth.clone()));
return Ok(Some(auth));
}
Ok(None) => {
continue;
}
Err(e) => {
tracing::warn!("Error retrieving credentials from backend: {}", e);
}
}
}
Ok(None)
}
pub fn get_by_url<U: IntoUrl>(
&self,
url: U,
) -> Result<(Url, Option<Authentication>), reqwest::Error> {
let url = url.into_url()?;
let Some(host) = url.host_str() else {
return Ok((url, None));
};
match self.get(host) {
Ok(None) => {}
Err(_) => return Ok((url, None)),
Ok(Some(credentials)) => return Ok((url, Some(credentials))),
};
let Some(mut domain) = url.domain() else {
return Ok((url, None));
};
loop {
let wildcard_host = format!("*.{domain}");
let Ok(credentials) = self.get(&wildcard_host) else {
return Ok((url, None));
};
if let Some(credentials) = credentials {
return Ok((url, Some(credentials)));
}
let possible_rest = domain.split_once('.').map(|(_, rest)| rest);
match possible_rest {
Some(rest) => {
domain = rest;
}
_ => return Ok((url, None)), }
}
}
pub fn delete(&self, host: &str) -> Result<()> {
{
let mut cache = self.cache.lock().unwrap();
cache.insert(host.to_string(), None);
}
let mut all_failed = true;
for backend in &self.backends {
if let Err(e) = backend.delete(host) {
tracing::warn!("Error deleting credentials from backend: {}", e);
} else {
all_failed = false;
}
}
if all_failed {
Err(anyhow!("All backends failed to delete credentials"))
} else {
Ok(())
}
}
}