webskrap 3.2.2__tar.gz → 3.2.4__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.2 → webskrap-3.2.4}/CHANGELOG.md +43 -1
- {webskrap-3.2.2 → webskrap-3.2.4}/CONTRIBUTING.md +1 -1
- {webskrap-3.2.2 → webskrap-3.2.4}/PKG-INFO +2 -1
- {webskrap-3.2.2 → webskrap-3.2.4}/SECURITY.md +9 -4
- {webskrap-3.2.2 → webskrap-3.2.4}/docs/diagnostics/datadome.md +27 -3
- {webskrap-3.2.2 → webskrap-3.2.4}/pyproject.toml +16 -3
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/browser_session.py +4 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/cdp.py +4 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/install.py +6 -1
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/search.py +13 -3
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/urls.py +4 -2
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_browser_session_unit.py +15 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_cli.py +2 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_install.py +12 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_mcp.py +2 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_search.py +24 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_urls.py +18 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/uv.lock +1 -1
- webskrap-3.2.2/.agents/skills/webskrap/SKILL.md +0 -151
- webskrap-3.2.2/.agents/skills/webskrap/references/cli.md +0 -154
- webskrap-3.2.2/.agents/skills/webskrap/references/mcp.md +0 -130
- webskrap-3.2.2/.agents/skills/webskrap/references/python-api.md +0 -147
- webskrap-3.2.2/.claude/skills/webskrap/SKILL.md +0 -1
- webskrap-3.2.2/.codex/skills/webskrap/SKILL.md +0 -1
- webskrap-3.2.2/tests/test_docs.py +0 -44
- {webskrap-3.2.2 → webskrap-3.2.4}/.gitattributes +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/.gitignore +0 -0
- {webskrap-3.2.2/.package-dist.dZyl3m → webskrap-3.2.4/.package-dist.sya6Sl}/.gitignore +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/LICENSE +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/README.md +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/benchmarks.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/docs/design/cdp-driver.md +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/docs/migration-3.0.md +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/scripts/driver_parity.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/scripts/live_stealth_report.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/__init__.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/browser_cli.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/cdp_dom.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/cdp_session.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/cdp_setup.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/challenges.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/cli.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/cli_output.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/client.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/consent.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/diagnostics.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/display.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/errors.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/fetch_runtime.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/guide.md +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/human.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/mcp_server.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/models.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/parsing.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/paths.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/profiles.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/proxy_auth.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/py.typed +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/src/webskrap/text_search.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/conftest.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/accepted-202.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/akamai-invalid-url-400.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/akamai-reference-403.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/akamai-sec-cpt.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/api-403.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/article-access-denied.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/cloudflare-1020.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/cloudflare-custom-block.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/blocks/not-found-404.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/search/bing.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/search/bing_challenge.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/search/bing_no_results.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/search/ddg.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/search/ddg_challenge.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/fixtures/search/ddg_no_results.html +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/live_stealth_helpers.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_bot_detection.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_bot_detection_headless.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_browser_cli.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_browser_integration.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_browser_vision.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_cdp.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_cdp_browser.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_cdp_child_targets.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_cdp_session.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_cdp_setup.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_challenges.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_client_unit.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_consent.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_consent_live.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_diagnostics.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_display.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_errors.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_fetch_runtime.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_human.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_live_stealth_helpers.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_live_stealth_report.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_mcp_efficiency.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_models.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_parsing.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_paths.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_profiles.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_search_live.py +0 -0
- {webskrap-3.2.2 → webskrap-3.2.4}/tests/test_text_search.py +0 -0
|
@@ -9,6 +9,46 @@ history, so they summarize each release rather than list every change.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [3.2.4] - 2026-11-01
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Python 3.15 is a supported version. CI runs the test suite on it, and the
|
|
17
|
+
package metadata lists it.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The source distribution no longer contains the agent skill folders and the
|
|
22
|
+
test that checks them. They stay in the Git repository. Before, the sdist
|
|
23
|
+
held only one of the linked skill reference paths, and its test failed.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The CLI guide lists the exit status for each error code. It said before that
|
|
28
|
+
every browser command failure exits with 1.
|
|
29
|
+
- The MCP guide and the agent reference now describe `browser_wait_for`.
|
|
30
|
+
- `SECURITY.md` names the supported release line without an old version
|
|
31
|
+
number.
|
|
32
|
+
|
|
33
|
+
## [3.2.3] - 2026-10-30
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- MCP URL checks now decode host escapes and treat backslashes as path
|
|
38
|
+
separators before checking for private addresses, as Chromium does.
|
|
39
|
+
- Browser installation rejects ZIP entries with Windows separators or drive
|
|
40
|
+
components that could write outside the install folder.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- On macOS, Chrome for Testing no longer asks for the "Chromium Safe Storage"
|
|
45
|
+
keychain password at each launch. Launches now use `--use-mock-keychain`.
|
|
46
|
+
Upgrade note: on macOS, persistent sessions and `user_data_dir` profiles
|
|
47
|
+
made before this release used the real Keychain key. They lose their saved
|
|
48
|
+
cookies and logins once, and you must sign in again.
|
|
49
|
+
- Search skips malformed result URLs instead of discarding all results from
|
|
50
|
+
the page. This also applies to URLs decoded from search-engine redirects.
|
|
51
|
+
|
|
12
52
|
## [3.2.2] - 2026-10-30
|
|
13
53
|
|
|
14
54
|
### Fixed
|
|
@@ -961,7 +1001,9 @@ entries below.
|
|
|
961
1001
|
|
|
962
1002
|
Earlier releases (0.1.0 - 0.4.9) are recorded in the git history and tags only.
|
|
963
1003
|
|
|
964
|
-
[Unreleased]: https://github.com/kacigaya/webskrap/compare/v3.2.
|
|
1004
|
+
[Unreleased]: https://github.com/kacigaya/webskrap/compare/v3.2.4...HEAD
|
|
1005
|
+
[3.2.4]: https://github.com/kacigaya/webskrap/compare/v3.2.3...v3.2.4
|
|
1006
|
+
[3.2.3]: https://github.com/kacigaya/webskrap/compare/v3.2.2...v3.2.3
|
|
965
1007
|
[3.2.2]: https://github.com/kacigaya/webskrap/compare/v3.2.1...v3.2.2
|
|
966
1008
|
[3.2.1]: https://github.com/kacigaya/webskrap/compare/v3.2.0...v3.2.1
|
|
967
1009
|
[3.2.0]: https://github.com/kacigaya/webskrap/compare/v3.1.0...v3.2.0
|
|
@@ -5,7 +5,7 @@ what a reviewable pull request looks like.
|
|
|
5
5
|
|
|
6
6
|
## Requirements
|
|
7
7
|
|
|
8
|
-
- Python 3.11, 3.12, 3.13, or 3.
|
|
8
|
+
- Python 3.11, 3.12, 3.13, 3.14, or 3.15 (CI runs all five).
|
|
9
9
|
- [uv](https://docs.astral.sh/uv/) for environments and dependencies.
|
|
10
10
|
- A Chromium build for browser tests, installed via `webskrap install`.
|
|
11
11
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webskrap
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.4
|
|
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
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
21
|
Classifier: Typing :: Typed
|
|
21
22
|
Requires-Python: >=3.11
|
|
22
23
|
Requires-Dist: mcp<3,>=2
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
WebSkrap is developed on `main` and released from tags. Fixes land in a new
|
|
6
6
|
patch release on top of the latest minor; older minors are not backported.
|
|
7
7
|
|
|
8
|
-
| Version
|
|
9
|
-
|
|
|
10
|
-
| Latest minor
|
|
11
|
-
| Older minors
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
| ------------ | --------- |
|
|
10
|
+
| Latest minor | Yes |
|
|
11
|
+
| Older minors | No |
|
|
12
12
|
|
|
13
13
|
## Reporting a vulnerability
|
|
14
14
|
|
|
@@ -65,6 +65,11 @@ to permit them. The Python API and CLI take URLs from the operator and skip
|
|
|
65
65
|
the DNS check; point them at internal hosts deliberately, not with
|
|
66
66
|
page-derived input.
|
|
67
67
|
|
|
68
|
+
This check applies to the supplied URL. It does not filter redirects,
|
|
69
|
+
subresources, or requests from page scripts. DNS can also change after the
|
|
70
|
+
check. Use network-level restrictions when the browser must not reach private
|
|
71
|
+
services. The URL check alone does not provide full SSRF isolation.
|
|
72
|
+
|
|
68
73
|
### Chromium sandbox
|
|
69
74
|
|
|
70
75
|
Persistent sessions keep Chromium's OS sandbox. That sandbox is what stops a
|
|
@@ -66,6 +66,30 @@ A temporary headed browser session and loopback VNC listener on port 15900 were
|
|
|
66
66
|
to prepare manual completion. Both were closed when manual completion was
|
|
67
67
|
declined. Xvfb alone does not give remote access.
|
|
68
68
|
|
|
69
|
+
### Reuse a solved session
|
|
70
|
+
|
|
71
|
+
A persistent session keeps its cookies in its profile on disk
|
|
72
|
+
(`~/.webskrap/browser/<session>/user-data`). The DataDome cookie from a
|
|
73
|
+
manual solve stays there. No extra flag or export step is necessary.
|
|
74
|
+
|
|
75
|
+
1. Open the session by the same name:
|
|
76
|
+
`webskrap browser open --session <name> --headed <url>`.
|
|
77
|
+
2. Keep the same exit IP. The cookie belongs to the session that passed
|
|
78
|
+
the check.
|
|
79
|
+
3. Do not use the profile headless. A headless visit can receive a new
|
|
80
|
+
challenge or a hard block, and DataDome can then replace the cookie.
|
|
81
|
+
|
|
82
|
+
On 2026-10-30, a macOS run checked this on a DataDome website from a
|
|
83
|
+
residential IP. A person solved the CAPTCHA once in a headed session. A
|
|
84
|
+
reload then returned HTTP 200. The session was closed, and a new browser
|
|
85
|
+
process opened the same profile. A direct load of the protected page
|
|
86
|
+
returned HTTP 200 again, without a challenge. This run did not measure how
|
|
87
|
+
long the cookie stays valid. DataDome can challenge the session again at
|
|
88
|
+
any time.
|
|
89
|
+
|
|
90
|
+
WebSkrap does not export the cookie or copy it into other browsers. That is
|
|
91
|
+
token reuse, which is out of scope.
|
|
92
|
+
|
|
69
93
|
## Verification limits
|
|
70
94
|
|
|
71
95
|
The final Linux ARM64 run passed 858 tests. It excluded 30 opt-in live tests
|
|
@@ -105,9 +129,9 @@ was dated March 2024. Some listed endpoints timed out or rejected authentication
|
|
|
105
129
|
The catalog was refreshed through Gluetun's existing updater. The original
|
|
106
130
|
catalog was backed up, and the VPN was restored to a healthy state.
|
|
107
131
|
|
|
108
|
-
The final verified exit
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
The final verified exit still required verification. The refreshed catalog is
|
|
133
|
+
in the container's writable layer; container recreation can replace it because
|
|
134
|
+
there is no catalog volume mount.
|
|
111
135
|
|
|
112
136
|
## CDP identity investigation
|
|
113
137
|
|
|
@@ -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.4"
|
|
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"
|
|
@@ -22,6 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
24
|
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: Python :: 3.15",
|
|
25
26
|
"Typing :: Typed",
|
|
26
27
|
]
|
|
27
28
|
dependencies = [
|
|
@@ -56,8 +57,20 @@ packages = ["src/webskrap"]
|
|
|
56
57
|
|
|
57
58
|
[tool.hatch.build.targets.sdist]
|
|
58
59
|
# The web/ frontend ships only to GitHub Pages, never to PyPI. assets/ holds
|
|
59
|
-
# the README logo, which no install step reads.
|
|
60
|
-
|
|
60
|
+
# the README logo, which no install step reads. The agent skill folders link
|
|
61
|
+
# to one shared references/ copy, and Hatch keeps only one path to a linked
|
|
62
|
+
# directory, so they stay in the Git checkout together with test_docs.py,
|
|
63
|
+
# which checks them.
|
|
64
|
+
exclude = [
|
|
65
|
+
"web",
|
|
66
|
+
".github",
|
|
67
|
+
"assets",
|
|
68
|
+
".agents",
|
|
69
|
+
".claude",
|
|
70
|
+
".codex",
|
|
71
|
+
"references",
|
|
72
|
+
"tests/test_docs.py",
|
|
73
|
+
]
|
|
61
74
|
|
|
62
75
|
[tool.pytest.ini_options]
|
|
63
76
|
asyncio_mode = "auto"
|
|
@@ -499,6 +499,10 @@ def launch_browser(
|
|
|
499
499
|
f"--user-data-dir={user_data_dir}",
|
|
500
500
|
"--no-first-run",
|
|
501
501
|
"--no-default-browser-check",
|
|
502
|
+
# Chrome for Testing is unsigned on macOS, so every launch asks the
|
|
503
|
+
# user for the "Chromium Safe Storage" keychain item. The mock keychain
|
|
504
|
+
# keeps cookie encryption profile-local; pages cannot see it.
|
|
505
|
+
*(["--use-mock-keychain"] if sys.platform == "darwin" else []),
|
|
502
506
|
# A bfcache restore re-fires no load events, stranding the
|
|
503
507
|
# go_back/go_forward load wait. Each command attaches anew and cannot
|
|
504
508
|
# track restores across attachments, so trade bfcache for
|
|
@@ -861,6 +861,10 @@ async def launch(
|
|
|
861
861
|
f"--user-data-dir={profile}",
|
|
862
862
|
"--no-first-run",
|
|
863
863
|
"--no-default-browser-check",
|
|
864
|
+
# Chrome for Testing is unsigned on macOS, so every launch asks the
|
|
865
|
+
# user for the "Chromium Safe Storage" keychain item. The mock keychain
|
|
866
|
+
# keeps cookie encryption profile-local; pages cannot see it.
|
|
867
|
+
*(["--use-mock-keychain"] if sys.platform == "darwin" else []),
|
|
864
868
|
# Chromium builds (not branded Chrome) apply their testing field
|
|
865
869
|
# trials by default. Stable Chrome never runs that config, and one of
|
|
866
870
|
# its trials trims navigator.languages to a single entry.
|
|
@@ -164,7 +164,12 @@ def _download(url: str, target: IO[bytes]) -> int:
|
|
|
164
164
|
def _safe_member(name: str) -> PurePosixPath:
|
|
165
165
|
"""Return ``name`` as a relative path, refusing absolute or ``..`` paths."""
|
|
166
166
|
path = PurePosixPath(name)
|
|
167
|
-
if
|
|
167
|
+
if (
|
|
168
|
+
path.is_absolute()
|
|
169
|
+
or ".." in path.parts
|
|
170
|
+
or "\\" in name
|
|
171
|
+
or any(":" in part for part in path.parts)
|
|
172
|
+
):
|
|
168
173
|
msg = f"refusing archive entry outside the install folder: {name}"
|
|
169
174
|
raise WebSkrapError(msg, code=ErrorCode.BROWSER_LAUNCH)
|
|
170
175
|
return path
|
|
@@ -165,7 +165,10 @@ def _ddg_unwrap(href: str) -> str:
|
|
|
165
165
|
"""
|
|
166
166
|
if href.startswith("//"):
|
|
167
167
|
href = "https:" + href
|
|
168
|
-
|
|
168
|
+
try:
|
|
169
|
+
parts = urlsplit(href)
|
|
170
|
+
except ValueError:
|
|
171
|
+
return href
|
|
169
172
|
if parts.netloc.endswith("duckduckgo.com") and parts.path.startswith("/l/"):
|
|
170
173
|
return parse_qs(parts.query).get("uddg", [""])[0]
|
|
171
174
|
return href
|
|
@@ -200,7 +203,10 @@ def _bing_unwrap(href: str) -> str:
|
|
|
200
203
|
URL-safe base64 of the destination, without padding. Anything that does
|
|
201
204
|
not decode comes back as given rather than failing the whole page.
|
|
202
205
|
"""
|
|
203
|
-
|
|
206
|
+
try:
|
|
207
|
+
parts = urlsplit(href)
|
|
208
|
+
except ValueError:
|
|
209
|
+
return href
|
|
204
210
|
if not (parts.netloc.endswith("bing.com") and parts.path == "/ck/a"):
|
|
205
211
|
return href
|
|
206
212
|
encoded = parse_qs(parts.query).get("u", [""])[0]
|
|
@@ -292,7 +298,11 @@ def parse_results(engine: SearchEngine, html: str) -> list[SearchHit]:
|
|
|
292
298
|
seen: set[str] = set()
|
|
293
299
|
hits: list[SearchHit] = []
|
|
294
300
|
for hit in spec.hits(root):
|
|
295
|
-
|
|
301
|
+
try:
|
|
302
|
+
scheme = urlsplit(hit.url).scheme
|
|
303
|
+
except ValueError:
|
|
304
|
+
continue
|
|
305
|
+
if scheme not in ("http", "https") or hit.url in seen:
|
|
296
306
|
continue
|
|
297
307
|
seen.add(hit.url)
|
|
298
308
|
hits.append(hit)
|
|
@@ -19,7 +19,7 @@ import asyncio
|
|
|
19
19
|
import ipaddress
|
|
20
20
|
import os
|
|
21
21
|
import socket
|
|
22
|
-
from urllib.parse import urlsplit
|
|
22
|
+
from urllib.parse import unquote, urlsplit
|
|
23
23
|
|
|
24
24
|
from webskrap.errors import ErrorCode, WebSkrapError
|
|
25
25
|
|
|
@@ -122,7 +122,9 @@ async def reject_private_network(url: str) -> None:
|
|
|
122
122
|
if urlsplit(url).scheme.lower() not in ("http", "https"):
|
|
123
123
|
# data: and about:blank perform no network request; nothing to guard.
|
|
124
124
|
return
|
|
125
|
-
|
|
125
|
+
# Chromium treats backslashes as path separators and decodes host escapes.
|
|
126
|
+
# Check the host it will contact, not urllib's literal authority.
|
|
127
|
+
hostname = unquote(urlsplit(url.replace("\\", "/")).hostname or "")
|
|
126
128
|
if _host_is_blocked_literal(hostname):
|
|
127
129
|
blocked = True
|
|
128
130
|
else:
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import signal
|
|
6
6
|
import stat
|
|
7
7
|
import subprocess
|
|
8
|
+
import sys
|
|
8
9
|
import threading
|
|
9
10
|
from datetime import datetime, timedelta
|
|
10
11
|
from pathlib import Path
|
|
@@ -62,6 +63,20 @@ def _launch(tmp_path: Path, **kwargs: Any) -> None:
|
|
|
62
63
|
browser_session.launch_browser(tmp_path, executable="/bin/chromium", **kwargs)
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
def test_launch_uses_mock_keychain_on_macos_only(
|
|
67
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
68
|
+
) -> None:
|
|
69
|
+
commands = _capture_launch(monkeypatch)
|
|
70
|
+
|
|
71
|
+
monkeypatch.setattr(sys, "platform", "darwin")
|
|
72
|
+
_launch(tmp_path, headless=True)
|
|
73
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
74
|
+
_launch(tmp_path, headless=True)
|
|
75
|
+
|
|
76
|
+
assert "--use-mock-keychain" in commands[0]
|
|
77
|
+
assert "--use-mock-keychain" not in commands[1]
|
|
78
|
+
|
|
79
|
+
|
|
65
80
|
def test_launch_sandboxes_by_default(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
66
81
|
commands = _capture_launch(monkeypatch)
|
|
67
82
|
|
|
@@ -786,6 +786,8 @@ def test_schema_human_format_lists_commands() -> None:
|
|
|
786
786
|
|
|
787
787
|
|
|
788
788
|
def test_fetch_stays_sandboxed_by_default(monkeypatch: Any) -> None:
|
|
789
|
+
# The host can opt out globally; this test checks the built-in default.
|
|
790
|
+
monkeypatch.delenv("WEBSKRAP_CHROMIUM_SANDBOX", raising=False)
|
|
789
791
|
_fake_client(monkeypatch)
|
|
790
792
|
|
|
791
793
|
result = runner.invoke(cli.app, ["fetch", "https://example.test", "--format", "json"])
|
|
@@ -183,3 +183,15 @@ def test_install_refuses_an_archive_without_the_browser(
|
|
|
183
183
|
def test_browsers_dir_honors_the_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
184
184
|
monkeypatch.setenv("WEBSKRAP_BROWSERS_PATH", str(tmp_path))
|
|
185
185
|
assert install.browsers_dir() == tmp_path
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.parametrize(
|
|
189
|
+
"name",
|
|
190
|
+
[r"..\escape", r"\escape", r"\\server\share\escape", "folder/C:/escape", "folder/C:escape"],
|
|
191
|
+
)
|
|
192
|
+
def test_extract_refuses_windows_path_escapes(tmp_path: Path, name: str) -> None:
|
|
193
|
+
archive = _archive(tmp_path, [(name, b"x", stat.S_IFREG | 0o644)])
|
|
194
|
+
with pytest.raises(WebSkrapError) as error:
|
|
195
|
+
install.extract(archive, tmp_path / "out")
|
|
196
|
+
assert error.value.code is ErrorCode.BROWSER_LAUNCH
|
|
197
|
+
assert not (tmp_path / "out").exists()
|
|
@@ -734,6 +734,8 @@ def test_guide_documents_every_error_code() -> None:
|
|
|
734
734
|
pytest.param("ftp://example.test/x", id="ftp"),
|
|
735
735
|
pytest.param("https://user:pass@example.test/", id="userinfo"),
|
|
736
736
|
pytest.param("http://169.254.169.254/", id="link-local"),
|
|
737
|
+
pytest.param("http://%31%32%37.0.0.1/", id="encoded-loopback"),
|
|
738
|
+
pytest.param(r"http://127.0.0.1\example.com/", id="backslash-host"),
|
|
737
739
|
],
|
|
738
740
|
)
|
|
739
741
|
def test_fetch_rejects_untrusted_targets_before_launch(monkeypatch: Any, url: str) -> None:
|
|
@@ -7,7 +7,9 @@ engine changes its markup, refresh the fixture and fix the one extractor.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import base64
|
|
10
11
|
from pathlib import Path
|
|
12
|
+
from urllib.parse import quote
|
|
11
13
|
|
|
12
14
|
import pytest
|
|
13
15
|
|
|
@@ -241,3 +243,25 @@ def test_unclosed_and_void_tags_do_not_swallow_later_results() -> None:
|
|
|
241
243
|
assert [hit.url for hit in hits] == ["https://a.example/", "https://b.example/"]
|
|
242
244
|
assert hits[0].title == "A one"
|
|
243
245
|
assert hits[0].snippet == "first"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@pytest.mark.parametrize("engine", list(SearchEngine))
|
|
249
|
+
@pytest.mark.parametrize("wrapped", [False, True])
|
|
250
|
+
def test_malformed_result_url_keeps_valid_neighbors(engine: SearchEngine, wrapped: bool) -> None:
|
|
251
|
+
href = "http://[broken"
|
|
252
|
+
if wrapped:
|
|
253
|
+
if engine is SearchEngine.DDG:
|
|
254
|
+
href = "https://duckduckgo.com/l/?uddg=" + quote(href, safe="")
|
|
255
|
+
else:
|
|
256
|
+
payload = base64.urlsafe_b64encode(href.encode()).decode().rstrip("=")
|
|
257
|
+
href = "https://www.bing.com/ck/a?u=a1" + payload
|
|
258
|
+
if engine is SearchEngine.DDG:
|
|
259
|
+
template = '<div class="result"><a class="result__a" href="{}">hit</a></div>'
|
|
260
|
+
else:
|
|
261
|
+
template = '<li class="b_algo"><h2><a href="{}">hit</a></h2></li>'
|
|
262
|
+
html = '<ol id="b_results">' + template.format(href) + template.format("https://example.com/")
|
|
263
|
+
html += "</ol>"
|
|
264
|
+
|
|
265
|
+
assert parse_results(engine, html) == [
|
|
266
|
+
SearchHit(title="hit", url="https://example.com/", snippet="")
|
|
267
|
+
]
|
|
@@ -75,6 +75,10 @@ def test_private_net_opt_in_values(monkeypatch: pytest.MonkeyPatch, value: str)
|
|
|
75
75
|
"url",
|
|
76
76
|
[
|
|
77
77
|
pytest.param("http://127.0.0.1:8000/", id="loopback-v4"),
|
|
78
|
+
pytest.param("http://%31%32%37.0.0.1/", id="encoded-loopback"),
|
|
79
|
+
pytest.param("http://%6cocalhost/", id="encoded-localhost"),
|
|
80
|
+
pytest.param(r"http://127.0.0.1\example.com/", id="backslash-host"),
|
|
81
|
+
pytest.param(r"http://%31%32%37.0.0.1\example.com/", id="encoded-backslash-host"),
|
|
78
82
|
pytest.param("http://[::1]/", id="loopback-v6"),
|
|
79
83
|
pytest.param("http://10.0.0.1/", id="rfc1918-10"),
|
|
80
84
|
pytest.param("http://192.168.1.1/", id="rfc1918-192"),
|
|
@@ -145,3 +149,17 @@ def test_validate_mcp_url_combines_both_checks(monkeypatch: pytest.MonkeyPatch)
|
|
|
145
149
|
asyncio.run(validate_mcp_url("http://169.254.169.254/"))
|
|
146
150
|
|
|
147
151
|
assert asyncio.run(validate_mcp_url("https://example.com/")) == "https://example.com/"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_private_network_resolves_decoded_public_hostname(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
155
|
+
monkeypatch.delenv(PRIVATE_NET_ENV, raising=False)
|
|
156
|
+
hosts: list[str] = []
|
|
157
|
+
|
|
158
|
+
def resolve(hostname: str) -> bool:
|
|
159
|
+
hosts.append(hostname)
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
monkeypatch.setattr("webskrap.urls._resolve_blocked", resolve)
|
|
163
|
+
url = "https://%65xample.com/path%20name"
|
|
164
|
+
assert asyncio.run(validate_mcp_url(url)) == url
|
|
165
|
+
assert hosts == ["example.com"]
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: webskrap
|
|
3
|
-
description: Use when writing, debugging, documenting, or reviewing Python scraping, web search, and visual browser automation with WebSkrap. Not for ordinary HTTP clients or search APIs.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# WebSkrap
|
|
7
|
-
|
|
8
|
-
WebSkrap is an async Python scraping package that drives Chromium through its
|
|
9
|
-
own Chrome DevTools Protocol (CDP) client, with no Playwright. The Python API,
|
|
10
|
-
`webskrap` CLI, and `webskrap-mcp` server share core behavior.
|
|
11
|
-
|
|
12
|
-
WebSkrap loads known URLs. To find URLs it loads DuckDuckGo or Bing's
|
|
13
|
-
results page in the same browser and parses the hits. It calls no search API
|
|
14
|
-
and it does not offer Google.
|
|
15
|
-
|
|
16
|
-
## Workflow
|
|
17
|
-
|
|
18
|
-
1. Identify the affected public surface: Python, CLI, MCP, or more than one.
|
|
19
|
-
2. Read its implementation and nearest tests. Trace calls into shared code
|
|
20
|
-
before editing.
|
|
21
|
-
3. Change the shared implementation when behavior belongs to more than one
|
|
22
|
-
surface. Do not patch each wrapper independently.
|
|
23
|
-
4. Update affected public contracts: models, serialization, errors, command
|
|
24
|
-
help, MCP schemas, guides, and tests.
|
|
25
|
-
5. Run focused tests during development, then the full validation gate before
|
|
26
|
-
handoff when practical.
|
|
27
|
-
|
|
28
|
-
## Route by task
|
|
29
|
-
|
|
30
|
-
| Task | Implementation | Tests and contracts |
|
|
31
|
-
| --- | --- | --- |
|
|
32
|
-
| Fetching, sessions, links, or screenshots | `src/webskrap/client.py`, `src/webskrap/models.py` | `tests/test_client_unit.py`, `tests/test_models.py` |
|
|
33
|
-
| Search engines, redirect unwrapping, or results parsing | `src/webskrap/search.py`, `src/webskrap/models.py` | `tests/test_search.py` and its fixtures under `tests/fixtures/search/` |
|
|
34
|
-
| Parsing | `src/webskrap/parsing.py` | `tests/test_parsing.py` |
|
|
35
|
-
| Errors, hints, or exit statuses | `src/webskrap/errors.py`, `src/webskrap/cli_output.py` | `tests/test_errors.py`, CLI and MCP callers |
|
|
36
|
-
| Main CLI commands | `src/webskrap/cli.py` | `tests/test_cli.py`, `tests/test_diagnostics.py` |
|
|
37
|
-
| Persistent browser state | `src/webskrap/browser_session.py`, `src/webskrap/paths.py` | `tests/test_browser_session_unit.py`, `tests/test_paths.py` |
|
|
38
|
-
| Interactive browser commands | `src/webskrap/browser_cli.py`, `src/webskrap/mcp_server.py` | `tests/test_browser_cli.py`, `tests/test_browser_integration.py`, `tests/test_mcp.py` |
|
|
39
|
-
| MCP tools, resources, or annotations | `src/webskrap/mcp_server.py` | `tests/test_mcp.py`, `src/webskrap/guide.md` |
|
|
40
|
-
| Profiles or stealth defaults | `src/webskrap/profiles.py`, `src/webskrap/models.py`, `src/webskrap/client.py` | `tests/test_profiles.py`, opt-in bot-detection tests when needed |
|
|
41
|
-
| Cookie rejection | `src/webskrap/consent.py` | `tests/test_consent.py`; `tests/test_consent_live.py` only for selector rot |
|
|
42
|
-
| Documentation and examples | `README.md`, `src/webskrap/guide.md` | `tests/test_docs.py`, current CLI help, public Python exports |
|
|
43
|
-
|
|
44
|
-
Search for every constructor, serializer, command, and schema that uses a
|
|
45
|
-
changed model or field. Fetch and search behavior cross Python, CLI, and MCP.
|
|
46
|
-
Persistent browser behavior crosses the browser CLI and MCP. Engine markup
|
|
47
|
-
lives only in `src/webskrap/search.py`. When an engine changes its page,
|
|
48
|
-
refresh that engine's fixture and fix that one extractor.
|
|
49
|
-
|
|
50
|
-
## Choose the public surface
|
|
51
|
-
|
|
52
|
-
| Need | Python | CLI | MCP |
|
|
53
|
-
| --- | --- | --- | --- |
|
|
54
|
-
| Fetch one known URL | `client.fetch()` | `webskrap fetch` | `stealth_fetch` |
|
|
55
|
-
| Find URLs for a query | `client.search()` | `webskrap search` | `search` |
|
|
56
|
-
| Preserve cookies or storage | `client.session()` | `webskrap browser open` | `browser_open` |
|
|
57
|
-
| See a rendered page or interact with canvas | `browser_session.view()` on a session page | `webskrap browser view` and `mouse` | `browser_view` and `browser_mouse` |
|
|
58
|
-
| Click, fill, wait, or run a flow | `session.new_page()` and `human_click()` | `webskrap browser` | `browser_interact` and related tools |
|
|
59
|
-
| Diagnose installation | Inspect raised error | `webskrap doctor` | `doctor` |
|
|
60
|
-
|
|
61
|
-
Use a one-shot fetch for one page. Use a persistent browser only when state or
|
|
62
|
-
interaction must survive between operations. Do not repeatedly fetch a page to
|
|
63
|
-
drive a multi-step flow.
|
|
64
|
-
|
|
65
|
-
Read only the reference needed for the task:
|
|
66
|
-
|
|
67
|
-
- [Python API](references/python-api.md): sessions, pages, profile modes, consent, links,
|
|
68
|
-
and fingerprint controls.
|
|
69
|
-
- [CLI](references/cli.md): one-shot fetches, persistent browser commands, and
|
|
70
|
-
Linux ARM64 behavior.
|
|
71
|
-
- [MCP](references/mcp.md): tool selection, confined paths, annotations, and
|
|
72
|
-
keeping tool results small.
|
|
73
|
-
|
|
74
|
-
## Visual browser interaction
|
|
75
|
-
|
|
76
|
-
Use `browser_view` (MCP) or `webskrap browser view --format json` (CLI) when
|
|
77
|
-
layout, images, canvas, or controls missing from the accessibility tree matter.
|
|
78
|
-
MCP returns an image block directly. CLI saves a PNG; open its returned `path`
|
|
79
|
-
with the agent's image-viewing tool before choosing coordinates. A path or
|
|
80
|
-
base64 text alone does not give a model vision. The client and model must
|
|
81
|
-
support image input.
|
|
82
|
-
|
|
83
|
-
The view covers the viewport at one PNG pixel per CSS pixel. Coordinates start
|
|
84
|
-
at its top-left; use the original `width` and `height` if the image viewer
|
|
85
|
-
resizes it. Do not add `scroll_x` or `scroll_y` to mouse coordinates. Full-page
|
|
86
|
-
screenshots are for inspection or export and do not map directly to the mouse.
|
|
87
|
-
|
|
88
|
-
Prefer refs/selectors when they identify a control. Use `browser_mouse` / CLI
|
|
89
|
-
`mouse` for visual targets, then `browser_insert_text` / CLI `insert-text` for
|
|
90
|
-
literal text in the focused control and `browser_press` / CLI `press` for keys.
|
|
91
|
-
Wait for the expected change and take a fresh view after scrolling, navigation,
|
|
92
|
-
or layout changes. Coordinate clicks hit whatever occupies that point.
|
|
93
|
-
Run dependent actions sequentially within a session.
|
|
94
|
-
|
|
95
|
-
This is the browser's rendered page, available headless without Xvfb. It does
|
|
96
|
-
not show browser chrome, OS dialogs, or a desktop stream. One-shot fetch
|
|
97
|
-
sessions are separate; open a persistent browser to drive a visual flow.
|
|
98
|
-
Read the [CLI](references/cli.md) or [MCP](references/mcp.md) reference for examples.
|
|
99
|
-
|
|
100
|
-
## Guardrails
|
|
101
|
-
|
|
102
|
-
- Use public pages, local test servers, or targets the user is authorized to
|
|
103
|
-
access.
|
|
104
|
-
- Do not add CAPTCHA solving, login-wall bypass, credential bypass, or
|
|
105
|
-
access-control circumvention.
|
|
106
|
-
- Do not commit cookies, storage state, proxy credentials, or persistent browser
|
|
107
|
-
data such as `.webskrap/`.
|
|
108
|
-
- Preserve Chromium's sandbox by default. Any opt-out must remain explicit and
|
|
109
|
-
outside page-controlled input.
|
|
110
|
-
- Keep MCP file paths confined to their configured roots. Reject absolute paths,
|
|
111
|
-
traversal, and symlink escapes.
|
|
112
|
-
- Prefer public `webskrap` exports. Use private helpers only in tests.
|
|
113
|
-
- Keep changes typed. Add focused tests for parsing, state transitions, output
|
|
114
|
-
contracts, and tool-safety behavior.
|
|
115
|
-
|
|
116
|
-
## Public contract checklist
|
|
117
|
-
|
|
118
|
-
When public behavior changes, check all applicable items:
|
|
119
|
-
|
|
120
|
-
- Python models and exports
|
|
121
|
-
- CLI arguments, help, JSON output, and exit status
|
|
122
|
-
- MCP input schema, result shape, annotations, and server instructions
|
|
123
|
-
- `README.md` and `src/webskrap/guide.md`
|
|
124
|
-
- unit tests for each affected surface
|
|
125
|
-
|
|
126
|
-
Use the implementation as the source of truth. In particular, read
|
|
127
|
-
`src/webskrap/models.py` for result fields, `src/webskrap/errors.py` for error
|
|
128
|
-
codes and exits, and `src/webskrap/diagnostics.py` for environment reporting.
|
|
129
|
-
Do not copy these catalogs into the skill.
|
|
130
|
-
|
|
131
|
-
## Validation
|
|
132
|
-
|
|
133
|
-
Run the nearest tests while editing. Before handoff, run the repository gate
|
|
134
|
-
when practical:
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
uv run --extra dev pytest -q -m "not browser and not live"
|
|
138
|
-
uv run --extra dev ruff check .
|
|
139
|
-
uv run --extra dev ruff format --check .
|
|
140
|
-
uvx --from pyright==1.1.411 pyright --pythonpath .venv/bin/python src/webskrap
|
|
141
|
-
uv run --extra dev bandit -c pyproject.toml -q -r src/
|
|
142
|
-
uv run --extra dev --with pip-audit==2.9.0 pip-audit
|
|
143
|
-
uv build
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Run browser tests when browser behavior changes and the required browsers are
|
|
147
|
-
installed.
|
|
148
|
-
|
|
149
|
-
Use `WEBSKRAP_LIVE=1 pytest -q -m live` only when explicitly checking public
|
|
150
|
-
third-party behavior. Live tests are non-deterministic and do not replace the
|
|
151
|
-
default suite.
|