Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ package-lock.json
# *.exe
temp
guest_server/winboat_guest_server.exe
guest_server/winboat_guest_server.zip
guest_server/winboat_guest_server.zip
rdp_exec/rdp_exec.exe
2 changes: 2 additions & 0 deletions build-rdp-exec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cd rdp_exec
GOOS=windows GOARCH=amd64 go build -o rdp_exec.exe -ldflags="-H windowsgui"
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions rdp_exec/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module rdp-exec

go 1.24.8
145 changes: 145 additions & 0 deletions rdp_exec/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 13 additions & 0 deletions src/renderer/lib/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`);
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/lib/winboat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -95,6 +96,8 @@ const customAppCallbacks: CustomAppCallbacks = {
},
};

const toBase64 = (s: string) => s ? btoa(s) : '';

export const ContainerStatus = {
Created: "created",
Restarting: "restarting",
Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -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}"\
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/views/Apps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
>
<div class="flex flex-row items-center gap-2 w-[85%]">
Expand Down