Initial Release

This commit is contained in:
h4ckf0r0day committed 2026-04-13 12:30:28 +02:00
1 parent 2e5d63cc35
commit 8749725b45
52 files changed
+19804

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
use serde_json::{json, Value};
pub async fn handle(method: &str, _params: &Value) -> Result<Value, String> {
match method {
"getVersion" => Ok(json!({
"protocolVersion": "1.3",
"product": "Obscura/0.1.0",
"revision": "0",
"userAgent": "Obscura/0.1.0 (Headless Browser)",
"jsVersion": "N/A",
})),
"close" => {
Ok(json!({}))
}
"getWindowForTarget" => Ok(json!({
"windowId": 1,
"bounds": {
"left": 0,
"top": 0,
"width": 1280,
"height": 720,
"windowState": "normal",
}
})),
"setDownloadBehavior" => Ok(json!({})),
"getWindowBounds" => Ok(json!({
"bounds": { "left": 0, "top": 0, "width": 1280, "height": 720, "windowState": "normal" }
})),
_ => Err(format!("Unknown Browser method: {}", method)),
}
}
+204
View File
@@ -0,0 +1,204 @@
use obscura_dom::{DomTree, NodeData, NodeId};
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"enable" => Ok(json!({})),
"getDocument" => {
let page = ctx.get_session_page(session_id).ok_or("No page")?;
let depth = params.get("depth").and_then(|v| v.as_i64()).unwrap_or(2);
page.with_dom(|dom| {
let node = serialize_node(dom, dom.document(), depth as u32, 0);
json!({ "root": node })
}).ok_or_else(|| "No DOM loaded".to_string())
}
"querySelector" => {
let page = ctx.get_session_page(session_id).ok_or("No page")?;
let selector = params.get("selector").and_then(|v| v.as_str()).ok_or("selector required")?;
let result = page.with_dom(|dom| {
dom.query_selector(selector).ok().flatten().map(|id| id.index()).unwrap_or(0)
}).unwrap_or(0);
Ok(json!({ "nodeId": result }))
}
"querySelectorAll" => {
let page = ctx.get_session_page(session_id).ok_or("No page")?;
let selector = params.get("selector").and_then(|v| v.as_str()).ok_or("selector required")?;
let ids = page.with_dom(|dom| {
dom.query_selector_all(selector).ok()
.map(|ids| ids.iter().map(|id| id.index() as u64).collect::<Vec<_>>())
.unwrap_or_default()
}).unwrap_or_default();
Ok(json!({ "nodeIds": ids }))
}
"getOuterHTML" => {
let page = ctx.get_session_page(session_id).ok_or("No page")?;
let node_id = params.get("nodeId").and_then(|v| v.as_u64())
.or_else(|| params.get("backendNodeId").and_then(|v| v.as_u64()))
.ok_or("nodeId required")?;
let html = page.with_dom(|dom| {
dom.outer_html(NodeId::new(node_id as u32))
}).unwrap_or_default();
Ok(json!({ "outerHTML": html }))
}
"describeNode" => {
let page = ctx.get_session_page_mut(session_id).ok_or("No page")?;
let depth = params.get("depth").and_then(|v| v.as_i64()).unwrap_or(0);
let node_id = if let Some(nid) = params.get("nodeId").and_then(|v| v.as_u64())
.or_else(|| params.get("backendNodeId").and_then(|v| v.as_u64()))
{
nid
} else if let Some(oid) = params.get("objectId").and_then(|v| v.as_str()) {
let escaped_oid = oid.replace('\\', "\\\\").replace('\'', "\\'");
let code = format!(
"(function() {{ var o = globalThis.__obscura_objects['{}']; if (!o) return -1; return (typeof o._nid === 'number') ? o._nid : -1; }})()",
escaped_oid
);
let result = page.evaluate(&code);
result.as_f64().map(|n| n as u64).unwrap_or(0)
} else {
return Err("nodeId or objectId required".to_string());
};
let node = page.with_dom(|dom| {
serialize_node(dom, NodeId::new(node_id as u32), depth as u32, 0)
}).unwrap_or(json!(null));
Ok(json!({ "node": node }))
}
"resolveNode" => {
let page = ctx.get_session_page_mut(session_id).ok_or("No page")?;
let node_id = if let Some(nid) = params.get("nodeId").and_then(|v| v.as_u64())
.or_else(|| params.get("backendNodeId").and_then(|v| v.as_u64()))
{
nid
} else if let Some(oid) = params.get("objectId").and_then(|v| v.as_str()) {
let code = format!(
"(function() {{ var o = globalThis.__obscura_objects['{}']; return (o && typeof o._nid === 'number') ? o._nid : -1; }})()",
oid
);
let result = page.evaluate(&code);
result.as_f64().map(|n| n as u64).unwrap_or(0)
} else {
return Err("nodeId or objectId required".to_string());
};
let js_code = format!(
"(function() {{\
var nid = {};\
var node = null;\
if (globalThis._cache && globalThis._cache.has(nid)) {{\
node = globalThis._cache.get(nid);\
}} else {{\
var t = +Deno.core.ops.op_dom('node_type', String(nid), '');\
if (t === 1) node = new Element(nid);\
else if (t === 9) node = globalThis.document;\
else node = new Node(nid);\
if (globalThis._cache) globalThis._cache.set(nid, node);\
}}\
return node;\
}})()",
node_id,
);
let info = if let Some(js) = &mut page.js {
match js.store_object_with_meta(&js_code) {
Ok(info) => info,
Err(_) => {
return Ok(json!({
"object": {
"type": "object",
"subtype": "node",
"className": "HTMLElement",
"objectId": format!("node-{}", node_id),
}
}));
}
}
} else {
return Err("No JS runtime".to_string());
};
Ok(json!({
"object": {
"type": "object",
"subtype": "node",
"className": if info.class_name.is_empty() { "HTMLElement".to_string() } else { info.class_name },
"description": info.description,
"objectId": info.object_id.unwrap_or_else(|| format!("node-{}", node_id)),
}
}))
}
"setAttributeValue" => Ok(json!({})),
"removeNode" => Ok(json!({})),
"getBoxModel" => {
Ok(json!({
"model": {
"content": [8,8, 108,8, 108,28, 8,28],
"padding": [8,8, 108,8, 108,28, 8,28],
"border": [8,8, 108,8, 108,28, 8,28],
"margin": [0,0, 116,0, 116,36, 0,36],
"width": 100, "height": 20,
}
}))
}
_ => Err(format!("Unknown DOM method: {}", method)),
}
}
fn serialize_node(dom: &DomTree, node_id: NodeId, max_depth: u32, current_depth: u32) -> Value {
let node = match dom.get_node(node_id) {
Some(n) => n,
None => return json!(null),
};
let children_ids = dom.children(node_id);
let child_count = children_ids.len();
let mut result = json!({ "nodeId": node_id.index(), "backendNodeId": node_id.index(), "childNodeCount": child_count });
match &node.data {
NodeData::Document => {
result["nodeType"] = json!(9); result["nodeName"] = json!("#document");
result["localName"] = json!(""); result["nodeValue"] = json!("");
result["documentURL"] = json!(""); result["baseURL"] = json!(""); result["xmlVersion"] = json!("");
}
NodeData::Doctype { name, public_id, system_id } => {
result["nodeType"] = json!(10); result["nodeName"] = json!(name);
result["localName"] = json!(""); result["nodeValue"] = json!("");
result["publicId"] = json!(public_id); result["systemId"] = json!(system_id);
}
NodeData::Element { name, attrs, .. } => {
result["nodeType"] = json!(1);
result["nodeName"] = json!(name.local.as_ref().to_ascii_uppercase());
result["localName"] = json!(name.local.as_ref());
result["nodeValue"] = json!("");
let cdp_attrs: Vec<String> = attrs.iter()
.flat_map(|a| vec![a.name.local.to_string(), a.value.clone()]).collect();
result["attributes"] = json!(cdp_attrs);
}
NodeData::Text { contents } => {
result["nodeType"] = json!(3); result["nodeName"] = json!("#text");
result["localName"] = json!(""); result["nodeValue"] = json!(contents);
}
NodeData::Comment { contents } => {
result["nodeType"] = json!(8); result["nodeName"] = json!("#comment");
result["localName"] = json!(""); result["nodeValue"] = json!(contents);
}
NodeData::ProcessingInstruction { target, data } => {
result["nodeType"] = json!(7); result["nodeName"] = json!(target);
result["localName"] = json!(""); result["nodeValue"] = json!(data);
}
}
if current_depth < max_depth && !children_ids.is_empty() {
let children: Vec<Value> = children_ids.iter()
.map(|&cid| serialize_node(dom, cid, max_depth, current_depth + 1)).collect();
result["children"] = json!(children);
}
result
}
+189
View File
@@ -0,0 +1,189 @@
use std::collections::HashMap;
use std::sync::Arc;
use serde_json::{json, Value};
use tokio::sync::Mutex;
use crate::dispatch::CdpContext;
use crate::types::CdpEvent;
pub struct PausedRequest {
pub request_id: String,
pub url: String,
pub method: String,
pub headers: HashMap<String, String>,
pub resource_type: String,
pub resolver: tokio::sync::oneshot::Sender<FetchResolution>,
}
pub enum FetchResolution {
Continue {
url: Option<String>,
method: Option<String>,
headers: Option<HashMap<String, String>>,
post_data: Option<String>,
},
Fulfill {
status: u16,
headers: Vec<(String, String)>,
body: String,
},
Fail {
reason: String,
},
}
pub struct FetchInterceptState {
pub enabled: bool,
pub patterns: Vec<String>,
pub paused: HashMap<String, PausedRequest>,
request_counter: u64,
}
impl FetchInterceptState {
pub fn new() -> Self {
FetchInterceptState {
enabled: false,
patterns: Vec::new(),
paused: HashMap::new(),
request_counter: 0,
}
}
pub fn next_request_id(&mut self) -> String {
self.request_counter += 1;
format!("interception-{}", self.request_counter)
}
}
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"enable" => {
let patterns = params
.get("patterns")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|p| {
p.get("urlPattern")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.collect::<Vec<_>>()
})
.unwrap_or_else(|| vec!["*".to_string()]);
ctx.fetch_intercept.enabled = true;
ctx.fetch_intercept.patterns = patterns.clone();
let tx_clone = ctx.intercept_tx.clone();
if let Some(page) = ctx.get_session_page_mut(session_id) {
page.intercept_enabled = false;
page.intercept_block_patterns.clear();
if let Some(tx) = tx_clone {
page.set_intercept_tx(tx);
}
}
tracing::info!("Fetch interception enabled");
Ok(json!({}))
}
"disable" => {
ctx.fetch_intercept.enabled = false;
ctx.fetch_intercept.patterns.clear();
if let Some(page) = ctx.get_session_page_mut(session_id) {
page.intercept_enabled = false;
page.intercept_block_patterns.clear();
}
let paused: Vec<_> = ctx.fetch_intercept.paused.drain().collect();
for (_, req) in paused {
let _ = req.resolver.send(FetchResolution::Continue {
url: None,
method: None,
headers: None,
post_data: None,
});
}
Ok(json!({}))
}
"continueRequest" => {
let request_id = params
.get("requestId")
.and_then(|v| v.as_str())
.ok_or("requestId required")?;
if let Some(paused) = ctx.fetch_intercept.paused.remove(request_id) {
let _ = paused.resolver.send(FetchResolution::Continue {
url: params.get("url").and_then(|v| v.as_str()).map(|s| s.to_string()),
method: params.get("method").and_then(|v| v.as_str()).map(|s| s.to_string()),
headers: None,
post_data: params.get("postData").and_then(|v| v.as_str()).map(|s| s.to_string()),
});
}
Ok(json!({}))
}
"fulfillRequest" => {
let request_id = params
.get("requestId")
.and_then(|v| v.as_str())
.ok_or("requestId required")?;
let status = params
.get("responseCode")
.and_then(|v| v.as_u64())
.unwrap_or(200) as u16;
let headers: HashMap<String, String> = params
.get("responseHeaders")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|h| {
let name = h.get("name")?.as_str()?.to_string();
let value = h.get("value")?.as_str()?.to_string();
Some((name, value))
})
.collect()
})
.unwrap_or_default();
let body = params
.get("body")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if let Some(paused) = ctx.fetch_intercept.paused.remove(request_id) {
let _ = paused.resolver.send(FetchResolution::Fulfill {
status,
headers: headers.into_iter().collect(),
body,
});
}
Ok(json!({}))
}
"failRequest" => {
let request_id = params
.get("requestId")
.and_then(|v| v.as_str())
.ok_or("requestId required")?;
let reason = params
.get("errorReason")
.and_then(|v| v.as_str())
.unwrap_or("Failed")
.to_string();
if let Some(paused) = ctx.fetch_intercept.paused.remove(request_id) {
let _ = paused.resolver.send(FetchResolution::Fail { reason });
}
Ok(json!({}))
}
"getResponseBody" => {
Ok(json!({ "body": "", "base64Encoded": false }))
}
_ => Err(format!("Unknown Fetch method: {}", method)),
}
}
+155
View File
@@ -0,0 +1,155 @@
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"dispatchMouseEvent" => {
let event_type = params.get("type").and_then(|v| v.as_str()).unwrap_or("");
let x = params.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
let y = params.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
let button = params.get("button").and_then(|v| v.as_str()).unwrap_or("left");
let click_count = params.get("clickCount").and_then(|v| v.as_u64()).unwrap_or(1);
if event_type == "mousePressed" {
if let Some(page) = ctx.get_session_page_mut(session_id) {
let code = format!(
"(function() {{\
var target = globalThis.__obscura_click_target || document.activeElement || document.body;\
if (!target) return;\
var evt = new MouseEvent('mousedown', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}});\
target.dispatchEvent(evt);\
var click = new MouseEvent('click', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}});\
var cancelled = !target.dispatchEvent(click);\
if (!cancelled) {{\
var link = target.closest ? target.closest('a[href]') : null;\
if (!link && target.tagName === 'A' && target.getAttribute('href')) link = target;\
if (link) {{\
var href = link.getAttribute('href');\
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {{\
location.assign(href);\
}}\
}}\
}}\
}})()",
x = x, y = y,
);
page.evaluate(&code);
}
} else if event_type == "mouseReleased" {
if let Some(page) = ctx.get_session_page_mut(session_id) {
let code = format!(
"(function() {{\
var target = globalThis.__obscura_click_target || document.activeElement || document.body;\
if (!target) return;\
var evt = new MouseEvent('mouseup', {{bubbles:true,cancelable:true,clientX:{x},clientY:{y},button:0}});\
target.dispatchEvent(evt);\
}})()",
x = x, y = y,
);
page.evaluate(&code);
}
}
Ok(json!({}))
}
"dispatchKeyEvent" => {
let event_type = params.get("type").and_then(|v| v.as_str()).unwrap_or("");
let key = params.get("key").and_then(|v| v.as_str()).unwrap_or("");
let code = params.get("code").and_then(|v| v.as_str()).unwrap_or("");
let text = params.get("text").and_then(|v| v.as_str()).unwrap_or("");
if let Some(page) = ctx.get_session_page_mut(session_id) {
match event_type {
"keyDown" | "rawKeyDown" => {
let js = format!(
"(function() {{\
var target = document.activeElement || document.body;\
var evt = new KeyboardEvent('keydown', {{bubbles:true,cancelable:true,key:'{key}',code:'{code}'}});\
target.dispatchEvent(evt);\
}})()",
key = key.replace('\'', "\\'"),
code = code.replace('\'', "\\'"),
);
page.evaluate(&js);
if !text.is_empty() && text != "\r" && text != "\n" {
let js = format!(
"(function() {{\
var target = document.activeElement;\
if (target && (target.localName === 'input' || target.localName === 'textarea')) {{\
target.value = (target.value || '') + '{text}';\
target.dispatchEvent(new Event('input', {{bubbles:true}}));\
}}\
}})()",
text = text.replace('\'', "\\'").replace('\\', "\\\\"),
);
page.evaluate(&js);
}
if key == "Enter" {
let js = "(function() {\
var target = document.activeElement;\
if (target) {\
target.dispatchEvent(new KeyboardEvent('keypress', {bubbles:true,key:'Enter',code:'Enter'}));\
var form = target.form || target.closest && target.closest('form');\
if (form && typeof form.submit === 'function') form.submit();\
}\
})()";
page.evaluate(js);
}
if key == "Backspace" {
let js = "(function() {\
var target = document.activeElement;\
if (target && (target.localName === 'input' || target.localName === 'textarea')) {\
target.value = target.value.slice(0, -1);\
target.dispatchEvent(new Event('input', {bubbles:true}));\
}\
})()";
page.evaluate(js);
}
}
"keyUp" => {
let js = format!(
"(function() {{\
var target = document.activeElement || document.body;\
var evt = new KeyboardEvent('keyup', {{bubbles:true,key:'{key}',code:'{code}'}});\
target.dispatchEvent(evt);\
}})()",
key = key.replace('\'', "\\'"),
code = code.replace('\'', "\\'"),
);
page.evaluate(&js);
}
"char" => {
if !text.is_empty() {
let js = format!(
"(function() {{\
var target = document.activeElement;\
if (target && (target.localName === 'input' || target.localName === 'textarea')) {{\
target.value = (target.value || '') + '{text}';\
target.dispatchEvent(new Event('input', {{bubbles:true}}));\
}}\
}})()",
text = text.replace('\'', "\\'").replace('\\', "\\\\"),
);
page.evaluate(&js);
}
}
_ => {}
}
}
Ok(json!({}))
}
"dispatchTouchEvent" => Ok(json!({})),
"setIgnoreInputEvents" => Ok(json!({})),
_ => Err(format!("Unknown Input method: {}", method)),
}
}
+85
View File
@@ -0,0 +1,85 @@
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn handle(
method: &str,
_params: &Value,
ctx: &mut CdpContext,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"getMarkdown" => {
let page = ctx.get_session_page_mut(session_id).ok_or("No page")?;
let code = r#"
(function() {
function toMd(el, depth) {
if (!el) return '';
var out = '';
if (el.nodeType === 3) return el.textContent || '';
if (el.nodeType !== 1) return '';
var tag = (el.tagName || '').toLowerCase();
var children = '';
var cn = el.childNodes || [];
for (var i = 0; i < cn.length; i++) children += toMd(cn[i], depth);
children = children.replace(/\n{3,}/g, '\n\n');
switch(tag) {
case 'h1': return '\n# ' + children.trim() + '\n\n';
case 'h2': return '\n## ' + children.trim() + '\n\n';
case 'h3': return '\n### ' + children.trim() + '\n\n';
case 'h4': return '\n#### ' + children.trim() + '\n\n';
case 'h5': return '\n##### ' + children.trim() + '\n\n';
case 'h6': return '\n###### ' + children.trim() + '\n\n';
case 'p': return '\n' + children.trim() + '\n\n';
case 'br': return '\n';
case 'hr': return '\n---\n\n';
case 'strong': case 'b': return '**' + children + '**';
case 'em': case 'i': return '*' + children + '*';
case 'code': return '`' + children + '`';
case 'pre': return '\n```\n' + children + '\n```\n\n';
case 'blockquote': return '\n> ' + children.trim().replace(/\n/g, '\n> ') + '\n\n';
case 'a':
var href = el.getAttribute('href') || '';
if (href && children.trim()) return '[' + children.trim() + '](' + href + ')';
return children;
case 'img':
var src = el.getAttribute('src') || '';
var alt = el.getAttribute('alt') || '';
return '![' + alt + '](' + src + ')';
case 'ul': case 'ol':
return '\n' + children + '\n';
case 'li':
var parent = el.parentNode;
var isOrdered = parent && parent.tagName && parent.tagName.toLowerCase() === 'ol';
var bullet = isOrdered ? '1. ' : '- ';
return bullet + children.trim() + '\n';
case 'table': return '\n' + children + '\n';
case 'thead': case 'tbody': case 'tfoot': return children;
case 'tr':
var cells = [];
var tds = el.childNodes || [];
for (var j = 0; j < tds.length; j++) {
if (tds[j].nodeType === 1) cells.push(toMd(tds[j], depth).trim());
}
return '| ' + cells.join(' | ') + ' |\n';
case 'th': case 'td': return children;
case 'script': case 'style': case 'noscript': case 'link': case 'meta': return '';
case 'div': case 'section': case 'article': case 'main': case 'aside': case 'nav': case 'header': case 'footer':
return '\n' + children;
case 'span': return children;
default: return children;
}
}
var body = document.body || document.documentElement;
var md = toMd(body, 0);
md = md.replace(/\n{3,}/g, '\n\n').trim();
return md;
})()
"#;
let result = page.evaluate(code);
let markdown = result.as_str().unwrap_or("").to_string();
Ok(json!({ "markdown": markdown }))
}
_ => Err(format!("Unknown LP method: {}", method)),
}
}
+10
View File
@@ -0,0 +1,10 @@
pub mod target;
pub mod browser;
pub mod page;
pub mod dom;
pub mod runtime;
pub mod network;
pub mod fetch;
pub mod input;
pub mod storage;
pub mod lp;
+81
View File
@@ -0,0 +1,81 @@
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"enable" => Ok(json!({})),
"setExtraHTTPHeaders" => {
let headers = params.get("headers").and_then(|v| v.as_object());
if let Some(page) = ctx.get_session_page(session_id) {
if let Some(headers) = headers {
let header_map: std::collections::HashMap<String, String> = headers
.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
.collect();
page.http_client.set_extra_headers(header_map).await;
}
}
Ok(json!({}))
}
"setUserAgentOverride" => {
let ua = params.get("userAgent").and_then(|v| v.as_str()).unwrap_or("");
if let Some(page) = ctx.get_session_page(session_id) {
page.http_client.set_user_agent(ua).await;
}
Ok(json!({}))
}
"getCookies" => {
let page = ctx.get_session_page(session_id).ok_or("No page")?;
let cookies = page.context.cookie_jar.get_all_cookies();
let cdp_cookies: Vec<Value> = cookies.iter().map(|c| {
json!({
"name": c.name,
"value": c.value,
"domain": c.domain,
"path": c.path,
"expires": -1,
"size": c.name.len() + c.value.len(),
"httpOnly": c.http_only,
"secure": c.secure,
"session": true,
"sameParty": false,
"sourceScheme": "Secure",
"sourcePort": 443,
})
}).collect();
Ok(json!({ "cookies": cdp_cookies }))
}
"setCookies" => {
let page = ctx.get_session_page(session_id).ok_or("No page")?;
if let Some(cookies) = params.get("cookies").and_then(|v| v.as_array()) {
let cookie_infos: Vec<obscura_net::CookieInfo> = cookies.iter().filter_map(|c| {
Some(obscura_net::CookieInfo {
name: c.get("name")?.as_str()?.to_string(),
value: c.get("value")?.as_str()?.to_string(),
domain: c.get("domain").and_then(|v| v.as_str()).unwrap_or("").to_string(),
path: c.get("path").and_then(|v| v.as_str()).unwrap_or("/").to_string(),
secure: c.get("secure").and_then(|v| v.as_bool()).unwrap_or(false),
http_only: c.get("httpOnly").and_then(|v| v.as_bool()).unwrap_or(false),
})
}).collect();
page.context.cookie_jar.set_cookies_from_cdp(cookie_infos);
}
Ok(json!({}))
}
"clearBrowserCookies" => {
if let Some(page) = ctx.get_session_page(session_id) {
page.context.cookie_jar.clear();
}
Ok(json!({}))
}
"setCacheDisabled" => Ok(json!({})),
"setRequestInterception" => Ok(json!({})),
_ => Err(format!("Unknown Network method: {}", method)),
}
}
+221
View File
@@ -0,0 +1,221 @@
use obscura_browser::lifecycle::WaitUntil;
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
use crate::types::CdpEvent;
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"enable" => Ok(json!({})),
"navigate" => {
let url = params.get("url").and_then(|v| v.as_str())
.ok_or("url required")?;
let wait_until = params.get("waitUntil")
.and_then(|v| {
if let Some(s) = v.as_str() {
Some(WaitUntil::from_str(s))
} else if let Some(arr) = v.as_array() {
arr.iter()
.filter_map(|item| item.as_str())
.map(WaitUntil::from_str)
.max_by_key(|w| match w {
WaitUntil::DomContentLoaded => 0,
WaitUntil::Load => 1,
WaitUntil::NetworkIdle2 => 2,
WaitUntil::NetworkIdle0 => 3,
})
} else {
None
}
})
.unwrap_or(WaitUntil::Load);
let preload_scripts: Vec<String> = ctx.preload_scripts.iter().map(|(_, s)| s.clone()).collect();
let (frame_id, loader_id, network_events, page_url, page_id, reached_network_idle) = {
let page = ctx.get_session_page_mut(session_id).ok_or("No page for session")?;
let frame_id = page.frame_id.clone();
let loader_id = format!("loader-{}", uuid::Uuid::new_v4());
let nav_method = params.get("__method").and_then(|v| v.as_str()).unwrap_or("GET");
let nav_body = params.get("__body").and_then(|v| v.as_str()).unwrap_or("");
if nav_method == "POST" && !nav_body.is_empty() {
page.navigate_with_wait_post(url, wait_until, nav_method, nav_body).await.map_err(|e| e.to_string())?;
} else {
page.navigate_with_wait(url, wait_until).await.map_err(|e| e.to_string())?;
}
for source in &preload_scripts {
if let Err(e) = page.execute_preload_script(source) {
tracing::debug!("Preload script error: {}", e);
}
}
let reached_network_idle = page.lifecycle.is_network_idle();
let network_events: Vec<_> = page.network_events.drain(..).collect();
let page_url = page.url_string();
let page_id = page.id.clone();
(frame_id, loader_id, network_events, page_url, page_id, reached_network_idle)
};
let es = session_id.clone();
let ts = timestamp();
let phase1 = vec![
CdpEvent { method: "Page.lifecycleEvent".into(), params: json!({"frameId": frame_id, "loaderId": loader_id, "name": "init", "timestamp": ts}), session_id: es.clone() },
CdpEvent { method: "Runtime.executionContextsCleared".into(), params: json!({}), session_id: es.clone() },
CdpEvent { method: "Page.frameNavigated".into(), params: json!({"frame": {"id": frame_id, "loaderId": loader_id, "url": page_url, "domainAndRegistry": "", "securityOrigin": page_url, "mimeType": "text/html", "adFrameStatus": {"adFrameType": "none"}}, "type": "Navigation"}), session_id: es.clone() },
CdpEvent { method: "Runtime.executionContextCreated".into(), params: json!({"context": {"id": 2, "origin": page_url, "name": "", "uniqueId": format!("ctx-nav-{}", page_id), "auxData": {"isDefault": true, "type": "default", "frameId": frame_id}}}), session_id: es.clone() },
CdpEvent { method: "Runtime.executionContextCreated".into(), params: json!({"context": {"id": 100, "origin": page_url, "name": "__puppeteer_utility_world__24.40.0", "uniqueId": format!("ctx-isolated-nav-{}", page_id), "auxData": {"isDefault": false, "type": "isolated", "frameId": frame_id}}}), session_id: es.clone() },
CdpEvent { method: "Page.lifecycleEvent".into(), params: json!({"frameId": frame_id, "loaderId": loader_id, "name": "commit", "timestamp": ts}), session_id: es.clone() },
];
ctx.pending_events.extend(phase1);
if ctx.fetch_intercept.enabled {
for net_event in &network_events {
ctx.pending_events.push(CdpEvent {
method: "Fetch.requestPaused".into(),
params: json!({
"requestId": net_event.request_id,
"request": {
"url": net_event.url,
"method": net_event.method,
"headers": net_event.headers,
},
"frameId": frame_id,
"resourceType": net_event.resource_type,
"networkId": net_event.request_id,
}),
session_id: es.clone(),
});
}
}
for net_event in &network_events {
ctx.pending_events.push(CdpEvent {
method: "Network.requestWillBeSent".into(),
params: json!({"requestId": net_event.request_id, "loaderId": loader_id, "documentURL": page_url, "request": {"url": net_event.url, "method": net_event.method, "headers": net_event.headers}, "timestamp": net_event.timestamp, "wallTime": net_event.timestamp, "initiator": {"type": "other"}, "type": net_event.resource_type, "frameId": frame_id}),
session_id: es.clone(),
});
ctx.pending_events.push(CdpEvent {
method: "Network.responseReceived".into(),
params: json!({"requestId": net_event.request_id, "loaderId": loader_id, "timestamp": net_event.timestamp, "type": net_event.resource_type, "response": {"url": net_event.url, "status": net_event.status, "statusText": "", "headers": &*net_event.response_headers, "mimeType": net_event.response_headers.get("content-type").cloned().unwrap_or_default()}, "frameId": frame_id}),
session_id: es.clone(),
});
ctx.pending_events.push(CdpEvent {
method: "Network.loadingFinished".into(),
params: json!({"requestId": net_event.request_id, "timestamp": net_event.timestamp, "encodedDataLength": net_event.body_size}),
session_id: es.clone(),
});
}
let mut phase3 = vec![
CdpEvent { method: "Page.lifecycleEvent".into(), params: json!({"frameId": frame_id, "loaderId": loader_id, "name": "DOMContentLoaded", "timestamp": ts}), session_id: es.clone() },
CdpEvent { method: "Page.domContentEventFired".into(), params: json!({"timestamp": ts}), session_id: es.clone() },
CdpEvent { method: "Page.lifecycleEvent".into(), params: json!({"frameId": frame_id, "loaderId": loader_id, "name": "load", "timestamp": ts}), session_id: es.clone() },
CdpEvent { method: "Page.loadEventFired".into(), params: json!({"timestamp": ts}), session_id: es.clone() },
];
if reached_network_idle || matches!(wait_until, WaitUntil::Load | WaitUntil::DomContentLoaded) {
let idle_ts = timestamp();
phase3.push(CdpEvent { method: "Page.lifecycleEvent".into(), params: json!({"frameId": frame_id, "loaderId": loader_id, "name": "networkIdle", "timestamp": idle_ts}), session_id: es.clone() });
}
phase3.push(CdpEvent { method: "Page.frameStoppedLoading".into(), params: json!({"frameId": frame_id}), session_id: es });
ctx.pending_events.extend(phase3);
Ok(json!({
"frameId": frame_id,
"loaderId": loader_id,
}))
}
"getFrameTree" => {
let page = ctx.get_session_page(session_id).ok_or("No page for session")?;
Ok(json!({
"frameTree": {
"frame": {
"id": page.frame_id,
"loaderId": "initial-loader",
"url": page.url_string(),
"domainAndRegistry": "",
"securityOrigin": page.url_string(),
"mimeType": "text/html",
"adFrameStatus": { "adFrameType": "none" },
},
"childFrames": [],
}
}))
}
"createIsolatedWorld" => {
let page = ctx.get_session_page(session_id).ok_or("No page for session")?;
let frame_id_param = params.get("frameId").and_then(|v| v.as_str())
.unwrap_or(&page.frame_id).to_string();
let world_name = params.get("worldName").and_then(|v| v.as_str())
.unwrap_or("").to_string();
let page_url = page.url_string();
let page_id = page.id.clone();
let context_id = 100;
ctx.pending_events.push(CdpEvent {
method: "Runtime.executionContextCreated".to_string(),
params: json!({
"context": {
"id": context_id,
"origin": page_url,
"name": world_name,
"uniqueId": format!("ctx-isolated-{}", page_id),
"auxData": {
"isDefault": false,
"type": "isolated",
"frameId": frame_id_param,
}
}
}),
session_id: session_id.clone(),
});
Ok(json!({ "executionContextId": context_id }))
}
"setLifecycleEventsEnabled" => Ok(json!({})),
"addScriptToEvaluateOnNewDocument" => {
let source = params.get("source").and_then(|v| v.as_str()).unwrap_or("");
ctx.preload_counter += 1;
let identifier = format!("{}", ctx.preload_counter);
if !source.is_empty() {
ctx.preload_scripts.push((identifier.clone(), source.to_string()));
}
Ok(json!({ "identifier": identifier }))
}
"removeScriptToEvaluateOnNewDocument" => {
let identifier = params.get("identifier").and_then(|v| v.as_str()).unwrap_or("");
ctx.preload_scripts.retain(|(id, _)| id != identifier);
Ok(json!({}))
}
"setInterceptFileChooserDialog" => Ok(json!({})),
"getNavigationHistory" => {
let page = ctx.get_session_page(session_id).ok_or("No page for session")?;
Ok(json!({
"currentIndex": 0,
"entries": [{
"id": 0,
"url": page.url_string(),
"userTypedURL": page.url_string(),
"title": page.title,
"transitionType": "typed",
}]
}))
}
_ => Err(format!("Unknown Page method: {}", method)),
}
}
fn timestamp() -> f64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64()
}
+213
View File
@@ -0,0 +1,213 @@
use obscura_js::runtime::RemoteObjectInfo;
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"enable" => {
let page = ctx.get_session_page(session_id).ok_or("No page")?;
let event = crate::types::CdpEvent {
method: "Runtime.executionContextCreated".to_string(),
params: json!({
"context": {
"id": 1,
"origin": page.url_string(),
"name": "",
"uniqueId": format!("ctx-{}", page.id),
"auxData": {
"isDefault": true,
"type": "default",
"frameId": page.frame_id,
}
}
}),
session_id: session_id.clone(),
};
ctx.pending_events.push(event);
Ok(json!({}))
}
"evaluate" => {
let expression = params
.get("expression")
.and_then(|v| v.as_str())
.ok_or("expression required")?;
let return_by_value = params
.get("returnByValue")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let page = ctx
.get_session_page_mut(session_id)
.ok_or("No page")?;
let info = page.evaluate_for_cdp(expression, return_by_value);
Ok(json!({ "result": remote_object_from_info(&info) }))
}
"callFunctionOn" => {
let function_declaration = params
.get("functionDeclaration")
.and_then(|v| v.as_str())
.unwrap_or("() => undefined");
let return_by_value = params
.get("returnByValue")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let await_promise = params
.get("awaitPromise")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let object_id = params.get("objectId").and_then(|v| v.as_str());
let arguments = params
.get("arguments")
.and_then(|v| v.as_array())
.map(|a| a.to_vec())
.unwrap_or_default();
let page = ctx
.get_session_page_mut(session_id)
.ok_or("No page")?;
let info =
page.call_function_on_for_cdp(function_declaration, object_id, &arguments, return_by_value, await_promise).await;
Ok(json!({ "result": remote_object_from_info(&info) }))
}
"getProperties" => {
let object_id = params.get("objectId").and_then(|v| v.as_str());
if let Some(oid) = object_id {
let page = ctx
.get_session_page_mut(session_id)
.ok_or("No page")?;
let escaped_oid = oid.replace('\\', "\\\\").replace('\'', "\\'");
let code = format!(
"(function() {{\
var obj = globalThis.__obscura_objects['{oid}'];\
if (!obj || typeof obj !== 'object') return [];\
return Object.keys(obj).map(function(k) {{\
var v = obj[k];\
return {{ name: k, value: v, type: typeof v }};\
}});\
}})()",
oid = escaped_oid,
);
let result = page.evaluate(&code);
if let serde_json::Value::Array(props) = result {
let descriptors: Vec<Value> = props
.iter()
.map(|p| {
let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
let value = p.get("value").unwrap_or(&Value::Null);
let prop_type =
p.get("type").and_then(|v| v.as_str()).unwrap_or("undefined");
let mut remote = json!({
"type": prop_type,
});
match value {
Value::Null => {
remote["type"] = json!("object");
remote["subtype"] = json!("null");
remote["value"] = json!(null);
}
Value::String(s) => {
remote["type"] = json!("string");
remote["value"] = json!(s);
}
Value::Number(n) => {
remote["type"] = json!("number");
remote["value"] = json!(n);
}
Value::Bool(b) => {
remote["type"] = json!("boolean");
remote["value"] = json!(b);
}
_ => {
remote["value"] = value.clone();
}
}
json!({
"name": name,
"value": remote,
"configurable": true,
"enumerable": true,
"writable": true,
"isOwn": true,
})
})
.collect();
Ok(json!({ "result": descriptors, "internalProperties": [] }))
} else {
Ok(json!({ "result": [], "internalProperties": [] }))
}
} else {
Ok(json!({ "result": [], "internalProperties": [] }))
}
}
"releaseObject" => {
if let Some(oid) = params.get("objectId").and_then(|v| v.as_str()) {
if let Some(page) = ctx.get_session_page_mut(session_id) {
page.release_object(oid);
}
}
Ok(json!({}))
}
"releaseObjectGroup" => {
if let Some(page) = ctx.get_session_page_mut(session_id) {
page.release_object_group();
}
Ok(json!({}))
}
"addBinding" => {
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
if !name.is_empty() {
if name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '$')
&& !name.chars().next().unwrap_or('0').is_ascii_digit() {
if let Some(page) = ctx.get_session_page_mut(session_id) {
let code = format!(
"if (typeof globalThis.{name} === 'undefined') {{\
globalThis.{name} = function() {{ return null; }};\
}}",
name = name,
);
page.evaluate(&code);
}
}
}
Ok(json!({}))
}
"runIfWaitingForDebugger" => Ok(json!({})),
"getExceptionDetails" => Ok(json!({ "exceptionDetails": null })),
"discardConsoleEntries" => Ok(json!({})),
_ => Err(format!("Unknown Runtime method: {}", method)),
}
}
fn remote_object_from_info(info: &RemoteObjectInfo) -> Value {
let mut obj = json!({ "type": info.js_type });
if let Some(ref subtype) = info.subtype {
obj["subtype"] = json!(subtype);
}
if !info.class_name.is_empty() {
obj["className"] = json!(info.class_name);
}
if !info.description.is_empty() {
obj["description"] = json!(info.description);
}
if let Some(ref oid) = info.object_id {
obj["objectId"] = json!(oid);
}
if let Some(ref value) = info.value {
obj["value"] = value.clone();
}
obj
}
+117
View File
@@ -0,0 +1,117 @@
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn handle(
method: &str,
params: &Value,
ctx: &mut CdpContext,
_session_id: &Option<String>,
) -> Result<Value, String> {
match method {
"getCookies" => {
let cookies = ctx.default_context.cookie_jar.get_all_cookies();
let cdp_cookies: Vec<Value> = cookies
.iter()
.map(|c| {
json!({
"name": c.name,
"value": c.value,
"domain": c.domain,
"path": c.path,
"expires": -1,
"size": c.name.len() + c.value.len(),
"httpOnly": c.http_only,
"secure": c.secure,
"session": true,
"priority": "Medium",
"sameParty": false,
"sourceScheme": if c.secure { "Secure" } else { "NonSecure" },
"sourcePort": 80,
})
})
.collect();
Ok(json!({ "cookies": cdp_cookies }))
}
"setCookies" => {
if let Some(cookies) = params.get("cookies").and_then(|v| v.as_array()) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
for c in cookies {
let name = match c.get("name").and_then(|v| v.as_str()) {
Some(n) => n.to_string(),
None => continue,
};
let value = c.get("value").and_then(|v| v.as_str()).unwrap_or("").to_string();
let domain = c
.get("domain")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
c.get("url")
.and_then(|v| v.as_str())
.and_then(|u| url::Url::parse(u).ok())
.and_then(|u| u.host_str().map(|h| h.to_string()))
})
.unwrap_or_default();
let expires = c.get("expires").and_then(|v| v.as_f64());
if let Some(exp) = expires {
if exp > 0.0 && exp < now {
ctx.default_context.cookie_jar.delete_cookie(&name, &domain);
continue;
}
}
let path = c
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string();
let secure =
c.get("secure").and_then(|v| v.as_bool()).unwrap_or(false);
let http_only =
c.get("httpOnly").and_then(|v| v.as_bool()).unwrap_or(false);
ctx.default_context.cookie_jar.set_cookies_from_cdp(vec![
obscura_net::CookieInfo {
name,
value,
domain,
path,
secure,
http_only,
},
]);
}
}
Ok(json!({}))
}
"deleteCookies" => {
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let domain_from_url = params
.get("url")
.and_then(|v| v.as_str())
.and_then(|u| url::Url::parse(u).ok())
.and_then(|u| u.host_str().map(|h| h.to_string()));
let domain = params
.get("domain")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or(domain_from_url)
.unwrap_or_default();
if !name.is_empty() {
ctx.default_context
.cookie_jar
.delete_cookie(name, &domain);
}
Ok(json!({}))
}
_ => Ok(json!({})),
}
}
+196
View File
@@ -0,0 +1,196 @@
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
use crate::types::CdpEvent;
pub async fn handle(method: &str, params: &Value, ctx: &mut CdpContext) -> Result<Value, String> {
match method {
"setDiscoverTargets" => {
ctx.pending_events.push(CdpEvent::new(
"Target.targetCreated",
json!({
"targetInfo": {
"targetId": "browser",
"type": "browser",
"title": "",
"url": "",
"attached": true,
"browserContextId": "",
}
}),
));
for page in &ctx.pages {
ctx.pending_events.push(CdpEvent::new(
"Target.targetCreated",
json!({
"targetInfo": {
"targetId": page.id,
"type": "page",
"title": page.title,
"url": page.url_string(),
"attached": false,
"browserContextId": page.context.id,
}
}),
));
}
Ok(json!({}))
}
"getTargets" => {
let targets: Vec<Value> = ctx
.pages
.iter()
.map(|page| {
json!({
"targetId": page.id,
"type": "page",
"title": page.title,
"url": page.url_string(),
"attached": true,
"browserContextId": page.context.id,
})
})
.collect();
Ok(json!({ "targetInfos": targets }))
}
"createTarget" => {
let url = params.get("url").and_then(|v| v.as_str()).unwrap_or("about:blank");
let page_id = ctx.create_page();
let session_id = format!("{}-session", page_id);
if let Some(page) = ctx.get_page_mut(&page_id) {
if url == "about:blank" || url.is_empty() {
page.navigate_blank();
} else {
let _ = page.navigate(url).await;
}
}
ctx.sessions.insert(session_id.clone(), page_id.clone());
if let Some(page) = ctx.get_page(&page_id) {
ctx.pending_events.push(CdpEvent::new(
"Target.targetCreated",
json!({
"targetInfo": {
"targetId": page_id,
"type": "page",
"title": page.title,
"url": page.url_string(),
"attached": false,
"browserContextId": page.context.id,
}
}),
));
}
if let Some(page) = ctx.get_page(&page_id) {
ctx.pending_events.push(CdpEvent::new(
"Target.attachedToTarget",
json!({
"sessionId": session_id,
"targetInfo": {
"targetId": page_id,
"type": "page",
"title": page.title,
"url": page.url_string(),
"attached": true,
"browserContextId": page.context.id,
},
"waitingForDebugger": false,
}),
));
}
Ok(json!({ "targetId": page_id }))
}
"attachToTarget" => {
let target_id = params.get("targetId").and_then(|v| v.as_str())
.ok_or("targetId required")?;
let session_id = format!("{}-session", target_id);
ctx.sessions.insert(session_id.clone(), target_id.to_string());
if let Some(page) = ctx.get_page(target_id) {
ctx.pending_events.push(CdpEvent::new(
"Target.attachedToTarget",
json!({
"sessionId": session_id,
"targetInfo": {
"targetId": target_id,
"type": "page",
"title": page.title,
"url": page.url_string(),
"attached": true,
"browserContextId": page.context.id,
},
"waitingForDebugger": false,
}),
));
}
Ok(json!({ "sessionId": session_id }))
}
"closeTarget" => {
let target_id = params.get("targetId").and_then(|v| v.as_str())
.ok_or("targetId required")?;
let session_id = format!("{}-session", target_id);
ctx.pending_events.push(CdpEvent::new(
"Target.detachedFromTarget",
json!({
"sessionId": session_id,
"targetId": target_id,
}),
));
ctx.pending_events.push(CdpEvent::new(
"Target.targetDestroyed",
json!({ "targetId": target_id }),
));
ctx.remove_page(target_id);
Ok(json!({ "success": true }))
}
"setAutoAttach" => Ok(json!({})),
"getBrowserContexts" => {
Ok(json!({ "browserContextIds": [ctx.default_context.id] }))
}
"createBrowserContext" => {
ctx.default_context.cookie_jar.clear();
Ok(json!({ "browserContextId": ctx.default_context.id }))
}
"disposeBrowserContext" => {
ctx.default_context.cookie_jar.clear();
Ok(json!({}))
}
"getTargetInfo" => {
let target_id = params.get("targetId").and_then(|v| v.as_str());
match target_id {
Some(id) => {
let page = ctx.get_page(id).ok_or("Target not found")?;
Ok(json!({
"targetInfo": {
"targetId": id,
"type": "page",
"title": page.title,
"url": page.url_string(),
"attached": true,
"browserContextId": page.context.id,
}
}))
}
None => {
Ok(json!({
"targetInfo": {
"targetId": "browser",
"type": "browser",
"title": "",
"url": "",
"attached": true,
}
}))
}
}
}
_ => Err(format!("Unknown Target method: {}", method)),
}
}