From b15fc6ce3a9d0e3904c94c0a19518193d222f627 Mon Sep 17 00:00:00 2001 From: miwu04 Date: Sun, 13 Apr 2025 00:19:00 +0800 Subject: [PATCH] refactor: replace password-based sudo with pkexec for improved security --- src/main/core/manager.ts | 10 ++- src/main/resolve/server.ts | 40 ++++------- src/main/utils/ipc.ts | 6 +- src/renderer/src/pages/substore.tsx | 108 ++++++++-------------------- src/renderer/src/pages/tun.tsx | 33 ++------- src/renderer/src/utils/ipc.ts | 10 ++- 6 files changed, 63 insertions(+), 144 deletions(-) diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index d40b5d6..743e99d 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -262,18 +262,22 @@ async function checkProfile(): Promise { } } -export async function manualGrantCorePermition(password?: string): Promise { +export async function manualGrantCorePermition(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) const execPromise = promisify(exec) + const execFilePromise = promisify(execFile) if (process.platform === 'darwin') { const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}` const command = `do shell script "${shell}" with administrator privileges` await execPromise(`osascript -e '${command}'`) } if (process.platform === 'linux') { - await execPromise(`echo "${password}" | sudo -S chown root:root "${corePath}"`) - await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`) + await execFilePromise('pkexec', [ + 'bash', + '-c', + `chown root:root "${corePath}" && chmod +sx "${corePath}"` + ]) } } diff --git a/src/main/resolve/server.ts b/src/main/resolve/server.ts index e2e49d3..95b1a61 100644 --- a/src/main/resolve/server.ts +++ b/src/main/resolve/server.ts @@ -1,12 +1,6 @@ import { getAppConfig, getControledMihomoConfig } from '../config' import { Worker } from 'worker_threads' -import { - dataDir, - mihomoWorkDir, - resourcesFilesDir, - subStoreDir, - substoreLogPath -} from '../utils/dirs' +import { mihomoWorkDir, resourcesFilesDir, subStoreDir, substoreLogPath } from '../utils/dirs' import subStoreIcon from '../../../resources/subStoreIcon.png?asset' import { createWriteStream, existsSync, mkdirSync } from 'fs' import { writeFile, rm, cp } from 'fs/promises' @@ -18,8 +12,9 @@ import express from 'express' import axios from 'axios' import AdmZip from 'adm-zip' import { promisify } from 'util' -import { exec } from 'child_process' +import { execFile } from 'child_process' import { platform } from 'os' +import { is } from '@electron-toolkit/utils' export let pacPort: number export let subStorePort: number @@ -148,12 +143,12 @@ export async function stopSubStoreBackendServer(): Promise { } } -export async function downloadSubStore(password?: string): Promise { +export async function downloadSubStore(): Promise { const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig() const frontendDir = path.join(resourcesFilesDir(), 'sub-store-frontend') const backendPath = path.join(resourcesFilesDir(), 'sub-store.bundle.js') - const tempDir = path.join(dataDir(), 'temp') - const execPromise = promisify(exec) + const tempDir = path.join(resourcesFilesDir(), 'temp') + const execFilePromise = promisify(execFile) try { // 创建临时目录 @@ -195,25 +190,20 @@ export async function downloadSubStore(password?: string): Promise { const zip = new AdmZip(Buffer.from(frontendRes.data)) zip.extractAllTo(tempDir, true) - // 如果是 Linux 平台,使用 sudo cp 移动文件 - if (platform() === 'linux') { + if (platform() === 'linux' && !is.dev) { try { - await execPromise(`echo "${password}" | sudo -S cp "${tempBackendPath}" "${backendPath}"`) - // 确保目标目录存在并清空 - if (existsSync(frontendDir)) { - await execPromise(`echo "${password}" | sudo -S rm -r "${frontendDir}"`) - } - await execPromise(`echo "${password}" | sudo -S mkdir "${frontendDir}"`) - // 将 dist 目录中的内容移动到目标目录 - await execPromise( - `echo "${password}" | sudo -S cp -r "${tempFrontendDir}"/* "${frontendDir}/"` - ) + const bashCmd = [ + `cp "${tempBackendPath}" "${backendPath}"`, + `rm -rf "${frontendDir}"`, + `mkdir -p "${frontendDir}"`, + `cp -r "${tempFrontendDir}"/* "${frontendDir}/"` + ].join(' && ') + await execFilePromise('pkexec', ['bash', '-c', bashCmd]) } catch (error) { console.error('substore.downloadFailed:', error) throw error } } else { - // 非 Linux 平台 await cp(tempBackendPath, backendPath) if (existsSync(frontendDir)) { await rm(frontendDir, { recursive: true }) @@ -221,8 +211,6 @@ export async function downloadSubStore(password?: string): Promise { mkdirSync(frontendDir, { recursive: true }) await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true }) } - - // 清理临时目录 await rm(tempDir, { recursive: true }) } catch (error) { console.error('substore.downloadFailed:', error) diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 5321053..fff75ec 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -174,9 +174,7 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('restartCore', ipcErrorWrapper(restartCore)) ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached)) ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable)) - ipcMain.handle('manualGrantCorePermition', (_e, password) => - ipcErrorWrapper(manualGrantCorePermition)(password) - ) + ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)()) ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext)) ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath)) ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr)) @@ -203,7 +201,7 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('stopSubStoreFrontendServer', () => ipcErrorWrapper(stopSubStoreFrontendServer)()) ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)()) ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)()) - ipcMain.handle('downloadSubStore', (_e, password) => ipcErrorWrapper(downloadSubStore)(password)) + ipcMain.handle('downloadSubStore', () => ipcErrorWrapper(downloadSubStore)()) ipcMain.handle('subStorePort', () => subStorePort) ipcMain.handle('subStoreFrontendPort', () => subStoreFrontendPort) ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)()) diff --git a/src/renderer/src/pages/substore.tsx b/src/renderer/src/pages/substore.tsx index e5c6f61..98efcbe 100644 --- a/src/renderer/src/pages/substore.tsx +++ b/src/renderer/src/pages/substore.tsx @@ -14,8 +14,6 @@ import React, { useEffect, useState } from 'react' import { HiExternalLink } from 'react-icons/hi' import { useTranslation } from 'react-i18next' import { IoMdCloudDownload } from 'react-icons/io' -import BasePasswordModal from '@renderer/components/base/base-password-modal' -import { platform } from '@renderer/utils/init' const SubStore: React.FC = () => { const { t } = useTranslation() @@ -24,7 +22,6 @@ const SubStore: React.FC = () => { const [backendPort, setBackendPort] = useState() const [frontendPort, setFrontendPort] = useState() const [isUpdating, setIsUpdating] = useState(false) - const [openPasswordModal, setOpenPasswordModal] = useState(false) const getPort = async (): Promise => { setBackendPort(await subStorePort()) setFrontendPort(await subStoreFrontendPort()) @@ -37,85 +34,40 @@ const SubStore: React.FC = () => { if (!frontendPort) return null return ( <> - {openPasswordModal && ( - setOpenPasswordModal(false)} - onConfirm={async (password: string) => { - try { - setOpenPasswordModal(false) - new Notification(t('substore.updating')) - await downloadSubStore(password) - await stopSubStoreBackendServer() - await startSubStoreBackendServer() - await new Promise((resolve) => setTimeout(resolve, 1000)) - setFrontendPort(0) - await stopSubStoreFrontendServer() - await startSubStoreFrontendServer() - await getPort() - new Notification(t('substore.updateCompleted')) - } catch (e) { - alert(e) - } - }} - /> - )} - {platform != 'linux' && ( - - )} - {platform === 'linux' && ( - - )} + +