specific-ai-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +177 -0
- package/README.ja.md +177 -0
- package/README.ko.md +177 -0
- package/README.md +174 -2
- package/README.zh-TW.md +177 -0
- package/assets/banner.txt +3 -0
- package/dist/cli/argv.js +98 -0
- package/dist/cli/commands/auth.js +11 -0
- package/dist/cli/commands/config.js +23 -0
- package/dist/cli/commands/doctor.js +134 -0
- package/dist/cli/commands/platforms.js +24 -0
- package/dist/cli/commands/version.js +4 -0
- package/dist/cli/confirm.js +22 -0
- package/dist/cli/dispatch.js +440 -0
- package/dist/cli/help.js +223 -0
- package/dist/cli/hints.js +60 -0
- package/dist/cli/main.js +18 -0
- package/dist/cli/output.js +66 -0
- package/dist/core/auth-handlers.js +41 -0
- package/dist/core/auth-store.js +119 -0
- package/dist/core/capabilities.js +163 -0
- package/dist/core/config.js +121 -0
- package/dist/core/cookies.js +164 -0
- package/dist/core/errors.js +37 -0
- package/dist/core/files.js +89 -0
- package/dist/core/fsutil.js +52 -0
- package/dist/core/hash.js +4 -0
- package/dist/core/http.js +331 -0
- package/dist/core/image.js +280 -0
- package/dist/core/log.js +40 -0
- package/dist/core/login.js +191 -0
- package/dist/core/mp4.js +130 -0
- package/dist/core/normalize.js +207 -0
- package/dist/core/options.js +110 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/pb.js +59 -0
- package/dist/core/py.js +181 -0
- package/dist/core/rand.js +131 -0
- package/dist/core/registry.js +129 -0
- package/dist/core/schemas.js +41 -0
- package/dist/core/stream.js +104 -0
- package/dist/core/toolkit.js +35 -0
- package/dist/core/vm.js +73 -0
- package/dist/core/vocab.js +157 -0
- package/dist/platforms/_shared/impaas.js +410 -0
- package/dist/platforms/bilibili/index.js +216 -0
- package/dist/platforms/bilibili/web/api.js +806 -0
- package/dist/platforms/bilibili/web/client.js +311 -0
- package/dist/platforms/bilibili/web/commands.js +783 -0
- package/dist/platforms/bilibili/web/ddddocr.js +342 -0
- package/dist/platforms/bilibili/web/gaia.js +130 -0
- package/dist/platforms/bilibili/web/geetest-manual.js +154 -0
- package/dist/platforms/bilibili/web/geetest-vision.js +232 -0
- package/dist/platforms/bilibili/web/geetest.js +450 -0
- package/dist/platforms/bilibili/web/normalize.js +234 -0
- package/dist/platforms/bilibili/web/profile.js +135 -0
- package/dist/platforms/bilibili/web/resolve.js +112 -0
- package/dist/platforms/bilibili/web/sign.js +223 -0
- package/dist/platforms/bilibili/web/upos.js +102 -0
- package/dist/platforms/douyin/index.js +142 -0
- package/dist/platforms/douyin/web/api.js +863 -0
- package/dist/platforms/douyin/web/client.js +413 -0
- package/dist/platforms/douyin/web/commands.js +712 -0
- package/dist/platforms/douyin/web/creator.js +601 -0
- package/dist/platforms/douyin/web/crypto.js +247 -0
- package/dist/platforms/douyin/web/dtrait.js +190 -0
- package/dist/platforms/douyin/web/im.js +502 -0
- package/dist/platforms/douyin/web/image.js +5 -0
- package/dist/platforms/douyin/web/live.js +225 -0
- package/dist/platforms/douyin/web/mssdk.js +148 -0
- package/dist/platforms/douyin/web/normalize.js +326 -0
- package/dist/platforms/douyin/web/passport.js +1247 -0
- package/dist/platforms/douyin/web/profile.js +221 -0
- package/dist/platforms/douyin/web/proto.js +26 -0
- package/dist/platforms/douyin/web/resolve.js +88 -0
- package/dist/platforms/douyin/web/sign.js +436 -0
- package/dist/platforms/douyin/web/tos.js +78 -0
- package/dist/platforms/index.js +17 -0
- package/dist/platforms/jd/index.js +67 -0
- package/dist/platforms/jd/web/aks.js +115 -0
- package/dist/platforms/jd/web/api.js +549 -0
- package/dist/platforms/jd/web/chat.js +132 -0
- package/dist/platforms/jd/web/client.js +470 -0
- package/dist/platforms/jd/web/commands.js +557 -0
- package/dist/platforms/jd/web/jcap/helper.js +90 -0
- package/dist/platforms/jd/web/jcap/host.js +107 -0
- package/dist/platforms/jd/web/jcap/lsd.js +300 -0
- package/dist/platforms/jd/web/jcap/solver.js +995 -0
- package/dist/platforms/jd/web/jcap.js +150 -0
- package/dist/platforms/jd/web/js.js +420 -0
- package/dist/platforms/jd/web/login.js +775 -0
- package/dist/platforms/jd/web/normalize.js +297 -0
- package/dist/platforms/jd/web/profile.js +256 -0
- package/dist/platforms/jd/web/util.js +175 -0
- package/dist/platforms/jd/web/webm.js +133 -0
- package/dist/platforms/kuaishou/index.js +98 -0
- package/dist/platforms/kuaishou/web/api.js +694 -0
- package/dist/platforms/kuaishou/web/captcha-crypto.js +134 -0
- package/dist/platforms/kuaishou/web/captcha-fp.js +180 -0
- package/dist/platforms/kuaishou/web/captcha.js +273 -0
- package/dist/platforms/kuaishou/web/client.js +487 -0
- package/dist/platforms/kuaishou/web/commands.js +863 -0
- package/dist/platforms/kuaishou/web/gap.js +144 -0
- package/dist/platforms/kuaishou/web/gdfp.js +376 -0
- package/dist/platforms/kuaishou/web/gql.js +11 -0
- package/dist/platforms/kuaishou/web/live.js +125 -0
- package/dist/platforms/kuaishou/web/normalize.js +241 -0
- package/dist/platforms/kuaishou/web/oracle.js +329 -0
- package/dist/platforms/kuaishou/web/profile.js +259 -0
- package/dist/platforms/kuaishou/web/resolve.js +84 -0
- package/dist/platforms/kuaishou/web/session.js +592 -0
- package/dist/platforms/kuaishou/web/sign.js +568 -0
- package/dist/platforms/taobao/index.js +56 -0
- package/dist/platforms/taobao/web/api.js +96 -0
- package/dist/platforms/taobao/web/client.js +102 -0
- package/dist/platforms/taobao/web/commands.js +172 -0
- package/dist/platforms/taobao/web/im.js +68 -0
- package/dist/platforms/taobao/web/normalize.js +103 -0
- package/dist/platforms/taobao/web/profile.js +85 -0
- package/dist/platforms/taobao/web/resolve.js +51 -0
- package/dist/platforms/taobao/web/sign.js +35 -0
- package/dist/platforms/tiktok/index.js +120 -0
- package/dist/platforms/tiktok/web/api.js +1234 -0
- package/dist/platforms/tiktok/web/client.js +362 -0
- package/dist/platforms/tiktok/web/commands.js +821 -0
- package/dist/platforms/tiktok/web/im.js +133 -0
- package/dist/platforms/tiktok/web/jsrun.js +126 -0
- package/dist/platforms/tiktok/web/normalize.js +286 -0
- package/dist/platforms/tiktok/web/profile.js +169 -0
- package/dist/platforms/tiktok/web/resolve.js +156 -0
- package/dist/platforms/tiktok/web/sign.js +585 -0
- package/dist/platforms/tiktok/web/upload.js +805 -0
- package/dist/platforms/tiktok/web/wire.js +490 -0
- package/dist/platforms/weibo/index.js +91 -0
- package/dist/platforms/weibo/web/api.js +206 -0
- package/dist/platforms/weibo/web/client.js +155 -0
- package/dist/platforms/weibo/web/commands.js +202 -0
- package/dist/platforms/weibo/web/normalize.js +137 -0
- package/dist/platforms/weibo/web/profile.js +246 -0
- package/dist/platforms/weibo/web/resolve.js +60 -0
- package/dist/platforms/weibo/web/sign.js +85 -0
- package/dist/platforms/x/index.js +107 -0
- package/dist/platforms/x/web/api.js +443 -0
- package/dist/platforms/x/web/article.js +212 -0
- package/dist/platforms/x/web/client.js +202 -0
- package/dist/platforms/x/web/commands.js +363 -0
- package/dist/platforms/x/web/graphql.js +108 -0
- package/dist/platforms/x/web/normalize.js +235 -0
- package/dist/platforms/x/web/profile.js +99 -0
- package/dist/platforms/x/web/resolve.js +44 -0
- package/dist/platforms/x/web/transaction.js +171 -0
- package/dist/platforms/xhs/index.js +101 -0
- package/dist/platforms/xhs/web/api.js +278 -0
- package/dist/platforms/xhs/web/client.js +540 -0
- package/dist/platforms/xhs/web/commands.js +1046 -0
- package/dist/platforms/xhs/web/creator-api.js +609 -0
- package/dist/platforms/xhs/web/creator.js +203 -0
- package/dist/platforms/xhs/web/js.js +194 -0
- package/dist/platforms/xhs/web/login.js +230 -0
- package/dist/platforms/xhs/web/normalize.js +166 -0
- package/dist/platforms/xhs/web/profile.js +127 -0
- package/dist/platforms/xhs/web/push.js +361 -0
- package/dist/platforms/xhs/web/resolve.js +69 -0
- package/dist/platforms/xhs/web/state.js +368 -0
- package/dist/platforms/xianyu/index.js +59 -0
- package/dist/platforms/xianyu/web/api.js +312 -0
- package/dist/platforms/xianyu/web/client.js +134 -0
- package/dist/platforms/xianyu/web/commands.js +305 -0
- package/dist/platforms/xianyu/web/im.js +73 -0
- package/dist/platforms/xianyu/web/normalize.js +123 -0
- package/dist/platforms/xianyu/web/profile.js +146 -0
- package/dist/platforms/xianyu/web/resolve.js +48 -0
- package/dist/platforms/xianyu/web/sign.js +79 -0
- package/package.json +77 -4
- package/static/douyin/Live.proto +194 -0
- package/static/douyin/Request.proto +89 -0
- package/static/douyin/Response.proto +46 -0
- package/static/douyin/acrawler_runtime/ac_vm.js +2 -0
- package/static/douyin/acrawler_runtime/browser_window_shape.json +1 -0
- package/static/douyin/acrawler_runtime/canvas_actual_exact.json +1 -0
- package/static/douyin/acrawler_runtime/run_ac_node.js +1005 -0
- package/static/douyin/acrawler_runtime/runner_window_keys_baseline.txt +240 -0
- package/static/douyin/challenge_profile.json +801 -0
- package/static/douyin/challenge_template_runner.js +217 -0
- package/static/douyin/dtrait_profile.json +66 -0
- package/static/douyin/mstoken_common_profile.json +1 -0
- package/static/douyin/mstoken_profile.json +214 -0
- package/static/jd/h5st5_env.js +750 -0
- package/static/jd/h5st5_lib.js +5 -0
- package/static/jd/h5st5_server.js +49 -0
- package/static/jd/jcap/env/env_core.js +512 -0
- package/static/jd/jcap/env/run.js +1307 -0
- package/static/jd/jcap/run/jcap_ujb96b.js +39 -0
- package/static/jd/package.json +4 -0
- package/static/jd/pc_tk_lib.js +1 -0
- package/static/jd/pc_tk_server.js +63 -0
- package/static/jd/summer_cryptico_runner.js +50 -0
- package/static/jd/webm/env/run.js +453 -0
- package/static/jd/webm/run/jdwebm-riskhandle.js +49 -0
- package/static/kuaishou/bundles/weapon/kwf-0.0.2.2cee19b4b7dec496.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-0-0.0.1-obfuscated.4b7f3de1ab0ddcc2.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-1-0.0.1-obfuscated.346ebdb1c78df316.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-10-0.0.1-obfuscated.a33db41897b983af.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-12-0.0.1-obfuscated.20ea29345706ec8c.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-13-0.0.1-obfuscated.6b74e9640ff18648.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-14-0.0.1-obfuscated.551e4429d156bf66.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-15-0.0.1-obfuscated.8625b087aaba4a60.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-16-0.0.1-obfuscated.d5ffa4020f3be99a.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-17-0.0.1-obfuscated.6d13e19b22f50a80.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-18-0.0.1-obfuscated.6d31405044166289.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-19-0.0.1-obfuscated.a9a8d925942f19e4.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-2-0.0.1-obfuscated.b849a136ae4a6b3e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-3-0.0.1-obfuscated.f9e77ebf6186e2ac.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-4-0.0.1-obfuscated.2c3c155aa52ceb0e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-5-0.0.1-obfuscated.4e553d17691b3c5a.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-6-0.0.1-obfuscated.f73ddd3ff13e1217.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-7-0.0.1-obfuscated.1628b8729beaab7e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-8-0.0.1-obfuscated.6da6baef7aa81fff.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-9-0.0.1-obfuscated.8645d55e5323b743.js +1 -0
- package/static/kuaishou/fixtures/current_follow_script_813.network-response +2 -0
- package/static/kuaishou/fixtures/live_ws_maps.json +61 -0
- package/static/kuaishou/fixtures/live_ws_proto.json +1342 -0
- package/static/kuaishou/js/cp-kwf.js +1 -0
- package/static/kuaishou/tools/kwf_oracle.js +799 -0
- package/static/kuaishou/tools/kws_oracle.js +885 -0
- package/static/kuaishou/tools/like_token_oracle.js +70 -0
- package/static/taobao/taobao_js_20260407.js +570 -0
- package/static/tiktok/Tiktok_Request.json +597 -0
- package/static/tiktok/package.json +3 -0
- package/static/tiktok/shop_bsid/core.js +1 -0
- package/static/tiktok/shop_bsid/env_core.js +512 -0
- package/static/tiktok/shop_bsid/loader.js +1 -0
- package/static/tiktok/shop_bsid/run.js +753 -0
- package/static/tiktok/shop_bsid/sign.js +100 -0
- package/static/tiktok/shop_bsid/webmssdk.js +1 -0
- package/static/tiktok/shop_bsid/webmssdk_ex.js +1 -0
- package/static/tiktok/signing/env/env_core.js +512 -0
- package/static/tiktok/signing/env/sign.js +455 -0
- package/static/tiktok/signing/sdk_config.json +230 -0
- package/static/tiktok/signing/source/webmssdk.js +1 -0
- package/static/tiktok/signing/source/webmssdk_ex.js +1 -0
- package/static/tiktok/signing/source/webmssdk_legacy.js +1 -0
- package/static/x/graphql.json +7777 -0
- package/static/x/transaction_l1.json +12 -0
- package/static/xhs/package.json +3 -0
- package/static/xhs/xhs_core/js/b1.js +210 -0
- package/static/xhs/xhs_core/js/mns.js +340 -0
- package/static/xhs/xhs_core/js/mns_0101_keystream.json +5 -0
- package/static/xhs/xhs_core/js/mns_keystreams.json +1 -0
- package/static/xhs/xhs_core/js/sign.js +225 -0
- package/static/xhs/xhs_core/js/websectiga_cli.js +220 -0
- package/static/xhs/xhs_core/js/websectiga_env.js +512 -0
- package/static/xhs/xhs_core/js/xs_common.js +79 -0
- package/static/xhs/xhs_creator/js/profile.js +115 -0
- package/static/xhs/xhs_creator/js/rap_fingerprint_creator.json +6 -0
- package/static/xhs/xhs_creator/js/reference_profile.json +282 -0
- package/static/xhs/xhs_creator/js/xhs_creator_sign.js +29 -0
- package/static/xhs/xhs_creator/js/xhs_creator_signature.js +23 -0
- package/static/xhs/xhs_pc/js/aes_decrypt.js +22 -0
- package/static/xhs/xhs_pc/js/aes_encrypt.js +30 -0
- package/static/xhs/xhs_pc/js/b1.js +9 -0
- package/static/xhs/xhs_pc/js/deflate.js +320 -0
- package/static/xhs/xhs_pc/js/mns.js +4 -0
- package/static/xhs/xhs_pc/js/profile.js +141 -0
- package/static/xhs/xhs_pc/js/rap.js +173 -0
- package/static/xhs/xhs_pc/js/rap_cli.js +30 -0
- package/static/xhs/xhs_pc/js/rap_crypto.js +73 -0
- package/static/xhs/xhs_pc/js/rap_fingerprint_template.json +8 -0
- package/static/xhs/xhs_pc/js/reference_profile.json +262 -0
- package/static/xhs/xhs_pc/js/sign.js +6 -0
- package/static/xhs/xhs_pc/js/web_ssk.js +82 -0
- package/static/xhs/xhs_pc/js/xs_common.js +4 -0
- package/static/xianyu/et_f.js +2 -0
- package/static/xianyu/gen_tfstk.js +410 -0
- package/static/xianyu/goofish_js_version_2.js +568 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
// h5st 5.3 运行时的浏览器环境引导。
|
|
2
|
+
// js_security_v3 会读 DOM / navigator / screen / canvas 做指纹,缺一个全局就抛
|
|
3
|
+
// ReferenceError,所以这里把 jsdom 的常用构造函数和对象整体提升到全局。
|
|
4
|
+
const jsdom = require("jsdom");
|
|
5
|
+
const { JSDOM } = jsdom;
|
|
6
|
+
|
|
7
|
+
const resourceLoader = new jsdom.ResourceLoader({
|
|
8
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// 页面 URL 直接进环境采集(location.href / origin / document.referrer 都是采集项),
|
|
12
|
+
// 用裸域名会和真实页面对不上,这里对齐成真实的搜索结果页。
|
|
13
|
+
const PAGE_URL = process.env.JD_PAGE_URL
|
|
14
|
+
|| "https://search.jd.com/Search?keyword=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&enc=utf-8";
|
|
15
|
+
|
|
16
|
+
const domOptions = {
|
|
17
|
+
url: PAGE_URL,
|
|
18
|
+
contentType: "text/html",
|
|
19
|
+
resources: resourceLoader,
|
|
20
|
+
pretendToBeVisual: true,
|
|
21
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36",
|
|
22
|
+
};
|
|
23
|
+
// jsdom 不接受空字符串 referrer,真实搜索页的 document.referrer 恰好就是空
|
|
24
|
+
if (process.env.JD_REFERRER) domOptions.referrer = process.env.JD_REFERRER;
|
|
25
|
+
|
|
26
|
+
const dom = new JSDOM("<!DOCTYPE html><html><head></head><body><p></p></body></html>",
|
|
27
|
+
domOptions);
|
|
28
|
+
|
|
29
|
+
window = dom.window;
|
|
30
|
+
document = window.document;
|
|
31
|
+
self = window;
|
|
32
|
+
globalThis.window = window;
|
|
33
|
+
globalThis.document = document;
|
|
34
|
+
globalThis.self = window;
|
|
35
|
+
|
|
36
|
+
// 把 jsdom window 上的所有属性挂到全局,缺失的构造函数(Element/Node/Event/…)一次补齐
|
|
37
|
+
for (const key of Object.getOwnPropertyNames(window)) {
|
|
38
|
+
if (key in globalThis) continue;
|
|
39
|
+
try {
|
|
40
|
+
const value = window[key];
|
|
41
|
+
if (value !== undefined) globalThis[key] = value;
|
|
42
|
+
} catch (e) { /* 某些属性访问即抛,跳过 */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
navigator = window.navigator;
|
|
46
|
+
location = window.location;
|
|
47
|
+
localStorage = window.localStorage;
|
|
48
|
+
sessionStorage = window.sessionStorage;
|
|
49
|
+
screen = window.screen;
|
|
50
|
+
history = window.history;
|
|
51
|
+
|
|
52
|
+
// jsdom 不实现 canvas,指纹代码会调这两个方法
|
|
53
|
+
try {
|
|
54
|
+
window.HTMLCanvasElement.prototype.getContext = function () {
|
|
55
|
+
return {
|
|
56
|
+
fillRect() {}, fillText() {}, getImageData() { return { data: [] }; },
|
|
57
|
+
measureText() { return { width: 0 }; }, arc() {}, beginPath() {},
|
|
58
|
+
stroke() {}, fill() {}, closePath() {}, rect() {}, save() {},
|
|
59
|
+
restore() {}, translate() {}, rotate() {}, scale() {},
|
|
60
|
+
createLinearGradient() { return { addColorStop() {} }; },
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
window.HTMLCanvasElement.prototype.toDataURL = function () {
|
|
64
|
+
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk";
|
|
65
|
+
};
|
|
66
|
+
} catch (e) { /* ignore */ }
|
|
67
|
+
|
|
68
|
+
// ---------- 指纹面补全 ----------
|
|
69
|
+
// h5st 的 token 要向 cactus 换,请求体里的 expandParams 是一整包环境采集。
|
|
70
|
+
// jsdom 裸环境采出来只有 ~1600 字符,真浏览器约 2900,服务端据此判定 token 可信度:
|
|
71
|
+
// 薄环境照发 token,但用它签的业务请求会被边缘层 403。下面按真实 Chrome 补齐各面。
|
|
72
|
+
const CHROME_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36";
|
|
73
|
+
|
|
74
|
+
function defineAll(target, props) {
|
|
75
|
+
for (const [k, v] of Object.entries(props)) {
|
|
76
|
+
try {
|
|
77
|
+
Object.defineProperty(target, k, {
|
|
78
|
+
get: () => v, configurable: true, enumerable: true,
|
|
79
|
+
});
|
|
80
|
+
} catch (e) { /* 只读属性跳过 */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// navigator:jsdom 默认报自己的 UA/平台,会被指纹逻辑识别
|
|
85
|
+
defineAll(window.navigator, {
|
|
86
|
+
userAgent: CHROME_UA,
|
|
87
|
+
appVersion: CHROME_UA.replace("Mozilla/", ""),
|
|
88
|
+
platform: "Win32",
|
|
89
|
+
vendor: "Google Inc.",
|
|
90
|
+
language: "zh-CN",
|
|
91
|
+
languages: ["zh-CN", "zh", "en", "zh-TW", "ja"],
|
|
92
|
+
// Keep the device surface identical to WebM and JCAP. These three
|
|
93
|
+
// independent risk collectors are correlated by the same JD session.
|
|
94
|
+
hardwareConcurrency: 32,
|
|
95
|
+
deviceMemory: 8,
|
|
96
|
+
maxTouchPoints: 0,
|
|
97
|
+
webdriver: false,
|
|
98
|
+
doNotTrack: null,
|
|
99
|
+
cookieEnabled: true,
|
|
100
|
+
pdfViewerEnabled: true,
|
|
101
|
+
userAgentData: {
|
|
102
|
+
brands: [
|
|
103
|
+
{ brand: "Chromium", version: "152" },
|
|
104
|
+
{ brand: "Not?A_Brand", version: "24" },
|
|
105
|
+
{ brand: "Google Chrome", version: "152" },
|
|
106
|
+
],
|
|
107
|
+
mobile: false,
|
|
108
|
+
platform: "Windows",
|
|
109
|
+
},
|
|
110
|
+
connection: { effectiveType: "4g", rtt: 50, downlink: 10, saveData: false },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 插件与 mimeTypes:Chrome 固定五个内置 PDF 插件
|
|
114
|
+
const PLUGIN_DEFS = [
|
|
115
|
+
["PDF Viewer", "internal-pdf-viewer"],
|
|
116
|
+
["Chrome PDF Viewer", "internal-pdf-viewer"],
|
|
117
|
+
["Chromium PDF Viewer", "internal-pdf-viewer"],
|
|
118
|
+
["Microsoft Edge PDF Viewer", "internal-pdf-viewer"],
|
|
119
|
+
["WebKit built-in PDF", "internal-pdf-viewer"],
|
|
120
|
+
];
|
|
121
|
+
const plugins = PLUGIN_DEFS.map(([name, filename]) => ({
|
|
122
|
+
name, filename, description: "Portable Document Format", length: 2,
|
|
123
|
+
}));
|
|
124
|
+
plugins.item = (i) => plugins[i];
|
|
125
|
+
plugins.namedItem = (n) => plugins.find((p) => p.name === n);
|
|
126
|
+
const mimeTypes = [
|
|
127
|
+
{ type: "application/pdf", suffixes: "pdf", description: "Portable Document Format" },
|
|
128
|
+
{ type: "text/pdf", suffixes: "pdf", description: "Portable Document Format" },
|
|
129
|
+
];
|
|
130
|
+
mimeTypes.item = (i) => mimeTypes[i];
|
|
131
|
+
mimeTypes.namedItem = (n) => mimeTypes.find((m) => m.type === n);
|
|
132
|
+
defineAll(window.navigator, { plugins, mimeTypes });
|
|
133
|
+
|
|
134
|
+
defineAll(window.screen, {
|
|
135
|
+
width: 2560, height: 1440, availWidth: 2560, availHeight: 1392,
|
|
136
|
+
colorDepth: 24, pixelDepth: 24, availLeft: 0, availTop: 0,
|
|
137
|
+
});
|
|
138
|
+
defineAll(window, { devicePixelRatio: 1, outerWidth: 2560, outerHeight: 1392,
|
|
139
|
+
innerWidth: 2560, innerHeight: 1297 });
|
|
140
|
+
|
|
141
|
+
// WebGL:jsdom 完全没有,而指纹会读 VENDOR / RENDERER / 扩展列表
|
|
142
|
+
const GL_PARAMS = {
|
|
143
|
+
7936: "WebKit", 7937: "WebKit WebGL", 7938: "WebGL 1.0 (OpenGL ES 2.0 Chromium)",
|
|
144
|
+
35724: "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)",
|
|
145
|
+
37445: "Google Inc. (NVIDIA)",
|
|
146
|
+
37446: "ANGLE (NVIDIA, NVIDIA GeForce RTX 5060 Ti (0x00002D04) " +
|
|
147
|
+
"Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
148
|
+
3379: 16384, 34930: 16, 35660: 16, 34076: 16384, 36349: 1024, 34024: 16384,
|
|
149
|
+
};
|
|
150
|
+
const GL_EXTENSIONS = [
|
|
151
|
+
"ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_color_buffer_half_float",
|
|
152
|
+
"EXT_float_blend", "EXT_frag_depth", "EXT_shader_texture_lod",
|
|
153
|
+
"EXT_texture_compression_bptc", "EXT_texture_compression_rgtc",
|
|
154
|
+
"EXT_texture_filter_anisotropic", "EXT_sRGB", "OES_element_index_uint",
|
|
155
|
+
"OES_fbo_render_mipmap", "OES_standard_derivatives", "OES_texture_float",
|
|
156
|
+
"OES_texture_float_linear", "OES_texture_half_float",
|
|
157
|
+
"OES_texture_half_float_linear", "OES_vertex_array_object",
|
|
158
|
+
"WEBGL_color_buffer_float", "WEBGL_compressed_texture_s3tc",
|
|
159
|
+
"WEBGL_debug_renderer_info", "WEBGL_debug_shaders", "WEBGL_depth_texture",
|
|
160
|
+
"WEBGL_draw_buffers", "WEBGL_lose_context", "WEBGL_multi_draw",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
function makeWebGLContext() {
|
|
164
|
+
return {
|
|
165
|
+
getParameter: (p) => (p in GL_PARAMS ? GL_PARAMS[p] : 0),
|
|
166
|
+
getSupportedExtensions: () => GL_EXTENSIONS.slice(),
|
|
167
|
+
getExtension: (name) => (name === "WEBGL_debug_renderer_info"
|
|
168
|
+
? { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 }
|
|
169
|
+
: (GL_EXTENSIONS.includes(name) ? {} : null)),
|
|
170
|
+
getShaderPrecisionFormat: () => ({ rangeMin: 127, rangeMax: 127, precision: 23 }),
|
|
171
|
+
getContextAttributes: () => ({ alpha: true, antialias: true, depth: true }),
|
|
172
|
+
canvas: null,
|
|
173
|
+
drawingBufferWidth: 300, drawingBufferHeight: 150,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 字体探测:指纹库靠 canvas measureText 逐个字体量宽度,宽度不同才算「已安装」。
|
|
178
|
+
// 桩若对所有字体返回同一宽度,等于报告一个字体都没有,采集体积会小很多。
|
|
179
|
+
const INSTALLED_FONTS = [
|
|
180
|
+
"Arial", "Arial Black", "Arial Narrow", "Calibri", "Cambria", "Candara",
|
|
181
|
+
"Comic Sans MS", "Consolas", "Constantia", "Corbel", "Courier New",
|
|
182
|
+
"Ebrima", "Franklin Gothic", "Gabriola", "Georgia", "Impact",
|
|
183
|
+
"Lucida Console", "Lucida Sans Unicode", "Malgun Gothic", "Marlett",
|
|
184
|
+
"Microsoft Himalaya", "Microsoft JhengHei", "Microsoft YaHei",
|
|
185
|
+
"Microsoft Sans Serif", "MingLiU", "Mongolian Baiti", "MS Gothic",
|
|
186
|
+
"MV Boli", "Myanmar Text", "Nirmala UI", "Palatino Linotype",
|
|
187
|
+
"Segoe Print", "Segoe Script", "Segoe UI", "SimHei", "SimSun",
|
|
188
|
+
"Sitka", "Sylfaen", "Symbol", "Tahoma", "Times New Roman",
|
|
189
|
+
"Trebuchet MS", "Verdana", "Webdings", "Wingdings", "Yu Gothic",
|
|
190
|
+
"KaiTi", "FangSong", "NSimSun", "DengXian",
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
function fontWidthFactor(fontSpec) {
|
|
194
|
+
const spec = String(fontSpec || "");
|
|
195
|
+
let hit = null;
|
|
196
|
+
for (const f of INSTALLED_FONTS) {
|
|
197
|
+
if (spec.indexOf(f) !== -1) { hit = f; break; }
|
|
198
|
+
}
|
|
199
|
+
if (!hit) return 7.0; // 未安装 → 回落到默认字体宽度
|
|
200
|
+
let h = 0;
|
|
201
|
+
for (let i = 0; i < hit.length; i++) h = (h * 31 + hit.charCodeAt(i)) % 997;
|
|
202
|
+
return 6.2 + (h % 220) / 100;
|
|
203
|
+
}
|
|
204
|
+
const CANVAS_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAAIABJREFUeF7tnQeYFEX6h7/ZnGCXHCXnnBEUFAOKgg";
|
|
205
|
+
try {
|
|
206
|
+
window.HTMLCanvasElement.prototype.getContext = function (type) {
|
|
207
|
+
if (type === "webgl" || type === "webgl2" || type === "experimental-webgl") {
|
|
208
|
+
const ctx = makeWebGLContext();
|
|
209
|
+
ctx.canvas = this;
|
|
210
|
+
return ctx;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
canvas: this,
|
|
214
|
+
font: "10px sans-serif",
|
|
215
|
+
fillRect() {}, clearRect() {}, fillText() {}, strokeText() {},
|
|
216
|
+
beginPath() {}, closePath() {}, moveTo() {}, lineTo() {}, arc() {},
|
|
217
|
+
rect() {}, stroke() {}, fill() {}, save() {}, restore() {},
|
|
218
|
+
translate() {}, rotate() {}, scale() {}, transform() {}, setTransform() {},
|
|
219
|
+
drawImage() {}, putImageData() {}, bezierCurveTo() {}, quadraticCurveTo() {},
|
|
220
|
+
createLinearGradient: () => ({ addColorStop() {} }),
|
|
221
|
+
createRadialGradient: () => ({ addColorStop() {} }),
|
|
222
|
+
createPattern: () => ({}),
|
|
223
|
+
measureText(txt) {
|
|
224
|
+
const s = String(txt || "");
|
|
225
|
+
const w = s.length * fontWidthFactor(this.font);
|
|
226
|
+
return {
|
|
227
|
+
width: w,
|
|
228
|
+
actualBoundingBoxAscent: 8, actualBoundingBoxDescent: 2,
|
|
229
|
+
actualBoundingBoxLeft: 0, actualBoundingBoxRight: w,
|
|
230
|
+
fontBoundingBoxAscent: 9, fontBoundingBoxDescent: 3,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
getImageData: (x, y, w, h) => ({
|
|
234
|
+
width: w, height: h,
|
|
235
|
+
data: new Uint8ClampedArray(Math.max(4, (w | 0) * (h | 0) * 4)),
|
|
236
|
+
}),
|
|
237
|
+
isPointInPath: () => false,
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
window.HTMLCanvasElement.prototype.toDataURL = () => CANVAS_DATA_URL;
|
|
241
|
+
window.HTMLCanvasElement.prototype.toBlob = function (cb) { cb && cb(null); };
|
|
242
|
+
} catch (e) { /* ignore */ }
|
|
243
|
+
|
|
244
|
+
// AudioContext:指纹常用 OfflineAudioContext 跑一段振荡器取哈希
|
|
245
|
+
class FakeAudioParam { constructor(v) { this.value = v; } setValueAtTime() {} }
|
|
246
|
+
class FakeAudioNode {
|
|
247
|
+
connect() { return this; }
|
|
248
|
+
disconnect() {}
|
|
249
|
+
start() {}
|
|
250
|
+
stop() {}
|
|
251
|
+
}
|
|
252
|
+
class FakeAudioContext {
|
|
253
|
+
constructor() {
|
|
254
|
+
this.sampleRate = 48000;
|
|
255
|
+
this.state = "suspended";
|
|
256
|
+
this.destination = new FakeAudioNode();
|
|
257
|
+
this.currentTime = 0;
|
|
258
|
+
}
|
|
259
|
+
createOscillator() {
|
|
260
|
+
const n = new FakeAudioNode();
|
|
261
|
+
n.frequency = new FakeAudioParam(440);
|
|
262
|
+
n.type = "sine";
|
|
263
|
+
return n;
|
|
264
|
+
}
|
|
265
|
+
createGain() {
|
|
266
|
+
const n = new FakeAudioNode();
|
|
267
|
+
n.gain = new FakeAudioParam(1);
|
|
268
|
+
return n;
|
|
269
|
+
}
|
|
270
|
+
createAnalyser() {
|
|
271
|
+
const n = new FakeAudioNode();
|
|
272
|
+
n.frequencyBinCount = 1024;
|
|
273
|
+
n.getFloatFrequencyData = (arr) => arr.fill(-100);
|
|
274
|
+
n.getByteFrequencyData = (arr) => arr.fill(0);
|
|
275
|
+
return n;
|
|
276
|
+
}
|
|
277
|
+
createDynamicsCompressor() {
|
|
278
|
+
const n = new FakeAudioNode();
|
|
279
|
+
for (const k of ["threshold", "knee", "ratio", "attack", "release"]) {
|
|
280
|
+
n[k] = new FakeAudioParam(0);
|
|
281
|
+
}
|
|
282
|
+
return n;
|
|
283
|
+
}
|
|
284
|
+
createScriptProcessor() { return new FakeAudioNode(); }
|
|
285
|
+
createBuffer() { return { getChannelData: () => new Float32Array(128) }; }
|
|
286
|
+
createBufferSource() { return new FakeAudioNode(); }
|
|
287
|
+
startRendering() { return Promise.resolve({ getChannelData: () => new Float32Array(128) }); }
|
|
288
|
+
close() { return Promise.resolve(); }
|
|
289
|
+
resume() { return Promise.resolve(); }
|
|
290
|
+
}
|
|
291
|
+
globalThis.AudioContext = FakeAudioContext;
|
|
292
|
+
globalThis.webkitAudioContext = FakeAudioContext;
|
|
293
|
+
globalThis.OfflineAudioContext = FakeAudioContext;
|
|
294
|
+
window.AudioContext = FakeAudioContext;
|
|
295
|
+
window.OfflineAudioContext = FakeAudioContext;
|
|
296
|
+
|
|
297
|
+
// 其它零散面
|
|
298
|
+
window.chrome = { runtime: {}, app: { isInstalled: false },
|
|
299
|
+
csi: () => ({}), loadTimes: () => ({}) };
|
|
300
|
+
globalThis.chrome = window.chrome;
|
|
301
|
+
if (!window.navigator.permissions) {
|
|
302
|
+
window.navigator.permissions = {
|
|
303
|
+
query: () => Promise.resolve({ state: "prompt", onchange: null }),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (!window.matchMedia) {
|
|
307
|
+
window.matchMedia = (q) => ({ matches: false, media: q,
|
|
308
|
+
addListener() {}, removeListener() {},
|
|
309
|
+
addEventListener() {}, removeEventListener() {} });
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
window.Intl.DateTimeFormat.prototype.resolvedOptions = function () {
|
|
313
|
+
return { timeZone: "Asia/Shanghai", locale: "zh-CN",
|
|
314
|
+
calendar: "gregory", numberingSystem: "latn" };
|
|
315
|
+
};
|
|
316
|
+
} catch (e) { /* ignore */ }
|
|
317
|
+
try { Date.prototype.getTimezoneOffset = function () { return -480; }; } catch (e) {}
|
|
318
|
+
|
|
319
|
+
// DOM 侧字体探测:另一种常见做法是往 span 上套字体再读 offsetWidth。
|
|
320
|
+
// jsdom 不做布局,一律返回 0,同样会被判成「无字体」。
|
|
321
|
+
try {
|
|
322
|
+
const proto = window.HTMLElement.prototype;
|
|
323
|
+
Object.defineProperty(proto, "offsetWidth", {
|
|
324
|
+
configurable: true,
|
|
325
|
+
get() {
|
|
326
|
+
const text = this.textContent || "";
|
|
327
|
+
const font = (this.style && this.style.fontFamily) || "";
|
|
328
|
+
return Math.round(text.length * fontWidthFactor(font)) || 0;
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
Object.defineProperty(proto, "offsetHeight", {
|
|
332
|
+
configurable: true,
|
|
333
|
+
get() {
|
|
334
|
+
const size = parseInt((this.style && this.style.fontSize) || "16", 10);
|
|
335
|
+
return Math.round((size || 16) * 1.2);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
} catch (e) { /* ignore */ }
|
|
339
|
+
|
|
340
|
+
// ---------- 指纹缓存种子 ----------
|
|
341
|
+
// 京东的风控 JS 把采集结果缓存在 localStorage 里,换 token 时一起打进 expandParams:
|
|
342
|
+
// __we_m_gl__ WebGL(vendor / unmasked renderer)
|
|
343
|
+
// __we_m_ft__ 已安装字体列表
|
|
344
|
+
// __we_m_cv__ canvas 指纹
|
|
345
|
+
// jsdom 现算出来的又薄又假,所以注入一份从真机导出的缓存,让库复用真实采集结果。
|
|
346
|
+
//
|
|
347
|
+
// ⚠️ **`WQ_dy1_vk` 是例外,绝对不能从种子里注入。** 它是「每个 appId 对应的 fp」,
|
|
348
|
+
// 也就是 cactus 记信誉分的**身份**。早先为了对齐把浏览器的 fp
|
|
349
|
+
// (`yzz5bivazpzzn227`)钉在种子里,等于和用户的浏览器共用一个身份 ——
|
|
350
|
+
// 我们这边密集打接口把这个 fp 的信誉打坏之后,**浏览器也跟着一起被降权**
|
|
351
|
+
// (实测浏览器自己新铸的 token 同样 403)。换成自己生成的新 fp 后,
|
|
352
|
+
// 待遇立刻从「裸 403(当机器人)」回到「200 + code:605(当浏览器,要验证)」。
|
|
353
|
+
// 不复用浏览器身份,避免脚本与浏览器共享风控信誉。
|
|
354
|
+
//
|
|
355
|
+
// 我们自己生成的 fp 由 token 缓存(`JD_TK_CACHE`)持久化,所以身份是稳定的,
|
|
356
|
+
// 不需要也不应该由种子提供。
|
|
357
|
+
//
|
|
358
|
+
// 同理,**所有「身份/风控态」类的键也一律不注入**。种子只该提供「这台机器长什么样」
|
|
359
|
+
// (WebGL / 字体 / canvas 这些设备能力),不该提供「我是谁、我之前干过什么」。
|
|
360
|
+
// 判据来自实测:用户在无痕窗口清空 jd cookie + 重新登录 + 过一次滑块,搜索立刻恢复
|
|
361
|
+
// —— 说明风控标记就绑在这套身份上。种子里钉着被标记的旧身份,等于一直背着它。
|
|
362
|
+
const IDENTITY_KEYS = new Set([
|
|
363
|
+
"WQ_dy1_vk", "WQ_dy1_tk_algo", // fp 与 token(cactus 记信誉分的身份)
|
|
364
|
+
"3AB9D23F7A4B3C9B", "3AB9D23F7A4B3CSS", // eid 设备号 / jsToken
|
|
365
|
+
"PCA9D23F7A4B3CSS", "PCTSD23F7A4B3CSS", "CA1AN5BV0CA8DS2EPC",
|
|
366
|
+
"shshshfpa", "shshshfpx", "shshshfpb", // 风控指纹三兄弟
|
|
367
|
+
"dra_union_device", // 监控侧设备号
|
|
368
|
+
"JDst_rac_nfd", "JDst_rac_last_update", // 风控任务态
|
|
369
|
+
"JDst_behavior_flag", "JDst_behavior_report_flag",
|
|
370
|
+
"WQ_dy1_gFlag", "WQ_dy1_vFlag", "WQ_dy1_pFlag",
|
|
371
|
+
"AntiDebug_Breaker", "hf_time",
|
|
372
|
+
]);
|
|
373
|
+
try {
|
|
374
|
+
const seedPath = require("path").join(__dirname, "fp_seed.json");
|
|
375
|
+
const fs = require("fs");
|
|
376
|
+
if (fs.existsSync(seedPath)) {
|
|
377
|
+
const seed = JSON.parse(fs.readFileSync(seedPath, "utf-8"));
|
|
378
|
+
for (const [k, v] of Object.entries(seed)) {
|
|
379
|
+
if (IDENTITY_KEYS.has(k)) continue;
|
|
380
|
+
try { window.localStorage.setItem(k, v); } catch (e) { /* ignore */ }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (e) { /* 没有种子就退回现算 */ }
|
|
384
|
+
|
|
385
|
+
// ---------- token 落盘复用 ----------
|
|
386
|
+
// 服务端发的 token 自带 24h 有效期(`WQ_dy1_tk_algo` 里的 `e:86400`),
|
|
387
|
+
// 但 jsdom 的 localStorage 是纯内存的,进程一重启就没了,于是每次冷启动
|
|
388
|
+
// 都得再向 cactus 换一个新的。这有两个坏处:白费一次 cactus 请求;
|
|
389
|
+
// token 可能携带签发时刻的信誉分,
|
|
390
|
+
// 手里那个"干净期"换来的好 token 不该随进程退出一起丢掉。
|
|
391
|
+
// 所以把这个键持久化:启动时读回来,库写进去时同步刷盘。
|
|
392
|
+
const TK_KEYS = ["WQ_dy1_tk_algo", "WQ_dy1_vk"];
|
|
393
|
+
const tkCachePath = process.env.JD_TK_CACHE;
|
|
394
|
+
const tkProfile = process.env.JD_TK_PROFILE || "default";
|
|
395
|
+
if (tkCachePath) {
|
|
396
|
+
const fs = require("fs");
|
|
397
|
+
try {
|
|
398
|
+
if (fs.existsSync(tkCachePath)) {
|
|
399
|
+
const cached = JSON.parse(fs.readFileSync(tkCachePath, "utf-8"));
|
|
400
|
+
const values = cached && cached.profile === tkProfile && cached.values;
|
|
401
|
+
for (const key of TK_KEYS) {
|
|
402
|
+
if (values && values[key]) window.localStorage.setItem(key, values[key]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch (e) { /* 缓存坏了就当没有 */ }
|
|
406
|
+
|
|
407
|
+
// 注意:不能靠 hook `localStorage.setItem` 来落盘 —— 库是用属性赋值
|
|
408
|
+
// (`localStorage[k] = v`)写的,走 jsdom Storage 的 Proxy,根本不经过 setItem。
|
|
409
|
+
// 所以改成「读出来刷盘」,由 h5st5_server.js 在每次签名后调一次。
|
|
410
|
+
let lastDump = "";
|
|
411
|
+
globalThis.__jdFlushTokenCache = () => {
|
|
412
|
+
try {
|
|
413
|
+
const values = {};
|
|
414
|
+
for (const key of TK_KEYS) {
|
|
415
|
+
const v = window.localStorage.getItem(key);
|
|
416
|
+
if (v) values[key] = v;
|
|
417
|
+
}
|
|
418
|
+
const dump = { profile: tkProfile, values };
|
|
419
|
+
const text = JSON.stringify(dump);
|
|
420
|
+
if (text === lastDump || Object.keys(values).length === 0) return;
|
|
421
|
+
const tempPath = tkCachePath + ".tmp";
|
|
422
|
+
fs.writeFileSync(tempPath, text);
|
|
423
|
+
fs.renameSync(tempPath, tkCachePath);
|
|
424
|
+
lastDump = text;
|
|
425
|
+
} catch (e) { /* 写不了就算了,不影响签名 */ }
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------- 环境画像对齐 ----------
|
|
430
|
+
// 库在换 token 前会把采集对象 JSON.stringify 后编码成 expandParams。我们的环境
|
|
431
|
+
// 代码先于库加载,所以这里 patch 到的就是库要用的那个 stringify,可以在编码前
|
|
432
|
+
// 把已知与真实浏览器不一致的字段校准过来。
|
|
433
|
+
//
|
|
434
|
+
// 目标值来自一次真实 Chrome 的实测(在真实搜索页里重新加载同一个库、patch
|
|
435
|
+
// stringify 截获采集对象,appId=f06cc)。几个要点:
|
|
436
|
+
//
|
|
437
|
+
// * bu1 是**故意抛一个 `Error: test err` 后读取的调用栈**,用来看
|
|
438
|
+
// `document.querySelector` 被页面上哪些脚本 hook 过(真页面有 jdwebm.js、
|
|
439
|
+
// js_security_v3_main.js、dra probe 等)。jsdom 里没有这些帧,产出空串——
|
|
440
|
+
// 这一项就占了约 1000 字符,正是 expandParams 体积对不上的主因。
|
|
441
|
+
// * bu2 是另一处调用栈,真浏览器里指向 js_security 自身。
|
|
442
|
+
// * extend.bu3/bu6 等是各项检测的计数结果,jsdom 下与真机不同。
|
|
443
|
+
const REAL_STACK_BU1 = [
|
|
444
|
+
"Error: test err",
|
|
445
|
+
" at HTMLDocument._$Pi (https://storage.360buyimg.com/webcontainer/js_security_v3_0.1.6.js?v=2024-06-20-17:5:9219)",
|
|
446
|
+
" at document.querySelector (https://storage.360buyimg.com/jsresource/ws_js/jdwebm.js?v=pcSearch:1:68279)",
|
|
447
|
+
" at HTMLDocument.querySelector (https://storage.360buyimg.com/webcontainer/main/js_security_v3_main.js?v=20260816:5:8919)",
|
|
448
|
+
" at https://storage.360buyimg.com/dev-static/dra/probe-web/1.2.3/browser.js:1:114252",
|
|
449
|
+
" at f (https://storage.360buyimg.com/dev-static/dra/probe-web/1.2.3/browser.js:1:60440)",
|
|
450
|
+
" at Generator.<anonymous> (https://storage.360buyimg.com/dev-static/dra/probe-web/1.2.3/browser.js:1:61762)",
|
|
451
|
+
" at Generator.next (https://storage.360buyimg.com/dev-static/dra/probe-web/1.2.3/browser.js:1:60848)",
|
|
452
|
+
" at fE (https://storage.360buyimg.com/dev-static/dra/probe-web/1.2.3/browser.js:1:66308)",
|
|
453
|
+
" at a (https://storage.360buyimg.com/dev-static/dra/probe-web/1.2.3/browser.js:1:66502)",
|
|
454
|
+
" at https://storage.360buyimg.com/dev-static/dra/probe-web/1.2.3/browser.js:1:66563",
|
|
455
|
+
].join("\n");
|
|
456
|
+
|
|
457
|
+
const REAL_STACK_BU2 =
|
|
458
|
+
" at https://storage.360buyimg.com/webcontainer/js_security_v3_0.1.6.js"
|
|
459
|
+
+ "?v=2024-06-20-17:5:71875";
|
|
460
|
+
|
|
461
|
+
const ENV_PROFILE = {
|
|
462
|
+
wk: 0,
|
|
463
|
+
bu1: REAL_STACK_BU1,
|
|
464
|
+
bu2: REAL_STACK_BU2,
|
|
465
|
+
ccn: 20,
|
|
466
|
+
extend: {
|
|
467
|
+
wk: 0, l: 0, ls: 5, wd: 0,
|
|
468
|
+
bu3: 105, bu4: 0, bu5: 0, bu6: 21, bu7: 0, bu8: 0,
|
|
469
|
+
bu10: 14, bu11: 4, bu12: -8,
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
function alignEnv(env) {
|
|
474
|
+
try {
|
|
475
|
+
for (const [k, v] of Object.entries(ENV_PROFILE)) {
|
|
476
|
+
if (k === "extend") continue;
|
|
477
|
+
env[k] = v;
|
|
478
|
+
}
|
|
479
|
+
if (env.extend && typeof env.extend === "object") {
|
|
480
|
+
Object.assign(env.extend, ENV_PROFILE.extend);
|
|
481
|
+
}
|
|
482
|
+
// 真浏览器里这两个 canvas 指纹是同一个值:canvas 取自缓存、canvas1 现算,
|
|
483
|
+
// 二者不一致同样是破绽。
|
|
484
|
+
if (env.canvas) env.canvas1 = env.canvas;
|
|
485
|
+
} catch (e) { /* ignore */ }
|
|
486
|
+
return env;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------- 签名串环境对齐(h5st 第 7 段)----------
|
|
490
|
+
// 这里有个容易踩的坑:库有**两个**环境采集对象,字段名重叠但语义不同。
|
|
491
|
+
// 1. 换 token 时 POST 给 cactus 的 expandParams —— 有 `ai`(appId),30 余项,
|
|
492
|
+
// `extend.bu3` 是个上百的计数。
|
|
493
|
+
// 2. 每次签名时编码进 h5st 第 7 段的采集对象 —— 没有 `ai`,只 20 余项,
|
|
494
|
+
// `extend.bu3` 是 0/1 的布尔位。
|
|
495
|
+
// 之前只校准了 ①,②一直是 jsdom 的原样输出,而边缘层校验的恰好是 ②
|
|
496
|
+
// (实测:把浏览器可用签名的第 7/8 段换成我们的,立刻 403)。
|
|
497
|
+
//
|
|
498
|
+
// 参照值取自真实 Chrome:在**真实搜索页里重新注入同一个库**(先 patch 页面的
|
|
499
|
+
// JSON.stringify 再注入),这样库既用我们的 stringify、又能看到完整页面环境。
|
|
500
|
+
// 早先在 about:blank iframe 里抓的参照是错的——那里没有页面的风控脚本,
|
|
501
|
+
// bu3/bu6 会退化成 1/-1,照它对齐反而对不上现网。
|
|
502
|
+
//
|
|
503
|
+
// 下列字段是逐项对齐网页环境后保留的必要采集项:
|
|
504
|
+
//
|
|
505
|
+
// wk 64 → 0 ★校验。检测位掩码,非 0 = 发现运行环境被篡改
|
|
506
|
+
// bu4 1 → 0 ★校验。同类检测位
|
|
507
|
+
// bu8 1 → 0 ★校验。同类检测位
|
|
508
|
+
// bu3 1 → 105 ★校验,但**不校验具体值**:7 / 43 / 105 都通,
|
|
509
|
+
// 只有 jsdom 的 1 不通。它是页面上被 hook 的函数计数
|
|
510
|
+
// ls -1 → 5 不校验。我们确实声明了 5 个语言,纠正它只是让画像自洽
|
|
511
|
+
// bu6 -1 不校验,且各站不同(search=22 / www=29)→ **不写死**
|
|
512
|
+
// bu13 "Fd" 不校验,取自各 origin 自己的 localStorage → **不写死**
|
|
513
|
+
//
|
|
514
|
+
// 所以只按需校准,不去复刻某个站点的完整画像 —— 站点相关的值会随京东前端
|
|
515
|
+
// 发版漂移,写死等于给自己埋雷。
|
|
516
|
+
//
|
|
517
|
+
// 注意:这个对象**没有**顶层 wk,只有 extend.wk。多加一个键会改变 JSON 形状,
|
|
518
|
+
// 第 7 段跟着变长,照样过不了校验。
|
|
519
|
+
const SIGN_ENV_EXTEND = {
|
|
520
|
+
wk: 0, l: 0, wd: 0, ls: 5,
|
|
521
|
+
bu3: 105, bu4: 0, bu6: 22, bu8: 0,
|
|
522
|
+
bu13: "M1,Fd",
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// `JD_SIGN_ENV_OVERRIDE` 收一段 JSON,覆盖 extend 里的任意项,用来试新画像。
|
|
526
|
+
function alignSignEnv(env) {
|
|
527
|
+
if (process.env.JD_NO_ALIGN) return env;
|
|
528
|
+
try {
|
|
529
|
+
if (env.extend && typeof env.extend === "object") {
|
|
530
|
+
Object.assign(env.extend, SIGN_ENV_EXTEND);
|
|
531
|
+
if (process.env.JD_SIGN_ENV_OVERRIDE) {
|
|
532
|
+
Object.assign(env.extend,
|
|
533
|
+
JSON.parse(process.env.JD_SIGN_ENV_OVERRIDE));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} catch (e) { /* ignore */ }
|
|
537
|
+
return env;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
{
|
|
541
|
+
const origStringify = JSON.stringify;
|
|
542
|
+
const debugPath = process.env.JD_DEBUG_ENV;
|
|
543
|
+
const dump = (suffix, o) => {
|
|
544
|
+
if (!debugPath) return;
|
|
545
|
+
try {
|
|
546
|
+
require("fs").writeFileSync(debugPath + suffix, origStringify(o, null, 1));
|
|
547
|
+
} catch (e) { /* ignore */ }
|
|
548
|
+
};
|
|
549
|
+
JSON.stringify = function (o, ...rest) {
|
|
550
|
+
try {
|
|
551
|
+
if (o && typeof o === "object" && "fp" in o && "extend" in o) {
|
|
552
|
+
if ("ai" in o) {
|
|
553
|
+
dump(".cactus.json", alignEnv(o));
|
|
554
|
+
} else {
|
|
555
|
+
dump(".sign.json", alignSignEnv(o));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} catch (e) { /* ignore */ }
|
|
559
|
+
return origStringify.apply(this, [o, ...rest]);
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ---------- 反自动化检测点 ----------
|
|
564
|
+
// 实测采集对象里有两项直接暴露了运行环境:
|
|
565
|
+
// bu2 = 某个 Error 的调用栈 → jsdom 下会漏出 "at listOnTimeout (node:internal/timers…)"
|
|
566
|
+
// bu3 = Window.toString() → jsdom 会漏出 "function Window() { throw new TypeError… }"
|
|
567
|
+
// 真浏览器分别是浏览器帧和 "function Window() { [native code] }"。
|
|
568
|
+
try {
|
|
569
|
+
const nativeWindow = "function Window() { [native code] }";
|
|
570
|
+
window.Window.toString = () => nativeWindow;
|
|
571
|
+
Object.defineProperty(window.Window, "toString", {
|
|
572
|
+
value: () => nativeWindow, writable: true, configurable: true,
|
|
573
|
+
});
|
|
574
|
+
} catch (e) { /* ignore */ }
|
|
575
|
+
|
|
576
|
+
// 让所有 Error 栈看起来像浏览器里的栈
|
|
577
|
+
try {
|
|
578
|
+
Error.prepareStackTrace = function (err, frames) {
|
|
579
|
+
const head = `${err.name}: ${err.message}`;
|
|
580
|
+
const lines = frames.slice(0, 6).map((f, i) => {
|
|
581
|
+
const fn = f.getFunctionName();
|
|
582
|
+
const loc = `${PAGE_URL}:${100 + i * 7}:${10 + i * 3}`;
|
|
583
|
+
return fn ? ` at ${fn} (${loc})` : ` at ${loc}`;
|
|
584
|
+
});
|
|
585
|
+
return [head].concat(lines).join("\n");
|
|
586
|
+
};
|
|
587
|
+
} catch (e) { /* ignore */ }
|
|
588
|
+
|
|
589
|
+
// 原生函数的 toString 不该暴露实现体;patch 本身也要自我隐藏,
|
|
590
|
+
// 否则 bu3 会漏出这段补丁的源码(bu3 = Window.toString() + "$" +
|
|
591
|
+
// Function.prototype.toString.toString())。
|
|
592
|
+
try {
|
|
593
|
+
const origFnToString = Function.prototype.toString;
|
|
594
|
+
const NATIVE = new WeakSet();
|
|
595
|
+
function toString() {
|
|
596
|
+
if (NATIVE.has(this)) {
|
|
597
|
+
return `function ${this.name || ""}() { [native code] }`;
|
|
598
|
+
}
|
|
599
|
+
return origFnToString.call(this);
|
|
600
|
+
}
|
|
601
|
+
NATIVE.add(toString);
|
|
602
|
+
NATIVE.add(origFnToString);
|
|
603
|
+
for (const fn of [window.HTMLCanvasElement.prototype.getContext,
|
|
604
|
+
window.HTMLCanvasElement.prototype.toDataURL,
|
|
605
|
+
window.navigator.permissions && window.navigator.permissions.query,
|
|
606
|
+
window.matchMedia]) {
|
|
607
|
+
if (typeof fn === "function") NATIVE.add(fn);
|
|
608
|
+
}
|
|
609
|
+
Function.prototype.toString = toString;
|
|
610
|
+
} catch (e) { /* ignore */ }
|
|
611
|
+
|
|
612
|
+
// ---------- 干净 iframe 探针 ----------
|
|
613
|
+
// 库会 appendChild 一个 iframe,然后从 contentWindow 里取「未被 hook 的原生实现」
|
|
614
|
+
// 与主 window 的对比 —— 这是很常见的反 hook 手法。jsdom 的 iframe 是另一个
|
|
615
|
+
// 独立 realm,上面所有补丁都不在,于是逐项露馅:
|
|
616
|
+
// contentWindow.navigator.languages ["en-US","en"](主窗口是 zh-CN,…)
|
|
617
|
+
// contentWindow.Window.toString() jsdom 的 throw new TypeError 桩
|
|
618
|
+
// contentWindow.HTMLCanvasElement.prototype.getContext jsdom 源码
|
|
619
|
+
// 结果就是采集对象里 wk(检测位掩码)非 0、bu4/bu6/bu8 被置成「已检测到篡改」。
|
|
620
|
+
// 让 contentWindow / contentDocument 直接指回主窗口,探针取到的就和主窗口一致,
|
|
621
|
+
// 对比自然相等。
|
|
622
|
+
try {
|
|
623
|
+
const iframeProto = window.HTMLIFrameElement.prototype;
|
|
624
|
+
Object.defineProperty(iframeProto, "contentWindow", {
|
|
625
|
+
configurable: true, get() { return window; },
|
|
626
|
+
});
|
|
627
|
+
Object.defineProperty(iframeProto, "contentDocument", {
|
|
628
|
+
configurable: true, get() { return window.document; },
|
|
629
|
+
});
|
|
630
|
+
} catch (e) { /* ignore */ }
|
|
631
|
+
|
|
632
|
+
// document.cookie:采集项 pp 会从 cookie 里读登录 pin(浏览器是 {"p2":"<pin>"})。
|
|
633
|
+
// 只把 cookie 给 XHR 是不够的,jsdom 的 document.cookie 仍然是空的,pp 就成了 {}。
|
|
634
|
+
// 注意要放在 JD_COOKIE 声明之后。
|
|
635
|
+
indexedDB = window.indexedDB || {};
|
|
636
|
+
console.log = () => {};
|
|
637
|
+
console.warn = () => {};
|
|
638
|
+
console.error = () => {};
|
|
639
|
+
|
|
640
|
+
// ---------- 真网 XHR ----------
|
|
641
|
+
// h5st 5.3 的签名 token 不是本地算的:库会 POST cactus.jd.com/request_algo,
|
|
642
|
+
// 服务端回一个 tk 和一段 algo 函数(内含随机盐 rd),签名 = SHA256(tk+fp+ts+appId+rd)。
|
|
643
|
+
// jsdom 自带的 XHR 到不了真网,库就会退回本地兜底 token,而兜底 token 服务端不认
|
|
644
|
+
// (PC 商品/搜索直接 403)。所以这里用 node https 实现一个能带 JD cookie 的 XHR。
|
|
645
|
+
const https = require("https");
|
|
646
|
+
const { URL } = require("url");
|
|
647
|
+
|
|
648
|
+
const JD_COOKIE = process.env.JD_COOKIE || "";
|
|
649
|
+
|
|
650
|
+
// 采集项 pp 会从 cookie 里读登录 pin(浏览器是 {"p2":"<pin>"})。只把 cookie 给
|
|
651
|
+
// XHR 不够——jsdom 的 document.cookie 仍是空的,pp 就成了 {},环境一看就是假的。
|
|
652
|
+
try {
|
|
653
|
+
for (const pair of JD_COOKIE.split(";")) {
|
|
654
|
+
const item = pair.trim();
|
|
655
|
+
if (!item || item.indexOf("=") === -1) continue;
|
|
656
|
+
try { window.document.cookie = item + "; path=/"; } catch (e) { /* skip */ }
|
|
657
|
+
}
|
|
658
|
+
} catch (e) { /* ignore */ }
|
|
659
|
+
const JD_ORIGIN = process.env.JD_ORIGIN || "https://search.jd.com";
|
|
660
|
+
const JD_REFERER = process.env.JD_REFERER || "https://search.jd.com/";
|
|
661
|
+
|
|
662
|
+
class NodeXHR {
|
|
663
|
+
constructor() {
|
|
664
|
+
this.readyState = 0;
|
|
665
|
+
this.status = 0;
|
|
666
|
+
this.responseText = "";
|
|
667
|
+
this.response = "";
|
|
668
|
+
this.timeout = 0;
|
|
669
|
+
this.withCredentials = false;
|
|
670
|
+
this._headers = {};
|
|
671
|
+
this._respHeaders = "";
|
|
672
|
+
}
|
|
673
|
+
open(method, url) {
|
|
674
|
+
this._method = (method || "GET").toUpperCase();
|
|
675
|
+
this._url = url;
|
|
676
|
+
this.readyState = 1;
|
|
677
|
+
}
|
|
678
|
+
setRequestHeader(k, v) { this._headers[k] = v; }
|
|
679
|
+
getAllResponseHeaders() { return this._respHeaders; }
|
|
680
|
+
getResponseHeader(k) {
|
|
681
|
+
const m = new RegExp("^" + k + ": (.*)$", "im").exec(this._respHeaders);
|
|
682
|
+
return m ? m[1] : null;
|
|
683
|
+
}
|
|
684
|
+
abort() {}
|
|
685
|
+
_done() {
|
|
686
|
+
this.readyState = 4;
|
|
687
|
+
if (this.onreadystatechange) this.onreadystatechange();
|
|
688
|
+
if (this.onload) this.onload();
|
|
689
|
+
}
|
|
690
|
+
send(data) {
|
|
691
|
+
let u;
|
|
692
|
+
try { u = new URL(this._url, JD_ORIGIN); } catch (e) { this.status = 0; this._done(); return; }
|
|
693
|
+
const headers = Object.assign({
|
|
694
|
+
// 换 token 那一发请求本身也会被打量,头要和浏览器一致。
|
|
695
|
+
// 注意 Accept-Encoding 用 identity:这里不打算再实现一遍解压。
|
|
696
|
+
"sec-ch-ua": '"Chromium";v="152", "Not?A_Brand";v="24", "Google Chrome";v="152"',
|
|
697
|
+
"sec-ch-ua-mobile": "?0",
|
|
698
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
699
|
+
"User-Agent": window.navigator.userAgent,
|
|
700
|
+
"Accept": "application/json",
|
|
701
|
+
"Accept-Encoding": "identity",
|
|
702
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ja;q=0.6",
|
|
703
|
+
"Origin": JD_ORIGIN,
|
|
704
|
+
"Referer": JD_REFERER,
|
|
705
|
+
"priority": "u=1, i",
|
|
706
|
+
"sec-fetch-dest": "empty",
|
|
707
|
+
"sec-fetch-mode": "cors",
|
|
708
|
+
"sec-fetch-site": "same-site",
|
|
709
|
+
}, this._headers);
|
|
710
|
+
if (JD_COOKIE) headers["Cookie"] = JD_COOKIE;
|
|
711
|
+
if (data && !headers["Content-Type"] && !headers["content-type"]) {
|
|
712
|
+
headers["Content-Type"] = "application/json";
|
|
713
|
+
}
|
|
714
|
+
const req = https.request({
|
|
715
|
+
method: this._method,
|
|
716
|
+
hostname: u.hostname,
|
|
717
|
+
path: u.pathname + u.search,
|
|
718
|
+
headers,
|
|
719
|
+
}, (res) => {
|
|
720
|
+
const chunks = [];
|
|
721
|
+
res.on("data", (d) => chunks.push(d));
|
|
722
|
+
res.on("end", () => {
|
|
723
|
+
this.status = res.statusCode;
|
|
724
|
+
this.responseText = Buffer.concat(chunks).toString("utf8");
|
|
725
|
+
this.response = this.responseText;
|
|
726
|
+
this._respHeaders = Object.entries(res.headers)
|
|
727
|
+
.map(([k, v]) => `${k}: ${v}`).join("\r\n");
|
|
728
|
+
if (process.env.JD_DEBUG) {
|
|
729
|
+
require("fs").appendFileSync(process.env.JD_DEBUG,
|
|
730
|
+
JSON.stringify({
|
|
731
|
+
url: String(u), status: this.status,
|
|
732
|
+
reqBody: data ? String(data) : null,
|
|
733
|
+
resp: this.responseText.slice(0, 1500),
|
|
734
|
+
}) + "\n");
|
|
735
|
+
}
|
|
736
|
+
this._done();
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
req.on("error", () => {
|
|
740
|
+
this.status = 0;
|
|
741
|
+
if (this.onerror) this.onerror();
|
|
742
|
+
this._done();
|
|
743
|
+
});
|
|
744
|
+
if (data) req.write(data);
|
|
745
|
+
req.end();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
window.XMLHttpRequest = NodeXHR;
|
|
750
|
+
globalThis.XMLHttpRequest = NodeXHR;
|