azalea_auth/
cache.rs

1//! Cache auth information
2
3use std::{
4    io,
5    path::Path,
6    time::{SystemTime, UNIX_EPOCH},
7};
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use tokio::{
12    fs::{self, File},
13    io::{AsyncReadExt, AsyncWriteExt},
14};
15use tracing::{debug, trace};
16
17#[derive(Debug, Error)]
18pub enum CacheError {
19    #[error("Failed to read cache file: {0}")]
20    Read(io::Error),
21    #[error("Failed to write cache file: {0}")]
22    Write(io::Error),
23    #[error("Failed to create cache file directory: {0}")]
24    MkDir(io::Error),
25    #[error("Failed to parse cache file: {0}")]
26    Parse(serde_json::Error),
27}
28
29#[derive(Deserialize, Serialize, Debug)]
30pub struct CachedAccount {
31    pub email: String,
32    /// Microsoft auth
33    pub msa: ExpiringValue<crate::auth::AccessTokenResponse>,
34    /// Xbox Live auth
35    pub xbl: ExpiringValue<crate::auth::XboxLiveAuth>,
36    /// Minecraft auth
37    pub mca: ExpiringValue<crate::auth::MinecraftAuthResponse>,
38    /// The user's Minecraft profile (i.e. username, UUID, skin)
39    pub profile: crate::auth::ProfileResponse,
40}
41
42#[derive(Deserialize, Serialize, Debug)]
43pub struct ExpiringValue<T> {
44    /// Seconds since the UNIX epoch
45    pub expires_at: u64,
46    pub data: T,
47}
48
49impl<T> ExpiringValue<T> {
50    pub fn is_expired(&self) -> bool {
51        self.expires_at
52            < SystemTime::now()
53                .duration_since(UNIX_EPOCH)
54                .unwrap()
55                .as_secs()
56    }
57
58    /// Return the data if it's not expired, otherwise return `None`
59    pub fn get(&self) -> Option<&T> {
60        if self.is_expired() {
61            None
62        } else {
63            Some(&self.data)
64        }
65    }
66}
67
68impl<T: Clone> Clone for ExpiringValue<T> {
69    fn clone(&self) -> Self {
70        Self {
71            expires_at: self.expires_at,
72            data: self.data.clone(),
73        }
74    }
75}
76
77async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, CacheError> {
78    let mut cache: Vec<CachedAccount> = Vec::new();
79    if cache_file.exists() {
80        let mut cache_file = File::open(cache_file).await.map_err(CacheError::Read)?;
81        // read the file into a string
82        let mut contents = String::new();
83        cache_file
84            .read_to_string(&mut contents)
85            .await
86            .map_err(CacheError::Read)?;
87        cache = serde_json::from_str(&contents).map_err(CacheError::Parse)?;
88    }
89    Ok(cache)
90}
91async fn set_entire_cache(cache_file: &Path, cache: Vec<CachedAccount>) -> Result<(), CacheError> {
92    trace!("saving cache: {:?}", cache);
93
94    if !cache_file.exists() {
95        let cache_file_parent = cache_file
96            .parent()
97            .expect("Cache file is root directory and also doesn't exist.");
98        debug!(
99            "Making cache file parent directory at {}",
100            cache_file_parent.to_string_lossy()
101        );
102        fs::create_dir_all(cache_file_parent)
103            .await
104            .map_err(CacheError::MkDir)?;
105    }
106    let mut cache_file = File::create(cache_file).await.map_err(CacheError::Write)?;
107    let cache = serde_json::to_string_pretty(&cache).map_err(CacheError::Parse)?;
108    cache_file
109        .write_all(cache.as_bytes())
110        .await
111        .map_err(CacheError::Write)?;
112
113    Ok(())
114}
115
116/// Gets cached data for the given email.
117///
118/// Technically it doesn't actually have to be an email since it's only the
119/// cache key. I considered using usernames or UUIDs as the cache key, but
120/// usernames change and no one has their UUID memorized.
121pub async fn get_account_in_cache(cache_file: &Path, email: &str) -> Option<CachedAccount> {
122    let cache = get_entire_cache(cache_file).await.unwrap_or_default();
123    cache.into_iter().find(|account| account.email == email)
124}
125
126pub async fn set_account_in_cache(
127    cache_file: &Path,
128    email: &str,
129    account: CachedAccount,
130) -> Result<(), CacheError> {
131    let mut cache = get_entire_cache(cache_file).await.unwrap_or_default();
132    cache.retain(|account| account.email != email);
133    cache.push(account);
134    set_entire_cache(cache_file, cache).await
135}