feat: support to grant permission to clash core

This commit is contained in:
GyDi 2023-03-16 11:16:54 +08:00
parent e25a455698
commit 54a5007c01
No known key found for this signature in database
GPG Key ID: 9C3AD40F1F99880A
7 changed files with 177 additions and 3 deletions

View File

@ -175,6 +175,15 @@ pub async fn restart_sidecar() -> CmdResult {
wrap_err!(CoreManager::global().run_core().await) wrap_err!(CoreManager::global().run_core().await)
} }
#[tauri::command]
pub fn grant_permission(core: String) -> CmdResult {
#[cfg(target_os = "macos")]
return wrap_err!(manager::grant_permission(core));
#[cfg(not(target_os = "macos"))]
return Err("Unsupported target");
}
/// get the system proxy /// get the system proxy
#[tauri::command] #[tauri::command]
pub fn get_sys_proxy() -> CmdResult<Mapping> { pub fn get_sys_proxy() -> CmdResult<Mapping> {

View File

@ -0,0 +1,37 @@
/// 给clash内核的tun模式授权
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn grant_permission(core: String) -> anyhow::Result<()> {
use std::process::Command;
use tauri::utils::platform::current_exe;
let path = current_exe()?.with_file_name(core).canonicalize()?;
let path = path.display();
log::debug!("grant_permission path: {path}");
#[cfg(target_os = "macos")]
let output = {
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
Command::new("osascript")
.args(vec!["-e", &command])
.output()?
};
#[cfg(target_os = "linux")]
let output = {
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
Command::new("sudo")
.arg("sh")
.arg("-c")
.arg(shell)
.output()?
};
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
anyhow::bail!("{stderr}");
}
}

View File

@ -3,6 +3,7 @@ mod core;
pub mod handle; pub mod handle;
pub mod hotkey; pub mod hotkey;
pub mod logger; pub mod logger;
pub mod manager;
pub mod sysopt; pub mod sysopt;
pub mod timer; pub mod timer;
pub mod tray; pub mod tray;

View File

@ -36,6 +36,7 @@ fn main() -> std::io::Result<()> {
cmds::open_core_dir, cmds::open_core_dir,
// cmds::kill_sidecar, // cmds::kill_sidecar,
cmds::restart_sidecar, cmds::restart_sidecar,
cmds::grant_permission,
// clash // clash
cmds::get_clash_info, cmds::get_clash_info,
cmds::get_clash_logs, cmds::get_clash_logs,

View File

@ -0,0 +1,106 @@
import { mutate } from "swr";
import { forwardRef, useImperativeHandle, useState } from "react";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useLockFn } from "ahooks";
import { Lock } from "@mui/icons-material";
import { IconButton, List, ListItemButton, ListItemText } from "@mui/material";
import { changeClashCore } from "@/services/cmds";
import { closeAllConnections } from "@/services/api";
import { grantPermission } from "@/services/cmds";
import getSystem from "@/utils/get-system";
const VALID_CORE = [
{ name: "Clash", core: "clash" },
{ name: "Clash Meta", core: "clash-meta" },
];
const OS = getSystem();
export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { verge, mutateVerge } = useVerge();
const [open, setOpen] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
const { clash_core = "clash" } = verge ?? {};
const onCoreChange = useLockFn(async (core: string) => {
if (core === clash_core) return;
try {
closeAllConnections();
await changeClashCore(core);
mutateVerge();
setTimeout(() => {
mutate("getClashConfig");
mutate("getVersion");
}, 100);
Notice.success(`Successfully switch to ${core}`, 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const onGrant = useLockFn(async (core: string) => {
try {
await grantPermission(core);
Notice.success(`Successfully grant permission to ${core}`, 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={t("Clash Core")}
contentSx={{
pb: 0,
width: 320,
height: 200,
overflowY: "auto",
userSelect: "text",
marginTop: "-8px",
}}
disableOk
cancelBtn={t("Back")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List component="nav">
{VALID_CORE.map((each) => (
<ListItemButton
key={each.core}
selected={each.core === clash_core}
onClick={() => onCoreChange(each.core)}
>
<ListItemText primary={each.name} secondary={`/${each.core}`} />
{(OS === "macos" || OS === "linux") && (
<IconButton
color="inherit"
size="small"
edge="end"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onGrant(each.core);
}}
>
<Lock fontSize="inherit" />
</IconButton>
)}
</ListItemButton>
))}
</List>
</BaseDialog>
);
});

View File

@ -8,16 +8,16 @@ import {
Typography, Typography,
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import { ArrowForward } from "@mui/icons-material"; import { ArrowForward, Settings } from "@mui/icons-material";
import { DialogRef } from "@/components/base"; import { DialogRef } from "@/components/base";
import { useClash } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
import { CoreSwitch } from "./mods/core-switch";
import { WebUIViewer } from "./mods/web-ui-viewer"; import { WebUIViewer } from "./mods/web-ui-viewer";
import { ClashFieldViewer } from "./mods/clash-field-viewer"; import { ClashFieldViewer } from "./mods/clash-field-viewer";
import { ClashPortViewer } from "./mods/clash-port-viewer"; import { ClashPortViewer } from "./mods/clash-port-viewer";
import { ControllerViewer } from "./mods/controller-viewer"; import { ControllerViewer } from "./mods/controller-viewer";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingList, SettingItem } from "./mods/setting-comp";
import { ClashCoreViewer } from "./mods/clash-core-viewer";
interface Props { interface Props {
onError: (err: Error) => void; onError: (err: Error) => void;
@ -39,6 +39,7 @@ const SettingClash = ({ onError }: Props) => {
const fieldRef = useRef<DialogRef>(null); const fieldRef = useRef<DialogRef>(null);
const portRef = useRef<DialogRef>(null); const portRef = useRef<DialogRef>(null);
const ctrlRef = useRef<DialogRef>(null); const ctrlRef = useRef<DialogRef>(null);
const coreRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => { const onChangeData = (patch: Partial<IConfigData>) => {
@ -51,6 +52,7 @@ const SettingClash = ({ onError }: Props) => {
<ClashFieldViewer ref={fieldRef} /> <ClashFieldViewer ref={fieldRef} />
<ClashPortViewer ref={portRef} /> <ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} /> <ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} />
<SettingItem label={t("Allow Lan")}> <SettingItem label={t("Allow Lan")}>
<GuardState <GuardState
@ -143,7 +145,21 @@ const SettingClash = ({ onError }: Props) => {
</IconButton> </IconButton>
</SettingItem> </SettingItem>
<SettingItem label={t("Clash Core")} extra={<CoreSwitch />}> <SettingItem
label={t("Clash Core")}
extra={
<IconButton
color="inherit"
size="small"
onClick={() => coreRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
}
>
<Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography> <Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography>
</SettingItem> </SettingItem>
</SettingList> </SettingList>

View File

@ -127,6 +127,10 @@ export async function restartSidecar() {
return invoke<void>("restart_sidecar"); return invoke<void>("restart_sidecar");
} }
export async function grantPermission(core: string) {
return invoke<void>("grant_permission", { core });
}
export async function openAppDir() { export async function openAppDir() {
return invoke<void>("open_app_dir").catch((err) => return invoke<void>("open_app_dir").catch((err) =>
Notice.error(err?.message || err.toString(), 1500) Notice.error(err?.message || err.toString(), 1500)