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