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}