Commit Graph
17 Commits
Author SHA1 Message Date
SGavrl 1950048138 fix(browser,cdp,js): about:blank navigation, real getBoxModel/getContentQuads, expose _wrap and self
- about: URL scheme now handled in navigate_single instead of
  hitting validate_url and failing with "Forbidden URL scheme"
- DOM.getBoxModel calls getBoundingClientRect via JS instead of
  returning hardcoded pixel coords
- DOM.getContentQuads added (was missing, needed by Playwright click)
- globalThis._wrap exposed so CDP eval code can look up nodes by id
- globalThis.self = globalThis for webpack/bundler worker detection
2026-05-18 10:11:54 +00:00
SGavrl 913e6a4865 fix(browser,cdp): correct document lifecycle ordering and add missing DOM methods
document.readyState was hardcoded to "complete" so scripts checking
readyState === 'loading' always took the wrong branch. Fixed by using a
JS global (__documentReadyState__) that Rust sets to "loading" before
any scripts execute, "interactive" before DOMContentLoaded fires, and
"complete" after load.

document.addEventListener and document.dispatchEvent on Document were
no-ops, so DOMContentLoaded listeners registered by inline scripts were
never called. Added proper listener storage and dispatch on Document.

Adds document.getElementsByName() (was missing entirely).

Page.reload is now implemented (re-navigates to the current URL),
resolving "Unknown Page method: reload" errors from Playwright.

data: URL navigation now works, fixing Playwright's goto("data:text/html,...")
pattern that previously errored with "invalid format".

Page.navigate logic extracted into do_navigate() so reload can reuse it.

