mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 13:10:30 +08:00
refactor: replace password-based sudo with pkexec for improved security
This commit is contained in:
parent
fcb323a17a
commit
b15fc6ce3a
@ -262,18 +262,22 @@ async function checkProfile(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function manualGrantCorePermition(password?: string): Promise<void> {
|
export async function manualGrantCorePermition(): Promise<void> {
|
||||||
const { core = 'mihomo' } = await getAppConfig()
|
const { core = 'mihomo' } = await getAppConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
|
const execFilePromise = promisify(execFile)
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
|
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
|
||||||
const command = `do shell script "${shell}" with administrator privileges`
|
const command = `do shell script "${shell}" with administrator privileges`
|
||||||
await execPromise(`osascript -e '${command}'`)
|
await execPromise(`osascript -e '${command}'`)
|
||||||
}
|
}
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
await execPromise(`echo "${password}" | sudo -S chown root:root "${corePath}"`)
|
await execFilePromise('pkexec', [
|
||||||
await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`)
|
'bash',
|
||||||
|
'-c',
|
||||||
|
`chown root:root "${corePath}" && chmod +sx "${corePath}"`
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||||
import { Worker } from 'worker_threads'
|
import { Worker } from 'worker_threads'
|
||||||
import {
|
import { mihomoWorkDir, resourcesFilesDir, subStoreDir, substoreLogPath } from '../utils/dirs'
|
||||||
dataDir,
|
|
||||||
mihomoWorkDir,
|
|
||||||
resourcesFilesDir,
|
|
||||||
subStoreDir,
|
|
||||||
substoreLogPath
|
|
||||||
} from '../utils/dirs'
|
|
||||||
import subStoreIcon from '../../../resources/subStoreIcon.png?asset'
|
import subStoreIcon from '../../../resources/subStoreIcon.png?asset'
|
||||||
import { createWriteStream, existsSync, mkdirSync } from 'fs'
|
import { createWriteStream, existsSync, mkdirSync } from 'fs'
|
||||||
import { writeFile, rm, cp } from 'fs/promises'
|
import { writeFile, rm, cp } from 'fs/promises'
|
||||||
@ -18,8 +12,9 @@ import express from 'express'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { exec } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { platform } from 'os'
|
import { platform } from 'os'
|
||||||
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
|
||||||
export let pacPort: number
|
export let pacPort: number
|
||||||
export let subStorePort: number
|
export let subStorePort: number
|
||||||
@ -148,12 +143,12 @@ export async function stopSubStoreBackendServer(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadSubStore(password?: string): Promise<void> {
|
export async function downloadSubStore(): Promise<void> {
|
||||||
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
||||||
const frontendDir = path.join(resourcesFilesDir(), 'sub-store-frontend')
|
const frontendDir = path.join(resourcesFilesDir(), 'sub-store-frontend')
|
||||||
const backendPath = path.join(resourcesFilesDir(), 'sub-store.bundle.js')
|
const backendPath = path.join(resourcesFilesDir(), 'sub-store.bundle.js')
|
||||||
const tempDir = path.join(dataDir(), 'temp')
|
const tempDir = path.join(resourcesFilesDir(), 'temp')
|
||||||
const execPromise = promisify(exec)
|
const execFilePromise = promisify(execFile)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建临时目录
|
// 创建临时目录
|
||||||
@ -195,25 +190,20 @@ export async function downloadSubStore(password?: string): Promise<void> {
|
|||||||
const zip = new AdmZip(Buffer.from(frontendRes.data))
|
const zip = new AdmZip(Buffer.from(frontendRes.data))
|
||||||
zip.extractAllTo(tempDir, true)
|
zip.extractAllTo(tempDir, true)
|
||||||
|
|
||||||
// 如果是 Linux 平台,使用 sudo cp 移动文件
|
if (platform() === 'linux' && !is.dev) {
|
||||||
if (platform() === 'linux') {
|
|
||||||
try {
|
try {
|
||||||
await execPromise(`echo "${password}" | sudo -S cp "${tempBackendPath}" "${backendPath}"`)
|
const bashCmd = [
|
||||||
// 确保目标目录存在并清空
|
`cp "${tempBackendPath}" "${backendPath}"`,
|
||||||
if (existsSync(frontendDir)) {
|
`rm -rf "${frontendDir}"`,
|
||||||
await execPromise(`echo "${password}" | sudo -S rm -r "${frontendDir}"`)
|
`mkdir -p "${frontendDir}"`,
|
||||||
}
|
`cp -r "${tempFrontendDir}"/* "${frontendDir}/"`
|
||||||
await execPromise(`echo "${password}" | sudo -S mkdir "${frontendDir}"`)
|
].join(' && ')
|
||||||
// 将 dist 目录中的内容移动到目标目录
|
await execFilePromise('pkexec', ['bash', '-c', bashCmd])
|
||||||
await execPromise(
|
|
||||||
`echo "${password}" | sudo -S cp -r "${tempFrontendDir}"/* "${frontendDir}/"`
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('substore.downloadFailed:', error)
|
console.error('substore.downloadFailed:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 Linux 平台
|
|
||||||
await cp(tempBackendPath, backendPath)
|
await cp(tempBackendPath, backendPath)
|
||||||
if (existsSync(frontendDir)) {
|
if (existsSync(frontendDir)) {
|
||||||
await rm(frontendDir, { recursive: true })
|
await rm(frontendDir, { recursive: true })
|
||||||
@ -221,8 +211,6 @@ export async function downloadSubStore(password?: string): Promise<void> {
|
|||||||
mkdirSync(frontendDir, { recursive: true })
|
mkdirSync(frontendDir, { recursive: true })
|
||||||
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理临时目录
|
|
||||||
await rm(tempDir, { recursive: true })
|
await rm(tempDir, { recursive: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('substore.downloadFailed:', error)
|
console.error('substore.downloadFailed:', error)
|
||||||
|
|||||||
@ -174,9 +174,7 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('restartCore', ipcErrorWrapper(restartCore))
|
ipcMain.handle('restartCore', ipcErrorWrapper(restartCore))
|
||||||
ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached))
|
ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached))
|
||||||
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
|
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
|
||||||
ipcMain.handle('manualGrantCorePermition', (_e, password) =>
|
ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)())
|
||||||
ipcErrorWrapper(manualGrantCorePermition)(password)
|
|
||||||
)
|
|
||||||
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
|
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
|
||||||
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
|
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
|
||||||
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
|
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
|
||||||
@ -203,7 +201,7 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('stopSubStoreFrontendServer', () => ipcErrorWrapper(stopSubStoreFrontendServer)())
|
ipcMain.handle('stopSubStoreFrontendServer', () => ipcErrorWrapper(stopSubStoreFrontendServer)())
|
||||||
ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)())
|
ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)())
|
||||||
ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)())
|
ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)())
|
||||||
ipcMain.handle('downloadSubStore', (_e, password) => ipcErrorWrapper(downloadSubStore)(password))
|
ipcMain.handle('downloadSubStore', () => ipcErrorWrapper(downloadSubStore)())
|
||||||
ipcMain.handle('subStorePort', () => subStorePort)
|
ipcMain.handle('subStorePort', () => subStorePort)
|
||||||
ipcMain.handle('subStoreFrontendPort', () => subStoreFrontendPort)
|
ipcMain.handle('subStoreFrontendPort', () => subStoreFrontendPort)
|
||||||
ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)())
|
ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)())
|
||||||
|
|||||||
@ -14,8 +14,6 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import { HiExternalLink } from 'react-icons/hi'
|
import { HiExternalLink } from 'react-icons/hi'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { IoMdCloudDownload } from 'react-icons/io'
|
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 SubStore: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -24,7 +22,6 @@ const SubStore: React.FC = () => {
|
|||||||
const [backendPort, setBackendPort] = useState<number | undefined>()
|
const [backendPort, setBackendPort] = useState<number | undefined>()
|
||||||
const [frontendPort, setFrontendPort] = useState<number | undefined>()
|
const [frontendPort, setFrontendPort] = useState<number | undefined>()
|
||||||
const [isUpdating, setIsUpdating] = useState(false)
|
const [isUpdating, setIsUpdating] = useState(false)
|
||||||
const [openPasswordModal, setOpenPasswordModal] = useState(false)
|
|
||||||
const getPort = async (): Promise<void> => {
|
const getPort = async (): Promise<void> => {
|
||||||
setBackendPort(await subStorePort())
|
setBackendPort(await subStorePort())
|
||||||
setFrontendPort(await subStoreFrontendPort())
|
setFrontendPort(await subStoreFrontendPort())
|
||||||
@ -37,85 +34,40 @@ const SubStore: React.FC = () => {
|
|||||||
if (!frontendPort) return null
|
if (!frontendPort) return null
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{openPasswordModal && (
|
|
||||||
<BasePasswordModal
|
|
||||||
onCancel={() => 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)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BasePage
|
<BasePage
|
||||||
title={t('substore.title')}
|
title={t('substore.title')}
|
||||||
header={
|
header={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{platform != 'linux' && (
|
<Button
|
||||||
<Button
|
title={t('substore.checkUpdate')}
|
||||||
title={t('substore.checkUpdate')}
|
isIconOnly
|
||||||
isIconOnly
|
size="sm"
|
||||||
size="sm"
|
className="app-nodrag"
|
||||||
className="app-nodrag"
|
variant="light"
|
||||||
variant="light"
|
isLoading={isUpdating}
|
||||||
isLoading={isUpdating}
|
onPress={async () => {
|
||||||
onPress={async () => {
|
try {
|
||||||
try {
|
new Notification(t('substore.updating'))
|
||||||
new Notification(t('substore.updating'))
|
setIsUpdating(true)
|
||||||
setIsUpdating(true)
|
await downloadSubStore()
|
||||||
await downloadSubStore()
|
await stopSubStoreBackendServer()
|
||||||
await stopSubStoreBackendServer()
|
await startSubStoreBackendServer()
|
||||||
await startSubStoreBackendServer()
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
setFrontendPort(0)
|
||||||
setFrontendPort(0)
|
await stopSubStoreFrontendServer()
|
||||||
await stopSubStoreFrontendServer()
|
await startSubStoreFrontendServer()
|
||||||
await startSubStoreFrontendServer()
|
await getPort()
|
||||||
await getPort()
|
new Notification(t('substore.updateCompleted'))
|
||||||
new Notification(t('substore.updateCompleted'))
|
} catch (e) {
|
||||||
} catch (e) {
|
new Notification(`${t('substore.updateFailed')}: ${e}`)
|
||||||
new Notification(`${t('substore.updateFailed')}: ${e}`)
|
} finally {
|
||||||
} finally {
|
setIsUpdating(false)
|
||||||
setIsUpdating(false)
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<IoMdCloudDownload className="text-lg" />
|
||||||
<IoMdCloudDownload className="text-lg" />
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{platform === 'linux' && (
|
|
||||||
<Button
|
|
||||||
title={t('substore.checkUpdate')}
|
|
||||||
isIconOnly
|
|
||||||
size="sm"
|
|
||||||
className="app-nodrag"
|
|
||||||
variant="light"
|
|
||||||
isLoading={isUpdating}
|
|
||||||
onPress={async () => {
|
|
||||||
try {
|
|
||||||
setIsUpdating(true)
|
|
||||||
setOpenPasswordModal(true)
|
|
||||||
} catch (e) {
|
|
||||||
new Notification(`${t('substore.updateFailed')}: ${e}`)
|
|
||||||
} finally {
|
|
||||||
setIsUpdating(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IoMdCloudDownload className="text-lg" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
title={t('substore.openInBrowser')}
|
title={t('substore.openInBrowser')}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
|||||||
import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc'
|
import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc'
|
||||||
import { platform } from '@renderer/utils/init'
|
import { platform } from '@renderer/utils/init'
|
||||||
import React, { Key, useState } from 'react'
|
import React, { Key, useState } from 'react'
|
||||||
import BasePasswordModal from '@renderer/components/base/base-password-modal'
|
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -18,7 +17,6 @@ const Tun: React.FC = () => {
|
|||||||
const { autoSetDNS = true } = appConfig || {}
|
const { autoSetDNS = true } = appConfig || {}
|
||||||
const { tun } = controledMihomoConfig || {}
|
const { tun } = controledMihomoConfig || {}
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [openPasswordModal, setOpenPasswordModal] = useState(false)
|
|
||||||
const {
|
const {
|
||||||
device = 'Mihomo',
|
device = 'Mihomo',
|
||||||
stack = 'mixed',
|
stack = 'mixed',
|
||||||
@ -71,21 +69,6 @@ const Tun: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{openPasswordModal && (
|
|
||||||
<BasePasswordModal
|
|
||||||
onCancel={() => setOpenPasswordModal(false)}
|
|
||||||
onConfirm={async (password: string) => {
|
|
||||||
try {
|
|
||||||
await manualGrantCorePermition(password)
|
|
||||||
new Notification(t('tun.notifications.coreAuthSuccess'))
|
|
||||||
await restartCore()
|
|
||||||
setOpenPasswordModal(false)
|
|
||||||
} catch (e) {
|
|
||||||
alert(e)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BasePage
|
<BasePage
|
||||||
title={t('tun.title')}
|
title={t('tun.title')}
|
||||||
header={
|
header={
|
||||||
@ -145,16 +128,12 @@ const Tun: React.FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (platform === 'darwin') {
|
try {
|
||||||
try {
|
await manualGrantCorePermition()
|
||||||
await manualGrantCorePermition()
|
new Notification(t('tun.notifications.coreAuthSuccess'))
|
||||||
new Notification(t('tun.notifications.coreAuthSuccess'))
|
await restartCore()
|
||||||
await restartCore()
|
} catch (e) {
|
||||||
} catch (e) {
|
alert(e)
|
||||||
alert(e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setOpenPasswordModal(true)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -207,10 +207,8 @@ export async function triggerSysProxy(enable: boolean): Promise<void> {
|
|||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function manualGrantCorePermition(password?: string): Promise<void> {
|
export async function manualGrantCorePermition(): Promise<void> {
|
||||||
return ipcErrorWrapper(
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition'))
|
||||||
await window.electron.ipcRenderer.invoke('manualGrantCorePermition', password)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
|
export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
|
||||||
@ -326,8 +324,8 @@ export async function startSubStoreBackendServer(): Promise<void> {
|
|||||||
export async function stopSubStoreBackendServer(): Promise<void> {
|
export async function stopSubStoreBackendServer(): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopSubStoreBackendServer'))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopSubStoreBackendServer'))
|
||||||
}
|
}
|
||||||
export async function downloadSubStore(password?: string): Promise<void> {
|
export async function downloadSubStore(): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('downloadSubStore', password))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('downloadSubStore'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subStorePort(): Promise<number> {
|
export async function subStorePort(): Promise<number> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user