Files

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