decrypt_cookies/firefox/
builder.rs

1use std::{
2    fmt::Display,
3    marker::PhantomData,
4    path::{Path, PathBuf},
5};
6
7use snafu::{Location, OptionExt, ResultExt, Snafu};
8use tokio::fs;
9
10use super::FirefoxCookieGetter;
11use crate::{
12    firefox::{items::cookie::dao::CookiesQuery, FirefoxGetter},
13    prelude::FirefoxPath,
14};
15
16// TODO: add browser name in error
17#[derive(Debug)]
18#[derive(Snafu)]
19#[snafu(visibility(pub))]
20pub enum FirefoxBuilderError {
21    #[snafu(display(r#"Not found {}
22The browser is not installed or started with `-P`/`-profile` arg
23@:{location}"#, path.display()))]
24    NotFoundBase {
25        path: PathBuf,
26        #[snafu(implicit)]
27        location: Location,
28    },
29    #[snafu(display("{source}\n@:{location}"))]
30    Ini {
31        source: ini::Error,
32        #[snafu(implicit)]
33        location: Location,
34    },
35    #[snafu(display("{source}\n@:{location}"))]
36    IniParser {
37        source: ini::ParseError,
38        #[snafu(implicit)]
39        location: Location,
40    },
41    #[snafu(display("Profile {profile} missing `Name` properties\n@:{location}"))]
42    ProfilePath {
43        profile: String,
44        #[snafu(implicit)]
45        location: Location,
46    },
47    #[snafu(display("Install {install} missing `Default` properties\n@:{location}"))]
48    InstallPath {
49        install: String,
50        #[snafu(implicit)]
51        location: Location,
52    },
53    #[snafu(display("{source}\n@:{location}"))]
54    Db {
55        source: sea_orm::DbErr,
56        #[snafu(implicit)]
57        location: Location,
58    },
59    #[snafu(display("Io: {source}, path: {}\n@:{location}",path.display()))]
60    Io {
61        source: std::io::Error,
62        path: PathBuf,
63        #[snafu(implicit)]
64        location: Location,
65    },
66    #[snafu(display("Can not found home dir\n@:{location}"))]
67    Home {
68        #[snafu(implicit)]
69        location: Location,
70    },
71}
72
73pub type Result<T> = std::result::Result<T, FirefoxBuilderError>;
74
75/// The `to` must have parent dir
76async fn copy<A, A0>(from: A, to: A0) -> Result<()>
77where
78    A: AsRef<Path> + Send,
79    A0: AsRef<Path> + Send,
80{
81    let parent = to
82        .as_ref()
83        .parent()
84        .expect("Get parent dir failed");
85    fs::create_dir_all(parent)
86        .await
87        .with_context(|_| IoSnafu { path: parent.to_owned() })?;
88
89    let from = from.as_ref();
90    fs::copy(from, to.as_ref())
91        .await
92        .with_context(|_| IoSnafu { path: from.to_owned() })?;
93
94    Ok(())
95}
96
97#[derive(Clone)]
98#[derive(Debug)]
99#[derive(Default)]
100pub struct FirefoxBuilder<'a, T> {
101    pub(crate) base: Option<PathBuf>,
102    pub(crate) profile: Option<&'a str>,
103    pub(crate) profile_path: Option<PathBuf>,
104    pub(crate) __browser: PhantomData<T>,
105}
106
107impl<B: FirefoxPath> Display for FirefoxBuilder<'_, B> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.write_fmt(format_args!("{}Builder", B::NAME))
110    }
111}
112
113impl<'b, B: FirefoxPath> FirefoxBuilder<'b, B> {
114    pub const fn new() -> Self {
115        Self {
116            base: None,
117            profile: None,
118            profile_path: None,
119            __browser: core::marker::PhantomData::<B>,
120        }
121    }
122
123    /// `profile_path`: when browser started with `-profile <profile_path>`
124    /// When set `profile_path` ignore other parameters like `base`, `profile`.
125    pub fn with_profile_path(profile_path: PathBuf) -> Self {
126        Self {
127            base: None,
128            profile: None,
129            profile_path: profile_path.into(),
130            __browser: core::marker::PhantomData::<B>,
131        }
132    }
133
134    /// `base`: When Firefox data path changed
135    pub fn base(&mut self, base: PathBuf) -> &mut Self {
136        self.base = base.into();
137        self
138    }
139
140    /// `profile`: When started with `-P <profile>`
141    pub fn profile(&mut self, profile: &'b str) -> &mut Self {
142        self.profile = profile.into();
143        self
144    }
145
146    // async fn cache_data(profile_path: PathBuf) -> Result<TempPaths> {
147    //     let cookies = B::cookies(profile_path.clone());
148    //     let cookies_temp = B::cookies_temp().context(HomeSnafu)?;
149    //
150    //     let login_data = B::login_data(profile_path.clone());
151    //     let login_data_temp = B::login_data_temp().context(HomeSnafu)?;
152    //
153    //     let key = B::key(profile_path.clone());
154    //     let key_temp = B::key_temp().context(HomeSnafu)?;
155    //
156    //     let (ck, lg, k) = join!(
157    //         copy(&cookies, &cookies_temp),
158    //         copy(&login_data, &login_data_temp),
159    //         copy(&key, &key_temp)
160    //     );
161    //     ck?;
162    //     lg?;
163    //     k?;
164    //
165    //     Ok(TempPaths {
166    //         cookies_temp,
167    //         login_data_temp,
168    //         key_temp,
169    //     })
170    // }
171
172    async fn cache_cookies(profile_path: PathBuf) -> Result<CookiesQuery> {
173        let cookies = B::cookies(profile_path.clone());
174        let cookies_temp = B::cookies_temp().context(HomeSnafu)?;
175
176        copy(&cookies, &cookies_temp).await?;
177        CookiesQuery::new(cookies_temp)
178            .await
179            .context(DbSnafu)
180    }
181}
182
183impl<'b, B: FirefoxPath + Send + Sync> FirefoxBuilder<'b, B> {
184    /// Get user specify profile path
185    pub async fn get_profile_path(self) -> Result<PathBuf> {
186        let mut base = if let Some(base) = self.base {
187            base
188        }
189        else {
190            let mut home = dirs::home_dir().context(HomeSnafu)?;
191            home.push(B::BASE);
192            home
193        };
194        let ini_path = base.join("profiles.ini");
195
196        let ini_str = fs::read_to_string(&ini_path)
197            .await
198            .context(IoSnafu { path: ini_path })?;
199
200        let ini_file = ini::Ini::load_from_str(&ini_str).context(IniParserSnafu)?;
201        for (section, prop) in ini_file {
202            let Some(section) = section
203            else {
204                continue;
205            };
206            if let Some(profile) = self.profile {
207                if !section.starts_with("Profile") {
208                    continue;
209                }
210                let Some(profile_name) = prop.get("Name")
211                else {
212                    continue;
213                };
214                if profile_name == profile {
215                    let Some(var) = prop.get("Path")
216                    else {
217                        return Err(ProfilePathSnafu { profile: profile_name.to_owned() }.build());
218                    };
219                    base.push(var);
220                    break;
221                }
222            }
223            else if section.starts_with("Install") {
224                let Some(default) = prop.get("Default")
225                else {
226                    return Err(InstallPathSnafu { install: section }.build());
227                };
228                base.push(default);
229                break;
230            }
231        }
232
233        Ok(base)
234    }
235
236    #[cfg_attr(
237        feature = "tracing",
238        tracing::instrument(name = "Firefox build", skip(self), fields(browser), level = "debug")
239    )]
240    pub async fn build(self) -> Result<FirefoxGetter<B>> {
241        let profile_path = if let Some(path) = self.profile_path {
242            path
243        }
244        else {
245            self.get_profile_path().await?
246        };
247
248        #[cfg(feature = "tracing")]
249        {
250            tracing::Span::current().record("browser", B::NAME);
251            tracing::debug!(profile_path = %profile_path.display());
252        };
253
254        let cookies_query = Self::cache_cookies(profile_path).await?;
255
256        Ok(FirefoxGetter {
257            cookies_query,
258            __browser: core::marker::PhantomData::<B>,
259        })
260    }
261
262    #[cfg_attr(
263        feature = "tracing",
264        tracing::instrument(
265            name = "Firefox Cookie build",
266            skip(self),
267            fields(browser),
268            level = "debug"
269        )
270    )]
271    pub async fn build_cookie(self) -> Result<FirefoxCookieGetter<B>> {
272        let profile_path = if let Some(path) = self.profile_path {
273            path
274        }
275        else {
276            self.get_profile_path().await?
277        };
278
279        #[cfg(feature = "tracing")]
280        {
281            tracing::Span::current().record("browser", B::NAME);
282            tracing::debug!(profile_path = %profile_path.display());
283        };
284
285        let cookies_query = Self::cache_cookies(profile_path).await?;
286
287        Ok(FirefoxCookieGetter {
288            cookies_query,
289            __browser: core::marker::PhantomData::<B>,
290        })
291    }
292
293    #[deprecated(note = "use build_cookie")]
294    pub async fn build_cookies(self) -> Result<FirefoxCookieGetter<B>> {
295        self.build_cookie().await
296    }
297}