Isolate CDP browser contexts

This commit is contained in:
SGavrl committed 2026-07-22 10:45:36 +00:00
1 parent 730bd01f57
commit 409ac781d2
6 files changed
+344 -41

No files matched your search

+5 -1
View File
@@ -236,7 +236,11 @@ async fn do_navigate(
// any file the obscura process can read. Opt in via
// `obscura serve --allow-file-access` when local-HTML
// testing is the intended workflow.
if url_is_file_scheme(url) && !ctx.default_context.allow_file_access {
let allow_file_access = ctx
.get_session_page(session_id)
.map(|page| page.context.allow_file_access)
.unwrap_or(ctx.default_context.allow_file_access);
if url_is_file_scheme(url) && !allow_file_access {
return Err(
"Page.navigate to file:// is disabled. Restart with `obscura serve --allow-file-access` to enable.".to_string()
);
+21 -4
View File
@@ -4,28 +4,45 @@ use crate::cookie_params::{parse_cdp_cookie, parse_delete_cookies_params};
use crate::dispatch::CdpContext;
use crate::domains::network::cookie_info_to_cdp_json;
fn cookie_jar_for(
ctx: &CdpContext,
params: &Value,
session_id: &Option<String>,
) -> Result<std::sync::Arc<obscura_net::CookieJar>, String> {
match params.get("browserContextId").and_then(|value| value.as_str()) {
Some(id) => ctx
.browser_context(id)
.map(|context| context.cookie_jar.clone())
.ok_or_else(|| format!("Browser context not found: {}", id)),
None => Ok(ctx
.get_session_page(session_id)
.map(|page| page.context.cookie_jar.clone())
.unwrap_or_else(|| ctx.default_context.cookie_jar.clone())),
}
}
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
_session_id: &Option<String>,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"getCookies" => {
let cookies = ctx.default_context.cookie_jar.get_all_cookies();
let cookies = cookie_jar_for(ctx, params, session_id)?.get_all_cookies();
let cdp_cookies: Vec<Value> = cookies.iter().map(cookie_info_to_cdp_json).collect();
Ok(json!({ "cookies": cdp_cookies }))
}
"setCookies" => {
if let Some(cookies) = params.get("cookies").and_then(|v| v.as_array()) {
let parsed: Vec<_> = cookies.iter().filter_map(parse_cdp_cookie).collect();
ctx.default_context.cookie_jar.set_cookies_from_cdp(parsed);
cookie_jar_for(ctx, params, session_id)?.set_cookies_from_cdp(parsed);
}
Ok(json!({}))
}
"deleteCookies" => {
if let Some(filter) = parse_delete_cookies_params(params) {
ctx.default_context.cookie_jar.delete_cookies_filtered(
cookie_jar_for(ctx, params, session_id)?.delete_cookies_filtered(
&filter.name,
&filter.domain,
filter.path.as_deref(),
+82 -6
View File
@@ -59,18 +59,25 @@ pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Resul
}
"createTarget" => {
let url = params.get("url").and_then(|v| v.as_str()).unwrap_or("about:blank");
let context_id = params.get("browserContextId").and_then(|v| v.as_str());
let context = match context_id {
Some(id) => ctx
.browser_context(id)
.ok_or_else(|| format!("Browser context not found: {}", id))?,
None => &ctx.default_context,
};
// Same gate as Page.navigate (GHSA-q55h-vfv9-qcr5). Without this,
// a CDP client can call Target.createTarget {url:"file:///etc/passwd"}
// and then Runtime.evaluate the body off the created target,
// bypassing the page-domain check entirely.
if url_is_file_scheme(url) && !ctx.default_context.allow_file_access {
if url_is_file_scheme(url) && !context.allow_file_access {
return Err(
"Target.createTarget to file:// is disabled. Restart with `obscura serve --allow-file-access` to enable.".to_string()
);
}
let page_id = ctx.create_page();
let page_id = ctx.create_page_in_context(context_id)?;
let session_id = format!("{}-session", page_id);
if let Some(page) = ctx.get_page_mut(&page_id) {
@@ -200,14 +207,41 @@ pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Resul
"detachFromTarget" => Ok(json!({})),
"activateTarget" => Ok(json!({})),
"getBrowserContexts" => {
Ok(json!({ "browserContextIds": [ctx.default_context.id] }))
let mut ids: Vec<&String> = ctx.browser_contexts.keys().collect();
ids.sort();
Ok(json!({ "browserContextIds": ids }))
}
"createBrowserContext" => {
ctx.default_context.cookie_jar.clear();
Ok(json!({ "browserContextId": ctx.default_context.id }))
let id = ctx.create_browser_context();
Ok(json!({ "browserContextId": id }))
}
"disposeBrowserContext" => {
ctx.default_context.cookie_jar.clear();
let context_id = params
.get("browserContextId")
.and_then(|v| v.as_str())
.ok_or("browserContextId required")?;
let sessions: Vec<(String, String)> = ctx
.sessions
.iter()
.filter_map(|(session_id, page_id)| {
ctx.get_page(page_id)
.filter(|page| page.context.id == context_id)
.map(|_| (session_id.clone(), page_id.clone()))
})
.collect();
let page_ids = ctx.dispose_browser_context(context_id)?;
for (session_id, page_id) in sessions {
ctx.pending_events.push(CdpEvent::new(
"Target.detachedFromTarget",
json!({ "sessionId": session_id, "targetId": page_id }),
));
}
for page_id in page_ids {
ctx.pending_events.push(CdpEvent::new(
"Target.targetDestroyed",
json!({ "targetId": page_id }),
));
}
Ok(json!({}))
}
"getTargetInfo" => {
@@ -252,6 +286,48 @@ pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Resul
mod tests {
use super::*;
#[tokio::test]
async fn browser_contexts_are_real_and_do_not_clear_default_cookies() {
let mut ctx = CdpContext::new();
ctx.default_context.cookie_jar.set_cookie(
"sid=default",
&url::Url::parse("https://example.com").unwrap(),
);
let created = handle("createBrowserContext", &json!({}), &mut ctx)
.await
.expect("context creation should succeed");
let context_id = created["browserContextId"].as_str().unwrap();
assert_ne!(context_id, "default");
assert!(ctx.browser_context(context_id).unwrap().cookie_jar.get_all_cookies().is_empty());
assert_eq!(ctx.default_context.cookie_jar.get_all_cookies().len(), 1);
let listed = handle("getBrowserContexts", &json!({}), &mut ctx)
.await
.expect("context listing should succeed");
assert_eq!(listed["browserContextIds"], json!([context_id]));
}
#[tokio::test]
async fn disposing_context_removes_only_its_pages() {
let mut ctx = CdpContext::new();
let context_id = ctx.create_browser_context();
let isolated_page = ctx.create_page_in_context(Some(&context_id)).unwrap();
let default_page = ctx.create_page();
handle(
"disposeBrowserContext",
&json!({"browserContextId": context_id}),
&mut ctx,
)
.await
.expect("context disposal should succeed");
assert!(ctx.get_page(&isolated_page).is_none());
assert!(ctx.get_page(&default_page).is_some());
assert!(ctx.browser_contexts.is_empty());
}
#[tokio::test]
async fn attach_to_browser_target_returns_session_id() {
let mut ctx = CdpContext::new();