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