gecko-web-runtime-client 1.4.2__tar.gz → 1.4.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.
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/PACKAGE_README.md +41 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/PKG-INFO +48 -1
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/__init__.py +1 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/__init__.py +12 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/__main__.py +23 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_article.py +110 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_diagnostics.py +207 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_diagnostics_round.py +177 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_flow.py +1429 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_frame_context.py +61 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_logging.py +18 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_manual_data.py +135 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_slider_image.py +90 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_variant_resources.py +52 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/client.py +87 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/exceptions.py +5 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/__init__.py +1 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/capability_profile.json +47 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/computed-style.js +89 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/drag-control.js +9 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/drag-motion.js +40 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/drag-target.js +25 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/ft.module.js +1 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/host-timeline.js +43 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/input-events.js +103 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/layout_profile.json +465 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/manual-gesture.js +39 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/media_profile.json +19 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native-gl-client.js +130 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native-webgl.js +66 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native_gl.py +323 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native_gl_service.py +168 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/plugin_profile.json +4 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/rtproxy.js +143 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/sdk-handshake.js +21 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/tracker.module.js +1 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/var-speed-drag-control.js +10 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/webgl_profile.json +45 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/window-message-bridge.js +32 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/code.js +841 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/package-lock.json +401 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/package.json +10 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/executor.py +215 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/native_gl_service.py +164 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/resources/frame-context-manifest.json +12 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/resources/migration-manifest.json +259 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/resources/var-speed-resource-manifest.json +28 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/runtime.py +116 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/models.py +54 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/transport.py +48 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime_client.egg-info/PKG-INFO +48 -1
- gecko_web_runtime_client-1.4.4/gecko_web_runtime_client.egg-info/SOURCES.txt +90 -0
- gecko_web_runtime_client-1.4.4/gecko_web_runtime_client.egg-info/requires.txt +12 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/pyproject.toml +18 -1
- gecko_web_runtime_client-1.4.2/gecko_web_runtime_client.egg-info/SOURCES.txt +0 -42
- gecko_web_runtime_client-1.4.2/gecko_web_runtime_client.egg-info/requires.txt +0 -5
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/README.md +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/__init__.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/__main__.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/_leases.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/__init__.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/context.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/page.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/pool.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/runtime.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/data_plane.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/__init__.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/gc_cc.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/logging.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/memory.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/metrics.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/resources.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/http.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/installer.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/iocp_pipe.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/media_capture.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/media_downloader.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/models/__init__.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/models/navigation.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/__init__.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/_urls.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/client.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/exceptions.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/models.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/transport.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/pipe.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/session.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/__init__.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/turnstile.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/wechat.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/wechat_challenge.py +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime_client.egg-info/dependency_links.txt +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime_client.egg-info/top_level.txt +0 -0
- {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/setup.cfg +0 -0
|
@@ -15,6 +15,47 @@ access to `childclm/gecko-web-runtime`. The Python client depends on
|
|
|
15
15
|
`wpr-runtime-core` for Worker lifecycle and protocol compatibility. Historical
|
|
16
16
|
HTTP imports are retained through `wpr-http`.
|
|
17
17
|
|
|
18
|
+
## Optional HTTP WeChat integration
|
|
19
|
+
|
|
20
|
+
The same distribution includes `gecko_web_runtime.integrations.wechat`.
|
|
21
|
+
It uses `wpr-http` and a local Node.js environment, with no browser Page.
|
|
22
|
+
Install version 1.4.4 or newer with its optional dependencies, then prepare Node.js
|
|
23
|
+
20+ dependencies in the user's cache:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
pip install --upgrade "gecko-web-runtime-client[wechat]>=1.4.4"
|
|
27
|
+
python -m gecko_web_runtime.integrations.wechat install-js
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
These commands require a client build containing this integration. An older
|
|
31
|
+
published package does not acquire the integration by installing the extra.
|
|
32
|
+
Local development: `pip install -e "./python[wechat]"`.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from gecko_web_runtime.integrations.wechat import AsyncWechatClient
|
|
36
|
+
|
|
37
|
+
async with AsyncWechatClient(proxy="") as client:
|
|
38
|
+
result = await client.get(article_url)
|
|
39
|
+
print(result.summary())
|
|
40
|
+
if result.ok:
|
|
41
|
+
article_html = result.html
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Only Node dependencies are installed by `install-js`; a compatible Firefox
|
|
45
|
+
network runtime is installed separately through `wpr-http`. Native graphics
|
|
46
|
+
queries additionally require an installed Firefox ANGLE directory containing
|
|
47
|
+
`libEGL.dll` and `libGLESv2.dll`, passed as `native_gl_dir` or `NATIVE_GL_DIR`.
|
|
48
|
+
Direct article responses need neither Node nor native graphics.
|
|
49
|
+
|
|
50
|
+
One client retains one HTTP session and serializes its article flows. Separate
|
|
51
|
+
client instances have separate HTTP and JavaScript state. `total_timeout`
|
|
52
|
+
bounds an active flow; cancellation terminates an active Node subprocess.
|
|
53
|
+
The module uses Python logging and does not configure application handlers.
|
|
54
|
+
Artifacts are off by default; `output_dir` opts into per-call directories that
|
|
55
|
+
can contain session-sensitive diagnostic data. Keep those directories outside
|
|
56
|
+
source control. Site rejection is returned as a structured failure, not a
|
|
57
|
+
promise that any verification will succeed.
|
|
58
|
+
|
|
18
59
|
## Media capture
|
|
19
60
|
|
|
20
61
|
For a browser-played Widevine page, use `AsyncMediaDownloader`. It starts the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gecko-web-runtime-client
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: Thin Python client for the Firefox-derived WPR Gecko Worker protocol
|
|
5
5
|
Author: WPR project
|
|
6
6
|
License: MPL-2.0-compatible client; see the runtime bundle notices
|
|
@@ -12,6 +12,12 @@ Requires-Dist: wpr-runtime-core<0.3,>=0.2.2
|
|
|
12
12
|
Requires-Dist: wpr-http<0.3,>=0.2.0
|
|
13
13
|
Provides-Extra: diagnostics
|
|
14
14
|
Requires-Dist: loguru>=0.7.2; extra == "diagnostics"
|
|
15
|
+
Provides-Extra: wechat
|
|
16
|
+
Requires-Dist: wpr-http<0.3,>=0.2.1; extra == "wechat"
|
|
17
|
+
Requires-Dist: wpr-runtime-core<0.3,>=0.2.3; extra == "wechat"
|
|
18
|
+
Requires-Dist: Pillow<13,>=11; extra == "wechat"
|
|
19
|
+
Requires-Dist: numpy<3,>=2; extra == "wechat"
|
|
20
|
+
Requires-Dist: opencv-python-headless<6,>=4.10; extra == "wechat"
|
|
15
21
|
|
|
16
22
|
# Gecko Web Runtime Client
|
|
17
23
|
|
|
@@ -30,6 +36,47 @@ access to `childclm/gecko-web-runtime`. The Python client depends on
|
|
|
30
36
|
`wpr-runtime-core` for Worker lifecycle and protocol compatibility. Historical
|
|
31
37
|
HTTP imports are retained through `wpr-http`.
|
|
32
38
|
|
|
39
|
+
## Optional HTTP WeChat integration
|
|
40
|
+
|
|
41
|
+
The same distribution includes `gecko_web_runtime.integrations.wechat`.
|
|
42
|
+
It uses `wpr-http` and a local Node.js environment, with no browser Page.
|
|
43
|
+
Install version 1.4.4 or newer with its optional dependencies, then prepare Node.js
|
|
44
|
+
20+ dependencies in the user's cache:
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
pip install --upgrade "gecko-web-runtime-client[wechat]>=1.4.4"
|
|
48
|
+
python -m gecko_web_runtime.integrations.wechat install-js
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
These commands require a client build containing this integration. An older
|
|
52
|
+
published package does not acquire the integration by installing the extra.
|
|
53
|
+
Local development: `pip install -e "./python[wechat]"`.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from gecko_web_runtime.integrations.wechat import AsyncWechatClient
|
|
57
|
+
|
|
58
|
+
async with AsyncWechatClient(proxy="") as client:
|
|
59
|
+
result = await client.get(article_url)
|
|
60
|
+
print(result.summary())
|
|
61
|
+
if result.ok:
|
|
62
|
+
article_html = result.html
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Only Node dependencies are installed by `install-js`; a compatible Firefox
|
|
66
|
+
network runtime is installed separately through `wpr-http`. Native graphics
|
|
67
|
+
queries additionally require an installed Firefox ANGLE directory containing
|
|
68
|
+
`libEGL.dll` and `libGLESv2.dll`, passed as `native_gl_dir` or `NATIVE_GL_DIR`.
|
|
69
|
+
Direct article responses need neither Node nor native graphics.
|
|
70
|
+
|
|
71
|
+
One client retains one HTTP session and serializes its article flows. Separate
|
|
72
|
+
client instances have separate HTTP and JavaScript state. `total_timeout`
|
|
73
|
+
bounds an active flow; cancellation terminates an active Node subprocess.
|
|
74
|
+
The module uses Python logging and does not configure application handlers.
|
|
75
|
+
Artifacts are off by default; `output_dir` opts into per-call directories that
|
|
76
|
+
can contain session-sensitive diagnostic data. Keep those directories outside
|
|
77
|
+
source control. Site rejection is returned as a structured failure, not a
|
|
78
|
+
promise that any verification will succeed.
|
|
79
|
+
|
|
33
80
|
## Media capture
|
|
34
81
|
|
|
35
82
|
For a browser-played Widevine page, use `AsyncMediaDownloader`. It starts the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional site integrations shipped with the browser client."""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""HTTP and local-JavaScript WeChat article client."""
|
|
2
|
+
|
|
3
|
+
from .client import AsyncWechatClient
|
|
4
|
+
from .exceptions import WechatDependencyError
|
|
5
|
+
from .models import WechatArticle, WechatConfig
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AsyncWechatClient",
|
|
9
|
+
"WechatArticle",
|
|
10
|
+
"WechatConfig",
|
|
11
|
+
"WechatDependencyError",
|
|
12
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Install optional Node dependencies into the user's WPR cache."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from .js_runtime.runtime import LocalJsRuntime
|
|
6
|
+
from .models import WechatConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
parser = argparse.ArgumentParser()
|
|
11
|
+
parser.add_argument("command", choices=["install-js"])
|
|
12
|
+
parser.add_argument("--cache-dir")
|
|
13
|
+
parser.add_argument("--node", default="node")
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
runtime = LocalJsRuntime(
|
|
16
|
+
WechatConfig(js_cache_dir=args.cache_dir, node_executable=args.node)
|
|
17
|
+
)
|
|
18
|
+
print(runtime.install())
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Article parsing without a Node dependency or a hard-coded article ID."""
|
|
2
|
+
|
|
3
|
+
from html.parser import HTMLParser
|
|
4
|
+
import hashlib
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import parse_qs, urlsplit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ArticleParser(HTMLParser):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
super().__init__(convert_charrefs=True)
|
|
12
|
+
self.stack = []
|
|
13
|
+
self.title = []
|
|
14
|
+
self.content = []
|
|
15
|
+
self.media = []
|
|
16
|
+
self.body_found = False
|
|
17
|
+
|
|
18
|
+
def handle_starttag(self, tag, attrs):
|
|
19
|
+
values = dict(attrs)
|
|
20
|
+
title = values.get("id") == "activity-name" or any(x[1] for x in self.stack)
|
|
21
|
+
body = values.get("id") == "js_content" or any(x[2] for x in self.stack)
|
|
22
|
+
self.body_found |= values.get("id") == "js_content"
|
|
23
|
+
skip = tag in {"script", "style"} or any(x[3] for x in self.stack)
|
|
24
|
+
if body and tag in {
|
|
25
|
+
"img",
|
|
26
|
+
"iframe",
|
|
27
|
+
"video",
|
|
28
|
+
"mp-video",
|
|
29
|
+
"mp-common-videosnap",
|
|
30
|
+
}:
|
|
31
|
+
self.media.append(
|
|
32
|
+
{
|
|
33
|
+
"tag": tag,
|
|
34
|
+
"src": values.get("data-src") or values.get("src", ""),
|
|
35
|
+
"vid": values.get("vid") or values.get("data-vid", ""),
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
if body and tag in {"p", "section", "br", "h1", "h2"}:
|
|
39
|
+
self.content.append("\n")
|
|
40
|
+
if tag not in {
|
|
41
|
+
"area",
|
|
42
|
+
"base",
|
|
43
|
+
"br",
|
|
44
|
+
"col",
|
|
45
|
+
"embed",
|
|
46
|
+
"hr",
|
|
47
|
+
"img",
|
|
48
|
+
"input",
|
|
49
|
+
"link",
|
|
50
|
+
"meta",
|
|
51
|
+
"param",
|
|
52
|
+
"source",
|
|
53
|
+
"track",
|
|
54
|
+
"wbr",
|
|
55
|
+
}:
|
|
56
|
+
self.stack.append((tag, title, body, skip))
|
|
57
|
+
|
|
58
|
+
def handle_startendtag(self, tag, attrs):
|
|
59
|
+
self.handle_starttag(tag, attrs)
|
|
60
|
+
self.handle_endtag(tag)
|
|
61
|
+
|
|
62
|
+
def handle_endtag(self, tag):
|
|
63
|
+
for index in range(len(self.stack) - 1, -1, -1):
|
|
64
|
+
if self.stack[index][0] == tag:
|
|
65
|
+
if self.stack[index][2] and tag in {"p", "section", "h1", "h2"}:
|
|
66
|
+
self.content.append("\n")
|
|
67
|
+
del self.stack[index:]
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
def handle_data(self, data):
|
|
71
|
+
if not self.stack or self.stack[-1][3]:
|
|
72
|
+
return
|
|
73
|
+
if self.stack[-1][1]:
|
|
74
|
+
self.title.append(data)
|
|
75
|
+
if self.stack[-1][2]:
|
|
76
|
+
self.content.append(data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_article(html, url):
|
|
80
|
+
parser = ArticleParser()
|
|
81
|
+
parser.feed(html)
|
|
82
|
+
title = " ".join("".join(parser.title).split())
|
|
83
|
+
text = re.sub(
|
|
84
|
+
r"\n\s*\n+", "\n\n", re.sub(r"[ \t]+", " ", "".join(parser.content))
|
|
85
|
+
).strip()
|
|
86
|
+
query = parse_qs(urlsplit(url).query)
|
|
87
|
+
identity = {
|
|
88
|
+
f"target_{key}_matches": query[key][0] in html if query.get(key) else None
|
|
89
|
+
for key in ("mid", "sn")
|
|
90
|
+
}
|
|
91
|
+
verified = (
|
|
92
|
+
bool(title)
|
|
93
|
+
and parser.body_found
|
|
94
|
+
and (len(text) > 100 or bool(parser.media))
|
|
95
|
+
and not re.search(r"register_code\s*:", html)
|
|
96
|
+
and all(value is not False for value in identity.values())
|
|
97
|
+
)
|
|
98
|
+
proof = {
|
|
99
|
+
"title": title,
|
|
100
|
+
"content_chars": len(text),
|
|
101
|
+
"media_count": len(parser.media),
|
|
102
|
+
"media": parser.media,
|
|
103
|
+
"html_bytes": len(html.encode("utf-8")),
|
|
104
|
+
"html_sha256": hashlib.sha256(html.encode("utf-8")).hexdigest(),
|
|
105
|
+
"article_verified": verified,
|
|
106
|
+
**identity,
|
|
107
|
+
}
|
|
108
|
+
if not verified:
|
|
109
|
+
raise ValueError("Article title/body/target identity verification failed")
|
|
110
|
+
return proof, text
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Request-boundary observations; no proof generation or request mutation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import ipaddress
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import secrets
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from urllib.parse import urlsplit
|
|
16
|
+
|
|
17
|
+
_COOKIE_KEY = secrets.token_bytes(32)
|
|
18
|
+
COOKIE_SCOPE = secrets.token_hex(6)
|
|
19
|
+
EGRESS_URL = "https://myip.ipip.net"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def observe_proxy_routes(
|
|
23
|
+
started_at, ended_at, *, process_names=("gecko-net-worker.exe",)
|
|
24
|
+
):
|
|
25
|
+
"""Read only target/echo routing records; never export subscriptions or auth."""
|
|
26
|
+
base = (
|
|
27
|
+
Path(os.environ.get("APPDATA", ""))
|
|
28
|
+
/ "io.github.clash-verge-rev.clash-verge-rev"
|
|
29
|
+
)
|
|
30
|
+
hosts = {
|
|
31
|
+
"mp.weixin.qq.com",
|
|
32
|
+
"t.captcha.qq.com",
|
|
33
|
+
"captcha.gtimg.com",
|
|
34
|
+
"api.ipify.org",
|
|
35
|
+
"myip.ipip.net",
|
|
36
|
+
}
|
|
37
|
+
start = datetime.fromisoformat(started_at)
|
|
38
|
+
end = datetime.fromisoformat(ended_at)
|
|
39
|
+
rows = []
|
|
40
|
+
errors = []
|
|
41
|
+
for path in (base / "logs/service").glob("*.log"):
|
|
42
|
+
try:
|
|
43
|
+
for line in path.read_text(encoding="utf8", errors="replace").splitlines():
|
|
44
|
+
if not any(name in line for name in process_names) or not any(
|
|
45
|
+
h in line for h in hosts
|
|
46
|
+
):
|
|
47
|
+
continue
|
|
48
|
+
stamp = re.search(r'time="([^\"]+)"', line)
|
|
49
|
+
route = re.search(
|
|
50
|
+
r'--> ([^: ]+):(\d+) match (.*?) using (.*?)"?$', line
|
|
51
|
+
)
|
|
52
|
+
if not stamp or not route or route[1] not in hosts:
|
|
53
|
+
continue
|
|
54
|
+
observed = datetime.fromisoformat(stamp[1])
|
|
55
|
+
if start <= observed <= end:
|
|
56
|
+
rows.append(
|
|
57
|
+
{
|
|
58
|
+
"at": stamp[1],
|
|
59
|
+
"host": route[1],
|
|
60
|
+
"port": int(route[2]),
|
|
61
|
+
"rule": route[3],
|
|
62
|
+
"chain": route[4].rstrip('"'),
|
|
63
|
+
"sourceLog": path.name,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
except (OSError, ValueError) as error:
|
|
67
|
+
errors.append({"file": path.name, "type": type(error).__name__})
|
|
68
|
+
config = base / "clash-verge.yaml"
|
|
69
|
+
return {
|
|
70
|
+
"evidence": "read-only Mihomo connection logs; routing is not a server-side public-IP receipt",
|
|
71
|
+
"rows": sorted(rows, key=lambda row: row["at"]),
|
|
72
|
+
"errors": errors,
|
|
73
|
+
"configSha256": hashlib.sha256(config.read_bytes()).hexdigest()
|
|
74
|
+
if config.is_file()
|
|
75
|
+
else None,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def utc_now():
|
|
80
|
+
return datetime.now(timezone.utc).isoformat(timespec="milliseconds")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def safe_error_text(value):
|
|
84
|
+
text = str(value)
|
|
85
|
+
text = re.sub(
|
|
86
|
+
r'https?://[^\s\'"<>]+',
|
|
87
|
+
lambda m: (
|
|
88
|
+
urlsplit(m[0]).scheme
|
|
89
|
+
+ "://"
|
|
90
|
+
+ str(urlsplit(m[0]).hostname)
|
|
91
|
+
+ urlsplit(m[0]).path
|
|
92
|
+
),
|
|
93
|
+
text,
|
|
94
|
+
)
|
|
95
|
+
text = re.sub(
|
|
96
|
+
r"(?i)\b(cookie|authorization|ticket|randstr|sess|token|collect|eks)\s*[:=]\s*[^\s,;]+",
|
|
97
|
+
lambda m: m[1] + "=<redacted>",
|
|
98
|
+
text,
|
|
99
|
+
)
|
|
100
|
+
return re.sub(r"[A-Za-z0-9_+/=-]{48,}", "<opaque>", text)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def exception_diagnostic(error, *, phase, round_number=None):
|
|
104
|
+
return {
|
|
105
|
+
"type": type(error).__name__,
|
|
106
|
+
"message": safe_error_text(error),
|
|
107
|
+
"phase": phase,
|
|
108
|
+
"round": round_number,
|
|
109
|
+
"atUtc": utc_now(),
|
|
110
|
+
"notes": [safe_error_text(x) for x in getattr(error, "__notes__", [])],
|
|
111
|
+
"frames": [
|
|
112
|
+
{
|
|
113
|
+
"file": frame.filename,
|
|
114
|
+
"line": frame.lineno,
|
|
115
|
+
"function": frame.name,
|
|
116
|
+
"source": safe_error_text(frame.line or ""),
|
|
117
|
+
}
|
|
118
|
+
for frame in traceback.extract_tb(error.__traceback__)
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def native_gl_error_summary(entry):
|
|
124
|
+
keys = (
|
|
125
|
+
"query",
|
|
126
|
+
"stage",
|
|
127
|
+
"command",
|
|
128
|
+
"commandIndex",
|
|
129
|
+
"elapsedMs",
|
|
130
|
+
"executeMs",
|
|
131
|
+
"transportQueueMs",
|
|
132
|
+
"exitCode",
|
|
133
|
+
"exitCodeHex",
|
|
134
|
+
"processErrorCode",
|
|
135
|
+
"responseGenerated",
|
|
136
|
+
"backendPid",
|
|
137
|
+
"error",
|
|
138
|
+
"requestNumber",
|
|
139
|
+
)
|
|
140
|
+
return {
|
|
141
|
+
key: safe_error_text(entry[key]) if isinstance(entry[key], str) else entry[key]
|
|
142
|
+
for key in keys
|
|
143
|
+
if key in entry
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cookie_observation(value=None):
|
|
148
|
+
"""Tags are process-scoped; raw Cookie values are never written to disk."""
|
|
149
|
+
|
|
150
|
+
def tag(text):
|
|
151
|
+
return hmac.new(_COOKIE_KEY, text.encode("utf8"), hashlib.sha256).hexdigest()[
|
|
152
|
+
:24
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
pairs = []
|
|
156
|
+
if value is not None:
|
|
157
|
+
for part in value.split(";"):
|
|
158
|
+
if "=" not in part:
|
|
159
|
+
continue
|
|
160
|
+
name, content = part.strip().split("=", 1)
|
|
161
|
+
pairs.append(
|
|
162
|
+
{
|
|
163
|
+
"name": name,
|
|
164
|
+
"valueBytes": len(content.encode("utf8")),
|
|
165
|
+
"valueTag": tag(name + "\0" + content),
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
return {
|
|
169
|
+
"scope": COOKIE_SCOPE,
|
|
170
|
+
"headerSeen": value is not None,
|
|
171
|
+
"headerBytes": len(value.encode("utf8")) if value is not None else 0,
|
|
172
|
+
"headerTag": tag(value) if value is not None else None,
|
|
173
|
+
"pairs": pairs,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def observe_public_egress(client, endpoint=EGRESS_URL):
|
|
178
|
+
started = time.perf_counter()
|
|
179
|
+
result = {
|
|
180
|
+
"atUtc": utc_now(),
|
|
181
|
+
"endpoint": endpoint,
|
|
182
|
+
"evidence": "IP echo through this HTTP client; domain-based proxy routing may differ for target",
|
|
183
|
+
"targetEgressVerified": False,
|
|
184
|
+
}
|
|
185
|
+
try:
|
|
186
|
+
response = await asyncio.wait_for(
|
|
187
|
+
client.get(endpoint, header_policy="passthrough"), 6
|
|
188
|
+
)
|
|
189
|
+
raw = await asyncio.wait_for(response.read(), 6)
|
|
190
|
+
result["httpStatus"] = response.status_code
|
|
191
|
+
if response.status_code != 200:
|
|
192
|
+
raise RuntimeError("IP echo returned HTTP " + str(response.status_code))
|
|
193
|
+
text = raw.decode("utf8")
|
|
194
|
+
try:
|
|
195
|
+
address = ipaddress.ip_address(json.loads(text)["ip"])
|
|
196
|
+
except (ValueError, KeyError, TypeError):
|
|
197
|
+
found = re.search(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text)
|
|
198
|
+
if not found:
|
|
199
|
+
raise ValueError("IP echo did not contain an address")
|
|
200
|
+
address = ipaddress.ip_address(found[0])
|
|
201
|
+
if not address.is_global:
|
|
202
|
+
raise ValueError("IP echo returned a non-public address")
|
|
203
|
+
result["publicIp"] = str(address)
|
|
204
|
+
except Exception as error:
|
|
205
|
+
result["error"] = exception_diagnostic(error, phase="public_egress_echo")
|
|
206
|
+
result["elapsedMs"] = round((time.perf_counter() - started) * 1000, 2)
|
|
207
|
+
return result
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Credential-free summaries of launcher lifecycle and parent/child inputs."""
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from urllib.parse import urlsplit, parse_qsl
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import re
|
|
8
|
+
import secrets
|
|
9
|
+
|
|
10
|
+
_BINDING_KEY = secrets.token_bytes(32)
|
|
11
|
+
_BINDING_SCOPE = secrets.token_hex(6)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verification_response_summary(value):
|
|
15
|
+
"""Keep diagnostic status text, never credentials or arbitrary response values."""
|
|
16
|
+
|
|
17
|
+
def safe_text(text):
|
|
18
|
+
text = re.sub(r"https?://\S+", "<url>", str(text))
|
|
19
|
+
text = re.sub(r"[A-Za-z0-9_+/=.-]{24,}", "<opaque>", text)
|
|
20
|
+
return " ".join(text.split())[:160]
|
|
21
|
+
|
|
22
|
+
result = {
|
|
23
|
+
"keys": sorted(str(k) for k in value),
|
|
24
|
+
"ticketPresent": bool(value.get("ticket")),
|
|
25
|
+
"randstrPresent": bool(value.get("randstr")),
|
|
26
|
+
"sessPresent": bool(value.get("sess")),
|
|
27
|
+
}
|
|
28
|
+
for key in [
|
|
29
|
+
"errorCode",
|
|
30
|
+
"errorMessage",
|
|
31
|
+
"errMessage",
|
|
32
|
+
"code",
|
|
33
|
+
"message",
|
|
34
|
+
"subcode",
|
|
35
|
+
"subCode",
|
|
36
|
+
"errorType",
|
|
37
|
+
"errcode",
|
|
38
|
+
"errmsg",
|
|
39
|
+
"msg",
|
|
40
|
+
"ret",
|
|
41
|
+
"err_msg",
|
|
42
|
+
"err_type",
|
|
43
|
+
]:
|
|
44
|
+
if key in value and isinstance(value[key], (str, int, float, bool)):
|
|
45
|
+
result[key] = (
|
|
46
|
+
safe_text(value[key]) if isinstance(value[key], str) else value[key]
|
|
47
|
+
)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def binding_summary(
|
|
52
|
+
*,
|
|
53
|
+
source,
|
|
54
|
+
eks,
|
|
55
|
+
pre_sess,
|
|
56
|
+
submitted_sess,
|
|
57
|
+
pre_device,
|
|
58
|
+
submitted_device,
|
|
59
|
+
input_token,
|
|
60
|
+
output_token,
|
|
61
|
+
):
|
|
62
|
+
def tag(kind, value):
|
|
63
|
+
if not value:
|
|
64
|
+
return None
|
|
65
|
+
return hmac.new(
|
|
66
|
+
_BINDING_KEY, (kind + "\0" + str(value)).encode("utf8"), hashlib.sha256
|
|
67
|
+
).hexdigest()[:24]
|
|
68
|
+
|
|
69
|
+
candidates = [
|
|
70
|
+
match.group(2)
|
|
71
|
+
for match in re.finditer(
|
|
72
|
+
r"""window\.[A-Za-z_$][\w$]*\s*=\s*(['"])([A-Za-z0-9+/=]{100,})\1""", source
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
return {
|
|
76
|
+
"scope": _BINDING_SCOPE,
|
|
77
|
+
"scopeMeaning": "process-local keyed equality tags; not authentication evidence",
|
|
78
|
+
"eksMatchesSourceLiteral": eks in candidates if candidates else None,
|
|
79
|
+
"sourceLiteralCandidates": len(candidates),
|
|
80
|
+
"sessPresent": bool(pre_sess),
|
|
81
|
+
"sessMatchesPrehandle": submitted_sess == pre_sess,
|
|
82
|
+
"deviceIdMatchesPrehandleQuery": submitted_device == pre_device,
|
|
83
|
+
"inputTokenPresent": bool(input_token),
|
|
84
|
+
"outputTokenPresent": bool(output_token),
|
|
85
|
+
"tokenRetained": bool(input_token) and input_token == output_token,
|
|
86
|
+
"preSessTag": tag("sess", pre_sess),
|
|
87
|
+
"submittedSessTag": tag("sess", submitted_sess),
|
|
88
|
+
"eksTag": tag("eks", eks),
|
|
89
|
+
"inputTokenTag": tag("token", input_token),
|
|
90
|
+
"outputTokenTag": tag("token", output_token),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def request_boundary(name, method, url, headers, data=None):
|
|
95
|
+
parsed = urlsplit(url)
|
|
96
|
+
safe_headers = {}
|
|
97
|
+
for key, value in headers.items():
|
|
98
|
+
if key.lower() in {
|
|
99
|
+
"user-agent",
|
|
100
|
+
"accept",
|
|
101
|
+
"accept-language",
|
|
102
|
+
"accept-encoding",
|
|
103
|
+
"content-type",
|
|
104
|
+
"origin",
|
|
105
|
+
"sec-fetch-dest",
|
|
106
|
+
"sec-fetch-mode",
|
|
107
|
+
"sec-fetch-site",
|
|
108
|
+
}:
|
|
109
|
+
safe_headers[key.lower()] = str(value)
|
|
110
|
+
elif key.lower() == "referer":
|
|
111
|
+
ref = urlsplit(str(value))
|
|
112
|
+
safe_headers["referer"] = ref.scheme + "://" + ref.netloc + ref.path
|
|
113
|
+
shape = (
|
|
114
|
+
{str(key): len(str(value).encode("utf8")) for key, value in data.items()}
|
|
115
|
+
if isinstance(data, dict)
|
|
116
|
+
else None
|
|
117
|
+
)
|
|
118
|
+
return {
|
|
119
|
+
"name": name,
|
|
120
|
+
"method": str(method),
|
|
121
|
+
"endpoint": parsed.scheme + "://" + parsed.netloc + parsed.path,
|
|
122
|
+
"queryKeys": sorted({key for key, value in parse_qsl(parsed.query)}),
|
|
123
|
+
"preparedHeaders": safe_headers,
|
|
124
|
+
"bodyFieldBytes": shape,
|
|
125
|
+
"wireHeadersVerified": False,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def resource_mismatch(stage, expected, actual):
|
|
130
|
+
return {
|
|
131
|
+
"stage": stage,
|
|
132
|
+
"expected": expected,
|
|
133
|
+
"actual": actual,
|
|
134
|
+
"matched": expected == actual,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def summarize_round(generated, continuity, tdc_hash):
|
|
139
|
+
events = generated.get("events") or {}
|
|
140
|
+
handshake = generated.get("sdkHandshake") or {}
|
|
141
|
+
rows = (handshake.get("trackerData") or {}).get("slideValue") or []
|
|
142
|
+
messages = handshake.get("messages") or []
|
|
143
|
+
execution = generated.get("execution") or {}
|
|
144
|
+
return {
|
|
145
|
+
"tdcSha256": tdc_hash,
|
|
146
|
+
"lifecycle": generated.get("lifecycle") or [],
|
|
147
|
+
"phasesMs": execution.get("phases") or {},
|
|
148
|
+
"tokenContinuity": dict(continuity),
|
|
149
|
+
"parent": {
|
|
150
|
+
"rows": len(rows),
|
|
151
|
+
"eventTypes": dict(Counter(str(row[0]) for row in rows[1:] if row)),
|
|
152
|
+
"trustFlags": dict(
|
|
153
|
+
Counter(str(row[4]) for row in rows[1:] if len(row) > 4)
|
|
154
|
+
),
|
|
155
|
+
"inputScope": events.get("parentInput"),
|
|
156
|
+
"completeParentSdk": handshake.get("completeParentSdk"),
|
|
157
|
+
"messages": [
|
|
158
|
+
{k: item.get(k) for k in ["phase", "from", "to", "type"]}
|
|
159
|
+
for item in messages
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
"child": {
|
|
163
|
+
"eventTypes": events.get("types") or {},
|
|
164
|
+
"callbacks": events.get("callbacks"),
|
|
165
|
+
"trusted": events.get("trusted"),
|
|
166
|
+
"errorCount": len(events.get("errors") or []),
|
|
167
|
+
},
|
|
168
|
+
"motion": generated.get("dragMotion"),
|
|
169
|
+
"inputPlan": generated.get("dragInputPlan"),
|
|
170
|
+
"coordinates": generated.get("dragCoordinateAudit"),
|
|
171
|
+
"pageContext": generated.get("pageContext"),
|
|
172
|
+
**(
|
|
173
|
+
{"timeline": generated["hostTimeline"]}
|
|
174
|
+
if generated.get("hostTimeline")
|
|
175
|
+
else {}
|
|
176
|
+
),
|
|
177
|
+
}
|