fix(mirror): ws 抓包改为原型级 hook(不动 window.WebSocket 构造函数),避免被游戏察觉

- 实测替换构造函数会让 WebSocket.toString() 不再是 native code → 进不去战场;改为只包 WebSocket.prototype 的 send / addEventListener / onmessage|onopen|onclose 访问器

- window.WebSocket 的名字/长度/toString/instanceof/静态常量全部保持原生
This commit is contained in:
TermiNexus committed 2026-10-11 20:54:38 +08:00
1 parent c021d5c164
commit 240892c9b5
1 file changed
+79 -36
+79 -36
View File
@@ -4,10 +4,16 @@
// 目的:在不改变「服务端看到的连接」的前提下,把浏览器真实的 ws 帧抓下来,
// 用于与「中继转存」逐帧对比,定位「直连能进、中继不能进」的差异。
//
// 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入(先于游戏创建 WebSocket)。
// 开关:**默认不启用**;URL hash 加 #hook 才抓(#nohook 可强制关)。
// 关键设计(避免被游戏察觉):
// **不替换 window.WebSocket 构造函数**,只在原型上补:
// WebSocket.prototype.send / addEventListener / onmessage|onopen|onclose 访问器
// → WebSocket.name / .length / .toString() / instanceof / 静态常量 全部保持原生。
// (替换构造函数会让 WebSocket.toString() 不再是 "[native code]",实测会进不去战场。)
//
// 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入。
// 开关:**默认不启用**;URL hash 加 #hook 才抓(#nohook 强制关)。
// 产物:
// window.__ws_capture__ —— 内存里的抓包(connections[].messages[] = {d,t,n,b},
// 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>
@@ -16,7 +22,6 @@
if (window.__ws_hooked__) return;
if (!/#hook/.test(location.hash) || /#nohook/.test(location.hash)) {
console.log('[WS Hook] 未启用(默认关闭;URL 加 #hook 开启抓包)');
// 兜底:即使没启用也定义,调用时提示怎么开,避免"__ws_save__ 不存在"
window.__ws_save__ = function () {
console.warn('[WS Hook] 未启用:请在 URL 末尾加 #hook(如 http://localhost:8080/#hook)后整页刷新,再调用 __ws_save__()');
return Promise.resolve(null);
@@ -27,7 +32,8 @@
var Capture = { startedAt: Date.now(), connections: [] };
window.__ws_capture__ = Capture;
var Original = window.WebSocket;
var Proto = window.WebSocket.prototype; // 只动原型,window.WebSocket 本体保持原生
var ByWs = new WeakMap();
function ToBytes(Data) {
if (Data instanceof ArrayBuffer) return new Uint8Array(Data);
@@ -37,7 +43,7 @@
}
function ToBase64(Bytes) {
var S = '';
// 注意:String.fromCharCode.apply 会把参数数组整包展开成实参,块太大必然爆栈
// 注意:String.fromCharCode.apply 会把参数数组整包展开成实参,块太大必爆栈
//(V8 实参上限约 6.5 万)。这里每块 8192 字节,远低于上限,且比逐字节拼接快得多。
var Chunk = 0x2000;
for (var I = 0; I < Bytes.length; I += Chunk) {
@@ -46,41 +52,78 @@
return btoa(S);
}
function Hooked(Url, Protocols) {
var Ws = Protocols ? new Original(Url, Protocols) : new Original(Url);
var Info = { id: Capture.connections.length, url: String(Url), openTime: null, closeCode: null, messages: [] };
Capture.connections.push(Info);
console.log('[WS Hook] #' + Info.id + ' 连接 ' + Info.url);
function Record(Dir, Data) {
var Bytes = ToBytes(Data);
Info.messages.push({ d: Dir, t: Date.now(), n: Bytes.length, b: ToBase64(Bytes) });
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);
}
var OrigSend = Ws.send;
Ws.send = function (Data) {
Record('c2s', Data);
return OrigSend.apply(Ws, arguments);
};
Ws.addEventListener('message', function (Ev) { Record('s2c', Ev.data); });
Ws.addEventListener('open', function () { Info.openTime = Date.now(); });
Ws.addEventListener('close', function (Ev) { Info.closeCode = Ev.code; });
return Ws;
if (!I.url && Ws.url) I.url = String(Ws.url);
return I;
}
Hooked.CONNECTING = Original.CONNECTING;
Hooked.OPEN = Original.OPEN;
Hooked.CLOSING = Original.CLOSING;
Hooked.CLOSED = Original.CLOSED;
Hooked.prototype = Original.prototype;
window.WebSocket = Hooked;
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) });
}
// ① 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 属性访问器(原型级)
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 Stamp() {
return new Date().toISOString().replace(/[:.]/g, '-');
}
function Save(Prefix) {
function Save() {
var Json = JSON.stringify(Capture);
var Url = '/__dump__/' + (Prefix || 'ws_capture_') + Stamp() + '.json';
var Url = '/__dump__/ws_capture_' + Stamp() + '.json';
return fetch(Url, { method: 'POST', body: Json })
.then(function (R) {
console.log('[WS Hook] 已保存 ' + Url + '(' + Json.length + ' 字节,' + Capture.connections.length + ' 个连接)');
@@ -88,7 +131,7 @@
})
.catch(function (E) { console.warn('[WS Hook] 保存失败: ' + (E && E.message)); });
}
window.__ws_save__ = function () { return Save('ws_capture_'); };
window.__ws_save__ = Save;
window.addEventListener('beforeunload', function () {
try {
@@ -97,5 +140,5 @@
} catch (E) {}
});
console.log('[WS Hook] 已挂载:window.__ws_capture__(内存)/ __ws_save__()(存到 3DTank/names/)');
console.log('[WS Hook] 已挂载(原型级,不改构造函数):__ws_save__() 存到 3DTank/names/');
})();