mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Add RDP state tracker for processing desktop recordings (#65140)
* Add RDP state tracker for processing desktop recordings * Guard against oversized screen dimensions * Reset cursor state on resize * Constrain screen dimensions to the same as we constrain RDP * Move fastpath test helpers to separate package for reuse * Remove mutex * Do not use require in test module, fix linting issues * Appease the linter * Use DecodePermissive * Avoid import cycle by inlining minimal message decoding in rdpstate * Add length check to legacy fast path PDU * t.Parallel()
This commit is contained in:
@@ -40,6 +40,8 @@ package decoder
|
||||
#cgo noescape rdp_decoder_process
|
||||
#cgo nocallback rdp_decoder_image_data
|
||||
#cgo noescape rdp_decoder_image_data
|
||||
#cgo nocallback rdp_decoder_cursor_state
|
||||
#cgo noescape rdp_decoder_cursor_state
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -51,6 +53,7 @@ void rdp_decoder_free(RdpDecoder* ptr);
|
||||
void rdp_decoder_resize(RdpDecoder* ptr, uint16_t width, uint16_t height);
|
||||
void rdp_decoder_process(RdpDecoder* ptr, const uint8_t* data, size_t len);
|
||||
const uint8_t* rdp_decoder_image_data(RdpDecoder* ptr, uint16_t* width, uint16_t* height);
|
||||
void rdp_decoder_cursor_state(RdpDecoder* ptr, uint8_t* out_visible, uint16_t* out_x, uint16_t* out_y);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -67,6 +70,11 @@ type Decoder struct {
|
||||
ptr *C.RdpDecoder
|
||||
}
|
||||
|
||||
type CursorState struct {
|
||||
Visible bool
|
||||
X, Y uint16
|
||||
}
|
||||
|
||||
func New(width, height uint16) (*Decoder, error) {
|
||||
ptr := C.rdp_decoder_new(C.uint16_t(width), C.uint16_t(height))
|
||||
if ptr == nil {
|
||||
@@ -163,3 +171,22 @@ func (d *Decoder) Thumbnail(width, height int) *image.RGBA {
|
||||
|
||||
return thumbnail
|
||||
}
|
||||
|
||||
// CursorState returns the cursor position and visibility as tracked by the
|
||||
// Rust decoder.
|
||||
func (d *Decoder) CursorState() CursorState {
|
||||
if d == nil || d.ptr == nil {
|
||||
return CursorState{}
|
||||
}
|
||||
|
||||
var outVisible C.uint8_t
|
||||
var outX, outY C.uint16_t
|
||||
|
||||
C.rdp_decoder_cursor_state(d.ptr, &outVisible, &outX, &outY)
|
||||
|
||||
return CursorState{
|
||||
Visible: outVisible != 0,
|
||||
X: uint16(outX),
|
||||
Y: uint16(outY),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ import (
|
||||
)
|
||||
|
||||
type Decoder struct{}
|
||||
type CursorState struct {
|
||||
Visible bool
|
||||
X, Y uint16
|
||||
}
|
||||
|
||||
func New(width, height uint16) (*Decoder, error) {
|
||||
return nil, trace.NotImplemented("the RDP decoder is not included in this build")
|
||||
@@ -35,3 +39,4 @@ func (d *Decoder) Resize(w, h uint16) {}
|
||||
func (d *Decoder) Process([]byte) {}
|
||||
func (d *Decoder) Image() *image.RGBA { return nil }
|
||||
func (d *Decoder) Thumbnail(w, h int) *image.RGBA { return nil }
|
||||
func (d *Decoder) CursorState() CursorState { return CursorState{} }
|
||||
|
||||
@@ -23,14 +23,23 @@ use std::ptr;
|
||||
|
||||
use ironrdp_core::WriteBuf;
|
||||
use ironrdp_graphics::image_processing::PixelFormat;
|
||||
use ironrdp_session::fast_path::UpdateKind;
|
||||
use ironrdp_session::{
|
||||
fast_path::{Processor, ProcessorBuilder},
|
||||
image::DecodedImage,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct CursorState {
|
||||
visible: bool,
|
||||
x: u16,
|
||||
y: u16,
|
||||
}
|
||||
|
||||
pub struct RdpDecoder {
|
||||
image: DecodedImage,
|
||||
fast_path_processor: Processor,
|
||||
cursor_state: CursorState,
|
||||
}
|
||||
|
||||
impl RdpDecoder {
|
||||
@@ -40,20 +49,24 @@ impl RdpDecoder {
|
||||
Self {
|
||||
image: DecodedImage::new(RdpDecoder::PIXEL_FORMAT, width, height),
|
||||
fast_path_processor: ProcessorBuilder {
|
||||
// Enable pointer updates so we can get the state of the cursor for when we create
|
||||
// cropped & zoomed in thumbnails in the session recording metadata generation.
|
||||
enable_server_pointer: true,
|
||||
// These options only matter in a real RDP session when we have
|
||||
// to send responses back to the server. We can safely leave them
|
||||
// at defaults when decoding session recordings.
|
||||
io_channel_id: 0,
|
||||
user_channel_id: 0,
|
||||
enable_server_pointer: false,
|
||||
pointer_software_rendering: false,
|
||||
}
|
||||
.build(),
|
||||
cursor_state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.image = DecodedImage::new(RdpDecoder::PIXEL_FORMAT, width, height);
|
||||
self.cursor_state = Default::default();
|
||||
}
|
||||
|
||||
pub fn image_data(&self) -> &[u8] {
|
||||
@@ -72,12 +85,28 @@ impl RdpDecoder {
|
||||
let mut output = WriteBuf::new();
|
||||
|
||||
// In a live RDP connection, this would return data that we need
|
||||
// to use to create reponses to send to the server.
|
||||
// to use to create responses to send to the server.
|
||||
// We're only interested in updating the internal frame buffer,
|
||||
// so we can ignore the result.
|
||||
let _ = self
|
||||
.fast_path_processor
|
||||
.process(&mut self.image, tdp_fast_path_frame, &mut output);
|
||||
if let Ok(updates) =
|
||||
self.fast_path_processor
|
||||
.process(&mut self.image, tdp_fast_path_frame, &mut output)
|
||||
{
|
||||
for update in updates {
|
||||
match update {
|
||||
UpdateKind::PointerBitmap(_) => {
|
||||
self.cursor_state.visible = true;
|
||||
}
|
||||
UpdateKind::PointerDefault => self.cursor_state.visible = true,
|
||||
UpdateKind::PointerHidden => self.cursor_state.visible = false,
|
||||
UpdateKind::PointerPosition { x, y } => {
|
||||
self.cursor_state.x = x;
|
||||
self.cursor_state.y = y;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +213,31 @@ pub unsafe extern "C" fn rdp_decoder_image_data(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current cursor position and visibility state.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `ptr` must be a valid pointer previously returned by `rdp_decoder_new`.
|
||||
/// - All out-params must be valid, non-null pointers.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rdp_decoder_cursor_state(
|
||||
ptr: *mut RdpDecoder,
|
||||
out_visible: *mut u8,
|
||||
out_x: *mut u16,
|
||||
out_y: *mut u16,
|
||||
) {
|
||||
if ptr.is_null() || out_visible.is_null() || out_x.is_null() || out_y.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = catch_unwind_and_drop_panic_payload(AssertUnwindSafe(move || unsafe {
|
||||
let decoder = &*ptr;
|
||||
*out_visible = decoder.cursor_state.visible as u8;
|
||||
*out_x = decoder.cursor_state.x;
|
||||
*out_y = decoder.cursor_state.y;
|
||||
}));
|
||||
}
|
||||
|
||||
fn catch_unwind_and_drop_panic_payload<F: FnOnce() -> R, R>(f: F) -> Result<R, ()> {
|
||||
catch_unwind(AssertUnwindSafe(f)).map_err(|e| {
|
||||
// If dropping the original panic payload causes another panic,
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rdpstate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
tdpbv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/desktop/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/types/events"
|
||||
"github.com/gravitational/teleport/lib/srv/desktop/rdp/decoder"
|
||||
)
|
||||
|
||||
const (
|
||||
// Legacy TDP message types relevant for recording replay.
|
||||
legacyTypeConnectionActivated = 31
|
||||
legacyTypeRDPFastPathPDU = 29
|
||||
|
||||
// tdpbHeaderLength is the size of the TDPB message length prefix.
|
||||
tdpbHeaderLength = 4
|
||||
|
||||
// maxMessageLength is the maximum allowed TDPB message body size (16 MiB - 1).
|
||||
maxMessageLength = (1 << 24) - 1
|
||||
)
|
||||
|
||||
// RDPState reconstructs the screen state of a desktop session by processing a sequence of DesktopRecording events.
|
||||
// It supports two recording formats:
|
||||
// - Legacy TDP (evt.Message): ConnectionActivated + RDPFastPathPDU messages, which are translated to modern TDPB
|
||||
// types before processing.
|
||||
// - Modern TDPB (evt.TDPBMessage): ServerHello + FastPathPDU messages.
|
||||
//
|
||||
// The RDP decoder is initialized on the first ServerHello (or its legacy equivalent, ConnectionActivated).
|
||||
// Subsequent FastPathPDU messages are fed to the decoder, which maintains the screen framebuffer and cursor state.
|
||||
type RDPState struct {
|
||||
decoder *decoder.Decoder
|
||||
}
|
||||
|
||||
// New creates a new RDPState.
|
||||
func New() *RDPState {
|
||||
return &RDPState{}
|
||||
}
|
||||
|
||||
// HandleMessage processes a single DesktopRecording event.
|
||||
// Events must be fed in recording order. Only one of evt.Message (legacy TDP) or evt.TDPBMessage (modern TDPB) should
|
||||
// be set; if both are empty the call is a no-op.
|
||||
func (s *RDPState) HandleMessage(evt *events.DesktopRecording) error {
|
||||
if len(evt.TDPBMessage) > 0 {
|
||||
return trace.Wrap(s.processTDPBMessage(evt.TDPBMessage), "processing TDPB message")
|
||||
}
|
||||
|
||||
if len(evt.Message) > 0 {
|
||||
return trace.Wrap(s.processTDPMessage(evt.Message), "processing legacy TDP message")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorState returns the current cursor visibility and position. If the decoder has not been initialized yet, it
|
||||
// returns a default hidden cursor at (0, 0).
|
||||
func (s *RDPState) CursorState() decoder.CursorState {
|
||||
if s.decoder == nil {
|
||||
return decoder.CursorState{}
|
||||
}
|
||||
|
||||
return s.decoder.CursorState()
|
||||
}
|
||||
|
||||
// Image returns the current screen image as an RGBA bitmap. If the decoder has not been initialized yet, it returns nil.
|
||||
func (s *RDPState) Image() *image.RGBA {
|
||||
if s.decoder == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.decoder.Image()
|
||||
}
|
||||
|
||||
// Release frees any resources associated with the RDPState, including the decoder.
|
||||
func (s *RDPState) Release() {
|
||||
if s.decoder != nil {
|
||||
s.decoder.Release()
|
||||
s.decoder = nil
|
||||
}
|
||||
}
|
||||
|
||||
type connectionActivated struct {
|
||||
IOChannelID, UserChannelID, ScreenWidth, ScreenHeight uint16
|
||||
}
|
||||
|
||||
func (s *RDPState) processTDPMessage(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return trace.BadParameter("empty legacy TDP message")
|
||||
}
|
||||
|
||||
msgType := data[0]
|
||||
r := bytes.NewReader(data[1:])
|
||||
|
||||
switch msgType {
|
||||
case legacyTypeConnectionActivated:
|
||||
var ca connectionActivated
|
||||
if err := binary.Read(r, binary.BigEndian, &ca); err != nil {
|
||||
return trace.Wrap(err, "decoding legacy ConnectionActivated")
|
||||
}
|
||||
|
||||
return s.handleServerHello(&tdpbv1.ServerHello{
|
||||
ActivationSpec: &tdpbv1.ConnectionActivated{
|
||||
IoChannelId: uint32(ca.IOChannelID),
|
||||
UserChannelId: uint32(ca.UserChannelID),
|
||||
ScreenWidth: uint32(ca.ScreenWidth),
|
||||
ScreenHeight: uint32(ca.ScreenHeight),
|
||||
},
|
||||
})
|
||||
|
||||
case legacyTypeRDPFastPathPDU:
|
||||
var dataLen uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &dataLen); err != nil {
|
||||
return trace.Wrap(err, "reading legacy RDPFastPathPDU length")
|
||||
}
|
||||
|
||||
if dataLen >= maxMessageLength {
|
||||
return trace.BadParameter("legacy RDPFastPathPDU length %d exceeds maximum %d", dataLen, maxMessageLength)
|
||||
}
|
||||
|
||||
pdu := make([]byte, dataLen)
|
||||
if _, err := io.ReadFull(r, pdu); err != nil {
|
||||
return trace.Wrap(err, "reading legacy RDPFastPathPDU data")
|
||||
}
|
||||
|
||||
return s.handleFastPathPDU(&tdpbv1.FastPathPDU{Pdu: pdu})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RDPState) processTDPBMessage(data []byte) error {
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
header := make([]byte, tdpbHeaderLength)
|
||||
if _, err := io.ReadFull(r, header); err != nil {
|
||||
return trace.Wrap(err, "reading TDPB header")
|
||||
}
|
||||
|
||||
msgLen := binary.BigEndian.Uint32(header)
|
||||
if msgLen >= maxMessageLength {
|
||||
return trace.BadParameter("TDPB message length %d exceeds maximum %d", msgLen, maxMessageLength)
|
||||
}
|
||||
|
||||
msg := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(r, msg); err != nil {
|
||||
return trace.Wrap(err, "reading TDPB body")
|
||||
}
|
||||
|
||||
env := &tdpbv1.Envelope{}
|
||||
if err := proto.Unmarshal(msg, env); err != nil {
|
||||
return trace.Wrap(err, "unmarshalling TDPB envelope")
|
||||
}
|
||||
|
||||
switch m := env.Payload.(type) {
|
||||
case *tdpbv1.Envelope_ServerHello:
|
||||
return s.handleServerHello(m.ServerHello)
|
||||
case *tdpbv1.Envelope_FastPathPdu:
|
||||
return s.handleFastPathPDU(m.FastPathPdu)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RDPState) handleServerHello(msg *tdpbv1.ServerHello) error {
|
||||
spec := msg.GetActivationSpec()
|
||||
if spec == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sw, sh := spec.GetScreenWidth(), spec.GetScreenHeight()
|
||||
if sw > types.MaxRDPScreenWidth || sh > types.MaxRDPScreenHeight {
|
||||
return trace.BadParameter("screen dimensions (%d x %d) exceed maximum (%d x %d)",
|
||||
sw, sh, types.MaxRDPScreenWidth, types.MaxRDPScreenHeight)
|
||||
}
|
||||
|
||||
w := uint16(sw)
|
||||
h := uint16(sh)
|
||||
|
||||
if w == 0 || h == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.decoder == nil {
|
||||
d, err := decoder.New(w, h) //nolint:staticcheck // err is always non-nil in nop build but nil in RDP build
|
||||
if err != nil { //nolint:staticcheck // err is always non-nil in nop build but nil in RDP build
|
||||
return trace.Wrap(err, "creating RDP decoder")
|
||||
}
|
||||
|
||||
s.decoder = d
|
||||
} else {
|
||||
s.decoder.Resize(w, h)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RDPState) handleFastPathPDU(msg *tdpbv1.FastPathPDU) error {
|
||||
pdu := msg.GetPdu()
|
||||
if len(pdu) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.decoder == nil {
|
||||
return trace.BadParameter("received FastPathPDU before ServerHello initialized decoder")
|
||||
}
|
||||
|
||||
s.decoder.Process(pdu)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
//go:build desktop_access_rdp || rust_rdp_decoder
|
||||
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rdpstate
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/lib/srv/desktop/rdp/decoder"
|
||||
"github.com/gravitational/teleport/lib/srv/desktop/rdpstate/rdpstatetest"
|
||||
)
|
||||
|
||||
func TestServerHello_CreatesDecoder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 800, 600)))
|
||||
require.NotNil(t, s.decoder)
|
||||
|
||||
img := s.Image()
|
||||
require.NotNil(t, img)
|
||||
|
||||
require.Equal(t, 800, img.Bounds().Dx())
|
||||
require.Equal(t, 600, img.Bounds().Dy())
|
||||
}
|
||||
|
||||
func TestServerHello_Resize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 800, 600)))
|
||||
|
||||
d := s.decoder
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 1920, 1080)))
|
||||
|
||||
require.Same(t, d, s.decoder)
|
||||
}
|
||||
|
||||
func TestFastPathPDU_UpdatesImage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 100, 100)))
|
||||
|
||||
// Draw a 4x2 white rectangle at (10, 20).
|
||||
sendPDU(t, s, rdpstatetest.BuildBitmapPDU(10, 20, 4, 2, rdpstatetest.RGB565White))
|
||||
|
||||
img := s.Image()
|
||||
require.NotNil(t, img)
|
||||
|
||||
// Pixel inside the bitmap should be white.
|
||||
r, g, b, a := img.At(10, 20).RGBA()
|
||||
require.Equal(t, uint32(0xFFFF), r)
|
||||
require.Equal(t, uint32(0xFFFF), g)
|
||||
require.Equal(t, uint32(0xFFFF), b)
|
||||
require.Equal(t, uint32(0xFFFF), a)
|
||||
|
||||
// Pixel outside should be untouched (black/transparent).
|
||||
r, g, b, a = img.At(99, 99).RGBA()
|
||||
require.Equal(t, uint32(0), r)
|
||||
require.Equal(t, uint32(0), g)
|
||||
require.Equal(t, uint32(0), b)
|
||||
require.Equal(t, uint32(0), a)
|
||||
}
|
||||
|
||||
func TestFastPathPDU_MultipleBitmaps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 100, 100)))
|
||||
|
||||
// Red at row 0, blue at row 1.
|
||||
sendPDU(t, s, rdpstatetest.BuildBitmapPDU(0, 0, 4, 1, rdpstatetest.RGB565Red))
|
||||
sendPDU(t, s, rdpstatetest.BuildBitmapPDU(0, 1, 4, 1, rdpstatetest.RGB565Blue))
|
||||
|
||||
img := s.Image()
|
||||
require.NotNil(t, img)
|
||||
|
||||
// Red pixel (RGB565 0xF800 -> R>0, G=0, B=0).
|
||||
r, g, b, _ := img.At(0, 0).RGBA()
|
||||
require.NotEqual(t, uint32(0), r, "expected red channel to be non-zero")
|
||||
require.Equal(t, uint32(0), g)
|
||||
require.Equal(t, uint32(0), b)
|
||||
|
||||
// Blue pixel (RGB565 0x001F -> R=0, G=0, B>0).
|
||||
r, g, b, _ = img.At(0, 1).RGBA()
|
||||
require.Equal(t, uint32(0), r)
|
||||
require.Equal(t, uint32(0), g)
|
||||
require.NotEqual(t, uint32(0), b, "expected blue channel to be non-zero")
|
||||
}
|
||||
|
||||
func TestFastPathPDU_AfterResize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 100, 100)))
|
||||
sendPDU(t, s, rdpstatetest.BuildBitmapPDU(0, 0, 4, 1, rdpstatetest.RGB565White))
|
||||
|
||||
// Resize clears the framebuffer.
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 200, 200)))
|
||||
|
||||
img := s.Image()
|
||||
require.NotNil(t, img)
|
||||
require.Equal(t, 200, img.Bounds().Dx())
|
||||
require.Equal(t, 200, img.Bounds().Dy())
|
||||
|
||||
// Old content should be gone.
|
||||
r, g, b, a := img.At(0, 0).RGBA()
|
||||
require.Equal(t, uint32(0), r)
|
||||
require.Equal(t, uint32(0), g)
|
||||
require.Equal(t, uint32(0), b)
|
||||
require.Equal(t, uint32(0), a)
|
||||
|
||||
// New PDU after resize still works.
|
||||
sendPDU(t, s, rdpstatetest.BuildBitmapPDU(0, 0, 4, 1, rdpstatetest.RGB565White))
|
||||
img = s.Image()
|
||||
require.NotNil(t, img)
|
||||
|
||||
r, g, b, a = img.At(0, 0).RGBA()
|
||||
require.Equal(t, uint32(0xFFFF), r)
|
||||
require.Equal(t, uint32(0xFFFF), g)
|
||||
require.Equal(t, uint32(0xFFFF), b)
|
||||
require.Equal(t, uint32(0xFFFF), a)
|
||||
}
|
||||
|
||||
func TestLegacyTDP_ConnectionActivated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
data := legacyConnectionActivated(t, 1024, 768)
|
||||
require.NoError(t, s.HandleMessage(data))
|
||||
require.NotNil(t, s.decoder)
|
||||
|
||||
img := s.Image()
|
||||
require.NotNil(t, img)
|
||||
|
||||
require.Equal(t, 1024, img.Bounds().Dx())
|
||||
require.Equal(t, 768, img.Bounds().Dy())
|
||||
}
|
||||
|
||||
func TestLegacyTDP_Resize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(legacyConnectionActivated(t, 800, 600)))
|
||||
d := s.decoder
|
||||
|
||||
require.NoError(t, s.HandleMessage(legacyConnectionActivated(t, 1920, 1080)))
|
||||
require.Same(t, d, s.decoder)
|
||||
|
||||
img := s.Image()
|
||||
require.NotNil(t, img)
|
||||
require.Equal(t, 1920, img.Bounds().Dx())
|
||||
require.Equal(t, 1080, img.Bounds().Dy())
|
||||
}
|
||||
|
||||
func TestCursorState_DefaultHidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 800, 600)))
|
||||
|
||||
cs := s.CursorState()
|
||||
require.False(t, cs.Visible)
|
||||
require.Equal(t, uint16(0), cs.X)
|
||||
require.Equal(t, uint16(0), cs.Y)
|
||||
}
|
||||
|
||||
func TestCursorState_BeforeServerHello(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
cs := s.CursorState()
|
||||
require.False(t, cs.Visible)
|
||||
require.Equal(t, uint16(0), cs.X)
|
||||
require.Equal(t, uint16(0), cs.Y)
|
||||
}
|
||||
|
||||
func TestCursorState_PointerPosition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 100, 100)))
|
||||
|
||||
sendPDU(t, s, rdpstatetest.BuildNewPointerPDU(2, 2, 0, 0, rdpstatetest.BGRARed))
|
||||
sendPDU(t, s, rdpstatetest.BuildPointerPositionPDU(50, 60))
|
||||
|
||||
cs := s.CursorState()
|
||||
require.Equal(t, decoder.CursorState{Visible: true, X: 50, Y: 60}, cs)
|
||||
}
|
||||
|
||||
func TestCursorState_PointerHidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 100, 100)))
|
||||
|
||||
sendPDU(t, s, rdpstatetest.BuildNewPointerPDU(2, 2, 0, 0, rdpstatetest.BGRARed))
|
||||
require.True(t, s.CursorState().Visible)
|
||||
|
||||
sendPDU(t, s, rdpstatetest.BuildPointerHiddenPDU())
|
||||
require.False(t, s.CursorState().Visible)
|
||||
}
|
||||
|
||||
func TestCursorState_PointerDefaultRestoresVisibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBServerHello(t, 100, 100)))
|
||||
|
||||
sendPDU(t, s, rdpstatetest.BuildNewPointerPDU(2, 2, 0, 0, rdpstatetest.BGRARed))
|
||||
sendPDU(t, s, rdpstatetest.BuildPointerHiddenPDU())
|
||||
require.False(t, s.CursorState().Visible)
|
||||
|
||||
sendPDU(t, s, rdpstatetest.BuildPointerDefaultPDU())
|
||||
require.True(t, s.CursorState().Visible)
|
||||
}
|
||||
|
||||
func TestLegacyCursorState_PointerPosition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(legacyConnectionActivated(t, 100, 100)))
|
||||
|
||||
sendLegacyPDU(t, s, rdpstatetest.BuildNewPointerPDU(2, 2, 0, 0, rdpstatetest.BGRARed))
|
||||
sendLegacyPDU(t, s, rdpstatetest.BuildPointerPositionPDU(50, 60))
|
||||
|
||||
cs := s.CursorState()
|
||||
require.Equal(t, decoder.CursorState{Visible: true, X: 50, Y: 60}, cs)
|
||||
}
|
||||
|
||||
func TestLegacyCursorState_PointerHidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(legacyConnectionActivated(t, 100, 100)))
|
||||
|
||||
sendLegacyPDU(t, s, rdpstatetest.BuildNewPointerPDU(2, 2, 0, 0, rdpstatetest.BGRARed))
|
||||
require.True(t, s.CursorState().Visible)
|
||||
|
||||
sendLegacyPDU(t, s, rdpstatetest.BuildPointerHiddenPDU())
|
||||
require.False(t, s.CursorState().Visible)
|
||||
}
|
||||
|
||||
func TestLegacyCursorState_PointerDefaultRestoresVisibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(legacyConnectionActivated(t, 100, 100)))
|
||||
|
||||
sendLegacyPDU(t, s, rdpstatetest.BuildNewPointerPDU(2, 2, 0, 0, rdpstatetest.BGRARed))
|
||||
sendLegacyPDU(t, s, rdpstatetest.BuildPointerHiddenPDU())
|
||||
require.False(t, s.CursorState().Visible)
|
||||
|
||||
sendLegacyPDU(t, s, rdpstatetest.BuildPointerDefaultPDU())
|
||||
require.True(t, s.CursorState().Visible)
|
||||
}
|
||||
|
||||
func sendPDU(t *testing.T, s *RDPState, pdu []byte) {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBFastPathPDU(t, pdu)))
|
||||
}
|
||||
|
||||
func sendLegacyPDU(t *testing.T, s *RDPState, pdu []byte) {
|
||||
t.Helper()
|
||||
|
||||
// Encode as legacy RDPFastPathPDU: type byte (29) + uint32 length + data.
|
||||
data := make([]byte, 1+4+len(pdu))
|
||||
data[0] = 29 // TypeRDPFastPathPDU
|
||||
binary.BigEndian.PutUint32(data[1:5], uint32(len(pdu)))
|
||||
copy(data[5:], pdu)
|
||||
require.NoError(t, s.HandleMessage(rdpstatetest.LegacyEvent(data)))
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rdpstate
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
tdpbv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/desktop/v1"
|
||||
"github.com/gravitational/teleport/api/types/events"
|
||||
"github.com/gravitational/teleport/lib/srv/desktop/rdpstate/rdpstatetest"
|
||||
)
|
||||
|
||||
// These tests cover message routing, error handling, and edge cases that don't require a real RDP decoder.
|
||||
// Tests that process actual FastPath PDUs and verify decoded image content are in rdpstate_rdp_test.go (build-tagged).
|
||||
|
||||
func TestHandleMessage_EmptyEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
require.NoError(t, s.HandleMessage(&events.DesktopRecording{}))
|
||||
require.Nil(t, s.decoder)
|
||||
}
|
||||
|
||||
func TestHandleMessage_InvalidData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
evt *events.DesktopRecording
|
||||
}{
|
||||
{"invalid TDPB", rdpstatetest.TDPBEvent([]byte{0xFF, 0xFF, 0xFF})},
|
||||
{"truncated legacy", rdpstatetest.LegacyEvent([]byte{29})}, // RDPFastPathPDU type with no payload
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Error(t, New().HandleMessage(tt.evt))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerHello_NoOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
evt *events.DesktopRecording
|
||||
}{
|
||||
{"zero width", encodeTDPBServerHello(t, 0, 600)},
|
||||
{"zero height", encodeTDPBServerHello(t, 800, 0)},
|
||||
{"legacy zero width", legacyConnectionActivated(t, 0, 600)},
|
||||
{"legacy zero height", legacyConnectionActivated(t, 800, 0)},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(tt.evt))
|
||||
require.Nil(t, s.decoder)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFastPathPDU_BeforeServerHello(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
err := s.HandleMessage(encodeTDPBFastPathPDU(t, []byte{0xDE, 0xAD}))
|
||||
|
||||
require.True(t, trace.IsBadParameter(err), "expected BadParameter, got: %v", err)
|
||||
}
|
||||
|
||||
func TestFastPathPDU_EmptyPDU(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
require.NoError(t, s.HandleMessage(encodeTDPBFastPathPDU(t, nil)))
|
||||
require.Nil(t, s.decoder)
|
||||
}
|
||||
|
||||
func TestUnknownMessage_Ignored(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New()
|
||||
|
||||
// Encode a SyncKeys message (not handled by RDPState) as a TDPB envelope.
|
||||
body, err := proto.Marshal(&tdpbv1.Envelope{
|
||||
Payload: &tdpbv1.Envelope_SyncKeys{SyncKeys: &tdpbv1.SyncKeys{}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
data := make([]byte, 4+len(body))
|
||||
binary.BigEndian.PutUint32(data[:4], uint32(len(body)))
|
||||
copy(data[4:], body)
|
||||
|
||||
require.NoError(t, s.HandleMessage(rdpstatetest.TDPBEvent(data)))
|
||||
require.Nil(t, s.decoder)
|
||||
}
|
||||
|
||||
func encodeTDPBServerHello(t *testing.T, width, height uint32) *events.DesktopRecording {
|
||||
t.Helper()
|
||||
|
||||
evt, err := rdpstatetest.EncodeTDPBServerHello(width, height)
|
||||
require.NoError(t, err)
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func encodeTDPBFastPathPDU(t *testing.T, pdu []byte) *events.DesktopRecording {
|
||||
t.Helper()
|
||||
|
||||
evt, err := rdpstatetest.EncodeTDPBFastPathPDU(pdu)
|
||||
require.NoError(t, err)
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func legacyConnectionActivated(t *testing.T, width, height uint16) *events.DesktopRecording {
|
||||
t.Helper()
|
||||
|
||||
evt, err := rdpstatetest.LegacyConnectionActivated(width, height)
|
||||
require.NoError(t, err)
|
||||
|
||||
return evt
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Package rdpstatetest provides shared test helpers for constructing
|
||||
// DesktopRecording events and raw FastPath PDUs used in RDP-related tests.
|
||||
package rdpstatetest
|
||||
|
||||
import (
|
||||
tdpbv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/desktop/v1"
|
||||
apievents "github.com/gravitational/teleport/api/types/events"
|
||||
"github.com/gravitational/teleport/lib/srv/desktop/tdp/protocol/legacy"
|
||||
"github.com/gravitational/teleport/lib/srv/desktop/tdp/protocol/tdpb"
|
||||
)
|
||||
|
||||
// TDPBEvent wraps raw TDPB-encoded bytes in a DesktopRecording event.
|
||||
func TDPBEvent(data []byte) *apievents.DesktopRecording {
|
||||
return &apievents.DesktopRecording{TDPBMessage: data}
|
||||
}
|
||||
|
||||
// LegacyEvent wraps raw legacy-TDP-encoded bytes in a DesktopRecording event.
|
||||
func LegacyEvent(data []byte) *apievents.DesktopRecording {
|
||||
return &apievents.DesktopRecording{Message: data}
|
||||
}
|
||||
|
||||
// EncodeTDPBServerHello creates a DesktopRecording event containing a TDPB ServerHello message with the given screen
|
||||
// dimensions.
|
||||
func EncodeTDPBServerHello(width, height uint32) (*apievents.DesktopRecording, error) {
|
||||
data, err := (&tdpb.ServerHello{
|
||||
ActivationSpec: &tdpbv1.ConnectionActivated{
|
||||
ScreenWidth: width,
|
||||
ScreenHeight: height,
|
||||
},
|
||||
}).Encode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return TDPBEvent(data), nil
|
||||
}
|
||||
|
||||
// EncodeTDPBFastPathPDU creates a DesktopRecording event containing a TDPB FastPathPDU message wrapping the given raw
|
||||
// PDU bytes.
|
||||
func EncodeTDPBFastPathPDU(pdu []byte) (*apievents.DesktopRecording, error) {
|
||||
data, err := (&tdpb.FastPathPDU{Pdu: pdu}).Encode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return TDPBEvent(data), nil
|
||||
}
|
||||
|
||||
// LegacyConnectionActivated creates a DesktopRecording event containing a legacy ConnectionActivated message with the
|
||||
// given screen dimensions.
|
||||
func LegacyConnectionActivated(width, height uint16) (*apievents.DesktopRecording, error) {
|
||||
data, err := legacy.ConnectionActivated{ScreenWidth: width, ScreenHeight: height}.Encode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return LegacyEvent(data), nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package rdpstatetest
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"slices"
|
||||
)
|
||||
|
||||
const (
|
||||
// Bitmap Update Data (MS-RDPBCGR 2.2.9.1.1.3.1.2).
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/d681bb11-f3b5-4add-b092-19fe7075f9e3
|
||||
|
||||
// BitmapUpdateType is the "update header" value for a bitmap update in a FastPath PDU.
|
||||
BitmapUpdateType = 0x0001
|
||||
|
||||
// Client Fast-Path Input Event PDU actions (MS-RDPBCGR 2.2.8.1.2).
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/b8e7c588-51cb-455b-bb73-92d480903133
|
||||
|
||||
// ActionFastPath is the "action" byte value for a FastPath PDU.
|
||||
ActionFastPath = 0x00
|
||||
|
||||
// FastPath update codes and fragmentation (MS-RDPBCGR 2.2.9.1.2.1).
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/a1c4caa8-00ed-45bb-a06e-5177473766d3
|
||||
|
||||
// SingleFragmentFlag is the bit flag to set in the update code byte of a FastPath PDU to indicate that the update is contained in a single fragment.
|
||||
SingleFragmentFlag = 0x00
|
||||
|
||||
// UpdateCodeBitmap indicates a bitmap update fragment.
|
||||
UpdateCodeBitmap = 0x01
|
||||
// UpdateCodePointerHidden indicates a pointer hidden update fragment.
|
||||
UpdateCodePointerHidden = 0x05
|
||||
// UpdateCodePointerDefault indicates a pointer default update fragment.
|
||||
UpdateCodePointerDefault = 0x06
|
||||
// UpdateCodePointerPos indicates a pointer position update fragment.
|
||||
UpdateCodePointerPos = 0x08
|
||||
// UpdateCodeNewPointer indicates a new pointer update fragment.
|
||||
UpdateCodeNewPointer = 0x0b
|
||||
|
||||
// RGB565 pixel values for test bitmaps.
|
||||
|
||||
// RGB565White is a white pixel in RGB565 format.
|
||||
RGB565White = 0xFFFF
|
||||
// RGB565Red is a red pixel in RGB565 format.
|
||||
RGB565Red = 0xF800
|
||||
// RGB565Blue is a blue pixel in RGB565 format.
|
||||
RGB565Blue = 0x001F
|
||||
)
|
||||
|
||||
// BGRARed is a BGRA pixel for a solid red color.
|
||||
var BGRARed = [4]byte{0x00, 0x00, 0xFF, 0xFF}
|
||||
|
||||
// WrapFastPathUpdate wraps inner update data in a FastPath PDU envelope.
|
||||
func WrapFastPathUpdate(updateCode byte, innerData []byte) []byte {
|
||||
// FastPathUpdatePdu
|
||||
fpUpdate := make([]byte, 3)
|
||||
fpUpdate[0] = updateCode | SingleFragmentFlag
|
||||
binary.LittleEndian.PutUint16(fpUpdate[1:], uint16(len(innerData)))
|
||||
|
||||
// FastPathHeader
|
||||
body := slices.Concat(fpUpdate, innerData)
|
||||
totalLen := 2 + len(body)
|
||||
header := []byte{ActionFastPath, byte(totalLen)}
|
||||
|
||||
return slices.Concat(header, body)
|
||||
}
|
||||
|
||||
// BuildBitmapPDU constructs a minimal valid RDP fast path PDU containing an uncompressed 16bpp bitmap update.
|
||||
// The bitmap fills a rectangle at (left, top) with dimensions w x h using the given RGB565 pixel value.
|
||||
func BuildBitmapPDU(left, top, w, h int, rgb565 uint16) []byte {
|
||||
bitmapData := make([]byte, 18)
|
||||
binary.LittleEndian.PutUint16(bitmapData[0:], uint16(left))
|
||||
binary.LittleEndian.PutUint16(bitmapData[2:], uint16(top))
|
||||
binary.LittleEndian.PutUint16(bitmapData[4:], uint16(left+w-1)) // right (inclusive)
|
||||
binary.LittleEndian.PutUint16(bitmapData[6:], uint16(top+h-1)) // bottom (inclusive)
|
||||
binary.LittleEndian.PutUint16(bitmapData[8:], uint16(w))
|
||||
binary.LittleEndian.PutUint16(bitmapData[10:], uint16(h))
|
||||
binary.LittleEndian.PutUint16(bitmapData[12:], 16) // bitsPerPixel
|
||||
binary.LittleEndian.PutUint16(bitmapData[14:], 0) // flags (uncompressed)
|
||||
|
||||
rowBytes := w * 2 // 2 bytes per pixel at 16bpp
|
||||
if rowBytes%4 != 0 {
|
||||
// rows must be 4-byte aligned
|
||||
rowBytes += 4 - (rowBytes % 4)
|
||||
}
|
||||
|
||||
pixelDataLen := rowBytes * h
|
||||
binary.LittleEndian.PutUint16(bitmapData[16:], uint16(pixelDataLen))
|
||||
|
||||
pixelData := make([]byte, pixelDataLen)
|
||||
for row := range h {
|
||||
for col := range w {
|
||||
binary.LittleEndian.PutUint16(pixelData[row*rowBytes+col*2:], rgb565)
|
||||
}
|
||||
}
|
||||
|
||||
// BitmapUpdateData header
|
||||
bitmapUpdate := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint16(bitmapUpdate[0:], BitmapUpdateType)
|
||||
binary.LittleEndian.PutUint16(bitmapUpdate[2:], 1) // number of rectangles
|
||||
|
||||
innerData := slices.Concat(bitmapUpdate, bitmapData, pixelData)
|
||||
|
||||
return WrapFastPathUpdate(UpdateCodeBitmap, innerData)
|
||||
}
|
||||
|
||||
// BuildNewPointerPDU constructs a FastPath "New Pointer" update with a solid-color 32bpp cursor of the given dimensions
|
||||
// and BGRA pixel value.
|
||||
// The resulting DecodedPointer bitmap will be in RGBA format after IronRDP decoding.
|
||||
func BuildNewPointerPDU(w, h, hotspotX, hotspotY int, bgra [4]byte) []byte {
|
||||
// XOR mask: 32bpp BGRA pixels, bottom-up scanlines. For 32bpp the stride is always w*4 (always 16-bit aligned).
|
||||
xorStride := w * 4
|
||||
xorMask := make([]byte, xorStride*h)
|
||||
for row := range h {
|
||||
for col := range w {
|
||||
off := row*xorStride + col*4
|
||||
xorMask[off] = bgra[0]
|
||||
xorMask[off+1] = bgra[1]
|
||||
xorMask[off+2] = bgra[2]
|
||||
xorMask[off+3] = bgra[3]
|
||||
}
|
||||
}
|
||||
|
||||
// TS_POINTERATTRIBUTE (New Pointer): xor_bpp + ColorPointerAttribute
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/584e4438-574c-45f4-947f-b0edcd9ae32c
|
||||
data := make([]byte, 16)
|
||||
binary.LittleEndian.PutUint16(data[0:], 32) // xor_bpp
|
||||
binary.LittleEndian.PutUint16(data[2:], 0) // cache_index
|
||||
binary.LittleEndian.PutUint16(data[4:], uint16(hotspotX)) // hot_spot.x
|
||||
binary.LittleEndian.PutUint16(data[6:], uint16(hotspotY)) // hot_spot.y
|
||||
binary.LittleEndian.PutUint16(data[8:], uint16(w)) // width
|
||||
binary.LittleEndian.PutUint16(data[10:], uint16(h)) // height
|
||||
binary.LittleEndian.PutUint16(data[12:], 0) // and_mask_len (empty → default opaque)
|
||||
binary.LittleEndian.PutUint16(data[14:], uint16(len(xorMask)))
|
||||
|
||||
return WrapFastPathUpdate(UpdateCodeNewPointer, slices.Concat(data, xorMask))
|
||||
}
|
||||
|
||||
// BuildPointerPositionPDU constructs a FastPath "Pointer Position" update.
|
||||
func BuildPointerPositionPDU(x, y int) []byte {
|
||||
data := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint16(data[0:], uint16(x))
|
||||
binary.LittleEndian.PutUint16(data[2:], uint16(y))
|
||||
|
||||
return WrapFastPathUpdate(UpdateCodePointerPos, data)
|
||||
}
|
||||
|
||||
// BuildPointerHiddenPDU constructs a FastPath "Pointer Hidden" update.
|
||||
func BuildPointerHiddenPDU() []byte {
|
||||
return WrapFastPathUpdate(UpdateCodePointerHidden, nil)
|
||||
}
|
||||
|
||||
// BuildPointerDefaultPDU constructs a FastPath "Pointer Default" update.
|
||||
func BuildPointerDefaultPDU() []byte {
|
||||
return WrapFastPathUpdate(UpdateCodePointerDefault, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user