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,196 @@
|
||||
// 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) }
|
||||
Reference in New Issue
Block a user