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.
Files changed (94) hide show
  1. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/PACKAGE_README.md +41 -0
  2. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/PKG-INFO +48 -1
  3. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/__init__.py +1 -0
  4. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/__init__.py +12 -0
  5. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/__main__.py +23 -0
  6. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_article.py +110 -0
  7. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_diagnostics.py +207 -0
  8. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_diagnostics_round.py +177 -0
  9. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_flow.py +1429 -0
  10. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_frame_context.py +61 -0
  11. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_logging.py +18 -0
  12. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_manual_data.py +135 -0
  13. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_slider_image.py +90 -0
  14. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/_variant_resources.py +52 -0
  15. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/client.py +87 -0
  16. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/exceptions.py +5 -0
  17. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/__init__.py +1 -0
  18. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/capability_profile.json +47 -0
  19. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/computed-style.js +89 -0
  20. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/drag-control.js +9 -0
  21. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/drag-motion.js +40 -0
  22. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/drag-target.js +25 -0
  23. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/ft.module.js +1 -0
  24. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/host-timeline.js +43 -0
  25. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/input-events.js +103 -0
  26. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/layout_profile.json +465 -0
  27. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/manual-gesture.js +39 -0
  28. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/media_profile.json +19 -0
  29. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native-gl-client.js +130 -0
  30. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native-webgl.js +66 -0
  31. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native_gl.py +323 -0
  32. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/native_gl_service.py +168 -0
  33. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/plugin_profile.json +4 -0
  34. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/rtproxy.js +143 -0
  35. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/sdk-handshake.js +21 -0
  36. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/tracker.module.js +1 -0
  37. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/var-speed-drag-control.js +10 -0
  38. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/webgl_profile.json +45 -0
  39. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/assets/window-message-bridge.js +32 -0
  40. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/code.js +841 -0
  41. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/package-lock.json +401 -0
  42. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/environment/package.json +10 -0
  43. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/executor.py +215 -0
  44. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/native_gl_service.py +164 -0
  45. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/resources/frame-context-manifest.json +12 -0
  46. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/resources/migration-manifest.json +259 -0
  47. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/resources/var-speed-resource-manifest.json +28 -0
  48. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/js_runtime/runtime.py +116 -0
  49. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/models.py +54 -0
  50. gecko_web_runtime_client-1.4.4/gecko_web_runtime/integrations/wechat/transport.py +48 -0
  51. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime_client.egg-info/PKG-INFO +48 -1
  52. gecko_web_runtime_client-1.4.4/gecko_web_runtime_client.egg-info/SOURCES.txt +90 -0
  53. gecko_web_runtime_client-1.4.4/gecko_web_runtime_client.egg-info/requires.txt +12 -0
  54. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/pyproject.toml +18 -1
  55. gecko_web_runtime_client-1.4.2/gecko_web_runtime_client.egg-info/SOURCES.txt +0 -42
  56. gecko_web_runtime_client-1.4.2/gecko_web_runtime_client.egg-info/requires.txt +0 -5
  57. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/README.md +0 -0
  58. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/__init__.py +0 -0
  59. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/__main__.py +0 -0
  60. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/_leases.py +0 -0
  61. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/__init__.py +0 -0
  62. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/context.py +0 -0
  63. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/page.py +0 -0
  64. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/pool.py +0 -0
  65. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/api/runtime.py +0 -0
  66. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/data_plane.py +0 -0
  67. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/__init__.py +0 -0
  68. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/gc_cc.py +0 -0
  69. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/logging.py +0 -0
  70. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/memory.py +0 -0
  71. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/metrics.py +0 -0
  72. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/diagnostics/resources.py +0 -0
  73. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/http.py +0 -0
  74. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/installer.py +0 -0
  75. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/iocp_pipe.py +0 -0
  76. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/media_capture.py +0 -0
  77. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/media_downloader.py +0 -0
  78. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/models/__init__.py +0 -0
  79. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/models/navigation.py +0 -0
  80. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/__init__.py +0 -0
  81. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/_urls.py +0 -0
  82. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/client.py +0 -0
  83. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/exceptions.py +0 -0
  84. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/models.py +0 -0
  85. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/network/transport.py +0 -0
  86. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/pipe.py +0 -0
  87. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/session.py +0 -0
  88. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/__init__.py +0 -0
  89. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/turnstile.py +0 -0
  90. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/wechat.py +0 -0
  91. {gecko_web_runtime_client-1.4.2 → gecko_web_runtime_client-1.4.4}/gecko_web_runtime/workflows/wechat_challenge.py +0 -0
  92. {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
  93. {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
  94. {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.2
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
+ }