// ============================================================ // WebSocket 流量捕获(页面内 hook,传输仍由浏览器发起) // ============================================================ // 目的:把浏览器真实的 ws 帧旁听下来,用于与「中继转存」逐帧对比。 // // 关键设计(尽量不被游戏察觉 / 不拖慢游戏): // 1) **不替换 window.WebSocket 构造函数**(否则 WebSocket.toString() 不再是 "[native code]", // 实测会导致「加入战斗失败」); // 2) 只补原型上游戏真正用到的那几处:send + onmessage/onopen/onclose 访问器 // (module_61860 里游戏就是 `ws.onopen/onclose/onmessage = fn`,不用 addEventListener); // **不碰 addEventListener**——包装它会破坏 removeEventListener 的按引用移除; // 3) **热路径不编码**:send/onmessage 里只做一次内存拷贝,base64 推迟到 __ws_save__() 时才做, // 避免给战斗入口的收发包增加延迟。 // // 挂载 / 开关(**不碰 URL**): // 游戏用 location.hash 做路由/状态,URL 里出现任何 hash(哪怕 #hook)都会让它进不去战场, // 所以开关改用启动参数:`serve.js --ws-hook` 时才把本脚本注入 HTML;不注入就是干净页面。 // 临时关闭(不改启动参数):控制台执行 localStorage.hornetWsHook='0' 后刷新;'1' 或删除则恢复。 // 产物:window.__ws_capture__ 内存抓包;__ws_save__() 保存到 3DTank/names/(离开页面自动再存一份) // ============================================================ (function () { if (window.__ws_hooked__) return; if (localStorage.getItem('hornetWsHook') === '0') { console.log('[WS Hook] 已被 localStorage.hornetWsHook=0 关闭'); return; } window.__ws_hooked__ = true; var Capture = { startedAt: Date.now(), connections: [] }; window.__ws_capture__ = Capture; var Proto = window.WebSocket.prototype; // 只动原型,window.WebSocket 本体保持原生 var ByWs = new WeakMap(); // 只要字节:拷贝一份(memcpy 级),不在这里做 base64 function Copy(Data) { if (Data instanceof ArrayBuffer) return new Uint8Array(Data.slice(0)); if (ArrayBuffer.isView(Data)) return new Uint8Array(Data.buffer.slice(Data.byteOffset, Data.byteOffset + Data.byteLength)); if (typeof Data === 'string') return new TextEncoder().encode(Data); return new Uint8Array(0); } function ToBase64(Bytes) { var S = ''; // String.fromCharCode.apply 会把参数数组整包展开成实参,块太大必爆栈(V8 上限约 6.5 万); // 每块 8192 字节,远低于上限,且比逐字节拼接快得多。 var Chunk = 0x2000; for (var I = 0; I < Bytes.length; I += Chunk) { S += String.fromCharCode.apply(null, Bytes.subarray(I, I + Chunk)); } return btoa(S); } function Info(Ws) { var I = ByWs.get(Ws); if (!I) { I = { id: Capture.connections.length, url: '', openTime: null, closeCode: null, messages: [] }; ByWs.set(Ws, I); Capture.connections.push(I); } if (!I.url && Ws.url) I.url = String(Ws.url); return I; } function Record(Ws, Dir, Data) { var I = Info(Ws); var B = Copy(Data); I.messages.push({ d: Dir, t: Date.now(), n: B.length, v: B }); } // ① send(原型级):只拷贝,不编码 var OrigSend = Proto.send; Proto.send = function (Data) { try { Record(this, 'c2s', Data); } catch (E) {} return OrigSend.apply(this, arguments); }; // ② onmessage / onopen / onclose 访问器(原型级):游戏用的就是这几个属性 function WrapAccessor(Name, OnEvent) { var D = Object.getOwnPropertyDescriptor(Proto, Name); if (!D || !D.set || !D.get) return; Object.defineProperty(Proto, Name, { configurable: true, enumerable: D.enumerable, get: function () { var F = D.get.call(this); return F && F.__hbOrig ? F.__hbOrig : F; // 还原原始处理函数,保持 `ws.onmessage === fn` }, set: function (Fn) { if (typeof Fn !== 'function') return D.set.call(this, Fn); var Self = this; var Wrapped = function (Ev) { try { OnEvent(Self, Ev); } catch (E) {} return Fn.call(this, Ev); }; Wrapped.__hbOrig = Fn; return D.set.call(this, Wrapped); }, }); } WrapAccessor('onmessage', function (W, Ev) { Record(W, 's2c', Ev.data); }); WrapAccessor('onopen', function (W) { var I = Info(W); I.openTime = Date.now(); // 直接在控制台可见:用来确认客户端有没有按服务端下发的 host/port 切换服务器 console.log('[WS] 打开 ' + (I.url || '(未知)')); }); WrapAccessor('onclose', function (W, Ev) { var I = Info(W); I.closeCode = Ev && Ev.code; console.log('[WS] 关闭 ' + (I.url || '(未知)') + ' code=' + I.closeCode + ' 存活=' + (I.openTime ? ((Date.now() - I.openTime) + 'ms') : '?')); }); function Serialize() { return JSON.stringify({ startedAt: Capture.startedAt, connections: Capture.connections.map(function (C) { return { id: C.id, url: C.url, openTime: C.openTime, closeCode: C.closeCode, messages: C.messages.map(function (M) { return { d: M.d, t: M.t, n: M.n, b: ToBase64(M.v) }; }), }; }), }); } function Save() { var Json = Serialize(); var Url = '/__dump__/ws_capture_' + new Date().toISOString().replace(/[:.]/g, '-') + '.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__ = Save; window.addEventListener('beforeunload', function () { try { navigator.sendBeacon('/__dump__/ws_capture_auto_' + Date.now() + '.json', new Blob([Serialize()], { type: 'application/json' })); } catch (E) {} }); console.log('[WS Hook] 已挂载(最小入侵:仅 prototype.send + on* 访问器,热路径只拷贝):__ws_save__() 存到 3DTank/names/'); })();