webskrap 3.2.0__tar.gz → 3.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webskrap-3.2.0 → webskrap-3.2.2}/.gitignore +1 -0
- webskrap-3.2.2/.package-dist.dZyl3m/.gitignore +1 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/CHANGELOG.md +35 -1
- {webskrap-3.2.0 → webskrap-3.2.2}/PKG-INFO +16 -3
- {webskrap-3.2.0 → webskrap-3.2.2}/README.md +15 -2
- webskrap-3.2.2/docs/diagnostics/datadome.md +212 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/pyproject.toml +1 -1
- {webskrap-3.2.0 → webskrap-3.2.2}/scripts/driver_parity.py +7 -108
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/cdp_dom.py +10 -2
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/cdp_session.py +5 -1
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/cdp_setup.py +46 -10
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/challenges.py +61 -10
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/client.py +8 -5
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/consent.py +2 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/guide.md +30 -5
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/models.py +22 -1
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_browser_vision.py +8 -2
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_cdp_browser.py +15 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_cdp_child_targets.py +126 -4
- webskrap-3.2.2/tests/test_cdp_setup.py +33 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_challenges.py +38 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_client_unit.py +19 -1
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_consent.py +13 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_human.py +1 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_models.py +31 -1
- {webskrap-3.2.0 → webskrap-3.2.2}/uv.lock +1 -1
- {webskrap-3.2.0 → webskrap-3.2.2}/.agents/skills/webskrap/SKILL.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/.agents/skills/webskrap/references/cli.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/.agents/skills/webskrap/references/mcp.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/.agents/skills/webskrap/references/python-api.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/.claude/skills/webskrap/SKILL.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/.codex/skills/webskrap/SKILL.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/.gitattributes +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/CONTRIBUTING.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/LICENSE +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/SECURITY.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/benchmarks.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/docs/design/cdp-driver.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/docs/migration-3.0.md +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/scripts/live_stealth_report.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/__init__.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/browser_cli.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/browser_session.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/cdp.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/cli.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/cli_output.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/diagnostics.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/display.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/errors.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/fetch_runtime.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/human.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/install.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/mcp_server.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/parsing.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/paths.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/profiles.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/proxy_auth.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/py.typed +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/search.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/text_search.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/src/webskrap/urls.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/conftest.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/accepted-202.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/akamai-invalid-url-400.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/akamai-reference-403.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/akamai-sec-cpt.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/api-403.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/article-access-denied.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/cloudflare-1020.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/cloudflare-custom-block.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/blocks/not-found-404.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/search/bing.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/search/bing_challenge.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/search/bing_no_results.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/search/ddg.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/search/ddg_challenge.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/fixtures/search/ddg_no_results.html +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/live_stealth_helpers.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_bot_detection.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_bot_detection_headless.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_browser_cli.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_browser_integration.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_browser_session_unit.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_cdp.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_cdp_session.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_cli.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_consent_live.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_diagnostics.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_display.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_docs.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_errors.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_fetch_runtime.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_install.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_live_stealth_helpers.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_live_stealth_report.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_mcp.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_mcp_efficiency.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_parsing.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_paths.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_profiles.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_search.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_search_live.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_text_search.py +0 -0
- {webskrap-3.2.0 → webskrap-3.2.2}/tests/test_urls.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -9,6 +9,38 @@ history, so they summarize each release rather than list every change.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [3.2.2] - 2026-10-30
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- DataDome detection now follows the device check when it navigates its
|
|
17
|
+
frame. It reads the live challenge frame, and it reports a manual
|
|
18
|
+
challenge apart from a hard block. The user-agent probe now uses the
|
|
19
|
+
session proxy and network flags, so it cannot connect directly.
|
|
20
|
+
- Full desktop profiles now set `navigator.platform` to match the user
|
|
21
|
+
agent. They also fit the viewport inside the real browser window before
|
|
22
|
+
the metrics override.
|
|
23
|
+
- Cookie decline now finds the reject button of Commanders Act
|
|
24
|
+
TrustCommander notices. Their markup has no "cookie" or "consent" name, so
|
|
25
|
+
the text search did not look inside them.
|
|
26
|
+
- Full profiles no longer put q-values in `navigator.languages`, and no longer
|
|
27
|
+
send a doubled q-value in `Accept-Language` (`fr;q=0.9;q=0.9`). Chromium
|
|
28
|
+
weights the header itself, so the override now gets a plain list.
|
|
29
|
+
- With `profile_mode="full"` (or `"locale"` off Linux),
|
|
30
|
+
`navigator.serviceWorker.register()` no longer hangs. The page and the
|
|
31
|
+
browser both held the worker paused, and each waited for the other. Now
|
|
32
|
+
only the browser-level setup prepares service and shared workers; the page
|
|
33
|
+
only resumes them.
|
|
34
|
+
- Mouse presses now report `PointerEvent.pressure` 0.5 while the button is
|
|
35
|
+
down, as a real mouse does. CDP's default of 0 was visible to pages.
|
|
36
|
+
|
|
37
|
+
## [3.2.1] - 2026-10-26
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
|
|
41
|
+
- `scripts/driver_parity.py` no longer offers `--click`. The script only
|
|
42
|
+
measures whether a target challenges each driver.
|
|
43
|
+
|
|
12
44
|
## [3.2.0] - 2026-10-26
|
|
13
45
|
|
|
14
46
|
### Added
|
|
@@ -929,7 +961,9 @@ entries below.
|
|
|
929
961
|
|
|
930
962
|
Earlier releases (0.1.0 - 0.4.9) are recorded in the git history and tags only.
|
|
931
963
|
|
|
932
|
-
[Unreleased]: https://github.com/kacigaya/webskrap/compare/v3.2.
|
|
964
|
+
[Unreleased]: https://github.com/kacigaya/webskrap/compare/v3.2.2...HEAD
|
|
965
|
+
[3.2.2]: https://github.com/kacigaya/webskrap/compare/v3.2.1...v3.2.2
|
|
966
|
+
[3.2.1]: https://github.com/kacigaya/webskrap/compare/v3.2.0...v3.2.1
|
|
933
967
|
[3.2.0]: https://github.com/kacigaya/webskrap/compare/v3.1.0...v3.2.0
|
|
934
968
|
[3.1.0]: https://github.com/kacigaya/webskrap/compare/v3.0.1...v3.1.0
|
|
935
969
|
[3.0.1]: https://github.com/kacigaya/webskrap/compare/v3.0.0...v3.0.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webskrap
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.2
|
|
4
4
|
Summary: Async Python scraping on Chromium over its own CDP client, with coherent browser profiles and session controls.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kacigaya/webskrap
|
|
6
6
|
Project-URL: Repository, https://github.com/kacigaya/webskrap
|
|
@@ -82,8 +82,21 @@ asyncio.run(main())
|
|
|
82
82
|
For deferred content, pass `ready_selector="#content"` to a Python fetch or
|
|
83
83
|
`--ready-selector '#content'` to CLI `fetch`. Both MCP fetch tools accept
|
|
84
84
|
`ready_selector` too. The selector must become visible after consent dismissal.
|
|
85
|
-
Known challenge pages (DataDome, Cloudflare, Akamai, AWS WAF) and provider block
|
|
86
|
-
|
|
85
|
+
Known challenge pages (DataDome, Cloudflare, Akamai, AWS WAF) and provider block
|
|
86
|
+
pages (DataDome, Cloudflare, Akamai) raise `blocked`. WebSkrap waits for device
|
|
87
|
+
checks that reload the page. It does not retry refusals or solve CAPTCHAs.
|
|
88
|
+
Ordinary HTTP errors still return `ok=False`.
|
|
89
|
+
|
|
90
|
+
For manual CAPTCHA completion, open a persistent browser with
|
|
91
|
+
`webskrap browser open --headed --session manual --proxy http://localhost:8888 URL`.
|
|
92
|
+
Complete the check in that browser. Keep the same session, profile, and proxy
|
|
93
|
+
for later commands. On a remote host, use a desktop connection to its display.
|
|
94
|
+
`virtual_display=True` alone does not provide a remote desktop.
|
|
95
|
+
|
|
96
|
+
Keep `profile_mode="native"` for the browser's own identity. On Linux with Xvfb,
|
|
97
|
+
`virtual_display=True` runs a headed browser without a visible desktop window.
|
|
98
|
+
Full profile emulation is useful for device tests, but cannot replace every
|
|
99
|
+
worker or iframe value. See the [identity limits](src/webskrap/guide.md).
|
|
87
100
|
|
|
88
101
|
## Search
|
|
89
102
|
|
|
@@ -48,8 +48,21 @@ asyncio.run(main())
|
|
|
48
48
|
For deferred content, pass `ready_selector="#content"` to a Python fetch or
|
|
49
49
|
`--ready-selector '#content'` to CLI `fetch`. Both MCP fetch tools accept
|
|
50
50
|
`ready_selector` too. The selector must become visible after consent dismissal.
|
|
51
|
-
Known challenge pages (DataDome, Cloudflare, Akamai, AWS WAF) and provider block
|
|
52
|
-
|
|
51
|
+
Known challenge pages (DataDome, Cloudflare, Akamai, AWS WAF) and provider block
|
|
52
|
+
pages (DataDome, Cloudflare, Akamai) raise `blocked`. WebSkrap waits for device
|
|
53
|
+
checks that reload the page. It does not retry refusals or solve CAPTCHAs.
|
|
54
|
+
Ordinary HTTP errors still return `ok=False`.
|
|
55
|
+
|
|
56
|
+
For manual CAPTCHA completion, open a persistent browser with
|
|
57
|
+
`webskrap browser open --headed --session manual --proxy http://localhost:8888 URL`.
|
|
58
|
+
Complete the check in that browser. Keep the same session, profile, and proxy
|
|
59
|
+
for later commands. On a remote host, use a desktop connection to its display.
|
|
60
|
+
`virtual_display=True` alone does not provide a remote desktop.
|
|
61
|
+
|
|
62
|
+
Keep `profile_mode="native"` for the browser's own identity. On Linux with Xvfb,
|
|
63
|
+
`virtual_display=True` runs a headed browser without a visible desktop window.
|
|
64
|
+
Full profile emulation is useful for device tests, but cannot replace every
|
|
65
|
+
worker or iframe value. See the [identity limits](src/webskrap/guide.md).
|
|
53
66
|
|
|
54
67
|
## Search
|
|
55
68
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# DataDome website navigation diagnosis
|
|
2
|
+
|
|
3
|
+
Checked on 2026-10-29 against a DataDome-protected website.
|
|
4
|
+
|
|
5
|
+
## Root cause
|
|
6
|
+
|
|
7
|
+
The request reaches the website through the existing Gluetun VPN. DataDome then
|
|
8
|
+
chooses whether to show a device check, a CAPTCHA, or a hard block.
|
|
9
|
+
The headless browser received a hard-block document. That document has no
|
|
10
|
+
CAPTCHA widget. A headed browser on the same VPN exit received a working
|
|
11
|
+
manual slider. This is an external verification requirement.
|
|
12
|
+
|
|
13
|
+
WebSkrap also had three integration defects:
|
|
14
|
+
|
|
15
|
+
1. Detection recognized `ct.captcha-delivery.com/c.js`, but missed the
|
|
16
|
+
`i.js` device-check script before its iframe appeared.
|
|
17
|
+
2. It treated the device check as a manual challenge and did not wait for
|
|
18
|
+
its own script to reload the page.
|
|
19
|
+
3. It read the parent document's iframe `src`. A cross-origin iframe can
|
|
20
|
+
navigate itself while that attribute stays unchanged. A hard block then
|
|
21
|
+
received the same hint as a solvable CAPTCHA.
|
|
22
|
+
|
|
23
|
+
The optional headless user-agent probe also started a separate browser
|
|
24
|
+
without the session's proxy or network flags.
|
|
25
|
+
|
|
26
|
+
## Evidence
|
|
27
|
+
|
|
28
|
+
- Gluetun reported a healthy tunnel and a French exit. A browser request to
|
|
29
|
+
the IP check service returned the same exit as a request from the container.
|
|
30
|
+
- The proxy required authentication. Browser credentials stayed in memory.
|
|
31
|
+
- The container resolved the website and the CAPTCHA script, frame, and asset hosts.
|
|
32
|
+
Its BusyBox `nslookup` printed a parse error for one CNAME response. The
|
|
33
|
+
proxy still resolved and loaded that host successfully.
|
|
34
|
+
- The website returned HTTP 403 with `x-datadome: protected` and `x-dd-b: 3`.
|
|
35
|
+
- The device-check script and iframe returned HTTP 200. In the headed run,
|
|
36
|
+
CAPTCHA JavaScript, CSS, fonts, and logo also returned HTTP 200.
|
|
37
|
+
- Browser logs showed GPU and canvas warnings, but no failed CAPTCHA resource
|
|
38
|
+
request in the captured headed flow.
|
|
39
|
+
- The headless provider frame contained
|
|
40
|
+
`data-dd-response-page="hard-block"`. Its parent had no visible body text.
|
|
41
|
+
- The headed provider frame showed the manual verification slider.
|
|
42
|
+
- Cookies included `datadome`, `__cf_bm`, and `__cflb`. Cookie values and
|
|
43
|
+
challenge URLs are excluded from this report.
|
|
44
|
+
|
|
45
|
+
## Changes
|
|
46
|
+
|
|
47
|
+
Detection now reads the active provider frame through the existing CDP frame
|
|
48
|
+
code. It uses provider origin and markup, not page titles or translated text.
|
|
49
|
+
Device checks use the existing bounded interstitial wait. Frame inspection
|
|
50
|
+
uses the remaining navigation budget. Manual challenges stay open in persistent
|
|
51
|
+
sessions. Hard blocks receive the block hint.
|
|
52
|
+
|
|
53
|
+
The user-agent probe now keeps the session's launch flags, including its
|
|
54
|
+
proxy and WebRTC policy. No CAPTCHA solver, token injection, provider script
|
|
55
|
+
patch, new dependency, or site-specific branch was added.
|
|
56
|
+
|
|
57
|
+
## Manual completion
|
|
58
|
+
|
|
59
|
+
1. Start a headed persistent browser on a display you can access.
|
|
60
|
+
2. Configure the VPN proxy before navigation. Keep all page resources enabled.
|
|
61
|
+
3. Complete the CAPTCHA yourself. Use its retry control if it has expired.
|
|
62
|
+
4. Continue with the same browser, profile, and proxy. Do not copy challenge
|
|
63
|
+
tokens into another browser.
|
|
64
|
+
|
|
65
|
+
A temporary headed browser session and loopback VNC listener on port 15900 were used
|
|
66
|
+
to prepare manual completion. Both were closed when manual completion was
|
|
67
|
+
declined. Xvfb alone does not give remote access.
|
|
68
|
+
|
|
69
|
+
## Verification limits
|
|
70
|
+
|
|
71
|
+
The final Linux ARM64 run passed 858 tests. It excluded 30 opt-in live tests
|
|
72
|
+
for unrelated public sites. Coverage was 88.85%, above the 85% project gate.
|
|
73
|
+
The focused suite passed 123 tests before the combined run.
|
|
74
|
+
|
|
75
|
+
Ruff lint and format checks, Pyright for Linux, Windows, and macOS, Bandit,
|
|
76
|
+
package builds, and Twine metadata checks passed. OSV found no vulnerabilities
|
|
77
|
+
in the 51 public packages from the lockfile. Pip-audit also found none.
|
|
78
|
+
Windows and macOS runtime tests require those operating systems and were not
|
|
79
|
+
run on this host.
|
|
80
|
+
|
|
81
|
+
Local regression fixtures cover device-check completion, a transition to a
|
|
82
|
+
manual challenge, a transition to a hard block, cross-origin frame navigation,
|
|
83
|
+
proxy authentication, image loading, and continuation with the same cookies.
|
|
84
|
+
The fixture's manual button is a local test control. It does not contact or
|
|
85
|
+
complete real CAPTCHA verification.
|
|
86
|
+
|
|
87
|
+
Live tests reached the rendered CAPTCHA. Navigation beyond verification
|
|
88
|
+
cannot be confirmed until a person completes it. The provider can still
|
|
89
|
+
refuse the session after manual verification. No forms were submitted.
|
|
90
|
+
|
|
91
|
+
## VPN exit retest
|
|
92
|
+
|
|
93
|
+
Eight distinct French NordVPN exits were tested, including the initial
|
|
94
|
+
baseline. Each headed run returned HTTP 403 and a DataDome manual challenge.
|
|
95
|
+
Fresh browser profiles were used for the replacement exits. No CAPTCHA was
|
|
96
|
+
completed or bypassed. This sample did not identify a CAPTCHA-free exit.
|
|
97
|
+
It does not establish that IP reputation is the only input to the decision.
|
|
98
|
+
|
|
99
|
+
A control run placed the browser directly inside Gluetun's network namespace.
|
|
100
|
+
It used the same VPN exit without the HTTP proxy and still received a challenge.
|
|
101
|
+
The proxy is therefore not the sole cause.
|
|
102
|
+
|
|
103
|
+
Rotation also exposed an operational defect: Gluetun's NordVPN server catalog
|
|
104
|
+
was dated March 2024. Some listed endpoints timed out or rejected authentication.
|
|
105
|
+
The catalog was refreshed through Gluetun's existing updater. The original
|
|
106
|
+
catalog was backed up, and the VPN was restored to a healthy state.
|
|
107
|
+
|
|
108
|
+
The final verified exit was `187.13.198.190` in Marseille. It still required
|
|
109
|
+
verification. The refreshed catalog is in the container's writable layer;
|
|
110
|
+
container recreation can replace it because there is no catalog volume mount.
|
|
111
|
+
|
|
112
|
+
## CDP identity investigation
|
|
113
|
+
|
|
114
|
+
An ordinary headed Chromium process was launched on the same VPN exit with
|
|
115
|
+
no debugging port, no debugging pipe, and no WebSkrap page setup. Its native
|
|
116
|
+
network log recorded an HTTP 403, a device check, and then a CAPTCHA.
|
|
117
|
+
This establishes that CDP is not required to trigger the current challenge.
|
|
118
|
+
It does not reveal DataDome's private scoring rule.
|
|
119
|
+
|
|
120
|
+
A separate public fingerprint probe reported `isBot: false` for default
|
|
121
|
+
headed WebSkrap. It reported no CDP, webdriver, worker-value, or client-hint
|
|
122
|
+
inconsistency. The headed Mesa/locale configuration also passed that probe.
|
|
123
|
+
Both configurations still received a CAPTCHA. The headless run was
|
|
124
|
+
flagged for its bot user agent and weak signals, with no CDP leak reported.
|
|
125
|
+
These are observations from one public probe, not universal detection claims.
|
|
126
|
+
|
|
127
|
+
Local page-owned probes compared request headers and JavaScript values in
|
|
128
|
+
documents, cross-origin iframes, dedicated workers, and shared workers.
|
|
129
|
+
Two full-profile setup defects were reproduced and fixed:
|
|
130
|
+
|
|
131
|
+
1. A Windows or macOS user agent changed client hints but left the document's
|
|
132
|
+
`navigator.platform` at its native Linux value. The CDP override now sets
|
|
133
|
+
an unambiguous legacy platform as well. Unknown CPU values are not guessed.
|
|
134
|
+
2. A desktop viewport could be wider than its real browser window. The new
|
|
135
|
+
regression reproduced `innerWidth=1365` with `outerWidth=780`. Setup now
|
|
136
|
+
resizes the real window before applying device metrics. A missing command
|
|
137
|
+
on an older browser keeps the previous behavior; other errors propagate.
|
|
138
|
+
|
|
139
|
+
The changes use Chromium's own
|
|
140
|
+
[user-agent override](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setUserAgentOverride)
|
|
141
|
+
and [content-size command](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-setContentsSize).
|
|
142
|
+
No JavaScript getter replacements were added.
|
|
143
|
+
|
|
144
|
+
The probe also confirmed existing limits. UA masking removes high-entropy
|
|
145
|
+
client hints. Cross-process frames keep their real screen and pixel ratio.
|
|
146
|
+
Workers keep their native legacy platform, and shared workers keep their
|
|
147
|
+
native user agent. Full cross-platform emulation therefore cannot promise a
|
|
148
|
+
coherent identity across all contexts. Native mode leaves those values intact.
|
|
149
|
+
Neither the fixes nor changing the GPU and locale removed the website's verification
|
|
150
|
+
requirement in the measured runs.
|
|
151
|
+
|
|
152
|
+
The final follow-up suite ran inside Gluetun's network namespace. It passed
|
|
153
|
+
872 tests with 88.89% coverage and excluded 30 opt-in live tests. The focused
|
|
154
|
+
suite passed 125 tests. Lint, format, Linux/Windows/macOS type checks, Bandit,
|
|
155
|
+
OSV, pip-audit, package builds, and Twine checks passed. Windows and macOS
|
|
156
|
+
runtime checks were not available on this Linux host.
|
|
157
|
+
|
|
158
|
+
## Protected form retest
|
|
159
|
+
|
|
160
|
+
A later retest compared ordinary pages with the website's quote form path.
|
|
161
|
+
Each run used a fresh browser profile and a VPN exit that the run had not
|
|
162
|
+
used before.
|
|
163
|
+
|
|
164
|
+
The browser passed DataDome on ordinary pages. On some exits the first page
|
|
165
|
+
returned HTTP 403 with a device check (`x-dd-b: 3`). The device check then
|
|
166
|
+
reloaded into the requested page with HTTP 200. On other exits the first page
|
|
167
|
+
returned HTTP 200 at once. DataDome's page tag ran on those pages.
|
|
168
|
+
|
|
169
|
+
The quote form path returned HTTP 403 with a CAPTCHA on every exit. This
|
|
170
|
+
occurred with a direct load and with a click-through from the landing page.
|
|
171
|
+
These changes did not remove the CAPTCHA:
|
|
172
|
+
|
|
173
|
+
1. Google Chrome stable for Linux ARM64 instead of Chrome for Testing. This
|
|
174
|
+
adds the `Google Chrome` brand.
|
|
175
|
+
2. A locale and a timezone that match the exit country.
|
|
176
|
+
3. Mesa WebGL.
|
|
177
|
+
4. `x86` client hints instead of the host's `arm` value.
|
|
178
|
+
5. A 25-second pause with mouse movement and wheel scrolling on each page.
|
|
179
|
+
6. A four-page session with about 75 seconds of activity before the form.
|
|
180
|
+
7. Exits in eleven countries on eight hosting ASNs.
|
|
181
|
+
|
|
182
|
+
A desktop-like host environment did not change the result. It added Noto
|
|
183
|
+
and Ubuntu fonts and PulseAudio audio devices. On a fresh exit, a direct load
|
|
184
|
+
of the form received a device check (`x-dd-b: 3`). The check reloaded into a
|
|
185
|
+
CAPTCHA. The same check reloaded into HTTP 200 on ordinary pages.
|
|
186
|
+
|
|
187
|
+
SwiftShader WebGL failed the device check and received a hard block.
|
|
188
|
+
The VPN provider's dedicated-IP servers refused this setup's credentials,
|
|
189
|
+
so a dedicated exit was not tested.
|
|
190
|
+
|
|
191
|
+
A Windows user agent made the result worse: the first page returned a hard
|
|
192
|
+
block. Repeated fresh profiles on one exit also made the result worse. A later
|
|
193
|
+
visit received the CAPTCHA, or a hard block, without a device check.
|
|
194
|
+
|
|
195
|
+
The VPN provider's catalog contains only hosting ASNs. The measured limit is
|
|
196
|
+
therefore the exit IP class for this form path, not a browser defect that
|
|
197
|
+
WebSkrap can fix. A residential or mobile IP was not tested.
|
|
198
|
+
|
|
199
|
+
The retest found two WebSkrap defects:
|
|
200
|
+
|
|
201
|
+
1. Cookie decline did not search Commanders Act TrustCommander notices.
|
|
202
|
+
2. Full profiles put q-values in `navigator.languages` and doubled them in
|
|
203
|
+
`Accept-Language`.
|
|
204
|
+
|
|
205
|
+
A later local fingerprint check found two more:
|
|
206
|
+
|
|
207
|
+
1. With a full profile, `navigator.serviceWorker.register()` never
|
|
208
|
+
finished. The page and the browser both held the worker paused.
|
|
209
|
+
2. Mouse presses reported `PointerEvent.pressure` 0. A real mouse reports
|
|
210
|
+
0.5 while a button is down.
|
|
211
|
+
|
|
212
|
+
All four are fixed and have regression tests. No CAPTCHA was solved.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webskrap"
|
|
7
|
-
version = "3.2.
|
|
7
|
+
version = "3.2.2"
|
|
8
8
|
description = "Async Python scraping on Chromium over its own CDP client, with coherent browser profiles and session controls."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -31,7 +31,6 @@ from collections.abc import AsyncIterator, Awaitable, Callable
|
|
|
31
31
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
32
32
|
from dataclasses import dataclass
|
|
33
33
|
from pathlib import Path
|
|
34
|
-
from random import SystemRandom
|
|
35
34
|
from typing import Any
|
|
36
35
|
from uuid import uuid4
|
|
37
36
|
|
|
@@ -43,8 +42,6 @@ from webskrap.display import VirtualDisplay
|
|
|
43
42
|
from webskrap.profiles import get_profile
|
|
44
43
|
|
|
45
44
|
REPORT = Path(".webskrap/reports/driver-parity.json")
|
|
46
|
-
# Cursor jitter only; system entropy keeps the scanner quiet, as in human.py.
|
|
47
|
-
_rng = SystemRandom()
|
|
48
45
|
|
|
49
46
|
# A page passes when no known challenge is on it and its title is not a
|
|
50
47
|
# challenge or block title.
|
|
@@ -120,8 +117,6 @@ TARGETS: dict[str, tuple[str, str]] = {
|
|
|
120
117
|
Evaluate = Callable[[str], Awaitable[Any]]
|
|
121
118
|
#: Writes a PNG of the page's viewport to the given path.
|
|
122
119
|
Screenshot = Callable[[Path], Awaitable[None]]
|
|
123
|
-
#: Sends one mouse event: ("move" | "down" | "up", x, y) in viewport CSS pixels.
|
|
124
|
-
Mouse = Callable[[str, float, float], Awaitable[None]]
|
|
125
120
|
|
|
126
121
|
|
|
127
122
|
@dataclass
|
|
@@ -130,51 +125,11 @@ class Page:
|
|
|
130
125
|
|
|
131
126
|
evaluate: Evaluate
|
|
132
127
|
screenshot: Screenshot
|
|
133
|
-
mouse: Mouse
|
|
134
128
|
#: Returns DataDome's response page kind from its iframe, or None when the
|
|
135
129
|
#: path cannot read that iframe.
|
|
136
130
|
datadome_kind: Callable[[], Awaitable[str | None]] | None = None
|
|
137
131
|
|
|
138
132
|
|
|
139
|
-
# Finds the Turnstile widget: the parent of its response input, or else the
|
|
140
|
-
# first widget-high box. The host can span the content width while the 300 px
|
|
141
|
-
# iframe sits at its left edge, so only the height is checked. Returns the
|
|
142
|
-
# host's viewport rectangle.
|
|
143
|
-
_TURNSTILE_BOX = """(() => {
|
|
144
|
-
const fits = (r) => r.width >= 250 && r.height >= 55 && r.height <= 80;
|
|
145
|
-
const input = document.querySelector('input[name="cf-turnstile-response"]');
|
|
146
|
-
const candidates = [input && input.parentElement, ...document.querySelectorAll('div')];
|
|
147
|
-
for (const element of candidates) {
|
|
148
|
-
if (!element) continue;
|
|
149
|
-
const r = element.getBoundingClientRect();
|
|
150
|
-
if (fits(r)) return { x: r.left, y: r.top, w: r.width, h: r.height };
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
})()"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
async def human_click(mouse: Mouse, x: float, y: float) -> None:
|
|
157
|
-
"""Move along an eased, jittered curve to (x, y), then press and release.
|
|
158
|
-
|
|
159
|
-
Both paths get the same cursor path through CDP input events, so the
|
|
160
|
-
comparison measures the browser setup, not the cursor model.
|
|
161
|
-
"""
|
|
162
|
-
start_x, start_y = x + _rng.uniform(250, 450), y + _rng.uniform(150, 300)
|
|
163
|
-
control_x, control_y = (start_x + x) / 2 + _rng.uniform(-80, 80), start_y - 60
|
|
164
|
-
steps = _rng.randint(22, 32)
|
|
165
|
-
for step in range(1, steps + 1):
|
|
166
|
-
t = step / steps
|
|
167
|
-
t = t * t * (3 - 2 * t) # ease in and out
|
|
168
|
-
px = (1 - t) ** 2 * start_x + 2 * (1 - t) * t * control_x + t * t * x
|
|
169
|
-
py = (1 - t) ** 2 * start_y + 2 * (1 - t) * t * control_y + t * t * y
|
|
170
|
-
await mouse("move", px + _rng.uniform(-0.8, 0.8), py + _rng.uniform(-0.8, 0.8))
|
|
171
|
-
await asyncio.sleep(_rng.uniform(0.008, 0.022))
|
|
172
|
-
await asyncio.sleep(_rng.uniform(0.12, 0.3))
|
|
173
|
-
await mouse("down", x, y)
|
|
174
|
-
await asyncio.sleep(_rng.uniform(0.06, 0.12))
|
|
175
|
-
await mouse("up", x, y)
|
|
176
|
-
|
|
177
|
-
|
|
178
133
|
def _config(mode: str) -> SessionConfig:
|
|
179
134
|
return SessionConfig(
|
|
180
135
|
channel="chromium",
|
|
@@ -189,22 +144,6 @@ def _page(page: CDPPage) -> Page:
|
|
|
189
144
|
shot = await page.send("Page.captureScreenshot", {"format": "png"})
|
|
190
145
|
path.write_bytes(base64.b64decode(shot["data"]))
|
|
191
146
|
|
|
192
|
-
async def mouse(kind: str, x: float, y: float) -> None:
|
|
193
|
-
# A held left button reports buttons=1 until it is released.
|
|
194
|
-
event = {"move": "mouseMoved", "down": "mousePressed", "up": "mouseReleased"}[kind]
|
|
195
|
-
pressed = kind == "down"
|
|
196
|
-
await page.send(
|
|
197
|
-
"Input.dispatchMouseEvent",
|
|
198
|
-
{
|
|
199
|
-
"type": event,
|
|
200
|
-
"x": x,
|
|
201
|
-
"y": y,
|
|
202
|
-
"button": "none" if kind == "move" else "left",
|
|
203
|
-
"buttons": 1 if pressed else 0,
|
|
204
|
-
"clickCount": 0 if kind == "move" else 1,
|
|
205
|
-
},
|
|
206
|
-
)
|
|
207
|
-
|
|
208
147
|
# Isolated worlds: the page cannot see the checks, and they need no globals.
|
|
209
148
|
async def datadome_kind() -> str | None:
|
|
210
149
|
for frame in await cdp_dom.frames(page):
|
|
@@ -212,7 +151,7 @@ def _page(page: CDPPage) -> Page:
|
|
|
212
151
|
return await frame.page.evaluate(_DATADOME_KIND)
|
|
213
152
|
return None
|
|
214
153
|
|
|
215
|
-
return Page(page.evaluate, screenshot,
|
|
154
|
+
return Page(page.evaluate, screenshot, datadome_kind)
|
|
216
155
|
|
|
217
156
|
|
|
218
157
|
@asynccontextmanager
|
|
@@ -257,7 +196,7 @@ def _via_session() -> Callable[[str, str], AbstractAsyncContextManager[Page]]:
|
|
|
257
196
|
"""Run a target through a persistent session, as `webskrap browser` does.
|
|
258
197
|
|
|
259
198
|
Every call attaches anew, like each CLI command or MCP tool call. Sessions
|
|
260
|
-
run headless or headed; a virtual display
|
|
199
|
+
run headless or headed; a virtual display is not offered.
|
|
261
200
|
"""
|
|
262
201
|
|
|
263
202
|
@asynccontextmanager
|
|
@@ -281,10 +220,7 @@ def _via_session() -> Callable[[str, str], AbstractAsyncContextManager[Page]]:
|
|
|
281
220
|
async def screenshot(path: Path) -> None:
|
|
282
221
|
await act(lambda page: browser_session.screenshot(page, path, full_page=False))
|
|
283
222
|
|
|
284
|
-
|
|
285
|
-
raise NotImplementedError("--click is not offered with --via session")
|
|
286
|
-
|
|
287
|
-
yield Page(evaluate, screenshot, mouse)
|
|
223
|
+
yield Page(evaluate, screenshot)
|
|
288
224
|
finally:
|
|
289
225
|
browser_session.close_session(name, delete_data=True)
|
|
290
226
|
|
|
@@ -293,29 +229,6 @@ def _via_session() -> Callable[[str, str], AbstractAsyncContextManager[Page]]:
|
|
|
293
229
|
|
|
294
230
|
SESSION_DRIVERS = {"session": _via_session()}
|
|
295
231
|
|
|
296
|
-
# The widget box renders before the checkbox inside its iframe does.
|
|
297
|
-
WIDGET_SETTLE_S = 3.0
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
async def _click_turnstile(page: Page, shot: Path | None) -> bool:
|
|
301
|
-
"""Click the Turnstile checkbox once it shows; False while it is absent."""
|
|
302
|
-
box = None
|
|
303
|
-
with contextlib.suppress(Exception):
|
|
304
|
-
box = await page.evaluate(_TURNSTILE_BOX)
|
|
305
|
-
if not box:
|
|
306
|
-
await asyncio.sleep(1)
|
|
307
|
-
return False
|
|
308
|
-
await asyncio.sleep(WIDGET_SETTLE_S)
|
|
309
|
-
if shot is not None:
|
|
310
|
-
with contextlib.suppress(Exception):
|
|
311
|
-
shot.parent.mkdir(parents=True, exist_ok=True)
|
|
312
|
-
await page.screenshot(shot.with_name(f"{shot.stem}-before-click.png"))
|
|
313
|
-
# The checkbox sits near the widget's left edge, centered vertically.
|
|
314
|
-
x = box["x"] + 22 + _rng.uniform(-3, 3)
|
|
315
|
-
y = box["y"] + box["h"] / 2 + _rng.uniform(-3, 3)
|
|
316
|
-
await human_click(page.mouse, x, y)
|
|
317
|
-
return True
|
|
318
|
-
|
|
319
232
|
|
|
320
233
|
async def run_once(
|
|
321
234
|
driver: str,
|
|
@@ -323,7 +236,6 @@ async def run_once(
|
|
|
323
236
|
target: str,
|
|
324
237
|
wait_s: float,
|
|
325
238
|
shot: Path | None = None,
|
|
326
|
-
click: bool = False,
|
|
327
239
|
via: str = "launch",
|
|
328
240
|
) -> dict[str, Any]:
|
|
329
241
|
url, check = TARGETS[target]
|
|
@@ -332,7 +244,6 @@ async def run_once(
|
|
|
332
244
|
# A pass after a challenge means the challenge cleared; a pass without one
|
|
333
245
|
# means the target never challenged this run.
|
|
334
246
|
challenged = False
|
|
335
|
-
clicked = False
|
|
336
247
|
try:
|
|
337
248
|
drivers = SESSION_DRIVERS if via == "session" else DRIVERS
|
|
338
249
|
async with drivers[driver](url, mode) as page:
|
|
@@ -348,9 +259,6 @@ async def run_once(
|
|
|
348
259
|
challenged = challenged or state in ("challenge", "datadome", "captcha")
|
|
349
260
|
if state == "passed":
|
|
350
261
|
break
|
|
351
|
-
if click and not clicked and state == "challenge":
|
|
352
|
-
clicked = await _click_turnstile(page, shot)
|
|
353
|
-
continue
|
|
354
262
|
if state in _SETTLING and time.monotonic() - started >= SETTLE_S:
|
|
355
263
|
break
|
|
356
264
|
await asyncio.sleep(1)
|
|
@@ -368,7 +276,6 @@ async def run_once(
|
|
|
368
276
|
"target": target,
|
|
369
277
|
"seconds": round(time.monotonic() - started, 1),
|
|
370
278
|
"challenged": challenged,
|
|
371
|
-
"clicked": clicked,
|
|
372
279
|
**verdict,
|
|
373
280
|
}
|
|
374
281
|
|
|
@@ -390,11 +297,6 @@ async def main() -> None:
|
|
|
390
297
|
type=Path,
|
|
391
298
|
help="Write a PNG of each run's last state to this directory.",
|
|
392
299
|
)
|
|
393
|
-
parser.add_argument(
|
|
394
|
-
"--click",
|
|
395
|
-
action="store_true",
|
|
396
|
-
help="Click a Cloudflare Turnstile checkbox when a challenge shows it.",
|
|
397
|
-
)
|
|
398
300
|
parser.add_argument(
|
|
399
301
|
"--via",
|
|
400
302
|
choices=("launch", "session"),
|
|
@@ -402,8 +304,8 @@ async def main() -> None:
|
|
|
402
304
|
help="launch a browser per run, or go through a persistent session.",
|
|
403
305
|
)
|
|
404
306
|
args = parser.parse_args()
|
|
405
|
-
if args.via == "session" and
|
|
406
|
-
parser.error("--via session runs headless or headed,
|
|
307
|
+
if args.via == "session" and "virtual-display" in (args.mode or []):
|
|
308
|
+
parser.error("--via session runs headless or headed, not on a virtual display")
|
|
407
309
|
modes = args.mode or (
|
|
408
310
|
["headless"] if args.via == "session" else ["headless", "virtual-display"]
|
|
409
311
|
)
|
|
@@ -429,16 +331,13 @@ async def main() -> None:
|
|
|
429
331
|
if args.screenshots
|
|
430
332
|
else None
|
|
431
333
|
)
|
|
432
|
-
result = await run_once(
|
|
433
|
-
driver, mode, target, args.wait, shot, click=args.click, via=args.via
|
|
434
|
-
)
|
|
334
|
+
result = await run_once(driver, mode, target, args.wait, shot, via=args.via)
|
|
435
335
|
results.append(result)
|
|
436
336
|
cleared = result["challenged"] and result["state"] == "passed"
|
|
437
337
|
print(
|
|
438
338
|
f"{target:13} {mode:15} {driver:10} {result['state']:9} "
|
|
439
339
|
f"{result['seconds']:5}s {result['detail']}"
|
|
440
|
-
f"{' (after challenge)' if cleared else ''}"
|
|
441
|
-
f"{' [clicked]' if result['clicked'] else ''}",
|
|
340
|
+
f"{' (after challenge)' if cleared else ''}",
|
|
442
341
|
flush=True,
|
|
443
342
|
)
|
|
444
343
|
|
|
@@ -38,6 +38,10 @@ from webskrap.human import MAX_WHEEL_BATCHES, VIEW_MARGIN, bezier_path, click_po
|
|
|
38
38
|
#: then ``e`` and Chromium's backend node id.
|
|
39
39
|
REF_PATTERN = re.compile(r"^(?:f(\d+))?e(\d+)$")
|
|
40
40
|
_POLL_S = 0.1
|
|
41
|
+
#: ``force`` for a held mouse button. CDP's default of 0 reaches pages as
|
|
42
|
+
#: ``PointerEvent.pressure`` 0, but a real mouse with a button down reports
|
|
43
|
+
#: 0.5, as the Pointer Events spec asks of devices without pressure sensing.
|
|
44
|
+
MOUSE_PRESSED_FORCE = 0.5
|
|
41
45
|
|
|
42
46
|
# Builds the snapshot text with "[ref=#N]" placeholders and returns it with
|
|
43
47
|
# the listed elements, so the caller can swap each placeholder for the
|
|
@@ -917,7 +921,10 @@ async def _human_pointer(element: _Element, *, click_count: int = 0) -> None:
|
|
|
917
921
|
msg = f"another element covers the click point of: {element.description}"
|
|
918
922
|
raise WebSkrapError(msg, code=ErrorCode.USAGE)
|
|
919
923
|
press = {"x": x, "y": y, "button": "left", "clickCount": count}
|
|
920
|
-
await page.send(
|
|
924
|
+
await page.send(
|
|
925
|
+
"Input.dispatchMouseEvent",
|
|
926
|
+
{"type": "mousePressed", "buttons": 1, "force": MOUSE_PRESSED_FORCE, **press},
|
|
927
|
+
)
|
|
921
928
|
await asyncio.sleep(uniform(60, 140) / 1000)
|
|
922
929
|
await page.send("Input.dispatchMouseEvent", {"type": "mouseReleased", **press})
|
|
923
930
|
|
|
@@ -1091,7 +1098,8 @@ async def instant_click(page: CDPPage, target: str, *, click_count: int = 1) ->
|
|
|
1091
1098
|
for count in range(1, click_count + 1):
|
|
1092
1099
|
press = {"x": x, "y": y, "button": "left", "clickCount": count}
|
|
1093
1100
|
await element.input_page.send(
|
|
1094
|
-
"Input.dispatchMouseEvent",
|
|
1101
|
+
"Input.dispatchMouseEvent",
|
|
1102
|
+
{"type": "mousePressed", "buttons": 1, "force": MOUSE_PRESSED_FORCE, **press},
|
|
1095
1103
|
)
|
|
1096
1104
|
await element.input_page.send(
|
|
1097
1105
|
"Input.dispatchMouseEvent", {"type": "mouseReleased", **press}
|
|
@@ -407,7 +407,9 @@ async def goto(
|
|
|
407
407
|
finally:
|
|
408
408
|
documents.stop()
|
|
409
409
|
status = challenges.response_status(response)
|
|
410
|
-
found = await challenges.detect(
|
|
410
|
+
found = await challenges.detect(
|
|
411
|
+
page, challenges.response_headers(response), status, deadline=deadline
|
|
412
|
+
)
|
|
411
413
|
result: dict[str, Any] = {"status": status}
|
|
412
414
|
if decline_cookies:
|
|
413
415
|
result["cookie_notice_declined"] = (
|
|
@@ -750,6 +752,8 @@ _BUTTON_BITS = {"left": 1, "right": 2, "middle": 4}
|
|
|
750
752
|
async def _mouse(
|
|
751
753
|
page: CDPPage, kind: str, x: float, y: float, *, button: str = "none", **extra: Any
|
|
752
754
|
) -> None:
|
|
755
|
+
if extra.get("buttons"):
|
|
756
|
+
extra.setdefault("force", cdp_dom.MOUSE_PRESSED_FORCE)
|
|
753
757
|
await page.send(
|
|
754
758
|
"Input.dispatchMouseEvent",
|
|
755
759
|
{"type": kind, "x": x, "y": y, "button": button, **extra},
|