use anyhow::{anyhow, Result};
use reqwest::IntoUrl;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use url::Url;
use crate::authentication_storage::{backends::file::FileStorage, AuthenticationStorageError};
use super::{authentication::Authentication, StorageBackend};
#[cfg(feature = "netrc-rs")]
use super::backends::netrc::NetRcStorage;
#[cfg(feature = "keyring")]
use crate::authentication_storage::backends::keyring::KeyringAuthenticationStorageError;
#[cfg(feature = "keyring")]
use super::backends::keyring::KeyringAuthenticationStorage;
#[derive(Debug, Clone)]
pub struct AuthenticationStorage {
pub backends: Vec<Arc<dyn StorageBackend + Send + Sync>>,
cache: Arc<Mutex<HashMap<String, Option<Authentication>>>>,
}
impl AuthenticationStorage {
pub fn empty() -> Self {
Self {
backends: vec![],
cache: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn from_env_and_defaults() -> Result<Self, AuthenticationStorageError> {
let mut storage = Self::empty();
if let Ok(auth_file) = std::env::var("RATTLER_AUTH_FILE") {
let path = std::path::Path::new(&auth_file);
tracing::info!(
"\"RATTLER_AUTH_FILE\" environment variable set, using file storage at {}",
auth_file
);
storage.add_backend(Arc::from(FileStorage::from_path(path.into())?));
}
#[cfg(feature = "keyring")]
storage.add_backend(Arc::from(KeyringAuthenticationStorage::default()));
#[cfg(feature = "dirs")]
storage.add_backend(Arc::from(FileStorage::new()?));
#[cfg(feature = "netrc-rs")]
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()
},
)));
Ok(storage)
}
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 {
#[allow(unused_variables)]
if let Err(error) = backend.store(host, authentication) {
#[cfg(feature = "keyring")]
if let AuthenticationStorageError::KeyringStorageError(
KeyringAuthenticationStorageError::StorageError(_),
) = error
{
tracing::debug!("Error storing credentials in keyring: {}", error);
} else {
tracing::warn!("Error storing credentials from backend: {}", error);
}
} else {
return Ok(());
}
}
Err(anyhow!(
"All backends failed to store credentials. Checked the following backends: {:?}",
self.backends
))
}
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) => {}
Err(_e) => {
#[cfg(feature = "keyring")]
if let AuthenticationStorageError::KeyringStorageError(
KeyringAuthenticationStorageError::StorageError(_),
) = _e
{
tracing::trace!("Error storing credentials in keyring: {}", _e);
} else {
tracing::warn!("Error retrieving credentials from backend: {}", _e);
}
}
}
}
let mut cache = self.cache.lock().unwrap();
cache.insert(host.to_string(), None);
Ok(None)
}
pub fn get_by_url_with_host<U: IntoUrl>(
&self,
url: U,
) -> Result<(Url, Option<(String, Authentication)>), reqwest::Error> {
let url = url.into_url()?;
let host = match url.host_str() {
Some(h) => h.to_string(),
None => return Ok((url, None)),
};
match self.get(&host) {
Ok(None) => {}
Err(_) => return Ok((url, None)),
Ok(Some(credentials)) => {
return Ok((url, Some((host, credentials))));
}
};
if url.scheme() == "s3" {
let mut current_url = url.clone();
loop {
match self.get(current_url.as_str()) {
Ok(None) => {
let possible_rest =
current_url.as_str().rsplit_once('/').map(|(rest, _)| rest);
match possible_rest {
Some(rest) => {
if let Ok(new_url) = Url::parse(rest) {
current_url = new_url;
} else {
return Ok((url, None));
}
}
_ => return Ok((url, None)), }
}
Ok(Some(credentials)) => {
return Ok((url, Some((current_url.as_str().to_string(), credentials))));
}
Err(_) => return Ok((url, None)),
}
}
}
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((wildcard_host, credentials))));
}
let possible_rest = domain.split_once('.').map(|(_, rest)| rest);
match possible_rest {
Some(rest) => {
domain = rest;
}
_ => return Ok((url, None)), }
}
}
pub fn get_by_url<U: IntoUrl>(
&self,
url: U,
) -> Result<(Url, Option<Authentication>), reqwest::Error> {
let (url, auth) = self.get_by_url_with_host(url)?;
Ok((url, auth.map(|(_, credentials)| credentials)))
}
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 {
#[allow(unused_variables)]
if let Err(error) = backend.delete(host) {
#[cfg(feature = "keyring")]
if let AuthenticationStorageError::KeyringStorageError(
KeyringAuthenticationStorageError::StorageError(_),
) = error
{
tracing::debug!("Error deleting credentials in keyring: {}", error);
} else {
tracing::warn!("Error deleting credentials from backend: {}", error);
}
} else {
all_failed = false;
}
}
if all_failed {
Err(anyhow!("All backends failed to delete credentials"))
} else {
Ok(())
}
}
}