139 lines
6.2 KiB
JavaScript
139 lines
6.2 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__() 时才做,
|
||
// 避免给战斗入口的收发包增加延迟。
|
||
//
|
||
// 挂载 / 开关(**不碰 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/');
|
||
})();
|