Files
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

197 lines
4.8 KiB
Go

// Package relay bridges a noVNC WebSocket client to a raw TCP VNC server,
// transparently carrying the RFB byte-stream in both directions.
//
// This file implements a minimal RFC 6455 WebSocket server using only the
// Go standard library, tailored to the needs of noVNC: binary message
// frames in both directions, with ping/pong and close handling.
package relay
import (
"bufio"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
const wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
// wsConn wraps a hijacked HTTP connection as a WebSocket (server side).
type wsConn struct {
nc net.Conn
br *bufio.Reader
bw *bufio.Writer
}
// handshake performs the WebSocket upgrade and returns a wsConn.
func handshake(w http.ResponseWriter, r *http.Request) (*wsConn, error) {
if !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") ||
!headerContains(r.Header, "Connection", "upgrade") {
return nil, errors.New("not a websocket upgrade request")
}
key := r.Header.Get("Sec-WebSocket-Key")
if key == "" {
return nil, errors.New("missing Sec-WebSocket-Key")
}
hj, ok := w.(http.Hijacker)
if !ok {
return nil, errors.New("response writer does not support hijacking")
}
nc, brw, err := hj.Hijack()
if err != nil {
return nil, err
}
accept := wsAcceptKey(key)
_, _ = fmt.Fprintf(brw, "HTTP/1.1 101 Switching Protocols\r\n")
_, _ = fmt.Fprintf(brw, "Upgrade: websocket\r\n")
_, _ = fmt.Fprintf(brw, "Connection: Upgrade\r\n")
_, _ = fmt.Fprintf(brw, "Sec-WebSocket-Accept: %s\r\n", accept)
_, _ = fmt.Fprintf(brw, "\r\n")
if err := brw.Flush(); err != nil {
_ = nc.Close()
return nil, err
}
return &wsConn{nc: nc, br: brw.Reader, bw: brw.Writer}, nil
}
func wsAcceptKey(key string) string {
h := sha1.New()
h.Write([]byte(key + wsGUID))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func headerContains(h http.Header, name, value string) bool {
for _, v := range h[http.CanonicalHeaderKey(name)] {
for _, tok := range strings.Split(v, ",") {
if strings.EqualFold(strings.TrimSpace(tok), value) {
return true
}
}
}
return false
}
// wsFrame opcodes
const (
opContinuation = 0x0
opText = 0x1
opBinary = 0x2
opClose = 0x8
opPing = 0x9
opPong = 0xA
)
// readFrame reads one WebSocket frame from the client.
// It returns the opcode and (de-masked) payload. Control frames keep their
// own opcode; data frames (text/binary/continuation) are returned as-is and
// the caller is responsible for stream semantics.
func (c *wsConn) readFrame() (opcode byte, payload []byte, err error) {
var hdr [2]byte
if _, err = io.ReadFull(c.br, hdr[:]); err != nil {
return 0, nil, err
}
opcode = hdr[0] & 0x0F
masked := hdr[1]&0x80 != 0
length := int64(hdr[1] & 0x7F)
switch length {
case 126:
var ext [2]byte
if _, err = io.ReadFull(c.br, ext[:]); err != nil {
return 0, nil, err
}
length = int64(binary.BigEndian.Uint16(ext[:]))
case 127:
var ext [8]byte
if _, err = io.ReadFull(c.br, ext[:]); err != nil {
return 0, nil, err
}
length = int64(binary.BigEndian.Uint64(ext[:]))
}
var mask [4]byte
if masked {
if _, err = io.ReadFull(c.br, mask[:]); err != nil {
return 0, nil, err
}
}
if length < 0 {
return 0, nil, errors.New("invalid frame length")
}
payload = make([]byte, length)
if length > 0 {
if _, err = io.ReadFull(c.br, payload); err != nil {
return 0, nil, err
}
}
if masked {
for i := range payload {
payload[i] ^= mask[i%4]
}
}
return opcode, payload, nil
}
// writeBinary sends a single binary frame to the client (server frames are unmasked).
func (c *wsConn) writeBinary(p []byte) error {
var hdr []byte
n := len(p)
switch {
case n < 126:
hdr = []byte{0x82, byte(n)}
case n < 65536:
hdr = make([]byte, 4)
hdr[0] = 0x82
hdr[1] = 126
binary.BigEndian.PutUint16(hdr[2:], uint16(n))
default:
hdr = make([]byte, 10)
hdr[0] = 0x82
hdr[1] = 127
binary.BigEndian.PutUint64(hdr[2:], uint64(n))
}
if _, err := c.bw.Write(hdr); err != nil {
return err
}
if _, err := c.bw.Write(p); err != nil {
return err
}
return c.bw.Flush()
}
// writePong sends a pong frame with the given payload.
func (c *wsConn) writePong(p []byte) error {
if len(p) > 125 {
return errors.New("control frame payload too large")
}
hdr := []byte{0x8A, byte(len(p))}
if _, err := c.bw.Write(hdr); err != nil {
return err
}
if _, err := c.bw.Write(p); err != nil {
return err
}
return c.bw.Flush()
}
// writeClose sends a close frame and flushes.
func (c *wsConn) writeClose() {
_, _ = c.bw.Write([]byte{0x88, 0x00})
_ = c.bw.Flush()
}
// close closes the underlying connection.
func (c *wsConn) close() error { return c.nc.Close() }
// setWriteDeadline forwards to the underlying connection.
func (c *wsConn) setWriteDeadline(t time.Time) error { return c.nc.SetWriteDeadline(t) }