feat: web-vnc single-binary browser VNC gateway with password access

- 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)
This commit is contained in:
Codex
2026-07-30 17:40:31 +03:00
commit b89477fb87
24 changed files with 2057 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
// Package config holds runtime configuration for the web-vnc gateway.
package config
import (
"crypto/rand"
"flag"
"fmt"
"os"
"runtime"
"strings"
"time"
)
// Config is the resolved application configuration.
type Config struct {
ListenAddr string // HTTP/WS listen address, e.g. ":8080"
VNCAddr string // upstream VNC server address, e.g. "127.0.0.1:5900"
VNCPassword string // optional VNC server password, auto-sent to noVNC (empty = none)
PasswordHash string // hash of the shared web access password
SessionSecret []byte // HMAC key for signing session cookies
CookieName string // name of the session cookie
SessionTTL time.Duration // validity of a session cookie
WebRoot string // optional on-disk override for static files (empty = embed.FS)
SpawnVNC bool // auto-launch a VNC server on startup
SpawnCommand string // optional explicit command to launch the VNC server
NoVNCPath string // path serving the noVNC client page
RelayPath string // path of the WS relay endpoint
LoginPath string
LogoutPath string
StatusPath string // VNC health-check endpoint path
OS string // resolved host OS (GOOS) for the spawner
}
// Parse reads flags + env and returns a validated Config.
func Parse(args []string) (Config, error) {
fs := flag.NewFlagSet("web-vnc", flag.ContinueOnError)
var (
secretStr string
)
c := Config{}
fs.StringVar(&c.ListenAddr, "listen", envStr("WEBVNC_LISTEN", ":8080"), "HTTP listen address")
fs.StringVar(&c.VNCAddr, "vnc", envStr("WEBVNC_VNC", "127.0.0.1:5900"), "upstream VNC server address")
fs.StringVar(&c.VNCPassword, "vnc-password", envStr("WEBVNC_VNC_PASSWORD", ""), "VNC server password to auto-send to noVNC (empty = server has no VNC password)")
fs.StringVar(&c.PasswordHash, "password-hash", envStr("WEBVNC_PASSWORD_HASH", ""), "hash of the shared password (use --gen-hash to create one)")
fs.StringVar(&secretStr, "session-secret", envStr("WEBVNC_SESSION_SECRET", ""), "HMAC secret for signing session cookies (random if empty)")
fs.StringVar(&c.CookieName, "cookie-name", "webvnc_session", "session cookie name")
fs.DurationVar(&c.SessionTTL, "session-ttl", envDur("WEBVNC_SESSION_TTL", 8*time.Hour), "session cookie lifetime")
fs.StringVar(&c.WebRoot, "web-root", envStr("WEBVNC_WEB_ROOT", ""), "optional on-disk static files dir (overrides embedded assets)")
fs.BoolVar(&c.SpawnVNC, "spawn", envBool("WEBVNC_SPAWN", false), "auto-launch a detected VNC server on startup")
fs.StringVar(&c.SpawnCommand, "spawn-command", envStr("WEBVNC_SPAWN_COMMAND", ""), "explicit command to launch the VNC server (overrides auto-detection)")
fs.StringVar(&c.NoVNCPath, "novnc-path", "/vnc.html", "path serving the noVNC client page")
fs.StringVar(&c.RelayPath, "relay-path", "/vnc", "WebSocket relay endpoint path")
fs.StringVar(&c.LoginPath, "login-path", "/login", "login endpoint path")
fs.StringVar(&c.LogoutPath, "logout-path", "/logout", "logout endpoint path")
fs.StringVar(&c.StatusPath, "status-path", "/api/status", "VNC health check endpoint path")
if err := fs.Parse(args); err != nil {
return c, err
}
c.OS = runtime.GOOS
if secretStr != "" {
c.SessionSecret = []byte(secretStr)
} else {
c.SessionSecret = randBytes(32)
}
if c.PasswordHash == "" {
return c, fmt.Errorf("a password is required: provide --password-hash (hash) or run 'web-vnc --gen-hash <password>'")
}
return c, nil
}
func envStr(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok && v != "" {
return v
}
return fallback
}
func envBool(key string, fallback bool) bool {
if v, ok := os.LookupEnv(key); ok && v != "" {
return strings.EqualFold(v, "true") || v == "1" || strings.EqualFold(v, "yes")
}
return fallback
}
func envDur(key string, fallback time.Duration) time.Duration {
if v, ok := os.LookupEnv(key); ok && v != "" {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return fallback
}
func randBytes(n int) []byte {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
panic("crypto/rand failed: " + err.Error())
}
return b
}