Files
newt/browsergateway/vnc.go
2026-06-09 16:01:45 -07:00

126 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package browsergateway
import (
"context"
"fmt"
"io"
"net"
"net/http"
"strconv"
"time"
"github.com/coder/websocket"
"github.com/fosrl/newt/logger"
)
const (
vncDialTimeout = 10 * time.Second
vncKeepAlive = 30 * time.Second
vncForwardBufSize = 32 * 1024
)
// handleVNC proxies a noVNC WebSocket connection to a raw TCP VNC backend.
// It follows the same auth-token-in-query-param pattern as handleSSH.
//
// Query parameters:
//
// authToken shared secret matching the -auth-token flag
// host VNC backend hostname or IP
// port VNC backend port (default: 5900)
func (g *Gateway) handleVNC(w http.ResponseWriter, r *http.Request) {
host := r.URL.Query().Get("host")
port := r.URL.Query().Get("port")
if host == "" {
http.Error(w, "missing host", http.StatusBadRequest)
return
}
if port == "" {
port = "5900"
}
vncPort, _ := strconv.Atoi(port)
authToken := r.URL.Query().Get("authToken")
if !g.isAllowed("vnc", host, vncPort, authToken) {
http.Error(w, "destination not allowed or auth token mismatch", http.StatusForbidden)
return
}
target := net.JoinHostPort(host, port)
// Optional HTTP probe used by the web UI to surface backend reachability
// failures before attempting a WebSocket session.
if r.URL.Query().Get("checkOnly") == "1" {
if err := dialVNCBackend(r.Context(), target); err != nil {
logger.Debug("vnc: preflight failed (%s): %v", target, err)
http.Error(w, fmt.Sprintf("failed to connect to VNC backend: %v", err), http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusNoContent)
return
}
// Accept the WebSocket. noVNC negotiates the "binary" subprotocol;
// fall back gracefully when the client sends "base64" as well.
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
Subprotocols: []string{"binary", "base64"},
})
if err != nil {
logger.Debug("vnc: websocket upgrade failed: %v", err)
return
}
ws.SetReadLimit(-1)
defer ws.CloseNow() //nolint:errcheck
ctx := r.Context()
if err := serveVNC(ctx, ws, target); err != nil {
logger.Debug("vnc: session error (%s): %v", target, err)
}
}
func dialVNCBackend(ctx context.Context, target string) error {
dialer := &net.Dialer{
Timeout: vncDialTimeout,
KeepAlive: vncKeepAlive,
}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil {
return err
}
defer conn.Close() //nolint:errcheck
return nil
}
func serveVNC(ctx context.Context, ws *websocket.Conn, target string) error {
// Dial the VNC backend TCP server.
dialer := &net.Dialer{
Timeout: vncDialTimeout,
KeepAlive: vncKeepAlive,
}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil {
return err
}
defer conn.Close() //nolint:errcheck
// Expose the WebSocket as a plain net.Conn byte stream (binary frames).
stream := websocket.NetConn(ctx, ws, websocket.MessageBinary)
defer stream.Close() //nolint:errcheck
// Proxy bidirectionally: VNC backend <-> browser.
errc := make(chan error, 2)
go func() {
buf := make([]byte, vncForwardBufSize)
_, err := io.CopyBuffer(conn, stream, buf)
errc <- err
}()
go func() {
buf := make([]byte, vncForwardBufSize)
_, err := io.CopyBuffer(stream, conn, buf)
errc <- err
}()
// Return when either direction closes.
err = <-errc
return err
}