Files
2026-07-28 01:16:15 +08:00

255 lines
7.1 KiB
JavaScript

// ============================================================
// WebSocket 捕获脚本 - 3DTank 服务器通信
// ============================================================
// 连接 wss://c1.3dtank.com:9090/ 并捕获所有通信数据
// 用法: node capture.js [session_name]
// ============================================================
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
// 参数
const sessionName = process.argv[2] || `session_${Date.now()}`;
const sessionDir = path.join(__dirname, sessionName);
// 创建会话目录
if (!fs.existsSync(sessionDir)) {
fs.mkdirSync(sessionDir, { recursive: true });
}
// 日志文件
const logFile = path.join(sessionDir, 'log.txt');
const logStream = fs.createWriteStream(logFile, { flags: 'w' });
function log(msg) {
const line = `[${new Date().toISOString()}] ${msg}`;
console.log(line);
logStream.write(line + '\n');
}
// 二进制转十六进制转储
function hexDump(buffer, prefix = '') {
const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
const lines = [];
for (let i = 0; i < bytes.length; i += 16) {
const chunk = bytes.slice(i, Math.min(i + 16, bytes.length));
const hex = Array.from(chunk).map(b => b.toString(16).padStart(2, '0')).join(' ');
const ascii = Array.from(chunk).map(b => (b >= 32 && b < 127) ? String.fromCharCode(b) : '.').join('');
lines.push(`${prefix}${i.toString(16).padStart(8, '0')} ${hex.padEnd(48)} ${ascii}`);
}
return lines.join('\n');
}
// 保存二进制数据
function saveBinary(data, direction, seq) {
const filename = `${direction}_${seq.toString().padStart(4, '0')}.bin`;
const filepath = path.join(sessionDir, filename);
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
fs.writeFileSync(filepath, buffer);
return filename;
}
// 尝试解析 Protocol Buffer 字段
function tryParseProtobuf(buffer) {
const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
const fields = [];
let offset = 0;
try {
while (offset < bytes.length) {
const startOffset = offset;
// 读取 varint key
let key = 0;
let shift = 0;
while (offset < bytes.length) {
const b = bytes[offset++];
key |= (b & 0x7f) << shift;
shift += 7;
if ((b & 0x80) === 0) break;
}
const fieldNum = key >>> 3;
const wireType = key & 0x07;
if (fieldNum === 0) break;
let value = null;
let valueDesc = '';
switch (wireType) {
case 0: // varint
let varintVal = 0;
let varintShift = 0;
while (offset < bytes.length) {
const b = bytes[offset++];
varintVal |= (b & 0x7f) << varintShift;
varintShift += 7;
if ((b & 0x80) === 0) break;
}
value = varintVal;
valueDesc = `varint=${varintVal}`;
break;
case 1: // 64-bit
if (offset + 8 <= bytes.length) {
value = bytes.readBigUInt64LE(offset);
valueDesc = `int64=${value}`;
offset += 8;
} else {
return { fields, truncated: true };
}
break;
case 2: // length-delimited
let len = 0;
let lenShift = 0;
while (offset < bytes.length) {
const b = bytes[offset++];
len |= (b & 0x7f) << lenShift;
lenShift += 7;
if ((b & 0x80) === 0) break;
}
if (offset + len <= bytes.length) {
const data = bytes.slice(offset, offset + len);
value = data;
// 尝试解析为 UTF-8 字符串
try {
const str = data.toString('utf8');
if (/^[\x20-\x7e\u4e00-\u9fff\u3000-\u303f]+$/.test(str) && str.length > 0) {
valueDesc = `string="${str}"`;
} else {
valueDesc = `bytes[${len}]=${data.slice(0, 32).toString('hex')}${len > 32 ? '...' : ''}`;
}
} catch (e) {
valueDesc = `bytes[${len}]`;
}
offset += len;
} else {
return { fields, truncated: true };
}
break;
case 5: // 32-bit
if (offset + 4 <= bytes.length) {
value = bytes.readUInt32LE(offset);
valueDesc = `int32=${value}`;
offset += 4;
} else {
return { fields, truncated: true };
}
break;
default:
return { fields, unknownWireType: wireType };
}
fields.push({
fieldNum,
wireType,
value: valueDesc,
offset: startOffset
});
}
} catch (e) {
return { fields, error: e.message };
}
return { fields };
}
// 连接参数
const url = 'wss://c1.3dtank.com:9090/';
let seq = 0;
let recvSeq = 0;
let sendSeq = 0;
log(`=== 3DTank WebSocket 捕获会话 ===`);
log(`会话名称: ${sessionName}`);
log(`会话目录: ${sessionDir}`);
log(`服务器: ${url}`);
log(`开始时间: ${new Date().toISOString()}`);
log('');
// 创建 WebSocket 连接
log('正在连接服务器...');
const ws = new WebSocket(url, {
// 不验证证书(测试用)
rejectUnauthorized: false,
// 模拟浏览器 headers
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Origin': 'https://3dtank.com'
}
});
ws.binaryType = 'arraybuffer';
// 连接成功
ws.on('open', function open() {
log('✓ 连接成功!');
log('');
log('等待服务器消息... (按 Ctrl+C 停止)');
log('---');
});
// 接收消息
ws.on('message', function incoming(data) {
const buffer = Buffer.from(data);
const filename = saveBinary(buffer, 'recv', recvSeq++);
const hex = hexDump(buffer);
log(`<<< 接收消息 #${recvSeq - 1} (${buffer.length} 字节) -> ${filename}`);
log(hex);
// 尝试解析为 Protocol Buffer
const pb = tryParseProtobuf(buffer);
if (pb.fields && pb.fields.length > 0) {
log(` PB 解析:`);
pb.fields.forEach(f => {
log(` field=${f.fieldNum} wire=${f.wireType} ${f.value}`);
});
}
if (pb.error) log(` PB 解析错误: ${pb.error}`);
log('');
});
// 发送消息(目前不主动发送,只监听)
// 如果需要发送握手,可以在这里添加
// 连接关闭
ws.on('close', function close(code, reason) {
log(`---`);
log(`✗ 连接关闭 (code=${code}, reason=${reason.toString()})`);
log(`总共接收: ${recvSeq} 条消息`);
logStream.end();
});
// 错误处理
ws.on('error', function error(err) {
log(`✗ 错误: ${err.message}`);
if (err.stack) {
log(err.stack);
}
logStream.end();
});
// 优雅退出
process.on('SIGINT', function() {
log('---');
log('用户中断,正在关闭...');
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
logStream.end();
process.exit(0);
});
// 超时退出(可选,10 分钟后自动退出)
setTimeout(() => {
log('---');
log('超时自动退出 (10 分钟)');
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
logStream.end();
process.exit(0);
}, 10 * 60 * 1000);