- 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)
125 lines
2.4 KiB
Go
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
|
|
}
|