decrypt_cookies/firefox/
builder.rs

1use std::{fmt::Display, marker::PhantomData, path::PathBuf};
2
3use tokio::{fs, join};
4
5use crate::{
6    firefox::{items::cookie::dao::CookiesQuery, FirefoxGetter},
7    prelude::FirefoxPath,
8};
9
10// TODO: add browser name in error
11#[derive(Debug)]
12#[derive(thiserror::Error)]
13pub enum FirefoxBuilderError {
14    #[error(transparent)]
15    Ini(#[from] ini::Error),
16    #[error(transparent)]
17    IniParser(#[from] ini::ParseError),
18    #[error("Profile {0} missing `Name` properties")]
19    ProfilePath(String),
20    #[error("Install {0} missing `Default` properties")]
21    InstallPath(String),
22    #[error(transparent)]
23    Db(#[from] sea_orm::DbErr),
24    #[error("Io: {source}, path: {path}")]
25    Io {
26        source: std::io::Error,
27        path: PathBuf,
28    },
29}
30
31pub type Result<T> = std::result::Result<T, FirefoxBuilderError>;
32
33#[derive(Clone)]
34#[derive(Debug)]
35#[derive(Default)]
36#[derive(PartialEq, Eq, PartialOrd, Ord)]
37pub(crate) struct TempPaths {
38    pub(crate) cookies_temp: PathBuf,
39    pub(crate) login_data_temp: PathBuf,
40    pub(crate) key_temp: PathBuf,
41}
42
43#[derive(Clone)]
44#[derive(Debug)]
45#[derive(Default)]
46pub struct FirefoxBuilder<'a, T> {
47    pub(crate) init: Option<PathBuf>,
48    pub(crate) profile: Option<&'a str>,
49    pub(crate) profile_path: Option<&'a str>,
50    pub(crate) __browser: PhantomData<T>,
51}
52
53impl<B: FirefoxPath> Display for FirefoxGetter<B> {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.write_str(B::NAME)
56    }
57}
58
59impl<B: FirefoxPath> Display for FirefoxBuilder<'_, B> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.write_fmt(format_args!("{}Builder", B::NAME))
62    }
63}
64
65impl<'b, B: FirefoxPath> FirefoxBuilder<'b, B> {
66    pub const fn new() -> Self {
67        Self {
68            init: None,
69            profile: None,
70            profile_path: None,
71            __browser: core::marker::PhantomData::<B>,
72        }
73    }
74
75    /// Get firefox data dir
76    pub fn init() -> PathBuf {
77        dirs::home_dir()
78            .expect("Get home dir failed")
79            .join(B::BASE)
80    }
81
82    /// `profile_path`: when browser start with `-profile <profile_path>`
83    pub fn with_profile_path<P>(profile_path: P) -> Result<Self>
84    where
85        P: Into<Option<&'b str>>,
86    {
87        Ok(Self {
88            init: None,
89            profile: None,
90            profile_path: profile_path.into(),
91            __browser: core::marker::PhantomData::<B>,
92        })
93    }
94
95    /// `init`: When firefox init path changed
96    /// `profile`: When start with `-P <profile>`
97    pub fn with_base_profile<I, P>(init: I, profile: P) -> Self
98    where
99        I: Into<Option<PathBuf>>,
100        P: Into<Option<&'b str>>,
101    {
102        Self {
103            init: init.into(),
104            profile: profile.into(),
105            profile_path: None,
106            __browser: core::marker::PhantomData::<B>,
107        }
108    }
109
110    async fn cache_data(profile_path: PathBuf) -> Result<TempPaths> {
111        let cookies = B::cookies(profile_path.clone());
112        let cookies_temp = B::cookies_temp();
113
114        let login_data = B::login_data(profile_path.clone());
115        let login_data_temp = B::login_data_temp();
116
117        let key = B::key(profile_path.clone());
118        let key_temp = B::key_temp();
119
120        let ck_temp_p = cookies_temp
121            .parent()
122            .expect("Get parent dir failed");
123        let cd_ck = fs::create_dir_all(ck_temp_p);
124        let lg_temp_p = login_data_temp
125            .parent()
126            .expect("Get parent dir failed");
127        let cd_lg = fs::create_dir_all(lg_temp_p);
128        let k_temp_p = key_temp
129            .parent()
130            .expect("Get parent dir failed");
131        let cd_k = fs::create_dir_all(k_temp_p);
132        let (cd_ck, cd_lg, cd_k) = join!(cd_ck, cd_lg, cd_k);
133        cd_ck.map_err(|e| FirefoxBuilderError::Io {
134            source: e,
135            path: ck_temp_p.to_owned(),
136        })?;
137        cd_lg.map_err(|e| FirefoxBuilderError::Io {
138            source: e,
139            path: lg_temp_p.to_owned(),
140        })?;
141        cd_k.map_err(|e| FirefoxBuilderError::Io {
142            source: e,
143            path: k_temp_p.to_owned(),
144        })?;
145
146        let cookies_cp = fs::copy(&cookies, &cookies_temp);
147        let login_cp = fs::copy(&login_data, &login_data_temp);
148        let key_cp = fs::copy(&key, &key_temp);
149
150        let (ck, lg, k) = join!(cookies_cp, login_cp, key_cp);
151        ck.map_err(|e| FirefoxBuilderError::Io { source: e, path: cookies })?;
152        lg.map_err(|e| FirefoxBuilderError::Io { source: e, path: login_data })?;
153        k.map_err(|e| FirefoxBuilderError::Io { source: e, path: key })?;
154
155        Ok(TempPaths {
156            cookies_temp,
157            login_data_temp,
158            key_temp,
159        })
160    }
161}
162
163impl<'b, B: FirefoxPath + Send + Sync> FirefoxBuilder<'b, B> {
164    /// Get user specify profile path
165    pub async fn get_profile_path(&self) -> Result<PathBuf> {
166        let mut base = self
167            .init
168            .clone()
169            .unwrap_or_else(Self::init);
170        let ini_path = base.join("profiles.ini");
171
172        let ini_str = fs::read_to_string(&ini_path)
173            .await
174            .map_err(|e| FirefoxBuilderError::Io { source: e, path: ini_path })?;
175
176        let ini_file = ini::Ini::load_from_str(&ini_str)?;
177        for (section, prop) in ini_file {
178            let Some(section) = section
179            else {
180                continue;
181            };
182            if let Some(profile) = self.profile {
183                if !section.starts_with("Profile") {
184                    continue;
185                }
186                let Some(profile_name) = prop.get("Name")
187                else {
188                    continue;
189                };
190                if profile_name == profile {
191                    let Some(var) = prop.get("Path")
192                    else {
193                        return Err(FirefoxBuilderError::ProfilePath(profile_name.to_owned()));
194                    };
195                    base.push(var);
196                    break;
197                }
198            }
199            else {
200                if !section.starts_with("Install") {
201                    continue;
202                }
203                let Some(default) = prop.get("Default")
204                else {
205                    return Err(FirefoxBuilderError::InstallPath(section));
206                };
207                base.push(default);
208                break;
209            }
210        }
211
212        tracing::debug!("path: {:?}", base);
213
214        Ok(base)
215    }
216
217    pub async fn build(self) -> Result<FirefoxGetter<B>> {
218        let profile_path = if let Some(path) = self.profile_path {
219            path.into()
220        }
221        else {
222            self.get_profile_path().await?
223        };
224        let temp_paths = Self::cache_data(profile_path).await?;
225
226        let query = CookiesQuery::new(temp_paths.cookies_temp).await?;
227
228        Ok(FirefoxGetter {
229            cookies_query: query,
230            __browser: core::marker::PhantomData::<B>,
231        })
232    }
233}