Files
web-vnc/internal/relay/relay.go
T
Codex b89477fb87 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)
2026-07-30 17:40:31 +03:00

125 lines
2.4 KiB
Go

package relay
import (
"io"
"log"
"net"
"net/http"
"sync"
"time"
)
// Server bridges WS connections on RelayPath to the upstream VNC TCP server.
type Server struct {
vncAddr string
}
// New returns a relay Server targeting the given VNC TCP address.
func New(vncAddr string) *Server {
return &Server{vncAddr: vncAddr}
}
// ServeHTTP upgrades to WebSocket and bridges to the VNC server.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ws, err := handshake(w, r)
if err != nil {
// handshake failed before hijack; respond with an error.
http.Error(w, "websocket handshake failed: "+err.Error(), http.StatusBadRequest)
return
}
defer ws.close()
vnc, err := net.DialTimeout("tcp", s.vncAddr, 10*time.Second)
if err != nil {
log.Printf("relay: dial vnc %s failed: %v", s.vncAddr, err)
ws.writeClose()
return
}
defer vnc.Close()
bridge(ws, vnc)
}
// bridge pumps bytes between the WebSocket and the TCP connection until either side closes.
func bridge(ws *wsConn, vnc net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
// TCP -> WS
go func() {
defer wg.Done()
buf := make([]byte, 4096)
for {
n, err := vnc.Read(buf)
if n > 0 {
if werr := ws.writeBinary(buf[:n]); werr != nil {
return
}
}
if err != nil {
if err != io.EOF {
log.Printf("relay: vnc read: %v", err)
}
ws.writeClose()
return
}
}
}()
// WS -> TCP
go func() {
defer wg.Done()
for {
opcode, payload, err := ws.readFrame()
if err != nil {
if err != io.EOF && !isClosedConnErr(err) {
log.Printf("relay: ws read: %v", err)
}
_ = vnc.Close()
return
}
switch opcode {
case opBinary, opText, opContinuation:
if len(payload) > 0 {
if _, err := vnc.Write(payload); err != nil {
return
}
}
case opPing:
_ = ws.writePong(payload)
case opPong:
// ignore
case opClose:
ws.writeClose()
_ = vnc.Close()
return
}
}
}()
wg.Wait()
}
func isClosedConnErr(err error) bool {
if err == nil {
return false
}
s := err.Error()
return contains(s, "use of closed network connection") ||
contains(s, "connection reset") ||
contains(s, "EOF")
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || indexOf(s, sub) >= 0)
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}