Files
h4ckf0r0day--obscura/docs/Adding-a-CDP-method-or-Web-API.md
2026-07-13 12:22:13 +08:00

155 lines
4.0 KiB
Markdown

Two recipes for the two most common extensions: a new CDP method, and a new JS Web API.
## Adding a CDP method
Worked example: `MyDomain.doThing` that takes `{ name }` and returns `{ ok }`.
### 1. Add the handler
Create or edit a file under `crates/obscura-cdp/src/domains/`:
```rust
// crates/obscura-cdp/src/domains/my_domain.rs
use serde_json::{json, Value};
use crate::dispatch::CdpContext;
pub async fn do_thing(
params: &Value,
_ctx: &mut CdpContext,
_session_id: &Option<String>,
) -> Result<Value, String> {
let name = params.get("name")
.and_then(|v| v.as_str())
.ok_or("missing name")?;
// do the work
Ok(json!({ "ok": true, "name": name }))
}
```
### 2. Register in the dispatcher
In `crates/obscura-cdp/src/dispatch.rs`, add a match arm:
```rust
"MyDomain.doThing" => domains::my_domain::do_thing(&req.params, ctx, &req.session_id).await,
```
### 3. Test it
`crates/obscura-cdp/tests/cdp_my_domain.rs`:
```rust
use obscura_cdp::dispatch::{dispatch, CdpContext};
use obscura_cdp::types::CdpRequest;
use serde_json::json;
#[tokio::test(flavor = "current_thread")]
async fn my_domain_do_thing_returns_ok() {
let mut ctx = CdpContext::new();
let resp = dispatch(&CdpRequest {
id: 1,
method: "MyDomain.doThing".into(),
params: json!({ "name": "test" }),
session_id: None,
}, &mut ctx).await;
assert!(resp.error.is_none());
assert_eq!(resp.result.unwrap()["ok"], true);
}
```
Run:
```bash
cargo test -p obscura-cdp my_domain
```
## Adding a Web API
Worked example: `crypto.subtle.digest`, real implementation backed by a Rust hash op.
### 1. Add the Rust op
In `crates/obscura-js/src/ops.rs`:
```rust
#[op2]
#[buffer]
fn op_subtle_digest(#[string] algorithm: &str, #[buffer] data: &[u8]) -> Vec<u8> {
use sha1::Digest as _;
match algorithm.to_ascii_uppercase().as_str() {
"SHA-1" => sha1::Sha1::digest(data).to_vec(),
"SHA-256" => sha2::Sha256::digest(data).to_vec(),
"SHA-384" => sha2::Sha384::digest(data).to_vec(),
"SHA-512" => sha2::Sha512::digest(data).to_vec(),
_ => sha2::Sha256::digest(data).to_vec(),
}
}
```
### 2. Register the op
In the same file, `build_extension()`:
```rust
ops: std::borrow::Cow::Owned(vec![
op_dom(),
op_console_msg(),
// ...
op_subtle_digest(),
]),
```
### 3. Add the JS shim
In `crates/obscura-js/js/bootstrap.js`:
```js
globalThis.crypto = globalThis.crypto || {};
globalThis.crypto.subtle = globalThis.crypto.subtle || {};
globalThis.crypto.subtle.digest = function digest(algorithm, data) {
const algName = typeof algorithm === 'string' ? algorithm : algorithm.name;
const bytes = data instanceof ArrayBuffer
? new Uint8Array(data)
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
const out = Deno.core.ops.op_subtle_digest(algName, bytes);
return Promise.resolve(out.buffer);
};
```
### 4. Add a dependency if needed
`crates/obscura-js/Cargo.toml`:
```toml
sha1 = "0.10"
sha2 = "0.10"
```
### 5. Smoke test
```bash
cargo build --release
./target/release/obscura fetch https://example.com --eval "
crypto.subtle.digest('SHA-256', new TextEncoder().encode('hi'))
.then(buf => Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''))
"
```
## Tips
- Keep the JS shim thin. All side effects go through ops.
- Use `Promise.resolve` to keep async-shaped APIs callable from sync ops.
- Match the spec: Web API names and shapes are checked by Puppeteer / Playwright wrappers.
- DOM mutations go through `op_dom`, not new ops.
- For events that need to fire across handlers, use the existing `_makeListenerBox` helper in `bootstrap.js`.
## Worked examples in the tree
- CDP method with intercept: `crates/obscura-cdp/src/domains/page.rs` `do_navigate`.
- Web API with op + JS shim: `crypto.subtle.digest` (above).
- Web API in pure JS (no op): `DOMParser` in `bootstrap.js`.
- Web API with async event firing: `WebSocket`, `IntersectionObserver` in `bootstrap.js`.