From c420f0397cdeb3813364268e5dc0eca30f88ee2b Mon Sep 17 00:00:00 2001 From: pompurin404 Date: Fri, 27 Sep 2024 16:26:54 +0800 Subject: [PATCH] use unix socket and namedpipe instead of http api --- src/main/config/controledMihomo.ts | 4 - src/main/core/manager.ts | 41 ++-- src/main/core/mihomoApi.ts | 338 +++++++++++++---------------- src/main/utils/init.ts | 12 + src/main/utils/template.ts | 4 +- src/shared/types.d.ts | 2 + 6 files changed, 190 insertions(+), 211 deletions(-) diff --git a/src/main/config/controledMihomo.ts b/src/main/config/controledMihomo.ts index 016eb34..b58af84 100644 --- a/src/main/config/controledMihomo.ts +++ b/src/main/config/controledMihomo.ts @@ -1,7 +1,6 @@ import { controledMihomoConfigPath } from '../utils/dirs' import { readFile, writeFile } from 'fs/promises' import yaml from 'yaml' -import { getAxios } from '../core/mihomoApi' import { generateProfile } from '../core/factory' import { getAppConfig } from './app' import { defaultControledMihomoConfig } from '../utils/template' @@ -52,9 +51,6 @@ export async function patchControledMihomoConfig(patch: Partial): if (process.platform === 'darwin') { delete controledMihomoConfig?.tun?.device } - if (patch['external-controller'] || patch.secret) { - await getAxios(true) - } await generateProfile() await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8') } diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index dc6cc1e..1c8ebd8 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -113,25 +113,19 @@ export async function startCore(detached = false): Promise[]> { }) return new Promise((resolve, reject) => { child.stdout?.on('data', async (data) => { - if (data.toString().includes('configure tun interface: operation not permitted')) { + const str = data.toString() + if (str.includes('configure tun interface: operation not permitted')) { patchControledMihomoConfig({ tun: { enable: false } }) mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') reject('虚拟网卡启动失败, 请尝试手动授予内核权限') } - if (data.toString().includes('External controller listen error')) { - if (retry) { - retry-- - try { - resolve(await startCore()) - } catch (e) { - reject(e) - } - } else { - reject('内核连接失败, 请尝试修改外部控制端口或重启电脑') - } - } - if (data.toString().includes('RESTful API listening at')) { + + if ( + (process.platform !== 'win32' && str.includes('RESTful API unix listening at')) || + (process.platform === 'win32' && str.includes('RESTful API pipe listening at')) + ) { + await autoGrantUnixSocket() resolve([ new Promise((resolve) => { child.stdout?.on('data', async (data) => { @@ -223,6 +217,25 @@ async function checkProfile(): Promise { } } +async function autoGrantUnixSocket(): Promise { + if (process.platform === 'win32') return + const { encryptedPassword } = await getAppConfig() + const { 'external-controller-unix': mihomoUnix = 'mihomo-party.sock' } = + await getControledMihomoConfig() + const execPromise = promisify(exec) + if (encryptedPassword && isEncryptionAvailable()) { + try { + const password = safeStorage.decryptString(Buffer.from(encryptedPassword)) + await execPromise( + `echo "${password}" | sudo -S chmod 777 "${path.join(mihomoWorkDir(), mihomoUnix)}"` + ) + } catch (error) { + patchAppConfig({ encryptedPassword: undefined }) + throw error + } + } +} + export async function autoGrantCorePermition(corePath: string): Promise { if (process.platform === 'win32') return const { encryptedPassword } = await getAppConfig() diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 90315f5..6b3b2e4 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -1,77 +1,104 @@ -import axios, { AxiosInstance } from 'axios' +import net from 'net' +import { getRuntimeConfig } from './factory' 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' -let axiosIns: AxiosInstance = null! -let mihomoTrafficWs: WebSocket | null = null +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + +let mihomoTrafficWs: net.Socket | null = null let trafficRetry = 10 -let mihomoMemoryWs: WebSocket | null = null +let mihomoMemoryWs: net.Socket | null = null let memoryRetry = 10 -let mihomoLogsWs: WebSocket | null = null +let mihomoLogsWs: net.Socket | null = null let logsRetry = 10 -let mihomoConnectionsWs: WebSocket | null = null +let mihomoConnectionsWs: net.Socket | null = null let connectionsRetry = 10 -export const getAxios = async (force: boolean = false): Promise => { - if (axiosIns && !force) return axiosIns - const controledMihomoConfig = await getControledMihomoConfig() - let server = controledMihomoConfig['external-controller'] - const secret = controledMihomoConfig.secret ?? '' - if (server?.startsWith(':')) server = `127.0.0.1${server}` - - axiosIns = axios.create({ - baseURL: `http://${server}`, - proxy: false, - headers: secret ? { Authorization: `Bearer ${secret}` } : {}, - 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 +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) } -export async function mihomoVersion(): Promise { - const instance = await getAxios() - return await instance.get('/version') +async function mihomoHttp(method: HttpMethod, path: string, data?: object): 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) + ) + client.on('data', function (res) { + try { + const data = trimJson(res.toString().split('\r\n\r\n')[1]) + if (res.toString().includes('HTTP/1.1 4') || res.toString().includes('HTTP/1.1 5')) { + reject(data ? JSON.parse(data) : undefined) + } else { + resolve(data ? JSON.parse(data) : undefined) + } + } catch (e) { + reject(e) + } finally { + client.end() + } + }) + 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: ${json.length}\r\n\r\n${json}` + ) + } else { + client.write(`${method} ${path} HTTP/1.1\r\nHost: mihomo-party\r\n\r\n`) + } + }) +} + +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 const patchMihomoConfig = async (patch: Partial): Promise => { - const instance = await getAxios() - return await instance.patch('/configs', patch) + return await mihomoHttp('PATCH', '/configs', patch) } export const mihomoCloseConnection = async (id: string): Promise => { - const instance = await getAxios() - return await instance.delete(`/connections/${encodeURIComponent(id)}`) + return await mihomoHttp('DELETE', `/connection/${id}`) } export const mihomoCloseAllConnections = async (): Promise => { - const instance = await getAxios() - return await instance.delete('/connections') + return await mihomoHttp('DELETE', '/connections') } export const mihomoRules = async (): Promise => { - const instance = await getAxios() - return await instance.get('/rules') + return await mihomoHttp('GET', '/rules') } export const mihomoProxies = async (): Promise => { - const instance = await getAxios() - const proxies = (await instance.get('/proxies')) as IMihomoProxies + const proxies = (await mihomoHttp('GET', '/proxies')) as IMihomoProxies if (!proxies.proxies['GLOBAL']) { throw new Error('GLOBAL proxy not found') } @@ -102,265 +129,194 @@ export const mihomoGroups = async (): Promise => { } export const mihomoProxyProviders = async (): Promise => { - const instance = await getAxios() - return await instance.get('/providers/proxies') + return await mihomoHttp('GET', '/providers/proxies') } export const mihomoUpdateProxyProviders = async (name: string): Promise => { - const instance = await getAxios() - return await instance.put(`/providers/proxies/${encodeURIComponent(name)}`) + return await mihomoHttp('PUT', `/providers/proxies/${encodeURIComponent(name)}`) } export const mihomoRuleProviders = async (): Promise => { - const instance = await getAxios() - return await instance.get('/providers/rules') + return await mihomoHttp('GET', '/providers/rules') } export const mihomoUpdateRuleProviders = async (name: string): Promise => { - const instance = await getAxios() - return await instance.put(`/providers/rules/${encodeURIComponent(name)}`) + return await mihomoHttp('PUT', `/providers/rules/${encodeURIComponent(name)}`) } export const mihomoChangeProxy = async (group: string, proxy: string): Promise => { - const instance = await getAxios() - return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy }) + return await mihomoHttp('PUT', `/proxies/${encodeURIComponent(group)}`, { name: proxy }) } export const mihomoUpgradeGeo = async (): Promise => { - const instance = await getAxios() - return await instance.post('/configs/geo') + return await mihomoHttp('POST', '/configs/geo') } export const mihomoProxyDelay = async (proxy: string, url?: string): Promise => { const appConfig = await getAppConfig() const { delayTestUrl, delayTestTimeout } = appConfig - 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 - } - }) + + return await mihomoHttp( + 'GET', + `/proxies/${encodeURIComponent(proxy)}/delay?url=${encodeURIComponent(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 - 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 - } - }) + return await mihomoHttp( + 'GET', + `/proxies/${encodeURIComponent(group)}/delay?url=${encodeURIComponent(url || delayTestUrl || 'https://www.gstatic.com/generate_204')}&timeout=${delayTestTimeout || 5000}` + ) } export const mihomoUpgrade = async (): Promise => { - const instance = await getAxios() - return await instance.post('/upgrade') + return await mihomoHttp('POST', '/upgrade') } export const startMihomoTraffic = async (): Promise => { await mihomoTraffic() } -export const stopMihomoTraffic = (): void => { +export const stopMihomoTraffic = async (): Promise => { if (mihomoTrafficWs) { - mihomoTrafficWs.removeAllListeners() - if (mihomoTrafficWs.readyState === WebSocket.OPEN) { - mihomoTrafficWs.close() - } + mihomoTrafficWs.end() mihomoTrafficWs = null } } const mihomoTraffic = async (): Promise => { - const controledMihomoConfig = await getControledMihomoConfig() - let server = controledMihomoConfig['external-controller'] - const secret = controledMihomoConfig.secret ?? '' - if (server?.startsWith(':')) server = `127.0.0.1${server}` stopMihomoTraffic() - - mihomoTrafficWs = new WebSocket(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`) - - mihomoTrafficWs.onmessage = async (e): Promise => { - const data = e.data as string - const json = JSON.parse(data) as IMihomoTrafficInfo - trafficRetry = 10 + mihomoTrafficWs = await mihomoWs('/traffic') + mihomoTrafficWs.on('data', (data) => { try { + const json = JSON.parse(trimJson(data.toString())) as IMihomoTrafficInfo + trafficRetry = 10 mainWindow?.webContents.send('mihomoTraffic', json) - if (process.platform !== 'linux') { - tray?.setToolTip( - '↑' + - `${calcTraffic(json.up)}/s`.padStart(9) + - '\n↓' + - `${calcTraffic(json.down)}/s`.padStart(9) - ) - } + tray?.setToolTip( + '↑' + + `${calcTraffic(json.up)}/s`.padStart(9) + + '\n↓' + + `${calcTraffic(json.down)}/s`.padStart(9) + ) } catch { // ignore } - } - - mihomoTrafficWs.onclose = (): void => { + }) + mihomoTrafficWs.on('close', () => { if (trafficRetry) { trafficRetry-- mihomoTraffic() } - } + }) - mihomoTrafficWs.onerror = (): void => { - if (mihomoTrafficWs) { - mihomoTrafficWs.close() - mihomoTrafficWs = null - } - } + mihomoTrafficWs.on('error', (): void => { + stopMihomoTraffic() + }) } export const startMihomoMemory = async (): Promise => { await mihomoMemory() } -export const stopMihomoMemory = (): void => { +export const stopMihomoMemory = async (): Promise => { if (mihomoMemoryWs) { - mihomoMemoryWs.removeAllListeners() - if (mihomoMemoryWs.readyState === WebSocket.OPEN) { - mihomoMemoryWs.close() - } + mihomoMemoryWs.end() mihomoMemoryWs = null } } const mihomoMemory = async (): Promise => { - const controledMihomoConfig = await getControledMihomoConfig() - let server = controledMihomoConfig['external-controller'] - const secret = controledMihomoConfig.secret ?? '' - if (server?.startsWith(':')) server = `127.0.0.1${server}` stopMihomoMemory() - - mihomoMemoryWs = new WebSocket(`ws://${server}/memory?token=${encodeURIComponent(secret)}`) - - mihomoMemoryWs.onmessage = (e): void => { - const data = e.data as string - memoryRetry = 10 + mihomoMemoryWs = await mihomoWs('/memory') + mihomoMemoryWs.on('data', (data) => { try { - mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo) + const json = JSON.parse(trimJson(data.toString())) as IMihomoMemoryInfo + memoryRetry = 10 + mainWindow?.webContents.send('mihomoMemory', json) } catch { // ignore } - } - - mihomoMemoryWs.onclose = (): void => { + }) + mihomoMemoryWs.on('close', () => { if (memoryRetry) { memoryRetry-- mihomoMemory() } - } + }) - mihomoMemoryWs.onerror = (): void => { - if (mihomoMemoryWs) { - mihomoMemoryWs.close() - mihomoMemoryWs = null - } - } + mihomoMemoryWs.on('error', (): void => { + stopMihomoMemory() + }) } export const startMihomoLogs = async (): Promise => { await mihomoLogs() } -export const stopMihomoLogs = (): void => { +export const stopMihomoLogs = async (): Promise => { if (mihomoLogsWs) { - mihomoLogsWs.removeAllListeners() - if (mihomoLogsWs.readyState === WebSocket.OPEN) { - mihomoLogsWs.close() - } + mihomoLogsWs.end() mihomoLogsWs = null } } const mihomoLogs = async (): Promise => { - const controledMihomoConfig = await getControledMihomoConfig() - const { secret = '', 'log-level': level = 'info' } = controledMihomoConfig - let { 'external-controller': server } = controledMihomoConfig - if (server?.startsWith(':')) server = `127.0.0.1${server}` stopMihomoLogs() - - mihomoLogsWs = new WebSocket( - `ws://${server}/logs?token=${encodeURIComponent(secret)}&level=${level}` - ) - - mihomoLogsWs.onmessage = (e): void => { - const data = e.data as string - logsRetry = 10 + mihomoLogsWs = await mihomoWs('/logs') + mihomoLogsWs.on('data', (data) => { try { - mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo) + const json = JSON.parse(trimJson(data.toString())) as IMihomoLogInfo + logsRetry = 10 + mainWindow?.webContents.send('mihomoLogs', json) } catch { // ignore } - } - - mihomoLogsWs.onclose = (): void => { + }) + mihomoLogsWs.on('close', () => { if (logsRetry) { logsRetry-- mihomoLogs() } - } + }) - mihomoLogsWs.onerror = (): void => { - if (mihomoLogsWs) { - mihomoLogsWs.close() - mihomoLogsWs = null - } - } + mihomoLogsWs.on('error', (): void => { + stopMihomoLogs() + }) } export const startMihomoConnections = async (): Promise => { await mihomoConnections() } -export const stopMihomoConnections = (): void => { +export const stopMihomoConnections = async (): Promise => { if (mihomoConnectionsWs) { - mihomoConnectionsWs.removeAllListeners() - if (mihomoConnectionsWs.readyState === WebSocket.OPEN) { - mihomoConnectionsWs.close() - } + mihomoConnectionsWs.end() mihomoConnectionsWs = null } } const mihomoConnections = async (): Promise => { - const controledMihomoConfig = await getControledMihomoConfig() - let server = controledMihomoConfig['external-controller'] - const secret = controledMihomoConfig.secret ?? '' - if (server?.startsWith(':')) server = `127.0.0.1${server}` stopMihomoConnections() - - mihomoConnectionsWs = new WebSocket( - `ws://${server}/connections?token=${encodeURIComponent(secret)}` - ) - - mihomoConnectionsWs.onmessage = (e): void => { - const data = e.data as string - connectionsRetry = 10 + mihomoConnectionsWs = await mihomoWs('/connections') + mihomoConnectionsWs.on('data', (data) => { try { - mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) + const json = JSON.parse(trimJson(data.toString())) as IMihomoConnectionsInfo + connectionsRetry = 10 + mainWindow?.webContents.send('mihomoConnections', json) } catch { // ignore } - } - - mihomoConnectionsWs.onclose = (): void => { + }) + mihomoConnectionsWs.on('close', () => { if (connectionsRetry) { connectionsRetry-- mihomoConnections() } - } + }) - mihomoConnectionsWs.onerror = (): void => { - if (mihomoConnectionsWs) { - mihomoConnectionsWs.close() - mihomoConnectionsWs = null - } - } + mihomoConnectionsWs.on('error', (): void => { + stopMihomoConnections() + }) } diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index b71d8d3..b71eea6 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -151,6 +151,8 @@ async function migration(): Promise { useSubStore = true } = await getAppConfig() const { + 'external-controller-pipe': externalControllerPipe, + 'external-controller-unix': externalControllerUnix, 'skip-auth-prefixes': skipAuthPrefixes, authentication, 'bind-address': bindAddress, @@ -189,6 +191,16 @@ async function migration(): Promise { if (typeof envType === 'string') { await patchAppConfig({ envType: [envType] }) } + // use unix socket + if (process.platform !== 'win32' && !externalControllerUnix) { + await patchControledMihomoConfig({ 'external-controller-unix': 'mihomo-party.sock' }) + } + // use named pipe + if (process.platform === 'win32' && !externalControllerPipe) { + await patchControledMihomoConfig({ + 'external-controller-pipe': '\\\\.\\pipe\\MihomoParty\\mihomo' + }) + } } function initDeeplink(): void { diff --git a/src/main/utils/template.ts b/src/main/utils/template.ts index dce3282..e86eb47 100644 --- a/src/main/utils/template.ts +++ b/src/main/utils/template.ts @@ -36,8 +36,8 @@ export const defaultConfig: IAppConfig = { } export const defaultControledMihomoConfig: Partial = { - 'external-controller': '127.0.0.1:9090', - secret: '', + 'external-controller-pipe': '\\\\.pipe\\MihomoParty\\mihomo', + 'external-controller-unix': 'mihomo-party.sock', ipv6: true, mode: 'rule', 'mixed-port': 7890, diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 49b530a..d758769 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -343,6 +343,8 @@ interface IMihomoProfileConfig { } interface IMihomoConfig { + 'external-controller-pipe': string + 'external-controller-unix': string 'external-controller': string secret?: string ipv6: boolean