初步增加, 扫码登录成功
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 在 jsdom 中执行 goods.shtml 的原始脚本并导出 fp-behv 参数。
|
||||
*
|
||||
* 该脚本只生成 DeviceFP,不会提交 fp-behv 或 web_save。
|
||||
* 调用方必须把同一份 goods HTML、goods URL 与生成的 fp 参数配对使用。
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { JSDOM, ResourceLoader } from 'jsdom';
|
||||
import { patchEnvironment } from './jsdom-patch-env.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const argv = process.argv.slice(2);
|
||||
const getArg = (name, fallback = '') => {
|
||||
const index = argv.indexOf(name);
|
||||
return index === -1 ? fallback : argv[index + 1];
|
||||
};
|
||||
|
||||
const htmlPath = getArg('--html');
|
||||
const goodsUrl = getArg('--goods-url');
|
||||
const cookiePath = getArg('--cookies');
|
||||
const outputPath = getArg('--output');
|
||||
const waitMs = Number.parseInt(getArg('--wait', '12000'), 10);
|
||||
const useArchivedAssets = argv.includes('--archived-assets');
|
||||
|
||||
function fail(message) {
|
||||
console.error(`generate-devicefp-jsdom: ${message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (!htmlPath || !goodsUrl || !outputPath) {
|
||||
fail('用法: node scripts/generate-devicefp-jsdom.mjs --html <goods.html> --goods-url <URL> --output <fp.json> [--cookies <session.json>]');
|
||||
}
|
||||
if (!fs.existsSync(htmlPath)) fail(`HTML 不存在: ${htmlPath}`);
|
||||
if (cookiePath && !fs.existsSync(cookiePath)) fail(`会话文件不存在: ${cookiePath}`);
|
||||
|
||||
const archived = {
|
||||
'vendor.': path.join(ROOT, 'replay', 'vendor.js'),
|
||||
'cgiVendor.': path.join(ROOT, 'replay', 'cgiVendor.js'),
|
||||
'goodsBiz.': path.join(ROOT, 'replay', 'goodsBiz.js'),
|
||||
'goods.': path.join(ROOT, 'replay', 'goods.js'),
|
||||
};
|
||||
|
||||
class GoodsLoader extends ResourceLoader {
|
||||
fetch(url, options) {
|
||||
if (!useArchivedAssets) return super.fetch(url, options);
|
||||
try {
|
||||
const file = new URL(url).pathname.split('/').pop();
|
||||
for (const [prefix, localPath] of Object.entries(archived)) {
|
||||
if (file.startsWith(prefix)) return Promise.resolve(fs.readFileSync(localPath));
|
||||
}
|
||||
} catch {
|
||||
// jsdom will surface resource errors through its virtual console.
|
||||
}
|
||||
return Promise.resolve(Buffer.from(''));
|
||||
}
|
||||
}
|
||||
|
||||
const session = cookiePath ? JSON.parse(fs.readFileSync(cookiePath, 'utf8')) : {};
|
||||
const cookies = session.cookies || {};
|
||||
const captured = [];
|
||||
const resourceErrors = [];
|
||||
const dom = new JSDOM(fs.readFileSync(htmlPath, 'utf8'), {
|
||||
url: goodsUrl,
|
||||
referrer: 'https://z.iwan.yyb.qq.com/',
|
||||
pretendToBeVisual: true,
|
||||
runScripts: 'dangerously',
|
||||
resources: new GoodsLoader(),
|
||||
beforeParse(window) {
|
||||
patchEnvironment(window);
|
||||
for (const [name, value] of Object.entries(cookies)) {
|
||||
if (value) window.document.cookie = `${name}=${value}`;
|
||||
}
|
||||
|
||||
// goods boot only needs an accepted fp-behv response to continue. Keep all
|
||||
// traffic local so this process cannot accidentally submit a payment.
|
||||
const NativeXHR = window.XMLHttpRequest;
|
||||
window.XMLHttpRequest = class extends NativeXHR {
|
||||
open(method, url, async = true) {
|
||||
this.__yybMethod = method;
|
||||
this.__yybUrl = String(url);
|
||||
return super.open(method, url, async);
|
||||
}
|
||||
|
||||
send(body) {
|
||||
const requestBody = String(body || '');
|
||||
if (this.__yybUrl.includes('fp-behv.fcg')) {
|
||||
captured.push({ url: this.__yybUrl, body: requestBody });
|
||||
}
|
||||
this.readyState = 4;
|
||||
this.status = 200;
|
||||
this.responseText = '{"ret":0,"msg":"","interval":10,"page_num":8,"report_flag":0}';
|
||||
if (typeof this.onreadystatechange === 'function') this.onreadystatechange();
|
||||
if (typeof this.onload === 'function') this.onload();
|
||||
}
|
||||
};
|
||||
window.addEventListener('error', event => {
|
||||
resourceErrors.push(String(event.error || event.message || 'unknown error'));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, Number.isFinite(waitMs) ? waitMs : 12000));
|
||||
const fp = captured.at(-1);
|
||||
if (!fp) {
|
||||
dom.window.close();
|
||||
fail(`未生成 fp-behv;加载错误: ${resourceErrors.slice(0, 3).join(' | ') || '无'}`);
|
||||
}
|
||||
|
||||
const values = Object.fromEntries(new URLSearchParams(fp.body));
|
||||
if (!values.SessionID || !values.DeviceFP) {
|
||||
dom.window.close();
|
||||
fail('fp-behv 缺少 SessionID 或 DeviceFP');
|
||||
}
|
||||
const result = {
|
||||
generated_at: new Date().toISOString(),
|
||||
goods_url: goodsUrl,
|
||||
fp_url: new URL(fp.url, goodsUrl).href,
|
||||
fp_body: fp.body,
|
||||
session_id: values.SessionID,
|
||||
device_fp_length: values.DeviceFP.length,
|
||||
archived_assets: useArchivedAssets,
|
||||
};
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2) + '\n');
|
||||
dom.window.close();
|
||||
console.log(`DeviceFP 已保存: ${outputPath}`);
|
||||
console.log(`SessionID: ${result.session_id}`);
|
||||
console.log(`DeviceFP 长度: ${result.device_fp_length}`);
|
||||
Reference in New Issue
Block a user