diff --git a/3DTank/tools/serve.js b/3DTank/tools/serve.js index d094a4e..b180cf2 100644 --- a/3DTank/tools/serve.js +++ b/3DTank/tools/serve.js @@ -208,6 +208,23 @@ const Server = http.createServer((Req, Res) => { }).end(Sw); return; } + // /ws_hook.js:页面内 window.WebSocket 抓包脚本(在主脚本之前引入)。 + // 传输仍由浏览器发起(服务端看不到差别),用于与中继转存做逐帧对比;#nohook 可关闭。 + if (UrlPath.split('?')[0] === '/ws_hook.js') { + const HookFile = path.join(ROOT, 'sockets', 'ws_hook.js'); + if (!fs.existsSync(HookFile)) { + Res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }).end('ws_hook.js not found'); + return; + } + const Hook = fs.readFileSync(HookFile); + Res.writeHead(200, { + 'Content-Type': 'application/javascript; charset=utf-8', + 'Content-Length': Hook.length, + 'Cache-Control': 'no-cache', + 'X-Hornet-WsHook': 'local', + }).end(Hook); + return; + } // /config.xml 本地覆盖:存在 local/config.xml 时用它,把游戏指向本地 wss_server if (!REAL_CONFIG && UrlPath.split('?')[0] === '/config.xml' && fs.existsSync(LOCAL_CONFIG)) { const Conf = fs.readFileSync(LOCAL_CONFIG); diff --git a/sockets/ws_hook.js b/sockets/ws_hook.js index 5fb2509..67e5700 100644 --- a/sockets/ws_hook.js +++ b/sockets/ws_hook.js @@ -1,195 +1,94 @@ // ============================================================ -// WebSocket 流量捕获注入脚本 +// WebSocket 流量捕获(页面内 hook,传输仍由浏览器发起) // ============================================================ -// 在游戏加载前注入,捕获所有 WebSocket 通信 -// 数据保存到 window.__ws_capture__ 数组 -// 使用 window.__download_ws_capture__() 下载捕获数据 +// 目的:在不改变「服务端看到的连接」的前提下,把浏览器真实的 ws 帧抓下来, +// 用于与「中继转存」逐帧对比,定位「直连能进、中继不能进」的差异。 +// +// 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入(先于游戏创建 WebSocket)。 +// 开关:URL hash 带 #nohook 则不抓(默认抓)。 +// 产物: +// window.__ws_capture__ —— 内存里的抓包(connections[].messages[] = {d,t,n,b}, +// d=c2s/s2c,n=字节数,b=base64 原始字节,顺序即收发顺序) +// window.__ws_save__() —— 手动保存;离开页面时自动 sendBeacon 再存一份 +// 落盘位置:serve.js 的 POST /__dump__/ → 3DTank/names/ // ============================================================ - (function () { if (window.__ws_hooked__) return; + if (/#nohook/.test(location.hash)) { + console.log('[WS Hook] #nohook:已跳过'); + return; + } window.__ws_hooked__ = true; - window.__ws_capture__ = []; - var captureDir = 'ws_capture_' + Date.now(); + var Capture = { startedAt: Date.now(), connections: [] }; + window.__ws_capture__ = Capture; + var Original = window.WebSocket; - function toHex(buffer) { - var bytes = new Uint8Array(buffer); - var hex = []; - for (var i = 0; i < bytes.length; i += 16) { - var chunk = bytes.slice(i, Math.min(i + 16, bytes.length)); - var hexPart = Array.from(chunk).map(function (b) { - return b.toString(16).padStart(2, '0'); - }).join(' '); - var asciiPart = Array.from(chunk).map(function (b) { - return (b >= 32 && b < 127) ? String.fromCharCode(b) : '.'; - }).join(''); - hex.push(i.toString(16).padStart(8, '0') + ' ' + hexPart.padEnd(48) + ' ' + asciiPart); + function ToBytes(Data) { + if (Data instanceof ArrayBuffer) return new Uint8Array(Data); + if (ArrayBuffer.isView(Data)) return new Uint8Array(Data.buffer, Data.byteOffset, Data.byteLength); + if (typeof Data === 'string') return new TextEncoder().encode(Data); + return new Uint8Array(0); + } + function ToBase64(Bytes) { + var S = ''; + var Chunk = 0x8000; // 分块,避免超长字符串参数 + for (var I = 0; I < Bytes.length; I += Chunk) { + S += String.fromCharCode.apply(null, Bytes.subarray(I, I + Chunk)); } - return hex.join('\n'); + return btoa(S); } - function toArrayBuffer(data) { - if (data instanceof ArrayBuffer) return data; - if (ArrayBuffer.isView(data)) return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); - if (typeof data === 'string') return new TextEncoder().encode(data).buffer; - return data; + function Hooked(Url, Protocols) { + var Ws = Protocols ? new Original(Url, Protocols) : new Original(Url); + var Info = { id: Capture.connections.length, url: String(Url), openTime: null, closeCode: null, messages: [] }; + Capture.connections.push(Info); + console.log('[WS Hook] #' + Info.id + ' 连接 ' + Info.url); + + function Record(Dir, Data) { + var Bytes = ToBytes(Data); + Info.messages.push({ d: Dir, t: Date.now(), n: Bytes.length, b: ToBase64(Bytes) }); + } + + var OrigSend = Ws.send; + Ws.send = function (Data) { + Record('c2s', Data); + return OrigSend.apply(Ws, arguments); + }; + Ws.addEventListener('message', function (Ev) { Record('s2c', Ev.data); }); + Ws.addEventListener('open', function () { Info.openTime = Date.now(); }); + Ws.addEventListener('close', function (Ev) { Info.closeCode = Ev.code; }); + + return Ws; } + Hooked.CONNECTING = Original.CONNECTING; + Hooked.OPEN = Original.OPEN; + Hooked.CLOSING = Original.CLOSING; + Hooked.CLOSED = Original.CLOSED; + Hooked.prototype = Original.prototype; + window.WebSocket = Hooked; - // 保存原始 WebSocket - var OriginalWebSocket = window.WebSocket; - - function HookedWebSocket(url, protocols) { - var ws = protocols ? new OriginalWebSocket(url, protocols) : new OriginalWebSocket(url); - - console.log('[WS Hook] 连接:', url); - - var wsId = window.__ws_capture__.length; - var wsInfo = { - id: wsId, - url: url, - openTime: null, - messages: [] - }; - window.__ws_capture__.push(wsInfo); - - // Hook send - var originalSend = ws.send.bind(ws); - ws.send = function (data) { - var ab = toArrayBuffer(data); - var msgInfo = { - direction: 'send', - time: Date.now(), - size: ab.byteLength, - hex: toHex(ab), - buffer: ab - }; - wsInfo.messages.push(msgInfo); - console.log('[WS Hook] #' + wsId + ' >>> 发送 ' + ab.byteLength + ' 字节:'); - console.log(toHex(ab)); - return originalSend(data); - }; - - // Hook onmessage - var originalOnMessage = null; - Object.defineProperty(ws, 'onmessage', { - get: function () { return originalOnMessage; }, - set: function (fn) { - originalOnMessage = fn; - // Wrap the handler - ws.addEventListener('message', function (event) { - var ab = toArrayBuffer(event.data); - var msgInfo = { - direction: 'recv', - time: Date.now(), - size: ab.byteLength, - hex: toHex(ab), - buffer: ab - }; - wsInfo.messages.push(msgInfo); - console.log('[WS Hook] #' + wsId + ' <<< 接收 ' + ab.byteLength + ' 字节:'); - console.log(toHex(ab)); - }); - } - }); - - // Also hook addEventListener for 'message' - var originalAddEventListener = ws.addEventListener.bind(ws); - ws.addEventListener = function (type, listener, options) { - if (type === 'message') { - var wrappedListener = function (event) { - var ab = toArrayBuffer(event.data); - var msgInfo = { - direction: 'recv', - time: Date.now(), - size: ab.byteLength, - hex: toHex(ab), - buffer: ab - }; - wsInfo.messages.push(msgInfo); - console.log('[WS Hook] #' + wsId + ' <<< 接收 ' + ab.byteLength + ' 字节:'); - console.log(toHex(ab)); - listener(event); - }; - return originalAddEventListener(type, wrappedListener, options); - } - return originalAddEventListener(type, listener, options); - }; - - ws.addEventListener('open', function () { - wsInfo.openTime = Date.now(); - console.log('[WS Hook] #' + wsId + ' 连接成功'); - }); - - ws.addEventListener('close', function (event) { - console.log('[WS Hook] #' + wsId + ' 连接关闭 (code=' + event.code + ')'); - }); - - ws.addEventListener('error', function (event) { - console.log('[WS Hook] #' + wsId + ' 连接错误:', event); - }); - - return ws; + function Stamp() { + return new Date().toISOString().replace(/[:.]/g, '-'); } + function Save(Prefix) { + var Json = JSON.stringify(Capture); + var Url = '/__dump__/' + (Prefix || 'ws_capture_') + Stamp() + '.json'; + return fetch(Url, { method: 'POST', body: Json }) + .then(function (R) { + console.log('[WS Hook] 已保存 ' + Url + '(' + Json.length + ' 字节,' + Capture.connections.length + ' 个连接)'); + return R; + }) + .catch(function (E) { console.warn('[WS Hook] 保存失败: ' + (E && E.message)); }); + } + window.__ws_save__ = function () { return Save('ws_capture_'); }; - // 复制静态属性 - HookedWebSocket.CONNECTING = OriginalWebSocket.CONNECTING; - HookedWebSocket.OPEN = OriginalWebSocket.OPEN; - HookedWebSocket.CLOSING = OriginalWebSocket.CLOSING; - HookedWebSocket.CLOSED = OriginalWebSocket.CLOSED; - HookedWebSocket.prototype = OriginalWebSocket.prototype; + window.addEventListener('beforeunload', function () { + try { + var Blob = new Blob([JSON.stringify(Capture)], { type: 'application/json' }); + navigator.sendBeacon('/__dump__/ws_capture_auto_' + Date.now() + '.json', Blob); + } catch (E) {} + }); - window.WebSocket = HookedWebSocket; - - // 下载捕获数据 - window.__download_ws_capture__ = function () { - var data = window.__ws_capture__.map(function (ws) { - return { - id: ws.id, - url: ws.url, - openTime: ws.openTime, - messages: ws.messages.map(function (m) { - // Convert buffer to base64 for JSON serialization - var bytes = new Uint8Array(m.buffer); - var binary = ''; - for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); - return { - direction: m.direction, - time: m.time, - size: m.size, - hex: m.hex, - base64: btoa(binary) - }; - }) - }; - }); - - var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - var url = URL.createObjectURL(blob); - var a = document.createElement('a'); - a.href = url; - a.download = 'ws_capture_' + Date.now() + '.json'; - a.click(); - URL.revokeObjectURL(url); - console.log('[WS Hook] 已下载捕获数据,共 ' + data.length + ' 个连接'); - }; - - // 导出单个消息的二进制 - window.__export_ws_binary__ = function (wsId, msgIdx) { - var ws = window.__ws_capture__[wsId]; - if (!ws) { console.log('未找到 WS #' + wsId); return; } - var msg = ws.messages[msgIdx]; - if (!msg) { console.log('未找到消息 #' + msgIdx); return; } - var blob = new Blob([msg.buffer], { type: 'application/octet-stream' }); - var url = URL.createObjectURL(blob); - var a = document.createElement('a'); - a.href = url; - a.download = msg.direction + '_' + wsId + '_' + msgIdx + '.bin'; - a.click(); - URL.revokeObjectURL(url); - }; - - console.log('[WS Hook] WebSocket 捕获脚本已加载'); - console.log('[WS Hook] 使用 __download_ws_capture__() 下载所有捕获数据'); - console.log('[WS Hook] 使用 __export_ws_binary__(wsId, msgIdx) 导出单个消息'); + console.log('[WS Hook] 已挂载:window.__ws_capture__(内存)/ __ws_save__()(存到 3DTank/names/)'); })();