fix(mirror): ws 抓包降到最小入侵(只包 send + on* 访问器,热路径只拷贝不编码)

- 不再包装 addEventListener(会破坏 removeEventListener 的按引用移除);游戏只用 onopen/onclose/onmessage

- 逐帧 base64 推迟到 __ws_save__(),去掉战斗入口的收发延迟
This commit is contained in:
TermiNexus committed 2026-10-11 21:31:16 +08:00
1 parent 240892c9b5
commit d6758b14e1
1 file changed
+35 -48
+35 -48
View File
@@ -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__/<name> → 3DTank/names/<name>
// 产物: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/');
})();