Isolate CDP browser contexts
This commit is contained in:
1 parent
730bd01f57
commit
409ac781d2
6 files changed
+344
-41
No files matched your search
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user