main
Raw Download raw file
  1package rfb
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"log/slog"
  8	"sync"
  9	"time"
 10
 11	"goVNC/pkg/transport"
 12)
 13
 14// ErrClientClosed is returned when operations are attempted on a closed client.
 15var ErrClientClosed = errors.New("client is closed")
 16
 17// MessageHandler is called when a server message is received.
 18type MessageHandler func(msg *ServerMessage)
 19
 20// ClipboardHandler is called when clipboard data is received from the server.
 21type ClipboardHandler func(text string)
 22
 23// Client is an RFB protocol client.
 24type Client struct {
 25	transport transport.Transport
 26	session   *Session
 27	config    *ClientConfig
 28
 29	// Message handlers
 30	onMessage   MessageHandler
 31	onClipboard ClipboardHandler
 32
 33	// Synchronization
 34	mu       sync.RWMutex
 35	closed   bool
 36
 37	// Last received clipboard (for API access)
 38	lastClipboard     string
 39	lastClipboardTime time.Time
 40	clipMu            sync.RWMutex
 41}
 42
 43// ClientConfig configures the RFB client.
 44type ClientConfig struct {
 45	// Handshake configuration
 46	Handshake *HandshakeConfig
 47
 48	// Encodings to request from the server.
 49	Encodings []int32
 50
 51	// ReadLimit is the maximum message size (default 64MB).
 52	ReadLimit int64
 53
 54	// KeyTimeout is the timeout for key events (default 1s).
 55	KeyTimeout time.Duration
 56}
 57
 58// DefaultClientConfig returns default client configuration.
 59func DefaultClientConfig() *ClientConfig {
 60	return &ClientConfig{
 61		Handshake:  DefaultHandshakeConfig(),
 62		Encodings:  DefaultEncodings(),
 63		ReadLimit:  64 * 1024 * 1024, // 64MB
 64		KeyTimeout: 1 * time.Second,
 65	}
 66}
 67
 68// NewClient creates a new RFB client with the given transport.
 69func NewClient(t transport.Transport, cfg *ClientConfig) *Client {
 70	if cfg == nil {
 71		cfg = DefaultClientConfig()
 72	}
 73	return &Client{
 74		transport: t,
 75		config:    cfg,
 76	}
 77}
 78
 79// Connect performs the RFB handshake and initializes the session.
 80func (c *Client) Connect(ctx context.Context) (*Session, error) {
 81	c.mu.Lock()
 82	if c.closed {
 83		c.mu.Unlock()
 84		return nil, ErrClientClosed
 85	}
 86
 87	// Perform handshake
 88	session, err := Handshake(ctx, c.transport, c.config.Handshake)
 89	if err != nil {
 90		c.mu.Unlock()
 91		return nil, fmt.Errorf("handshaking: %w", err)
 92	}
 93	c.session = session
 94
 95	// Set read limit
 96	c.transport.SetReadLimit(c.config.ReadLimit)
 97
 98	// Send encodings
 99	if len(c.config.Encodings) > 0 {
100		msg := EncodeSetEncodings(c.config.Encodings)
101		err = c.transport.Write(ctx, msg)
102		if err != nil {
103			c.mu.Unlock()
104			return nil, fmt.Errorf("sending encodings: %w", err)
105		}
106	}
107
108	// Release lock before calling RequestFramebuffer (which also takes the lock)
109	c.mu.Unlock()
110
111	// Request initial framebuffer
112	err = c.RequestFramebuffer(ctx, false)
113	if err != nil {
114		return nil, fmt.Errorf("requesting initial framebuffer: %w", err)
115	}
116
117	return session, nil
118}
119
120// Session returns the current session info.
121func (c *Client) Session() *Session {
122	c.mu.RLock()
123	defer c.mu.RUnlock()
124	return c.session
125}
126
127// OnMessage sets the handler for all server messages.
128func (c *Client) OnMessage(handler MessageHandler) {
129	c.mu.Lock()
130	defer c.mu.Unlock()
131	c.onMessage = handler
132}
133
134// OnClipboard sets the handler for clipboard messages.
135func (c *Client) OnClipboard(handler ClipboardHandler) {
136	c.mu.Lock()
137	defer c.mu.Unlock()
138	c.onClipboard = handler
139}
140
141// Listen reads and processes server messages until the context is cancelled.
142func (c *Client) Listen(ctx context.Context) error {
143	for {
144		data, err := c.transport.Read(ctx)
145		if err != nil {
146			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
147				return err // Return unwrapped for clean shutdown detection
148			}
149			return fmt.Errorf("reading message: %w", err)
150		}
151
152		if len(data) == 0 {
153			slog.Warn("received empty message")
154			continue
155		}
156
157		msg, err := ParseServerMessage(data)
158		if err != nil {
159			slog.Warn("parsing message", slog.String("error", err.Error()))
160			continue
161		}
162
163		// Call message handler
164		c.mu.RLock()
165		onMessage := c.onMessage
166		onClipboard := c.onClipboard
167		c.mu.RUnlock()
168
169		if onMessage != nil {
170			onMessage(msg)
171		}
172
173		// Handle specific message types
174		switch msg.Type {
175		case MsgTypeFramebufferUpdate:
176			// Request next incremental update
177			err = c.RequestFramebuffer(ctx, true)
178			if err != nil {
179				return fmt.Errorf("requesting framebuffer: %w", err)
180			}
181
182		case MsgTypeServerCutText:
183			cut, err := ParseServerCutText(data)
184			if err != nil {
185				slog.Warn("parsing clipboard", slog.String("error", err.Error()))
186				continue
187			}
188			// Store for API access
189			c.clipMu.Lock()
190			c.lastClipboard = cut.Text
191			c.lastClipboardTime = time.Now()
192			c.clipMu.Unlock()
193
194			if onClipboard != nil {
195				onClipboard(cut.Text)
196			}
197
198		case MsgTypeBell:
199			slog.Info("bell received")
200		}
201	}
202}
203
204// RequestFramebuffer sends a framebuffer update request.
205func (c *Client) RequestFramebuffer(ctx context.Context, incremental bool) error {
206	c.mu.RLock()
207	session := c.session
208	c.mu.RUnlock()
209
210	if session == nil {
211		return fmt.Errorf("not connected")
212	}
213
214	msg := EncodeFramebufferUpdateRequest(incremental, 0, 0, session.Width, session.Height)
215	return c.transport.Write(ctx, msg)
216}
217
218// KeyEvent sends a key press or release event.
219func (c *Client) KeyEvent(ctx context.Context, key uint32, down bool) error {
220	ctx, cancel := context.WithTimeout(ctx, c.config.KeyTimeout)
221	defer cancel()
222
223	msg := EncodeKeyEvent(down, key)
224	return c.transport.Write(ctx, msg)
225}
226
227// Tap sends a key press followed by release.
228func (c *Client) Tap(ctx context.Context, key uint32) error {
229	err := c.KeyEvent(ctx, key, true)
230	if err != nil {
231		return fmt.Errorf("pressing key (key=%#x): %w", key, err)
232	}
233	err = c.KeyEvent(ctx, key, false)
234	if err != nil {
235		return fmt.Errorf("releasing key (key=%#x): %w", key, err)
236	}
237	return nil
238}
239
240// Type sends a string as key events.
241func (c *Client) Type(ctx context.Context, text string) error {
242	for _, r := range text {
243		select {
244		case <-ctx.Done():
245			return ctx.Err()
246		default:
247		}
248
249		key := RuneToKeysym(r)
250		err := c.Tap(ctx, key)
251		if err != nil {
252			return err
253		}
254	}
255	return nil
256}
257
258// PointerEvent sends a mouse/pointer event.
259func (c *Client) PointerEvent(ctx context.Context, buttonMask uint8, x, y uint16) error {
260	msg := EncodePointerEvent(buttonMask, x, y)
261	return c.transport.Write(ctx, msg)
262}
263
264// SetClipboard sends clipboard text to the server.
265func (c *Client) SetClipboard(ctx context.Context, text string) error {
266	msg := EncodeClientCutText(text)
267	return c.transport.Write(ctx, msg)
268}
269
270// GetLastClipboard returns the last received clipboard data.
271func (c *Client) GetLastClipboard() (string, time.Time) {
272	c.clipMu.RLock()
273	defer c.clipMu.RUnlock()
274	return c.lastClipboard, c.lastClipboardTime
275}
276
277// Close closes the client and transport.
278func (c *Client) Close() error {
279	c.mu.Lock()
280	defer c.mu.Unlock()
281
282	if c.closed {
283		return nil
284	}
285	c.closed = true
286
287	return c.transport.Close()
288}
289
290// Transport returns the underlying transport.
291// This is useful for advanced operations or passing to the proxy.
292func (c *Client) Transport() transport.Transport {
293	return c.transport
294}