- Go stdlib-only gateway: serves noVNC client, password auth, WS<->TCP relay - auth: PBKDF2-HMAC-SHA256 password hash, HMAC session cookies, login rate limit - relay: hand-written RFC 6455 WebSocket + transparent RFB bridge - vncspawner: cross-OS VNC server detection/launch (Windows/Linux/macOS) - server: /login /logout /vnc /api/status routes, session middleware, embed.FS - scripts: get-novnc, get-vnc (zip-verified), list-ips, open-firewall - run.bat: one-click launcher (asks only password), lists access IPs - README/AGENTS.md (Russian), .gitignore (root-anchored binaries)
116 lines
2.7 KiB
Go
116 lines
2.7 KiB
Go
// Package vncspawner discovers and launches a VNC server as a child process
|
|
// so the gateway can run with a single command.
|
|
package vncspawner
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Candidate is a discovered VNC server program + args to launch it.
|
|
type Candidate struct {
|
|
Path string
|
|
Args []string
|
|
Desc string
|
|
}
|
|
|
|
// Process is a launched VNC server child process.
|
|
type Process struct {
|
|
cmd *exec.Cmd
|
|
}
|
|
|
|
// Stop terminates the child process.
|
|
func (p *Process) Stop() {
|
|
if p == nil || p.cmd == nil || p.cmd.Process == nil {
|
|
return
|
|
}
|
|
log.Printf("vncspawner: stopping %s", p.cmd.String())
|
|
_ = p.cmd.Process.Kill()
|
|
}
|
|
|
|
// Detect searches for an installed VNC server and returns a launch candidate.
|
|
func Detect() (Candidate, error) {
|
|
cands := candidates()
|
|
for _, c := range cands {
|
|
if c.Path != "" {
|
|
return c, nil
|
|
}
|
|
}
|
|
return Candidate{}, fmt.Errorf("no supported VNC server found; install one of: %s", strings.Join(detectionHints(), ", "))
|
|
}
|
|
|
|
// detectionHints returns human-readable installation hints per OS.
|
|
func detectionHints() []string {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
return []string{"UltraVNC (winvnc.exe)", "TightVNC (tvnserver.exe)"}
|
|
case "darwin":
|
|
return []string{"macOS Screen Sharing"}
|
|
default:
|
|
return []string{"x11vnc", "TigerVNC (Xvnc/tigervncserver)"}
|
|
}
|
|
}
|
|
|
|
// Launch starts a VNC server from a candidate (or explicit command line) as a child.
|
|
// cmdLine is non-empty it takes precedence (split by spaces, simple shlex).
|
|
func Launch(c Candidate, cmdLine string) (*Process, error) {
|
|
var path string
|
|
var args []string
|
|
var desc string
|
|
|
|
if cmdLine != "" {
|
|
parts := splitArgs(cmdLine)
|
|
if len(parts) == 0 {
|
|
return nil, fmt.Errorf("empty spawn command")
|
|
}
|
|
path = parts[0]
|
|
args = parts[1:]
|
|
desc = cmdLine
|
|
} else {
|
|
if c.Path == "" {
|
|
return nil, fmt.Errorf("no VNC server candidate available")
|
|
}
|
|
path = c.Path
|
|
args = c.Args
|
|
desc = c.Desc
|
|
}
|
|
|
|
cmd := exec.Command(path, args...)
|
|
hideWindow(cmd)
|
|
log.Printf("vncspawner: launching %s -> %s", desc, cmd.String())
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("launch %s: %w", path, err)
|
|
}
|
|
|
|
p := &Process{cmd: cmd}
|
|
|
|
// Reap/wait the child in background; log exit.
|
|
go func() {
|
|
err := cmd.Wait()
|
|
if err != nil {
|
|
log.Printf("vncspawner: %s exited: %v", desc, err)
|
|
} else {
|
|
log.Printf("vncspawner: %s exited cleanly", desc)
|
|
}
|
|
}()
|
|
|
|
// Give the server a moment to start listening.
|
|
time.Sleep(1200 * time.Millisecond)
|
|
return p, nil
|
|
}
|
|
|
|
// splitArgs is a minimal whitespace splitter (no quote handling). For complex
|
|
// commands prefer an explicit binary path via configuration instead.
|
|
func splitArgs(s string) []string {
|
|
var out []string
|
|
for _, f := range strings.Fields(s) {
|
|
out = append(out, f)
|
|
}
|
|
return out
|
|
}
|