mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 15:19:55 +00:00
204 lines
5.9 KiB
Go
204 lines
5.9 KiB
Go
//go:build !js && !ios && !android
|
|
|
|
package server
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
// clipboardPoll periodically checks the server-side clipboard and sends
|
|
// changes to the VNC client. Only runs during active sessions.
|
|
func (s *session) clipboardPoll(done <-chan struct{}) {
|
|
ticker := time.NewTicker(2 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
var lastClip string
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-ticker.C:
|
|
text := s.injector.GetClipboard()
|
|
if len(text) > maxCutTextBytes {
|
|
text = text[:maxCutTextBytes]
|
|
}
|
|
if text == "" || text == lastClip {
|
|
continue
|
|
}
|
|
lastClip = text
|
|
s.encMu.RLock()
|
|
ext := s.clientSupportsExtClipboard
|
|
s.encMu.RUnlock()
|
|
if ext {
|
|
if err := s.writeExtClipMessage(buildExtClipNotify(extClipFormatText)); err != nil {
|
|
s.log.Debugf("send ext clipboard notify: %v", err)
|
|
return
|
|
}
|
|
} else if err := s.sendServerCutText(text); err != nil {
|
|
s.log.Debugf("send clipboard to client: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *session) handleCutText() error {
|
|
var header [7]byte // 3 padding + 4 length
|
|
if _, err := io.ReadFull(s.conn, header[:]); err != nil {
|
|
return fmt.Errorf("read CutText header: %w", err)
|
|
}
|
|
rawLen := int32(binary.BigEndian.Uint32(header[3:7]))
|
|
if rawLen < 0 {
|
|
// Negative length signals ExtendedClipboard; absolute value is the
|
|
// payload size. Guard against MinInt32 overflow before negating.
|
|
if rawLen == -2147483648 {
|
|
return fmt.Errorf("ext clipboard payload too large")
|
|
}
|
|
return s.handleExtCutText(uint32(-rawLen))
|
|
}
|
|
length := uint32(rawLen)
|
|
if length > maxCutTextBytes {
|
|
return fmt.Errorf("cut text too large: %d bytes", length)
|
|
}
|
|
buf := make([]byte, length)
|
|
if _, err := io.ReadFull(s.conn, buf); err != nil {
|
|
return fmt.Errorf("read CutText payload: %w", err)
|
|
}
|
|
s.injector.SetClipboard(string(buf))
|
|
return nil
|
|
}
|
|
|
|
// handleExtCutText parses an ExtendedClipboard message (any of Caps,
|
|
// Notify, Request, Peek, Provide) carried as a negative-length CutText.
|
|
// Unknown actions and formats we don't handle (RTF/HTML/DIB/Files) are
|
|
// dropped without aborting the session.
|
|
func (s *session) handleExtCutText(payloadLen uint32) error {
|
|
if payloadLen < 4 {
|
|
return fmt.Errorf("ext clipboard payload too short: %d", payloadLen)
|
|
}
|
|
if payloadLen > extClipMaxPayload {
|
|
return fmt.Errorf("ext clipboard payload too large: %d", payloadLen)
|
|
}
|
|
buf := make([]byte, payloadLen)
|
|
if _, err := io.ReadFull(s.conn, buf); err != nil {
|
|
return fmt.Errorf("read ext clipboard payload: %w", err)
|
|
}
|
|
flags := binary.BigEndian.Uint32(buf[0:4])
|
|
action := flags & extClipActionMask
|
|
formats := flags & extClipFormatMask
|
|
rest := buf[4:]
|
|
|
|
switch action {
|
|
case extClipActionCaps:
|
|
// Client max sizes are informational for us today: we only emit
|
|
// text and already cap it at extClipMaxText.
|
|
return nil
|
|
case extClipActionRequest:
|
|
if formats&extClipFormatText != 0 {
|
|
return s.sendExtClipProvideText()
|
|
}
|
|
return nil
|
|
case extClipActionPeek:
|
|
return s.writeExtClipMessage(buildExtClipNotify(extClipFormatText))
|
|
case extClipActionNotify:
|
|
if formats&extClipFormatText != 0 {
|
|
return s.writeExtClipMessage(buildExtClipRequest(extClipFormatText))
|
|
}
|
|
return nil
|
|
case extClipActionProvide:
|
|
s.handleExtClipProvide(flags, rest)
|
|
return nil
|
|
default:
|
|
s.log.Debugf("unknown ext clipboard action 0x%x", action)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// handleExtClipProvide decodes a Provide payload and pushes the recovered
|
|
// text into the host clipboard. Decode errors and unsupported formats (RTF,
|
|
// HTML, etc.) are logged and dropped so a malformed message doesn't tear
|
|
// down the session.
|
|
func (s *session) handleExtClipProvide(flags uint32, payload []byte) {
|
|
if len(payload) == 0 {
|
|
return
|
|
}
|
|
text, err := parseExtClipProvideText(flags, payload)
|
|
if err != nil {
|
|
s.log.Debugf("parse ext clipboard provide: %v", err)
|
|
return
|
|
}
|
|
if text != "" {
|
|
s.injector.SetClipboard(text)
|
|
}
|
|
}
|
|
|
|
// sendExtClipProvideText answers an inbound Request(text) with the current
|
|
// host clipboard contents, capped to extClipMaxText.
|
|
func (s *session) sendExtClipProvideText() error {
|
|
text := s.injector.GetClipboard()
|
|
if len(text) > extClipMaxText {
|
|
text = text[:extClipMaxText]
|
|
}
|
|
payload, err := buildExtClipProvideText(text)
|
|
if err != nil {
|
|
return fmt.Errorf("build provide: %w", err)
|
|
}
|
|
return s.writeExtClipMessage(payload)
|
|
}
|
|
|
|
// writeExtClipMessage frames an ExtendedClipboard payload as a ServerCutText
|
|
// message with a negative length, then writes it under writeMu.
|
|
func (s *session) writeExtClipMessage(payload []byte) error {
|
|
if len(payload) == 0 {
|
|
return nil
|
|
}
|
|
buf := make([]byte, 8+len(payload))
|
|
buf[0] = serverCutText
|
|
// buf[1:4] = padding (zero)
|
|
binary.BigEndian.PutUint32(buf[4:8], uint32(-int32(len(payload))))
|
|
copy(buf[8:], payload)
|
|
|
|
s.writeMu.Lock()
|
|
_, err := s.conn.Write(buf)
|
|
s.writeMu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// handleTypeText handles the NetBird-specific PasteAndType message used by
|
|
// the dashboard's Paste button. Wire format mirrors CutText: 3-byte
|
|
// padding + 4-byte length + text bytes.
|
|
func (s *session) handleTypeText() error {
|
|
var header [7]byte
|
|
if _, err := io.ReadFull(s.conn, header[:]); err != nil {
|
|
return fmt.Errorf("read TypeText header: %w", err)
|
|
}
|
|
length := binary.BigEndian.Uint32(header[3:7])
|
|
if length > maxCutTextBytes {
|
|
return fmt.Errorf("type text too large: %d bytes", length)
|
|
}
|
|
buf := make([]byte, length)
|
|
if _, err := io.ReadFull(s.conn, buf); err != nil {
|
|
return fmt.Errorf("read TypeText payload: %w", err)
|
|
}
|
|
s.injector.TypeText(string(buf))
|
|
return nil
|
|
}
|
|
|
|
// sendServerCutText sends clipboard text from the server to the client.
|
|
func (s *session) sendServerCutText(text string) error {
|
|
data := []byte(text)
|
|
buf := make([]byte, 8+len(data))
|
|
buf[0] = serverCutText
|
|
// buf[1:4] = padding (zero)
|
|
binary.BigEndian.PutUint32(buf[4:8], uint32(len(data)))
|
|
copy(buf[8:], data)
|
|
|
|
s.writeMu.Lock()
|
|
_, err := s.conn.Write(buf)
|
|
s.writeMu.Unlock()
|
|
return err
|
|
}
|