Fix ExtendedClipboard auto-request by advertising all actions in Caps

This commit is contained in:
Viktor Liu
2026-05-17 16:47:53 +02:00
parent a11341f57a
commit 76add0b9b2
4 changed files with 37 additions and 17 deletions

View File

@@ -10,6 +10,7 @@ type vncServer interface{}
func (e *Engine) updateVNC(_ *mgmProto.SSHConfig) error { return nil }
// updateVNCServerAuth is a no-op on platforms without a VNC server.
func (e *Engine) updateVNCServerAuth(_ *mgmProto.VNCAuth) {}
func (e *Engine) stopVNCServer() error { return nil }

View File

@@ -52,11 +52,16 @@ const (
extClipMaxPayload = extClipMaxText + 1024
)
// buildExtClipCaps emits the Caps payload advertising the formats we accept
// and our maximum size per format. One uint32 size follows the flags word
// for each format bit set, in ascending bit order.
// buildExtClipCaps emits the Caps payload. The flags word advertises every
// action we support in the high byte (Caps + Request + Peek + Notify +
// Provide) and every format we accept in the low 16 bits. noVNC uses these
// action bits to decide whether to auto-Request on Notify; without
// Request in our Caps it silently drops our Notify messages. After the
// flags word we emit one uint32 max size per format bit set, in ascending
// bit order.
func buildExtClipCaps() []byte {
flags := extClipActionCaps | extClipFormatText
flags := extClipActionCaps | extClipActionRequest | extClipActionPeek |
extClipActionNotify | extClipActionProvide | extClipFormatText
payload := make([]byte, 4+4)
binary.BigEndian.PutUint32(payload[0:4], flags)
binary.BigEndian.PutUint32(payload[4:8], uint32(extClipMaxText))

View File

@@ -16,7 +16,13 @@ func TestBuildExtClipCaps(t *testing.T) {
require.Len(t, payload, 8, "Caps with one format should be 4 bytes flags + 4 bytes size")
flags := binary.BigEndian.Uint32(payload[0:4])
assert.Equal(t, extClipActionCaps, flags&extClipActionMask, "action should be Caps")
// noVNC checks individual action bits in our Caps to decide whether to
// auto-Request on Notify, so all supported actions must be advertised.
assert.NotZero(t, flags&extClipActionCaps, "Caps action bit must be set")
assert.NotZero(t, flags&extClipActionRequest, "Request action bit must be set")
assert.NotZero(t, flags&extClipActionPeek, "Peek action bit must be set")
assert.NotZero(t, flags&extClipActionNotify, "Notify action bit must be set")
assert.NotZero(t, flags&extClipActionProvide, "Provide action bit must be set")
assert.Equal(t, extClipFormatText, flags&extClipFormatMask, "should advertise text format")
maxSize := binary.BigEndian.Uint32(payload[4:8])

View File

@@ -877,24 +877,32 @@ func (s *session) handleExtCutText(payloadLen uint32) error {
}
return nil
case extClipActionProvide:
if len(rest) == 0 {
return nil
}
text, err := parseExtClipProvideText(flags, rest)
if err != nil {
s.log.Debugf("parse ext clipboard provide: %v", err)
return nil
}
if text != "" {
s.injector.SetClipboard(text)
}
return nil
return s.handleExtClipProvide(flags, rest)
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. Errors and other unsupported formats (RTF,
// HTML, etc.) are swallowed so a malformed message doesn't tear down the
// session.
func (s *session) handleExtClipProvide(flags uint32, payload []byte) error {
if len(payload) == 0 {
return nil
}
text, err := parseExtClipProvideText(flags, payload)
if err != nil {
s.log.Debugf("parse ext clipboard provide: %v", err)
return nil
}
if text != "" {
s.injector.SetClipboard(text)
}
return nil
}
// sendExtClipProvideText answers an inbound Request(text) with the current
// host clipboard contents, capped to extClipMaxText.
func (s *session) sendExtClipProvideText() error {