Files

363 lines
14 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
function parseArgs(argv) {
const args = { caseDir: '', target: '', entry: '', param: '', api: '', force: false, json: false };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--case-dir' || a === '--dir' || a === '-d') args.caseDir = argv[++i] || '';
else if (a === '--target') args.target = argv[++i] || '';
else if (a === '--entry') args.entry = argv[++i] || '';
else if (a === '--param') args.param = argv[++i] || '';
else if (a === '--api') args.api = argv[++i] || '';
else if (a === '--force') args.force = true;
else if (a === '--json') args.json = true;
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
return args;
}
function usage() {
return `用法:
node scripts/init_env_case.js --case-dir case --target app.js --entry window.makeSign --param sign --api https://example.com/api/search
说明:已有文件默认不覆盖;如需覆盖模板文件,请显式添加 --force。`;
}
function isDangerousDir(p) {
const resolved = path.resolve(p);
const root = path.parse(resolved).root;
return resolved === root || resolved.length <= root.length + 2;
}
function ensureDir(p, actions) {
if (!fs.existsSync(p)) {
fs.mkdirSync(p, { recursive: true });
actions.push({ action: 'create-dir', path: p });
} else {
actions.push({ action: 'keep-dir', path: p });
}
}
function writeFileIfNeeded(p, content, force, actions) {
if (fs.existsSync(p) && !force) {
actions.push({ action: 'keep-file', path: p });
return;
}
fs.mkdirSync(path.dirname(p), { recursive: true });
const existed = fs.existsSync(p);
fs.writeFileSync(p, content, 'utf8');
actions.push({ action: existed ? 'write-file' : 'create-file', path: p });
}
function initCase(args) {
if (!args.caseDir) throw new Error('必须提供 --case-dir');
const caseDir = path.resolve(args.caseDir);
if (isDangerousDir(caseDir)) throw new Error(`拒绝在危险目录中初始化:${caseDir}`);
const actions = [];
const dirs = ['js/original', 'js/pretty', 'js/extracted', 'requests', 'fixtures', 'notes', 'hooks', 'env', 'ruyi-trace/logs', 'browser/ruyipage', 'result', 'tmp'];
ensureDir(caseDir, actions);
for (const d of dirs) ensureDir(path.join(caseDir, d), actions);
const fixture = {
name: 'sample-001',
pageUrl: '',
apiUrl: args.api || '',
method: '',
param: args.param || '',
paramLocation: '',
browser: {
userAgent: '',
language: 'zh-CN',
timezone: 'Asia/Shanghai',
screen: { width: 1920, height: 1080 },
},
request: { query: {}, headers: {}, body: {}, cookies: {}, localStorage: {}, sessionStorage: {} },
runtime: { now: null, performanceNow: null, randomBytes: [] },
fingerprint: {
source: { mode: '', pageUrl: '', userAgent: '', timezone: '', locale: '', capturedAt: '' },
canvas: { toDataURL: [], toBlob: [], measureText: [], getImageData: [] },
webgl: { getParameter: [], getSupportedExtensions: null, getExtension: [], getShaderPrecisionFormat: [], readPixels: [] },
webgpu: { requestAdapter: [] },
audio: { startRendering: [], getChannelData: [] },
domGeometry: { getBoundingClientRect: [], getClientRects: [], offset: [] },
},
entry: args.entry || '',
args: [],
expected: args.param ? { [args.param]: '' } : {},
};
writeFileIfNeeded(path.join(caseDir, 'fixtures', 'sample.fixture.json'), JSON.stringify(fixture, null, 2) + '\n', args.force, actions);
const fingerprintFixture = {
version: 1,
source: { mode: '', pageUrl: '', userAgent: '', timezone: '', locale: '', capturedAt: '' },
canvas: { toDataURL: [], toBlob: [], measureText: [], getImageData: [] },
webgl: { getParameter: [], getSupportedExtensions: null, getExtension: [], getShaderPrecisionFormat: [], readPixels: [] },
webgpu: { requestAdapter: [] },
audio: { startRendering: [], getChannelData: [] },
domGeometry: { getBoundingClientRect: [], getClientRects: [], offset: [] },
};
writeFileIfNeeded(path.join(caseDir, 'fixtures', 'fingerprint.fixture.json'), JSON.stringify(fingerprintFixture, null, 2) + '\n', args.force, actions);
const notes = `# Node.js 补环境任务笔记
## 基本信息
- 目标 API:${args.api || ''}
- 目标 JS:${args.target || ''}
- 入口函数:${args.entry || ''}
- 目标参数:${args.param || ''}
## 前置材料检查
- [ ] 请求样本已确认
- [ ] 已列出请求样本中的全部可疑加密参数
- [ ] 用户已确认本次要分析哪些加密参数
- [ ] 加密参数已在请求中确认
- [ ] 已输出 source / entry / builder / writer 四层链路
- [ ] 目标 JS 文件已保存
- [ ] 加密入口已定位
- [ ] 浏览器取证模式已由用户选择:ruyiPage + RuyiTrace / ruyiPage / CloakBrowser / 手动取证 / AI 自行决定
- [ ] 如选择 RuyiTrace,NDJSON 日志已导入或确认不需要
- [ ] Node 泄露阻断已检查
- [ ] 补环境初始化阶段已加载 / 检测 addon,并记录可用性、导出 API 或降级原因
- [ ] 默认启用 addon-first;如用户明确要求不使用 addon,已记录豁免原因
- [ ] 默认启用 toString / 属性描述符 / 原型链 / 访问器 / 实例对象保护;如用户明确要求关闭,已记录原因
- [ ] 已读取 code-style.md,并先规划 result 目录、文件职责和调用关系
- [ ] 补环境代码默认要求简洁、模块化、具名函数清晰,并写入中文注释
- [ ] 中文注释使用 UTF-8 正常显示,没有问号、连续问号或乱码
- [ ] 已创建 / 读取 notes/代码变更记忆.md;修改关键源码前已搜索历史记录,修改后已记录失败尝试、禁止回退、验证范围和遗留风险
- [ ] 六项纯计算预检已执行或明确不需要
- [ ] 如目标访问 Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何指纹,已采集真实浏览器终端 API 返回值并写入 fixtures/fingerprint.fixture.json
- [ ] 用户已确认进入补环境阶段
## 缺失环境追踪
- trace 文件:tmp/env-trace.jsonl
- 缺失环境摘要:tmp/missing-env.json
- trace 分析:notes/missing-env-priority.md
## 可疑加密参数确认
| 参数名 | 位置 | 样本值摘要 | 疑似原因 | 用户是否确认分析 |
|---|---|---|---|---|
> cURL / HAR 中已有的加密参数值只能作为 fixture expected,最终产物必须通过补环境重新生成,不能直接复用。
## 补齐记录
| 轮次 | 缺失环境 | 处理方式 | 验证结果 |
|---|---|---|---|
## 通用代码变更记忆
- 记录文件:notes/代码变更记忆.md
- [ ] 修改关键源码前已读取该文件
- [ ] 修改关键源码后已追加本轮变更记录
- [ ] 已记录修改前逻辑、问题证据、本次修改、修改理由、已失败尝试、禁止回退、验证命令、验证结果、当前验证范围、遗留风险和当前状态
- [ ] 已运行 scripts/check_change_memory.js --case-dir case --markdown
## 代码质量
- [ ] 已运行 scripts/check_code_quality.js --case-dir case --markdown
- [ ] 已修复超长行、超长函数、压缩堆叠代码和过深嵌套
- [ ] 已确认每个手写源码文件顶部都有中文职责注释
- [ ] 已确认关键 WebAPI、getter / setter、addon-first、fallback、指纹回放和加密入口有中文说明
- [ ] 已确认中文注释没有问号、连续问号或乱码
## 指纹值回放
- [ ] 已确认哪些指纹终端 API 被目标 JS 访问
- [ ] 已使用真实浏览器采样值回放,而不是在 Node.js 中真实模拟渲染
- [ ] 未把浏览器自动化代码放入最终 result 目录
- [ ] 缺失指纹样本时已阻塞并提示补采样,没有静默伪造默认值
## 最终请求验证
- [ ] fixtures 已通过
- [ ] 用户已确认是否发起真实请求
- [ ] 如普通客户端疑似 TLS 指纹不一致,已确认是否使用 CycleTLS / curl_cffi / cffi_curl / cyCronet
- [ ] 请求次数、状态码、响应关键字段已记录
- [ ] 临时响应和敏感请求副本已清理
## 最终总结
- [ ] 已从前置阶段开始生成 case/阶段报告/01-需求信息确认.md 等中文命名阶段报告
- [ ] 项目完成后已默认生成 result/最终项目总结.md
- [ ] 最终总结使用 UTF-8 写入,中文正常显示
- [ ] 总结包含 native addon / NativeProtect 使用情况、加密参数生成与样本复用检查、最终交付结构、测试结果和清理结果
- [ ] 如用户明确要求不生成最终总结,已记录原因
## 清理检查
- [ ] 已删除临时 trace / 日志
- [ ] 已删除失败 runner
- [ ] 已保留必要 fixtures 和 notes
`;
writeFileIfNeeded(path.join(caseDir, 'notes', 'env-task.md'), notes, args.force, actions);
const codeChangeMemory = `# 代码变更记忆
本文件由通用代码变更记忆机制维护,用于记录复杂 case 中关键源码修改的事实、原因、失败尝试、禁止回退、验证范围和遗留风险。不要把“当前报错消失”直接写成永久结论。
## 使用规则
- 修改关键源码前先读取本文件,并搜索文件名、函数名、错误关键词和参数名。
- 修改关键源码后立即追加一条“变更”记录。
- 不删除失败记录;如历史记录被新证据替代,新增记录说明替代原因。
- 状态只能使用:临时修复 / 当前验证通过 / 已失败 / 被替代 / 稳定基线。
## 变更 001 - 待填写
- 时间:YYYY-MM-DD HH:mm
- 涉及文件:
- 涉及函数 / 模块:
- 当前状态:临时修复
### 修改前逻辑
待填写。
### 问题证据
待填写。
### 本次修改
待填写。
### 修改理由
待填写。
### 已失败尝试
待填写。
### 禁止回退
待填写。
### 验证命令
\`\`\`bash
# 待填写
\`\`\`
### 验证结果
待填写。
### 当前验证范围
待填写。
### 遗留风险
待填写。
`;
writeFileIfNeeded(path.join(caseDir, 'notes', '代码变更记忆.md'), codeChangeMemory, args.force, actions);
const entryChain = `# 加密入口链路记录
## writer 最终写入
- 类型:fetch / XHR / URLSearchParams / Header / Cookie / Body
- 文件与行列号:
- 调用栈:
- 写入前对象快照:
- 写入后对象快照:
## builder 请求构造
- 函数 / 方法:
- 所在模块:
- 输入:
- 输出:
- 是否经过拦截器:
## entry 加密入口
- 函数名 / 表达式:${args.entry || ''}
- 文件 / chunk / module id:${args.target || ''}
- 行列号:
- 入参:
- 返回:
- 是否需要初始化:
## source 数据源
- URL / Query:
- Body:
- Cookie:
- localStorage / sessionStorage:
- 时间 / 随机数:
- navigator / screen / document / canvas / WebGL:
- Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何指纹 fixture:
- 其他:
## 可信度
- 证据来源:
- 是否可进入补环境:否
- 阻塞点:
`;
writeFileIfNeeded(path.join(caseDir, 'notes', 'entry-chain.md'), entryChain, args.force, actions);
const silentChecklist = `# 静默失败排查清单
当目标 JS 不报错但 ${args.param || '目标参数'} 不一致时逐项检查。
- [ ] 请求 URL、Query、Body、Header 与浏览器样本一致
- [ ] SDK 初始化参数、版本、appId、页面配置一致
- [ ] Cookie、localStorage、sessionStorage 已按 fixture 注入
- [ ] Date.now / new Date / performance.now 可复现
- [ ] Math.random / crypto.getRandomValues 可复现
- [ ] Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何等指纹终端 API 返回值来自真实浏览器采样并可回放
- [ ] navigator / screen / timezone / language 与浏览器样本一致
- [ ] JS 加载顺序、runtime chunk、动态 chunk 完整
- [ ] Function.prototype.toString / Object.prototype.toString 已处理
- [ ] 属性描述符与原型链已记录
- [ ] document.all 等特殊对象已处理或确认未触发
- [ ] Worker / WASM / postMessage 已排查
`;
writeFileIfNeeded(path.join(caseDir, 'notes', 'silent-failure-checklist.md'), silentChecklist, args.force, actions);
const trustMatrix = `# 证据可信度矩阵
| 项目 | 当前值 / 结论 | 来源 | 等级 A/B/C/D | 待验证 |
|---|---|---|---|---|
| 目标 API | ${args.api || ''} | 用户输入 | B | |
| 目标参数 | ${args.param || ''} | 用户输入 | B | |
| 加密入口 | ${args.entry || ''} | 用户输入 / 待验证 | C | |
| JS 文件 | ${args.target || ''} | 用户输入 / 待验证 | C | |
| Cookie / token | 仅记录键名和脱敏摘要 | fixture | A/B | |
`;
writeFileIfNeeded(path.join(caseDir, 'notes', 'trust-matrix.md'), trustMatrix, args.force, actions);
return { caseDir, actions };
}
function render(result) {
const label = { 'create-dir': '创建目录', 'keep-dir': '保留目录', 'create-file': '创建文件', 'write-file': '写入文件', 'keep-file': '保留已有文件' };
const lines = ['# 补环境 case 初始化结果', '', `- case 目录:${result.caseDir}`];
for (const a of result.actions) lines.push(`- ${label[a.action] || a.action}:${a.path}`);
return lines.join('\n') + '\n';
}
try {
const args = parseArgs(process.argv);
if (args.help) { console.log(usage()); process.exit(0); }
const result = initCase(args);
if (args.json) console.log(JSON.stringify(result, null, 2));
else process.stdout.write(render(result));
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}