diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e43ea2b..caf3891 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -199,6 +199,7 @@ jobs: npm_config_target_arch: ${{ matrix.arch }} run: | sed -i "" -e "s/productName: mihomo-party/productName: Mihomo Party/" electron-builder.yml + chmod +x build/pkg-scripts/postinstall pnpm build:mac --${{ matrix.arch }} - name: Generate checksums run: pnpm checksum .pkg diff --git a/build/pkg-scripts/postinstall b/build/pkg-scripts/postinstall new file mode 100644 index 0000000..0eb38c2 --- /dev/null +++ b/build/pkg-scripts/postinstall @@ -0,0 +1,6 @@ +#!/bin/sh +chown root:admin $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo +chown root:admin $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo-alpha +chmod +s $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo +chmod +s $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo-alpha +exit 0 \ No newline at end of file diff --git a/changelog.md b/changelog.md index bf33103..4c20c23 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +### Breaking Changes + +- macOS 改用 pkg 安装方式,不再支持 dmg 安装方式,因此本次更新需要手动下载安装包进行安装 + ### New Features -- Linux 不再存储 root 密码 +- macOS/Linux 均不再存储 root 密码 + +### Bug Fixes + +- 修复 macOS 10.15 无法安装的问题 diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 114ec24..a2a4b8f 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -17,7 +17,7 @@ import { patchAppConfig, patchControledMihomoConfig } from '../config' -import { app, dialog, ipcMain, net, safeStorage } from 'electron' +import { app, dialog, ipcMain, net } from 'electron' import { startMihomoTraffic, startMihomoConnections, @@ -57,27 +57,12 @@ let child: ChildProcess let retry = 10 export async function startCore(detached = false): Promise[]> { - const { - core = 'mihomo', - autoSetDNS = true, - encryptedPassword, - diffWorkDir = false - } = await getAppConfig() + const { core = 'mihomo', autoSetDNS = true, diffWorkDir = false } = await getAppConfig() const { 'log-level': logLevel } = await getControledMihomoConfig() if (existsSync(path.join(dataDir(), 'core.pid'))) { const pid = parseInt(await readFile(path.join(dataDir(), 'core.pid'), 'utf-8')) try { process.kill(pid, 'SIGINT') - } catch { - if (process.platform === 'darwin' && encryptedPassword && isEncryptionAvailable()) { - const execPromise = promisify(exec) - const password = safeStorage.decryptString(Buffer.from(encryptedPassword)) - try { - await execPromise(`echo "${password}" | sudo -S kill ${pid}`) - } catch { - // ignore - } - } } finally { await rm(path.join(dataDir(), 'core.pid')) } @@ -85,7 +70,6 @@ export async function startCore(detached = false): Promise[]> { const { current } = await getProfileConfig() const { tun } = await getControledMihomoConfig() const corePath = mihomoCorePath(core) - await autoGrantCorePermition(corePath) await generateProfile() await checkProfile() await stopCore() @@ -242,24 +226,6 @@ async function checkProfile(): Promise { } } -export async function autoGrantCorePermition(corePath: string): Promise { - if (process.platform !== 'darwin') return - const { encryptedPassword } = await getAppConfig() - const execPromise = promisify(exec) - if (encryptedPassword && isEncryptionAvailable()) { - try { - const password = safeStorage.decryptString(Buffer.from(encryptedPassword)) - if (process.platform === 'darwin') { - await execPromise(`echo "${password}" | sudo -S chown root:admin "${corePath}"`) - await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`) - } - } catch (error) { - patchAppConfig({ encryptedPassword: undefined }) - throw error - } - } -} - export async function manualGrantCorePermition(password?: string): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) @@ -275,27 +241,19 @@ export async function manualGrantCorePermition(password?: string): Promise } } -export function isEncryptionAvailable(): boolean { - return safeStorage.isEncryptionAvailable() -} - -export async function getDefaultDevice(password?: string): Promise { +export async function getDefaultDevice(): Promise { const execPromise = promisify(exec) - let sudo = '' - if (password) sudo = `echo "${password}" | sudo -S ` - const { stdout: deviceOut } = await execPromise(`${sudo}route -n get default`) + const { stdout: deviceOut } = await execPromise(`route -n get default`) let device = deviceOut.split('\n').find((s) => s.includes('interface:')) device = device?.trim().split(' ').slice(1).join(' ') if (!device) throw new Error('Get device failed') return device } -async function getDefaultService(password?: string): Promise { +async function getDefaultService(): Promise { const execPromise = promisify(exec) - let sudo = '' - if (password) sudo = `echo "${password}" | sudo -S ` - const device = await getDefaultDevice(password) - const { stdout: order } = await execPromise(`${sudo}networksetup -listnetworkserviceorder`) + const device = await getDefaultDevice() + const { stdout: order } = await execPromise(`networksetup -listnetworkserviceorder`) const block = order.split('\n\n').find((s) => s.includes(`Device: ${device}`)) if (!block) throw new Error('Get networkservice failed') for (const line of block.split('\n')) { @@ -306,12 +264,10 @@ async function getDefaultService(password?: string): Promise { throw new Error('Get service failed') } -async function getOriginDNS(password?: string): Promise { +async function getOriginDNS(): Promise { const execPromise = promisify(exec) - let sudo = '' - if (password) sudo = `echo "${password}" | sudo -S ` - const service = await getDefaultService(password) - const { stdout: dns } = await execPromise(`${sudo}networksetup -getdnsservers "${service}"`) + const service = await getDefaultService() + const { stdout: dns } = await execPromise(`networksetup -getdnsservers "${service}"`) if (dns.startsWith("There aren't any DNS Servers set on")) { await patchAppConfig({ originDNS: 'Empty' }) } else { @@ -319,25 +275,19 @@ async function getOriginDNS(password?: string): Promise { } } -async function setDNS(dns: string, password?: string): Promise { - const service = await getDefaultService(password) - let sudo = '' - if (password) sudo = `echo "${password}" | sudo -S ` +async function setDNS(dns: string): Promise { + const service = await getDefaultService() const execPromise = promisify(exec) - await execPromise(`${sudo}networksetup -setdnsservers "${service}" ${dns}`) + await execPromise(`networksetup -setdnsservers "${service}" ${dns}`) } async function setPublicDNS(): Promise { if (process.platform !== 'darwin') return if (net.isOnline()) { - const { originDNS, encryptedPassword } = await getAppConfig() + const { originDNS } = await getAppConfig() if (!originDNS) { - let password: string | undefined - if (encryptedPassword && isEncryptionAvailable()) { - password = safeStorage.decryptString(Buffer.from(encryptedPassword)) - } - await getOriginDNS(password) - await setDNS('223.5.5.5', password) + await getOriginDNS() + await setDNS('223.5.5.5') } } else { if (setPublicDNSTimer) clearTimeout(setPublicDNSTimer) @@ -348,13 +298,9 @@ async function setPublicDNS(): Promise { async function recoverDNS(): Promise { if (process.platform !== 'darwin') return if (net.isOnline()) { - const { originDNS, encryptedPassword } = await getAppConfig() + const { originDNS } = await getAppConfig() if (originDNS) { - let password: string | undefined - if (encryptedPassword && isEncryptionAvailable()) { - password = safeStorage.decryptString(Buffer.from(encryptedPassword)) - } - await setDNS(originDNS, password) + await setDNS(originDNS) await patchAppConfig({ originDNS: undefined }) } } else { diff --git a/src/main/resolve/autoUpdater.ts b/src/main/resolve/autoUpdater.ts index 835c0bc..49afae3 100644 --- a/src/main/resolve/autoUpdater.ts +++ b/src/main/resolve/autoUpdater.ts @@ -91,20 +91,9 @@ export async function downloadAndInstallUpdate(version: string): Promise { if (file.endsWith('.pkg')) { try { const execPromise = promisify(exec) - const name = exePath().split('.app')[0].replace('/Applications/', '') - await execPromise( - `hdiutil attach "${path.join(dataDir(), file)}" -mountpoint "/Volumes/mihomo-party" -nobrowse` - ) - try { - await execPromise(`mv "/Applications/${name}.app" /tmp`) - await execPromise('cp -R "/Volumes/mihomo-party/Mihomo Party.app" /Applications/') - await execPromise(`rm -rf "/tmp/${name}.app"`) - } catch (e) { - await execPromise(`mv "/tmp/${name}.app" /Applications`) - throw e - } finally { - await execPromise('hdiutil detach "/Volumes/mihomo-party"') - } + const shell = `installer -pkg ${path.join(dataDir(), file).replace(' ', '\\\\ ')} -target /` + const command = `do shell script "${shell}" with administrator privileges` + await execPromise(`osascript -e '${command}'`) app.relaunch() app.quit() } catch { diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 1f6b734..f3fe16f 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -216,7 +216,7 @@ async function migration(): Promise { await patchAppConfig({ disableTray: false }) } // remove password - if (process.platform === 'linux' && encryptedPassword) { + if (encryptedPassword) { await patchAppConfig({ encryptedPassword: undefined }) } } diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index b5a382b..dd3b9a5 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -1,4 +1,4 @@ -import { app, dialog, ipcMain, safeStorage } from 'electron' +import { app, dialog, ipcMain } from 'electron' import { mihomoChangeProxy, mihomoCloseAllConnections, @@ -51,12 +51,7 @@ import { subStoreFrontendPort, subStorePort } from '../resolve/server' -import { - isEncryptionAvailable, - manualGrantCorePermition, - quitWithoutCore, - restartCore -} from '../core/manager' +import { manualGrantCorePermition, quitWithoutCore, restartCore } from '../core/manager' import { triggerSysProxy } from '../sys/sysproxy' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' import { @@ -172,8 +167,6 @@ 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('isEncryptionAvailable', isEncryptionAvailable) - ipcMain.handle('encryptString', (_e, str) => encryptString(str)) ipcMain.handle('manualGrantCorePermition', (_e, password) => ipcErrorWrapper(manualGrantCorePermition)(password) ) @@ -256,7 +249,3 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('quitWithoutCore', ipcErrorWrapper(quitWithoutCore)) ipcMain.handle('quitApp', () => app.quit()) } - -function encryptString(str: string): number[] { - return safeStorage.encryptString(str).toJSON().data -} diff --git a/src/renderer/src/components/sider/tun-switcher.tsx b/src/renderer/src/components/sider/tun-switcher.tsx index a2b3f07..97b7020 100644 --- a/src/renderer/src/components/sider/tun-switcher.tsx +++ b/src/renderer/src/components/sider/tun-switcher.tsx @@ -3,19 +3,16 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c import BorderSwitch from '@renderer/components/base/border-swtich' import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb' import { useLocation } from 'react-router-dom' -import { encryptString, isEncryptionAvailable, restartCore } from '@renderer/utils/ipc' +import { restartCore } from '@renderer/utils/ipc' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { platform } from '@renderer/utils/init' -import React, { useState } from 'react' +import React from 'react' import { useAppConfig } from '@renderer/hooks/use-app-config' -import BasePasswordModal from '../base/base-password-modal' const TunSwitcher: React.FC = () => { const location = useLocation() const match = location.pathname.includes('/tun') || false - const [openPasswordModal, setOpenPasswordModal] = useState(false) - const { appConfig, patchAppConfig } = useAppConfig() + const { appConfig } = useAppConfig() const { tunCardStatus = 'col-span-1' } = appConfig || {} const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() const { tun } = controledMihomoConfig || {} @@ -32,19 +29,6 @@ const TunSwitcher: React.FC = () => { }) const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const onChange = async (enable: boolean): Promise => { - if (enable && platform === 'darwin') { - const encryptionAvailable = await isEncryptionAvailable() - if (!appConfig?.encryptedPassword && encryptionAvailable) { - setOpenPasswordModal(true) - return - } - if (!appConfig?.encryptedPassword && !encryptionAvailable) { - alert('加密不可用,请手动给内核授权') - await patchAppConfig({ encryptedPassword: [] }) - return - } - } - if (enable) { await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) } else { @@ -65,24 +49,6 @@ const TunSwitcher: React.FC = () => { }} className={`${tunCardStatus} tun-card`} > - {openPasswordModal && ( - setOpenPasswordModal(false)} - onConfirm={async (password: string) => { - try { - const encrypted = await encryptString(password) - await patchAppConfig({ encryptedPassword: encrypted }) - await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } }) - await restartCore() - window.electron.ipcRenderer.send('updateTrayMenu') - setOpenPasswordModal(false) - } catch (e) { - alert(e) - } - }} - /> - )} - { setTimeout(() => { PubSub.publish('mihomo-core-changed') }, 2000) - if (platform === 'linux') { + if (platform !== 'win32') { new Notification('内核权限丢失', { body: '内核升级成功,若要使用虚拟网卡(Tun),请到虚拟网卡页面重新手动授权内核' }) diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 90aa6fc..1097967 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -199,14 +199,6 @@ export async function triggerSysProxy(enable: boolean): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable)) } -export async function isEncryptionAvailable(): Promise { - return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('isEncryptionAvailable')) -} - -export async function encryptString(str: string): Promise { - return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('encryptString', str)) -} - export async function manualGrantCorePermition(password?: string): Promise { return ipcErrorWrapper( await window.electron.ipcRenderer.invoke('manualGrantCorePermition', password)