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:
1 parent
c021d5c164
commit
240892c9b5
1 file changed
+79
-36
+79
-36
@@ -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/');
|
||||
})();
|
||||
Reference in new issue
Block a user