- 不再包装 addEventListener(会破坏 removeEventListener 的按引用移除);游戏只用 onopen/onclose/onmessage - 逐帧 base64 推迟到 __ws_save__(),去掉战斗入口的收发延迟
132 lines
5.8 KiB
JavaScript
132 lines
5.8 KiB
JavaScript
// ============================================================
|
||
// 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/');
|
||
})();
|