166 lines
7.1 KiB
JavaScript
Executable File
166 lines
7.1 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
const { spawnSync } = require('child_process');
|
||
|
||
function parseArgs(argv) {
|
||
const args = { python: '', json: false, markdown: false };
|
||
for (let i = 2; i < argv.length; i++) {
|
||
const a = argv[i];
|
||
if (a === '--python') args.python = argv[++i] || '';
|
||
else if (a === '--json') args.json = true;
|
||
else if (a === '--markdown') args.markdown = true;
|
||
else if (a === '--help' || a === '-h') args.help = true;
|
||
else throw new Error(`未知参数:${a}`);
|
||
}
|
||
if (!args.json && !args.markdown) args.markdown = true;
|
||
return args;
|
||
}
|
||
|
||
function usage() {
|
||
return `用法:
|
||
node scripts/check_tls_clients.js --markdown
|
||
node scripts/check_tls_clients.js --python python --json
|
||
|
||
说明:检测最终请求阶段可选的 TLS 指纹兼容客户端:Node.js CycleTLS / impers / curl-cffi-node、Python curl_cffi / cffi_curl / cyCronet;真实请求交付时必须再封装为 Session 模式。`;
|
||
}
|
||
|
||
function run(cmd, args, timeout = 12000) {
|
||
const ret = spawnSync(cmd, args, { encoding: 'utf8', timeout, windowsHide: true });
|
||
return {
|
||
ok: ret.status === 0,
|
||
status: ret.status,
|
||
stdout: (ret.stdout || '').trim(),
|
||
stderr: (ret.stderr || '').trim(),
|
||
error: ret.error ? ret.error.message : '',
|
||
command: [cmd].concat(args).join(' '),
|
||
};
|
||
}
|
||
|
||
function nodeRequire(name) {
|
||
const code = `
|
||
(async()=>{
|
||
const name=${JSON.stringify(name)};
|
||
try {
|
||
const m=require(name);
|
||
console.log(JSON.stringify({ok:true, loader:"require", keys:Object.keys(m).slice(0,20)}));
|
||
} catch (e1) {
|
||
try {
|
||
const m=await import(name);
|
||
console.log(JSON.stringify({ok:true, loader:"import", keys:Object.keys(m).slice(0,20)}));
|
||
} catch (e2) {
|
||
console.log(JSON.stringify({ok:false,error:e1.message + " / " + e2.message}));
|
||
process.exit(2);
|
||
}
|
||
}
|
||
})();`;
|
||
const ret = run(process.execPath, ['-e', code]);
|
||
try {
|
||
const parsed = JSON.parse(ret.stdout.replace(/^\uFEFF/, ''));
|
||
return { name, installed: !!parsed.ok, loader: parsed.loader || '', keys: parsed.keys || [], error: parsed.error || '' };
|
||
} catch {
|
||
return { name, installed: false, loader: '', keys: [], error: ret.stderr || ret.error || '检测失败' };
|
||
}
|
||
}
|
||
|
||
function pythonCandidates(explicit) {
|
||
const out = [];
|
||
if (explicit) out.push({ cmd: explicit, prefix: [] });
|
||
out.push({ cmd: 'python', prefix: [] });
|
||
out.push({ cmd: 'python3', prefix: [] });
|
||
out.push({ cmd: 'py', prefix: ['-3'] });
|
||
const seen = new Set();
|
||
return out.filter(x => {
|
||
const k = x.cmd + ' ' + x.prefix.join(' ');
|
||
if (seen.has(k)) return false;
|
||
seen.add(k);
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function detectPythonModules(explicitPython) {
|
||
const modules = ['curl_cffi', 'cffi_curl', 'cycronet', 'cyCronet'];
|
||
const code = [
|
||
'import importlib, json',
|
||
`mods=${JSON.stringify(modules)}`,
|
||
'out={}',
|
||
'for name in mods:',
|
||
' try:',
|
||
' m=importlib.import_module(name)',
|
||
' out[name]={"installed": True, "version": getattr(m, "__version__", "")}',
|
||
' except Exception as e:',
|
||
' out[name]={"installed": False, "error": str(e)}',
|
||
'print(json.dumps(out, ensure_ascii=False))',
|
||
].join('\n');
|
||
for (const c of pythonCandidates(explicitPython)) {
|
||
const ret = run(c.cmd, c.prefix.concat(['-c', code]));
|
||
if (!ret.ok && !ret.stdout) continue;
|
||
try {
|
||
const parsed = JSON.parse(ret.stdout.replace(/^\uFEFF/, ''));
|
||
return { python: c.cmd, pythonArgsPrefix: c.prefix, modules: parsed };
|
||
} catch {}
|
||
}
|
||
return {
|
||
python: '',
|
||
pythonArgsPrefix: [],
|
||
modules: Object.fromEntries(modules.map(m => [m, { installed: false, error: '未找到可用 Python 或检测失败' }])),
|
||
};
|
||
}
|
||
|
||
function detect(args) {
|
||
const nodePackages = ['cycletls', '@luminati-io/cycletls', 'cycle-tls', 'cycleTls', 'impers', 'curl-cffi', 'curl-cffi-node'].map(nodeRequire);
|
||
const python = detectPythonModules(args.python);
|
||
const cycleTlsAvailable = nodePackages.some(p => p.installed && /cycle/i.test(p.name));
|
||
const impersAvailable = nodePackages.some(p => p.installed && p.name === 'impers');
|
||
const curlCffiNodeAvailable = nodePackages.some(p => p.installed && p.name === 'curl-cffi');
|
||
return {
|
||
node: {
|
||
executable: process.execPath,
|
||
packages: nodePackages,
|
||
cycleTlsAvailable,
|
||
impersAvailable,
|
||
curlCffiNodeAvailable,
|
||
nodeTlsAvailable: cycleTlsAvailable || impersAvailable || curlCffiNodeAvailable,
|
||
},
|
||
python,
|
||
pythonTlsAvailable: Object.values(python.modules).some(v => v && v.installed),
|
||
note: '这些客户端只用于授权范围内的最终低频请求验证;如果本 case 需要最终发送真实请求,应在前置阶段先选择 Node.js CycleTLS/impers/curl-cffi-node 或 Python curl_cffi/cyCronet,并在最终项目中封装为 Session 模式,不要等普通客户端失败后才考虑。',
|
||
};
|
||
}
|
||
|
||
function renderMarkdown(result) {
|
||
const lines = ['# TLS 指纹兼容客户端检测结果', '', `说明:${result.note}`, '', '## Node.js / CycleTLS / impers'];
|
||
lines.push(`- Node.js:${result.node.executable}`);
|
||
lines.push(`- 是否检测到 CycleTLS:${result.node.cycleTlsAvailable ? '是' : '否'}`);
|
||
lines.push(`- 是否检测到 impers:${result.node.impersAvailable ? '是' : '否'}`);
|
||
lines.push(`- 是否检测到 Node.js TLS 指纹兼容客户端:${result.node.nodeTlsAvailable ? '是' : '否'}`);
|
||
for (const p of result.node.packages) {
|
||
lines.push(`- ${p.name}:${p.installed ? '已安装' : '未安装'}${p.loader ? `(${p.loader})` : ''}${p.error ? `(${p.error})` : ''}`);
|
||
}
|
||
lines.push('', '## Python / curl_cffi / cffi_curl / cyCronet');
|
||
lines.push(`- Python:${result.python.python ? `${result.python.python} ${result.python.pythonArgsPrefix.join(' ')}`.trim() : '未检测到'}`);
|
||
lines.push(`- 是否检测到 Python TLS 客户端:${result.pythonTlsAvailable ? '是' : '否'}`);
|
||
for (const [name, info] of Object.entries(result.python.modules)) {
|
||
lines.push(`- ${name}:${info.installed ? '已安装' : '未安装'}${info.version ? `(版本 ${info.version})` : ''}${info.error && !info.installed ? `(${info.error})` : ''}`);
|
||
}
|
||
lines.push('', '## 下一步');
|
||
if (!result.node.nodeTlsAvailable && !result.pythonTlsAvailable) {
|
||
lines.push('- 未检测到可用 TLS 指纹兼容客户端;如果本 case 需要最终发送真实请求,需在前置阶段询问用户安装 CycleTLS / impers / curl_cffi / cyCronet,或改为只输出本地 sign 结果。');
|
||
} else {
|
||
lines.push('- 在任务确认阶段选择一个最终请求客户端,并限制为少量授权验证请求;最终项目中只保留所选客户端的 Session 请求逻辑。');
|
||
}
|
||
return lines.join('\n') + '\n';
|
||
}
|
||
|
||
try {
|
||
const args = parseArgs(process.argv);
|
||
if (args.help) { console.log(usage()); process.exit(0); }
|
||
const result = detect(args);
|
||
if (args.json) console.log(JSON.stringify(result, null, 2));
|
||
if (args.markdown) process.stdout.write(renderMarkdown(result));
|
||
} catch (err) {
|
||
console.error(err.message || String(err));
|
||
console.error(usage());
|
||
process.exit(1);
|
||
}
|