255 lines
7.1 KiB
JavaScript
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);
|