Files
TankIC/sockets/ws_hook.js
T
TermiNexus d6758b14e1 fix(mirror): ws 抓包降到最小入侵(只包 send + on* 访问器,热路径只拷贝不编码)
- 不再包装 addEventListener(会破坏 removeEventListener 的按引用移除);游戏只用 onopen/onclose/onmessage

- 逐帧 base64 推迟到 __ws_save__(),去掉战斗入口的收发延迟
2026-10-11 21:31:16 +08:00

132 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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__() 时才做,
// 避免给战斗入口的收发包增加延迟。
//
// 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入。
// 开关:**默认不启用**;URL hash 加 #hook 才抓(#nohook 强制关)。
// 产物:window.__ws_capture__ 内存抓包;__ws_save__() 保存到 3DTank/names/(离开页面自动再存一份)
// ============================================================
(function () {
if (window.__ws_hooked__) return;
if (!/#hook/.test(location.hash) || /#nohook/.test(location.hash)) {
console.log('[WS Hook] 未启用(默认关闭;URL 加 #hook 开启抓包)');
window.__ws_save__ = function () {
console.warn('[WS Hook] 未启用:请在 URL 末尾加 #hook(如 http://localhost:8080/#hook)后整页刷新,再调用 __ws_save__()');
return Promise.resolve(null);
};
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) { Info(W).openTime = Date.now(); });
WrapAccessor('onclose', function (W, Ev) { Info(W).closeCode = Ev && Ev.code; });
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/');
})();