Commit 6001ced

bryfry <bryon@fryer.io>
2026-01-24 09:40:05
tidy
1 parent 05a6350
cmd/govnc/main.go
@@ -1,3 +1,9 @@
+// Command govnc is a VNC client and proxy server.
+// It connects to a VNC server over WebSocket and provides:
+//   - Interactive terminal for typing to the VNC session
+//   - HTTP API for programmatic control
+//   - WebSocket proxy for noVNC browser clients
+//   - Clipboard synchronization via files
 package main
 
 import (
@@ -21,8 +27,9 @@ import (
 )
 
 func main() {
-	if err := run(); err != nil {
-		slog.Error("fatal", slog.String("error", err.Error()))
+	err := run()
+	if err != nil {
+		slog.Error("startup failed", slog.String("error", err.Error()))
 		os.Exit(1)
 	}
 }
@@ -123,7 +130,8 @@ func setupLogging(verbose int, silent bool) {
 
 func handleTypeAndExit(ctx context.Context, client *rfb.Client, text string) error {
 	slog.Info("typing", slog.Int("len", len(text)))
-	if err := client.Type(ctx, text); err != nil {
+	err := client.Type(ctx, text)
+	if err != nil {
 		return fmt.Errorf("typing: %w", err)
 	}
 	slog.Info("done")
@@ -141,7 +149,8 @@ func handleClipSendAndExit(ctx context.Context, client *rfb.Client, text string)
 	}
 
 	slog.Info("sending clipboard", slog.Int("len", len(text)))
-	if err := client.SetClipboard(ctx, text); err != nil {
+	err := client.SetClipboard(ctx, text)
+	if err != nil {
 		return fmt.Errorf("sending clipboard: %w", err)
 	}
 	slog.Info("done")
@@ -254,7 +263,8 @@ func readStdin(ctx context.Context, client *rfb.Client, serverName string) error
 
 		line = line + "\n"
 		slog.Debug("typing", slog.String("line", strings.TrimSpace(line)))
-		if err := client.Type(ctx, line); err != nil {
+		err = client.Type(ctx, line)
+		if err != nil {
 			return fmt.Errorf("typing line: %w", err)
 		}
 	}
@@ -267,7 +277,8 @@ func readInputFile(ctx context.Context, client *rfb.Client, path string) error {
 	}
 
 	slog.Info("typing from file", slog.String("file", path), slog.Int("len", len(data)))
-	if err := client.Type(ctx, string(data)); err != nil {
+	err = client.Type(ctx, string(data))
+	if err != nil {
 		return fmt.Errorf("typing: %w", err)
 	}
 
pkg/input/clipboard.go
@@ -61,17 +61,20 @@ func NewClipboardManager(sender ClipboardSender, cfg *ClipboardConfig) (*Clipboa
 		cm.sentDir = filepath.Join(cfg.WatchDir, "sent")
 
 		// Create directories
-		if err := os.MkdirAll(cm.watchDir, 0755); err != nil {
+		err := os.MkdirAll(cm.watchDir, 0755)
+		if err != nil {
 			return nil, fmt.Errorf("creating watch dir: %w", err)
 		}
-		if err := os.MkdirAll(cm.sentDir, 0755); err != nil {
+		err = os.MkdirAll(cm.sentDir, 0755)
+		if err != nil {
 			return nil, fmt.Errorf("creating sent dir: %w", err)
 		}
 	}
 
 	if cfg.SaveDir != "" {
 		cm.saveDir = cfg.SaveDir
-		if err := os.MkdirAll(cm.saveDir, 0755); err != nil {
+		err := os.MkdirAll(cm.saveDir, 0755)
+		if err != nil {
 			return nil, fmt.Errorf("creating save dir: %w", err)
 		}
 	}
@@ -94,7 +97,8 @@ func (cm *ClipboardManager) Start(ctx context.Context) error {
 	cm.watcher = watcher
 	defer watcher.Close()
 
-	if err := watcher.Add(cm.watchDir); err != nil {
+	err = watcher.Add(cm.watchDir)
+	if err != nil {
 		return fmt.Errorf("watching directory: %w", err)
 	}
 
@@ -153,14 +157,16 @@ func (cm *ClipboardManager) processFile(ctx context.Context, path string) {
 		slog.String("file", filepath.Base(path)),
 		slog.Int("len", len(data)))
 
-	if err := cm.sender.SetClipboard(ctx, string(data)); err != nil {
+	err = cm.sender.SetClipboard(ctx, string(data))
+	if err != nil {
 		slog.Warn("sending clipboard", slog.String("error", err.Error()))
 		return
 	}
 
 	// Move to sent/ after successful send
 	sentPath := filepath.Join(cm.sentDir, filepath.Base(path))
-	if err := os.Rename(path, sentPath); err != nil {
+	err = os.Rename(path, sentPath)
+	if err != nil {
 		slog.Warn("moving sent file", slog.String("error", err.Error()))
 	}
 }
@@ -181,7 +187,8 @@ func (cm *ClipboardManager) OnReceive(text string) {
 	// Save to file if configured
 	if cm.saveDir != "" {
 		filename := filepath.Join(cm.saveDir, fmt.Sprintf("%x.clip", time.Now().Unix()))
-		if err := os.WriteFile(filename, []byte(text), 0644); err != nil {
+		err := os.WriteFile(filename, []byte(text), 0644)
+		if err != nil {
 			slog.Warn("saving clipboard", slog.String("error", err.Error()))
 		} else {
 			slog.Debug("saved clipboard", slog.String("file", filename))
@@ -190,7 +197,8 @@ func (cm *ClipboardManager) OnReceive(text string) {
 
 	// Call handler if configured
 	if cm.handler != nil {
-		if err := cm.handler(text); err != nil {
+		err := cm.handler(text)
+		if err != nil {
 			slog.Warn("clipboard handler error", slog.String("error", err.Error()))
 		}
 	}
pkg/rfb/client.go
@@ -2,6 +2,7 @@ package rfb
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"log/slog"
 	"sync"
@@ -10,6 +11,9 @@ import (
 	"goVNC/pkg/transport"
 )
 
+// ErrClientClosed is returned when operations are attempted on a closed client.
+var ErrClientClosed = errors.New("client is closed")
+
 // MessageHandler is called when a server message is received.
 type MessageHandler func(msg *ServerMessage)
 
@@ -77,14 +81,14 @@ func (c *Client) Connect(ctx context.Context) (*Session, error) {
 	c.mu.Lock()
 	if c.closed {
 		c.mu.Unlock()
-		return nil, fmt.Errorf("client is closed")
+		return nil, ErrClientClosed
 	}
 
 	// Perform handshake
 	session, err := Handshake(ctx, c.transport, c.config.Handshake)
 	if err != nil {
 		c.mu.Unlock()
-		return nil, fmt.Errorf("handshake failed: %w", err)
+		return nil, fmt.Errorf("handshaking: %w", err)
 	}
 	c.session = session
 
@@ -94,7 +98,8 @@ func (c *Client) Connect(ctx context.Context) (*Session, error) {
 	// Send encodings
 	if len(c.config.Encodings) > 0 {
 		msg := EncodeSetEncodings(c.config.Encodings)
-		if err := c.transport.Write(ctx, msg); err != nil {
+		err = c.transport.Write(ctx, msg)
+		if err != nil {
 			c.mu.Unlock()
 			return nil, fmt.Errorf("sending encodings: %w", err)
 		}
@@ -104,7 +109,8 @@ func (c *Client) Connect(ctx context.Context) (*Session, error) {
 	c.mu.Unlock()
 
 	// Request initial framebuffer
-	if err := c.RequestFramebuffer(ctx, false); err != nil {
+	err = c.RequestFramebuffer(ctx, false)
+	if err != nil {
 		return nil, fmt.Errorf("requesting initial framebuffer: %w", err)
 	}
 
@@ -137,6 +143,9 @@ func (c *Client) Listen(ctx context.Context) error {
 	for {
 		data, err := c.transport.Read(ctx)
 		if err != nil {
+			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+				return err // Return unwrapped for clean shutdown detection
+			}
 			return fmt.Errorf("reading message: %w", err)
 		}
 
@@ -165,7 +174,8 @@ func (c *Client) Listen(ctx context.Context) error {
 		switch msg.Type {
 		case MsgTypeFramebufferUpdate:
 			// Request next incremental update
-			if err := c.RequestFramebuffer(ctx, true); err != nil {
+			err = c.RequestFramebuffer(ctx, true)
+			if err != nil {
 				return fmt.Errorf("requesting framebuffer: %w", err)
 			}
 
@@ -216,10 +226,15 @@ func (c *Client) KeyEvent(ctx context.Context, key uint32, down bool) error {
 
 // Tap sends a key press followed by release.
 func (c *Client) Tap(ctx context.Context, key uint32) error {
-	if err := c.KeyEvent(ctx, key, true); err != nil {
-		return err
+	err := c.KeyEvent(ctx, key, true)
+	if err != nil {
+		return fmt.Errorf("pressing key (key=%#x): %w", key, err)
+	}
+	err = c.KeyEvent(ctx, key, false)
+	if err != nil {
+		return fmt.Errorf("releasing key (key=%#x): %w", key, err)
 	}
-	return c.KeyEvent(ctx, key, false)
+	return nil
 }
 
 // Type sends a string as key events.
@@ -232,7 +247,8 @@ func (c *Client) Type(ctx context.Context, text string) error {
 		}
 
 		key := RuneToKeysym(r)
-		if err := c.Tap(ctx, key); err != nil {
+		err := c.Tap(ctx, key)
+		if err != nil {
 			return err
 		}
 	}
pkg/web/api.go
@@ -81,12 +81,14 @@ func (a *API) getClipboard(w http.ResponseWriter, _ *http.Request) {
 
 func (a *API) setClipboard(w http.ResponseWriter, r *http.Request) {
 	var req ClipboardRequest
-	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
 		http.Error(w, "Invalid JSON", http.StatusBadRequest)
 		return
 	}
 
-	if err := a.vnc.SetClipboard(r.Context(), req.Text); err != nil {
+	err = a.vnc.SetClipboard(r.Context(), req.Text)
+	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -102,7 +104,8 @@ func (a *API) HandleKeys(w http.ResponseWriter, r *http.Request) {
 	}
 
 	var req KeysRequest
-	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
 		http.Error(w, "Invalid JSON", http.StatusBadRequest)
 		return
 	}
@@ -122,7 +125,8 @@ func (a *API) HandleKeys(w http.ResponseWriter, r *http.Request) {
 					return
 				}
 			}
-			if err := a.vnc.KeyEvent(ctx, key, ke.Down); err != nil {
+			err = a.vnc.KeyEvent(ctx, key, ke.Down)
+			if err != nil {
 				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return
 			}
@@ -134,9 +138,10 @@ func (a *API) HandleKeys(w http.ResponseWriter, r *http.Request) {
 	// Otherwise, type the text
 	if req.Text != "" {
 		// Add delay between keystrokes if specified
-		for _, r := range req.Text {
-			key := rfb.RuneToKeysym(r)
-			if err := a.vnc.Tap(ctx, key); err != nil {
+		for _, ch := range req.Text {
+			key := rfb.RuneToKeysym(ch)
+			err = a.vnc.Tap(ctx, key)
+			if err != nil {
 				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return
 			}
pkg/web/server.go
@@ -86,10 +86,12 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
 
 	// WebSocket proxy endpoint
 	wsHandler := NewWebsockifyHandler(s.mux)
+	wsHandler.SetMaxClients(s.config.MaxClients)
 	mux.Handle("/websockify", wsHandler)
 
 	// noVNC static files
-	if err := s.setupNoVNC(mux); err != nil {
+	err := s.setupNoVNC(mux)
+	if err != nil {
 		return fmt.Errorf("setting up noVNC: %w", err)
 	}
 
pkg/web/websockify.go
@@ -15,19 +15,32 @@ import (
 
 // WebsockifyHandler handles WebSocket proxy connections.
 type WebsockifyHandler struct {
-	mux       *proxy.Multiplexer
-	clientSeq uint64
+	mux        *proxy.Multiplexer
+	maxClients int
+	clientSeq  uint64
 }
 
 // NewWebsockifyHandler creates a new WebSocket proxy handler.
 func NewWebsockifyHandler(mux *proxy.Multiplexer) *WebsockifyHandler {
 	return &WebsockifyHandler{
-		mux: mux,
+		mux:        mux,
+		maxClients: 10, // default max clients
 	}
 }
 
+// SetMaxClients sets the maximum number of concurrent clients.
+func (h *WebsockifyHandler) SetMaxClients(max int) {
+	h.maxClients = max
+}
+
 // ServeHTTP handles WebSocket upgrade and proxying.
 func (h *WebsockifyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// Enforce max clients limit
+	if h.maxClients > 0 && h.mux.ClientCount() >= h.maxClients {
+		http.Error(w, "too many clients", http.StatusServiceUnavailable)
+		return
+	}
+
 	// Accept WebSocket connection
 	conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
 		Subprotocols:   []string{"binary"},
main.go
@@ -1,3 +1,6 @@
+// Package main provides a standalone VNC-over-WebSocket client.
+// This is a minimal client for testing and development purposes.
+// For production use, see cmd/govnc.
 package main
 
 import (
@@ -22,6 +25,18 @@ import (
 
 const (
 	_rfbVersion = "RFB 003.008\n"
+
+	// RFB client-to-server message types
+	_msgTypeSetEncodings             = 2
+	_msgTypeFramebufferUpdateRequest = 3
+	_msgTypeKeyEvent                 = 4
+	_msgTypeClientCutText            = 6
+
+	// RFB server-to-client message types
+	_msgTypeFramebufferUpdate   = 0
+	_msgTypeSetColourMapEntries = 1
+	_msgTypeBell                = 2
+	_msgTypeServerCutText       = 3
 )
 
 var (
@@ -33,7 +48,7 @@ func main() {
 	flag.Parse()
 	err := run()
 	if err != nil {
-		slog.Error("run failed", slog.String("error", err.Error()))
+		slog.Error("startup failed", slog.String("error", err.Error()))
 		os.Exit(1)
 	}
 }
@@ -173,9 +188,9 @@ func run() error {
 
 // drainFrames reads and logs incoming VNC messages
 func drainFrames(ctx context.Context, c *websocket.Conn, width, height uint16) error {
-	// Buffer for reassembling fragmented messages
-	var pendingBuf []byte
-	var pendingExpected int
+	// Buffer for reassembling fragmented clipboard messages
+	var clipboardReassemblyBuf []byte
+	var clipboardExpectedLen int
 
 	for {
 		_, b, err := c.Read(ctx)
@@ -189,38 +204,39 @@ func drainFrames(ctx context.Context, c *websocket.Conn, width, height uint16) e
 		}
 
 		// If we're waiting for more data on a fragmented message, append it
-		if pendingBuf != nil {
-			pendingBuf = append(pendingBuf, b...)
-			slog.Debug("reassembly", slog.Int("have", len(pendingBuf)), slog.Int("need", pendingExpected))
+		if clipboardReassemblyBuf != nil {
+			clipboardReassemblyBuf = append(clipboardReassemblyBuf, b...)
+			slog.Debug("reassembly", slog.Int("have", len(clipboardReassemblyBuf)), slog.Int("need", clipboardExpectedLen))
 
-			if len(pendingBuf) >= pendingExpected {
+			if len(clipboardReassemblyBuf) >= clipboardExpectedLen {
 				// We have enough data, process the complete message
-				clipboardText := string(pendingBuf[8:pendingExpected])
+				clipboardText := string(clipboardReassemblyBuf[8:clipboardExpectedLen])
 				slog.Info("clipboard recv", slog.Int("len", len(clipboardText)))
 				slog.Debug("clipboard", slog.String("text", clipboardText))
-				if err := saveClipboard(clipboardText); err != nil {
-					slog.Warn("failed to save clipboard", slog.String("error", err.Error()))
+				saveErr := saveClipboard(clipboardText)
+				if saveErr != nil {
+					slog.Warn("saving clipboard", slog.String("error", saveErr.Error()))
 				}
-				pendingBuf = nil
-				pendingExpected = 0
+				clipboardReassemblyBuf = nil
+				clipboardExpectedLen = 0
 			}
 			continue
 		}
 
 		msgType := b[0]
 		switch msgType {
-		case 0: // FramebufferUpdate
+		case _msgTypeFramebufferUpdate:
 			if len(b) >= 4 {
 				numRects := binary.BigEndian.Uint16(b[2:4])
 				slog.Debug("recv", slog.String("type", "FramebufferUpdate"),
 					slog.Int("numRects", int(numRects)),
 					slog.Int("len", len(b)))
 			}
-		case 1: // SetColourMapEntries
+		case _msgTypeSetColourMapEntries:
 			slog.Debug("recv", slog.String("type", "SetColourMapEntries"), slog.Int("len", len(b)))
-		case 2: // Bell
+		case _msgTypeBell:
 			slog.Info("recv", slog.String("type", "Bell"))
-		case 3: // ServerCutText (clipboard)
+		case _msgTypeServerCutText:
 			if len(b) >= 8 {
 				textLen := binary.BigEndian.Uint32(b[4:8])
 
@@ -237,8 +253,9 @@ func drainFrames(ctx context.Context, c *websocket.Conn, width, height uint16) e
 					clipboardText := string(b[8:expectedLen])
 					slog.Info("clipboard recv", slog.Int("len", int(textLen)))
 					slog.Debug("clipboard", slog.String("text", clipboardText))
-					if err := saveClipboard(clipboardText); err != nil {
-						slog.Warn("failed to save clipboard", slog.String("error", err.Error()))
+					saveErr := saveClipboard(clipboardText)
+					if saveErr != nil {
+						slog.Warn("saving clipboard", slog.String("error", saveErr.Error()))
 					}
 				} else {
 					// Start buffering for reassembly
@@ -246,9 +263,9 @@ func drainFrames(ctx context.Context, c *websocket.Conn, width, height uint16) e
 						slog.Int("textLen", int(textLen)),
 						slog.Int("have", len(b)),
 						slog.String("note", "fragmented, buffering"))
-					pendingBuf = make([]byte, len(b))
-					copy(pendingBuf, b)
-					pendingExpected = expectedLen
+					clipboardReassemblyBuf = make([]byte, len(b))
+					copy(clipboardReassemblyBuf, b)
+					clipboardExpectedLen = expectedLen
 				}
 			}
 		default:
@@ -265,10 +282,12 @@ func drainFrames(ctx context.Context, c *websocket.Conn, width, height uint16) e
 
 // watchClipboardIn watches clipboard_in/ directory and sends files as clipboard data
 func watchClipboardIn(ctx context.Context, c *websocket.Conn) error {
-	if err := os.MkdirAll("clipboard_in", 0755); err != nil {
+	err := os.MkdirAll("clipboard_in", 0755)
+	if err != nil {
 		return fmt.Errorf("creating clipboard_in dir: %w", err)
 	}
-	if err := os.MkdirAll("clipboard_in/sent", 0755); err != nil {
+	err = os.MkdirAll("clipboard_in/sent", 0755)
+	if err != nil {
 		return fmt.Errorf("creating clipboard_in/sent dir: %w", err)
 	}
 
@@ -278,7 +297,8 @@ func watchClipboardIn(ctx context.Context, c *websocket.Conn) error {
 	}
 	defer watcher.Close()
 
-	if err := watcher.Add("clipboard_in"); err != nil {
+	err = watcher.Add("clipboard_in")
+	if err != nil {
 		return fmt.Errorf("watching clipboard_in: %w", err)
 	}
 
@@ -327,14 +347,18 @@ func processClipboardFile(ctx context.Context, c *websocket.Conn, path string) {
 	}
 
 	slog.Info("clipboard send", slog.String("file", filepath.Base(path)), slog.Int("len", len(data)))
-	if err := SendClipboard(ctx, c, string(data)); err != nil {
+	err = SendClipboard(ctx, c, string(data))
+	if err != nil {
 		slog.Warn("sending clipboard", slog.String("error", err.Error()))
 		return
 	}
 
 	// Move to sent/ after sending
 	sentPath := filepath.Join("clipboard_in", "sent", filepath.Base(path))
-	os.Rename(path, sentPath)
+	err = os.Rename(path, sentPath)
+	if err != nil {
+		slog.Warn("moving sent file", slog.String("path", path), slog.String("error", err.Error()))
+	}
 }
 
 // readStdin reads lines from stdin with readline support and sends as key events
@@ -371,25 +395,27 @@ func readStdin(ctx context.Context, c *websocket.Conn, serverName string) error
 
 		line = line + "\n"
 		slog.Info("stdin", slog.String("line", strings.TrimSpace(line)))
-		if err := Type(ctx, c, line); err != nil {
+		err = Type(ctx, c, line)
+		if err != nil {
 			return fmt.Errorf("typing line: %w", err)
 		}
 	}
 }
 
 type vncTyper struct {
+	ctx  context.Context
 	conn *websocket.Conn
 }
 
 func (vt vncTyper) Write(p []byte) (n int, err error) {
-	return len(p), Type(context.Background(), vt.conn, string(p))
+	return len(p), Type(vt.ctx, vt.conn, string(p))
 }
 
 func SendKey(sessionCtx context.Context, c *websocket.Conn, down bool, key uint32) error {
 	msg := make([]byte, 8)
-	msg[0] = 0x04 // KeyEvent
+	msg[0] = _msgTypeKeyEvent
 	if down {
-		msg[1] = 0x01 // downkey
+		msg[1] = 1
 	}
 
 	ctx, cancel := context.WithTimeout(sessionCtx, 1*time.Second)
@@ -403,9 +429,13 @@ func SendKey(sessionCtx context.Context, c *websocket.Conn, down bool, key uint3
 func Tap(ctx context.Context, c *websocket.Conn, key uint32) error {
 	err := SendKey(ctx, c, true, key)
 	if err != nil {
-		return err
+		return fmt.Errorf("pressing key (key=%#x): %w", key, err)
 	}
-	return SendKey(ctx, c, false, key)
+	err = SendKey(ctx, c, false, key)
+	if err != nil {
+		return fmt.Errorf("releasing key (key=%#x): %w", key, err)
+	}
+	return nil
 }
 
 func Type(ctx context.Context, c *websocket.Conn, s string) error {
@@ -439,7 +469,8 @@ func Type(ctx context.Context, c *websocket.Conn, s string) error {
 
 // saveClipboard writes clipboard text to clipboard/<hex_unix_time>.clip
 func saveClipboard(text string) error {
-	if err := os.MkdirAll("clipboard", 0755); err != nil {
+	err := os.MkdirAll("clipboard", 0755)
+	if err != nil {
 		return err
 	}
 	filename := fmt.Sprintf("clipboard/%x.clip", time.Now().Unix())
@@ -451,7 +482,7 @@ func saveClipboard(text string) error {
 func SendClipboard(ctx context.Context, c *websocket.Conn, text string) error {
 	// ClientCutText: type(1) + padding(3) + length(4) + text
 	msg := make([]byte, 8+len(text))
-	msg[0] = 6 // ClientCutText
+	msg[0] = _msgTypeClientCutText
 	// msg[1:4] padding zeros
 	binary.BigEndian.PutUint32(msg[4:8], uint32(len(text)))
 	copy(msg[8:], text)
@@ -460,11 +491,11 @@ func SendClipboard(ctx context.Context, c *websocket.Conn, text string) error {
 	return c.Write(ctx, websocket.MessageBinary, msg)
 }
 
-// SetEncodings: say you support RAW only (encoding 0)
+// SendSetEncodings sends encoding preferences to the server.
 func SendSetEncodings(ctx context.Context, c *websocket.Conn, encs []int32) error {
-	// type(1)=2, pad(1), count(2), encodings(4*N)
+	// type(1), pad(1), count(2), encodings(4*N)
 	msg := make([]byte, 4+4*len(encs))
-	msg[0] = 2
+	msg[0] = _msgTypeSetEncodings
 	binary.BigEndian.PutUint16(msg[2:4], uint16(len(encs)))
 	off := 4
 	for _, e := range encs {
@@ -474,10 +505,10 @@ func SendSetEncodings(ctx context.Context, c *websocket.Conn, encs []int32) erro
 	return c.Write(ctx, websocket.MessageBinary, msg)
 }
 
-// FramebufferUpdateRequest: request full screen once
+// SendFBURequest sends a framebuffer update request to the server.
 func SendFBURequest(ctx context.Context, c *websocket.Conn, incremental bool, w, h uint16) error {
 	msg := make([]byte, 10)
-	msg[0] = 3 // FramebufferUpdateRequest
+	msg[0] = _msgTypeFramebufferUpdateRequest
 	if incremental {
 		msg[1] = 1
 	}