done all
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
src/probe/reports
|
||||||
|
asset
|
||||||
|
docs
|
||||||
|
.claude
|
||||||
|
.venv
|
||||||
|
CLAUDE.md
|
||||||
|
body.bin
|
||||||
|
node_modules
|
||||||
213
analyze_new.py
213
analyze_new.py
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
608
node_modules/.package-lock.json
generated
vendored
608
node_modules/.package-lock.json
generated
vendored
@@ -4,6 +4,12 @@
|
|||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"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": {
|
"node_modules/@msgpack/msgpack": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
|
||||||
@@ -13,12 +19,399 @@
|
|||||||
"node": ">= 18"
|
"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": {
|
"node_modules/event-lite": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
|
||||||
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
|
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -45,12 +438,73 @@
|
|||||||
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
|
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/msgpack-lite": {
|
||||||
"version": "0.1.26",
|
"version": "0.1.26",
|
||||||
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
|
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
|
||||||
@@ -65,6 +519,160 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"msgpack": "bin/msgpack"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
609
package-lock.json
generated
609
package-lock.json
generated
@@ -9,9 +9,16 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@msgpack/msgpack": "^3.0.0",
|
"@msgpack/msgpack": "^3.0.0",
|
||||||
|
"got-scraping": "^4.2.0",
|
||||||
"msgpack-lite": "^0.1.26"
|
"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": {
|
"node_modules/@msgpack/msgpack": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
|
||||||
@@ -21,12 +28,399 @@
|
|||||||
"node": ">= 18"
|
"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": {
|
"node_modules/event-lite": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
|
||||||
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
|
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -53,12 +447,73 @@
|
|||||||
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
|
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/msgpack-lite": {
|
||||||
"version": "0.1.26",
|
"version": "0.1.26",
|
||||||
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
|
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
|
||||||
@@ -73,6 +528,160 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"msgpack": "bin/msgpack"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@msgpack/msgpack": "^3.0.0",
|
"@msgpack/msgpack": "^3.0.0",
|
||||||
|
"got-scraping": "^4.2.0",
|
||||||
"msgpack-lite": "^0.1.26"
|
"msgpack-lite": "^0.1.26"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
163
src/core/http_client.js
Normal file
163
src/core/http_client.js
Normal file
@@ -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<string,Map<string,string>>} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,15 +8,17 @@
|
|||||||
* 使用 hsw.js 在 Node 沙盒中运行(全局污染方式)
|
* 使用 hsw.js 在 Node 沙盒中运行(全局污染方式)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const vm = require('vm');
|
||||||
const { readFileSync } = require('fs');
|
const { readFileSync } = require('fs');
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const msgpack = require('msgpack-lite');
|
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 } = require('./utils/logger');
|
||||||
|
|
||||||
const logger = new Logger('hcaptcha_solver');
|
const logger = new Logger('hcaptcha_solver');
|
||||||
|
|
||||||
// 保存原始 fetch(在全局被 mock 污染之前)
|
// 保存原始 fetch(供真实网络请求使用)
|
||||||
const realFetch = globalThis.fetch;
|
const realFetch = globalThis.fetch;
|
||||||
|
|
||||||
// ── 常量 ──────────────────────────────────────────────────────
|
// ── 常量 ──────────────────────────────────────────────────────
|
||||||
@@ -82,76 +84,86 @@ class HswBridge {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.hswFn = null;
|
this.hswFn = null;
|
||||||
this.initialized = false;
|
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 {string} hswPath - hsw.js 文件路径
|
||||||
* @param {object} fingerprint - 指纹覆盖
|
* @param {object} fingerprint - 指纹覆盖
|
||||||
*/
|
*/
|
||||||
async init(hswPath, fingerprint = {}) {
|
async init(hswPath, fingerprint = {}) {
|
||||||
if (this.initialized) return;
|
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');
|
const code = readFileSync(hswPath, 'utf-8');
|
||||||
logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`);
|
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 {
|
try {
|
||||||
const fn = new Function(`(function() { ${code} })();`);
|
script.runInContext(ctx, { timeout: 10000 });
|
||||||
fn();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`hsw.js 执行失败: ${err.message}`);
|
logger.error(`hsw.js 执行失败: ${err.message}`);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找 hsw 函数
|
// 查找 hsw 函数
|
||||||
if (typeof globalThis.window?.hsw === 'function') {
|
if (typeof ctx.hsw === 'function') {
|
||||||
this.hswFn = globalThis.window.hsw;
|
this.hswFn = ctx.hsw;
|
||||||
} else if (typeof globalThis.hsw === 'function') {
|
} else if (typeof ctx.window?.hsw === 'function') {
|
||||||
this.hswFn = globalThis.hsw;
|
this.hswFn = ctx.window.hsw;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.hswFn) {
|
if (!this.hswFn) {
|
||||||
@@ -159,7 +171,7 @@ class HswBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
logger.success('Bridge 已就绪');
|
logger.success('Bridge 已就绪 (vm 沙盒)');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 计算 PoW n 值: hsw(req_jwt_string) */
|
/** 计算 PoW n 值: hsw(req_jwt_string) */
|
||||||
|
|||||||
273
src/probe/deep_proxy.js
Normal file
273
src/probe/deep_proxy.js
Normal file
@@ -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<path, entry>, 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 };
|
||||||
241
src/probe/probe_env.js
Normal file
241
src/probe/probe_env.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
229
src/probe/report.js
Normal file
229
src/probe/report.js
Normal file
@@ -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 };
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* HSW Runner
|
* HSW Runner
|
||||||
* 用 vm 沙盒加载 hsw.js,注入 mock window,调用 window.hsw(req, callback)
|
* 用 vm 沙盒加载 hsw.js,注入 mock window,调用 window.hsw(req, callback)
|
||||||
|
* Now with: constructor chain escape defense, error stack rewriting
|
||||||
*
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* const { solveHsw } = require('./hsw_runner');
|
* const { solveHsw } = require('./hsw_runner');
|
||||||
@@ -16,6 +17,7 @@ const HSW_PATH = path.resolve(__dirname, '../../asset/hsw.js');
|
|||||||
|
|
||||||
// ── 加载 window mock ─────────────────────────────────────────
|
// ── 加载 window mock ─────────────────────────────────────────
|
||||||
const windowMock = require('./mocks/window');
|
const windowMock = require('./mocks/window');
|
||||||
|
const { applySandboxPatches } = require('./mocks/index');
|
||||||
|
|
||||||
// ── 读取 hsw.js 源码(只读一次) ─────────────────────────────
|
// ── 读取 hsw.js 源码(只读一次) ─────────────────────────────
|
||||||
const hswCode = fs.readFileSync(HSW_PATH, 'utf-8');
|
const hswCode = fs.readFileSync(HSW_PATH, 'utf-8');
|
||||||
@@ -47,7 +49,12 @@ function buildContext() {
|
|||||||
ctx.clearInterval = clearInterval;
|
ctx.clearInterval = clearInterval;
|
||||||
ctx.queueMicrotask = queueMicrotask;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 编译脚本(只编译一次,复用) ─────────────────────────────
|
// ── 编译脚本(只编译一次,复用) ─────────────────────────────
|
||||||
|
|||||||
@@ -30,10 +30,18 @@ const BOT_KEYS = new Set([
|
|||||||
'CDCJStestRunStatus',
|
'CDCJStestRunStatus',
|
||||||
'$cdc_asdjflasutopfhvcZLmcfl_',
|
'$cdc_asdjflasutopfhvcZLmcfl_',
|
||||||
'$chrome_asyncScriptInfo',
|
'$chrome_asyncScriptInfo',
|
||||||
|
'__wdata',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Node.js 宿主字段 — 真实 Chrome window 上连描述符都不该有
|
||||||
|
const NODE_KEYS = new Set([
|
||||||
|
'process', 'require', 'Buffer', 'module', 'exports',
|
||||||
|
'__dirname', '__filename',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isBotKey(key) {
|
function isBotKey(key) {
|
||||||
if (BOT_KEYS.has(key)) return true;
|
if (BOT_KEYS.has(key)) return true;
|
||||||
|
if (NODE_KEYS.has(key)) return true;
|
||||||
if (typeof key === 'string' && (
|
if (typeof key === 'string' && (
|
||||||
key.startsWith('cdc_') ||
|
key.startsWith('cdc_') ||
|
||||||
key.startsWith('$cdc_') ||
|
key.startsWith('$cdc_') ||
|
||||||
|
|||||||
@@ -2,116 +2,299 @@
|
|||||||
/**
|
/**
|
||||||
* P1: Canvas mock
|
* P1: Canvas mock
|
||||||
* hsw 检测:HTMLCanvasElement / CanvasRenderingContext2D / fillStyle 默认值 / measureText
|
* 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 () {});
|
const CanvasRenderingContext2D = createNative('CanvasRenderingContext2D', function () {});
|
||||||
CanvasRenderingContext2D.prototype = {
|
CanvasRenderingContext2D.prototype = {
|
||||||
constructor: CanvasRenderingContext2D,
|
constructor: CanvasRenderingContext2D,
|
||||||
fillStyle: '#000000', // P1: 默认值必须是黑色
|
fillStyle: '#000000',
|
||||||
strokeStyle: '#000000',
|
strokeStyle: '#000000',
|
||||||
font: '10px sans-serif',
|
font: '10px sans-serif',
|
||||||
textAlign: 'start',
|
textAlign: 'start',
|
||||||
textBaseline: 'alphabetic',
|
textBaseline: 'alphabetic',
|
||||||
globalAlpha: 1,
|
globalAlpha: 1,
|
||||||
|
globalCompositeOperation: 'source-over',
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
fillRect: createNative('fillRect', function () {}),
|
lineCap: 'butt',
|
||||||
strokeRect: createNative('strokeRect', function () {}),
|
lineJoin: 'miter',
|
||||||
clearRect: createNative('clearRect', function () {}),
|
miterLimit: 10,
|
||||||
fillText: createNative('fillText', function () {}),
|
shadowBlur: 0,
|
||||||
strokeText: createNative('strokeText', function () {}),
|
shadowColor: 'rgba(0, 0, 0, 0)',
|
||||||
beginPath: createNative('beginPath', function () {}),
|
shadowOffsetX: 0,
|
||||||
closePath: createNative('closePath', function () {}),
|
shadowOffsetY: 0,
|
||||||
moveTo: createNative('moveTo', function () {}),
|
imageSmoothingEnabled: true,
|
||||||
lineTo: createNative('lineTo', function () {}),
|
imageSmoothingQuality: 'low',
|
||||||
arc: createNative('arc', function () {}),
|
filter: 'none',
|
||||||
fill: createNative('fill', function () {}),
|
direction: 'ltr',
|
||||||
stroke: createNative('stroke', function () {}),
|
fontKerning: 'auto',
|
||||||
save: createNative('save', function () {}),
|
letterSpacing: '0px',
|
||||||
restore: createNative('restore', function () {}),
|
wordSpacing: '0px',
|
||||||
scale: createNative('scale', function () {}),
|
textRendering: 'auto',
|
||||||
rotate: createNative('rotate', function () {}),
|
|
||||||
translate: createNative('translate', function () {}),
|
// ── Drawing methods (seed deterministic pixels) ─────────────
|
||||||
drawImage: createNative('drawImage', function () {}),
|
fillRect: M('fillRect', 4, function (x, y, w, h) {
|
||||||
getImageData: createNative('getImageData', function (x, y, w, h) {
|
if (!this._pbuf) return;
|
||||||
return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h };
|
// 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 () {}),
|
strokeRect: M('strokeRect', 4, function () {}),
|
||||||
createImageData: createNative('createImageData', function (w, h) {
|
clearRect: M('clearRect', 4, function (x, y, w, h) {
|
||||||
return { data: new Uint8ClampedArray(w * h * 4), width: w, height: 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 {
|
return {
|
||||||
width: text.length * 5.5,
|
width: totalWidth * scale,
|
||||||
actualBoundingBoxAscent: 7,
|
actualBoundingBoxLeft: 0,
|
||||||
actualBoundingBoxDescent: 2,
|
actualBoundingBoxRight: totalWidth * scale,
|
||||||
fontBoundingBoxAscent: 8,
|
actualBoundingBoxAscent: 7 * scale,
|
||||||
fontBoundingBoxDescent: 2,
|
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 () {}),
|
setTransform: M('setTransform', 0, function () {}),
|
||||||
clip: createNative('clip', function () {}),
|
resetTransform: M('resetTransform', 0, function () {}),
|
||||||
isPointInPath: createNative('isPointInPath', function () { return false; }),
|
getTransform: M('getTransform', 0, function () {
|
||||||
createLinearGradient: createNative('createLinearGradient', function () {
|
return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
||||||
return { addColorStop: createNative('addColorStop', function () {}) };
|
|
||||||
}),
|
}),
|
||||||
createRadialGradient: createNative('createRadialGradient', function () {
|
clip: M('clip', 0, function () {}),
|
||||||
return { addColorStop: createNative('addColorStop', 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; }),
|
createRadialGradient: M('createRadialGradient', 6, function () {
|
||||||
canvas: null, // 会在 createElement 里回填
|
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() {
|
function makeWebGLContext() {
|
||||||
return {
|
return {
|
||||||
getParameter: createNative('getParameter', function (param) {
|
getParameter: M('getParameter', 1, function (param) {
|
||||||
// RENDERER / VENDOR 参数
|
|
||||||
if (param === 0x1F01) return 'Google Inc. (Intel)'; // RENDERER
|
if (param === 0x1F01) return 'Google Inc. (Intel)'; // RENDERER
|
||||||
if (param === 0x1F00) return 'WebKit WebGL'; // VENDOR
|
if (param === 0x1F00) return 'WebKit WebGL'; // VENDOR
|
||||||
if (param === 0x8B8C) return 'WebGL GLSL ES 3.00'; // SHADING_LANGUAGE_VERSION
|
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 === 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;
|
return null;
|
||||||
}),
|
}),
|
||||||
getExtension: createNative('getExtension', function () { return null; }),
|
getExtension: M('getExtension', 1, function (name) {
|
||||||
getSupportedExtensions: createNative('getSupportedExtensions', function () { return []; }),
|
if (name === 'WEBGL_debug_renderer_info') {
|
||||||
createBuffer: createNative('createBuffer', function () { return {}; }),
|
return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 };
|
||||||
bindBuffer: createNative('bindBuffer', function () {}),
|
}
|
||||||
bufferData: createNative('bufferData', function () {}),
|
return null;
|
||||||
createShader: createNative('createShader', function () { return {}; }),
|
}),
|
||||||
shaderSource: createNative('shaderSource', function () {}),
|
getSupportedExtensions: M('getSupportedExtensions', 0, function () {
|
||||||
compileShader: createNative('compileShader', function () {}),
|
return ['WEBGL_debug_renderer_info', 'OES_texture_float', 'OES_element_index_uint'];
|
||||||
createProgram: createNative('createProgram', function () { return {}; }),
|
}),
|
||||||
attachShader: createNative('attachShader', function () {}),
|
createBuffer: M('createBuffer', 0, () => ({})),
|
||||||
linkProgram: createNative('linkProgram', function () {}),
|
bindBuffer: M('bindBuffer', 2, () => {}),
|
||||||
useProgram: createNative('useProgram', function () {}),
|
bufferData: M('bufferData', 3, () => {}),
|
||||||
getUniformLocation: createNative('getUniformLocation', function () { return {}; }),
|
createShader: M('createShader', 1, () => ({})),
|
||||||
uniform1f: createNative('uniform1f', function () {}),
|
shaderSource: M('shaderSource', 2, () => {}),
|
||||||
drawArrays: createNative('drawArrays', function () {}),
|
compileShader: M('compileShader', 1, () => {}),
|
||||||
readPixels: createNative('readPixels', function () {}),
|
createProgram: M('createProgram', 0, () => ({})),
|
||||||
enable: createNative('enable', function () {}),
|
attachShader: M('attachShader', 2, () => {}),
|
||||||
clear: createNative('clear', function () {}),
|
linkProgram: M('linkProgram', 1, () => {}),
|
||||||
clearColor: createNative('clearColor', function () {}),
|
useProgram: M('useProgram', 1, () => {}),
|
||||||
viewport: createNative('viewport', function () {}),
|
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 {
|
class HTMLCanvasElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.width = 300;
|
this.width = 300;
|
||||||
this.height = 150;
|
this.height = 150;
|
||||||
this._ctx2d = null;
|
this._ctx2d = null;
|
||||||
|
this._pbuf = null;
|
||||||
}
|
}
|
||||||
getContext(type) {
|
getContext(type) {
|
||||||
if (type === '2d') {
|
if (type === '2d') {
|
||||||
if (!this._ctx2d) {
|
if (!this._ctx2d) {
|
||||||
|
this._pbuf = new PixelBuffer(this.width, this.height);
|
||||||
this._ctx2d = Object.create(CanvasRenderingContext2D.prototype);
|
this._ctx2d = Object.create(CanvasRenderingContext2D.prototype);
|
||||||
this._ctx2d.canvas = this;
|
this._ctx2d.canvas = this;
|
||||||
|
this._ctx2d._pbuf = this._pbuf;
|
||||||
}
|
}
|
||||||
return this._ctx2d;
|
return this._ctx2d;
|
||||||
}
|
}
|
||||||
@@ -121,11 +304,30 @@ class HTMLCanvasElement {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
toDataURL(type) {
|
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==';
|
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||||
}
|
}
|
||||||
toBlob(cb) { cb(null); }
|
toBlob(cb, type, quality) {
|
||||||
captureStream() { return {}; }
|
// 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);
|
nativeClass(HTMLCanvasElement);
|
||||||
|
|
||||||
|
|||||||
310
src/sandbox/mocks/class_registry.js
Normal file
310
src/sandbox/mocks/class_registry.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -1,32 +1,39 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* P1: Crypto / Storage / IDBFactory / atob / btoa mock
|
* 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');
|
const nodeCrypto = require('crypto');
|
||||||
|
|
||||||
// ── Crypto ───────────────────────────────────────────────────
|
// ── SubtleCrypto instance with proper prototype ─────────────────
|
||||||
const cryptoMock = {
|
const subtle = Object.create(SubtleCrypto.prototype);
|
||||||
getRandomValues: createNative('getRandomValues', function (array) {
|
Object.assign(subtle, {
|
||||||
return nodeCrypto.randomFillSync(array);
|
digest: M('digest', 2, () => Promise.resolve(new ArrayBuffer(32))),
|
||||||
}),
|
encrypt: M('encrypt', 3, () => Promise.resolve(new ArrayBuffer(0))),
|
||||||
randomUUID: createNative('randomUUID', function () {
|
decrypt: M('decrypt', 3, () => Promise.resolve(new ArrayBuffer(0))),
|
||||||
return nodeCrypto.randomUUID();
|
sign: M('sign', 3, () => Promise.resolve(new ArrayBuffer(32))),
|
||||||
}),
|
verify: M('verify', 4, () => Promise.resolve(true)),
|
||||||
subtle: {
|
generateKey: M('generateKey', 3, () => Promise.resolve({})),
|
||||||
digest: createNative('digest', function () { return Promise.resolve(new ArrayBuffer(32)); }),
|
importKey: M('importKey', 5, () => Promise.resolve({})),
|
||||||
encrypt: createNative('encrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }),
|
exportKey: M('exportKey', 2, () => Promise.resolve({})),
|
||||||
decrypt: createNative('decrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }),
|
deriveBits: M('deriveBits', 3, () => Promise.resolve(new ArrayBuffer(32))),
|
||||||
sign: createNative('sign', function () { return Promise.resolve(new ArrayBuffer(32)); }),
|
deriveKey: M('deriveKey', 5, () => Promise.resolve({})),
|
||||||
verify: createNative('verify', function () { return Promise.resolve(true); }),
|
wrapKey: M('wrapKey', 4, () => Promise.resolve(new ArrayBuffer(0))),
|
||||||
generateKey: createNative('generateKey', function () { return Promise.resolve({}); }),
|
unwrapKey: M('unwrapKey', 7, () => Promise.resolve({})),
|
||||||
importKey: createNative('importKey', function () { return Promise.resolve({}); }),
|
});
|
||||||
exportKey: createNative('exportKey', function () { return 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 {
|
class Storage {
|
||||||
constructor() { this._store = {}; }
|
constructor() { this._store = {}; }
|
||||||
get length() { return Object.keys(this._store).length; }
|
get length() { return Object.keys(this._store).length; }
|
||||||
@@ -36,31 +43,33 @@ class Storage {
|
|||||||
removeItem(k) { delete this._store[k]; }
|
removeItem(k) { delete this._store[k]; }
|
||||||
clear() { this._store = {}; }
|
clear() { this._store = {}; }
|
||||||
}
|
}
|
||||||
|
// Set proper prototype chain: Storage instance -> StorageProto.prototype -> Object
|
||||||
|
Object.setPrototypeOf(Storage.prototype, StorageProto.prototype);
|
||||||
nativeClass(Storage);
|
nativeClass(Storage);
|
||||||
|
|
||||||
// ── IDBFactory (indexedDB) ────────────────────────────────────
|
// ── IDBFactory (indexedDB) ──────────────────────────────────────
|
||||||
class IDBFactory {
|
class IDBFactory {
|
||||||
open() { return { result: null, onerror: null, onsuccess: null }; }
|
open(name) { return { result: null, onerror: null, onsuccess: null }; }
|
||||||
deleteDatabase() { return {}; }
|
deleteDatabase(name) { return {}; }
|
||||||
databases() { return Promise.resolve([]); }
|
databases() { return Promise.resolve([]); }
|
||||||
cmp() { return 0; }
|
cmp(first, second) { return 0; }
|
||||||
}
|
}
|
||||||
nativeClass(IDBFactory);
|
nativeClass(IDBFactory);
|
||||||
|
|
||||||
// ── Notification ──────────────────────────────────────────────
|
// ── Notification ────────────────────────────────────────────────
|
||||||
class Notification {
|
class Notification {
|
||||||
constructor(title, opts) {
|
constructor(title, opts) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.options = opts || {};
|
this.options = opts || {};
|
||||||
}
|
}
|
||||||
close() {}
|
close() {}
|
||||||
static get permission() { return 'denied'; } // P2: denied 或 default
|
static get permission() { return 'denied'; }
|
||||||
static requestPermission() { return Promise.resolve('denied'); }
|
static requestPermission() { return Promise.resolve('denied'); }
|
||||||
}
|
}
|
||||||
nativeClass(Notification);
|
nativeClass(Notification);
|
||||||
|
|
||||||
// ── atob / btoa ───────────────────────────────────────────────
|
// ── atob / btoa ─────────────────────────────────────────────────
|
||||||
const atob = createNative('atob', (str) => Buffer.from(str, 'base64').toString('binary'));
|
const atob = M('atob', 1, (str) => Buffer.from(str, 'base64').toString('binary'));
|
||||||
const btoa = createNative('btoa', (str) => Buffer.from(str, 'binary').toString('base64'));
|
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 };
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
/**
|
/**
|
||||||
* P1: Document / HTMLDocument mock
|
* P1: Document / HTMLDocument mock
|
||||||
* hsw 检测:document 类型、createElement、cookie 等
|
* 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 { HTMLCanvasElement } = require('./canvas');
|
||||||
|
const CR = require('./class_registry');
|
||||||
|
|
||||||
class HTMLDocument {
|
class HTMLDocument {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -20,34 +22,80 @@ class HTMLDocument {
|
|||||||
this.contentType = 'text/html';
|
this.contentType = 'text/html';
|
||||||
this.URL = '';
|
this.URL = '';
|
||||||
this.domain = '';
|
this.domain = '';
|
||||||
this.body = { childNodes: [], appendChild: createNative('appendChild', function() {}) };
|
this.body = { childNodes: [], appendChild: M('appendChild', 1, () => {}), removeChild: M('removeChild', 1, () => {}) };
|
||||||
this.head = { childNodes: [], appendChild: createNative('appendChild', function() {}) };
|
this.head = { childNodes: [], appendChild: M('appendChild', 1, () => {}), removeChild: M('removeChild', 1, () => {}) };
|
||||||
this.documentElement = { clientWidth: 1920, clientHeight: 1080 };
|
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();
|
const t = tag.toLowerCase();
|
||||||
if (t === 'canvas') return new HTMLCanvasElement();
|
if (t === 'canvas') return new HTMLCanvasElement();
|
||||||
if (t === 'div' || t === 'span' || t === 'p') {
|
if (t === 'iframe') {
|
||||||
return {
|
const el = Object.create(CR.HTMLIFrameElement.prototype);
|
||||||
style: {},
|
Object.assign(el, { style: {}, contentWindow: null, contentDocument: null, src: '', sandbox: '' });
|
||||||
appendChild: createNative('appendChild', function() {}),
|
el.appendChild = M('appendChild', 1, () => {});
|
||||||
getAttribute: createNative('getAttribute', function() { return null; }),
|
el.getAttribute = M('getAttribute', 1, () => null);
|
||||||
setAttribute: createNative('setAttribute', function() {}),
|
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.createElementNS = M('createElementNS', 2, function (ns, tag) {
|
||||||
HTMLDocument.prototype.querySelector = createNative('querySelector', function () { return null; });
|
return this.createElement(tag);
|
||||||
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.createEvent = M('createEvent', 1, function (type) {
|
||||||
HTMLDocument.prototype.addEventListener = createNative('addEventListener', function () {});
|
return {
|
||||||
HTMLDocument.prototype.removeEventListener = createNative('removeEventListener', function () {});
|
type: '',
|
||||||
HTMLDocument.prototype.dispatchEvent = createNative('dispatchEvent', function () { return true; });
|
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);
|
nativeClass(HTMLDocument);
|
||||||
|
|
||||||
module.exports = HTMLDocument;
|
module.exports = HTMLDocument;
|
||||||
|
|||||||
83
src/sandbox/mocks/error.js
Normal file
83
src/sandbox/mocks/error.js
Normal file
@@ -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 <anonymous>: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() || '<anonymous>';
|
||||||
|
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 = `<anonymous>:${line}:${col}`;
|
||||||
|
} else if (file === 'evalmachine.<anonymous>' || file === '<anonymous>') {
|
||||||
|
location = `<anonymous>:${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 };
|
||||||
@@ -3,9 +3,12 @@
|
|||||||
* Mock 总装工厂
|
* Mock 总装工厂
|
||||||
* 导出 createBrowserEnvironment(),返回 { window, document, navigator, ... }
|
* 导出 createBrowserEnvironment(),返回 { window, document, navigator, ... }
|
||||||
* 供 HswRunner 注入全局作用域
|
* 供 HswRunner 注入全局作用域
|
||||||
|
* Now integrates: class_registry, math, error stack rewriting, constructor chain patching
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const windowProxy = require('./window');
|
const windowProxy = require('./window');
|
||||||
|
const { patchConstructorChain } = require('./native');
|
||||||
|
const { installErrorStackRewrite } = require('./error');
|
||||||
|
|
||||||
function createBrowserEnvironment(fingerprint = {}) {
|
function createBrowserEnvironment(fingerprint = {}) {
|
||||||
const win = windowProxy;
|
const win = windowProxy;
|
||||||
@@ -29,7 +32,6 @@ function createBrowserEnvironment(fingerprint = {}) {
|
|||||||
win.screen.availHeight = fingerprint.screenHeight - 40;
|
win.screen.availHeight = fingerprint.screenHeight - 40;
|
||||||
}
|
}
|
||||||
if (fingerprint.host) {
|
if (fingerprint.host) {
|
||||||
// 更新 location 中与 host 相关的字段
|
|
||||||
const loc = win.location;
|
const loc = win.location;
|
||||||
if (loc.ancestorOrigins) {
|
if (loc.ancestorOrigins) {
|
||||||
loc.ancestorOrigins[0] = `https://${fingerprint.host}`;
|
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 };
|
||||||
|
|||||||
64
src/sandbox/mocks/math.js
Normal file
64
src/sandbox/mocks/math.js
Normal file
@@ -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;
|
||||||
@@ -17,9 +17,11 @@ Function.prototype.toString = function () {
|
|||||||
}
|
}
|
||||||
return _origToString.call(this);
|
return _origToString.call(this);
|
||||||
};
|
};
|
||||||
|
// toString 自身也要过检测
|
||||||
|
nativeSet.add(Function.prototype.toString);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将一个 JS 函数包装成"看起来像原生"的函数
|
* 将一个 JS 函数包装成"看起来像原生"的函数(保留 .prototype,用于构造函数)
|
||||||
* @param {string} name - 函数名(影响 toString 输出)
|
* @param {string} name - 函数名(影响 toString 输出)
|
||||||
* @param {Function} fn - 实际实现
|
* @param {Function} fn - 实际实现
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
@@ -30,6 +32,21 @@ function createNative(name, fn) {
|
|||||||
return 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
|
* 将一个 class 的构造函数 + 所有原型方法 全部标记为 native
|
||||||
* @param {Function} cls
|
* @param {Function} cls
|
||||||
@@ -47,4 +64,64 @@ function nativeClass(cls) {
|
|||||||
return 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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,23 +2,87 @@
|
|||||||
/**
|
/**
|
||||||
* P0/P1: Navigator mock
|
* P0/P1: Navigator mock
|
||||||
* hsw 检测:webdriver / languages / maxTouchPoints / plugins / userAgentData
|
* 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 结构
|
// ── PluginArray with proper prototype ───────────────────────────
|
||||||
const plugins = Object.assign(Object.create({
|
const plugins = Object.create(PluginArray.prototype);
|
||||||
item: createNative('item', function (i) { return this[i] || null; }),
|
Object.assign(plugins, {
|
||||||
namedItem: createNative('namedItem', function () { return null; }),
|
|
||||||
refresh: createNative('refresh', function () {}),
|
|
||||||
}), {
|
|
||||||
0: { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
|
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 },
|
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 },
|
2: { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
|
||||||
length: 3,
|
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',
|
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',
|
appVersion: '5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
|
||||||
appName: 'Netscape',
|
appName: 'Netscape',
|
||||||
@@ -27,65 +91,76 @@ const navigatorMock = {
|
|||||||
product: 'Gecko',
|
product: 'Gecko',
|
||||||
vendor: 'Google Inc.',
|
vendor: 'Google Inc.',
|
||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
languages: ['en-US', 'en'], // P1: 必须是非空数组
|
languages: Object.freeze(['en-US', 'en']),
|
||||||
webdriver: false, // navigator.webdriver = false(window.webdriver = undefined)
|
webdriver: false,
|
||||||
maxTouchPoints: 0, // P1: 桌面为 0
|
maxTouchPoints: 0,
|
||||||
hardwareConcurrency: 8,
|
hardwareConcurrency: 8,
|
||||||
deviceMemory: 8,
|
deviceMemory: 8,
|
||||||
cookieEnabled: true,
|
cookieEnabled: true,
|
||||||
onLine: true,
|
onLine: true,
|
||||||
doNotTrack: null,
|
doNotTrack: null,
|
||||||
|
pdfViewerEnabled: true,
|
||||||
plugins,
|
plugins,
|
||||||
mimeTypes: { length: 0 },
|
mimeTypes,
|
||||||
|
userAgentData,
|
||||||
|
connection,
|
||||||
|
permissions,
|
||||||
|
|
||||||
// P2: userAgentData (NavigatorUAData)
|
// P2: missing sub-properties from probe report
|
||||||
userAgentData: {
|
mediaDevices: {
|
||||||
brands: [
|
enumerateDevices: M('enumerateDevices', 0, () => Promise.resolve([])),
|
||||||
{ brand: 'Not:A-Brand', version: '99' },
|
getUserMedia: M('getUserMedia', 1, () => Promise.reject(new DOMException('NotAllowedError'))),
|
||||||
{ brand: 'Google Chrome', version: '145' },
|
getDisplayMedia: M('getDisplayMedia', 0, () => Promise.reject(new DOMException('NotAllowedError'))),
|
||||||
{ 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: connection (NetworkInformation)
|
storage: {
|
||||||
connection: {
|
estimate: M('estimate', 0, () => Promise.resolve({ quota: 2147483648, usage: 0 })),
|
||||||
effectiveType: '4g',
|
persist: M('persist', 0, () => Promise.resolve(false)),
|
||||||
downlink: 10,
|
persisted: M('persisted', 0, () => Promise.resolve(false)),
|
||||||
rtt: 50,
|
getDirectory: M('getDirectory', 0, () => Promise.resolve({})),
|
||||||
saveData: false,
|
},
|
||||||
|
|
||||||
|
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: {
|
geolocation: {
|
||||||
getCurrentPosition: createNative('getCurrentPosition', function (s, e) { e && e({ code: 1, message: 'denied' }); }),
|
getCurrentPosition: M('getCurrentPosition', 1, (s, e) => { if (e) e({ code: 1, message: 'denied' }); }),
|
||||||
watchPosition: createNative('watchPosition', function () { return 0; }),
|
watchPosition: M('watchPosition', 1, () => 0),
|
||||||
clearWatch: createNative('clearWatch', function () {}),
|
clearWatch: M('clearWatch', 1, () => {}),
|
||||||
},
|
},
|
||||||
|
|
||||||
permissions: {
|
sendBeacon: M('sendBeacon', 1, () => true),
|
||||||
query: createNative('query', function (desc) {
|
vibrate: M('vibrate', 1, () => false),
|
||||||
return Promise.resolve({ state: desc.name === 'notifications' ? 'denied' : 'prompt' });
|
});
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
sendBeacon: createNative('sendBeacon', function () { return true; }),
|
// oscpu: do NOT define as own property — Chrome doesn't have it at all.
|
||||||
vibrate: createNative('vibrate', function () { return false; }),
|
// 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;
|
module.exports = navigatorMock;
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
/**
|
/**
|
||||||
* P0: Performance mock
|
* P0: Performance mock
|
||||||
* hsw 检测:timing / timeOrigin / getEntriesByType('resource') / getEntriesByType('navigation')
|
* 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;
|
const NAV_START = Date.now() - 1200;
|
||||||
|
|
||||||
@@ -32,9 +41,23 @@ const timingData = {
|
|||||||
unloadEventEnd: 0,
|
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 请求痕迹)
|
// 模拟 resource 条目(hsw 会查 checksiteconfig 请求痕迹)
|
||||||
const resourceEntries = [
|
const resourceEntries = [
|
||||||
{
|
makeResourceEntry({
|
||||||
name: 'https://api.hcaptcha.com/checksiteconfig?v=xxx&host=b.stripecdn.com&sitekey=xxx&sc=1&swa=1&spst=1',
|
name: 'https://api.hcaptcha.com/checksiteconfig?v=xxx&host=b.stripecdn.com&sitekey=xxx&sc=1&swa=1&spst=1',
|
||||||
entryType: 'resource',
|
entryType: 'resource',
|
||||||
initiatorType: 'xmlhttprequest',
|
initiatorType: 'xmlhttprequest',
|
||||||
@@ -60,11 +83,11 @@ const resourceEntries = [
|
|||||||
requestStart: 0,
|
requestStart: 0,
|
||||||
responseStart: 0,
|
responseStart: 0,
|
||||||
firstInterimResponseStart: 0,
|
firstInterimResponseStart: 0,
|
||||||
finalResponseHeadersStart: 0, // P2 要求的字段
|
finalResponseHeadersStart: 0,
|
||||||
serverTiming: [],
|
serverTiming: [],
|
||||||
renderBlockingStatus: 'non-blocking',
|
renderBlockingStatus: 'non-blocking',
|
||||||
},
|
}),
|
||||||
{
|
makeResourceEntry({
|
||||||
name: 'https://newassets.hcaptcha.com/c/xxx/hsw.js',
|
name: 'https://newassets.hcaptcha.com/c/xxx/hsw.js',
|
||||||
entryType: 'resource',
|
entryType: 'resource',
|
||||||
initiatorType: 'script',
|
initiatorType: 'script',
|
||||||
@@ -93,11 +116,11 @@ const resourceEntries = [
|
|||||||
redirectEnd: 0,
|
redirectEnd: 0,
|
||||||
serverTiming: [],
|
serverTiming: [],
|
||||||
renderBlockingStatus: 'non-blocking',
|
renderBlockingStatus: 'non-blocking',
|
||||||
},
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 模拟 navigation 条目
|
// 模拟 navigation 条目
|
||||||
const navigationEntry = {
|
const navigationEntry = makeNavEntry({
|
||||||
name: 'https://newassets.hcaptcha.com/captcha/v1/xxx/static/hcaptcha.html',
|
name: 'https://newassets.hcaptcha.com/captcha/v1/xxx/static/hcaptcha.html',
|
||||||
entryType: 'navigation',
|
entryType: 'navigation',
|
||||||
initiatorType: 'navigation',
|
initiatorType: 'navigation',
|
||||||
@@ -140,31 +163,47 @@ const navigationEntry = {
|
|||||||
workerStart: 0,
|
workerStart: 0,
|
||||||
contentEncoding: 'br',
|
contentEncoding: 'br',
|
||||||
renderBlockingStatus: 'non-blocking',
|
renderBlockingStatus: 'non-blocking',
|
||||||
};
|
});
|
||||||
|
|
||||||
const performanceMock = {
|
// ── Build performance object with proper prototype ──────────────
|
||||||
|
const performanceMock = Object.create(Performance.prototype);
|
||||||
|
|
||||||
|
Object.assign(performanceMock, {
|
||||||
timeOrigin: NAV_START,
|
timeOrigin: NAV_START,
|
||||||
timing: timingData,
|
timing: timingData,
|
||||||
navigation: { type: 0, redirectCount: 0 },
|
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 === 'resource') return resourceEntries;
|
||||||
if (type === 'navigation') return [navigationEntry];
|
if (type === 'navigation') return [navigationEntry];
|
||||||
|
if (type === 'paint') return [];
|
||||||
|
if (type === 'mark') return [];
|
||||||
|
if (type === 'measure') return [];
|
||||||
return [];
|
return [];
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getEntriesByName: createNative('getEntriesByName', function (name) {
|
getEntriesByName: M('getEntriesByName', 1, (name) => {
|
||||||
return resourceEntries.filter(e => e.name === name);
|
return [...resourceEntries, navigationEntry].filter(e => e.name === name);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
now: createNative('now', function () {
|
mark: M('mark', 1, () => {}),
|
||||||
return Date.now() - NAV_START;
|
measure: M('measure', 1, () => {}),
|
||||||
}),
|
clearMarks: M('clearMarks', 0, () => {}),
|
||||||
|
clearMeasures: M('clearMeasures', 0, () => {}),
|
||||||
mark: createNative('mark', function () {}),
|
clearResourceTimings: M('clearResourceTimings', 0, () => {}),
|
||||||
measure: createNative('measure', function () {}),
|
setResourceTimingBufferSize: M('setResourceTimingBufferSize', 1, () => {}),
|
||||||
clearMarks: createNative('clearMarks', function () {}),
|
addEventListener: M('addEventListener', 2, () => {}),
|
||||||
clearMeasures: createNative('clearMeasures', function () {}),
|
removeEventListener: M('removeEventListener', 2, () => {}),
|
||||||
};
|
});
|
||||||
|
|
||||||
module.exports = performanceMock;
|
module.exports = performanceMock;
|
||||||
|
|||||||
@@ -2,9 +2,19 @@
|
|||||||
/**
|
/**
|
||||||
* P1: Screen mock
|
* P1: Screen mock
|
||||||
* hsw 检测:screen.width / height / colorDepth / pixelDepth / availWidth / availHeight
|
* 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,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
availWidth: 1920,
|
availWidth: 1920,
|
||||||
@@ -13,10 +23,8 @@ const screenMock = {
|
|||||||
availTop: 0,
|
availTop: 0,
|
||||||
colorDepth: 24,
|
colorDepth: 24,
|
||||||
pixelDepth: 24,
|
pixelDepth: 24,
|
||||||
orientation: {
|
isExtended: false,
|
||||||
type: 'landscape-primary',
|
orientation,
|
||||||
angle: 0,
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = screenMock;
|
module.exports = screenMock;
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
/**
|
/**
|
||||||
* P0: RTCPeerConnection mock
|
* P0: RTCPeerConnection mock
|
||||||
* P0: OfflineAudioContext mock
|
* P0: OfflineAudioContext mock
|
||||||
|
* P2: Blob / Worker mock (stack depth detection)
|
||||||
* hsw 检测:构造函数存在性 + 原型链 + toString() 不暴露源码
|
* hsw 检测:构造函数存在性 + 原型链 + toString() 不暴露源码
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { createNative, nativeClass } = require('./native');
|
const { nativeMethod: M, nativeClass } = require('./native');
|
||||||
|
const {
|
||||||
|
RTCRtpSender, RTCRtpReceiver, RTCSessionDescription,
|
||||||
|
} = require('./class_registry');
|
||||||
|
|
||||||
// ── RTCPeerConnection ────────────────────────────────────────
|
// ── RTCPeerConnection ──────────────────────────────────────────
|
||||||
class RTCPeerConnection {
|
class RTCPeerConnection {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.localDescription = null;
|
this.localDescription = null;
|
||||||
@@ -17,26 +21,40 @@ class RTCPeerConnection {
|
|||||||
this.iceGatheringState = 'new';
|
this.iceGatheringState = 'new';
|
||||||
this.connectionState = 'new';
|
this.connectionState = 'new';
|
||||||
this._config = config || {};
|
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) {
|
RTCPeerConnection.prototype.createOffer = M('createOffer', 0, (options) =>
|
||||||
return Promise.resolve({ type: 'offer', sdp: 'v=0\r\n' });
|
Promise.resolve({ type: 'offer', sdp: 'v=0\r\n' })
|
||||||
});
|
);
|
||||||
RTCPeerConnection.prototype.createAnswer = createNative('createAnswer', function () {
|
RTCPeerConnection.prototype.createAnswer = M('createAnswer', 0, () =>
|
||||||
return Promise.resolve({ type: 'answer', sdp: 'v=0\r\n' });
|
Promise.resolve({ type: 'answer', sdp: 'v=0\r\n' })
|
||||||
});
|
);
|
||||||
RTCPeerConnection.prototype.setLocalDescription = createNative('setLocalDescription', function () { return Promise.resolve(); });
|
RTCPeerConnection.prototype.setLocalDescription = M('setLocalDescription', 0, () => Promise.resolve());
|
||||||
RTCPeerConnection.prototype.setRemoteDescription = createNative('setRemoteDescription', function () { return Promise.resolve(); });
|
RTCPeerConnection.prototype.setRemoteDescription = M('setRemoteDescription', 1, () => Promise.resolve());
|
||||||
RTCPeerConnection.prototype.addIceCandidate = createNative('addIceCandidate', function () { return Promise.resolve(); });
|
RTCPeerConnection.prototype.addIceCandidate = M('addIceCandidate', 0, () => Promise.resolve());
|
||||||
RTCPeerConnection.prototype.createDataChannel = createNative('createDataChannel', function (label) {
|
RTCPeerConnection.prototype.createDataChannel = M('createDataChannel', 1, (label) => ({
|
||||||
return { label, readyState: 'open', close: createNative('close', function(){}) };
|
label, readyState: 'open', close: M('close', 0, () => {}),
|
||||||
});
|
send: M('send', 1, () => {}), onmessage: null, onopen: null, onclose: null,
|
||||||
RTCPeerConnection.prototype.close = createNative('close', function () {});
|
}));
|
||||||
RTCPeerConnection.prototype.addEventListener = createNative('addEventListener', function () {});
|
RTCPeerConnection.prototype.close = M('close', 0, () => {});
|
||||||
RTCPeerConnection.prototype.removeEventListener = createNative('removeEventListener', function () {});
|
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);
|
nativeClass(RTCPeerConnection);
|
||||||
|
|
||||||
// ── OfflineAudioContext ──────────────────────────────────────
|
// ── OfflineAudioContext ────────────────────────────────────────
|
||||||
class OfflineAudioContext {
|
class OfflineAudioContext {
|
||||||
constructor(channels, length, sampleRate) {
|
constructor(channels, length, sampleRate) {
|
||||||
this.length = length || 4096;
|
this.length = length || 4096;
|
||||||
@@ -46,40 +64,141 @@ class OfflineAudioContext {
|
|||||||
this.destination = { channelCount: channels || 1 };
|
this.destination = { channelCount: channels || 1 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OfflineAudioContext.prototype.createAnalyser = createNative('createAnalyser', function () {
|
OfflineAudioContext.prototype.createAnalyser = M('createAnalyser', 0, function () {
|
||||||
return {
|
return {
|
||||||
fftSize: 2048,
|
fftSize: 2048,
|
||||||
frequencyBinCount: 1024,
|
frequencyBinCount: 1024,
|
||||||
connect: createNative('connect', function () {}),
|
connect: M('connect', 1, () => {}),
|
||||||
getFloatFrequencyData: createNative('getFloatFrequencyData', function (arr) {
|
disconnect: M('disconnect', 0, () => {}),
|
||||||
|
getFloatFrequencyData: M('getFloatFrequencyData', 1, (arr) => {
|
||||||
for (let i = 0; i < arr.length; i++) arr[i] = -100 + Math.random() * 5;
|
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 {
|
return {
|
||||||
type: 'triangle',
|
type: 'triangle',
|
||||||
frequency: { value: 10000 },
|
frequency: { value: 10000 },
|
||||||
connect: createNative('connect', function () {}),
|
connect: M('connect', 1, () => {}),
|
||||||
start: createNative('start', function () {}),
|
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 {
|
return {
|
||||||
threshold: { value: -50 }, knee: { value: 40 },
|
threshold: { value: -50 }, knee: { value: 40 },
|
||||||
ratio: { value: 12 }, attack: { value: 0 }, release: { value: 0.25 },
|
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 len = this.length;
|
||||||
// 固定指纹数据,保持每次一致(稳定指纹)
|
|
||||||
const data = new Float32Array(len);
|
const data = new Float32Array(len);
|
||||||
for (let i = 0; i < len; i++) data[i] = Math.sin(i * 0.001) * 0.01;
|
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.addEventListener = M('addEventListener', 2, () => {});
|
||||||
OfflineAudioContext.prototype.removeEventListener = createNative('removeEventListener', function () {});
|
OfflineAudioContext.prototype.removeEventListener = M('removeEventListener', 2, () => {});
|
||||||
nativeClass(OfflineAudioContext);
|
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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,20 +2,31 @@
|
|||||||
/**
|
/**
|
||||||
* 总装:window 沙盒
|
* 总装:window 沙盒
|
||||||
* 按 P0→P1→P2 顺序挂载所有 mock,并用 Proxy 屏蔽 bot 字段
|
* 按 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 { isBotKey } = require('./bot_shield');
|
||||||
const performanceMock = require('./performance');
|
const performanceMock = require('./performance');
|
||||||
const navigatorMock = require('./navigator');
|
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 { HTMLCanvasElement, CanvasRenderingContext2D } = require('./canvas');
|
||||||
const { cryptoMock, Storage, IDBFactory, Notification, atob, btoa } = require('./crypto');
|
const { cryptoMock, Storage, IDBFactory, Notification, atob, btoa } = require('./crypto');
|
||||||
const screenMock = require('./screen');
|
const screenMock = require('./screen');
|
||||||
const HTMLDocument = require('./document');
|
const HTMLDocument = require('./document');
|
||||||
|
const chromeMath = require('./math');
|
||||||
|
const CR = require('./class_registry');
|
||||||
|
|
||||||
// ── 基础 window 对象 ─────────────────────────────────────────
|
// ── 基础 window 对象 ─────────────────────────────────────────
|
||||||
const _win = {
|
const _win = Object.create(CR.Window.prototype);
|
||||||
|
|
||||||
|
Object.assign(_win, {
|
||||||
|
|
||||||
// ── P0: 核心 API ──────────────────────────────────────
|
// ── P0: 核心 API ──────────────────────────────────────
|
||||||
performance: performanceMock,
|
performance: performanceMock,
|
||||||
@@ -26,6 +37,9 @@ const _win = {
|
|||||||
RTCPeerConnection,
|
RTCPeerConnection,
|
||||||
webkitRTCPeerConnection: RTCPeerConnection,
|
webkitRTCPeerConnection: RTCPeerConnection,
|
||||||
OfflineAudioContext,
|
OfflineAudioContext,
|
||||||
|
RTCRtpSender,
|
||||||
|
RTCRtpReceiver,
|
||||||
|
RTCSessionDescription,
|
||||||
|
|
||||||
// ── P1: Canvas ────────────────────────────────────────
|
// ── P1: Canvas ────────────────────────────────────────
|
||||||
HTMLCanvasElement,
|
HTMLCanvasElement,
|
||||||
@@ -48,14 +62,18 @@ const _win = {
|
|||||||
document: new HTMLDocument(),
|
document: new HTMLDocument(),
|
||||||
HTMLDocument,
|
HTMLDocument,
|
||||||
|
|
||||||
|
// ── P1: Blob / Worker ─────────────────────────────────
|
||||||
|
Blob,
|
||||||
|
Worker,
|
||||||
|
|
||||||
// ── P2: 移动端触摸 → 桌面不存在 ──────────────────────
|
// ── P2: 移动端触摸 → 桌面不存在 ──────────────────────
|
||||||
// ontouchstart: 不定义,Proxy 返回 undefined
|
// ontouchstart: 不定义,Proxy 返回 undefined
|
||||||
|
|
||||||
// ── 基础 JS 全局 ─────────────────────────────────────
|
// ── 基础 JS 全局 (use SafeFunction to block escape) ──
|
||||||
Promise,
|
Promise,
|
||||||
Object,
|
Object,
|
||||||
Array,
|
Array,
|
||||||
Function,
|
Function: SafeFunction,
|
||||||
Number,
|
Number,
|
||||||
String,
|
String,
|
||||||
Boolean,
|
Boolean,
|
||||||
@@ -63,7 +81,13 @@ const _win = {
|
|||||||
Date,
|
Date,
|
||||||
RegExp,
|
RegExp,
|
||||||
Error,
|
Error,
|
||||||
Math,
|
TypeError,
|
||||||
|
RangeError,
|
||||||
|
SyntaxError,
|
||||||
|
ReferenceError,
|
||||||
|
URIError,
|
||||||
|
EvalError,
|
||||||
|
Math: chromeMath,
|
||||||
JSON,
|
JSON,
|
||||||
parseInt,
|
parseInt,
|
||||||
parseFloat,
|
parseFloat,
|
||||||
@@ -75,7 +99,7 @@ const _win = {
|
|||||||
encodeURIComponent,
|
encodeURIComponent,
|
||||||
escape,
|
escape,
|
||||||
unescape,
|
unescape,
|
||||||
eval,
|
eval: safeEval,
|
||||||
undefined,
|
undefined,
|
||||||
Infinity,
|
Infinity,
|
||||||
NaN,
|
NaN,
|
||||||
@@ -100,6 +124,9 @@ const _win = {
|
|||||||
search: '',
|
search: '',
|
||||||
hash: '',
|
hash: '',
|
||||||
ancestorOrigins: { 0: 'https://b.stripecdn.com', 1: 'https://js.stripe.com', length: 2 },
|
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,
|
innerWidth: 530,
|
||||||
@@ -133,20 +160,21 @@ const _win = {
|
|||||||
length: 1,
|
length: 1,
|
||||||
state: null,
|
state: null,
|
||||||
scrollRestoration: 'auto',
|
scrollRestoration: 'auto',
|
||||||
go: createNative('go', function () {}),
|
go: M('go', 0, () => {}),
|
||||||
back: createNative('back', function () {}),
|
back: M('back', 0, () => {}),
|
||||||
forward: createNative('forward', function () {}),
|
forward: M('forward', 0, () => {}),
|
||||||
pushState: createNative('pushState', function () {}),
|
pushState: M('pushState', 2, () => {}),
|
||||||
replaceState: createNative('replaceState', function () {}),
|
replaceState: M('replaceState', 2, () => {}),
|
||||||
},
|
},
|
||||||
|
|
||||||
fetch: createNative('fetch', function (url, opts) {
|
fetch: M('fetch', 1, (url, opts) => {
|
||||||
// 沙盒里一般不真正发请求,返回 resolved 空 response
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: true, status: 200,
|
ok: true, status: 200,
|
||||||
json: () => Promise.resolve({}),
|
json: M('json', 0, () => Promise.resolve({})),
|
||||||
text: () => Promise.resolve(''),
|
text: M('text', 0, () => Promise.resolve('')),
|
||||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
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; }),
|
Response: createNative('Response', function (body, opts) { this.status = opts?.status || 200; }),
|
||||||
Headers: createNative('Headers', function () { this._h = {}; }),
|
Headers: createNative('Headers', function () { this._h = {}; }),
|
||||||
|
|
||||||
URL: createNative('URL', function (url, base) {
|
URL: (() => {
|
||||||
const u = new (require('url').URL)(url, base);
|
const _URL = createNative('URL', function (url, base) {
|
||||||
Object.assign(this, u);
|
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,
|
URLSearchParams,
|
||||||
|
|
||||||
addEventListener: createNative('addEventListener', function () {}),
|
addEventListener: M('addEventListener', 2, () => {}),
|
||||||
removeEventListener: createNative('removeEventListener', function () {}),
|
removeEventListener: M('removeEventListener', 2, () => {}),
|
||||||
dispatchEvent: createNative('dispatchEvent', function () { return true; }),
|
dispatchEvent: M('dispatchEvent', 1, () => true),
|
||||||
postMessage: createNative('postMessage', function () {}),
|
postMessage: M('postMessage', 1, () => {}),
|
||||||
|
|
||||||
alert: createNative('alert', function () {}),
|
alert: M('alert', 0, () => {}),
|
||||||
confirm: createNative('confirm', function () { return false; }),
|
confirm: M('confirm', 1, () => false),
|
||||||
prompt: createNative('prompt', function () { return null; }),
|
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); }),
|
requestAnimationFrame: M('requestAnimationFrame', 1, (cb) => setTimeout(cb, 16)),
|
||||||
cancelAnimationFrame: createNative('cancelAnimationFrame', function (id) { clearTimeout(id); }),
|
cancelAnimationFrame: M('cancelAnimationFrame', 1, (id) => clearTimeout(id)),
|
||||||
requestIdleCallback: createNative('requestIdleCallback', function (cb) { return setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 1); }),
|
requestIdleCallback: M('requestIdleCallback', 1, (cb) => setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 1)),
|
||||||
cancelIdleCallback: createNative('cancelIdleCallback', function (id) { clearTimeout(id); }),
|
cancelIdleCallback: M('cancelIdleCallback', 1, (id) => clearTimeout(id)),
|
||||||
|
|
||||||
getComputedStyle: createNative('getComputedStyle', function () {
|
getComputedStyle: M('getComputedStyle', 1, () => {
|
||||||
return new Proxy({}, { get: (_, p) => p === 'getPropertyValue' ? (() => '') : '' });
|
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,
|
TextEncoder,
|
||||||
TextDecoder,
|
TextDecoder,
|
||||||
@@ -192,22 +238,158 @@ const _win = {
|
|||||||
Float32Array,
|
Float32Array,
|
||||||
Float64Array,
|
Float64Array,
|
||||||
ArrayBuffer,
|
ArrayBuffer,
|
||||||
|
SharedArrayBuffer,
|
||||||
DataView,
|
DataView,
|
||||||
Map,
|
Map,
|
||||||
Set,
|
Set,
|
||||||
WeakMap,
|
WeakMap,
|
||||||
WeakSet,
|
WeakSet,
|
||||||
|
WeakRef,
|
||||||
Proxy,
|
Proxy,
|
||||||
Reflect,
|
Reflect,
|
||||||
BigInt,
|
BigInt,
|
||||||
|
BigInt64Array,
|
||||||
|
BigUint64Array,
|
||||||
Symbol,
|
Symbol,
|
||||||
WebAssembly,
|
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 字段 + 回填自引用 ────────────────────
|
// ── 建 Proxy:屏蔽 bot 字段 + 回填自引用 ────────────────────
|
||||||
const windowProxy = new Proxy(_win, {
|
const windowProxy = new Proxy(_win, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
if (isBotKey(prop)) return undefined; // 🚨 bot 字段全部返回 undefined
|
if (isBotKey(prop)) return undefined;
|
||||||
const val = target[prop];
|
const val = target[prop];
|
||||||
if (val === null && ['self','window','frames','parent','top','globalThis'].includes(prop)) {
|
if (val === null && ['self','window','frames','parent','top','globalThis'].includes(prop)) {
|
||||||
return windowProxy;
|
return windowProxy;
|
||||||
@@ -215,17 +397,30 @@ const windowProxy = new Proxy(_win, {
|
|||||||
return val;
|
return val;
|
||||||
},
|
},
|
||||||
has(target, prop) {
|
has(target, prop) {
|
||||||
if (isBotKey(prop)) return false; // 拦截 'webdriver' in window
|
if (isBotKey(prop)) return false;
|
||||||
return prop in target;
|
return prop in target;
|
||||||
},
|
},
|
||||||
set(target, prop, val) {
|
set(target, prop, val) {
|
||||||
if (isBotKey(prop)) return true; // 静默丢弃 bot 字段的写入
|
if (isBotKey(prop)) return true;
|
||||||
target[prop] = val;
|
target[prop] = val;
|
||||||
return true;
|
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) {
|
ownKeys(target) {
|
||||||
return Reflect.ownKeys(target).filter(k => !isBotKey(k));
|
return Reflect.ownKeys(target).filter(k => !isBotKey(k));
|
||||||
},
|
},
|
||||||
|
getPrototypeOf() {
|
||||||
|
return CR.Window.prototype;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 回填自引用
|
// 回填自引用
|
||||||
@@ -236,4 +431,7 @@ _win.frames = windowProxy;
|
|||||||
_win.parent = windowProxy;
|
_win.parent = windowProxy;
|
||||||
_win.top = windowProxy;
|
_win.top = windowProxy;
|
||||||
|
|
||||||
|
// global 别名 (some code checks for `global`)
|
||||||
|
_win.global = windowProxy;
|
||||||
|
|
||||||
module.exports = windowProxy;
|
module.exports = windowProxy;
|
||||||
|
|||||||
Reference in New Issue
Block a user