Files
TermiNexus fd0dffe31b feat(bundle): 迁移到新版 main.7cfa4058.js(重做拆分 + 移植 7 处针对性修改)
- 拆分:tools/split.js 重切新版(1347 模块 / 126 代码模块),主文件 main.7cfa4058.js,语法验证通过

- 修复 split.js 内自动刷新拦截的 \\n 转义:它在模板字符串里,必须写 \\\\n,否则生成的主文件字符串断行、语法报错

- 符号映射:map_symbols.js 对齐旧→新(qyr→ryz、w187→c18o、z4t→jtn、vgt→rkt、Bmt→Ezt、Qjt→Fjt、txt→Gjt、k15b→s15n、azi_1→vzr_1…)

- 移植 7 处针对性修改:炮塔摆头不回弹(vSt.ryz)、摆头不过冲(case 0)、精准跟手(case 1/2)、悬浮底盘 raw 两级(Ezt.n36 + Zmt 实际速度)、电磁炮蓄力时长(jtn.c18o)

- 重新打包 unpacked.pack.json(127 文件 / 版本 96833eb2b0cf8909)
2026-10-16 14:20:15 +08:00

372 lines
14 KiB
JavaScript

// probe_cdn.js - 探测 3dtank.com / tankionline.com 的边缘 CDN 与 source map 暴露情况
// 用法:
// node probe_cdn.js --mode dns # 子域枚举(DNS 解析)
// node probe_cdn.js --mode sourcemap --js <url> # 拉取 js 尾部找 sourceMappingURL,并试候选 map 路径
// node probe_cdn.js --mode tail --js <url> # 只取 js 末尾 N 字节(找 sourceMappingURL / 版本注释)
// node probe_cdn.js --mode paths --base <url> # 试一批常见路径(目录列表/备份/配置)
// node probe_cdn.js --mode cors --base <url> # 试探来源校验与 CORS
// node probe_cdn.js --mode all --js <url> --base <url>
// 说明:
// 只用 GET/HEAD/OPTIONS,不做任何写操作(避免对线上产生影响)。
// 默认忽略证书错误(站点多为老配置),并把结果落盘到 names/cdn_probe.json。
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const dns = require('dns');
const { URL } = require('url');
const PROJECT = path.resolve(__dirname, '..');
const Argv = process.argv.slice(2);
const Arg = (Name, Def) => {
const I = Argv.indexOf('--' + Name);
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
};
const MODE = Arg('mode', 'all');
const JS_URL = Arg('js', 'https://3dtank.com/play/static/js/main.6469b966.js');
const BASE = Arg('base', 'https://3dtank.com');
const TIMEOUT = Number(Arg('timeout', '12000'));
const CONCURRENCY = Number(Arg('concurrency', '8'));
const OUT = Arg('out', path.join(PROJECT, 'names', 'cdn_probe.json'));
const CORS_PATH = Arg('path', '/');
const UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0';
const SITE_HEADERS = {
accept: '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
origin: 'https://3dtank.com',
referer: 'https://3dtank.com/',
'user-agent': UA,
};
// 候选子域(两个站点都试)
const HOSTS = {
'3dtank.com': [
'3dtank.com', 'www', 'res', 'static', 'cdn', 'img', 'images', 'assets', 's',
'c1', 'c2', 'c3', 'c4', 'game', 'play', 'patch', 'update', 'updater', 'dl',
'download', 'files', 'media', 'mirror', 'client', 'launcher', 'api', 'open-api',
'mgr', 'ops', 'forum', 'union', 'ratings', 'test', 'dev', 'beta', 'new', 'old',
'bbs', 'sso', 'pay', 'stat', 'log', 'oss',
],
'tankionline.com': [
'tankionline.com', 'www', 'res', 'static', 'sstatic', 'cdn', 'img', 'images',
'assets', 's', 'c1', 'c2', 'c3', 'game', 'play', 'patch', 'update', 'updater',
'dl', 'download', 'files', 'media', 'mirror', 'client', 'launcher', 'api',
'm', 'test', 'dev', 'beta', 'new', 'old', 'forum', 'en', 'ru', 'de',
],
};
// 各域通用的子域候选(--domains 追加新域时复用)
const SUBS_DEFAULT = [
'www', 'res', 'static', 'cdn', 'img', 'images', 'assets', 's', 'play', 'game',
'c1', 'c2', 'c3', 'dl', 'download', 'files', 'patch', 'update', 'api', 'm',
];
// 常见可疑路径(开放目录/备份/配置泄漏)
const PATHS = [
'/', '/index.html', '/resources/', '/resources/index.json', '/play/', '/play/static/',
'/play/static/js/', '/play/static/js/index.json', '/play/static/js/resources.json',
'/play/static/js/main.js.map', '/static/', '/assets/', '/files/', '/dl/', '/patch/',
'/config.xml', '/s/status.js', '/robots.txt', '/sitemap.xml', '/.git/HEAD',
'/crossdomain.xml', '/version.txt', '/manifest.json', '/favicon.ico',
];
// 单次请求;Follow 为真时自动跟 3xx
function Request(Url, Opt) {
const O = Opt || {};
return new Promise((Resolve) => {
let U;
try {
U = new URL(Url);
} catch (E) {
return Resolve({ Error: 'bad url' });
}
const Lib = U.protocol === 'http:' ? http : https;
const Headers = Object.assign({}, O.Headers || {});
const Req = Lib.request(
{
protocol: U.protocol,
hostname: U.hostname,
port: U.port || (U.protocol === 'http:' ? 80 : 443),
path: U.pathname + U.search,
method: O.Method || 'GET',
headers: Headers,
rejectUnauthorized: false,
servername: U.hostname,
timeout: TIMEOUT,
},
(Res) => {
const Chunks = [];
let Total = 0;
let Finished = false;
const Cap = O.Cap || 0; // >0 时只读前 Cap 字节
const Done = () => {
if (Finished) return;
Finished = true;
const Body = Buffer.concat(Chunks);
if (O.Follow && Res.statusCode >= 300 && Res.statusCode < 400 && Res.headers.location) {
const Next = new URL(Res.headers.location, Url).href;
return Resolve(Request(Next, Object.assign({}, O, { Follow: (O.Follow || 1) - 1 })));
}
Resolve({
Url,
Status: Res.statusCode,
Bytes: Total,
Type: String(Res.headers['content-type'] || ''),
Server: String(Res.headers.server || ''),
Location: String(Res.headers.location || ''),
Acao: String(Res.headers['access-control-allow-origin'] || ''),
Allow: String(Res.headers.allow || ''),
Headers: Res.headers,
Body,
});
};
Res.on('data', (C) => {
Total += C.length;
if (!Cap || Total <= Cap) Chunks.push(C);
if (Cap && Total > Cap) {
Res.destroy();
Done();
}
});
Res.on('end', Done);
}
);
Req.on('timeout', () => {
Req.destroy();
Resolve({ Url, Error: 'timeout' });
});
Req.on('error', (E) => Resolve({ Url, Error: E.code || E.message }));
if (O.Body) Req.write(O.Body);
Req.end();
});
}
async function Pool(Items, Worker, N) {
let Next = 0;
const Out = new Array(Items.length);
await Promise.all(
new Array(Math.min(N, Items.length || 1)).fill(0).map(async () => {
while (true) {
const I = Next++;
if (I >= Items.length) return;
Out[I] = await Worker(Items[I], I);
}
})
);
return Out;
}
function Brief(R) {
if (R.Error) return 'ERR ' + R.Error;
return (
String(R.Status).padEnd(4) +
String(R.Bytes).padStart(9) +
' ' +
(R.Type || '-').split(';')[0].padEnd(24) +
(R.Server ? 'srv=' + R.Server + ' ' : '') +
(R.Acao ? 'ACAO=' + R.Acao + ' ' : '') +
(R.Location ? '-> ' + R.Location : '')
);
}
// ---- DNS 子域枚举 ----
async function ModeDns() {
const Result = {};
const Extra = Arg('domains', '');
if (Extra) for (const D of Extra.split(',').map((S) => S.trim()).filter(Boolean)) HOSTS[D] = HOSTS[D] || SUBS_DEFAULT;
const SubsOverride = Arg('subs', '');
if (SubsOverride) {
const S = SubsOverride.split(',').map((X) => X.trim()).filter(Boolean);
for (const D of Object.keys(HOSTS)) HOSTS[D] = S;
}
for (const [Domain, Subs] of Object.entries(HOSTS)) {
console.log('== DNS ' + Domain + ' ==');
const Found = [];
const Rows = await Pool(
Subs,
async (S) => {
const Fqdn = S.includes('.') ? S : S + '.' + Domain;
const A = await new Promise((R) => dns.resolve4(Fqdn, (E, Addr) => R(E ? null : Addr)));
const C = await new Promise((R) => dns.resolveCname(Fqdn, (E, Addr) => R(E ? null : Addr)));
if (!A && !C) return null;
const Row = { Host: Fqdn, A: A || [], Cname: C || [] };
Found.push(Row);
console.log(' ' + Fqdn.padEnd(34) + (A ? 'A=' + A.join(',') : '') + (C ? ' CNAME=' + C.join(',') : ''));
return Row;
},
CONCURRENCY
);
Result[Domain] = Rows.filter(Boolean);
}
return Result;
}
// ---- source map ----
async function ModeSourcemap() {
console.log('== sourcemap 探测: ' + JS_URL + ' ==');
const Result = { Js: JS_URL, Tail: '', MappingUrl: '', Candidates: [] };
const Main = await Request(JS_URL, { Headers: SITE_HEADERS, Follow: 3, Cap: 2 * 1024 * 1024 });
console.log(' js 本体: ' + Brief(Main));
if (Main.Body) {
const Text = Main.Body.toString('latin1');
const M = Text.match(/sourceMappingURL=([^\s'"*]+)/);
if (M) {
Result.MappingUrl = M[1];
console.log(' 发现 sourceMappingURL: ' + M[1]);
} else {
const Tail = Text.slice(-300).replace(/[^\x20-\x7e]/g, '.');
Result.Tail = Tail;
console.log(' 正文尾部(未见 sourceMappingURL): ' + Tail.slice(-160));
}
}
const Base = JS_URL.replace(/\.js($|\?)/, '');
const Cands = [
JS_URL + '.map',
Base + '.js.map',
Base + '.map',
Base.replace(/\/[^/]+$/, '/') + 'main.js.map',
Base + '.js.map?',
];
if (Result.MappingUrl) Cands.unshift(new URL(Result.MappingUrl, JS_URL).href);
const Rows = await Pool(
[...new Set(Cands)],
async (U) => {
const R = await Request(U, { Headers: SITE_HEADERS, Follow: 3 });
console.log(' ' + Brief(R) + ' ' + U);
return { Url: U, Status: R.Status, Bytes: R.Bytes, Type: R.Type, Error: R.Error };
},
CONCURRENCY
);
Result.Candidates = Rows;
return Result;
}
// ---- 尾部字节(sourceMappingURL / 版本注释常在这里)----
async function ModeTail() {
const N = Number(Arg('bytes', '2048'));
console.log('== 尾部 ' + N + ' 字节: ' + JS_URL + ' ==');
const R = await Request(JS_URL, {
Headers: Object.assign({}, SITE_HEADERS, { range: 'bytes=-' + N }),
Follow: 2,
});
console.log(' ' + Brief(R));
const Text = (R.Body || Buffer.alloc(0)).toString('latin1');
const Tail = Text.slice(-N);
const Result = { Js: JS_URL, Status: R.Status, Bytes: R.Bytes, Range: R.Headers && R.Headers['content-range'], Tail: Tail };
const M = Tail.match(/sourceMappingURL=([^\s'"*]+)/);
if (M) {
Result.MappingUrl = M[1];
console.log(' >>> sourceMappingURL = ' + M[1]);
}
console.log(' --- 尾部原文 ---');
console.log(Tail.replace(/[\x00-\x08\x0b-\x1f]/g, '.'));
return Result;
}
// ---- 常见路径 ----
async function ModePaths() {
console.log('== 路径探测: ' + BASE + ' ==');
const Rows = await Pool(
PATHS,
async (P) => {
const R = await Request(BASE + P, { Headers: SITE_HEADERS, Follow: 2, Cap: 4096 });
console.log(' ' + Brief(R).padEnd(80) + ' ' + P);
return { Path: P, Status: R.Status, Bytes: R.Bytes, Type: R.Type, Server: R.Server, Acao: R.Acao };
},
CONCURRENCY
);
return Rows;
}
// ---- 任意 URL 列表 ----
async function ModeList() {
const Inline = Arg('urls', '');
const File = Arg('file', '');
let List = Inline ? Inline.split(',').map((S) => S.trim()).filter(Boolean) : [];
if (File) {
List = List.concat(
fs.readFileSync(path.resolve(File), 'utf8').split(/\r?\n/).map((S) => S.trim()).filter((S) => S && !S.startsWith('#'))
);
}
const Cap = Number(Arg('cap', '4096'));
const HostOverride = Arg('host', ''); // 指定 Host(配 IP 直连做 CDN 回源探测)
const ExtraHeaders = Arg('headers', ''); // 额外请求头,形如 "k:v|k:v"
let HeadersBase = HostOverride ? Object.assign({}, SITE_HEADERS, { host: HostOverride }) : SITE_HEADERS;
if (ExtraHeaders) {
HeadersBase = Object.assign({}, HeadersBase);
for (const Pair of ExtraHeaders.split('|')) {
const C = Pair.indexOf(':');
if (C > 0) HeadersBase[Pair.slice(0, C).trim().toLowerCase()] = Pair.slice(C + 1).trim();
}
}
console.log('== 列表探测 ' + List.length + ' 个 URL ==' + (HostOverride ? ' Host 覆盖: ' + HostOverride : ''));
const Rows = await Pool(
List,
async (U) => {
const R = await Request(U, { Headers: HeadersBase, Follow: 3, Cap });
const Preview = R.Body ? R.Body.slice(0, Cap).toString('latin1').replace(/\s+/g, ' ').slice(0, 200) : '';
console.log(' ' + Brief(R).padEnd(76) + ' ' + U);
if (Preview && (!R.Type || !/image|font|octet/.test(R.Type))) console.log(' | ' + Preview);
return {
Url: U,
Status: R.Status,
Bytes: R.Bytes,
Type: R.Type,
Server: R.Server,
Acao: R.Acao,
Range: R.Headers && R.Headers['content-range'],
Preview,
Error: R.Error,
};
},
CONCURRENCY
);
return Rows;
}
// ---- CORS / 来源校验 / 写方法 ----
async function ModeCors() {
console.log('== CORS/来源校验/方法探测: ' + BASE + ' ==');
const Tests = [
{ Name: '纯 GET 无任何头', Headers: { 'user-agent': UA }, Method: 'GET' },
{ Name: '外来 Origin', Headers: { 'user-agent': UA, origin: 'https://evil.example' }, Method: 'GET' },
{ Name: '外来 Referer', Headers: { 'user-agent': UA, referer: 'https://evil.example/' }, Method: 'GET' },
{ Name: '空 Referer', Headers: { 'user-agent': UA, referer: '' }, Method: 'GET' },
{ Name: '站点头(基准)', Headers: SITE_HEADERS, Method: 'GET' },
{ Name: 'null Origin', Headers: { 'user-agent': UA, origin: 'null' }, Method: 'GET' },
{ Name: 'OPTIONS 预检', Headers: SITE_HEADERS, Method: 'OPTIONS' },
];
const Rows = [];
for (const T of Tests) {
const R = await Request(BASE + CORS_PATH, { Headers: T.Headers, Method: T.Method, Follow: 2, Cap: 2048 });
const Row = {
Name: T.Name,
Status: R.Status,
Bytes: R.Bytes,
Acao: R.Acao,
Allow: R.Allow,
Server: R.Server,
Error: R.Error,
};
Rows.push(Row);
console.log(' ' + T.Name.padEnd(20) + Brief(R).padEnd(70) + (R.Allow ? 'Allow=' + R.Allow : ''));
}
return Rows;
}
(async () => {
const Out = {};
if (MODE === 'dns' || MODE === 'all') Out.Dns = await ModeDns();
if (MODE === 'sourcemap' || MODE === 'all') Out.Sourcemap = await ModeSourcemap();
if (MODE === 'tail') Out.Tail = await ModeTail();
if (MODE === 'list') Out.List = await ModeList();
if (MODE === 'paths' || MODE === 'all') Out.Paths = await ModePaths();
if (MODE === 'cors' || MODE === 'all') Out.Cors = await ModeCors();
fs.writeFileSync(OUT, JSON.stringify(Out, (K, V) => (K === 'Body' ? undefined : V), 2), 'utf8');
console.log('结果落盘: ' + OUT);
})();