// ============================================================ // 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);