Enhance Mihomo WebSocket functionality and testing for connections
- Added support for WebSocket connections to the `/connections` endpoint, allowing for improved handling of WebSocket requests. - Updated the `useMihomoWebSocketTunnel` function to accept `Sec-WebSocket-Key` for `/connections`, enhancing compatibility with various proxies. - Introduced new test cases in `mihomo_test.go` to validate WebSocket tunnel behavior for connections, ensuring comprehensive coverage. - Enhanced the `applyMihomoWSDialAuth` function to manage token handling for WebSocket connections, improving security and functionality. - Updated Svelte components to implement native WebSocket loops for connections, providing real-time data updates and improved user experience.
This commit is contained in:
@@ -23,7 +23,7 @@ export function mihomoUrl(alias: string, path: string): string {
|
||||
/**
|
||||
* WebSocket к шлюзу: `ws(s)://…/api/{alias}/mihomo/{path}`.
|
||||
* Authorization к Mihomo подставляет шлюз; браузеру токен не нужен.
|
||||
* Обзор Mihomo: нативный WS для /traffic и /memory + резервный потоковый GET при необходимости.
|
||||
* Обзор Mihomo: WS для /traffic, /memory, /connections (токен на upstream подставляет шлюз; для /connections — ?token=…).
|
||||
*/
|
||||
export function mihomoWsUrl(alias: string, path: string): string {
|
||||
const p = path.replace(/^\/+/, '');
|
||||
|
||||
@@ -18,6 +18,10 @@ export type MihomoProxyEntry = {
|
||||
export type MihomoConnectionsResponse = {
|
||||
total?: number;
|
||||
connections?: MihomoConnection[];
|
||||
/** Снимок по WebSocket /connections (Mihomo). */
|
||||
downloadTotal?: number;
|
||||
uploadTotal?: number;
|
||||
memory?: number;
|
||||
};
|
||||
|
||||
export type MihomoConnection = {
|
||||
|
||||
@@ -371,6 +371,7 @@
|
||||
// Только нативный WS: без HTTP-обхода, чтобы сразу видеть проблемы апгрейда.
|
||||
void trafficWsLoop(a, signal);
|
||||
void memoryWsLoop(a, signal);
|
||||
void connectionsWsLoop(a, signal);
|
||||
}
|
||||
|
||||
function topProxyCounts(conns: MihomoConnectionsResponse['connections']): { name: string; n: number }[] {
|
||||
@@ -387,28 +388,96 @@
|
||||
.slice(0, 8);
|
||||
}
|
||||
|
||||
/** Снимок как у WebSocket /connections у Mihomo (downloadTotal, uploadTotal, memory, connections). */
|
||||
function applyConnectionsSnapshot(j: MihomoConnectionsResponse) {
|
||||
const list = j.connections ?? [];
|
||||
connTotal = typeof j.total === 'number' ? j.total : list.length;
|
||||
if (typeof j.uploadTotal === 'number' && typeof j.downloadTotal === 'number') {
|
||||
totalUp = j.uploadTotal;
|
||||
totalDown = j.downloadTotal;
|
||||
}
|
||||
if (typeof j.memory === 'number') {
|
||||
memKB = j.memory;
|
||||
histMem = [...histMem, memKB];
|
||||
while (histMem.length > MAX_POINTS) histMem.shift();
|
||||
}
|
||||
let t = 0,
|
||||
u = 0;
|
||||
for (const c of list) {
|
||||
const net = (c.metadata?.network ?? '').toLowerCase();
|
||||
if (net === 'tcp') t++;
|
||||
else if (net === 'udp') u++;
|
||||
}
|
||||
tcpN = t;
|
||||
udpN = u;
|
||||
histConn = [...histConn, connTotal];
|
||||
while (histConn.length > MAX_POINTS) histConn.shift();
|
||||
topList = topProxyCounts(list);
|
||||
}
|
||||
|
||||
async function pollConnections(a: string) {
|
||||
try {
|
||||
const j = await fetchMihomoJson<MihomoConnectionsResponse>(a, 'connections');
|
||||
const list = j.connections ?? [];
|
||||
connTotal = typeof j.total === 'number' ? j.total : list.length;
|
||||
let t = 0,
|
||||
u = 0;
|
||||
for (const c of list) {
|
||||
const net = (c.metadata?.network ?? '').toLowerCase();
|
||||
if (net === 'tcp') t++;
|
||||
else if (net === 'udp') u++;
|
||||
}
|
||||
tcpN = t;
|
||||
udpN = u;
|
||||
histConn = [...histConn, connTotal];
|
||||
while (histConn.length > MAX_POINTS) histConn.shift();
|
||||
topList = topProxyCounts(list);
|
||||
applyConnectionsSnapshot(j);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket /connections: один JSON на кадр с полным снимком (как wss://…/connections?token=… у Mihomo).
|
||||
* Шлюз сам подставляет token на upstream; браузер без секрета.
|
||||
*/
|
||||
async function connectionsWsLoop(a: string, signal: AbortSignal) {
|
||||
while (!signal.aborted) {
|
||||
await new Promise<void>((resolve) => {
|
||||
let settled = false;
|
||||
const done = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve();
|
||||
};
|
||||
let ws: WebSocket;
|
||||
try {
|
||||
ws = new WebSocket(mihomoWsUrl(a, 'connections'));
|
||||
} catch {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const onAbort = () => {
|
||||
try {
|
||||
ws.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
done();
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const j = JSON.parse(String(ev.data)) as MihomoConnectionsResponse;
|
||||
applyConnectionsSnapshot(j);
|
||||
} catch {
|
||||
/* ignore frame */
|
||||
}
|
||||
};
|
||||
ws.onerror = () => {
|
||||
try {
|
||||
ws.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
ws.onclose = () => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
done();
|
||||
};
|
||||
});
|
||||
if (signal.aborted) break;
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const a = alias;
|
||||
if (!a || !browser) return;
|
||||
@@ -425,7 +494,8 @@
|
||||
const streamAc = new AbortController();
|
||||
runTrafficMemoryRealtime(a, streamAc.signal);
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = setInterval(() => void pollConnections(a), 2000);
|
||||
// Резервный HTTP, если WS /connections недоступен.
|
||||
pollTimer = setInterval(() => void pollConnections(a), 15000);
|
||||
void pollConnections(a);
|
||||
return () => {
|
||||
streamAc.abort();
|
||||
|
||||
Reference in New Issue
Block a user