Skip to main content

leyline/core/
device.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5use url::Url;
6
7use crate::cookie::Jar;
8use crate::core::error::{Error, Kind, Result};
9use crate::core::{Identity, ProxyConfig, ProxyUrl, Session, SessionBuilder, Tab};
10use crate::profile::{BrowserProfile, ChromiumBrand, Platform};
11use crate::util::atomic::{FileMode, write_atomic};
12
13mod autosave;
14mod check;
15mod secret;
16mod state;
17mod version;
18mod wire;
19
20pub use autosave::{DeviceAutosave, DeviceAutosaveOptions};
21pub use state::SessionState;
22pub(crate) use state::{StateParts, unix_secs};
23pub(crate) use version::FileVersion;
24
25#[derive(Debug, Clone, Deserialize)]
26#[non_exhaustive]
27pub struct Device {
28    #[serde(default)]
29    pub identity: Option<Identity>,
30    pub platform: Platform,
31    #[serde(default)]
32    pub brand: Option<ChromiumBrand>,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub profile_toml: Option<String>,
35    #[serde(default)]
36    pub profile_id: Option<String>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub user_agent: Option<String>,
39    #[serde(default)]
40    pub proxy: Option<ProxyUrl>,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub proxy_password_env: Option<String>,
43    #[serde(default)]
44    pub languages: Option<Vec<String>>,
45    #[serde(default)]
46    pub env_proxy: bool,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub jar_path: Option<PathBuf>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub jar: Option<Jar>,
51    #[serde(default)]
52    pub state: SessionState,
53    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
54    pub app: BTreeMap<String, serde_json::Value>,
55    #[serde(default)]
56    pub strict: bool,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub page: Option<Url>,
59}
60
61impl Device {
62    #[must_use]
63    pub fn capture(session: &Session, proxy: Option<ProxyUrl>) -> Device {
64        let id = session.identity();
65        Device {
66            identity: id.to_identity(),
67            platform: id.platform(),
68            brand: id.brand(),
69            profile_toml: None,
70            profile_id: id.profile_id().map(str::to_owned),
71            user_agent: id.identity().map(|_| id.user_agent().to_owned()),
72            proxy,
73            proxy_password_env: None,
74            languages: id.languages().map(<[String]>::to_vec),
75            env_proxy: false,
76            jar_path: None,
77            jar: None,
78            state: session.state(),
79            app: BTreeMap::new(),
80            strict: false,
81            page: None,
82        }
83    }
84
85    pub fn pin_profile(&mut self, session: &Session) -> Result<()> {
86        let id = session.identity();
87        if id.to_identity().is_some_and(|i| i.http() != i.tls()) {
88            return Err(config(
89                "a session with a rotated TLS hello cannot pin one profile; keep the identity",
90            ));
91        }
92        let toml = id
93            .export_profile()
94            .ok_or_else(|| config("the session has no profile source to pin"))?;
95        self.profile_toml = Some(toml);
96        self.profile_id = id.profile_id().map(str::to_owned);
97        Ok(())
98    }
99
100    pub fn session_builder(&self) -> Result<SessionBuilder> {
101        let mut builder = match (&self.profile_toml, self.identity) {
102            (Some(toml), _) => Session::builder()
103                .profile(BrowserProfile::from_toml(toml).map_err(|e| config(e.to_string()))?)
104                .platform(self.platform),
105            (None, Some(identity)) => Session::builder().identity(identity),
106            (None, None) => Session::builder().platform(self.platform),
107        };
108        if let Some(brand) = self.brand {
109            builder = builder.brand(brand);
110        }
111        builder = builder.proxy(self.proxy_config()?);
112        if let Some(languages) = &self.languages {
113            builder = builder.languages(languages);
114        }
115        Ok(builder.cookie_jar(self.load_jar()?))
116    }
117
118    pub fn open(&self) -> Result<Session> {
119        let session = self.session_builder()?.build()?;
120        self.check(&session)?;
121        self.state.restore_into(&session);
122        Ok(session)
123    }
124
125    #[must_use]
126    pub fn tab(&self, session: &Session) -> Tab {
127        let tab = session.tab();
128        tab.set_current(self.page.clone());
129        tab
130    }
131
132    pub fn save_to(&self, path: impl AsRef<Path>) -> Result<()> {
133        let file = DeviceFileOut {
134            version: FileVersion::default(),
135            device: self,
136        };
137        let bytes = serde_json::to_vec_pretty(&file).map_err(Error::from_json)?;
138        write_atomic(path.as_ref(), &bytes, FileMode::Private)
139    }
140
141    pub fn load_from(path: impl AsRef<Path>) -> Result<Device> {
142        let bytes = std::fs::read(path)?;
143        let file: DeviceFileIn = serde_json::from_slice(&bytes).map_err(Error::from_json)?;
144        match file.version {
145            FileVersion::V1 => Ok(file.device),
146        }
147    }
148
149    fn proxy_config(&self) -> Result<ProxyConfig> {
150        Ok(self
151            .resolved_proxy()?
152            .map_or_else(ProxyConfig::new, ProxyConfig::from)
153            .env(self.env_proxy))
154    }
155
156    fn load_jar(&self) -> Result<Jar> {
157        if let Some(jar) = &self.jar {
158            return Ok(jar.clone());
159        }
160        match &self.jar_path {
161            Some(path) if path.exists() => Jar::load_from(path),
162            _ => Ok(Jar::new()),
163        }
164    }
165}
166
167#[derive(Serialize)]
168struct DeviceFileOut<'a> {
169    version: FileVersion,
170    #[serde(flatten)]
171    device: &'a Device,
172}
173
174#[derive(Deserialize)]
175struct DeviceFileIn {
176    version: FileVersion,
177    #[serde(flatten)]
178    device: Device,
179}
180
181fn config(message: impl Into<String>) -> Error {
182    Error::new(Kind::Config).with_message(message.into())
183}