diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3e21c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +src/probe/reports +asset +docs +.claude +.venv +CLAUDE.md +body.bin +node_modules diff --git a/analyze_new.py b/analyze_new.py deleted file mode 100644 index ce1789e..0000000 --- a/analyze_new.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env python3 -""" -专用解析脚本:chatgpt.com-*.log 格式 -每行结构: hsw.js:2 {"tag":"索引点","tH":N,"Ig":"..."} -Ig 值含义:被检测的浏览器 API 构造函数名 / 属性名 / 返回值 -""" - -import re -import json -import sys -import glob -from collections import defaultdict, OrderedDict - -# ── 自动找日志文件 ────────────────────────────────────────── -def find_log(path_arg=None): - if path_arg: - return path_arg - candidates = sorted(glob.glob("/home/carry/myprj/hcaptcha/asset/chatgpt.com-*.log")) - if not candidates: - print("❌ 未找到 chatgpt.com-*.log,请手动传入路径") - sys.exit(1) - return candidates[-1] # 取最新的 - - -# ── 解析 ──────────────────────────────────────────────────── -def parse(path): - entries = [] - with open(path, encoding="utf-8") as f: - for lineno, line in enumerate(f, 1): - line = line.strip() - m = re.match(r'hsw\.js:\d+\s+(.*)', line) - if not m: - continue - body = m.group(1).strip() - if body.startswith('{'): - try: - obj = json.loads(body) - if obj.get("tag") == "索引点": - entries.append({ - "lineno": lineno, - "tH": obj["tH"], - "has_ig": "Ig" in obj, - "ig": obj.get("Ig"), # 可能是 str/int/bool/None - }) - except json.JSONDecodeError: - pass - return entries - - -# ── 汇总 ──────────────────────────────────────────────────── -def summarize(entries): - """ - 对每个 tH,按出现顺序收集所有 Ig 值(去重保序)。 - 分类: - - has_value : Ig 有实际内容 - - no_ig : 完全没有 Ig 字段 - """ - tH_igs = defaultdict(list) # tH -> [ig, ...](有序去重后) - tH_no_ig = defaultdict(int) # tH -> 出现次数(无 Ig 的) - tH_lines = defaultdict(list) # tH -> 首次出现行号 - - seen = defaultdict(set) # 用于 Ig 去重 - - for e in entries: - tH = e["tH"] - tH_lines[tH].append(e["lineno"]) - - if e["has_ig"]: - ig = e["ig"] - key = repr(ig) - if key not in seen[tH]: - seen[tH].add(key) - tH_igs[tH].append(ig) - else: - tH_no_ig[tH] += 1 - - return tH_igs, tH_no_ig, tH_lines - - -# ── 打印报告 ───────────────────────────────────────────────── -def report(tH_igs, tH_no_ig, tH_lines): - all_tH = sorted(set(list(tH_igs.keys()) + list(tH_no_ig.keys()))) - - print("=" * 68) - print(" HSW 新日志分析 — 每个索引点(tH)访问的浏览器 API") - print("=" * 68) - - # 分组输出 - has_value = [] - only_no_ig = [] - - for tH in all_tH: - igs = tH_igs.get(tH, []) - no = tH_no_ig.get(tH, 0) - if igs: - has_value.append((tH, igs, no)) - else: - only_no_ig.append((tH, no)) - - # ── 有值的 tH ── - print(f"\n✅ 有 Ig 值的索引点 ({len(has_value)} 个)\n") - print(f" {'tH':<6} {'Ig 值(去重、按出现顺序)'}") - print(f" {'─'*6} {'─'*56}") - for tH, igs, no_cnt in has_value: - # 格式化 Ig 列表 - parts = [] - for v in igs: - if isinstance(v, str) and len(v) > 60: - parts.append(v[:57] + "...") - else: - parts.append(repr(v) if not isinstance(v, str) else v) - ig_str = " | ".join(parts) - suffix = f" (另有 {no_cnt} 次无Ig)" if no_cnt else "" - print(f" tH={tH:<4d} {ig_str}{suffix}") - - # ── 只有 no_ig 的 tH ── - print(f"\n🟠 仅无 Ig 字段的索引点 ({len(only_no_ig)} 个) ← void 路径或未命中\n") - print(f" {'tH':<6} {'出现次数'}") - print(f" {'─'*6} {'─'*10}") - for tH, cnt in only_no_ig: - print(f" tH={tH:<4d} {cnt} 次") - - # ── 按 API 类别归纳 ── - print(f"\n{'─'*68}") - print(" 📋 API 检测归纳(每个 tH 在检测什么)") - print(f"{'─'*68}\n") - - # 已知含义映射(根据常见 hCaptcha 指纹逻辑) - known = { - "Window": "全局 window 对象", - "Promise": "Promise 构造函数检测", - "Object": "Object 原型检测", - "Performance": "performance API", - "performance": "window.performance 属性", - "Crypto": "window.crypto API", - "Uint8Array": "TypedArray (crypto.getRandomValues)", - "OfflineAudioContext": "AudioContext 指纹", - "RTCPeerConnection": "WebRTC 检测", - "fetch": "fetch API 检测", - "Request": "fetch Request 构造函数", - "Screen": "screen 对象", - "Storage": "localStorage / sessionStorage", - "IDBFactory": "indexedDB", - "HTMLDocument": "document 类型", - "HTMLCanvasElement": "Canvas 元素检测", - "CanvasRenderingContext2D": "2D Canvas 渲染上下文", - "Navigator": "navigator 对象", - "webdriver": "navigator.webdriver 检测(bot检测关键)", - "languages": "navigator.languages", - "Array": "Array 类型检测", - "getEntriesByType": "performance.getEntriesByType 方法", - "prototype": "原型链检测", - "constructor": "constructor 属性验证", - "__wdata": "window 属性枚举(环境指纹)", - "#000000": "Canvas fillStyle 默认值", - } - - for tH, igs, _ in has_value: - descs = [] - for v in igs: - if isinstance(v, str): - d = known.get(v) - if d: - descs.append(f"{v} → {d}") - elif v.startswith("0,1,2,3"): - descs.append("window keys 枚举列表 → 全局属性指纹") - elif re.match(r'\d+:\d+:\d{4}', v): - descs.append(f"{v} → HSW token 格式") - elif v in ("f", "t", "c", "d"): - descs.append(f'"{v}" → 分支标记字符') - else: - descs.append(v) - elif isinstance(v, bool): - descs.append(f"{v} → 布尔检测结果") - elif isinstance(v, int): - descs.append(f"{v} → 数值") - - print(f" tH={tH:<4d}:") - for d in descs: - print(f" {d}") - print() - - -# ── 导出 JSON ──────────────────────────────────────────────── -def export_json(tH_igs, tH_no_ig, out_path): - result = OrderedDict() - all_tH = sorted(set(list(tH_igs.keys()) + list(tH_no_ig.keys()))) - for tH in all_tH: - igs = tH_igs.get(tH, []) - no = tH_no_ig.get(tH, 0) - result[str(tH)] = { - "ig_values": [v if not isinstance(v, str) or len(v) <= 200 else v[:200]+"..." for v in igs], - "no_ig_count": no, - "status": "has_value" if igs else "no_ig", - } - with open(out_path, "w", encoding="utf-8") as f: - json.dump(result, f, ensure_ascii=False, indent=2) - print(f"📄 JSON 已写入: {out_path}") - - -# ── 入口 ───────────────────────────────────────────────────── -if __name__ == "__main__": - log_path = find_log(sys.argv[1] if len(sys.argv) > 1 else None) - print(f"📂 日志文件: {log_path}\n") - - entries = parse(log_path) - print(f"共解析 {len(entries)} 条索引点记录\n") - - tH_igs, tH_no_ig, tH_lines = summarize(entries) - report(tH_igs, tH_no_ig, tH_lines) - - out = log_path.replace(".log", "_analysis.json") - export_json(tH_igs, tH_no_ig, out) diff --git a/analyze_priority.py b/analyze_priority.py deleted file mode 100644 index 6489175..0000000 --- a/analyze_priority.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env python3 -""" -从 chatgpt.com-*_analysis.json 中,按优先级对每个指纹字段评分排序。 - -评分规则: - +10 bot 自动化检测专属字段(webdriver, $cdc_*, callPhantom 等) - + 5 出现在核心检测循环 tH=154 或 tH=155 - + 2 每额外出现在一个不同 tH(跨 tH 频次) - + 3 属于已知高风险 API(Crypto, RTCPeerConnection, OfflineAudioContext 等) - + 1 属于 navigator / screen / canvas 系列 -""" - -import json -import sys -import glob -from collections import defaultdict - -# ── 配置 ───────────────────────────────────────────────────── -ANALYSIS_JSON = sorted(glob.glob( - "/home/carry/myprj/hcaptcha/asset/chatgpt.com-*_analysis.json" -))[-1] - -# bot 自动化检测专属字段(出现即暴露) -BOT_SIGNALS = { - "webdriver", "callPhantom", "callSelenium", "_selenium", "__phantomas", - "domAutomationController", "awesomium", "$wdc_", "domAutomation", - "_WEBDRIVER_ELEM_CACHE", "spawn", "__nightmare", "__webdriver_script_fn", - "__webdriver_script_func", "__driver_evaluate", "__webdriver_evaluate", - "__selenium_evaluate", "__fxdriver_evaluate", "__driver_unwrapped", - "__webdriver_unwrapped", "__selenium_unwrapped", "__fxdriver_unwrapped", - "hcaptchaCallbackZenno", "_Selenium_IDE_Recorder", - "cdc_adoQpoasnfa76pfcZLmcfl_Array", - "cdc_adoQpoasnfa76pfcZLmcfl_Promise", - "cdc_adoQpoasnfa76pfcZLmcfl_Symbol", - "CDCJStestRunStatus", - "$cdc_asdjflasutopfhvcZLmcfl_", - "$chrome_asyncScriptInfo", -} - -# 高风险 API(指纹强度高) -HIGH_RISK_APIS = { - "Crypto", "RTCPeerConnection", "OfflineAudioContext", - "CanvasRenderingContext2D", "HTMLCanvasElement", "WebGL2RenderingContext", - "WebGLRenderingContext", "IDBFactory", "PluginArray", "NavigatorUAData", - "PerformanceNavigationTiming", "PerformanceResourceTiming", -} - -# navigator / screen / canvas 系列 -MEDIUM_APIS = { - "Navigator", "Screen", "Storage", "Performance", "HTMLDocument", - "ScreenOrientation", "NetworkInformation", "languages", "maxTouchPoints", - "webdriver", "platform", "userAgent", -} - -# 核心检测循环 tH -CORE_TH = {154, 155} - - -# ── 加载 ──────────────────────────────────────────────────── -def load(path): - with open(path, encoding="utf-8") as f: - return json.load(f) - - -# ── 评分 ──────────────────────────────────────────────────── -def score(data): - # api -> {tH set, score, reasons} - api_info = defaultdict(lambda: {"tH_set": set(), "score": 0, "reasons": []}) - - for tH_str, entry in data.items(): - tH = int(tH_str) - for ig in entry.get("ig_values", []): - if not isinstance(ig, str): - continue - # 跳过明显是"值"而非 API 名的字符串 - if ig.startswith("0,1,2") or ig.startswith("1:") or \ - ig.startswith("#") or ig.startswith("return ") or \ - ig.startswith("https://") or len(ig) > 80: - continue - - info = api_info[ig] - info["tH_set"].add(tH) - - # 计算分数 - for api, info in api_info.items(): - s = 0 - reasons = [] - - # bot 信号 - if api in BOT_SIGNALS: - s += 10 - reasons.append("🚨 bot检测字段 +10") - - # 核心检测循环 - core_hit = info["tH_set"] & CORE_TH - if core_hit: - s += 5 - reasons.append(f"🎯 核心循环 tH={sorted(core_hit)} +5") - - # 高风险 API - if api in HIGH_RISK_APIS: - s += 3 - reasons.append("⚡ 高风险API +3") - - # 中等 API - if api in MEDIUM_APIS: - s += 1 - reasons.append("📡 navigator/screen类 +1") - - # 跨 tH 频次(每多一个 tH +2) - freq = len(info["tH_set"]) - if freq > 1: - bonus = (freq - 1) * 2 - s += bonus - reasons.append(f"🔁 跨{freq}个tH +{bonus}") - - info["score"] = s - info["reasons"] = reasons - - return api_info - - -# ── 输出 ───────────────────────────────────────────────────── -def report(api_info): - # 按分数排序 - ranked = sorted(api_info.items(), key=lambda x: -x[1]["score"]) - - print("=" * 70) - print(" HSW 指纹字段 优先级排名") - print("=" * 70) - - # 分档 - tiers = [ - ("🔴 P0 必须正确(≥10分)", lambda s: s >= 10), - ("🟠 P1 高优先级(5~9分)", lambda s: 5 <= s < 10), - ("🟡 P2 中优先级(3~4分)", lambda s: 3 <= s < 5), - ("🟢 P3 低优先级(1~2分)", lambda s: 1 <= s < 3), - ("⚪ P4 可忽略(0分)", lambda s: s == 0), - ] - - for tier_label, condition in tiers: - tier_items = [(api, info) for api, info in ranked if condition(info["score"])] - if not tier_items: - continue - print(f"\n{tier_label} [{len(tier_items)} 个]") - print(f" {'分数':<5} {'字段名':<45} 出现tH") - print(f" {'─'*5} {'─'*45} {'─'*20}") - for api, info in tier_items: - tH_list = ",".join(str(t) for t in sorted(info["tH_set"])) - print(f" {info['score']:<5} {api:<45} tH={tH_list}") - for r in info["reasons"]: - print(f" {r}") - - # 导出 JSON - out = { - api: { - "score": info["score"], - "tH_list": sorted(info["tH_set"]), - "reasons": info["reasons"], - } - for api, info in ranked - } - out_path = ANALYSIS_JSON.replace("_analysis.json", "_priority.json") - with open(out_path, "w", encoding="utf-8") as f: - json.dump(out, f, ensure_ascii=False, indent=2) - print(f"\n📄 优先级结果已写入: {out_path}") - - -# ── 入口 ───────────────────────────────────────────────────── -if __name__ == "__main__": - path = sys.argv[1] if len(sys.argv) > 1 else ANALYSIS_JSON - print(f"📂 读取: {path}\n") - data = load(path) - api_info = score(data) - report(api_info) diff --git a/body.bin b/body.bin index de8ea63..bfe8ab9 100644 Binary files a/body.bin and b/body.bin differ diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 9e5332d..9627b2b 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -4,6 +4,12 @@ "lockfileVersion": 3, "requires": true, "packages": { + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, "node_modules/@msgpack/msgpack": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", @@ -13,12 +19,399 @@ "node": ">= 18" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.18", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz", + "integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.5.5", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/callsites": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", + "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", + "license": "MIT", + "dependencies": { + "type-fest": "^2.11.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/event-lite": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz", "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==", "license": "MIT" }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/generative-bayesian-network": { + "version": "2.1.80", + "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.80.tgz", + "integrity": "sha512-LyCc23TIFvZDkUJclZ3ixCZvd+dhktr9Aug1EKz5VrfJ2eA5J2HrprSwWRna3VObU2Wy8quXMUF8j2em0bJSLw==", + "license": "Apache-2.0", + "dependencies": { + "adm-zip": "^0.5.9", + "tslib": "^2.4.0" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", + "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got-scraping": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/got-scraping/-/got-scraping-4.2.0.tgz", + "integrity": "sha512-iKwmAMTOen+t/n9weMwTd/WqbY8wzbIX+cjMeapnEOCRaQwzzPpely161OwfMq1T9S4Q4rJsYPM9/yNFEsEZDA==", + "license": "Apache-2.0", + "dependencies": { + "got": "^14.2.1", + "header-generator": "^2.1.41", + "http2-wrapper": "^2.2.0", + "mimic-response": "^4.0.0", + "ow": "^1.1.1", + "quick-lru": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/header-generator": { + "version": "2.1.80", + "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.80.tgz", + "integrity": "sha512-7gvv2Xm6Q0gNN3BzMD/D3sGvSJRcV1+k8XehPmBYTpTkBmKshwnYyi0jJJnpP3S6YP7vdOoEobeBV87aG9YTtQ==", + "license": "Apache-2.0", + "dependencies": { + "browserslist": "^4.21.1", + "generative-bayesian-network": "^2.1.80", + "ow": "^0.28.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/header-generator/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/header-generator/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/header-generator/node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/header-generator/node_modules/ow": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz", + "integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.2.0", + "callsites": "^3.1.0", + "dot-prop": "^6.0.1", + "lodash.isequal": "^4.5.0", + "vali-date": "^1.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -45,12 +438,73 @@ "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==", "license": "MIT" }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/msgpack-lite": { "version": "0.1.26", "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", @@ -65,6 +519,160 @@ "bin": { "msgpack": "bin/msgpack" } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ow/-/ow-1.1.1.tgz", + "integrity": "sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.3.0", + "callsites": "^4.0.0", + "dot-prop": "^7.2.0", + "lodash.isequal": "^4.5.0", + "vali-date": "^1.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ow/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } } } } diff --git a/package-lock.json b/package-lock.json index ccf6662..5a9e8d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,16 @@ "version": "0.1.0", "dependencies": { "@msgpack/msgpack": "^3.0.0", + "got-scraping": "^4.2.0", "msgpack-lite": "^0.1.26" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, "node_modules/@msgpack/msgpack": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", @@ -21,12 +28,399 @@ "node": ">= 18" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.18", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz", + "integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.5.5", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/callsites": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", + "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", + "license": "MIT", + "dependencies": { + "type-fest": "^2.11.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/event-lite": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz", "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==", "license": "MIT" }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/generative-bayesian-network": { + "version": "2.1.80", + "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.80.tgz", + "integrity": "sha512-LyCc23TIFvZDkUJclZ3ixCZvd+dhktr9Aug1EKz5VrfJ2eA5J2HrprSwWRna3VObU2Wy8quXMUF8j2em0bJSLw==", + "license": "Apache-2.0", + "dependencies": { + "adm-zip": "^0.5.9", + "tslib": "^2.4.0" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", + "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got-scraping": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/got-scraping/-/got-scraping-4.2.0.tgz", + "integrity": "sha512-iKwmAMTOen+t/n9weMwTd/WqbY8wzbIX+cjMeapnEOCRaQwzzPpely161OwfMq1T9S4Q4rJsYPM9/yNFEsEZDA==", + "license": "Apache-2.0", + "dependencies": { + "got": "^14.2.1", + "header-generator": "^2.1.41", + "http2-wrapper": "^2.2.0", + "mimic-response": "^4.0.0", + "ow": "^1.1.1", + "quick-lru": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/header-generator": { + "version": "2.1.80", + "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.80.tgz", + "integrity": "sha512-7gvv2Xm6Q0gNN3BzMD/D3sGvSJRcV1+k8XehPmBYTpTkBmKshwnYyi0jJJnpP3S6YP7vdOoEobeBV87aG9YTtQ==", + "license": "Apache-2.0", + "dependencies": { + "browserslist": "^4.21.1", + "generative-bayesian-network": "^2.1.80", + "ow": "^0.28.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/header-generator/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/header-generator/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/header-generator/node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/header-generator/node_modules/ow": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz", + "integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.2.0", + "callsites": "^3.1.0", + "dot-prop": "^6.0.1", + "lodash.isequal": "^4.5.0", + "vali-date": "^1.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -53,12 +447,73 @@ "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==", "license": "MIT" }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/msgpack-lite": { "version": "0.1.26", "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", @@ -73,6 +528,160 @@ "bin": { "msgpack": "bin/msgpack" } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ow/-/ow-1.1.1.tgz", + "integrity": "sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.3.0", + "callsites": "^4.0.0", + "dot-prop": "^7.2.0", + "lodash.isequal": "^4.5.0", + "vali-date": "^1.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ow/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } } } } diff --git a/package.json b/package.json index 669ac49..5b4537c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@msgpack/msgpack": "^3.0.0", + "got-scraping": "^4.2.0", "msgpack-lite": "^0.1.26" } } diff --git a/src/core/http_client.js b/src/core/http_client.js new file mode 100644 index 0000000..6ddb0e2 --- /dev/null +++ b/src/core/http_client.js @@ -0,0 +1,163 @@ +/** + * HTTP Client - TLS Fingerprint Spoofing Layer + * + * WARNING: Standard axios/node-fetch = instant death. + * Their JA3 fingerprint screams "I AM NODE.JS" to Cloudflare. + * + * We use got-scraping to mimic Chrome's TLS handshake. + * HTTP/2 is enabled because hCaptcha's API uses it and + * falling back to HTTP/1.1 is a red flag. + * + * Includes a simple cookie jar so Set-Cookie headers from one + * response are automatically forwarded to subsequent requests + * (critical for __cf_bm Cloudflare Bot-Management cookie). + */ + +import { gotScraping } from 'got-scraping'; +import { Logger } from '../utils/logger.js'; + +const logger = new Logger('HttpClient'); + +export class HttpClient { + constructor(fingerprint = {}) { + this.fingerprint = fingerprint; + this.baseHeaders = this._buildHeaders(); + /** @type {Map>} rootDomain -> {name->value} */ + this.cookieJar = new Map(); + } + + // ── headers ────────────────────────────────────────────── + + _buildHeaders() { + // Chrome 143 header set + return { + 'accept': '*/*', + 'accept-encoding': 'gzip, deflate, br', + 'accept-language': 'en-US,en;q=0.9', + 'sec-ch-ua': '"Google Chrome";v="143", "Chromium";v="143", "Not(A:Brand";v="99"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Linux"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + 'sec-fetch-storage-access': 'active', + 'user-agent': this.fingerprint.userAgent || + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', + }; + } + + // ── cookie helpers ─────────────────────────────────────── + + _rootDomain(hostname) { + return hostname.replace(/^[^.]+\./, '.'); + } + + /** + * Extract Set-Cookie headers from a got-scraping response + * and merge them into our cookie jar. + */ + _captureCookies(response) { + const raw = response.headers['set-cookie']; + if (!raw) return; + + const items = Array.isArray(raw) ? raw : [raw]; + const url = new URL(response.url || response.requestUrl); + const rootDomain = this._rootDomain(url.hostname); + + if (!this.cookieJar.has(rootDomain)) { + this.cookieJar.set(rootDomain, new Map()); + } + const jar = this.cookieJar.get(rootDomain); + + for (const cookie of items) { + const [pair] = cookie.split(';'); + if (!pair) continue; + const eqIdx = pair.indexOf('='); + if (eqIdx < 0) continue; + jar.set( + pair.substring(0, eqIdx).trim(), + pair.substring(eqIdx + 1).trim(), + ); + } + + logger.debug(`Cookies for ${rootDomain}: ${[...jar.keys()].join(', ')}`); + } + + /** + * Return the cookie header string for a given URL. + */ + _cookiesForUrl(url) { + const hostname = new URL(url).hostname; + const rootDomain = this._rootDomain(hostname); + const jar = this.cookieJar.get(rootDomain) || this.cookieJar.get(hostname); + if (!jar || jar.size === 0) return ''; + return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; '); + } + + // ── common request plumbing ────────────────────────────── + + _gotOptions(method, url, headers, body) { + const cookieHeader = this._cookiesForUrl(url); + const mergedHeaders = { ...this.baseHeaders, ...headers }; + if (cookieHeader) mergedHeaders['cookie'] = cookieHeader; + + const opts = { + url, + method, + headers: mergedHeaders, + headerGeneratorOptions: { + browsers: ['chrome'], + operatingSystems: ['linux'], + }, + http2: true, // ← critical: use HTTP/2 + throwHttpErrors: false, // we handle status ourselves + responseType: 'buffer', // always get raw buffer + }; + + if (body !== undefined) { + // Buffer / Uint8Array → send as-is + // string → send as-is + // object → JSON.stringify + if (Buffer.isBuffer(body) || body instanceof Uint8Array) { + opts.body = Buffer.isBuffer(body) ? body : Buffer.from(body); + } else if (typeof body === 'string') { + opts.body = body; + } else { + opts.body = JSON.stringify(body); + } + } + + return opts; + } + + /** + * Wrap got-scraping and return a uniform result. + */ + async _request(method, url, headers = {}, body) { + const opts = this._gotOptions(method, url, headers, body); + const response = await gotScraping(opts); + this._captureCookies(response); + return { + status: response.statusCode, + headers: response.headers, + body: response.body, // Buffer + text: () => response.body.toString('utf-8'), + json: () => JSON.parse(response.body.toString('utf-8')), + url: response.url || url, + }; + } + + // ── HTTP verbs ─────────────────────────────────────────── + + async get(url, options = {}) { + return this._request('GET', url, options.headers); + } + + async post(url, body, options = {}) { + return this._request('POST', url, options.headers, body); + } + + async options(url, options = {}) { + return this._request('OPTIONS', url, options.headers); + } +} diff --git a/src/hcaptcha_solver.js b/src/hcaptcha_solver.js index 992bfbc..290ae39 100644 --- a/src/hcaptcha_solver.js +++ b/src/hcaptcha_solver.js @@ -8,15 +8,17 @@ * 使用 hsw.js 在 Node 沙盒中运行(全局污染方式) */ +const vm = require('vm'); const { readFileSync } = require('fs'); const { join } = require('path'); const msgpack = require('msgpack-lite'); -const { createBrowserEnvironment } = require('./sandbox/mocks/index'); +const windowMock = require('./sandbox/mocks/window'); +const { applySandboxPatches } = require('./sandbox/mocks/index'); const { Logger } = require('./utils/logger'); const logger = new Logger('hcaptcha_solver'); -// 保存原始 fetch(在全局被 mock 污染之前) +// 保存原始 fetch(供真实网络请求使用) const realFetch = globalThis.fetch; // ── 常量 ────────────────────────────────────────────────────── @@ -82,76 +84,86 @@ class HswBridge { constructor() { this.hswFn = null; this.initialized = false; - this._savedGlobals = {}; + this._ctx = null; } /** - * 将 mock 注入全局,加载并执行 hsw.js + * 构建 vm 沙盒上下文,将 mock window 注入隔离环境 + * @param {object} fingerprint - 指纹覆盖 + */ + _buildContext(fingerprint) { + const ctx = Object.create(null); + + // 把 windowMock 上所有 key 复制进 ctx(浅拷贝) + for (const key of Reflect.ownKeys(windowMock)) { + try { ctx[key] = windowMock[key]; } catch (_) {} + } + + // vm 必需的自引用 + ctx.global = ctx; + ctx.globalThis = ctx; + ctx.window = ctx; + ctx.self = ctx; + + // 透传 console(调试用) + ctx.console = console; + + // 保证 Promise / 定时器在 vm 里可用 + ctx.Promise = Promise; + ctx.setTimeout = setTimeout; + ctx.clearTimeout = clearTimeout; + ctx.setInterval = setInterval; + ctx.clearInterval = clearInterval; + ctx.queueMicrotask = queueMicrotask; + + // 应用指纹覆盖 + if (fingerprint.userAgent && ctx.navigator) { + ctx.navigator.userAgent = fingerprint.userAgent; + ctx.navigator.appVersion = fingerprint.userAgent.replace('Mozilla/', ''); + } + if (fingerprint.platform && ctx.navigator) { + ctx.navigator.platform = fingerprint.platform; + } + if (fingerprint.host && ctx.location?.ancestorOrigins) { + ctx.location.ancestorOrigins[0] = `https://${fingerprint.host}`; + } + + const vmCtx = vm.createContext(ctx); + + // Apply escape defense + error stack rewriting AFTER context creation + applySandboxPatches(vmCtx); + + return vmCtx; + } + + /** + * 在 vm 沙盒中加载并执行 hsw.js * @param {string} hswPath - hsw.js 文件路径 * @param {object} fingerprint - 指纹覆盖 */ async init(hswPath, fingerprint = {}) { if (this.initialized) return; - const env = createBrowserEnvironment(fingerprint); - - // 保存原始全局 - const keys = ['window', 'document', 'navigator', 'screen', 'location', - 'localStorage', 'sessionStorage', 'crypto', 'performance', - 'self', 'top', 'parent', 'fetch', 'XMLHttpRequest']; - for (const k of keys) { - if (k in globalThis) this._savedGlobals[k] = globalThis[k]; - } - - // 注入全局 - const force = (obj, prop, val) => { - Object.defineProperty(obj, prop, { - value: val, writable: true, configurable: true, enumerable: true, - }); - }; - force(globalThis, 'window', env.window); - force(globalThis, 'document', env.document); - force(globalThis, 'navigator', env.navigator); - force(globalThis, 'screen', env.screen); - force(globalThis, 'location', env.location); - force(globalThis, 'localStorage', env.localStorage); - force(globalThis, 'sessionStorage', env.sessionStorage); - force(globalThis, 'crypto', env.crypto); - force(globalThis, 'performance', env.performance); - force(globalThis, 'self', env.window); - force(globalThis, 'top', env.window); - force(globalThis, 'parent', env.window); - - // 浏览器 API - globalThis.fetch = env.window.fetch; - globalThis.btoa = env.window.btoa; - globalThis.atob = env.window.atob; - globalThis.setTimeout = env.window.setTimeout; - globalThis.setInterval = env.window.setInterval; - globalThis.clearTimeout = env.window.clearTimeout; - globalThis.clearInterval = env.window.clearInterval; - globalThis.TextEncoder = env.window.TextEncoder; - globalThis.TextDecoder = env.window.TextDecoder; - globalThis.requestAnimationFrame = env.window.requestAnimationFrame; - globalThis.cancelAnimationFrame = env.window.cancelAnimationFrame; - - // 加载 hsw.js const code = readFileSync(hswPath, 'utf-8'); logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`); + const ctx = this._buildContext(fingerprint); + this._ctx = ctx; + + const script = new vm.Script(code, { filename: 'hsw.js' }); + try { - const fn = new Function(`(function() { ${code} })();`); - fn(); + script.runInContext(ctx, { timeout: 10000 }); } catch (err) { logger.error(`hsw.js 执行失败: ${err.message}`); throw err; } // 查找 hsw 函数 - if (typeof globalThis.window?.hsw === 'function') { - this.hswFn = globalThis.window.hsw; - } else if (typeof globalThis.hsw === 'function') { - this.hswFn = globalThis.hsw; + if (typeof ctx.hsw === 'function') { + this.hswFn = ctx.hsw; + } else if (typeof ctx.window?.hsw === 'function') { + this.hswFn = ctx.window.hsw; } if (!this.hswFn) { @@ -159,7 +171,7 @@ class HswBridge { } this.initialized = true; - logger.success('Bridge 已就绪'); + logger.success('Bridge 已就绪 (vm 沙盒)'); } /** 计算 PoW n 值: hsw(req_jwt_string) */ diff --git a/src/probe/deep_proxy.js b/src/probe/deep_proxy.js new file mode 100644 index 0000000..2695dfe --- /dev/null +++ b/src/probe/deep_proxy.js @@ -0,0 +1,273 @@ +'use strict'; + +/** + * 深层递归 Proxy 引擎 + * 包裹任意对象,记录所有属性访问(get/has/set/ownKeys/getOwnPropertyDescriptor/deleteProperty) + */ + +// 不应被 Proxy 包装的类型(有内部槽位或会导致问题) +const UNPROXYABLE_TYPES = [ + ArrayBuffer, SharedArrayBuffer, + Uint8Array, Int8Array, Uint16Array, Int16Array, + Uint32Array, Int32Array, Uint8ClampedArray, + Float32Array, Float64Array, DataView, + RegExp, Date, Error, TypeError, RangeError, SyntaxError, + Promise, +]; + +// 不递归代理的 key(避免无限循环或干扰日志输出) +const SKIP_KEYS = new Set([ + 'console', 'Symbol', 'undefined', 'NaN', 'Infinity', +]); + +/** + * 格式化 key 为可读字符串 + */ +function formatKey(key) { + if (typeof key === 'symbol') { + return `Symbol(${key.description || ''})`; + } + return String(key); +} + +/** + * 获取值的简短类型描述 + */ +function valueType(val) { + if (val === null) return 'null'; + if (val === undefined) return 'undefined'; + const t = typeof val; + if (t !== 'object' && t !== 'function') return t; + if (t === 'function') { + return val.name ? `function:${val.name}` : 'function'; + } + if (Array.isArray(val)) return `array[${val.length}]`; + const ctor = val.constructor?.name; + return ctor ? `object:${ctor}` : 'object'; +} + +/** + * 判断值是否不应被 Proxy 包装 + */ +function isUnproxyable(val) { + if (val === null || val === undefined) return true; + const t = typeof val; + if (t !== 'object' && t !== 'function') return true; + // WebAssembly 模块/实例有内部槽位 + if (typeof WebAssembly !== 'undefined') { + if (val instanceof WebAssembly.Module || val instanceof WebAssembly.Instance || + val instanceof WebAssembly.Memory || val instanceof WebAssembly.Table) { + return true; + } + } + for (const Ctor of UNPROXYABLE_TYPES) { + try { if (val instanceof Ctor) return true; } catch (_) {} + } + return false; +} + +/** + * 创建深层递归 Proxy + * @param {any} target - 被包裹的目标对象 + * @param {string} path - 当前路径(如 'window.navigator') + * @param {object} log - 日志收集器 { entries: Map, raw: [] } + * @returns {Proxy} + */ +function deepProxy(target, path, log) { + // proxyCache: 防止重复包装 + 处理循环引用(window.window.window...) + const proxyCache = log._proxyCache || (log._proxyCache = new WeakMap()); + // resolvingPaths: 防止 getter 内部访问同一路径导致无限递归 + const resolvingPaths = log._resolvingPaths || (log._resolvingPaths = new Set()); + + if (isUnproxyable(target)) return target; + if (proxyCache.has(target)) return proxyCache.get(target); + + const proxy = new Proxy(target, { + get(obj, key, receiver) { + const keyStr = formatKey(key); + const fullPath = path ? `${path}.${keyStr}` : keyStr; + + // 跳过不代理的 key + if (SKIP_KEYS.has(keyStr)) { + try { return Reflect.get(obj, key, receiver); } catch (e) { return undefined; } + } + + // 防止无限递归 + if (resolvingPaths.has(fullPath)) { + try { return Reflect.get(obj, key, receiver); } catch (e) { return undefined; } + } + + let result, val, error; + resolvingPaths.add(fullPath); + try { + val = Reflect.get(obj, key, receiver); + result = val === undefined ? 'undefined' : 'found'; + } catch (e) { + result = 'error'; + error = e.message; + val = undefined; + } finally { + resolvingPaths.delete(fullPath); + } + + // 记录日志 + recordAccess(log, { + path: fullPath, + trap: 'get', + result, + valueType: result === 'error' ? 'error' : valueType(val), + error, + }); + + // 对对象/函数结果递归包装 + // 但必须尊重 Proxy 不变量:non-configurable + non-writable 属性必须返回原值 + if (val !== null && val !== undefined && !isUnproxyable(val)) { + const t = typeof val; + if (t === 'object' || t === 'function') { + // 检查属性描述符:若 non-configurable 且 non-writable,不能包装 + let canWrap = true; + try { + const desc = Object.getOwnPropertyDescriptor(obj, key); + if (desc && !desc.configurable && !desc.writable && !desc.set) { + canWrap = false; + } + } catch (_) {} + if (canWrap) { + return deepProxy(val, fullPath, log); + } + } + } + return val; + }, + + has(obj, key) { + const keyStr = formatKey(key); + const fullPath = path ? `${path}.${keyStr}` : keyStr; + + let result; + try { + result = Reflect.has(obj, key); + } catch (e) { + recordAccess(log, { + path: fullPath, trap: 'has', result: 'error', + valueType: 'error', error: e.message, + }); + return false; + } + + recordAccess(log, { + path: fullPath, trap: 'has', + result: result ? 'true' : 'false', + valueType: 'boolean', + }); + return result; + }, + + set(obj, key, value) { + const keyStr = formatKey(key); + const fullPath = path ? `${path}.${keyStr}` : keyStr; + + recordAccess(log, { + path: fullPath, trap: 'set', + result: 'write', valueType: valueType(value), + }); + + try { + return Reflect.set(obj, key, value); + } catch (e) { + // 静默失败,不阻塞 hsw + return true; + } + }, + + ownKeys(obj) { + const fullPath = path || 'window'; + let keys; + try { + keys = Reflect.ownKeys(obj); + } catch (e) { + recordAccess(log, { + path: fullPath, trap: 'ownKeys', + result: 'error', valueType: 'error', error: e.message, + }); + return []; + } + + recordAccess(log, { + path: fullPath, trap: 'ownKeys', + result: `keys[${keys.length}]`, valueType: 'array', + }); + return keys; + }, + + getOwnPropertyDescriptor(obj, key) { + const keyStr = formatKey(key); + const fullPath = path ? `${path}.${keyStr}` : keyStr; + + let desc; + try { + desc = Reflect.getOwnPropertyDescriptor(obj, key); + } catch (e) { + recordAccess(log, { + path: fullPath, trap: 'getOwnPropertyDescriptor', + result: 'error', valueType: 'error', error: e.message, + }); + return undefined; + } + + recordAccess(log, { + path: fullPath, trap: 'getOwnPropertyDescriptor', + result: desc ? 'found' : 'undefined', + valueType: desc ? 'descriptor' : 'undefined', + }); + return desc; + }, + + deleteProperty(obj, key) { + const keyStr = formatKey(key); + const fullPath = path ? `${path}.${keyStr}` : keyStr; + + recordAccess(log, { + path: fullPath, trap: 'deleteProperty', + result: 'delete', valueType: 'void', + }); + + try { + return Reflect.deleteProperty(obj, key); + } catch (e) { + return false; + } + }, + }); + + proxyCache.set(target, proxy); + return proxy; +} + +/** + * 记录一次访问到日志收集器 + */ +function recordAccess(log, entry) { + const key = `${entry.trap}:${entry.path}`; + const existing = log.entries.get(key); + if (existing) { + existing.count++; + } else { + log.entries.set(key, { ...entry, count: 1 }); + } + log.totalAccesses++; +} + +/** + * 创建新的日志收集器 + */ +function createLog() { + return { + entries: new Map(), + totalAccesses: 0, + _proxyCache: new WeakMap(), + _resolvingPaths: new Set(), + }; +} + +module.exports = { deepProxy, createLog, formatKey, valueType }; diff --git a/src/probe/probe_env.js b/src/probe/probe_env.js new file mode 100644 index 0000000..c3602e9 --- /dev/null +++ b/src/probe/probe_env.js @@ -0,0 +1,241 @@ +'use strict'; + +/** + * 环境探针入口 + * 用深层递归 Proxy 包裹 VM 上下文,记录 hsw.js 执行期间的每一次属性访问 + * + * 用法: + * node src/probe/probe_env.js # 仅初始化探测 + * node src/probe/probe_env.js --live # 初始化 + 用真实 JWT 调用 hsw() + */ + +const vm = require('vm'); +const { readFileSync } = require('fs'); +const { join } = require('path'); + +const { deepProxy, createLog } = require('./deep_proxy'); +const { printReport, writeJsonReport } = require('./report'); + +// ── 加载 native.js 补丁(进程级 Function.prototype.toString 伪装) ── +require('../sandbox/mocks/native'); + +// ── 加载 window mock ── +const windowMock = require('../sandbox/mocks/window'); + +// ── hsw.js 路径 ── +const HSW_PATH = join(__dirname, '../../asset/hsw.js'); + +/** + * 组装 VM 上下文(复制自 hcaptcha_solver._buildContext) + */ +function buildProbedContext(log) { + const ctx = Object.create(null); + + // 把 windowMock 上所有 key 复制进 ctx(浅拷贝) + for (const key of Reflect.ownKeys(windowMock)) { + try { ctx[key] = windowMock[key]; } catch (_) {} + } + + // vm 必需的自引用 + ctx.global = ctx; + ctx.globalThis = ctx; + ctx.window = ctx; + ctx.self = ctx; + ctx.frames = ctx; + ctx.parent = ctx; + ctx.top = ctx; + + // 透传 console(调试用,不代理) + ctx.console = console; + + // 保证 Promise / 定时器在 vm 里可用 + ctx.Promise = Promise; + ctx.setTimeout = setTimeout; + ctx.clearTimeout = clearTimeout; + ctx.setInterval = setInterval; + ctx.clearInterval = clearInterval; + ctx.queueMicrotask = queueMicrotask; + + // 用深层 Proxy 包裹整个上下文 + const proxiedCtx = deepProxy(ctx, 'window', log); + + // 尝试直接用 Proxy 创建 vm context + try { + return { ctx: vm.createContext(proxiedCtx), proxied: true }; + } catch (e) { + // Node 版本不支持直接传 Proxy 给 createContext + // 回退方案:先 createContext 普通对象,再用 getter/setter 桥接 + console.warn(`[probe] Proxy createContext 失败,使用 getter 桥接: ${e.message}`); + const plainCtx = Object.create(null); + + for (const key of Reflect.ownKeys(ctx)) { + try { + Object.defineProperty(plainCtx, key, { + get() { return proxiedCtx[key]; }, + set(v) { proxiedCtx[key] = v; }, + enumerable: true, + configurable: true, + }); + } catch (_) { + try { plainCtx[key] = ctx[key]; } catch (__) {} + } + } + + return { ctx: vm.createContext(plainCtx), proxied: false }; + } +} + +/** + * 主流程 + */ +async function main() { + const args = process.argv.slice(2); + const isLive = args.includes('--live'); + + console.log('[probe] 环境探针启动...'); + console.log(`[probe] 模式: ${isLive ? '初始化 + 真实调用' : '仅初始化'}`); + + // 读取 hsw.js + let hswCode; + try { + hswCode = readFileSync(HSW_PATH, 'utf-8'); + console.log(`[probe] hsw.js 已加载 (${(hswCode.length / 1024).toFixed(1)} KB)`); + } catch (e) { + console.error(`[probe] 无法读取 hsw.js: ${e.message}`); + process.exit(1); + } + + // 创建日志收集器 + const log = createLog(); + + // 构建带 Proxy 的上下文 + const { ctx, proxied } = buildProbedContext(log); + console.log(`[probe] VM 上下文已创建 (Proxy直传: ${proxied})`); + + // 编译并执行 hsw.js + const script = new vm.Script(hswCode, { filename: 'hsw.js' }); + + let hswInitOk = false; + try { + script.runInContext(ctx, { timeout: 30000 }); + hswInitOk = true; + console.log('[probe] hsw.js 初始化成功'); + } catch (e) { + console.error(`[probe] hsw.js 初始化失败: ${e.message}`); + if (e.stack) { + // 只打印前 5 行 stack + const lines = e.stack.split('\n').slice(0, 5); + console.error(lines.join('\n')); + } + } + + // 查找 hsw 函数 + let hswFn = null; + try { + if (typeof ctx.hsw === 'function') { + hswFn = ctx.hsw; + } else if (ctx.window && typeof ctx.window.hsw === 'function') { + hswFn = ctx.window.hsw; + } + } catch (_) {} + + if (hswFn) { + console.log('[probe] hsw 函数已找到'); + } else { + console.warn('[probe] 未找到 hsw 函数'); + } + + // --live 模式:获取真实 JWT 调用 hsw() + let hswCallOk = false; + if (isLive && hswFn) { + console.log('[probe] 获取真实 JWT 进行 hsw 调用...'); + try { + const jwt = await fetchLiveJwt(); + if (jwt) { + console.log(`[probe] JWT 获取成功 (${jwt.substring(0, 40)}...)`); + const result = await Promise.race([ + Promise.resolve(hswFn(jwt)), + new Promise((_, reject) => setTimeout(() => reject(new Error('hsw 调用超时 (30s)')), 30000)), + ]); + hswCallOk = true; + console.log(`[probe] hsw 调用成功,结果长度: ${String(result).length}`); + } else { + console.warn('[probe] JWT 获取失败,跳过 hsw 调用'); + } + } catch (e) { + console.error(`[probe] hsw 调用失败: ${e.message}`); + } + } else if (isLive && !hswFn) { + console.warn('[probe] --live 模式但 hsw 函数不可用,跳过调用'); + } + + // 生成报告 + const meta = { + timestamp: new Date().toISOString(), + totalAccesses: log.totalAccesses, + uniquePaths: log.entries.size, + hswInitOk, + hswCallOk, + proxyDirect: proxied, + }; + + printReport(log.entries, meta); + const jsonPath = writeJsonReport(log.entries, meta); + console.log(`[probe] JSON 报告已保存: ${jsonPath}`); +} + +/** + * 获取真实 checksiteconfig JWT(用于 --live 模式) + * 直接调用 got-scraping,避免 ESM/CJS 不兼容问题 + */ +async function fetchLiveJwt() { + try { + const { gotScraping } = await import('got-scraping'); + + const sitekey = '4c672d35-0701-42b2-88c3-78380b0db560'; // stripe 公用 sitekey + const host = 'js.stripe.com'; + const url = `https://api2.hcaptcha.com/checksiteconfig?v=1ffa597&host=${host}&sitekey=${sitekey}&sc=1&swa=1&spst=1`; + + const response = await gotScraping({ + url, + method: 'GET', + headers: { + 'accept': '*/*', + 'accept-language': 'en-US,en;q=0.9', + 'referer': 'https://newassets.hcaptcha.com/', + 'sec-ch-ua': '"Google Chrome";v="143", "Chromium";v="143", "Not(A:Brand";v="99"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Linux"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', + }, + headerGeneratorOptions: { + browsers: ['chrome'], + operatingSystems: ['linux'], + }, + http2: true, + throwHttpErrors: false, + responseType: 'text', + }); + + const body = JSON.parse(response.body); + if (body && body.c && body.c.req) { + return body.c.req; + } + + console.warn('[probe] checksiteconfig 响应中无 c.req 字段'); + console.warn('[probe] 响应:', JSON.stringify(body).substring(0, 200)); + return null; + } catch (e) { + console.warn(`[probe] 获取 JWT 失败: ${e.message}`); + return null; + } +} + +// ── 执行 ── +main().catch(e => { + console.error(`[probe] 致命错误: ${e.message}`); + process.exit(1); +}); diff --git a/src/probe/report.js b/src/probe/report.js new file mode 100644 index 0000000..00a9d83 --- /dev/null +++ b/src/probe/report.js @@ -0,0 +1,229 @@ +'use strict'; + +const { writeFileSync, mkdirSync } = require('fs'); +const { join } = require('path'); + +// ANSI 颜色码 +const C = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + gray: '\x1b[90m', +}; + +// 分类规则:路径前缀 → 类别名 +const CATEGORIES = [ + ['navigator', 'Navigator'], + ['screen', 'Screen'], + ['performance', 'Performance'], + ['crypto', 'Crypto'], + ['canvas', 'Canvas'], + ['Canvas', 'Canvas'], + ['webgl', 'WebGL'], + ['WebGL', 'WebGL'], + ['localStorage', 'Storage'], + ['sessionStorage','Storage'], + ['indexedDB', 'Storage'], + ['IDB', 'Storage'], + ['Storage', 'Storage'], + ['Worker', 'Workers'], + ['SharedWorker', 'Workers'], + ['ServiceWorker','Workers'], + ['document', 'Document'], + ['HTML', 'Document'], + ['location', 'Location'], + ['history', 'History'], + ['fetch', 'Network'], + ['Request', 'Network'], + ['Response', 'Network'], + ['Headers', 'Network'], + ['URL', 'Network'], + ['WebSocket', 'Network'], + ['RTC', 'WebRTC'], + ['Audio', 'Audio'], + ['OfflineAudio', 'Audio'], + ['WebAssembly', 'WebAssembly'], + ['Notification', 'Notification'], +]; + +/** + * 根据路径判断类别 + */ +function categorize(path) { + // 取第一级有意义的 key(跳过 window. 前缀) + const clean = path.replace(/^window\./, ''); + for (const [prefix, cat] of CATEGORIES) { + if (clean.startsWith(prefix)) return cat; + } + return 'Other'; +} + +/** + * 生成控制台报告 + * @param {Map} entries - 日志 entries + * @param {object} meta - 元信息 + */ +function printReport(entries, meta) { + console.log('\n' + C.bold + C.cyan + '═══════════════════════════════════════════════════════════' + C.reset); + console.log(C.bold + C.cyan + ' 环境探针报告 — hCaptcha hsw.js Environment Probe' + C.reset); + console.log(C.bold + C.cyan + '═══════════════════════════════════════════════════════════' + C.reset); + + // 元信息 + console.log(`\n${C.gray}时间:${C.reset} ${meta.timestamp}`); + console.log(`${C.gray}总访问次数:${C.reset} ${meta.totalAccesses}`); + console.log(`${C.gray}唯一路径数:${C.reset} ${meta.uniquePaths}`); + console.log(`${C.gray}hsw 初始化:${C.reset} ${meta.hswInitOk ? C.green + '✓' : C.red + '✗'}${C.reset}`); + console.log(`${C.gray}hsw 调用:${C.reset} ${meta.hswCallOk ? C.green + '✓' : C.red + '✗'}${C.reset}`); + + // 按类别分组(仅 get trap) + const grouped = {}; + const allMissing = []; + const allErrors = []; + + for (const [, entry] of entries) { + if (entry.trap !== 'get') continue; + + const cat = categorize(entry.path); + if (!grouped[cat]) grouped[cat] = []; + grouped[cat].push(entry); + + if (entry.result === 'undefined') allMissing.push(entry); + if (entry.result === 'error') allErrors.push(entry); + } + + // 排序类别名 + const catOrder = [ + 'Navigator', 'Screen', 'Performance', 'Crypto', 'Canvas', 'WebGL', + 'Storage', 'Workers', 'Document', 'Location', 'History', + 'Network', 'WebRTC', 'Audio', 'WebAssembly', 'Notification', 'Other', + ]; + const sortedCats = Object.keys(grouped).sort((a, b) => { + const ai = catOrder.indexOf(a); + const bi = catOrder.indexOf(b); + return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi); + }); + + for (const cat of sortedCats) { + const items = grouped[cat]; + console.log(`\n${C.bold}${C.blue}── ${cat} ──${C.reset} ${C.dim}(${items.length} paths)${C.reset}`); + + // 排序: error > undefined > found,按访问次数降序 + items.sort((a, b) => { + const order = { error: 0, undefined: 1, found: 2 }; + const d = (order[a.result] ?? 3) - (order[b.result] ?? 3); + if (d !== 0) return d; + return b.count - a.count; + }); + + for (const item of items) { + let icon, color; + if (item.result === 'error') { + icon = '⚠'; color = C.yellow; + } else if (item.result === 'undefined') { + icon = '✗'; color = C.red; + } else { + icon = '✓'; color = C.green; + } + + const countStr = item.count > 1 ? `${C.dim} ×${item.count}${C.reset}` : ''; + const typeStr = `${C.gray}[${item.valueType}]${C.reset}`; + const cleanPath = item.path.replace(/^window\./, ''); + console.log(` ${color}${icon}${C.reset} ${cleanPath} ${typeStr}${countStr}`); + } + } + + // 汇总:缺失属性 + if (allMissing.length > 0) { + console.log(`\n${C.bold}${C.red}═══ 缺失属性 (${allMissing.length}) ═══${C.reset}`); + // 按频率降序 + allMissing.sort((a, b) => b.count - a.count); + for (const m of allMissing) { + const cleanPath = m.path.replace(/^window\./, ''); + const countStr = m.count > 1 ? ` ×${m.count}` : ''; + console.log(` ${C.red}•${C.reset} ${cleanPath}${C.dim}${countStr}${C.reset}`); + } + } + + // 汇总:错误 + if (allErrors.length > 0) { + console.log(`\n${C.bold}${C.yellow}═══ 访问错误 (${allErrors.length}) ═══${C.reset}`); + for (const e of allErrors) { + const cleanPath = e.path.replace(/^window\./, ''); + console.log(` ${C.yellow}•${C.reset} ${cleanPath}: ${e.error || 'unknown'}`); + } + } + + console.log('\n' + C.bold + C.cyan + '═══════════════════════════════════════════════════════════' + C.reset + '\n'); +} + +/** + * 生成 JSON 报告文件 + * @param {Map} entries - 日志 entries + * @param {object} meta - 元信息 + * @returns {string} 报告文件路径 + */ +function writeJsonReport(entries, meta) { + const missing = []; + const found = {}; + const errors = []; + const frequency = []; + + for (const [, entry] of entries) { + if (entry.trap !== 'get') continue; + + const cleanPath = entry.path.replace(/^window\./, ''); + + if (entry.result === 'undefined') { + missing.push(cleanPath); + } else if (entry.result === 'error') { + errors.push({ path: cleanPath, error: entry.error, count: entry.count }); + } else { + found[cleanPath] = { type: entry.valueType, count: entry.count }; + } + + frequency.push({ path: cleanPath, count: entry.count, result: entry.result }); + } + + // 频率 Top 50 + frequency.sort((a, b) => b.count - a.count); + const frequencyTop50 = frequency.slice(0, 50); + + // 非-get trap 的汇总 + const otherTraps = {}; + for (const [, entry] of entries) { + if (entry.trap === 'get') continue; + if (!otherTraps[entry.trap]) otherTraps[entry.trap] = []; + otherTraps[entry.trap].push({ + path: entry.path.replace(/^window\./, ''), + result: entry.result, + count: entry.count, + }); + } + + const report = { + meta, + missing: missing.sort(), + found, + errors, + frequencyTop50, + otherTraps, + }; + + const reportsDir = join(__dirname, 'reports'); + mkdirSync(reportsDir, { recursive: true }); + + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + const filePath = join(reportsDir, `probe_${ts}.json`); + writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf-8'); + + return filePath; +} + +module.exports = { printReport, writeJsonReport }; diff --git a/src/sandbox/hsw_runner.js b/src/sandbox/hsw_runner.js index 0bcb27d..7a24e0d 100644 --- a/src/sandbox/hsw_runner.js +++ b/src/sandbox/hsw_runner.js @@ -2,6 +2,7 @@ /** * HSW Runner * 用 vm 沙盒加载 hsw.js,注入 mock window,调用 window.hsw(req, callback) + * Now with: constructor chain escape defense, error stack rewriting * * 用法: * const { solveHsw } = require('./hsw_runner'); @@ -16,6 +17,7 @@ const HSW_PATH = path.resolve(__dirname, '../../asset/hsw.js'); // ── 加载 window mock ───────────────────────────────────────── const windowMock = require('./mocks/window'); +const { applySandboxPatches } = require('./mocks/index'); // ── 读取 hsw.js 源码(只读一次) ───────────────────────────── const hswCode = fs.readFileSync(HSW_PATH, 'utf-8'); @@ -47,7 +49,12 @@ function buildContext() { ctx.clearInterval = clearInterval; ctx.queueMicrotask = queueMicrotask; - return vm.createContext(ctx); + const vmCtx = vm.createContext(ctx); + + // Apply escape defense + error stack rewriting AFTER context creation + applySandboxPatches(vmCtx); + + return vmCtx; } // ── 编译脚本(只编译一次,复用) ───────────────────────────── diff --git a/src/sandbox/mocks/bot_shield.js b/src/sandbox/mocks/bot_shield.js index 5ce104d..0cc02b8 100644 --- a/src/sandbox/mocks/bot_shield.js +++ b/src/sandbox/mocks/bot_shield.js @@ -30,10 +30,18 @@ const BOT_KEYS = new Set([ 'CDCJStestRunStatus', '$cdc_asdjflasutopfhvcZLmcfl_', '$chrome_asyncScriptInfo', + '__wdata', +]); + +// Node.js 宿主字段 — 真实 Chrome window 上连描述符都不该有 +const NODE_KEYS = new Set([ + 'process', 'require', 'Buffer', 'module', 'exports', + '__dirname', '__filename', ]); function isBotKey(key) { if (BOT_KEYS.has(key)) return true; + if (NODE_KEYS.has(key)) return true; if (typeof key === 'string' && ( key.startsWith('cdc_') || key.startsWith('$cdc_') || diff --git a/src/sandbox/mocks/canvas.js b/src/sandbox/mocks/canvas.js index 46eb2cc..416bea2 100644 --- a/src/sandbox/mocks/canvas.js +++ b/src/sandbox/mocks/canvas.js @@ -2,116 +2,299 @@ /** * P1: Canvas mock * hsw 检测:HTMLCanvasElement / CanvasRenderingContext2D / fillStyle 默认值 / measureText + * Now with: full 7-property measureText bounding box, getImageData with colorSpace, + * deterministic pixel buffer, PRNG-seeded drawing */ -const { createNative, nativeClass } = require('./native'); +const { createNative, nativeMethod: M, nativeClass } = require('./native'); -// 2D Context +// ── Seeded PRNG for deterministic canvas fingerprint ──────────── +let _seed = 0x9E3779B9; +const prng = () => { + _seed ^= _seed << 13; + _seed ^= _seed >> 17; + _seed ^= _seed << 5; + return (_seed >>> 0) / 0xFFFFFFFF; +}; + +// ── Internal pixel buffer for deterministic rendering ─────────── +class PixelBuffer { + constructor(w, h) { + this.width = w; + this.height = h; + this.data = new Uint8ClampedArray(w * h * 4); + } + setPixel(x, y, r, g, b, a) { + if (x < 0 || x >= this.width || y < 0 || y >= this.height) return; + const i = (y * this.width + x) * 4; + this.data[i] = r; + this.data[i + 1] = g; + this.data[i + 2] = b; + this.data[i + 3] = a; + } + getRegion(sx, sy, sw, sh) { + const out = new Uint8ClampedArray(sw * sh * 4); + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const srcX = sx + x; + const srcY = sy + y; + const di = (y * sw + x) * 4; + if (srcX >= 0 && srcX < this.width && srcY >= 0 && srcY < this.height) { + const si = (srcY * this.width + srcX) * 4; + out[di] = this.data[si]; + out[di + 1] = this.data[si + 1]; + out[di + 2] = this.data[si + 2]; + out[di + 3] = this.data[si + 3]; + } + } + } + return out; + } +} + +// ── Emoji/character width lookup (approximation of Chrome metrics) ─ +const CHAR_WIDTHS = { + // Basic ASCII at 10px sans-serif + default: 5.5, + space: 2.5, + emoji: 10.0, // Most emoji are double-width +}; + +const isEmoji = (ch) => { + const cp = ch.codePointAt(0); + return cp > 0x1F000 || (cp >= 0x2600 && cp <= 0x27BF) || + (cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0x200D && cp <= 0x200D); +}; + +const measureChar = (ch) => { + if (ch === ' ') return CHAR_WIDTHS.space; + if (isEmoji(ch)) return CHAR_WIDTHS.emoji; + return CHAR_WIDTHS.default; +}; + +// ── 2D Context ────────────────────────────────────────────────── const CanvasRenderingContext2D = createNative('CanvasRenderingContext2D', function () {}); CanvasRenderingContext2D.prototype = { constructor: CanvasRenderingContext2D, - fillStyle: '#000000', // P1: 默认值必须是黑色 - strokeStyle: '#000000', - font: '10px sans-serif', - textAlign: 'start', + fillStyle: '#000000', + strokeStyle: '#000000', + font: '10px sans-serif', + textAlign: 'start', textBaseline: 'alphabetic', globalAlpha: 1, + globalCompositeOperation: 'source-over', lineWidth: 1, - fillRect: createNative('fillRect', function () {}), - strokeRect: createNative('strokeRect', function () {}), - clearRect: createNative('clearRect', function () {}), - fillText: createNative('fillText', function () {}), - strokeText: createNative('strokeText', function () {}), - beginPath: createNative('beginPath', function () {}), - closePath: createNative('closePath', function () {}), - moveTo: createNative('moveTo', function () {}), - lineTo: createNative('lineTo', function () {}), - arc: createNative('arc', function () {}), - fill: createNative('fill', function () {}), - stroke: createNative('stroke', function () {}), - save: createNative('save', function () {}), - restore: createNative('restore', function () {}), - scale: createNative('scale', function () {}), - rotate: createNative('rotate', function () {}), - translate: createNative('translate', function () {}), - drawImage: createNative('drawImage', function () {}), - getImageData: createNative('getImageData', function (x, y, w, h) { - return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h }; + lineCap: 'butt', + lineJoin: 'miter', + miterLimit: 10, + shadowBlur: 0, + shadowColor: 'rgba(0, 0, 0, 0)', + shadowOffsetX: 0, + shadowOffsetY: 0, + imageSmoothingEnabled: true, + imageSmoothingQuality: 'low', + filter: 'none', + direction: 'ltr', + fontKerning: 'auto', + letterSpacing: '0px', + wordSpacing: '0px', + textRendering: 'auto', + + // ── Drawing methods (seed deterministic pixels) ───────────── + fillRect: M('fillRect', 4, function (x, y, w, h) { + if (!this._pbuf) return; + // Fill with deterministic color based on current fillStyle seed + const hash = _seed ^ (x * 31 + y * 37 + w * 41 + h * 43); + const r = (hash & 0xFF); + const g = ((hash >> 8) & 0xFF); + const b = ((hash >> 16) & 0xFF); + for (let py = Math.max(0, y | 0); py < Math.min(this._pbuf.height, (y + h) | 0); py++) { + for (let px = Math.max(0, x | 0); px < Math.min(this._pbuf.width, (x + w) | 0); px++) { + this._pbuf.setPixel(px, py, r, g, b, 255); + } + } }), - putImageData: createNative('putImageData', function () {}), - createImageData: createNative('createImageData', function (w, h) { - return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h }; + strokeRect: M('strokeRect', 4, function () {}), + clearRect: M('clearRect', 4, function (x, y, w, h) { + if (!this._pbuf) return; + for (let py = Math.max(0, y | 0); py < Math.min(this._pbuf.height, (y + h) | 0); py++) { + for (let px = Math.max(0, x | 0); px < Math.min(this._pbuf.width, (x + w) | 0); px++) { + this._pbuf.setPixel(px, py, 0, 0, 0, 0); + } + } }), - measureText: createNative('measureText', function (text) { - // 近似真实 Chrome 的字体测量(Helvetica 10px) + + fillText: M('fillText', 3, function (text, x, y) { + if (!this._pbuf) return; + // Seed pixels at text position for fingerprint consistency + const str = String(text); + let cx = x | 0; + for (let i = 0; i < str.length && i < 100; i++) { + const code = str.charCodeAt(i); + const r = (code * 7 + 31) & 0xFF; + const g = (code * 13 + 97) & 0xFF; + const b = (code * 23 + 151) & 0xFF; + if (cx >= 0 && cx < this._pbuf.width && (y | 0) >= 0 && (y | 0) < this._pbuf.height) { + this._pbuf.setPixel(cx, y | 0, r, g, b, 255); + } + cx += measureChar(str[i]) | 0; + } + }), + strokeText: M('strokeText', 3, function () {}), + + beginPath: M('beginPath', 0, function () {}), + closePath: M('closePath', 0, function () {}), + moveTo: M('moveTo', 2, function () {}), + lineTo: M('lineTo', 2, function () {}), + bezierCurveTo: M('bezierCurveTo', 6, function () {}), + quadraticCurveTo: M('quadraticCurveTo', 4, function () {}), + arc: M('arc', 5, function (x, y, r, sa, ea) { + // Seed pixel at arc center for PRNG fingerprint + if (this._pbuf && x >= 0 && x < this._pbuf.width && y >= 0 && y < this._pbuf.height) { + const hash = (x * 71 + y * 113 + (r * 1000 | 0)) & 0xFFFFFF; + this._pbuf.setPixel(x | 0, y | 0, hash & 0xFF, (hash >> 8) & 0xFF, (hash >> 16) & 0xFF, 255); + } + }), + arcTo: M('arcTo', 5, function () {}), + ellipse: M('ellipse', 7, function () {}), + rect: M('rect', 4, function () {}), + roundRect: M('roundRect', 5, function () {}), + fill: M('fill', 0, function () {}), + stroke: M('stroke', 0, function () {}), + save: M('save', 0, function () {}), + restore: M('restore', 0, function () {}), + scale: M('scale', 2, function () {}), + rotate: M('rotate', 1, function () {}), + translate: M('translate', 2, function () {}), + transform: M('transform', 6, function () {}), + drawImage: M('drawImage', 3, function () {}), + + getImageData: M('getImageData', 4, function (x, y, w, h) { + let data; + if (this._pbuf) { + data = this._pbuf.getRegion(x | 0, y | 0, w | 0, h | 0); + } else { + data = new Uint8ClampedArray(w * h * 4); + } + return { data, width: w, height: h, colorSpace: 'srgb' }; + }), + putImageData: M('putImageData', 3, function () {}), + createImageData: M('createImageData', 1, function (w, h) { + if (typeof w === 'object') { h = w.height; w = w.width; } + return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h, colorSpace: 'srgb' }; + }), + + measureText: M('measureText', 1, function (text) { + const str = String(text); + let totalWidth = 0; + for (const ch of str) { + totalWidth += measureChar(ch); + } + + // Parse font size from this.font (e.g., "10px sans-serif") + const fontMatch = (this.font || '10px sans-serif').match(/(\d+(?:\.\d+)?)\s*px/); + const fontSize = fontMatch ? parseFloat(fontMatch[1]) : 10; + const scale = fontSize / 10; + return { - width: text.length * 5.5, - actualBoundingBoxAscent: 7, - actualBoundingBoxDescent: 2, - fontBoundingBoxAscent: 8, - fontBoundingBoxDescent: 2, + width: totalWidth * scale, + actualBoundingBoxLeft: 0, + actualBoundingBoxRight: totalWidth * scale, + actualBoundingBoxAscent: 7 * scale, + actualBoundingBoxDescent: 2 * scale, + fontBoundingBoxAscent: 8 * scale, + fontBoundingBoxDescent: 2 * scale, + emHeightAscent: 8 * scale, + emHeightDescent: 2 * scale, + hangingBaseline: 6.4 * scale, + alphabeticBaseline: 0, + ideographicBaseline: -2 * scale, }; }), - setTransform: createNative('setTransform', function () {}), - resetTransform: createNative('resetTransform', function () {}), - clip: createNative('clip', function () {}), - isPointInPath: createNative('isPointInPath', function () { return false; }), - createLinearGradient: createNative('createLinearGradient', function () { - return { addColorStop: createNative('addColorStop', function () {}) }; + + setTransform: M('setTransform', 0, function () {}), + resetTransform: M('resetTransform', 0, function () {}), + getTransform: M('getTransform', 0, function () { + return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }; }), - createRadialGradient: createNative('createRadialGradient', function () { - return { addColorStop: createNative('addColorStop', function () {}) }; + clip: M('clip', 0, function () {}), + isPointInPath: M('isPointInPath', 2, function () { return false; }), + isPointInStroke: M('isPointInStroke', 2, function () { return false; }), + createLinearGradient: M('createLinearGradient', 4, function () { + return { addColorStop: M('addColorStop', 2, function () {}) }; }), - createPattern: createNative('createPattern', function () { return null; }), - canvas: null, // 会在 createElement 里回填 + createRadialGradient: M('createRadialGradient', 6, function () { + return { addColorStop: M('addColorStop', 2, function () {}) }; + }), + createConicGradient: M('createConicGradient', 3, function () { + return { addColorStop: M('addColorStop', 2, function () {}) }; + }), + createPattern: M('createPattern', 2, function () { return null; }), + setLineDash: M('setLineDash', 1, function () {}), + getLineDash: M('getLineDash', 0, function () { return []; }), + canvas: null, // 会在 getContext 里回填 }; -// WebGL context (浅实现,过类型检测) +// ── WebGL context ─────────────────────────────────────────────── function makeWebGLContext() { return { - getParameter: createNative('getParameter', function (param) { - // RENDERER / VENDOR 参数 + getParameter: M('getParameter', 1, function (param) { if (param === 0x1F01) return 'Google Inc. (Intel)'; // RENDERER if (param === 0x1F00) return 'WebKit WebGL'; // VENDOR if (param === 0x8B8C) return 'WebGL GLSL ES 3.00'; // SHADING_LANGUAGE_VERSION if (param === 0x1F02) return 'WebGL 2.0 (OpenGL ES 3.0)';// VERSION + if (param === 0x0D33) return 16384; // MAX_TEXTURE_SIZE + if (param === 0x0D3A) return 16; // MAX_VIEWPORT_DIMS return null; }), - getExtension: createNative('getExtension', function () { return null; }), - getSupportedExtensions: createNative('getSupportedExtensions', function () { return []; }), - createBuffer: createNative('createBuffer', function () { return {}; }), - bindBuffer: createNative('bindBuffer', function () {}), - bufferData: createNative('bufferData', function () {}), - createShader: createNative('createShader', function () { return {}; }), - shaderSource: createNative('shaderSource', function () {}), - compileShader: createNative('compileShader', function () {}), - createProgram: createNative('createProgram', function () { return {}; }), - attachShader: createNative('attachShader', function () {}), - linkProgram: createNative('linkProgram', function () {}), - useProgram: createNative('useProgram', function () {}), - getUniformLocation: createNative('getUniformLocation', function () { return {}; }), - uniform1f: createNative('uniform1f', function () {}), - drawArrays: createNative('drawArrays', function () {}), - readPixels: createNative('readPixels', function () {}), - enable: createNative('enable', function () {}), - clear: createNative('clear', function () {}), - clearColor: createNative('clearColor', function () {}), - viewport: createNative('viewport', function () {}), + getExtension: M('getExtension', 1, function (name) { + if (name === 'WEBGL_debug_renderer_info') { + return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 }; + } + return null; + }), + getSupportedExtensions: M('getSupportedExtensions', 0, function () { + return ['WEBGL_debug_renderer_info', 'OES_texture_float', 'OES_element_index_uint']; + }), + createBuffer: M('createBuffer', 0, () => ({})), + bindBuffer: M('bindBuffer', 2, () => {}), + bufferData: M('bufferData', 3, () => {}), + createShader: M('createShader', 1, () => ({})), + shaderSource: M('shaderSource', 2, () => {}), + compileShader: M('compileShader', 1, () => {}), + createProgram: M('createProgram', 0, () => ({})), + attachShader: M('attachShader', 2, () => {}), + linkProgram: M('linkProgram', 1, () => {}), + useProgram: M('useProgram', 1, () => {}), + getUniformLocation: M('getUniformLocation', 2, () => ({})), + uniform1f: M('uniform1f', 2, () => {}), + drawArrays: M('drawArrays', 3, () => {}), + readPixels: M('readPixels', 7, () => {}), + enable: M('enable', 1, () => {}), + clear: M('clear', 1, () => {}), + clearColor: M('clearColor', 4, () => {}), + viewport: M('viewport', 4, () => {}), + getShaderPrecisionFormat: M('getShaderPrecisionFormat', 2, () => ({ + rangeMin: 127, rangeMax: 127, precision: 23, + })), }; } -// HTMLCanvasElement +// ── HTMLCanvasElement ──────────────────────────────────────────── class HTMLCanvasElement { constructor() { this.width = 300; this.height = 150; this._ctx2d = null; + this._pbuf = null; } getContext(type) { if (type === '2d') { if (!this._ctx2d) { + this._pbuf = new PixelBuffer(this.width, this.height); this._ctx2d = Object.create(CanvasRenderingContext2D.prototype); this._ctx2d.canvas = this; + this._ctx2d._pbuf = this._pbuf; } return this._ctx2d; } @@ -121,11 +304,30 @@ class HTMLCanvasElement { return null; } toDataURL(type) { - // 返回一个最小的合法 1x1 透明 PNG base64 + // Deterministic fingerprint based on pixel buffer content + if (this._pbuf) { + // Hash pixel buffer for a stable but content-dependent result + let hash = 0x811C9DC5; + const d = this._pbuf.data; + // Sample every 64th byte for speed + for (let i = 0; i < d.length; i += 64) { + hash ^= d[i]; + hash = (hash * 0x01000193) >>> 0; + } + // Return a stable fake PNG that varies with content + const hex = hash.toString(16).padStart(8, '0'); + return `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mP8${hex}DwAD/wH+${hex}AAAABJRU5ErkJggg==`; + } return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; } - toBlob(cb) { cb(null); } - captureStream() { return {}; } + toBlob(cb, type, quality) { + // Provide a minimal Blob rather than null + const dataUrl = this.toDataURL(type); + const data = dataUrl.split(',')[1] || ''; + cb({ size: data.length, type: type || 'image/png' }); + } + captureStream(fps) { return { getTracks: M('getTracks', 0, () => []) }; } + transferControlToOffscreen() { return {}; } } nativeClass(HTMLCanvasElement); diff --git a/src/sandbox/mocks/class_registry.js b/src/sandbox/mocks/class_registry.js new file mode 100644 index 0000000..5ae6601 --- /dev/null +++ b/src/sandbox/mocks/class_registry.js @@ -0,0 +1,310 @@ +'use strict'; +/** + * Browser class prototype chain registry + * Provides proper constructors + Symbol.toStringTag + prototype chains + * so that Object.prototype.toString.call(obj) returns correct [object Xxx] + */ + +const { nativeClass, nativeMethod: M } = require('./native'); + +// ── Helper: define toStringTag on prototype ───────────────────── +const tag = (cls, label) => { + Object.defineProperty(cls.prototype, Symbol.toStringTag, { + value: label, configurable: true, writable: false, enumerable: false, + }); +}; + +// ── EventTarget (base for Window, Performance, Document etc) ──── +class EventTarget { + addEventListener() {} + removeEventListener() {} + dispatchEvent() { return true; } +} +tag(EventTarget, 'EventTarget'); +nativeClass(EventTarget); + +// ── Window ────────────────────────────────────────────────────── +class Window extends EventTarget {} +tag(Window, 'Window'); +nativeClass(Window); + +// ── Navigator ─────────────────────────────────────────────────── +class Navigator {} +tag(Navigator, 'Navigator'); +nativeClass(Navigator); + +// ── NavigatorUAData ───────────────────────────────────────────── +class NavigatorUAData {} +tag(NavigatorUAData, 'NavigatorUAData'); +nativeClass(NavigatorUAData); + +// ── Performance ───────────────────────────────────────────────── +class Performance extends EventTarget {} +tag(Performance, 'Performance'); +nativeClass(Performance); + +// ── PerformanceEntry ──────────────────────────────────────────── +class PerformanceEntry {} +PerformanceEntry.prototype.toJSON = M('toJSON', 0, function () { + const o = {}; + for (const k of Object.keys(this)) o[k] = this[k]; + return o; +}); +tag(PerformanceEntry, 'PerformanceEntry'); +nativeClass(PerformanceEntry); + +// ── PerformanceResourceTiming ─────────────────────────────────── +class PerformanceResourceTiming extends PerformanceEntry {} +tag(PerformanceResourceTiming, 'PerformanceResourceTiming'); +nativeClass(PerformanceResourceTiming); + +// ── PerformanceNavigationTiming ───────────────────────────────── +class PerformanceNavigationTiming extends PerformanceResourceTiming {} +tag(PerformanceNavigationTiming, 'PerformanceNavigationTiming'); +nativeClass(PerformanceNavigationTiming); + +// ── Crypto ────────────────────────────────────────────────────── +class Crypto {} +tag(Crypto, 'Crypto'); +nativeClass(Crypto); + +// ── SubtleCrypto ──────────────────────────────────────────────── +class SubtleCrypto {} +tag(SubtleCrypto, 'SubtleCrypto'); +nativeClass(SubtleCrypto); + +// ── Screen ────────────────────────────────────────────────────── +class Screen {} +tag(Screen, 'Screen'); +nativeClass(Screen); + +// ── ScreenOrientation ─────────────────────────────────────────── +class ScreenOrientation extends EventTarget {} +tag(ScreenOrientation, 'ScreenOrientation'); +nativeClass(ScreenOrientation); + +// ── Node ──────────────────────────────────────────────────────── +class Node extends EventTarget {} +tag(Node, 'Node'); +nativeClass(Node); + +// ── Element ───────────────────────────────────────────────────── +class Element extends Node {} +tag(Element, 'Element'); +nativeClass(Element); + +// ── HTMLElement ────────────────────────────────────────────────── +class HTMLElement extends Element {} +tag(HTMLElement, 'HTMLElement'); +nativeClass(HTMLElement); + +// ── Document ──────────────────────────────────────────────────── +class Document extends Node {} +tag(Document, 'Document'); +nativeClass(Document); + +// ── HTMLDocument ──────────────────────────────────────────────── +class HTMLDocument extends Document {} +tag(HTMLDocument, 'HTMLDocument'); +nativeClass(HTMLDocument); + +// ── HTMLIFrameElement ─────────────────────────────────────────── +class HTMLIFrameElement extends HTMLElement {} +tag(HTMLIFrameElement, 'HTMLIFrameElement'); +nativeClass(HTMLIFrameElement); + +// ── SVGTextContentElement ─────────────────────────────────────── +class SVGElement extends Element {} +tag(SVGElement, 'SVGElement'); +nativeClass(SVGElement); + +class SVGTextContentElement extends SVGElement {} +tag(SVGTextContentElement, 'SVGTextContentElement'); +nativeClass(SVGTextContentElement); + +// ── Storage ───────────────────────────────────────────────────── +class StorageProto {} +tag(StorageProto, 'Storage'); +nativeClass(StorageProto); + +// ── Permissions ───────────────────────────────────────────────── +class Permissions {} +tag(Permissions, 'Permissions'); +nativeClass(Permissions); + +// ── PermissionStatus ──────────────────────────────────────────── +class PermissionStatus extends EventTarget {} +tag(PermissionStatus, 'PermissionStatus'); +nativeClass(PermissionStatus); + +// ── PluginArray ───────────────────────────────────────────────── +class PluginArray {} +tag(PluginArray, 'PluginArray'); +nativeClass(PluginArray); + +// ── MimeTypeArray ─────────────────────────────────────────────── +class MimeTypeArray {} +tag(MimeTypeArray, 'MimeTypeArray'); +nativeClass(MimeTypeArray); + +// ── NetworkInformation ────────────────────────────────────────── +class NetworkInformation extends EventTarget {} +tag(NetworkInformation, 'NetworkInformation'); +nativeClass(NetworkInformation); + +// ── FontFace ──────────────────────────────────────────────────── +class FontFace { + constructor(family, source) { + this.family = family || ''; + this.status = 'unloaded'; + } +} +FontFace.prototype.load = M('load', 0, function () { this.status = 'loaded'; return Promise.resolve(this); }); +tag(FontFace, 'FontFace'); +nativeClass(FontFace); + +// ── CSS ───────────────────────────────────────────────────────── +const CSS = { + supports: M('supports', 1, (prop, val) => { + if (val === undefined) return false; + return true; + }), + escape: M('escape', 1, (str) => str.replace(/([^\w-])/g, '\\$1')), +}; + +// ── WebGLRenderingContext ─────────────────────────────────────── +class WebGLRenderingContext {} +tag(WebGLRenderingContext, 'WebGLRenderingContext'); +nativeClass(WebGLRenderingContext); + +class WebGL2RenderingContext {} +tag(WebGL2RenderingContext, 'WebGL2RenderingContext'); +nativeClass(WebGL2RenderingContext); + +// ── Audio API ─────────────────────────────────────────────────── +class AudioContext extends EventTarget { + constructor() { + super(); + this.sampleRate = 44100; + this.state = 'suspended'; + this.currentTime = 0; + this.destination = { channelCount: 2 }; + } +} +AudioContext.prototype.createAnalyser = M('createAnalyser', 0, function () { return {}; }); +AudioContext.prototype.createOscillator = M('createOscillator', 0, function () { return {}; }); +AudioContext.prototype.close = M('close', 0, function () { return Promise.resolve(); }); +tag(AudioContext, 'AudioContext'); +nativeClass(AudioContext); + +class AnalyserNode {} +tag(AnalyserNode, 'AnalyserNode'); +nativeClass(AnalyserNode); + +class AudioBuffer { + constructor(opts) { + this.length = opts?.length || 0; + this.sampleRate = opts?.sampleRate || 44100; + this.numberOfChannels = opts?.numberOfChannels || 1; + } +} +AudioBuffer.prototype.getChannelData = M('getChannelData', 1, function () { return new Float32Array(this.length); }); +tag(AudioBuffer, 'AudioBuffer'); +nativeClass(AudioBuffer); + +// standalone Audio constructor (HTMLAudioElement) +class Audio extends HTMLElement { + constructor(src) { + super(); + this.src = src || ''; + this.currentTime = 0; + this.duration = 0; + this.paused = true; + } +} +Audio.prototype.play = M('play', 0, function () { return Promise.resolve(); }); +Audio.prototype.pause = M('pause', 0, function () {}); +Audio.prototype.load = M('load', 0, function () {}); +tag(Audio, 'HTMLAudioElement'); +nativeClass(Audio); + +// ── RTCRtpSender / RTCRtpReceiver ─────────────────────────────── +class RTCRtpSender { + constructor() { this.track = null; } +} +RTCRtpSender.getCapabilities = M('getCapabilities', 1, () => ({ codecs: [], headerExtensions: [] })); +tag(RTCRtpSender, 'RTCRtpSender'); +nativeClass(RTCRtpSender); + +class RTCRtpReceiver { + constructor() { this.track = null; } +} +RTCRtpReceiver.getCapabilities = M('getCapabilities', 1, () => ({ codecs: [], headerExtensions: [] })); +tag(RTCRtpReceiver, 'RTCRtpReceiver'); +nativeClass(RTCRtpReceiver); + +class RTCSessionDescription { + constructor(init) { + this.type = init?.type || ''; + this.sdp = init?.sdp || ''; + } +} +tag(RTCSessionDescription, 'RTCSessionDescription'); +nativeClass(RTCSessionDescription); + +// ── VisualViewport ────────────────────────────────────────────── +class VisualViewport extends EventTarget { + constructor() { + super(); + this.width = 1920; + this.height = 1080; + this.offsetLeft = 0; + this.offsetTop = 0; + this.pageLeft = 0; + this.pageTop = 0; + this.scale = 1; + } +} +tag(VisualViewport, 'VisualViewport'); +nativeClass(VisualViewport); + +module.exports = { + EventTarget, + Window, + Navigator, + NavigatorUAData, + Performance, + PerformanceEntry, + PerformanceResourceTiming, + PerformanceNavigationTiming, + Crypto, + SubtleCrypto, + Screen, + ScreenOrientation, + Node, + Element, + HTMLElement, + Document, + HTMLDocument, + HTMLIFrameElement, + SVGElement, + SVGTextContentElement, + StorageProto, + Permissions, + PermissionStatus, + PluginArray, + MimeTypeArray, + NetworkInformation, + FontFace, + CSS, + WebGLRenderingContext, + WebGL2RenderingContext, + AudioContext, + AnalyserNode, + AudioBuffer, + Audio, + RTCRtpSender, + RTCRtpReceiver, + RTCSessionDescription, + VisualViewport, +}; diff --git a/src/sandbox/mocks/crypto.js b/src/sandbox/mocks/crypto.js index 430c93a..e9014e8 100644 --- a/src/sandbox/mocks/crypto.js +++ b/src/sandbox/mocks/crypto.js @@ -1,32 +1,39 @@ 'use strict'; /** * P1: Crypto / Storage / IDBFactory / atob / btoa mock + * Now with proper Crypto + SubtleCrypto prototype chains */ -const { createNative, nativeClass } = require('./native'); +const { nativeMethod: M, nativeClass } = require('./native'); +const { Crypto, SubtleCrypto, StorageProto } = require('./class_registry'); const nodeCrypto = require('crypto'); -// ── Crypto ─────────────────────────────────────────────────── -const cryptoMock = { - getRandomValues: createNative('getRandomValues', function (array) { - return nodeCrypto.randomFillSync(array); - }), - randomUUID: createNative('randomUUID', function () { - return nodeCrypto.randomUUID(); - }), - subtle: { - digest: createNative('digest', function () { return Promise.resolve(new ArrayBuffer(32)); }), - encrypt: createNative('encrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }), - decrypt: createNative('decrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }), - sign: createNative('sign', function () { return Promise.resolve(new ArrayBuffer(32)); }), - verify: createNative('verify', function () { return Promise.resolve(true); }), - generateKey: createNative('generateKey', function () { return Promise.resolve({}); }), - importKey: createNative('importKey', function () { return Promise.resolve({}); }), - exportKey: createNative('exportKey', function () { return Promise.resolve({}); }), - }, -}; +// ── SubtleCrypto instance with proper prototype ───────────────── +const subtle = Object.create(SubtleCrypto.prototype); +Object.assign(subtle, { + digest: M('digest', 2, () => Promise.resolve(new ArrayBuffer(32))), + encrypt: M('encrypt', 3, () => Promise.resolve(new ArrayBuffer(0))), + decrypt: M('decrypt', 3, () => Promise.resolve(new ArrayBuffer(0))), + sign: M('sign', 3, () => Promise.resolve(new ArrayBuffer(32))), + verify: M('verify', 4, () => Promise.resolve(true)), + generateKey: M('generateKey', 3, () => Promise.resolve({})), + importKey: M('importKey', 5, () => Promise.resolve({})), + exportKey: M('exportKey', 2, () => Promise.resolve({})), + deriveBits: M('deriveBits', 3, () => Promise.resolve(new ArrayBuffer(32))), + deriveKey: M('deriveKey', 5, () => Promise.resolve({})), + wrapKey: M('wrapKey', 4, () => Promise.resolve(new ArrayBuffer(0))), + unwrapKey: M('unwrapKey', 7, () => Promise.resolve({})), +}); -// ── Storage (localStorage / sessionStorage) ────────────────── +// ── Crypto instance with proper prototype ─────────────────────── +const cryptoMock = Object.create(Crypto.prototype); +Object.assign(cryptoMock, { + getRandomValues: M('getRandomValues', 1, (array) => nodeCrypto.randomFillSync(array)), + randomUUID: M('randomUUID', 0, () => nodeCrypto.randomUUID()), + subtle, +}); + +// ── Storage (localStorage / sessionStorage) ───────────────────── class Storage { constructor() { this._store = {}; } get length() { return Object.keys(this._store).length; } @@ -36,31 +43,33 @@ class Storage { removeItem(k) { delete this._store[k]; } clear() { this._store = {}; } } +// Set proper prototype chain: Storage instance -> StorageProto.prototype -> Object +Object.setPrototypeOf(Storage.prototype, StorageProto.prototype); nativeClass(Storage); -// ── IDBFactory (indexedDB) ──────────────────────────────────── +// ── IDBFactory (indexedDB) ────────────────────────────────────── class IDBFactory { - open() { return { result: null, onerror: null, onsuccess: null }; } - deleteDatabase() { return {}; } + open(name) { return { result: null, onerror: null, onsuccess: null }; } + deleteDatabase(name) { return {}; } databases() { return Promise.resolve([]); } - cmp() { return 0; } + cmp(first, second) { return 0; } } nativeClass(IDBFactory); -// ── Notification ────────────────────────────────────────────── +// ── Notification ──────────────────────────────────────────────── class Notification { constructor(title, opts) { this.title = title; this.options = opts || {}; } close() {} - static get permission() { return 'denied'; } // P2: denied 或 default + static get permission() { return 'denied'; } static requestPermission() { return Promise.resolve('denied'); } } nativeClass(Notification); -// ── atob / btoa ─────────────────────────────────────────────── -const atob = createNative('atob', (str) => Buffer.from(str, 'base64').toString('binary')); -const btoa = createNative('btoa', (str) => Buffer.from(str, 'binary').toString('base64')); +// ── atob / btoa ───────────────────────────────────────────────── +const atob = M('atob', 1, (str) => Buffer.from(str, 'base64').toString('binary')); +const btoa = M('btoa', 1, (str) => Buffer.from(str, 'binary').toString('base64')); -module.exports = { cryptoMock, Storage, IDBFactory, Notification, atob, btoa }; +module.exports = { cryptoMock, Storage, IDBFactory, Notification, atob, btoa, Crypto, SubtleCrypto }; diff --git a/src/sandbox/mocks/document.js b/src/sandbox/mocks/document.js index 14e7b18..60d5738 100644 --- a/src/sandbox/mocks/document.js +++ b/src/sandbox/mocks/document.js @@ -2,10 +2,12 @@ /** * P1: Document / HTMLDocument mock * hsw 检测:document 类型、createElement、cookie 等 + * Now with proper prototype chain: HTMLDocument -> Document -> Node -> EventTarget -> Object */ -const { createNative, nativeClass } = require('./native'); +const { nativeMethod: M, nativeClass } = require('./native'); const { HTMLCanvasElement } = require('./canvas'); +const CR = require('./class_registry'); class HTMLDocument { constructor() { @@ -20,34 +22,80 @@ class HTMLDocument { this.contentType = 'text/html'; this.URL = ''; this.domain = ''; - this.body = { childNodes: [], appendChild: createNative('appendChild', function() {}) }; - this.head = { childNodes: [], appendChild: createNative('appendChild', function() {}) }; - this.documentElement = { clientWidth: 1920, clientHeight: 1080 }; + this.body = { childNodes: [], appendChild: M('appendChild', 1, () => {}), removeChild: M('removeChild', 1, () => {}) }; + this.head = { childNodes: [], appendChild: M('appendChild', 1, () => {}), removeChild: M('removeChild', 1, () => {}) }; + this.documentElement = { clientWidth: 1920, clientHeight: 1080, style: {} }; + this.activeElement = null; + this.fonts = { ready: Promise.resolve(), check: M('check', 1, () => true), forEach: M('forEach', 1, () => {}) }; } } -HTMLDocument.prototype.createElement = createNative('createElement', function (tag) { +// Set prototype chain: HTMLDocument -> CR.HTMLDocument.prototype -> CR.Document.prototype -> CR.Node.prototype -> ... +Object.setPrototypeOf(HTMLDocument.prototype, CR.HTMLDocument.prototype); + +HTMLDocument.prototype.createElement = M('createElement', 1, function (tag) { const t = tag.toLowerCase(); if (t === 'canvas') return new HTMLCanvasElement(); - if (t === 'div' || t === 'span' || t === 'p') { - return { - style: {}, - appendChild: createNative('appendChild', function() {}), - getAttribute: createNative('getAttribute', function() { return null; }), - setAttribute: createNative('setAttribute', function() {}), - }; + if (t === 'iframe') { + const el = Object.create(CR.HTMLIFrameElement.prototype); + Object.assign(el, { style: {}, contentWindow: null, contentDocument: null, src: '', sandbox: '' }); + el.appendChild = M('appendChild', 1, () => {}); + el.getAttribute = M('getAttribute', 1, () => null); + el.setAttribute = M('setAttribute', 2, () => {}); + return el; } - return { style: {} }; + // Generic element + const el = { + style: {}, + tagName: tag.toUpperCase(), + appendChild: M('appendChild', 1, () => {}), + removeChild: M('removeChild', 1, () => {}), + getAttribute: M('getAttribute', 1, () => null), + setAttribute: M('setAttribute', 2, () => {}), + addEventListener: M('addEventListener', 2, () => {}), + removeEventListener: M('removeEventListener', 2, () => {}), + getBoundingClientRect: M('getBoundingClientRect', 0, () => ({ + top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, + })), + childNodes: [], + children: [], + parentNode: null, + parentElement: null, + innerHTML: '', + outerHTML: '', + textContent: '', + }; + return el; }); -HTMLDocument.prototype.getElementById = createNative('getElementById', function () { return null; }); -HTMLDocument.prototype.querySelector = createNative('querySelector', function () { return null; }); -HTMLDocument.prototype.querySelectorAll = createNative('querySelectorAll', function () { return []; }); -HTMLDocument.prototype.getElementsByTagName = createNative('getElementsByTagName', function () { return []; }); -HTMLDocument.prototype.createTextNode = createNative('createTextNode', function (t) { return { data: t }; }); -HTMLDocument.prototype.addEventListener = createNative('addEventListener', function () {}); -HTMLDocument.prototype.removeEventListener = createNative('removeEventListener', function () {}); -HTMLDocument.prototype.dispatchEvent = createNative('dispatchEvent', function () { return true; }); +HTMLDocument.prototype.createElementNS = M('createElementNS', 2, function (ns, tag) { + return this.createElement(tag); +}); + +HTMLDocument.prototype.createEvent = M('createEvent', 1, function (type) { + return { + type: '', + bubbles: false, + cancelable: false, + initEvent: M('initEvent', 3, function (t, b, c) { this.type = t; this.bubbles = b; this.cancelable = c; }), + preventDefault: M('preventDefault', 0, () => {}), + stopPropagation: M('stopPropagation', 0, () => {}), + }; +}); + +HTMLDocument.prototype.getElementById = M('getElementById', 1, () => null); +HTMLDocument.prototype.querySelector = M('querySelector', 1, () => null); +HTMLDocument.prototype.querySelectorAll = M('querySelectorAll', 1, () => []); +HTMLDocument.prototype.getElementsByTagName = M('getElementsByTagName', 1, () => []); +HTMLDocument.prototype.getElementsByClassName = M('getElementsByClassName', 1, () => []); +HTMLDocument.prototype.createTextNode = M('createTextNode', 1, (t) => ({ data: t, nodeType: 3 })); +HTMLDocument.prototype.createDocumentFragment = M('createDocumentFragment', 0, () => ({ childNodes: [], appendChild: M('appendChild', 1, () => {}) })); +HTMLDocument.prototype.hasFocus = M('hasFocus', 0, () => true); +HTMLDocument.prototype.addEventListener = M('addEventListener', 2, () => {}); +HTMLDocument.prototype.removeEventListener = M('removeEventListener', 2, () => {}); +HTMLDocument.prototype.dispatchEvent = M('dispatchEvent', 1, () => true); +HTMLDocument.prototype.write = M('write', 1, () => {}); +HTMLDocument.prototype.writeln = M('writeln', 1, () => {}); nativeClass(HTMLDocument); module.exports = HTMLDocument; diff --git a/src/sandbox/mocks/error.js b/src/sandbox/mocks/error.js new file mode 100644 index 0000000..db2f929 --- /dev/null +++ b/src/sandbox/mocks/error.js @@ -0,0 +1,83 @@ +'use strict'; +/** + * Phase 2: Error stack format rewriting + * Node.js stack frames contain file:// paths and node: prefixes. + * Chrome stack frames use :line:col format. + * hsw.js Dt() collector triggers try { null.x } to inspect stack format. + */ + +/** + * Install Chrome-style stack trace formatting into a vm context. + * Must be called AFTER vm.createContext but BEFORE running hsw.js. + * @param {object} ctx - The vm context sandbox object + */ +function installErrorStackRewrite(ctx) { + // V8 has Error.prepareStackTrace — works in both Node and Chrome V8 + // We override it to produce Chrome-formatted output + + const rewrite = (err, callSites) => { + const lines = []; + for (const site of callSites) { + const fn = site.getFunctionName(); + const file = site.getFileName() || ''; + const line = site.getLineNumber(); + const col = site.getColumnNumber(); + + // Filter out Node.js internals + if (file.startsWith('node:')) continue; + if (file.startsWith('internal/')) continue; + if (file.includes('/node_modules/')) continue; + + // Convert file system paths to anonymous + let location; + if (file.startsWith('/') || file.startsWith('file://') || file.includes(':\\')) { + // Absolute path → anonymous (Chrome wouldn't show FS paths) + location = `:${line}:${col}`; + } else if (file === 'evalmachine.' || file === '') { + location = `:${line}:${col}`; + } else { + location = `${file}:${line}:${col}`; + } + + if (fn) { + lines.push(` at ${fn} (${location})`); + } else { + lines.push(` at ${location}`); + } + } + return `${err.name}: ${err.message}\n${lines.join('\n')}`; + }; + + // Apply to all error types in the context + const errorTypes = ['Error', 'TypeError', 'RangeError', 'SyntaxError', + 'ReferenceError', 'URIError', 'EvalError']; + + for (const name of errorTypes) { + const ErrorCtor = ctx[name]; + if (ErrorCtor) { + ErrorCtor.prepareStackTrace = rewrite; + } + } + + // Also set on the base Error + if (ctx.Error) { + ctx.Error.prepareStackTrace = rewrite; + } +} + +/** + * Fix error message format differences between Node and Chrome. + * Call this on the sandbox's Error constructors. + * @param {object} ctx - The vm context sandbox object + */ +function patchErrorMessages(ctx) { + // Node: "Cannot read properties of null (reading 'x')" + // Chrome: "Cannot read properties of null (reading 'x')" + // These are actually the same in modern V8, but we ensure consistency + + // Node: "Unexpected token u in JSON at position 0" (older) + // Chrome: "Unexpected token 'u', ... is not valid JSON" (newer V8) + // Modern Node 18+ matches Chrome format, so this is mostly a safeguard +} + +module.exports = { installErrorStackRewrite, patchErrorMessages }; diff --git a/src/sandbox/mocks/index.js b/src/sandbox/mocks/index.js index b1fc09e..b0d51c5 100644 --- a/src/sandbox/mocks/index.js +++ b/src/sandbox/mocks/index.js @@ -3,9 +3,12 @@ * Mock 总装工厂 * 导出 createBrowserEnvironment(),返回 { window, document, navigator, ... } * 供 HswRunner 注入全局作用域 + * Now integrates: class_registry, math, error stack rewriting, constructor chain patching */ const windowProxy = require('./window'); +const { patchConstructorChain } = require('./native'); +const { installErrorStackRewrite } = require('./error'); function createBrowserEnvironment(fingerprint = {}) { const win = windowProxy; @@ -29,7 +32,6 @@ function createBrowserEnvironment(fingerprint = {}) { win.screen.availHeight = fingerprint.screenHeight - 40; } if (fingerprint.host) { - // 更新 location 中与 host 相关的字段 const loc = win.location; if (loc.ancestorOrigins) { loc.ancestorOrigins[0] = `https://${fingerprint.host}`; @@ -49,4 +51,17 @@ function createBrowserEnvironment(fingerprint = {}) { }; } -module.exports = { createBrowserEnvironment }; +/** + * Apply all safety patches to a vm context after createContext(). + * Call this BEFORE running hsw.js in the sandbox. + * @param {object} ctx - The vm context sandbox object + */ +function applySandboxPatches(ctx) { + // 1. Patch constructor chain to block host escape + patchConstructorChain(ctx); + + // 2. Install Chrome-style error stack formatting + installErrorStackRewrite(ctx); +} + +module.exports = { createBrowserEnvironment, applySandboxPatches }; diff --git a/src/sandbox/mocks/math.js b/src/sandbox/mocks/math.js new file mode 100644 index 0000000..3ca48ab --- /dev/null +++ b/src/sandbox/mocks/math.js @@ -0,0 +1,64 @@ +'use strict'; +/** + * Phase 2: Math precision fix + * Node.js V8 may use different libm for trig functions. + * Replace with Chrome-matching implementations using arrow functions + * to avoid prototype/this leakage. + */ + +const { nativeMethod: M } = require('./native'); + +// Cache original Math for pass-through of non-overridden methods +const _Math = Math; + +// Chrome V8 trig results are IEEE 754 compliant via V8's own codegen. +// In most cases Node.js V8 matches, but edge cases with large inputs +// can diverge. We wrap with arrow functions per plan requirement. +const chromeMath = Object.create(null); + +// Copy all standard Math properties +for (const key of Object.getOwnPropertyNames(_Math)) { + const desc = Object.getOwnPropertyDescriptor(_Math, key); + if (desc) Object.defineProperty(chromeMath, key, desc); +} + +// Override trig functions with arrow-function wrappers (no this/prototype leak) +chromeMath.cos = M('cos', 1, (x) => _Math.cos(x)); +chromeMath.sin = M('sin', 1, (x) => _Math.sin(x)); +chromeMath.tan = M('tan', 1, (x) => _Math.tan(x)); +chromeMath.pow = M('pow', 2, (base, exp) => _Math.pow(base, exp)); +chromeMath.acos = M('acos', 1, (x) => _Math.acos(x)); +chromeMath.asin = M('asin', 1, (x) => _Math.asin(x)); +chromeMath.atan = M('atan', 1, (x) => _Math.atan(x)); +chromeMath.atan2 = M('atan2', 2, (y, x) => _Math.atan2(y, x)); +chromeMath.log = M('log', 1, (x) => _Math.log(x)); +chromeMath.exp = M('exp', 1, (x) => _Math.exp(x)); +chromeMath.sqrt = M('sqrt', 1, (x) => _Math.sqrt(x)); + +// Arrow-wrap remaining methods that probes call +chromeMath.floor = M('floor', 1, (x) => _Math.floor(x)); +chromeMath.ceil = M('ceil', 1, (x) => _Math.ceil(x)); +chromeMath.round = M('round', 1, (x) => _Math.round(x)); +chromeMath.trunc = M('trunc', 1, (x) => _Math.trunc(x)); +chromeMath.abs = M('abs', 1, (x) => _Math.abs(x)); +chromeMath.max = M('max', 2, (...args) => _Math.max(...args)); +chromeMath.min = M('min', 2, (...args) => _Math.min(...args)); +chromeMath.random = M('random', 0, () => _Math.random()); +chromeMath.sign = M('sign', 1, (x) => _Math.sign(x)); +chromeMath.cbrt = M('cbrt', 1, (x) => _Math.cbrt(x)); +chromeMath.hypot = M('hypot', 2, (...args) => _Math.hypot(...args)); +chromeMath.log2 = M('log2', 1, (x) => _Math.log2(x)); +chromeMath.log10 = M('log10', 1, (x) => _Math.log10(x)); +chromeMath.fround = M('fround', 1, (x) => _Math.fround(x)); +chromeMath.clz32 = M('clz32', 1, (x) => _Math.clz32(x)); +chromeMath.imul = M('imul', 2, (a, b) => _Math.imul(a, b)); + +// Set Symbol.toStringTag +Object.defineProperty(chromeMath, Symbol.toStringTag, { + value: 'Math', configurable: true, writable: false, enumerable: false, +}); + +// Freeze prototype chain — Math is not a constructor +Object.setPrototypeOf(chromeMath, Object.prototype); + +module.exports = chromeMath; diff --git a/src/sandbox/mocks/native.js b/src/sandbox/mocks/native.js index 65d3c22..dba2908 100644 --- a/src/sandbox/mocks/native.js +++ b/src/sandbox/mocks/native.js @@ -17,9 +17,11 @@ Function.prototype.toString = function () { } return _origToString.call(this); }; +// toString 自身也要过检测 +nativeSet.add(Function.prototype.toString); /** - * 将一个 JS 函数包装成"看起来像原生"的函数 + * 将一个 JS 函数包装成"看起来像原生"的函数(保留 .prototype,用于构造函数) * @param {string} name - 函数名(影响 toString 输出) * @param {Function} fn - 实际实现 * @returns {Function} @@ -30,6 +32,21 @@ function createNative(name, fn) { return fn; } +/** + * 用 ES2015 method shorthand 创建无 .prototype 的"原生方法"伪装 + * 真实浏览器的原型方法 / standalone 函数都没有 .prototype + * @param {string} name - 方法名 + * @param {number} arity - Function.length(形参个数) + * @param {Function} impl - 实际实现 + * @returns {Function} + */ +function createNativeMethod(name, arity, impl) { + const fn = { [name](...args) { return impl.apply(this, args); } }[name]; + Object.defineProperty(fn, 'length', { value: arity, configurable: true }); + nativeSet.add(fn); + return fn; +} + /** * 将一个 class 的构造函数 + 所有原型方法 全部标记为 native * @param {Function} cls @@ -47,4 +64,64 @@ function nativeClass(cls) { return cls; } -module.exports = { createNative, nativeClass, nativeSet }; +// ── SafeFunction: blocks constructor chain escape ─────────────── +// Prevents obj.constructor.constructor("return process")() from +// reaching the host Node.js realm +const SafeFunction = createNative('Function', function (...args) { + const body = args.length > 0 ? String(args[args.length - 1]) : ''; + // Block any attempt to access Node.js globals + const blocked = /\b(process|require|module|exports|Buffer|global|__dirname|__filename|child_process)\b/; + if (blocked.test(body)) { + throw new TypeError('Function constructor is not allowed in this context'); + } + // For benign cases, delegate to real Function but in restricted form + try { + return Function(...args); + } catch (e) { + throw new TypeError('Function constructor is not allowed in this context'); + } +}); +Object.defineProperty(SafeFunction, 'prototype', { + value: Function.prototype, writable: false, configurable: false, +}); +nativeSet.add(SafeFunction); + +// ── Safe eval wrapper ─────────────────────────────────────────── +const safeEval = createNativeMethod('eval', 1, (code) => { + const str = String(code); + const blocked = /\b(process|require|module|exports|Buffer|child_process)\b/; + if (blocked.test(str)) return undefined; + // Don't actually eval — hsw only probes for its existence + return undefined; +}); + +/** + * Patch constructor chain on all objects in a sandbox context. + * Rewrites .constructor.constructor to SafeFunction to prevent + * host realm escape via Function("return process")(). + * @param {object} ctx - The vm context sandbox object + */ +function patchConstructorChain(ctx) { + ctx.Function = SafeFunction; + ctx.eval = safeEval; + + // Kill host references + ctx.process = undefined; + ctx.require = undefined; + ctx.Buffer = undefined; + ctx.module = undefined; + ctx.exports = undefined; + ctx.global = undefined; + ctx.__dirname = undefined; + ctx.__filename = undefined; +} + +module.exports = { + createNative, + nativeMethod: createNativeMethod, + nativeClass, + nativeSet, + SafeFunction, + safeEval, + patchConstructorChain, +}; diff --git a/src/sandbox/mocks/navigator.js b/src/sandbox/mocks/navigator.js index 4ae61a2..c9b67d7 100644 --- a/src/sandbox/mocks/navigator.js +++ b/src/sandbox/mocks/navigator.js @@ -2,23 +2,87 @@ /** * P0/P1: Navigator mock * hsw 检测:webdriver / languages / maxTouchPoints / plugins / userAgentData + * Now with proper Navigator prototype chain + missing sub-properties */ -const { createNative } = require('./native'); +const { nativeMethod: M } = require('./native'); +const { + Navigator, NavigatorUAData, Permissions, PermissionStatus, + PluginArray, MimeTypeArray, NetworkInformation, +} = require('./class_registry'); -// PluginArray 结构 -const plugins = Object.assign(Object.create({ - item: createNative('item', function (i) { return this[i] || null; }), - namedItem: createNative('namedItem', function () { return null; }), - refresh: createNative('refresh', function () {}), -}), { +// ── PluginArray with proper prototype ─────────────────────────── +const plugins = Object.create(PluginArray.prototype); +Object.assign(plugins, { 0: { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 }, 1: { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 }, 2: { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 }, length: 3, + item: M('item', 1, function (i) { return this[i] || null; }), + namedItem: M('namedItem', 1, function () { return null; }), + refresh: M('refresh', 0, function () {}), }); -const navigatorMock = { +// ── MimeTypeArray with proper prototype ───────────────────────── +const mimeTypes = Object.create(MimeTypeArray.prototype); +Object.assign(mimeTypes, { + length: 2, + 0: { type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' }, + 1: { type: 'text/pdf', description: 'Portable Document Format', suffixes: 'pdf' }, + item: M('item', 1, function (i) { return this[i] || null; }), + namedItem: M('namedItem', 1, function () { return null; }), +}); + +// ── Permissions with proper prototype ─────────────────────────── +const permissions = Object.create(Permissions.prototype); +permissions.query = M('query', 1, (desc) => { + const status = Object.create(PermissionStatus.prototype); + status.state = desc.name === 'notifications' ? 'denied' : 'prompt'; + status.onchange = null; + return Promise.resolve(status); +}); + +// ── NetworkInformation (connection) with proper prototype ─────── +const connection = Object.create(NetworkInformation.prototype); +Object.assign(connection, { + effectiveType: '4g', + type: '4g', + downlink: 10, + rtt: 50, + saveData: false, + onchange: null, +}); + +// ── NavigatorUAData with proper prototype ──────────────────────── +const userAgentData = Object.create(NavigatorUAData.prototype); +Object.assign(userAgentData, { + brands: [ + { brand: 'Not:A-Brand', version: '99' }, + { brand: 'Google Chrome', version: '145' }, + { brand: 'Chromium', version: '145' }, + ], + mobile: false, + platform: 'Linux', + getHighEntropyValues: M('getHighEntropyValues', 1, (hints) => { + return Promise.resolve({ + architecture: 'x86', + bitness: '64', + model: '', + platform: 'Linux', + platformVersion: '6.1.0', + uaFullVersion: '145.0.0.0', + fullVersionList: [ + { brand: 'Not:A-Brand', version: '99.0.0.0' }, + { brand: 'Google Chrome', version: '145.0.0.0' }, + { brand: 'Chromium', version: '145.0.0.0' }, + ], + }); + }), +}); + +// ── Main navigator object with Navigator prototype ────────────── +const navigatorMock = Object.create(Navigator.prototype); +Object.assign(navigatorMock, { userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', appVersion: '5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', appName: 'Netscape', @@ -27,65 +91,76 @@ const navigatorMock = { product: 'Gecko', vendor: 'Google Inc.', language: 'en-US', - languages: ['en-US', 'en'], // P1: 必须是非空数组 - webdriver: false, // navigator.webdriver = false(window.webdriver = undefined) - maxTouchPoints: 0, // P1: 桌面为 0 + languages: Object.freeze(['en-US', 'en']), + webdriver: false, + maxTouchPoints: 0, hardwareConcurrency: 8, deviceMemory: 8, cookieEnabled: true, onLine: true, doNotTrack: null, + pdfViewerEnabled: true, plugins, - mimeTypes: { length: 0 }, + mimeTypes, + userAgentData, + connection, + permissions, - // P2: userAgentData (NavigatorUAData) - userAgentData: { - brands: [ - { brand: 'Not:A-Brand', version: '99' }, - { brand: 'Google Chrome', version: '145' }, - { brand: 'Chromium', version: '145' }, - ], - mobile: false, - platform: 'Linux', - getHighEntropyValues: createNative('getHighEntropyValues', function (hints) { - return Promise.resolve({ - architecture: 'x86', - bitness: '64', - model: '', - platform: 'Linux', - platformVersion: '6.1.0', - uaFullVersion: '145.0.0.0', - fullVersionList: [ - { brand: 'Not:A-Brand', version: '99.0.0.0' }, - { brand: 'Google Chrome', version: '145.0.0.0' }, - { brand: 'Chromium', version: '145.0.0.0' }, - ], - }); - }), + // P2: missing sub-properties from probe report + mediaDevices: { + enumerateDevices: M('enumerateDevices', 0, () => Promise.resolve([])), + getUserMedia: M('getUserMedia', 1, () => Promise.reject(new DOMException('NotAllowedError'))), + getDisplayMedia: M('getDisplayMedia', 0, () => Promise.reject(new DOMException('NotAllowedError'))), }, - // P2: connection (NetworkInformation) - connection: { - effectiveType: '4g', - downlink: 10, - rtt: 50, - saveData: false, + storage: { + estimate: M('estimate', 0, () => Promise.resolve({ quota: 2147483648, usage: 0 })), + persist: M('persist', 0, () => Promise.resolve(false)), + persisted: M('persisted', 0, () => Promise.resolve(false)), + getDirectory: M('getDirectory', 0, () => Promise.resolve({})), + }, + + webkitTemporaryStorage: { + queryUsageAndQuota: M('queryUsageAndQuota', 1, (cb) => { if (cb) cb(0, 2147483648); }), + }, + + keyboard: (() => { + const kb = { + getLayoutMap: M('getLayoutMap', 0, () => Promise.resolve(new Map())), + lock: M('lock', 0, () => Promise.resolve()), + unlock: M('unlock', 0, () => {}), + }; + Object.defineProperty(kb, Symbol.toStringTag, { + value: 'Keyboard', configurable: true, writable: false, enumerable: false, + }); + Object.defineProperty(kb, Symbol.toPrimitive, { + value: () => '[object Keyboard]', configurable: true, writable: false, enumerable: false, + }); + return kb; + })(), + + credentials: { + create: M('create', 1, () => Promise.resolve(null)), + get: M('get', 1, () => Promise.resolve(null)), + store: M('store', 1, () => Promise.resolve()), + preventSilentAccess: M('preventSilentAccess', 0, () => Promise.resolve()), }, geolocation: { - getCurrentPosition: createNative('getCurrentPosition', function (s, e) { e && e({ code: 1, message: 'denied' }); }), - watchPosition: createNative('watchPosition', function () { return 0; }), - clearWatch: createNative('clearWatch', function () {}), + getCurrentPosition: M('getCurrentPosition', 1, (s, e) => { if (e) e({ code: 1, message: 'denied' }); }), + watchPosition: M('watchPosition', 1, () => 0), + clearWatch: M('clearWatch', 1, () => {}), }, - permissions: { - query: createNative('query', function (desc) { - return Promise.resolve({ state: desc.name === 'notifications' ? 'denied' : 'prompt' }); - }), - }, + sendBeacon: M('sendBeacon', 1, () => true), + vibrate: M('vibrate', 1, () => false), +}); - sendBeacon: createNative('sendBeacon', function () { return true; }), - vibrate: createNative('vibrate', function () { return false; }), -}; +// oscpu: do NOT define as own property — Chrome doesn't have it at all. +// Defining it as undefined still creates a descriptor that hsw can detect. + +// languages Symbol.toStringTag — real Chrome: Object.prototype.toString.call(navigator.languages) = '[object Array]' +// but the probe checks navigator.languages.Symbol(Symbol.toStringTag) — which shouldn't exist on Array +// This is fine as-is since we froze the array. module.exports = navigatorMock; diff --git a/src/sandbox/mocks/performance.js b/src/sandbox/mocks/performance.js index 6bf1e8e..4d72465 100644 --- a/src/sandbox/mocks/performance.js +++ b/src/sandbox/mocks/performance.js @@ -2,9 +2,18 @@ /** * P0: Performance mock * hsw 检测:timing / timeOrigin / getEntriesByType('resource') / getEntriesByType('navigation') + * Now with: 5µs quantization, proper prototype chain (Performance -> EventTarget -> Object), + * PerformanceEntry / PerformanceResourceTiming / PerformanceNavigationTiming classes, + * getEntries() method */ -const { createNative } = require('./native'); +const { nativeMethod: M } = require('./native'); +const { + Performance, + PerformanceEntry, + PerformanceResourceTiming, + PerformanceNavigationTiming, +} = require('./class_registry'); const NAV_START = Date.now() - 1200; @@ -32,9 +41,23 @@ const timingData = { unloadEventEnd: 0, }; +// ── Helper: create PerformanceResourceTiming instance ──────────── +const makeResourceEntry = (data) => { + const entry = Object.create(PerformanceResourceTiming.prototype); + Object.assign(entry, data); + return entry; +}; + +// ── Helper: create PerformanceNavigationTiming instance ────────── +const makeNavEntry = (data) => { + const entry = Object.create(PerformanceNavigationTiming.prototype); + Object.assign(entry, data); + return entry; +}; + // 模拟 resource 条目(hsw 会查 checksiteconfig 请求痕迹) const resourceEntries = [ - { + makeResourceEntry({ name: 'https://api.hcaptcha.com/checksiteconfig?v=xxx&host=b.stripecdn.com&sitekey=xxx&sc=1&swa=1&spst=1', entryType: 'resource', initiatorType: 'xmlhttprequest', @@ -60,11 +83,11 @@ const resourceEntries = [ requestStart: 0, responseStart: 0, firstInterimResponseStart: 0, - finalResponseHeadersStart: 0, // P2 要求的字段 + finalResponseHeadersStart: 0, serverTiming: [], renderBlockingStatus: 'non-blocking', - }, - { + }), + makeResourceEntry({ name: 'https://newassets.hcaptcha.com/c/xxx/hsw.js', entryType: 'resource', initiatorType: 'script', @@ -93,11 +116,11 @@ const resourceEntries = [ redirectEnd: 0, serverTiming: [], renderBlockingStatus: 'non-blocking', - }, + }), ]; // 模拟 navigation 条目 -const navigationEntry = { +const navigationEntry = makeNavEntry({ name: 'https://newassets.hcaptcha.com/captcha/v1/xxx/static/hcaptcha.html', entryType: 'navigation', initiatorType: 'navigation', @@ -140,31 +163,47 @@ const navigationEntry = { workerStart: 0, contentEncoding: 'br', renderBlockingStatus: 'non-blocking', -}; +}); -const performanceMock = { +// ── Build performance object with proper prototype ────────────── +const performanceMock = Object.create(Performance.prototype); + +Object.assign(performanceMock, { timeOrigin: NAV_START, timing: timingData, navigation: { type: 0, redirectCount: 0 }, - getEntriesByType: createNative('getEntriesByType', function (type) { + // 5µs quantization (Chromium feature) + now: M('now', 0, () => { + const raw = Date.now() - NAV_START; + return Math.round(raw * 200) / 200; // 0.005ms = 5µs steps + }), + + getEntries: M('getEntries', 0, () => { + return [navigationEntry, ...resourceEntries]; + }), + + getEntriesByType: M('getEntriesByType', 1, (type) => { if (type === 'resource') return resourceEntries; if (type === 'navigation') return [navigationEntry]; + if (type === 'paint') return []; + if (type === 'mark') return []; + if (type === 'measure') return []; return []; }), - getEntriesByName: createNative('getEntriesByName', function (name) { - return resourceEntries.filter(e => e.name === name); + getEntriesByName: M('getEntriesByName', 1, (name) => { + return [...resourceEntries, navigationEntry].filter(e => e.name === name); }), - now: createNative('now', function () { - return Date.now() - NAV_START; - }), - - mark: createNative('mark', function () {}), - measure: createNative('measure', function () {}), - clearMarks: createNative('clearMarks', function () {}), - clearMeasures: createNative('clearMeasures', function () {}), -}; + mark: M('mark', 1, () => {}), + measure: M('measure', 1, () => {}), + clearMarks: M('clearMarks', 0, () => {}), + clearMeasures: M('clearMeasures', 0, () => {}), + clearResourceTimings: M('clearResourceTimings', 0, () => {}), + setResourceTimingBufferSize: M('setResourceTimingBufferSize', 1, () => {}), + addEventListener: M('addEventListener', 2, () => {}), + removeEventListener: M('removeEventListener', 2, () => {}), +}); module.exports = performanceMock; diff --git a/src/sandbox/mocks/screen.js b/src/sandbox/mocks/screen.js index 837c4da..8533264 100644 --- a/src/sandbox/mocks/screen.js +++ b/src/sandbox/mocks/screen.js @@ -2,9 +2,19 @@ /** * P1: Screen mock * hsw 检测:screen.width / height / colorDepth / pixelDepth / availWidth / availHeight + * Now with proper Screen prototype chain + ScreenOrientation */ -const screenMock = { +const { Screen, ScreenOrientation } = require('./class_registry'); + +const orientation = Object.create(ScreenOrientation.prototype); +Object.assign(orientation, { + type: 'landscape-primary', + angle: 0, +}); + +const screenMock = Object.create(Screen.prototype); +Object.assign(screenMock, { width: 1920, height: 1080, availWidth: 1920, @@ -13,10 +23,8 @@ const screenMock = { availTop: 0, colorDepth: 24, pixelDepth: 24, - orientation: { - type: 'landscape-primary', - angle: 0, - }, -}; + isExtended: false, + orientation, +}); module.exports = screenMock; diff --git a/src/sandbox/mocks/webapi.js b/src/sandbox/mocks/webapi.js index e8eb078..61719b2 100644 --- a/src/sandbox/mocks/webapi.js +++ b/src/sandbox/mocks/webapi.js @@ -2,12 +2,16 @@ /** * P0: RTCPeerConnection mock * P0: OfflineAudioContext mock + * P2: Blob / Worker mock (stack depth detection) * hsw 检测:构造函数存在性 + 原型链 + toString() 不暴露源码 */ -const { createNative, nativeClass } = require('./native'); +const { nativeMethod: M, nativeClass } = require('./native'); +const { + RTCRtpSender, RTCRtpReceiver, RTCSessionDescription, +} = require('./class_registry'); -// ── RTCPeerConnection ──────────────────────────────────────── +// ── RTCPeerConnection ────────────────────────────────────────── class RTCPeerConnection { constructor(config) { this.localDescription = null; @@ -17,26 +21,40 @@ class RTCPeerConnection { this.iceGatheringState = 'new'; this.connectionState = 'new'; this._config = config || {}; + this.onicecandidate = null; + this.onicegatheringstatechange = null; + this.onconnectionstatechange = null; + this.oniceconnectionstatechange = null; + this.onsignalingstatechange = null; + this.ondatachannel = null; + this.ontrack = null; } } -RTCPeerConnection.prototype.createOffer = createNative('createOffer', function (options) { - return Promise.resolve({ type: 'offer', sdp: 'v=0\r\n' }); -}); -RTCPeerConnection.prototype.createAnswer = createNative('createAnswer', function () { - return Promise.resolve({ type: 'answer', sdp: 'v=0\r\n' }); -}); -RTCPeerConnection.prototype.setLocalDescription = createNative('setLocalDescription', function () { return Promise.resolve(); }); -RTCPeerConnection.prototype.setRemoteDescription = createNative('setRemoteDescription', function () { return Promise.resolve(); }); -RTCPeerConnection.prototype.addIceCandidate = createNative('addIceCandidate', function () { return Promise.resolve(); }); -RTCPeerConnection.prototype.createDataChannel = createNative('createDataChannel', function (label) { - return { label, readyState: 'open', close: createNative('close', function(){}) }; -}); -RTCPeerConnection.prototype.close = createNative('close', function () {}); -RTCPeerConnection.prototype.addEventListener = createNative('addEventListener', function () {}); -RTCPeerConnection.prototype.removeEventListener = createNative('removeEventListener', function () {}); +RTCPeerConnection.prototype.createOffer = M('createOffer', 0, (options) => + Promise.resolve({ type: 'offer', sdp: 'v=0\r\n' }) +); +RTCPeerConnection.prototype.createAnswer = M('createAnswer', 0, () => + Promise.resolve({ type: 'answer', sdp: 'v=0\r\n' }) +); +RTCPeerConnection.prototype.setLocalDescription = M('setLocalDescription', 0, () => Promise.resolve()); +RTCPeerConnection.prototype.setRemoteDescription = M('setRemoteDescription', 1, () => Promise.resolve()); +RTCPeerConnection.prototype.addIceCandidate = M('addIceCandidate', 0, () => Promise.resolve()); +RTCPeerConnection.prototype.createDataChannel = M('createDataChannel', 1, (label) => ({ + label, readyState: 'open', close: M('close', 0, () => {}), + send: M('send', 1, () => {}), onmessage: null, onopen: null, onclose: null, +})); +RTCPeerConnection.prototype.close = M('close', 0, () => {}); +RTCPeerConnection.prototype.getSenders = M('getSenders', 0, () => []); +RTCPeerConnection.prototype.getReceivers = M('getReceivers', 0, () => []); +RTCPeerConnection.prototype.getStats = M('getStats', 0, () => Promise.resolve(new Map())); +RTCPeerConnection.prototype.addTrack = M('addTrack', 1, () => Object.create(RTCRtpSender.prototype)); +RTCPeerConnection.prototype.removeTrack = M('removeTrack', 1, () => {}); +RTCPeerConnection.prototype.getConfiguration = M('getConfiguration', 0, function () { return this._config; }); +RTCPeerConnection.prototype.addEventListener = M('addEventListener', 2, () => {}); +RTCPeerConnection.prototype.removeEventListener = M('removeEventListener', 2, () => {}); nativeClass(RTCPeerConnection); -// ── OfflineAudioContext ────────────────────────────────────── +// ── OfflineAudioContext ──────────────────────────────────────── class OfflineAudioContext { constructor(channels, length, sampleRate) { this.length = length || 4096; @@ -46,40 +64,141 @@ class OfflineAudioContext { this.destination = { channelCount: channels || 1 }; } } -OfflineAudioContext.prototype.createAnalyser = createNative('createAnalyser', function () { +OfflineAudioContext.prototype.createAnalyser = M('createAnalyser', 0, function () { return { fftSize: 2048, frequencyBinCount: 1024, - connect: createNative('connect', function () {}), - getFloatFrequencyData: createNative('getFloatFrequencyData', function (arr) { + connect: M('connect', 1, () => {}), + disconnect: M('disconnect', 0, () => {}), + getFloatFrequencyData: M('getFloatFrequencyData', 1, (arr) => { for (let i = 0; i < arr.length; i++) arr[i] = -100 + Math.random() * 5; }), + getByteFrequencyData: M('getByteFrequencyData', 1, (arr) => { + for (let i = 0; i < arr.length; i++) arr[i] = 0; + }), }; }); -OfflineAudioContext.prototype.createOscillator = createNative('createOscillator', function () { +OfflineAudioContext.prototype.createOscillator = M('createOscillator', 0, function () { return { type: 'triangle', frequency: { value: 10000 }, - connect: createNative('connect', function () {}), - start: createNative('start', function () {}), + connect: M('connect', 1, () => {}), + disconnect: M('disconnect', 0, () => {}), + start: M('start', 0, () => {}), + stop: M('stop', 0, () => {}), }; }); -OfflineAudioContext.prototype.createDynamicsCompressor = createNative('createDynamicsCompressor', function () { +OfflineAudioContext.prototype.createDynamicsCompressor = M('createDynamicsCompressor', 0, function () { return { threshold: { value: -50 }, knee: { value: 40 }, ratio: { value: 12 }, attack: { value: 0 }, release: { value: 0.25 }, - connect: createNative('connect', function () {}), + connect: M('connect', 1, () => {}), + disconnect: M('disconnect', 0, () => {}), }; }); -OfflineAudioContext.prototype.startRendering = createNative('startRendering', function () { +OfflineAudioContext.prototype.createGain = M('createGain', 0, function () { + return { + gain: { value: 1 }, + connect: M('connect', 1, () => {}), + disconnect: M('disconnect', 0, () => {}), + }; +}); +OfflineAudioContext.prototype.createBiquadFilter = M('createBiquadFilter', 0, function () { + return { + type: 'lowpass', + frequency: { value: 350 }, + Q: { value: 1 }, + gain: { value: 0 }, + connect: M('connect', 1, () => {}), + disconnect: M('disconnect', 0, () => {}), + }; +}); +OfflineAudioContext.prototype.startRendering = M('startRendering', 0, function () { const len = this.length; - // 固定指纹数据,保持每次一致(稳定指纹) const data = new Float32Array(len); for (let i = 0; i < len; i++) data[i] = Math.sin(i * 0.001) * 0.01; - return Promise.resolve({ getChannelData: () => data }); + return Promise.resolve({ getChannelData: M('getChannelData', 1, () => data), length: len, sampleRate: this.sampleRate, numberOfChannels: 1, duration: len / this.sampleRate }); }); -OfflineAudioContext.prototype.addEventListener = createNative('addEventListener', function () {}); -OfflineAudioContext.prototype.removeEventListener = createNative('removeEventListener', function () {}); +OfflineAudioContext.prototype.addEventListener = M('addEventListener', 2, () => {}); +OfflineAudioContext.prototype.removeEventListener = M('removeEventListener', 2, () => {}); nativeClass(OfflineAudioContext); -module.exports = { RTCPeerConnection, OfflineAudioContext }; +// ── Blob ──────────────────────────────────────────────────────── +class Blob { + constructor(parts, options) { + this._parts = parts || []; + this.type = (options && options.type) || ''; + this.size = this._parts.reduce((s, p) => { + if (typeof p === 'string') return s + p.length; + if (p && p.byteLength !== undefined) return s + p.byteLength; + return s + String(p).length; + }, 0); + } + slice(start, end, type) { + return new Blob([], { type: type || this.type }); + } + text() { + return Promise.resolve(this._parts.join('')); + } + arrayBuffer() { + return Promise.resolve(new ArrayBuffer(this.size)); + } +} +nativeClass(Blob); + +// ── Worker (stack depth detection) ────────────────────────────── +// hsw creates Blob Workers to run stack depth tests. +// Chrome typical stack depth: ~10000-13000 +// Node.js default: ~15000+ +// Must return Chrome-range values. +class Worker { + constructor(url) { + this.onmessage = null; + this.onerror = null; + this._terminated = false; + this._url = url; + } + postMessage(data) { + if (this._terminated) return; + // Simulate async worker response with Chrome-range stack depth + const self = this; + setTimeout(() => { + if (self._terminated) return; + if (self.onmessage) { + // Stack depth test: Chrome returns ~10000-13000 + const stackDepth = 11847 + Math.floor(Math.random() * 500); + self.onmessage({ data: { stackDepth, result: stackDepth } }); + } + }, 5); + } + terminate() { this._terminated = true; } + addEventListener(type, fn) { + if (type === 'message') this.onmessage = fn; + if (type === 'error') this.onerror = fn; + } + removeEventListener() {} +} +nativeClass(Worker); + +// URL.createObjectURL / URL.revokeObjectURL for Blob Workers +const blobURLStore = new Map(); +const createObjectURL = M('createObjectURL', 1, (blob) => { + const id = `blob:https://newassets.hcaptcha.com/${Math.random().toString(36).slice(2)}`; + blobURLStore.set(id, blob); + return id; +}); +const revokeObjectURL = M('revokeObjectURL', 1, (url) => { + blobURLStore.delete(url); +}); + +module.exports = { + RTCPeerConnection, + OfflineAudioContext, + RTCRtpSender, + RTCRtpReceiver, + RTCSessionDescription, + Blob, + Worker, + createObjectURL, + revokeObjectURL, +}; diff --git a/src/sandbox/mocks/window.js b/src/sandbox/mocks/window.js index c0623e3..5c65700 100644 --- a/src/sandbox/mocks/window.js +++ b/src/sandbox/mocks/window.js @@ -2,20 +2,31 @@ /** * 总装:window 沙盒 * 按 P0→P1→P2 顺序挂载所有 mock,并用 Proxy 屏蔽 bot 字段 + * Now with: proper Window prototype chain, getOwnPropertyDescriptor trap, + * Chrome-accurate ownKeys list, missing globals (chrome, matchMedia, etc), + * SafeFunction/safeEval for constructor chain escape defense */ -const { createNative, nativeClass } = require('./native'); +const { createNative, nativeMethod: M, nativeClass, SafeFunction, safeEval } = require('./native'); const { isBotKey } = require('./bot_shield'); const performanceMock = require('./performance'); const navigatorMock = require('./navigator'); -const { RTCPeerConnection, OfflineAudioContext } = require('./webapi'); +const { + RTCPeerConnection, OfflineAudioContext, + RTCRtpSender, RTCRtpReceiver, RTCSessionDescription, + Blob, Worker, createObjectURL, revokeObjectURL, +} = require('./webapi'); const { HTMLCanvasElement, CanvasRenderingContext2D } = require('./canvas'); const { cryptoMock, Storage, IDBFactory, Notification, atob, btoa } = require('./crypto'); const screenMock = require('./screen'); const HTMLDocument = require('./document'); +const chromeMath = require('./math'); +const CR = require('./class_registry'); // ── 基础 window 对象 ───────────────────────────────────────── -const _win = { +const _win = Object.create(CR.Window.prototype); + +Object.assign(_win, { // ── P0: 核心 API ────────────────────────────────────── performance: performanceMock, @@ -26,6 +37,9 @@ const _win = { RTCPeerConnection, webkitRTCPeerConnection: RTCPeerConnection, OfflineAudioContext, + RTCRtpSender, + RTCRtpReceiver, + RTCSessionDescription, // ── P1: Canvas ──────────────────────────────────────── HTMLCanvasElement, @@ -48,14 +62,18 @@ const _win = { document: new HTMLDocument(), HTMLDocument, + // ── P1: Blob / Worker ───────────────────────────────── + Blob, + Worker, + // ── P2: 移动端触摸 → 桌面不存在 ────────────────────── // ontouchstart: 不定义,Proxy 返回 undefined - // ── 基础 JS 全局 ───────────────────────────────────── + // ── 基础 JS 全局 (use SafeFunction to block escape) ── Promise, Object, Array, - Function, + Function: SafeFunction, Number, String, Boolean, @@ -63,7 +81,13 @@ const _win = { Date, RegExp, Error, - Math, + TypeError, + RangeError, + SyntaxError, + ReferenceError, + URIError, + EvalError, + Math: chromeMath, JSON, parseInt, parseFloat, @@ -75,7 +99,7 @@ const _win = { encodeURIComponent, escape, unescape, - eval, + eval: safeEval, undefined, Infinity, NaN, @@ -100,6 +124,9 @@ const _win = { search: '', hash: '', ancestorOrigins: { 0: 'https://b.stripecdn.com', 1: 'https://js.stripe.com', length: 2 }, + assign: M('assign', 1, () => {}), + replace: M('replace', 1, () => {}), + reload: M('reload', 0, () => {}), }, innerWidth: 530, @@ -133,20 +160,21 @@ const _win = { length: 1, state: null, scrollRestoration: 'auto', - go: createNative('go', function () {}), - back: createNative('back', function () {}), - forward: createNative('forward', function () {}), - pushState: createNative('pushState', function () {}), - replaceState: createNative('replaceState', function () {}), + go: M('go', 0, () => {}), + back: M('back', 0, () => {}), + forward: M('forward', 0, () => {}), + pushState: M('pushState', 2, () => {}), + replaceState: M('replaceState', 2, () => {}), }, - fetch: createNative('fetch', function (url, opts) { - // 沙盒里一般不真正发请求,返回 resolved 空 response + fetch: M('fetch', 1, (url, opts) => { return Promise.resolve({ ok: true, status: 200, - json: () => Promise.resolve({}), - text: () => Promise.resolve(''), - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), + json: M('json', 0, () => Promise.resolve({})), + text: M('text', 0, () => Promise.resolve('')), + arrayBuffer: M('arrayBuffer', 0, () => Promise.resolve(new ArrayBuffer(0))), + blob: M('blob', 0, () => Promise.resolve(new Blob([]))), + headers: new Map(), }); }), @@ -154,31 +182,49 @@ const _win = { Response: createNative('Response', function (body, opts) { this.status = opts?.status || 200; }), Headers: createNative('Headers', function () { this._h = {}; }), - URL: createNative('URL', function (url, base) { - const u = new (require('url').URL)(url, base); - Object.assign(this, u); - }), + URL: (() => { + const _URL = createNative('URL', function (url, base) { + const u = new (require('url').URL)(url, base); + Object.assign(this, { href: u.href, origin: u.origin, protocol: u.protocol, + host: u.host, hostname: u.hostname, port: u.port, pathname: u.pathname, + search: u.search, hash: u.hash, searchParams: u.searchParams }); + }); + _URL.createObjectURL = createObjectURL; + _URL.revokeObjectURL = revokeObjectURL; + return _URL; + })(), URLSearchParams, - addEventListener: createNative('addEventListener', function () {}), - removeEventListener: createNative('removeEventListener', function () {}), - dispatchEvent: createNative('dispatchEvent', function () { return true; }), - postMessage: createNative('postMessage', function () {}), + addEventListener: M('addEventListener', 2, () => {}), + removeEventListener: M('removeEventListener', 2, () => {}), + dispatchEvent: M('dispatchEvent', 1, () => true), + postMessage: M('postMessage', 1, () => {}), - alert: createNative('alert', function () {}), - confirm: createNative('confirm', function () { return false; }), - prompt: createNative('prompt', function () { return null; }), + alert: M('alert', 0, () => {}), + confirm: M('confirm', 1, () => false), + prompt: M('prompt', 0, () => null), + close: M('close', 0, () => {}), + stop: M('stop', 0, () => {}), + focus: M('focus', 0, () => {}), + blur: M('blur', 0, () => {}), + print: M('print', 0, () => {}), + open: M('open', 0, () => null), - requestAnimationFrame: createNative('requestAnimationFrame', function (cb) { return setTimeout(cb, 16); }), - cancelAnimationFrame: createNative('cancelAnimationFrame', function (id) { clearTimeout(id); }), - requestIdleCallback: createNative('requestIdleCallback', function (cb) { return setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 1); }), - cancelIdleCallback: createNative('cancelIdleCallback', function (id) { clearTimeout(id); }), + requestAnimationFrame: M('requestAnimationFrame', 1, (cb) => setTimeout(cb, 16)), + cancelAnimationFrame: M('cancelAnimationFrame', 1, (id) => clearTimeout(id)), + requestIdleCallback: M('requestIdleCallback', 1, (cb) => setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 1)), + cancelIdleCallback: M('cancelIdleCallback', 1, (id) => clearTimeout(id)), - getComputedStyle: createNative('getComputedStyle', function () { + getComputedStyle: M('getComputedStyle', 1, () => { return new Proxy({}, { get: (_, p) => p === 'getPropertyValue' ? (() => '') : '' }); }), - structuredClone: createNative('structuredClone', (v) => JSON.parse(JSON.stringify(v))), + getSelection: M('getSelection', 0, () => ({ + rangeCount: 0, toString: () => '', removeAllRanges: () => {}, + addRange: () => {}, getRangeAt: () => ({}), + })), + + structuredClone: M('structuredClone', 1, (v) => JSON.parse(JSON.stringify(v))), TextEncoder, TextDecoder, @@ -192,22 +238,158 @@ const _win = { Float32Array, Float64Array, ArrayBuffer, + SharedArrayBuffer, DataView, Map, Set, WeakMap, WeakSet, + WeakRef, Proxy, Reflect, BigInt, + BigInt64Array, + BigUint64Array, Symbol, WebAssembly, -}; + Atomics, + AggregateError, + + // ── Missing globals from probe report (P1) ──────────── + chrome: { + runtime: { + connect: M('connect', 0, () => ({})), + sendMessage: M('sendMessage', 1, () => {}), + onMessage: { addListener: M('addListener', 1, () => {}), removeListener: M('removeListener', 1, () => {}) }, + id: undefined, + }, + loadTimes: M('loadTimes', 0, () => ({})), + csi: M('csi', 0, () => ({})), + }, + + clientInformation: navigatorMock, + + matchMedia: M('matchMedia', 1, (query) => ({ + matches: false, + media: query || '', + onchange: null, + addListener: M('addListener', 1, () => {}), + removeListener: M('removeListener', 1, () => {}), + addEventListener: M('addEventListener', 2, () => {}), + removeEventListener: M('removeEventListener', 2, () => {}), + dispatchEvent: M('dispatchEvent', 1, () => false), + })), + + visualViewport: new CR.VisualViewport(), + + Intl, + FinalizationRegistry, + + // ── Class constructors from registry ────────────────── + EventTarget: CR.EventTarget, + Window: CR.Window, + Navigator: CR.Navigator, + Performance: CR.Performance, + PerformanceEntry: CR.PerformanceEntry, + PerformanceResourceTiming: CR.PerformanceResourceTiming, + PerformanceNavigationTiming: CR.PerformanceNavigationTiming, + Crypto: CR.Crypto, + SubtleCrypto: CR.SubtleCrypto, + Screen: CR.Screen, + Node: CR.Node, + Element: CR.Element, + HTMLElement: CR.HTMLElement, + Document: CR.Document, + HTMLIFrameElement: CR.HTMLIFrameElement, + SVGElement: CR.SVGElement, + SVGTextContentElement: CR.SVGTextContentElement, + FontFace: CR.FontFace, + CSS: CR.CSS, + WebGLRenderingContext: CR.WebGLRenderingContext, + WebGL2RenderingContext: CR.WebGL2RenderingContext, + AudioContext: CR.AudioContext, + AnalyserNode: CR.AnalyserNode, + AudioBuffer: CR.AudioBuffer, + Audio: CR.Audio, + Permissions: CR.Permissions, + PermissionStatus: CR.PermissionStatus, + PluginArray: CR.PluginArray, + MimeTypeArray: CR.MimeTypeArray, + NetworkInformation: CR.NetworkInformation, + NavigatorUAData: CR.NavigatorUAData, + VisualViewport: CR.VisualViewport, + DOMException, + + // ── DOMRect / DOMMatrix stubs ──────────────────────── + DOMRect: createNative('DOMRect', function (x, y, w, h) { + this.x = x || 0; this.y = y || 0; this.width = w || 0; this.height = h || 0; + this.top = this.y; this.left = this.x; this.bottom = this.y + this.height; this.right = this.x + this.width; + }), + DOMMatrix: createNative('DOMMatrix', function () { + this.a = 1; this.b = 0; this.c = 0; this.d = 1; this.e = 0; this.f = 0; + }), + + // ── MessageChannel / MessagePort ───────────────────── + MessageChannel: createNative('MessageChannel', function () { + this.port1 = { postMessage: M('postMessage', 1, () => {}), onmessage: null, close: M('close', 0, () => {}) }; + this.port2 = { postMessage: M('postMessage', 1, () => {}), onmessage: null, close: M('close', 0, () => {}) }; + }), + + // ── BroadcastChannel ───────────────────────────────── + BroadcastChannel: createNative('BroadcastChannel', function (name) { + this.name = name; this.onmessage = null; + this.postMessage = M('postMessage', 1, () => {}); + this.close = M('close', 0, () => {}); + }), + + // ── MutationObserver / IntersectionObserver / ResizeObserver ─ + MutationObserver: createNative('MutationObserver', function (cb) { + this.observe = M('observe', 1, () => {}); this.disconnect = M('disconnect', 0, () => {}); + this.takeRecords = M('takeRecords', 0, () => []); + }), + IntersectionObserver: createNative('IntersectionObserver', function (cb) { + this.observe = M('observe', 1, () => {}); this.unobserve = M('unobserve', 1, () => {}); + this.disconnect = M('disconnect', 0, () => {}); + }), + ResizeObserver: createNative('ResizeObserver', function (cb) { + this.observe = M('observe', 1, () => {}); this.unobserve = M('unobserve', 1, () => {}); + this.disconnect = M('disconnect', 0, () => {}); + }), + + // ── XMLHttpRequest (stub) ──────────────────────────── + XMLHttpRequest: createNative('XMLHttpRequest', function () { + this.readyState = 0; this.status = 0; this.responseText = ''; this.response = null; + this.onreadystatechange = null; this.onload = null; this.onerror = null; + }), + + // ── Image / HTMLImageElement ────────────────────────── + Image: createNative('Image', function (w, h) { + this.width = w || 0; this.height = h || 0; this.src = ''; + this.onload = null; this.onerror = null; this.complete = false; + }), + + // ── AbortController ────────────────────────────────── + AbortController, + AbortSignal, +}); + +// ── Iterator (newer JS global, may not exist in all Node versions) ── +if (typeof globalThis.Iterator !== 'undefined') { + _win.Iterator = globalThis.Iterator; +} +// ── SuppressedError (ES2024) ──────────────────────────────────── +if (typeof globalThis.SuppressedError !== 'undefined') { + _win.SuppressedError = globalThis.SuppressedError; +} + +// Node.js globals are filtered by bot_shield (NODE_KEYS). +// Do NOT define them as own properties — even undefined values +// create descriptors that getOwnPropertyDescriptor can detect. // ── 建 Proxy:屏蔽 bot 字段 + 回填自引用 ──────────────────── const windowProxy = new Proxy(_win, { get(target, prop) { - if (isBotKey(prop)) return undefined; // 🚨 bot 字段全部返回 undefined + if (isBotKey(prop)) return undefined; const val = target[prop]; if (val === null && ['self','window','frames','parent','top','globalThis'].includes(prop)) { return windowProxy; @@ -215,17 +397,30 @@ const windowProxy = new Proxy(_win, { return val; }, has(target, prop) { - if (isBotKey(prop)) return false; // 拦截 'webdriver' in window + if (isBotKey(prop)) return false; return prop in target; }, set(target, prop, val) { - if (isBotKey(prop)) return true; // 静默丢弃 bot 字段的写入 + if (isBotKey(prop)) return true; target[prop] = val; return true; }, + getOwnPropertyDescriptor(target, prop) { + if (isBotKey(prop)) return undefined; + if (prop in target) { + const desc = Object.getOwnPropertyDescriptor(target, prop); + if (desc) return desc; + // For inherited properties, make them appear as own (Chrome behavior) + return { value: target[prop], writable: true, enumerable: true, configurable: true }; + } + return undefined; + }, ownKeys(target) { return Reflect.ownKeys(target).filter(k => !isBotKey(k)); }, + getPrototypeOf() { + return CR.Window.prototype; + }, }); // 回填自引用 @@ -236,4 +431,7 @@ _win.frames = windowProxy; _win.parent = windowProxy; _win.top = windowProxy; +// global 别名 (some code checks for `global`) +_win.global = windowProxy; + module.exports = windowProxy;