feat(mirror): 页面内 window.WebSocket 抓包(/ws_hook.js),用于与中继转存逐帧对比
- sockets/ws_hook.js:hook window.WebSocket,按收发顺序记录 {d,t,n,b(base64)};__ws_save__() 存到 3DTank/names/,离开页面自动 sendBeacon;#nohook 关闭
- serve.js:根路径提供 /ws_hook.js(no-cache)
- web/index.html:在主脚本之前引入,确保先于游戏创建 WebSocket
This commit is contained in:
1 parent
bd35ca1c12
commit
418298e2ec
2 files changed
+94
-178
No files matched your search
@@ -208,6 +208,23 @@ const Server = http.createServer((Req, Res) => {
|
||||
}).end(Sw);
|
||||
return;
|
||||
}
|
||||
// /ws_hook.js:页面内 window.WebSocket 抓包脚本(在主脚本之前引入)。
|
||||
// 传输仍由浏览器发起(服务端看不到差别),用于与中继转存做逐帧对比;#nohook 可关闭。
|
||||
if (UrlPath.split('?')[0] === '/ws_hook.js') {
|
||||
const HookFile = path.join(ROOT, 'sockets', 'ws_hook.js');
|
||||
if (!fs.existsSync(HookFile)) {
|
||||
Res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }).end('ws_hook.js not found');
|
||||
return;
|
||||
}
|
||||
const Hook = fs.readFileSync(HookFile);
|
||||
Res.writeHead(200, {
|
||||
'Content-Type': 'application/javascript; charset=utf-8',
|
||||
'Content-Length': Hook.length,
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Hornet-WsHook': 'local',
|
||||
}).end(Hook);
|
||||
return;
|
||||
}
|
||||
// /config.xml 本地覆盖:存在 local/config.xml 时用它,把游戏指向本地 wss_server
|
||||
if (!REAL_CONFIG && UrlPath.split('?')[0] === '/config.xml' && fs.existsSync(LOCAL_CONFIG)) {
|
||||
const Conf = fs.readFileSync(LOCAL_CONFIG);
|
||||
|
||||
+77
-178
@@ -1,195 +1,94 @@
|
||||
// ============================================================
|
||||
// WebSocket 流量捕获注入脚本
|
||||
// WebSocket 流量捕获(页面内 hook,传输仍由浏览器发起)
|
||||
// ============================================================
|
||||
// 在游戏加载前注入,捕获所有 WebSocket 通信
|
||||
// 数据保存到 window.__ws_capture__ 数组
|
||||
// 使用 window.__download_ws_capture__() 下载捕获数据
|
||||
// 目的:在不改变「服务端看到的连接」的前提下,把浏览器真实的 ws 帧抓下来,
|
||||
// 用于与「中继转存」逐帧对比,定位「直连能进、中继不能进」的差异。
|
||||
//
|
||||
// 挂载:serve.js 提供 /ws_hook.js;web/index.html 在主脚本之前引入(先于游戏创建 WebSocket)。
|
||||
// 开关:URL hash 带 #nohook 则不抓(默认抓)。
|
||||
// 产物:
|
||||
// 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>
|
||||
// ============================================================
|
||||
|
||||
(function () {
|
||||
if (window.__ws_hooked__) return;
|
||||
if (/#nohook/.test(location.hash)) {
|
||||
console.log('[WS Hook] #nohook:已跳过');
|
||||
return;
|
||||
}
|
||||
window.__ws_hooked__ = true;
|
||||
|
||||
window.__ws_capture__ = [];
|
||||
var captureDir = 'ws_capture_' + Date.now();
|
||||
var Capture = { startedAt: Date.now(), connections: [] };
|
||||
window.__ws_capture__ = Capture;
|
||||
var Original = window.WebSocket;
|
||||
|
||||
function toHex(buffer) {
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var hex = [];
|
||||
for (var i = 0; i < bytes.length; i += 16) {
|
||||
var chunk = bytes.slice(i, Math.min(i + 16, bytes.length));
|
||||
var hexPart = Array.from(chunk).map(function (b) {
|
||||
return b.toString(16).padStart(2, '0');
|
||||
}).join(' ');
|
||||
var asciiPart = Array.from(chunk).map(function (b) {
|
||||
return (b >= 32 && b < 127) ? String.fromCharCode(b) : '.';
|
||||
}).join('');
|
||||
hex.push(i.toString(16).padStart(8, '0') + ' ' + hexPart.padEnd(48) + ' ' + asciiPart);
|
||||
function ToBytes(Data) {
|
||||
if (Data instanceof ArrayBuffer) return new Uint8Array(Data);
|
||||
if (ArrayBuffer.isView(Data)) return new Uint8Array(Data.buffer, Data.byteOffset, Data.byteLength);
|
||||
if (typeof Data === 'string') return new TextEncoder().encode(Data);
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
function ToBase64(Bytes) {
|
||||
var S = '';
|
||||
var Chunk = 0x8000; // 分块,避免超长字符串参数
|
||||
for (var I = 0; I < Bytes.length; I += Chunk) {
|
||||
S += String.fromCharCode.apply(null, Bytes.subarray(I, I + Chunk));
|
||||
}
|
||||
return hex.join('\n');
|
||||
return btoa(S);
|
||||
}
|
||||
|
||||
function toArrayBuffer(data) {
|
||||
if (data instanceof ArrayBuffer) return data;
|
||||
if (ArrayBuffer.isView(data)) return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
||||
if (typeof data === 'string') return new TextEncoder().encode(data).buffer;
|
||||
return data;
|
||||
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) });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Hooked.CONNECTING = Original.CONNECTING;
|
||||
Hooked.OPEN = Original.OPEN;
|
||||
Hooked.CLOSING = Original.CLOSING;
|
||||
Hooked.CLOSED = Original.CLOSED;
|
||||
Hooked.prototype = Original.prototype;
|
||||
window.WebSocket = Hooked;
|
||||
|
||||
// 保存原始 WebSocket
|
||||
var OriginalWebSocket = window.WebSocket;
|
||||
|
||||
function HookedWebSocket(url, protocols) {
|
||||
var ws = protocols ? new OriginalWebSocket(url, protocols) : new OriginalWebSocket(url);
|
||||
|
||||
console.log('[WS Hook] 连接:', url);
|
||||
|
||||
var wsId = window.__ws_capture__.length;
|
||||
var wsInfo = {
|
||||
id: wsId,
|
||||
url: url,
|
||||
openTime: null,
|
||||
messages: []
|
||||
};
|
||||
window.__ws_capture__.push(wsInfo);
|
||||
|
||||
// Hook send
|
||||
var originalSend = ws.send.bind(ws);
|
||||
ws.send = function (data) {
|
||||
var ab = toArrayBuffer(data);
|
||||
var msgInfo = {
|
||||
direction: 'send',
|
||||
time: Date.now(),
|
||||
size: ab.byteLength,
|
||||
hex: toHex(ab),
|
||||
buffer: ab
|
||||
};
|
||||
wsInfo.messages.push(msgInfo);
|
||||
console.log('[WS Hook] #' + wsId + ' >>> 发送 ' + ab.byteLength + ' 字节:');
|
||||
console.log(toHex(ab));
|
||||
return originalSend(data);
|
||||
};
|
||||
|
||||
// Hook onmessage
|
||||
var originalOnMessage = null;
|
||||
Object.defineProperty(ws, 'onmessage', {
|
||||
get: function () { return originalOnMessage; },
|
||||
set: function (fn) {
|
||||
originalOnMessage = fn;
|
||||
// Wrap the handler
|
||||
ws.addEventListener('message', function (event) {
|
||||
var ab = toArrayBuffer(event.data);
|
||||
var msgInfo = {
|
||||
direction: 'recv',
|
||||
time: Date.now(),
|
||||
size: ab.byteLength,
|
||||
hex: toHex(ab),
|
||||
buffer: ab
|
||||
};
|
||||
wsInfo.messages.push(msgInfo);
|
||||
console.log('[WS Hook] #' + wsId + ' <<< 接收 ' + ab.byteLength + ' 字节:');
|
||||
console.log(toHex(ab));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Also hook addEventListener for 'message'
|
||||
var originalAddEventListener = ws.addEventListener.bind(ws);
|
||||
ws.addEventListener = function (type, listener, options) {
|
||||
if (type === 'message') {
|
||||
var wrappedListener = function (event) {
|
||||
var ab = toArrayBuffer(event.data);
|
||||
var msgInfo = {
|
||||
direction: 'recv',
|
||||
time: Date.now(),
|
||||
size: ab.byteLength,
|
||||
hex: toHex(ab),
|
||||
buffer: ab
|
||||
};
|
||||
wsInfo.messages.push(msgInfo);
|
||||
console.log('[WS Hook] #' + wsId + ' <<< 接收 ' + ab.byteLength + ' 字节:');
|
||||
console.log(toHex(ab));
|
||||
listener(event);
|
||||
};
|
||||
return originalAddEventListener(type, wrappedListener, options);
|
||||
}
|
||||
return originalAddEventListener(type, listener, options);
|
||||
};
|
||||
|
||||
ws.addEventListener('open', function () {
|
||||
wsInfo.openTime = Date.now();
|
||||
console.log('[WS Hook] #' + wsId + ' 连接成功');
|
||||
});
|
||||
|
||||
ws.addEventListener('close', function (event) {
|
||||
console.log('[WS Hook] #' + wsId + ' 连接关闭 (code=' + event.code + ')');
|
||||
});
|
||||
|
||||
ws.addEventListener('error', function (event) {
|
||||
console.log('[WS Hook] #' + wsId + ' 连接错误:', event);
|
||||
});
|
||||
|
||||
return ws;
|
||||
function Stamp() {
|
||||
return new Date().toISOString().replace(/[:.]/g, '-');
|
||||
}
|
||||
function Save(Prefix) {
|
||||
var Json = JSON.stringify(Capture);
|
||||
var Url = '/__dump__/' + (Prefix || 'ws_capture_') + Stamp() + '.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__ = function () { return Save('ws_capture_'); };
|
||||
|
||||
// 复制静态属性
|
||||
HookedWebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
|
||||
HookedWebSocket.OPEN = OriginalWebSocket.OPEN;
|
||||
HookedWebSocket.CLOSING = OriginalWebSocket.CLOSING;
|
||||
HookedWebSocket.CLOSED = OriginalWebSocket.CLOSED;
|
||||
HookedWebSocket.prototype = OriginalWebSocket.prototype;
|
||||
window.addEventListener('beforeunload', function () {
|
||||
try {
|
||||
var Blob = new Blob([JSON.stringify(Capture)], { type: 'application/json' });
|
||||
navigator.sendBeacon('/__dump__/ws_capture_auto_' + Date.now() + '.json', Blob);
|
||||
} catch (E) {}
|
||||
});
|
||||
|
||||
window.WebSocket = HookedWebSocket;
|
||||
|
||||
// 下载捕获数据
|
||||
window.__download_ws_capture__ = function () {
|
||||
var data = window.__ws_capture__.map(function (ws) {
|
||||
return {
|
||||
id: ws.id,
|
||||
url: ws.url,
|
||||
openTime: ws.openTime,
|
||||
messages: ws.messages.map(function (m) {
|
||||
// Convert buffer to base64 for JSON serialization
|
||||
var bytes = new Uint8Array(m.buffer);
|
||||
var binary = '';
|
||||
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return {
|
||||
direction: m.direction,
|
||||
time: m.time,
|
||||
size: m.size,
|
||||
hex: m.hex,
|
||||
base64: btoa(binary)
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'ws_capture_' + Date.now() + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
console.log('[WS Hook] 已下载捕获数据,共 ' + data.length + ' 个连接');
|
||||
};
|
||||
|
||||
// 导出单个消息的二进制
|
||||
window.__export_ws_binary__ = function (wsId, msgIdx) {
|
||||
var ws = window.__ws_capture__[wsId];
|
||||
if (!ws) { console.log('未找到 WS #' + wsId); return; }
|
||||
var msg = ws.messages[msgIdx];
|
||||
if (!msg) { console.log('未找到消息 #' + msgIdx); return; }
|
||||
var blob = new Blob([msg.buffer], { type: 'application/octet-stream' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = msg.direction + '_' + wsId + '_' + msgIdx + '.bin';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
console.log('[WS Hook] WebSocket 捕获脚本已加载');
|
||||
console.log('[WS Hook] 使用 __download_ws_capture__() 下载所有捕获数据');
|
||||
console.log('[WS Hook] 使用 __export_ws_binary__(wsId, msgIdx) 导出单个消息');
|
||||
console.log('[WS Hook] 已挂载:window.__ws_capture__(内存)/ __ws_save__()(存到 3DTank/names/)');
|
||||
})();
|
||||
Reference in new issue
Block a user