Skip to main content

rightkit_browser/
config.rs

1use crate::error::{BrowserError, Result};
2use crate::policy::{AdmissionHook, EventSink, NetworkPolicy};
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6/// Where the browser keeps cookies, storage, and logins.
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum ProfileSpec {
9    /// Throwaway profile deleted when the session drops (default).
10    Temporary,
11    /// Named persistent profile at `<root>/<name>`. One live session per name;
12    /// a second launch fails with `ProfileInUse` instead of corrupting state.
13    Named { root: PathBuf, name: String },
14}
15
16#[derive(Clone)]
17pub struct LaunchOptions {
18    pub headless: bool,
19    /// Passes `--mute-audio`. Default true.
20    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    /// Extra Chrome flags, with or without leading `--`.
26    pub extra_args: Vec<String>,
27    /// Defaults to `<profile>/downloads` for temporary profiles and
28    /// `<profile>/rightkit-downloads` for named ones.
29    pub download_dir: Option<PathBuf>,
30    /// When set, `upload` refuses files outside this root.
31    pub upload_root: Option<PathBuf>,
32    /// Gates every navigation, input, and script action before it has any effect.
33    /// `None` admits everything (trusted callers).
34    pub admission: Option<AdmissionHook>,
35    /// Network/SSRF policy. Default blocks loopback, link-local, private ranges,
36    /// `localhost`, and `file:` unless allowed.
37    pub network: NetworkPolicy,
38    /// Receives start/stop/crash/denied lifecycle events (session id, reason, time).
39    pub on_event: Option<EventSink>,
40    /// Windows: the caller's job object (see [`Self::windows_job`]). Chromium and every
41    /// helper it starts are bound to it; a clone of these options keeps the duplicate open.
42    #[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    /// Windows: bind Chromium to the caller's job object (an in-process host's own job).
99    /// Chromium joins it before running its first instruction, beneath RightKit's own
100    /// kill-on-close jobs, none of which allow breakaway: terminating or closing the
101    /// caller's job kills the browser and all of its helpers. The handle is duplicated; the
102    /// session closes its duplicate as soon as Chromium has been bound.
103    #[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
136/// System Chrome, Chromium, then Edge. `CHROME` env overrides.
137pub 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
171/// Chrome flags a caller may not pass: they would redirect the profile to the
172/// user's real browser data or expose the debugging endpoint outside this session.
173pub(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
195/// Refuse roots that are (or sit inside) a real browser's own user-data directory.
196pub(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}