Fixes #137, #146 (SPA bootstrap lifecycle ordering).
2026-05-18 09:50:23 +00:00
SGavrl 3158a1fd31 fix(cdp): gate Target.createTarget file:// like Page.navigate
GHSA-q55h-vfv9-qcr5 patched Page.navigate to refuse file:// URLs
unless --allow-file-access was set, but Target.createTarget accepted
the same scheme and called page.navigate(url) directly, bypassing
the policy. A CDP client could call Target.createTarget {url: file://...}
then Runtime.evaluate to read document.body.textContent and recover
arbitrary local files.

Apply the same gate at the Target.createTarget entrypoint. Pull
the file-scheme detector into a shared util module so both domains
stay in lock-step instead of one drifting from the other.

Verified blocked vs the unpatched build (pr69):
  - Patched: Target.createTarget {url: file:///tmp/secret} errors
    "Target.createTarget to file:// is disabled. ..." before any
    navigation runs.
  - Unpatched: same call returns the file contents via
    Runtime.evaluate {expression: "document.body.textContent"}.
2026-05-16 15:26:58 +00:00
Alexandr KondakovandSGavrl 6dd1e4e65f Fix CDP click submit navigation parity (#69)
* Fix Runtime.evaluate awaitPromise settling

* Fix DOM event navigation side effects

* Fix CDP click-triggered navigation parity

* Strengthen CDP inline script click parity test

* Handle positions-like submit URLs in CDP parity


Co-authored-by: SGavrl <[email protected]>
2026-05-15 20:38:54 +00:00
SGavrl 18dc796df0 fix(cdp): gate file:// navigation behind --allow-file-access
GHSA-q55h-vfv9-qcr5. Any process able to reach the CDP port could
send Page.navigate {"url":"file:///etc/shadow"} and read arbitrary
local files (the process's UID's worth of read access). When obscura
runs as root inside Docker or as a systemd service that translates
straight into /etc/shadow, /root/.ssh/id_rsa, /proc/1/environ, etc.

Validate at the CDP boundary, not in obscura-net, so direct CLI
callers ("obscura fetch file://...") keep working. A new
BrowserContext.allow_file_access flag defaults to false; Page.navigate
returns an error when the requested URL is file:// and the flag is
off. CdpContext::new_with_security threads the flag from the server
entry point, and "obscura serve --allow-file-access" opts in for
local-HTML testing workflows.

Tested:
- Default obscura serve: CDP Page.navigate {file:///etc/passwd} ->
  refused with descriptive error.
- obscura serve --allow-file-access: same call succeeds.
- obscura fetch file://... CLI path: unchanged, still reads the file.
- All earlier PoCs (GHSA-6pj9 JS->file SOP, GHSA-4789 sub-resource
  SSRF, GHSA-4789 CSS template injection) still pass.
- cargo test --workspace clean.
2026-05-14 23:46:35 +02:00
Ben Younes 2ff0f8fe1d fix(cdp): read and validate contextId in Runtime.evaluate / callFunctionOn (#51) (#149)
Fixes #51. `Runtime.evaluate` and `Runtime.callFunctionOn` previously
ignored the `contextId` / `executionContextId` parameter entirely, so
clients targeting an isolated world (e.g. Playwright's locator path,
which calls `Page.createIsolatedWorld` → contextId 100 → evaluates the
UtilityScript against that id) silently fell through to the default
context, breaking the contract advertised via
Runtime.executionContextCreated.

Track the set of advertised execution context ids on CdpContext
(pre-seeded with 1 and 2 — the default-frame contexts emitted by
Runtime.enable and post-navigation) and extend it from
Page.createIsolatedWorld. Both handlers now reject unknown ids with
`Cannot find context with specified id: <id>` — matching the wording
real Chrome returns — and log accepted ids at debug for observability.

Obscura still routes every JS call against the page's single V8 isolate,
so this PR is spec-compliance scaffolding rather than full multi-world
execution. Follow-up work to physically isolate worlds can build on the
same valid_context_ids registry.
2026-05-14 09:15:19 +00:00
Ben YounesandOra Studio d8b7c5fad1 feat(cli): add --dump markdown output mode (#54) (#78)
Surfaces the existing LP.getMarkdown CDP machinery as a CLI flag so
text-pipeline users (LLM agents, scrapers feeding markdown to other
tools) don't need a separate HTML-to-markdown converter step.

  obscura fetch --dump markdown 'https://example.com'

Layout:
- crates/obscura-js/src/markdown.rs holds HTML_TO_MARKDOWN_JS as a
  pub const so every layer can share the same DOM-walker. obscura-js
  is at the bottom of the dep DAG (obscura-browser -> obscura-js,
  obscura-cdp -> obscura-js, obscura-cli -> obscura-js) so a single
  source of truth works without a circular dependency.
- crates/obscura-browser/src/lib.rs re-exports it for ergonomics.
- crates/obscura-cdp/src/domains/lp.rs drops its inline duplicate
  and calls the shared const.
- crates/obscura-cli/src/main.rs adds the `Markdown` DumpFormat
  variant + dump_markdown(&mut Page) which page.evaluate's the
  shared script and prints the result.

Net diff: a new 75-line markdown.rs, a 70+ line deletion in lp.rs
that's reborn as 3 lines, and ~10 lines added across the other four
files. No behavior change for `--dump html|text|links`, and
LP.getMarkdown over CDP keeps the exact same JSON shape.

Four new unit tests in obscura-js cover headings, inline emphasis +
links, ordered/unordered lists, and that <script> / <style> contents
do not leak into the output.

Generated by Ora Studio
Vibe coded by ousamabenyounes

Co-authored-by: Ora Studio <[email protected]>
2026-05-13 22:36:17 +02:00
Adam Ma a8b1ec0968 chore: silence unused import/variable/dead-code warnings (#101) (#126)
- obscura-net/cookies.rs: #[allow(dead_code)] on CookieEntry.same_site
  (parsed today, kept for future SameSite enforcement).
- obscura-js/runtime.rs: drop vestigial BOOTSTRAP_JS const; bootstrap.js
  is already embedded into the V8 snapshot via build.rs.
- obscura-js/ops.rs: prefix the four unused fields in
  InterceptResolution::Continue with _.
- obscura-cdp/dispatch.rs: drop unused InterceptResolution import.
- obscura-cdp/domains/fetch.rs: drop unused Arc / Mutex / CdpEvent imports.
- obscura-cdp/domains/input.rs: prefix _button / _click_count; CDP
  parses them but the JS click stub is hardcoded for now.
- obscura-cdp/server.rs: prefix _processor_handle and _ctx; drop dead
  initial Ok(())/None for navigate_result/page_back - they are
  unconditionally overwritten in the only loop break arm.

cargo build --workspace: 0 warnings.
cargo test --workspace: 113 passed.
2026-05-09 17:52:32 +02:00
Ben YounesandOra Studio 023caf9ce5 fix(cdp): return descriptive error for Page.printToPDF instead of generic Unknown Page method (#53) (#112)
Playwright's `page.pdf()` issues a `Page.printToPDF` CDP request which
Obscura was not handling, so the dispatcher fell through to the generic
catch-all and clients saw:

    Page.pdf: Protocol error (Page.printToPDF): Unknown Page method: printToPDF

This reads like an oversight rather than a deliberate limitation.
Obscura intentionally has no layout engine — option (2) from the issue —
so make the limitation explicit by handling the method and returning a
clear error that names the method, says why it isn't supported, and
points users at a working alternative (extract HTML via
Runtime.evaluate, then render to PDF in their client).

The behavior change is small but meaningful for DX: the JSON-RPC error
code stays -32601 (the dispatcher's mapping for `Err(_)`), but the
human-readable message becomes actionable instead of looking like a
forgotten method.

Adds a unit test that pins the regression: printToPDF must NOT fall
through to the "Unknown Page method" catch-all and must mention both
"not supported" and a workaround.

Generated by Ora Studio
Vibe coded by ousamabenyounes

Co-authored-by: Ora Studio <[email protected]>
2026-05-09 17:36:11 +02:00
Ben YounesandOra Studio 23753405e5 fix(cdp): implement Target.attachToBrowserTarget for Playwright (#64) (#81)
Playwright (and recent Puppeteer) call Target.attachToBrowserTarget on
connect to obtain a session for the implicit "browser" target. Without
the handler the dispatcher returns "Unknown Target method:
attachToBrowserTarget" and the connect handshake aborts before any
user code runs.

Add a handler that mirrors Target.attachToTarget but for the browser
target: register a "browser-session" → "browser" mapping in
ctx.sessions, emit Target.attachedToTarget with type=browser, and
return the new session id.

Note: this addresses item (1) from #64. The Rocket Loader / Cloudflare
JS execution gap and Page.getLayoutMetrics are tracked separately
(getLayoutMetrics is fixed in PR #79).

Generated by Ora Studio
Vibe coded by ousamabenyounes

Co-authored-by: Ora Studio <[email protected]>
2026-05-03 17:10:07 +02:00
Ben YounesandOra Studio 3bb628eaf0 fix(cdp): implement Page.getLayoutMetrics for screenshot compatibility (#52) (#79)
Playwright calls Page.getLayoutMetrics before every page.screenshot() to
size the capture. Without it the call fails with "Unknown Page method:
getLayoutMetrics" and screenshot() throws.

Obscura has no visual layout engine, so we return a fixed 1280x720
viewport (Chrome's default) and try to derive the content height from
document.documentElement.scrollHeight via the JS runtime, falling back
to the viewport height when no live page is bound.

Generated by Ora Studio
Vibe coded by ousamabenyounes

Co-authored-by: Ora Studio <[email protected]>
2026-05-03 17:09:44 +02:00
zhangyang-crazy-oneandzhangyang-crazy-one 90ededfc44 fix(cdp): improve AXNode CDP spec compliance (#67)
Addresses follow-up issues from PR #44 bot review:

- Add parentId field to AXNode (walk DOM ancestors to resolve AX parent)
- Omit optional fields when empty (name, value, properties, childIds)
  instead of emitting null/empty arrays, per CDP spec
- Add ax_value_boolean() and ax_value_integer() helpers for consistent
  AXValue construction across all property types
- Use typed helpers throughout compute_properties() instead of inline
  JSON construction

CDP spec reference: Accessibility.AXNode requires parentId as AXNodeId,
and optional fields should be absent when not applicable.

Co-authored-by: zhangyang-crazy-one <[email protected]>
2026-05-03 17:09:19 +02:00
pryceupandClaude Opus 4.7 25b6d58f3e fix: enable request interception when Fetch.enable is called (#50)
Fetch.enable was setting page.intercept_enabled to false and clearing
intercept_block_patterns, which caused should_block_url() to always
return false. As a result, Fetch.requestPaused events were never
triggered and Playwright's page.route() handlers never executed.

- Set intercept_enabled to true (was false)
- Propagate URL patterns to page.intercept_block_patterns (was cleared)

Fixes #49

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-03 17:09:02 +02:00
zhangyang-crazy-one 266afa7c34 fix(cdp): wrap role/name/value as CDP AXValue objects
Per qodo-ai-reviewer feedback on PR #44: Accessibility.getFullAXTree
was emitting AXNode.role as a plain string and name/value as plain
strings or null, instead of CDP-compliant AXValue objects.

Changes:
- Add ax_value_role() and ax_value_string() helpers
- Change compute_name()/compute_value() to return Option<String>
- Wrap role, name, value as AXValue in build_ax_node()

The properties array already used the correct AXValue format.
2026-04-27 20:59:47 +08:00
zhangyang-crazy-one a4591707e3 feat(cdp): implement Accessibility domain (getFullAXTree)
Implements the CDP Accessibility domain with getFullAXTree support,
enabling AI agent tools (agent-browser, Playwright MCP) that rely on
Accessibility.getFullAXTree to work with Obscura.

Changes:
- Add crates/obscura-cdp/src/domains/accessibility.rs (~380 lines)
  - Accessibility.enable: returns {}
  - Accessibility.getFullAXTree: walks DOM tree, produces AXNode array
  - Role mapping: 50+ HTML tags → ARIA roles, respects explicit role attr
  - Name computation: aria-label → aria-labelledby → alt → title → placeholder
  - Properties: focusable, editable, checked, disabled, level, required, multiline
- Update mod.rs: register accessibility module
- Update dispatch.rs: route Accessibility domain to real handler

Tested with example.com (18 nodes), httpbin.org/html (16 nodes),
news.ycombinator.com (1308 nodes). Zero structure errors.

Closes #12
2026-04-27 12:02:38 +08:00
Mike Mooring 9e3e1e4ef7 fix(cdp): five Playwright drop-in compatibility patches
Brings Obscura's CDP server in line with what Playwright's
`chromium.connectOverCDP()` expects. Each fix is small and surgical;
together they make `connect → newContext → newPage → goto → page.title()`
work end-to-end against `https://example.com`.

1. **server.rs — emit pending events before command response.**
   Chromium CDP semantics require side-effect events (e.g.
   `Target.targetCreated` + `Target.attachedToTarget` from
   `Target.createTarget`) to arrive *before* the originating command's
   response. Playwright's wire reader assumes this ordering — it
   resolves the response promise on the response and then synchronously
   looks up the freshly-created session. Reversing the order makes that
   lookup miss and the page hang at `newPage()`.

2. **domains/browser.rs — accept Browser.setWindowBounds as a no-op.**
   Playwright calls `Browser.setWindowBounds` during teardown. Returning
   "Unknown Browser method" causes Playwright to treat the target as
   broken and tear it down mid-test, surfacing as "Frame has been
   detached" on the next `page.title()`.

3. **obscura-browser/page.rs — frame_id == target_id for main frame.**
   Was `format!("{id}.1", id)`. Playwright's frame manager indexes
   frames by frame id and looks up the main frame by target id, so the
   `.1` suffix made every main-frame lookup miss.

4. **domains/page.rs + dispatch.rs — track isolated worlds and re-emit
   them post-navigation.** Playwright registers a utility world via
   `Page.createIsolatedWorld` and expects `Runtime.executionContextCreated`
   for each registered world after every navigation. Without re-emission,
   the utility-world handle goes stale post-`goto` and `page.title()`
   hangs waiting for the world's execution context. Falls back to the
   legacy `__puppeteer_utility_world__24.40.0` name when no worlds have
   been registered, preserving prior Puppeteer support.

5. **obscura-js/runtime.rs — strip trailing semicolons before wrapping.**
   Playwright's utility-script expression is an IIFE ending in `})();`.
   Both `wrap_expression` (CDP `Runtime.evaluate`) and `evaluate_for_cdp`
   (the `meta_code` IIFE) embed the user expression inside `({EXPR})` /
   `return ({EXPR});`. With the trailing `;` left in, this produces
   `({...;})` — a SyntaxError. Parse errors are not catchable, so the
   surrounding try/catch never fires and the call silently returns
   `undefined`, surfacing as `page.title() === ""`.

Verified end-to-end against `https://example.com`:
  [probe] connect OK
  [probe] newContext OK
  [probe] newPage OK
  [probe] goto OK
  [probe] title = Example Domain
2026-04-24 23:27:18 -07:00
h4ckf0r0day 8749725b45 Initial Release 2026-04-13 12:30:28 +02:00