rightkit_browser/
config.rs1use crate::error::{BrowserError, Result};
2use crate::policy::{AdmissionHook, EventSink, NetworkPolicy};
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum ProfileSpec {
9 Temporary,
11 Named { root: PathBuf, name: String },
14}
15
16#[derive(Clone)]
17pub struct LaunchOptions {
18 pub headless: bool,
19 pub mute_audio: bool,
21 pub profile: ProfileSpec,
22 pub chrome_path: Option<PathBuf>,
23 pub viewport: (u32, u32),
24 pub launch_timeout: Duration,
25 pub extra_args: Vec<String>,
27 pub download_dir: Option<PathBuf>,
30 pub upload_root: Option<PathBuf>,
32 pub admission: Option<AdmissionHook>,
35 pub network: NetworkPolicy,
38 pub on_event: Option<EventSink>,
40 #[cfg(windows)]
43 pub windows_job: Option<std::sync::Arc<std::os::windows::io::OwnedHandle>>,
44}
45
46impl std::fmt::Debug for LaunchOptions {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("LaunchOptions")
49 .field("headless", &self.headless)
50 .field("profile", &self.profile)
51 .field("network", &self.network)
52 .field("admission", &self.admission.is_some())
53 .finish_non_exhaustive()
54 }
55}
56
57impl Default for LaunchOptions {
58 fn default() -> Self {
59 Self {
60 headless: true,
61 mute_audio: true,
62 profile: ProfileSpec::Temporary,
63 chrome_path: None,
64 viewport: (1280, 800),
65 launch_timeout: Duration::from_secs(60),
66 extra_args: Vec::new(),
67 download_dir: None,
68 upload_root: None,
69 admission: None,
70 network: NetworkPolicy::default(),
71 on_event: None,
72 #[cfg(windows)]
73 windows_job: None,
74 }
75 }
76}
77
78impl LaunchOptions {
79 pub fn headed(mut self) -> Self {
80 self.headless = false;
81 self
82 }
83 pub fn admission(mut self, hook: AdmissionHook) -> Self {
84 self.admission = Some(hook);
85 self
86 }
87 pub fn network(mut self, policy: NetworkPolicy) -> Self {
88 self.network = policy;
89 self
90 }
91 pub fn on_event(
92 mut self,
93 f: impl Fn(&crate::policy::BrowserEvent) + Send + Sync + 'static,
94 ) -> Self {
95 self.on_event = Some(std::sync::Arc::new(f));
96 self
97 }
98 #[cfg(windows)]
104 pub fn windows_job(
105 mut self,
106 job: std::os::windows::io::BorrowedHandle<'_>,
107 ) -> std::io::Result<Self> {
108 self.windows_job = Some(std::sync::Arc::new(job.try_clone_to_owned()?));
109 Ok(self)
110 }
111 pub fn named_profile(mut self, root: impl Into<PathBuf>, name: impl Into<String>) -> Self {
112 self.profile = ProfileSpec::Named {
113 root: root.into(),
114 name: name.into(),
115 };
116 self
117 }
118}
119
120pub(crate) fn validate_profile_name(name: &str) -> Result<()> {
121 let ok = !name.is_empty()
122 && name.len() <= 64
123 && !name.starts_with('.')
124 && name
125 .chars()
126 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'));
127 if ok {
128 Ok(())
129 } else {
130 Err(BrowserError::Profile(format!(
131 "invalid profile name '{name}'"
132 )))
133 }
134}
135
136pub fn find_chrome() -> Option<PathBuf> {
138 if let Ok(p) = std::env::var("CHROME") {
139 let p = PathBuf::from(p);
140 if p.exists() {
141 return Some(p);
142 }
143 }
144 let candidates: &[&str] = if cfg!(target_os = "windows") {
145 &[
146 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
147 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
148 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
149 r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
150 ]
151 } else if cfg!(target_os = "macos") {
152 &[
153 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
154 "/Applications/Chromium.app/Contents/MacOS/Chromium",
155 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
156 ]
157 } else {
158 &[
159 "/usr/bin/google-chrome",
160 "/usr/bin/chromium",
161 "/usr/bin/chromium-browser",
162 "/usr/bin/microsoft-edge",
163 ]
164 };
165 candidates
166 .iter()
167 .map(PathBuf::from)
168 .find(|p| Path::new(p).exists())
169}
170
171pub(crate) fn validate_extra_args(args: &[String]) -> Result<()> {
174 const FORBIDDEN: &[&str] = &[
175 "user-data-dir",
176 "profile-directory",
177 "remote-debugging-port",
178 "remote-debugging-address",
179 "remote-debugging-pipe",
180 "remote-allow-origins",
181 "disable-web-security",
182 "incognito-bypass",
183 ];
184 for a in args {
185 let name = a.trim_start_matches('-').split('=').next().unwrap_or("");
186 if FORBIDDEN.contains(&name) {
187 return Err(BrowserError::Invalid(format!(
188 "chrome flag '--{name}' is managed by the session"
189 )));
190 }
191 }
192 Ok(())
193}
194
195pub(crate) fn reject_real_browser_profile(root: &Path) -> Result<()> {
197 let mut real: Vec<PathBuf> = Vec::new();
198 if let Some(home) = std::env::var_os("HOME")
199 .or_else(|| std::env::var_os("USERPROFILE"))
200 .map(PathBuf::from)
201 {
202 for rel in [
203 "Library/Application Support/Google/Chrome",
204 "Library/Application Support/Chromium",
205 "Library/Application Support/Microsoft Edge",
206 ".config/google-chrome",
207 ".config/chromium",
208 ".config/microsoft-edge",
209 ] {
210 real.push(home.join(rel));
211 }
212 }
213 if let Some(local) = std::env::var_os("LOCALAPPDATA").map(PathBuf::from) {
214 for rel in [
215 r"Google\Chrome\User Data",
216 r"Chromium\User Data",
217 r"Microsoft\Edge\User Data",
218 ] {
219 real.push(local.join(rel));
220 }
221 }
222 std::fs::create_dir_all(root)?;
223 let root = root.canonicalize()?;
224 for r in real {
225 if let Ok(r) = r.canonicalize() {
226 if root.starts_with(&r) {
227 return Err(BrowserError::Profile(
228 "refusing to use the user's real browser profile".into(),
229 ));
230 }
231 }
232 }
233 Ok(())
234}