diff --git a/package.json b/package.json index 282e938..9cdd37f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dayjs": "^1.11.13", "express": "^5.0.0", "webdav": "^5.7.1", + "ws": "^8.18.0", "yaml": "^2.5.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83be4c5..8bed1a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: webdav: specifier: ^5.7.1 version: 5.7.1 + ws: + specifier: ^8.18.0 + version: 8.18.0 yaml: specifier: ^2.5.1 version: 2.5.1 @@ -5264,6 +5267,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -11849,6 +11864,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + xmlbuilder@15.1.1: {} y18n@5.0.8: {} diff --git a/scripts/prepare.mjs b/scripts/prepare.mjs index 09db6f4..1083238 100644 --- a/scripts/prepare.mjs +++ b/scripts/prepare.mjs @@ -288,7 +288,7 @@ const resolveMonitor = async () => { if (fs.existsSync(targetPath)) { fs.rmSync(targetPath, { recursive: true }) } - zip.extractAllTo(resDir, true) + zip.extractAllTo(targetPath, true) console.log(`[INFO]: TrafficMonitor finished`) } diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 82ae9f1..7f49917 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -1,116 +1,78 @@ -import net from 'net' -import { getRuntimeConfig } from './factory' +import axios, { AxiosInstance } from 'axios' import { getAppConfig, getControledMihomoConfig } from '../config' import { mainWindow } from '..' +import WebSocket from 'ws' import { tray } from '../resolve/tray' import { calcTraffic } from '../utils/calc' +import { getRuntimeConfig } from './factory' import { join } from 'path' import { mihomoWorkDir } from '../utils/dirs' -type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' - -let mihomoTrafficWs: net.Socket | null = null +let axiosIns: AxiosInstance = null! +let mihomoTrafficWs: WebSocket | null = null let trafficRetry = 10 -let mihomoMemoryWs: net.Socket | null = null +let mihomoMemoryWs: WebSocket | null = null let memoryRetry = 10 -let mihomoLogsWs: net.Socket | null = null +let mihomoLogsWs: WebSocket | null = null let logsRetry = 10 -let mihomoConnectionsWs: net.Socket | null = null +let mihomoConnectionsWs: WebSocket | null = null let connectionsRetry = 10 -function trimJson(data: string): string { - if (data.trim().length === 0) return '' - const start = data.indexOf('{') - const end = data.lastIndexOf('}') - return data.slice(start, end + 1) -} - -async function mihomoHttp(method: HttpMethod, path: string, data?: object): Promise { +export const getAxios = async (force: boolean = false): Promise => { const { 'external-controller-pipe': mihomoPipe = '\\\\.\\pipe\\MihomoParty\\mihomo', 'external-controller-unix': mihomoUnix = 'mihomo-party.sock' } = await getControledMihomoConfig() - return new Promise((resolve, reject) => { - const client = net.connect( - process.platform === 'win32' ? mihomoPipe : join(mihomoWorkDir(), mihomoUnix) - ) - const parseResult = (str: string): void => { - try { - const data = str.split('\r\n\r\n')[1] - const json = trimJson(data) - if (str.includes('HTTP/1.1 4') || str.includes('HTTP/1.1 5')) { - reject(json ? JSON.parse(json) : data) - } else { - resolve(json ? JSON.parse(json) : undefined) - } - } catch (e) { - reject(e) - } finally { - client.end() - } - } - let buffer = '' - client.on('data', function (res) { - if (res.toString().includes('Transfer-Encoding: chunked') || buffer !== '') { - buffer += res.toString() - if (buffer.endsWith('\r\n\r\n')) { - parseResult(buffer) - } - } else { - parseResult(res.toString()) - } - }) - client.on('error', function (error) { - reject(error) - }) - if (data) { - const json = JSON.stringify(data) - client.write( - `${method} ${path} HTTP/1.1\r\nHost: mihomo-party\r\nContent-Type: application/json\r\nContent-Length: ${Buffer.from(json).length}\r\n\r\n${json}` - ) - } else { - client.write(`${method} ${path} HTTP/1.1\r\nHost: mihomo-party\r\n\r\n`) - } + if (axiosIns && !force) return axiosIns + + axiosIns = axios.create({ + baseURL: `http://localhost`, + socketPath: process.platform === 'win32' ? mihomoPipe : join(mihomoWorkDir(), mihomoUnix), + timeout: 15000 }) + + axiosIns.interceptors.response.use( + (response) => { + return response.data + }, + (error) => { + if (error.response && error.response.data) { + return Promise.reject(error.response.data) + } + return Promise.reject(error) + } + ) + return axiosIns } -async function mihomoWs(path: string): Promise { - const { - 'external-controller-pipe': mihomoPipe = '\\\\.\\pipe\\MihomoParty\\mihomo', - 'external-controller-unix': mihomoUnix = 'mihomo-party.sock' - } = await getControledMihomoConfig() - const client = net.connect( - process.platform === 'win32' ? mihomoPipe : join(mihomoWorkDir(), mihomoUnix) - ) - client.write( - `GET ${path} HTTP/1.1\r\nHost: mihomo-party\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: xxxxxxxxxxxxxxxxxxxxxxxx\r\n\r\n` - ) - - return client -} - -export const mihomoVersion = async (): Promise => { - return await mihomoHttp('GET', '/version') +export async function mihomoVersion(): Promise { + const instance = await getAxios() + return await instance.get('/version') } export const patchMihomoConfig = async (patch: Partial): Promise => { - return await mihomoHttp('PATCH', '/configs', patch) + const instance = await getAxios() + return await instance.patch('/configs', patch) } export const mihomoCloseConnection = async (id: string): Promise => { - return await mihomoHttp('DELETE', `/connections/${id}`) + const instance = await getAxios() + return await instance.delete(`/connections/${encodeURIComponent(id)}`) } export const mihomoCloseAllConnections = async (): Promise => { - return await mihomoHttp('DELETE', '/connections') + const instance = await getAxios() + return await instance.delete('/connections') } export const mihomoRules = async (): Promise => { - return await mihomoHttp('GET', '/rules') + const instance = await getAxios() + return await instance.get('/rules') } export const mihomoProxies = async (): Promise => { - const proxies = (await mihomoHttp('GET', '/proxies')) as IMihomoProxies + const instance = await getAxios() + const proxies = (await instance.get('/proxies')) as IMihomoProxies if (!proxies.proxies['GLOBAL']) { throw new Error('GLOBAL proxy not found') } @@ -141,199 +103,271 @@ export const mihomoGroups = async (): Promise => { } export const mihomoProxyProviders = async (): Promise => { - return await mihomoHttp('GET', '/providers/proxies') + const instance = await getAxios() + return await instance.get('/providers/proxies') } export const mihomoUpdateProxyProviders = async (name: string): Promise => { - return await mihomoHttp('PUT', `/providers/proxies/${encodeURIComponent(name)}`) + const instance = await getAxios() + return await instance.put(`/providers/proxies/${encodeURIComponent(name)}`) } export const mihomoRuleProviders = async (): Promise => { - return await mihomoHttp('GET', '/providers/rules') + const instance = await getAxios() + return await instance.get('/providers/rules') } export const mihomoUpdateRuleProviders = async (name: string): Promise => { - return await mihomoHttp('PUT', `/providers/rules/${encodeURIComponent(name)}`) + const instance = await getAxios() + return await instance.put(`/providers/rules/${encodeURIComponent(name)}`) } export const mihomoChangeProxy = async (group: string, proxy: string): Promise => { - return await mihomoHttp('PUT', `/proxies/${encodeURIComponent(group)}`, { name: proxy }) + const instance = await getAxios() + return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy }) } -export const mihomoUnfixedProxy = async (group: string): Promise => { - return await mihomoHttp('DELETE', `/proxies/${encodeURIComponent(group)}`) +export const mihomoUnfixedProxy = async (group: string): Promise => { + const instance = await getAxios() + return await instance.delete(`/proxies/${encodeURIComponent(group)}`) } export const mihomoUpgradeGeo = async (): Promise => { - return await mihomoHttp('POST', '/configs/geo') + const instance = await getAxios() + return await instance.post('/configs/geo') } export const mihomoProxyDelay = async (proxy: string, url?: string): Promise => { const appConfig = await getAppConfig() const { delayTestUrl, delayTestTimeout } = appConfig - - return await mihomoHttp( - 'GET', - `/proxies/${encodeURIComponent(proxy)}/delay?url=${encodeURIComponent(url || delayTestUrl || 'https://www.gstatic.com/generate_204')}&timeout=${delayTestTimeout || 5000}` - ) + const instance = await getAxios() + return await instance.get(`/proxies/${encodeURIComponent(proxy)}/delay`, { + params: { + url: url || delayTestUrl || 'https://www.gstatic.com/generate_204', + timeout: delayTestTimeout || 5000 + } + }) } export const mihomoGroupDelay = async (group: string, url?: string): Promise => { const appConfig = await getAppConfig() const { delayTestUrl, delayTestTimeout } = appConfig - return await mihomoHttp( - 'GET', - `/group/${encodeURIComponent(group)}/delay?url=${encodeURIComponent(url || delayTestUrl || 'https://www.gstatic.com/generate_204')}&timeout=${delayTestTimeout || 5000}` - ) + const instance = await getAxios() + return await instance.get(`/group/${encodeURIComponent(group)}/delay`, { + params: { + url: url || delayTestUrl || 'https://www.gstatic.com/generate_204', + timeout: delayTestTimeout || 5000 + } + }) } export const mihomoUpgrade = async (): Promise => { - return await mihomoHttp('POST', '/upgrade') + const instance = await getAxios() + return await instance.post('/upgrade') } export const startMihomoTraffic = async (): Promise => { await mihomoTraffic() } -export const stopMihomoTraffic = async (): Promise => { +export const stopMihomoTraffic = (): void => { if (mihomoTrafficWs) { - mihomoTrafficWs.end() + mihomoTrafficWs.removeAllListeners() + if (mihomoTrafficWs.readyState === WebSocket.OPEN) { + mihomoTrafficWs.close() + } mihomoTrafficWs = null } } const mihomoTraffic = async (): Promise => { - stopMihomoTraffic() - mihomoTrafficWs = await mihomoWs('/traffic') - mihomoTrafficWs.on('data', (data) => { + const { + 'external-controller-pipe': mihomoPipe = '\\\\.\\pipe\\MihomoParty\\mihomo', + 'external-controller-unix': mihomoUnix = 'mihomo-party.sock' + } = await getControledMihomoConfig() + + mihomoTrafficWs = new WebSocket( + `ws+unix:${process.platform === 'win32' ? mihomoPipe : join(mihomoWorkDir(), mihomoUnix)}:/traffic` + ) + + mihomoTrafficWs.onmessage = async (e): Promise => { + const data = e.data as string + const json = JSON.parse(data) as IMihomoTrafficInfo + trafficRetry = 10 try { - const json = JSON.parse(trimJson(data.toString())) as IMihomoTrafficInfo - trafficRetry = 10 mainWindow?.webContents.send('mihomoTraffic', json) - tray?.setToolTip( - 'ā†‘' + - `${calcTraffic(json.up)}/s`.padStart(9) + - '\nā†“' + - `${calcTraffic(json.down)}/s`.padStart(9) - ) + if (process.platform !== 'linux') { + tray?.setToolTip( + 'ā†‘' + + `${calcTraffic(json.up)}/s`.padStart(9) + + '\nā†“' + + `${calcTraffic(json.down)}/s`.padStart(9) + ) + } } catch { // ignore } - }) - mihomoTrafficWs.on('close', () => { + } + + mihomoTrafficWs.onclose = (): void => { if (trafficRetry) { trafficRetry-- mihomoTraffic() } - }) + } - mihomoTrafficWs.on('error', (): void => { - stopMihomoTraffic() - }) + mihomoTrafficWs.onerror = (): void => { + if (mihomoTrafficWs) { + mihomoTrafficWs.close() + mihomoTrafficWs = null + } + } } export const startMihomoMemory = async (): Promise => { await mihomoMemory() } -export const stopMihomoMemory = async (): Promise => { +export const stopMihomoMemory = (): void => { if (mihomoMemoryWs) { - mihomoMemoryWs.end() + mihomoMemoryWs.removeAllListeners() + if (mihomoMemoryWs.readyState === WebSocket.OPEN) { + mihomoMemoryWs.close() + } mihomoMemoryWs = null } } const mihomoMemory = async (): Promise => { - stopMihomoMemory() - mihomoMemoryWs = await mihomoWs('/memory') - mihomoMemoryWs.on('data', (data) => { + const { + 'external-controller-pipe': mihomoPipe = '\\\\.\\pipe\\MihomoParty\\mihomo', + 'external-controller-unix': mihomoUnix = 'mihomo-party.sock' + } = await getControledMihomoConfig() + + mihomoMemoryWs = new WebSocket( + `ws+unix:${process.platform === 'win32' ? mihomoPipe : join(mihomoWorkDir(), mihomoUnix)}:/memory` + ) + + mihomoMemoryWs.onmessage = (e): void => { + const data = e.data as string + memoryRetry = 10 try { - const json = JSON.parse(trimJson(data.toString())) as IMihomoMemoryInfo - memoryRetry = 10 - mainWindow?.webContents.send('mihomoMemory', json) + mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo) } catch { // ignore } - }) - mihomoMemoryWs.on('close', () => { + } + + mihomoMemoryWs.onclose = (): void => { if (memoryRetry) { memoryRetry-- mihomoMemory() } - }) + } - mihomoMemoryWs.on('error', (): void => { - stopMihomoMemory() - }) + mihomoMemoryWs.onerror = (): void => { + if (mihomoMemoryWs) { + mihomoMemoryWs.close() + mihomoMemoryWs = null + } + } } export const startMihomoLogs = async (): Promise => { await mihomoLogs() } -export const stopMihomoLogs = async (): Promise => { +export const stopMihomoLogs = (): void => { if (mihomoLogsWs) { - mihomoLogsWs.end() + mihomoLogsWs.removeAllListeners() + if (mihomoLogsWs.readyState === WebSocket.OPEN) { + mihomoLogsWs.close() + } mihomoLogsWs = null } } const mihomoLogs = async (): Promise => { - stopMihomoLogs() - const { 'log-level': logLevel } = await getControledMihomoConfig() - mihomoLogsWs = await mihomoWs(`/logs?level=${logLevel}`) - mihomoLogsWs.on('data', (data) => { + const { + 'external-controller-pipe': mihomoPipe = '\\\\.\\pipe\\MihomoParty\\mihomo', + 'external-controller-unix': mihomoUnix = 'mihomo-party.sock', + 'log-level': logLevel = 'info' + } = await getControledMihomoConfig() + + mihomoLogsWs = new WebSocket( + `ws+unix:${process.platform === 'win32' ? mihomoPipe : join(mihomoWorkDir(), mihomoUnix)}:/logs?level=${logLevel}` + ) + + mihomoLogsWs.onmessage = (e): void => { + const data = e.data as string + logsRetry = 10 try { - const json = JSON.parse(trimJson(data.toString())) as IMihomoLogInfo - logsRetry = 10 - mainWindow?.webContents.send('mihomoLogs', json) + mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo) } catch { // ignore } - }) - mihomoLogsWs.on('close', () => { + } + + mihomoLogsWs.onclose = (): void => { if (logsRetry) { logsRetry-- mihomoLogs() } - }) + } - mihomoLogsWs.on('error', (): void => { - stopMihomoLogs() - }) + mihomoLogsWs.onerror = (): void => { + if (mihomoLogsWs) { + mihomoLogsWs.close() + mihomoLogsWs = null + } + } } export const startMihomoConnections = async (): Promise => { await mihomoConnections() } -export const stopMihomoConnections = async (): Promise => { +export const stopMihomoConnections = (): void => { if (mihomoConnectionsWs) { - mihomoConnectionsWs.end() + mihomoConnectionsWs.removeAllListeners() + if (mihomoConnectionsWs.readyState === WebSocket.OPEN) { + mihomoConnectionsWs.close() + } mihomoConnectionsWs = null } } const mihomoConnections = async (): Promise => { - stopMihomoConnections() - mihomoConnectionsWs = await mihomoWs('/connections') - mihomoConnectionsWs.on('data', (data) => { + const { + 'external-controller-pipe': mihomoPipe = '\\\\.\\pipe\\MihomoParty\\mihomo', + 'external-controller-unix': mihomoUnix = 'mihomo-party.sock' + } = await getControledMihomoConfig() + + mihomoConnectionsWs = new WebSocket( + `ws+unix:${process.platform === 'win32' ? mihomoPipe : join(mihomoWorkDir(), mihomoUnix)}:/connections` + ) + + mihomoConnectionsWs.onmessage = (e): void => { + const data = e.data as string + connectionsRetry = 10 try { - const json = JSON.parse(trimJson(data.toString())) as IMihomoConnectionsInfo - connectionsRetry = 10 - mainWindow?.webContents.send('mihomoConnections', json) + mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) } catch { // ignore } - }) - mihomoConnectionsWs.on('close', () => { + } + + mihomoConnectionsWs.onclose = (): void => { if (connectionsRetry) { connectionsRetry-- mihomoConnections() } - }) + } - mihomoConnectionsWs.on('error', (): void => { - stopMihomoConnections() - }) + mihomoConnectionsWs.onerror = (): void => { + if (mihomoConnectionsWs) { + mihomoConnectionsWs.close() + mihomoConnectionsWs = null + } + } }