import { emitKillProcessEvent } from "@kksh/api/events" import { Channel, invoke } from "@tauri-apps/api/core" import { emitTo } from "@tauri-apps/api/event" import { getCurrentWindow } from "@tauri-apps/api/window" import { toast } from "svelte-sonner" import { hasCommand, whereIsCommand, type ChildProcess, type CommandEvent, type InternalSpawnOptions, type IOPayload } from "tauri-plugin-shellx-api" import * as shell from "tauri-plugin-shellx-api" import type { DenoRunConfig } from "../../api/client" import type { IShellServer } from "../../api/server-types" import { RECORD_EXTENSION_PROCESS_EVENT, type IRecordExtensionProcessEvent } from "../../events" import { ShellPermissionMap } from "../../permissions/permission-map" import { type ShellPermission, type ShellPermissionScoped } from "../../permissions/schema" import { verifyScopedPermission } from "../../utils/path" import { translateDenoCommand, verifyDenoCmdPermission } from "./deno" function matchRegexArgs(args: string[], regexes: string[]): boolean { if (args.length !== regexes.length) { return false } for (let i = 0; i < args.length; i++) { let regex = regexes[i]! if (!regex.startsWith("^")) { regex = `^${regex}` } if (!regex.endsWith("$")) { regex = `${regex}$` } if (!new RegExp(regex).test(args[i]!)) { return false } } return true } async function verifyShellCmdPermission( requiredPermissions: ShellPermission[], userPermissionScopes: ShellPermissionScoped[], program: string, args: string[] ): Promise { for (const permission of userPermissionScopes) { if (requiredPermissions.includes(permission.permission)) { for (const deny of permission.deny || []) { if (deny.cmd && deny.cmd.program === program && matchRegexArgs(args, deny.cmd.args || [])) { return Promise.reject("Shell Command Permission Denied by deny rule") } } for (const allow of permission.allow || []) { if ( allow.cmd && allow.cmd.program === program && matchRegexArgs(args, allow.cmd.args || []) ) { return Promise.resolve() } } } } return Promise.reject("Shell Command Permission Denied, no allow rule found") } /** * `tauri-api-adapter` provides shell API. * In kunkun I provide a more granular permission system and extra shell script execution APIs, so I rewrite the shell server API constructor * @param permissions * @param extPath absolute path to the extension * @returns */ export function constructShellApi( permissions: (ShellPermissionScoped | ShellPermission)[], extPath: string, recordSpawnedProcess: (pid: number) => Promise, getSpawnedProcesses: () => Promise ): IShellServer { const stringPermissiongs = permissions.filter((p) => typeof p === "string") as ShellPermission[] const objectPermissions = permissions.filter( (p) => typeof p !== "string" ) as ShellPermissionScoped[] async function execute( program: string, args: string[], options: InternalSpawnOptions ): Promise> { await verifyShellCmdPermission( ShellPermissionMap.execute, objectPermissions, program, args ).catch((err) => { throw new Error(err) }) return invoke>("plugin:shellx|execute", { program: program, args: args, options: options }) } async function kill(pid: number) { if (!stringPermissiongs.some((p) => ShellPermissionMap.kill.includes(p))) { return Promise.reject( new Error(`Permission denied. Requires one of ${ShellPermissionMap.kill}`) ) } const pids = await getSpawnedProcesses() if (!pids.includes(pid)) { return Promise.reject(new Error(`Process ${pid} not spawned by this extension`)) } return invoke("plugin:shellx|kill", { cmd: "killChild", pid }).then(() => { emitKillProcessEvent(pid) }) } function stdinWrite(buffer: string | number[], pid: number) { if (!stringPermissiongs.some((p) => ShellPermissionMap.stdinWrite.includes(p))) return Promise.reject( new Error(`Permission denied. Requires one of ${ShellPermissionMap.stdinWrite}`) ) return invoke("plugin:shellx|stdin_write", { buffer: buffer, pid: pid }) } async function open(path: string, openWith?: string) { // TODO: consider adding a base dir option like the fs api's // this should throw if permission is denied if ( await verifyScopedPermission( objectPermissions.filter( (p) => p.permission === "shell:open" || p.permission === "shell:all" ), path, "url", extPath ) ) { return open(path, openWith) } else { throw new Error(`Permission denied to open file: ${path}`) } } // shellOpen: verifyShellCmdPermission(["shell:open"], permissions)(shellOpen), async function rawSpawn( program: string, args: string[], options: InternalSpawnOptions, cb: (evt: CommandEvent) => void ) { await verifyShellCmdPermission( ShellPermissionMap.rawSpawn, objectPermissions, program, args ).catch((err) => { toast.error("Permission denied", { description: err.message }) console.error("rawSpawn permission denied", err) throw err }) const onEvent = new Channel>() onEvent.onmessage = cb return invoke("plugin:shellx|spawn", { program, args, options, onEvent }) } async function executeBashScript(script: string): Promise> { await verifyShellCmdPermission(ShellPermissionMap.execute, objectPermissions, "bash", [ "-c", script ]) return executeBashScript(script) } async function executePowershellScript(script: string): Promise> { await verifyShellCmdPermission( ShellPermissionMap.executePowershellScript, objectPermissions, "powershell", ["-WindowStyle", "Hidden", "-Command", script] ) return executePowershellScript(script) } async function executeAppleScript(script: string): Promise> { await verifyShellCmdPermission( ShellPermissionMap.executeAppleScript, objectPermissions, "osascript", ["-e", script] ) return executeAppleScript(script) } async function executePythonScript(script: string): Promise> { await verifyShellCmdPermission( ShellPermissionMap.executePythonScript, objectPermissions, "python", ["-c", script] ) return executePythonScript(script) } async function executeZshScript(script: string): Promise> { await verifyShellCmdPermission(ShellPermissionMap.executeZshScript, objectPermissions, "zsh", [ "-c", script ]) return executeZshScript(script) } async function executeNodeScript(script: string): Promise> { await verifyShellCmdPermission( ShellPermissionMap.executeNodeScript, objectPermissions, "node", ["-e", script] ) return executeNodeScript(script) } async function likelyOnWindows(): Promise { return likelyOnWindows() } function killPid(pid: number) { if (!stringPermissiongs.some((p) => ShellPermissionMap.killPid.includes(p))) return Promise.reject( new Error(`Permission denied. Requires one of ${ShellPermissionMap.killPid}`) ) return shell.killPid(pid) } return { whereIsCommand(command: string): Promise { const cleanedCommand = command.trim().split(" ")[0] if (!cleanedCommand) { return Promise.resolve(null) } return whereIsCommand(cleanedCommand).then((res) => (res === "" ? null : res)) }, recordSpawnedProcess, async denoExecute( scriptPath: string, config: DenoRunConfig, args1: string[] ): Promise> { await verifyDenoCmdPermission( objectPermissions.filter((p) => p.permission.startsWith("shell:deno:")), "deno", scriptPath, config, extPath ) const { program, args, options } = await translateDenoCommand( scriptPath, config, args1, extPath ) return invoke>("plugin:shellx|execute", { program, args, options }) // return execute(program, args, options) }, async denoRawSpawn( scriptPath: string, config: DenoRunConfig, args1: string[], cb: (evt: CommandEvent) => void ): Promise { await verifyDenoCmdPermission( objectPermissions.filter((p) => p.permission.startsWith("shell:deno:")), "deno", scriptPath, config, extPath ) const { program, args, options } = await translateDenoCommand( scriptPath, config, args1, extPath ) const onEvent = new Channel>() onEvent.onmessage = cb return invoke("plugin:shellx|spawn", { program, args, options, onEvent }) // return rawSpawn(program, args, options, cb) }, execute, kill, killPid, stdinWrite, open, rawSpawn, executeBashScript, executePowershellScript, executeAppleScript, executePythonScript, executeZshScript, executeNodeScript, hasCommand: (command: string): Promise => { // check if command is clean, check if it's a single command without arguments or semicolons with regex. if (!/^[a-zA-Z0-9_-]+$/.test(command)) { return Promise.reject(new Error("Invalid command")) } return hasCommand(command) }, likelyOnWindows } }