main
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}