diff --git a/.gitignore b/.gitignore index 784b4536..7b14a388 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ package-lock.json # *.exe temp guest_server/winboat_guest_server.exe -guest_server/winboat_guest_server.zip \ No newline at end of file +guest_server/winboat_guest_server.zip +rdp_exec/rdp_exec.exe \ No newline at end of file diff --git a/build-rdp-exec.sh b/build-rdp-exec.sh new file mode 100644 index 00000000..5188113c --- /dev/null +++ b/build-rdp-exec.sh @@ -0,0 +1,2 @@ +cd rdp_exec +GOOS=windows GOARCH=amd64 go build -o rdp_exec.exe -ldflags="-H windowsgui" \ No newline at end of file diff --git a/package.json b/package.json index 1088d97d..2174ee48 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "scripts": { "dev": "node scripts/dev-server.ts", "build-guest-server": "bash build-guest-server.sh", - "build:linux-gs": "bash build-guest-server.sh && node scripts/build.ts && electron-builder --linux" + "build-rdp-exec": "bash build-rdp-exec.sh", + "build:linux-gs": "bash build-guest-server.sh && bash build-rdp-exec.sh && node scripts/build.ts && electron-builder --linux" }, "repository": "https://github.com/TibixDev/winboat", "author": { diff --git a/rdp_exec/go.mod b/rdp_exec/go.mod new file mode 100644 index 00000000..ab61c245 --- /dev/null +++ b/rdp_exec/go.mod @@ -0,0 +1,3 @@ +module rdp-exec + +go 1.24.8 diff --git a/rdp_exec/main.go b/rdp_exec/main.go new file mode 100644 index 00000000..dba1f2dd --- /dev/null +++ b/rdp_exec/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "encoding/base64" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "syscall" + "unsafe" +) + +var ( + cmd = flag.String("cmd", "", "Command to run") + cmd_args = flag.String("cmd_args", "", "Command args") + dummy = flag.String("dummy", "", "Dummy info") +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + shlwapi = syscall.NewLazyDLL("Shlwapi.dll") + procAssocQueryStringW = shlwapi.NewProc("AssocQueryStringW") + procExpandEnvString = kernel32.NewProc("ExpandEnvironmentStringsW") +) + +const ( + ASSOCF_NONE = 0 + ASSOCSTR_EXECUTABLE = 2 +) + +func getDefaultApp(extension string) (string, error) { + if !strings.HasPrefix(extension, ".") { + extension = "." + extension + } + + extUTF16, err := syscall.UTF16PtrFromString(extension) + if err != nil { + return "", err + } + + var size uint32 = 260 + buf := make([]uint16, size) + + ret, _, _ := procAssocQueryStringW.Call( + uintptr(ASSOCF_NONE), + uintptr(ASSOCSTR_EXECUTABLE), + uintptr(unsafe.Pointer(extUTF16)), + 0, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size)), + ) + + if ret != 0 { + return "", fmt.Errorf("failed to get default app") + } + + return syscall.UTF16ToString(buf), nil +} + +func expandWindowsEnv(s string) string { + utf16Str, _ := syscall.UTF16FromString(s) + + n, _, _ := procExpandEnvString.Call( + uintptr(unsafe.Pointer(&utf16Str[0])), + 0, + 0, + ) + + if n == 0 { + return s + } + + buf := make([]uint16, n) + procExpandEnvString.Call( + uintptr(unsafe.Pointer(&utf16Str[0])), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(n), + ) + + return syscall.UTF16ToString(buf) +} + +func decodeb64(b64 string) string { + res, _ := base64.StdEncoding.DecodeString(b64) + return string(res) +} + +func main() { + flag.Parse() + + if *cmd == "" || *dummy == "" { + os.Exit(1) + } + + cmd_decoded := decodeb64(*cmd) + args := strings.Fields(cmd_decoded) + + if len(args) == 0 { + os.Exit(1) + } + + cmd_full := cmd_decoded + + ext := strings.ToLower(filepath.Ext(cmd_decoded)) + + if ext != "" && ext != ".exe" && ext != ".bat" && ext != ".cmd" { + defaultApp, err := getDefaultApp(ext) + if err == nil { + cmd_decoded = `"` + defaultApp + `" "` + cmd_decoded + `"` + } + } + + cmd_full = cmd_decoded + + if *cmd_args != "" { + cmd_full += " " + decodeb64(*cmd_args) + } + + expandedCmd := expandWindowsEnv(cmd_full) + commandLine := syscall.StringToUTF16Ptr(expandedCmd) + workingDir := syscall.StringToUTF16Ptr(`C:\Windows\System32`) + + var startupInfo syscall.StartupInfo + var processInfo syscall.ProcessInformation + + startupInfo.Cb = uint32(unsafe.Sizeof(startupInfo)) + + syscall.CreateProcess( + nil, + commandLine, + nil, // Default process security attributes + nil, // Default thread security attributes + false, // Inherit handles + 0, // Creation flags + nil, // Use parent's environment + workingDir, + &startupInfo, + &processInfo, + ) + + syscall.WaitForSingleObject(processInfo.Process, syscall.INFINITE) + syscall.CloseHandle(processInfo.Process) + syscall.CloseHandle(processInfo.Thread) +} diff --git a/src/renderer/lib/install.ts b/src/renderer/lib/install.ts index a3169e60..5d22515c 100644 --- a/src/renderer/lib/install.ts +++ b/src/renderer/lib/install.ts @@ -187,6 +187,10 @@ export class InstallManager { ? path.join(process.resourcesPath, "guest_server") // For packaged app : path.join(remote.app.getAppPath(), "..", "..", "guest_server"); // For dev mode + const rdpExecPath = remote.app.isPackaged + ? path.join(process.resourcesPath, "rdp_exec") // For packaged app + : path.join(remote.app.getAppPath(), "..", "..", "rdp_exec"); // For dev mode + logger.info(`Guest server source path: ${appPath}`); // Check if the source directory exists @@ -228,6 +232,15 @@ export class InstallManager { const destPath = path.join(oemPath, entry); copyRecursive(srcPath, destPath); }); + fs.readdirSync(rdpExecPath).forEach(entry => { + const srcPath = path.join(rdpExecPath, entry); + const rdpExecOemPath = path.join(oemPath, "rdp_exec"); + if (!fs.existsSync(rdpExecOemPath)) { + fs.mkdirSync(rdpExecOemPath); + } + const destPath = path.join(rdpExecOemPath, entry); + copyRecursive(srcPath, destPath); + }); logger.info("OEM assets created successfully"); } catch (error) { logger.error(`Failed to copy OEM assets: ${error}`); diff --git a/src/renderer/lib/winboat.ts b/src/renderer/lib/winboat.ts index 8ea5f406..159d94af 100644 --- a/src/renderer/lib/winboat.ts +++ b/src/renderer/lib/winboat.ts @@ -29,6 +29,7 @@ const { promisify }: typeof import("util") = require("node:util"); const { exec }: typeof import("child_process") = require("node:child_process"); const remote: typeof import("@electron/remote") = require("@electron/remote"); const FormData: typeof import("form-data") = require("form-data"); +const crypto: typeof import("crypto") = require("crypto"); const execAsync = promisify(exec); const USAGE_PATH = path.join(WINBOAT_DIR, "appUsage.json"); @@ -95,6 +96,8 @@ const customAppCallbacks: CustomAppCallbacks = { }, }; +const toBase64 = (s: string) => s ? btoa(s) : ''; + export const ContainerStatus = { Created: "created", Restarting: "restarting", @@ -669,7 +672,7 @@ export class Winboat { console.info("So long and thanks for all the fish!"); } - async launchApp(app: WinApp) { + async launchApp(app: WinApp, shiftPressed=false) { if (!this.isOnline) throw new Error("Cannot launch app, Winboat is offline"); if (customAppCallbacks[app.Path]) { @@ -715,7 +718,8 @@ export class Winboat { /scale-desktop:${this.#wbConfig?.config.scaleDesktop ?? 100}\ ${combinedArgs}\ /wm-class:"winboat-${cleanAppName}"\ - /app:program:"${app.Path}",name:"${cleanAppName}",cmd:"${app.Args}" &`; + /app:program:"C:\\Program Files\\WinBoat\\rdp_exec\\rdp_exec.exe",cmd:"-cmd ${toBase64(app.Path)} ${app.Args ? '-cmd_args ' + toBase64(app.Args) : ''} -dummy ${shiftPressed ? crypto.randomUUID().toString(): 1}",name:"${cleanAppName}" &`; + if (app.Path == InternalApps.WINDOWS_DESKTOP) { cmd = `${freeRDPBin} /u:"${username}"\ diff --git a/src/renderer/views/Apps.vue b/src/renderer/views/Apps.vue index 07122063..0e5af892 100644 --- a/src/renderer/views/Apps.vue +++ b/src/renderer/views/Apps.vue @@ -180,7 +180,7 @@ :key="app.id" class="flex relative flex-row gap-2 justify-between items-center p-2 my-0 backdrop-blur-xl backdrop-brightness-150 cursor-pointer generic-hover bg-neutral-800/20" :class="{ 'bg-gradient-to-r from-yellow-600/20 bg-neutral-800/20': app.Source === 'custom' }" - @click="winboat.launchApp(app)" + @click="(e: any) => winboat.launchApp(app, e.shiftKey)" @contextmenu="openContextMenu($event, app)" >