quilt-rs 0.8.11

Rust library for accessing Quilt data packages.
Documentation
use std::collections::HashMap;

use chrono::serde::ts_seconds;
use reqwest::Client as HttpClient;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;

use crate::io::storage::auth::AuthIo;
use crate::io::storage::auth::Credentials;
use crate::io::storage::auth::Tokens;
use crate::io::storage::LocalStorage;
use crate::paths::DomainPaths;
use crate::uri::Host;
use crate::Res;

const USER_AGENT: &str =
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)";

#[derive(Deserialize, Serialize, Debug)]
pub struct RemoteTokens {
    pub access_token: String,
    pub refresh_token: String,
    #[serde(with = "ts_seconds")]
    pub expires_at: chrono::DateTime<chrono::Utc>,
}

impl From<RemoteTokens> for Tokens {
    fn from(raw: RemoteTokens) -> Self {
        Tokens {
            access_token: raw.access_token,
            refresh_token: raw.refresh_token,
            expires_at: raw.expires_at,
        }
    }
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct RemoteCredentials {
    access_key_id: String,
    #[serde(deserialize_with = "date_from_rfc3339")]
    expiration: chrono::DateTime<chrono::Utc>,
    secret_access_key: String,
    session_token: String,
}

impl From<RemoteCredentials> for Credentials {
    fn from(raw: RemoteCredentials) -> Self {
        Credentials {
            access_key: raw.access_key_id,
            secret_key: raw.secret_access_key,
            token: raw.session_token,
            expires_at: raw.expiration,
        }
    }
}

fn date_from_rfc3339<'de, D: Deserializer<'de>>(
    deserializer: D,
) -> Result<chrono::DateTime<chrono::Utc>, D::Error> {
    use serde::de::Error;
    String::deserialize(deserializer).and_then(|s| {
        chrono::DateTime::parse_from_rfc3339(&s)
            .map_err(|e| Error::custom(format!("Invalid RFC3339 date: {}", e)))
            .map(|dt| dt.with_timezone(&chrono::Utc))
    })
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct QuiltStackConfig {
    registry_url: url::Url,
}

async fn get_registry_url(http_client: &HttpClient, host: &Host) -> Res<url::Host> {
    let request = http_client
        .get(format!("https://{}/config.json", host))
        .header("User-Agent", USER_AGENT)
        .build()?;
    let response = http_client.execute(request).await?;

    let QuiltStackConfig { registry_url } = response.json().await?;
    Ok(url::Host::Domain(
        registry_url
            .domain()
            .ok_or(crate::Error::LoginRequiredRegistryUrl(host.to_owned()))?
            .to_string(),
    ))
}

async fn get_auth_tokens(
    http_client: &HttpClient,
    host: &Host,
    refresh_token: &str,
) -> Res<Tokens> {
    let registry = get_registry_url(http_client, host).await?;

    let mut form_data: HashMap<String, String> = HashMap::new();
    form_data.insert("refresh_token".to_string(), refresh_token.to_string());
    let request = http_client
        .post(format!("https://{}/api/token", registry))
        .header("User-Agent", USER_AGENT)
        .form(&form_data)
        .build()?;
    let response = http_client.execute(request).await?;

    let tokens_json: RemoteTokens = response.json().await?;
    let tokens = Tokens::from(tokens_json);

    Ok(tokens)
}

async fn refresh_credentials(
    http_client: &HttpClient,
    host: &Host,
    access_token: &str,
) -> Res<Credentials> {
    let registry = get_registry_url(http_client, host).await?;

    let empty: HashMap<String, String> = HashMap::new();
    let request = http_client
        .get(format!("https://{}/api/auth/get_credentials", registry))
        .bearer_auth(access_token)
        .header("User-Agent", USER_AGENT)
        .json(&empty)
        .build()?;
    let response = http_client.execute(request).await?;

    let creds_json: RemoteCredentials = response.json().await?;
    let credentials = Credentials::from(creds_json);

    Ok(credentials)
}

#[derive(Debug, Clone)]
pub struct Auth {
    pub paths: DomainPaths,
    pub storage: LocalStorage,
}

impl Auth {
    pub fn new(paths: DomainPaths, storage: LocalStorage) -> Self {
        Self { paths, storage }
    }

    pub async fn login(&self, http_client: &HttpClient, host: &Host, refresh_token: String) -> Res {
        let tokens = self
            .get_auth_tokens(http_client, host, &refresh_token)
            .await?;

        self.save_tokens(host, &tokens).await?;

        self.refresh_credentials(http_client, host, &tokens.access_token)
            .await?;

        Ok(())
    }

    async fn get_auth_tokens(
        &self,
        http_client: &HttpClient,
        host: &Host,
        refresh_token: &str,
    ) -> Res<Tokens> {
        get_auth_tokens(http_client, host, refresh_token).await
    }

    async fn save_tokens(&self, host: &Host, tokens: &Tokens) -> Res<()> {
        let auth_io = AuthIo::new(self.storage.clone(), self.paths.auth_host(host));
        auth_io.write_tokens(tokens).await
    }

    async fn refresh_credentials(
        &self,
        http_client: &HttpClient,
        host: &Host,
        access_token: &str,
    ) -> Res<Credentials> {
        let credentials = refresh_credentials(http_client, host, access_token).await?;

        let auth_io = AuthIo::new(self.storage.clone(), self.paths.auth_host(host));
        auth_io.write_credentials(&credentials).await?;

        Ok(credentials)
    }

    pub async fn get_credentials_or_refresh(
        &self,
        http_client: &HttpClient,
        host: &Host,
    ) -> Res<Credentials> {
        let auth_io = AuthIo::new(self.storage.clone(), self.paths.auth_host(host));
        match auth_io.read_credentials().await? {
            Some(creds) => Ok(creds),
            None => match auth_io.read_tokens().await? {
                Some(tokens) => {
                    self.refresh_credentials(http_client, host, &tokens.access_token)
                        .await
                }
                None => Err(crate::Error::LoginRequired),
            },
        }
    }
}