- 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)
197 lines
4.8 KiB
Go
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) }
|