main
Raw Download raw file
  1package web
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log/slog"
  7	"net/http"
  8	"sync/atomic"
  9
 10	"github.com/coder/websocket"
 11
 12	"goVNC/pkg/proxy"
 13	"goVNC/pkg/transport"
 14)
 15
 16// WebsockifyHandler handles WebSocket proxy connections.
 17type WebsockifyHandler struct {
 18	mux        *proxy.Multiplexer
 19	maxClients int
 20	clientSeq  uint64
 21}
 22
 23// NewWebsockifyHandler creates a new WebSocket proxy handler.
 24func NewWebsockifyHandler(mux *proxy.Multiplexer) *WebsockifyHandler {
 25	return &WebsockifyHandler{
 26		mux:        mux,
 27		maxClients: 10, // default max clients
 28	}
 29}
 30
 31// SetMaxClients sets the maximum number of concurrent clients.
 32func (h *WebsockifyHandler) SetMaxClients(max int) {
 33	h.maxClients = max
 34}
 35
 36// ServeHTTP handles WebSocket upgrade and proxying.
 37func (h *WebsockifyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 38	// Enforce max clients limit
 39	if h.maxClients > 0 && h.mux.ClientCount() >= h.maxClients {
 40		http.Error(w, "too many clients", http.StatusServiceUnavailable)
 41		return
 42	}
 43
 44	// Accept WebSocket connection
 45	conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
 46		Subprotocols:   []string{"binary"},
 47		OriginPatterns: []string{"*"}, // Allow all origins for now
 48	})
 49	if err != nil {
 50		slog.Error("accepting websocket", slog.String("error", err.Error()))
 51		return
 52	}
 53
 54	// Generate client ID
 55	seq := atomic.AddUint64(&h.clientSeq, 1)
 56	clientID := fmt.Sprintf("client-%d", seq)
 57
 58	// Wrap in transport
 59	clientTransport := &wsTransport{conn: conn}
 60
 61	// Register with multiplexer
 62	client := h.mux.AddClient(clientID, clientTransport)
 63	defer h.mux.RemoveClient(clientID)
 64
 65	// Set up context
 66	ctx := r.Context()
 67
 68	// Get current session info to send initial state
 69	session := h.mux.VNCClient().Session()
 70	if session != nil {
 71		// Send ServerInit-like message so client knows dimensions
 72		// noVNC expects to receive the full handshake, but since we've
 73		// already done that, we need to replay it
 74		slog.Info("client connected to session",
 75			slog.String("client", clientID),
 76			slog.String("session", session.Name))
 77	}
 78
 79	// Run reader and writer concurrently
 80	errCh := make(chan error, 2)
 81
 82	go func() {
 83		errCh <- h.mux.RunClientWriter(ctx, client)
 84	}()
 85
 86	go func() {
 87		errCh <- h.mux.RunClientReader(ctx, client)
 88	}()
 89
 90	// Wait for either to complete
 91	err = <-errCh
 92	if err != nil && err != context.Canceled {
 93		slog.Debug("client connection ended",
 94			slog.String("client", clientID),
 95			slog.String("error", err.Error()))
 96	}
 97
 98	// Close the connection
 99	conn.Close(websocket.StatusNormalClosure, "")
100}
101
102// wsTransport wraps a WebSocket connection as a Transport.
103type wsTransport struct {
104	conn *websocket.Conn
105}
106
107func (t *wsTransport) Read(ctx context.Context) ([]byte, error) {
108	_, data, err := t.conn.Read(ctx)
109	return data, err
110}
111
112func (t *wsTransport) Write(ctx context.Context, data []byte) error {
113	return t.conn.Write(ctx, websocket.MessageBinary, data)
114}
115
116func (t *wsTransport) Close() error {
117	return t.conn.CloseNow()
118}
119
120func (t *wsTransport) SetReadLimit(limit int64) {
121	t.conn.SetReadLimit(limit)
122}
123
124var _ transport.Transport = (*wsTransport)(nil)