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:
@@ -0,0 +1,124 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user