From d6758b14e1bd16460c1c7fe728dabb1188c06d69 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 11 Oct 2026 21:31:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(mirror):=20ws=20=E6=8A=93=E5=8C=85=E9=99=8D?= =?UTF-8?q?=E5=88=B0=E6=9C=80=E5=B0=8F=E5=85=A5=E4=BE=B5=EF=BC=88=E5=8F=AA?= =?UTF-8?q?=E5=8C=85=20send=20+=20on*=20=E8=AE=BF=E9=97=AE=E5=99=A8?= =?UTF-8?q?=EF=BC=8C=E7=83=AD=E8=B7=AF=E5=BE=84=E5=8F=AA=E6=8B=B7=E8=B4=9D?= =?UTF-8?q?=E4=B8=8D=E7=BC=96=E7=A0=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 不再包装 addEventListener(会破坏 removeEventListener 的按引用移除);游戏只用 onopen/onclose/onmessage - 逐帧 base64 推迟到 __ws_save__(),去掉战斗入口的收发延迟 --- sockets/ws_hook.js | 83 +++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/sockets/ws_hook.js b/sockets/ws_hook.js index b61c371..3e2842d 100644 --- a/sockets/ws_hook.js +++ b/sockets/ws_hook.js @@ -1,22 +1,20 @@ // ============================================================ // WebSocket 流量捕获(页面内 hook,传输仍由浏览器发起) // ============================================================ -// 目的:在不改变「服务端看到的连接」的前提下,把浏览器真实的 ws 帧抓下来, -// 用于与「中继转存」逐帧对比,定位「直连能进、中继不能进」的差异。 +// 目的:把浏览器真实的 ws 帧旁听下来,用于与「中继转存」逐帧对比。 // -// 关键设计(避免被游戏察觉): -// **不替换 window.WebSocket 构造函数**,只在原型上补: -// WebSocket.prototype.send / addEventListener / onmessage|onopen|onclose 访问器 -// → WebSocket.name / .length / .toString() / instanceof / 静态常量 全部保持原生。 -// (替换构造函数会让 WebSocket.toString() 不再是 "[native code]",实测会进不去战场。) +// 关键设计(尽量不被游戏察觉 / 不拖慢游戏): +// 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__() 时才做, +// 避免给战斗入口的收发包增加延迟。 // // 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入。 // 开关:**默认不启用**;URL hash 加 #hook 才抓(#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/ +// 产物:window.__ws_capture__ 内存抓包;__ws_save__() 保存到 3DTank/names/(离开页面自动再存一份) // ============================================================ (function () { if (window.__ws_hooked__) return; @@ -35,16 +33,17 @@ var Proto = window.WebSocket.prototype; // 只动原型,window.WebSocket 本体保持原生 var ByWs = new WeakMap(); - function ToBytes(Data) { - if (Data instanceof ArrayBuffer) return new Uint8Array(Data); - if (ArrayBuffer.isView(Data)) return new Uint8Array(Data.buffer, Data.byteOffset, Data.byteLength); + // 只要字节:拷贝一份(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 字节,远低于上限,且比逐字节拼接快得多。 + // 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)); @@ -64,37 +63,18 @@ } function Record(Ws, Dir, Data) { var I = Info(Ws); - var B = ToBytes(Data); - I.messages.push({ d: Dir, t: Date.now(), n: B.length, b: ToBase64(B) }); + var B = Copy(Data); + I.messages.push({ d: Dir, t: Date.now(), n: B.length, v: B }); } - // ① send(原型级) + // ① send(原型级):只拷贝,不编码 var OrigSend = Proto.send; Proto.send = function (Data) { try { Record(this, 'c2s', Data); } catch (E) {} return OrigSend.apply(this, arguments); }; - // ② addEventListener(原型级,只包 message/open/close) - var OrigAdd = Proto.addEventListener; - Proto.addEventListener = function (Type, Listener, Opts) { - if (typeof Listener !== 'function') return OrigAdd.call(this, Type, Listener, Opts); - var Self = this; - var Wrapped; - if (Type === 'message') { - Wrapped = function (Ev) { try { Record(Self, 's2c', Ev.data); } catch (E) {} return Listener.call(this, Ev); }; - } else if (Type === 'open') { - Wrapped = function (Ev) { try { Info(Self).openTime = Date.now(); } catch (E) {} return Listener.call(this, Ev); }; - } else if (Type === 'close') { - Wrapped = function (Ev) { try { Info(Self).closeCode = Ev && Ev.code; } catch (E) {} return Listener.call(this, Ev); }; - } else { - return OrigAdd.call(this, Type, Listener, Opts); - } - Wrapped.__hbOrig = Listener; - return OrigAdd.call(this, Type, Wrapped, Opts); - }; - - // ③ onmessage / onopen / onclose 属性访问器(原型级) + // ② onmessage / onopen / onclose 访问器(原型级):游戏用的就是这几个属性 function WrapAccessor(Name, OnEvent) { var D = Object.getOwnPropertyDescriptor(Proto, Name); if (!D || !D.set || !D.get) return; @@ -103,7 +83,7 @@ enumerable: D.enumerable, get: function () { var F = D.get.call(this); - return F && F.__hbOrig ? F.__hbOrig : F; // 还原本来的处理函数,保持 `ws.onmessage === fn` 成立 + return F && F.__hbOrig ? F.__hbOrig : F; // 还原原始处理函数,保持 `ws.onmessage === fn` }, set: function (Fn) { if (typeof Fn !== 'function') return D.set.call(this, Fn); @@ -118,12 +98,20 @@ WrapAccessor('onopen', function (W) { Info(W).openTime = Date.now(); }); WrapAccessor('onclose', function (W, Ev) { Info(W).closeCode = Ev && Ev.code; }); - function Stamp() { - return new Date().toISOString().replace(/[:.]/g, '-'); + 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 = JSON.stringify(Capture); - var Url = '/__dump__/ws_capture_' + Stamp() + '.json'; + 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 + ' 个连接)'); @@ -135,10 +123,9 @@ 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); + navigator.sendBeacon('/__dump__/ws_capture_auto_' + Date.now() + '.json', new Blob([Serialize()], { type: 'application/json' })); } catch (E) {} }); - console.log('[WS Hook] 已挂载(原型级,不改构造函数):__ws_save__() 存到 3DTank/names/'); + console.log('[WS Hook] 已挂载(最小入侵:仅 prototype.send + on* 访问器,热路径只拷贝):__ws_save__() 存到 3DTank/names/'); })();