From c018cd7deb9ec727dcf1592e9fdf4d5d74f38f1a Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Tue, 14 Jun 2022 18:42:19 -0400 Subject: [PATCH] TDP Shared Directory Announce and Acknowledge (#12405) Co-authored-by: Zac Bergquist --- Cargo.lock | 10 + lib/srv/desktop/rdp/rdpclient/Cargo.toml | 2 + lib/srv/desktop/rdp/rdpclient/client.go | 31 + lib/srv/desktop/rdp/rdpclient/librdprs.h | 37 +- lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs | 6 +- lib/srv/desktop/rdp/rdpclient/src/errors.rs | 8 + lib/srv/desktop/rdp/rdpclient/src/lib.rs | 105 +- .../desktop/rdp/rdpclient/src/rdpdr/consts.rs | 97 +- .../desktop/rdp/rdpclient/src/rdpdr/flags.rs | 200 +++ .../desktop/rdp/rdpclient/src/rdpdr/mod.rs | 1186 +++++++++++++++-- lib/srv/desktop/rdp/rdpclient/src/util.rs | 53 +- lib/srv/desktop/tdp/proto.go | 96 +- lib/web/desktop/playback.go | 2 - 13 files changed, 1658 insertions(+), 175 deletions(-) create mode 100644 lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs diff --git a/Cargo.lock b/Cargo.lock index 61d5f7d430603..3aedc13fbc538 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -980,6 +980,7 @@ dependencies = [ "rand_chacha 0.3.1", "rdp-rs", "rsa", + "utf16string", "uuid", ] @@ -1324,6 +1325,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "utf16string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b62a1e85e12d5d712bf47a85f426b73d303e2d00a90de5f3004df3596e9d216" +dependencies = [ + "byteorder", +] + [[package]] name = "uuid" version = "1.1.2" diff --git a/lib/srv/desktop/rdp/rdpclient/Cargo.toml b/lib/srv/desktop/rdp/rdpclient/Cargo.toml index a452e16b2da8c..2224b2ed336f2 100644 --- a/lib/srv/desktop/rdp/rdpclient/Cargo.toml +++ b/lib/srv/desktop/rdp/rdpclient/Cargo.toml @@ -22,3 +22,5 @@ rand_chacha = "0.3.1" rsa = "0.6.1" rdp-rs = { git = "https://github.com/gravitational/rdp-rs", rev = "17ec446ecb73c58b77ac47c6fc8598153f673076" } uuid = { version = "1.1.2", features = ["v4"] } +utf16string = "0.2.0" + diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go index 112e75d539c22..ffd27289ce85d 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -369,6 +369,18 @@ func (c *Client) start() { } else { c.cfg.Log.Warning("Recieved an empty clipboard message") } + case tdp.SharedDirectoryAnnounce: + if c.cfg.AllowDirectorySharing { + driveName := C.CString(m.Name) + defer C.free(unsafe.Pointer(driveName)) + if err := C.handle_tdp_sd_announce(c.rustClient, C.CGOSharedDirectoryAnnounce{ + directory_id: C.uint32_t(m.DirectoryID), + name: driveName, + }); err != C.ErrCodeSuccess { + c.cfg.Log.Errorf("Device announce failed: %v", err) + return + } + } default: c.cfg.Log.Warningf("Skipping unimplemented TDP message type %T", msg) } @@ -434,6 +446,25 @@ func (c *Client) handleRemoteCopy(data []byte) C.CGOErrCode { return C.ErrCodeSuccess } +//export tdp_sd_acknowledge +func tdp_sd_acknowledge(handle C.uintptr_t, ack *C.CGOSharedDirectoryAcknowledge) C.CGOErrCode { + return cgo.Handle(handle).Value().(*Client).sharedDirectoryAcknowledge(tdp.SharedDirectoryAcknowledge{ + Err: uint32(ack.err), + DirectoryID: uint32(ack.directory_id), + }) +} + +// sharedDirectoryAcknowledge acknowledges that a `Shared Directory Announce` TDP message was processed. +func (c *Client) sharedDirectoryAcknowledge(ack tdp.SharedDirectoryAcknowledge) C.CGOErrCode { + if c.cfg.AllowDirectorySharing { + if err := c.cfg.Conn.OutputMessage(ack); err != nil { + c.cfg.Log.Errorf("failed to send SharedDirectoryAcknowledge: %v", err) + return C.ErrCodeFailure + } + } + return C.ErrCodeSuccess +} + // Wait blocks until the client disconnects and runs the cleanup. func (c *Client) Wait() error { c.wg.Wait() diff --git a/lib/srv/desktop/rdp/rdpclient/librdprs.h b/lib/srv/desktop/rdp/rdpclient/librdprs.h index d4f71273a29da..31839b63065a9 100644 --- a/lib/srv/desktop/rdp/rdpclient/librdprs.h +++ b/lib/srv/desktop/rdp/rdpclient/librdprs.h @@ -7,18 +7,20 @@ #define SPECIAL_NO_RESPONSE 4294967295 +#define SCARD_DEVICE_ID 1 + #define VERSION_MAJOR 1 #define VERSION_MINOR 12 #define SMARTCARD_CAPABILITY_VERSION_01 1 +#define DRIVE_CAPABILITY_VERSION_02 2 + #define GENERAL_CAPABILITY_VERSION_01 1 #define GENERAL_CAPABILITY_VERSION_02 2 -#define SCARD_DEVICE_ID 1 - /** * The default maximum chunk size for virtual channel data. * @@ -68,6 +70,11 @@ typedef struct ClientOrError { enum CGOErrCode err; } ClientOrError; +typedef struct CGOSharedDirectoryAnnounce { + uint32_t directory_id; + char *name; +} CGOSharedDirectoryAnnounce; + /** * CGOMousePointerEvent is a CGO-compatible version of PointerEvent that we pass back to Go. * PointerEvent is a mouse move or click update from the user. @@ -107,6 +114,15 @@ typedef struct CGOBitmap { uintptr_t data_cap; } CGOBitmap; +/** + * CGOSharedDirectoryAcknowledge is a CGO-compatible version of + * the TDP Shared Directory Knowledge message that we pass back to Go. + */ +typedef struct CGOSharedDirectoryAcknowledge { + uint32_t err; + uint32_t directory_id; +} CGOSharedDirectoryAcknowledge; + void init(void); /** @@ -141,6 +157,17 @@ struct ClientOrError connect_rdp(uintptr_t go_ref, */ enum CGOErrCode update_clipboard(struct Client *client_ptr, uint8_t *data, uint32_t len); +/** + * handle_tdp_sd_announce announces a new drive that's ready to be + * redirected over RDP. + * + * # Safety + * + * The caller must ensure that sd_announce.name points to a valid buffer. + */ +enum CGOErrCode handle_tdp_sd_announce(struct Client *client_ptr, + struct CGOSharedDirectoryAnnounce sd_announce); + /** * `read_rdp_output` reads incoming RDP bitmap frames from client at client_ref and forwards them to * handle_bitmap. @@ -192,3 +219,9 @@ void free_rust_string(char *s); extern enum CGOErrCode handle_bitmap(uintptr_t client_ref, struct CGOBitmap *b); extern enum CGOErrCode handle_remote_copy(uintptr_t client_ref, uint8_t *data, uint32_t len); + +/** + * Shared Directory Acknowledge + */ +extern enum CGOErrCode tdp_sd_acknowledge(uintptr_t client_ref, + struct CGOSharedDirectoryAcknowledge *ack); diff --git a/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs b/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs index b1c1b5445e61f..338e3bd8d6068 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs @@ -564,7 +564,7 @@ fn encode_clipboard(mut data: String) -> (Vec, ClipboardFormat) { (data.into_bytes(), ClipboardFormat::CF_TEXT) } else { - let encoded = util::to_nul_terminated_utf16le(&data); + let encoded = util::to_unicode(&data, true); (encoded, ClipboardFormat::CF_UNICODETEXT) } } @@ -676,7 +676,7 @@ impl FormatName for LongFormatName { // must be encoded as a single Unicode null character (two zero bytes) None => w.write_u16::(0)?, Some(name) => { - w.append(&mut util::to_nul_terminated_utf16le(name)); + w.append(&mut util::to_unicode(name, true)); } }; @@ -1037,7 +1037,7 @@ mod tests { #[test] fn responds_to_format_data_request_hasdata() { // a null-terminated utf-16 string, represented as a Vec - let test_data = util::to_nul_terminated_utf16le("test"); + let test_data = util::to_unicode("test", true); let mut c: Client = Default::default(); c.clipboard diff --git a/lib/srv/desktop/rdp/rdpclient/src/errors.rs b/lib/srv/desktop/rdp/rdpclient/src/errors.rs index 7e1ec566a19bf..4965c90ad3ac6 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/errors.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/errors.rs @@ -19,10 +19,18 @@ pub fn invalid_data_error(msg: &str) -> Error { Error::RdpError(RdpError::new(RdpErrorKind::InvalidData, msg)) } +pub fn not_implemented_error(msg: &str) -> Error { + Error::RdpError(RdpError::new(RdpErrorKind::NotImplemented, msg)) +} + pub fn try_error(msg: &str) -> Error { Error::TryError(msg.to_string()) } +pub fn rejected_by_server_error(msg: &str) -> Error { + Error::RdpError(RdpError::new(RdpErrorKind::RejectedByServer, msg)) +} + // NTSTATUS_OK is a Windows NTStatus value that means "success". pub const NTSTATUS_OK: u32 = 0; // SPECIAL_NO_RESPONSE is our custom (not defined by Windows) NTStatus value that means "don't send diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index db7749c9bfc12..33c8496412fd0 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod cliprdr; -pub mod errors; -pub mod piv; -pub mod rdpdr; -pub mod util; -pub mod vchan; +mod cliprdr; +mod errors; +mod piv; +mod rdpdr; +mod util; +mod vchan; #[macro_use] extern crate log; @@ -251,12 +251,27 @@ fn connect_rdp_inner( KeyboardLayout::US, "rdp-rs", ); - // Client for the "rdpdr" channel - smartcard emulation. + + let tdp_sd_acknowledge = Box::new(move |ack: SharedDirectoryAcknowledge| -> RdpResult<()> { + unsafe { + if tdp_sd_acknowledge(go_ref, &mut CGOSharedDirectoryAcknowledge::from(ack)) + != CGOErrCode::ErrCodeSuccess + { + return Err(RdpError::TryError(String::from( + "call to tdp_sd_acknowledge failed", + ))); + } + } + Ok(()) + }); + + // Client for the "rdpdr" channel - smartcard emulation and drive redirection. let rdpdr = rdpdr::Client::new( params.cert_der, params.key_der, pin, params.allow_directory_sharing, + tdp_sd_acknowledge, ); // Client for the "cliprdr" channel - clipboard sharing. @@ -336,6 +351,14 @@ impl RdpClient { } } + pub fn write_client_device_list_announce( + &mut self, + req: rdpdr::ClientDeviceListAnnounce, + ) -> RdpResult<()> { + self.rdpdr + .write_client_device_list_announce(req, &mut self.mcs) + } + pub fn shutdown(&mut self) -> RdpResult<()> { self.mcs.shutdown() } @@ -458,6 +481,38 @@ pub unsafe extern "C" fn update_clipboard( } } +/// handle_tdp_sd_announce announces a new drive that's ready to be +/// redirected over RDP. +/// +/// # Safety +/// +/// The caller must ensure that sd_announce.name points to a valid buffer. +#[no_mangle] +pub unsafe extern "C" fn handle_tdp_sd_announce( + client_ptr: *mut Client, + sd_announce: CGOSharedDirectoryAnnounce, +) -> CGOErrCode { + let client = match Client::from_ptr(client_ptr) { + Ok(client) => client, + Err(cgo_error) => { + return cgo_error; + } + }; + + let drive_name = from_go_string(sd_announce.name); + let new_drive = + rdpdr::ClientDeviceListAnnounce::new_drive(sd_announce.directory_id, drive_name); + + let mut rdp_client = client.rdp_client.lock().unwrap(); + match rdp_client.write_client_device_list_announce(new_drive) { + Ok(()) => CGOErrCode::ErrCodeSuccess, + Err(e) => { + error!("failed to announce new drive: {:?}", e); + CGOErrCode::ErrCodeFailure + } + } +} + /// `read_rdp_output` reads incoming RDP bitmap frames from client at client_ref and forwards them to /// handle_bitmap. /// @@ -521,7 +576,7 @@ fn read_rdp_output_inner(client: &Client) -> Option { match res { Err(RdpError::Io(io_err)) if io_err.kind() == ErrorKind::UnexpectedEof => return None, Err(e) => { - return Some(format!("failed forwarding RDP bitmap frame: {:?}", e)); + return Some(format!("RDP read failed: {:?}", e)); } _ => {} } @@ -700,6 +755,8 @@ pub unsafe extern "C" fn free_rust_string(s: *mut c_char) { /// # Safety /// /// s must be a C-style null terminated string. +/// s is copied here, and the caller is responsible for ensuring +/// that the original memory is freed unsafe fn from_go_string(s: *mut c_char) -> String { CStr::from_ptr(s).to_string_lossy().into_owned() } @@ -718,11 +775,43 @@ pub enum CGOErrCode { ErrCodeFailure = 1, } +#[repr(C)] +pub struct CGOSharedDirectoryAnnounce { + pub directory_id: u32, + pub name: *mut c_char, +} + +pub struct SharedDirectoryAcknowledge { + pub err: u32, + pub directory_id: u32, +} + +/// CGOSharedDirectoryAcknowledge is a CGO-compatible version of +/// the TDP Shared Directory Knowledge message that we pass back to Go. +#[repr(C)] +pub struct CGOSharedDirectoryAcknowledge { + pub err: u32, + pub directory_id: u32, +} + +impl From for CGOSharedDirectoryAcknowledge { + fn from(ack: SharedDirectoryAcknowledge) -> CGOSharedDirectoryAcknowledge { + CGOSharedDirectoryAcknowledge { + err: ack.err, + directory_id: ack.directory_id, + } + } +} + // These functions are defined on the Go side. Look for functions with '//export funcname' // comments. extern "C" { fn handle_bitmap(client_ref: usize, b: *mut CGOBitmap) -> CGOErrCode; fn handle_remote_copy(client_ref: usize, data: *mut u8, len: u32) -> CGOErrCode; + + /// Shared Directory Acknowledge + fn tdp_sd_acknowledge(client_ref: usize, ack: *mut CGOSharedDirectoryAcknowledge) + -> CGOErrCode; } /// Payload is a generic type used to represent raw incoming RDP messages for parsing. diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs index 4a3bf5d7bb5e1..2f0b2903fdbd8 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs @@ -14,6 +14,19 @@ pub const CHANNEL_NAME: &str = "rdpdr"; +// Each redirected device requires a unique ID. We only share +// one permanent smartcard device, so we can give it hardcoded ID 1. +pub const SCARD_DEVICE_ID: u32 = 1; + +pub const VERSION_MAJOR: u16 = 0x0001; +pub const VERSION_MINOR: u16 = 0x000c; + +pub const SMARTCARD_CAPABILITY_VERSION_01: u32 = 0x00000001; +pub const DRIVE_CAPABILITY_VERSION_02: u32 = 0x00000002; +#[allow(dead_code)] +pub const GENERAL_CAPABILITY_VERSION_01: u32 = 0x00000001; +pub const GENERAL_CAPABILITY_VERSION_02: u32 = 0x00000002; + #[derive(Debug, FromPrimitive, ToPrimitive)] #[allow(non_camel_case_types)] pub enum Component { @@ -39,14 +52,6 @@ pub enum PacketId { PAKID_PRN_USING_XPS = 0x5543, } -pub const VERSION_MAJOR: u16 = 0x0001; -pub const VERSION_MINOR: u16 = 0x000c; - -pub const SMARTCARD_CAPABILITY_VERSION_01: u32 = 0x00000001; -#[allow(dead_code)] -pub const GENERAL_CAPABILITY_VERSION_01: u32 = 0x00000001; -pub const GENERAL_CAPABILITY_VERSION_02: u32 = 0x00000002; - #[derive(Debug, FromPrimitive, ToPrimitive)] #[allow(non_camel_case_types)] pub enum CapabilityType { @@ -57,10 +62,6 @@ pub enum CapabilityType { CAP_SMARTCARD_TYPE = 0x0005, } -// If there were multiple redirected devices, they would need unique IDs. In our case there is only -// one permanent smartcard device, so we hardcode an ID 1. -pub const SCARD_DEVICE_ID: u32 = 1; - #[derive(Debug, FromPrimitive, ToPrimitive)] #[allow(non_camel_case_types)] pub enum DeviceType { @@ -71,7 +72,8 @@ pub enum DeviceType { RDPDR_DTYP_SMARTCARD = 0x00000020, } -#[derive(Debug, FromPrimitive, ToPrimitive)] +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a087ffa8-d0d5-4874-ac7b-0494f63e2d5d +#[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, Clone)] #[allow(non_camel_case_types)] pub enum MajorFunction { IRP_MJ_CREATE = 0x00000000, @@ -94,3 +96,72 @@ pub enum MinorFunction { IRP_MN_QUERY_DIRECTORY = 0x00000001, IRP_MN_NOTIFY_CHANGE_DIRECTORY = 0x00000002, } + +/// Windows defines an absolutely massive list of potential NTSTATUS values. +/// This enum includes the basic ones we support for communicating with the windows machine. +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 +#[derive(ToPrimitive, Debug)] +#[repr(u32)] +#[allow(non_camel_case_types)] +#[allow(dead_code)] +pub enum NTSTATUS { + STATUS_SUCCESS = 0x00000000, + STATUS_UNSUCCESSFUL = 0xC0000001, + STATUS_NOT_IMPLEMENTED = 0xC0000002, + STATUS_NO_MORE_FILES = 0x80000006, +} + +/// 2.4 File Information Classes [MS-FSCC] +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4718fc40-e539-4014-8e33-b675af74e3e1 +#[derive(FromPrimitive, Debug, PartialEq)] +#[repr(u32)] +#[allow(clippy::enum_variant_names)] +pub enum FsInformationClassLevel { + FileAccessInformation = 8, + FileAlignmentInformation = 17, + FileAllInformation = 18, + FileAllocationInformation = 19, + FileAlternateNameInformation = 21, + FileAttributeTagInformation = 35, + FileBasicInformation = 4, + FileBothDirectoryInformation = 3, + FileCompressionInformation = 28, + FileDirectoryInformation = 1, + FileDispositionInformation = 13, + FileEaInformation = 7, + FileEndOfFileInformation = 20, + FileFullDirectoryInformation = 2, + FileFullEaInformation = 15, + FileHardLinkInformation = 46, + FileIdBothDirectoryInformation = 37, + FileIdExtdDirectoryInformation = 60, + FileIdFullDirectoryInformation = 38, + FileIdGlobalTxDirectoryInformation = 50, + FileIdInformation = 59, + FileInternalInformation = 6, + FileLinkInformation = 11, + FileMailslo = 26, + FileMailslotSetInformation = 27, + FileModeInformation = 16, + FileMoveClusterInformation = 31, + FileNameInformation = 9, + FileNamesInformation = 12, + FileNetworkOpenInformation = 34, + FileNormalizedNameInformation = 48, + FileObjectIdInformation = 29, + FilePipeInformation = 23, + FilePipInformation = 24, + FilePipeRemoteInformation = 25, + FilePositionInformation = 14, + FileQuotaInformation = 32, + FileRenameInformation = 10, + FileReparsePointInformation = 33, + FileSfioReserveInformation = 44, + FileSfioVolumeInformation = 45, + FileShortNameInformation = 40, + FileStandardInformation = 5, + FileStandardLinkInformation = 54, + FileStreamInformation = 22, + FileTrackingInformation = 36, + FileValidDataLengthInformation = 39, +} diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs new file mode 100644 index 0000000000000..6bff374d8da2e --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs @@ -0,0 +1,200 @@ +// Copyright 2022 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bitflags::bitflags; + +bitflags! { + /// DesiredAccess can be interpreted as either + /// 2.2.13.1.1 File_Pipe_Printer_Access_Mask [MS-SMB2] (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/77b36d0f-6016-458a-a7a0-0f4a72ae1534) + /// or + /// 2.2.13.1.2 Directory_Access_Mask [MS-SMB2] (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/0a5934b1-80f1-4da0-b1bf-5e021c309b71) + /// + /// This implements the combination of the two. For flags where the names and/or functions are distinct between the two, + /// the names are appended with an "_OR_", and the File_Pipe_Printer_Access_Mask functionality is described on the top line comment, + /// and the Directory_Access_Mask functionality is described on the bottom (2nd) line comment. + pub struct DesiredAccess: u32 { + /// This value indicates the right to read data from the file or named pipe. + /// This value indicates the right to enumerate the contents of the directory. + const FILE_READ_DATA_OR_FILE_LIST_DIRECTORY = 0x00000001; + /// This value indicates the right to write data into the file or named pipe beyond the end of the file. + /// This value indicates the right to create a file under the directory. + const FILE_WRITE_DATA_OR_FILE_ADD_FILE = 0x00000002; + /// This value indicates the right to append data into the file or named pipe. + /// This value indicates the right to add a sub-directory under the directory. + const FILE_APPEND_DATA_OR_FILE_ADD_SUBDIRECTORY = 0x00000004; + /// This value indicates the right to read the extended attributes of the file or named pipe. + const FILE_READ_EA = 0x00000008; + /// This value indicates the right to write or change the extended attributes to the file or named pipe. + const FILE_WRITE_EA = 0x00000010; + /// This value indicates the right to traverse this directory if the server enforces traversal checking. + const FILE_TRAVERSE = 0x00000020; + /// This value indicates the right to delete entries within a directory. + const FILE_DELETE_CHILD = 0x00000040; + /// This value indicates the right to execute the file/directory. + const FILE_EXECUTE = 0x00000020; + /// This value indicates the right to read the attributes of the file/directory. + const FILE_READ_ATTRIBUTES = 0x00000080; + /// This value indicates the right to change the attributes of the file/directory. + const FILE_WRITE_ATTRIBUTES = 0x00000100; + /// This value indicates the right to delete the file/directory. + const DELETE = 0x00010000; + /// This value indicates the right to read the security descriptor for the file/directory or named pipe. + const READ_CONTROL = 0x00020000; + /// This value indicates the right to change the discretionary access control list (DACL) in the security descriptor for the file/directory or named pipe. For the DACL data pub structure, see ACL in [MS-DTYP]. + const WRITE_DAC = 0x00040000; + /// This value indicates the right to change the owner in the security descriptor for the file/directory or named pipe. + const WRITE_OWNER = 0x00080000; + /// SMB2 clients set this flag to any value. SMB2 servers SHOULD ignore this flag. + const SYNCHRONIZE = 0x00100000; + /// This value indicates the right to read or change the system access control list (SACL) in the security descriptor for the file/directory or named pipe. For the SACL data pub structure, see ACL in [MS-DTYP]. + const ACCESS_SYSTEM_SECURITY = 0x01000000; + /// This value indicates that the client is requesting an open to the file with the highest level of access the client has on this file. If no access is granted for the client on this file, the server MUST fail the open with STATUS_ACCESS_DENIED. + const MAXIMUM_ALLOWED = 0x02000000; + /// This value indicates a request for all the access flags that are previously listed except MAXIMUM_ALLOWED and ACCESS_SYSTEM_SECURITY. + const GENERIC_ALL = 0x10000000; + /// This value indicates a request for the following combination of access flags listed above: FILE_READ_ATTRIBUTES| FILE_EXECUTE| SYNCHRONIZE| READ_CONTROL. + const GENERIC_EXECUTE = 0x20000000; + /// This value indicates a request for the following combination of access flags listed above: FILE_WRITE_DATA| FILE_APPEND_DATA| FILE_WRITE_ATTRIBUTES| FILE_WRITE_EA| SYNCHRONIZE| READ_CONTROL. + const GENERIC_WRITE = 0x40000000; + /// This value indicates a request for the following combination of access flags listed above: FILE_READ_DATA| FILE_READ_ATTRIBUTES| FILE_READ_EA| SYNCHRONIZE| READ_CONTROL. + const GENERIC_READ = 0x80000000; + } +} + +bitflags! { + /// 2.6 File Attributes [MS-FSCC] + /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ca28ec38-f155-4768-81d6-4bfeb8586fc9 + pub struct FileAttributes: u32 { + const FILE_ATTRIBUTE_READONLY = 0x00000001; + const FILE_ATTRIBUTE_HIDDEN = 0x00000002; + const FILE_ATTRIBUTE_SYSTEM = 0x00000004; + const FILE_ATTRIBUTE_DIRECTORY = 0x00000010; + const FILE_ATTRIBUTE_ARCHIVE = 0x00000020; + const FILE_ATTRIBUTE_NORMAL = 0x00000080; + const FILE_ATTRIBUTE_TEMPORARY = 0x00000100; + const FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200; + const FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400; + const FILE_ATTRIBUTE_COMPRESSED = 0x00000800; + const FILE_ATTRIBUTE_OFFLINE = 0x00001000; + const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000; + const FILE_ATTRIBUTE_ENCRYPTED = 0x00004000; + const FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000; + const FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000; + const FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x00040000; + const FILE_ATTRIBUTE_PINNED = 0x00080000; + const FILE_ATTRIBUTE_UNPINNED = 0x00100000; + const FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 0x00400000; + } +} + +bitflags! { + /// Specifies the sharing mode for the open. If ShareAccess values of FILE_SHARE_READ, FILE_SHARE_WRITE and FILE_SHARE_DELETE are set for a printer file or a named pipe, the server SHOULD<35> ignore these values. The field MUST be pub constructed using a combination of zero or more of the following bit values. + /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e8fb45c1-a03d-44ca-b7ae-47385cfd7997 + pub struct SharedAccess: u32 { + const FILE_SHARE_READ = 0x00000001; + const FILE_SHARE_WRITE = 0x00000002; + const FILE_SHARE_DELETE = 0x00000004; + } +} + +bitflags! { + /// Defines the action the server MUST take if the file that is specified in the name field already exists. For opening named pipes, this field can be set to any value by the client and MUST be ignored by the server. For other files, this field MUST contain one of the following values. + /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e8fb45c1-a03d-44ca-b7ae-47385cfd7997 + /// See https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L207 + /// for information about how these should be interpreted. + pub struct CreateDisposition: u32 { + const FILE_SUPERSEDE = 0x00000000; + const FILE_OPEN = 0x00000001; + const FILE_CREATE = 0x00000002; + const FILE_OPEN_IF = 0x00000003; + const FILE_OVERWRITE = 0x00000004; + const FILE_OVERWRITE_IF = 0x00000005; + } +} + +bitflags! { + /// Specifies the options to be applied when creating or opening the file. Combinations of the bit positions listed below are valid, unless otherwise noted. This field MUST be pub constructed using the following values. + /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e8fb45c1-a03d-44ca-b7ae-47385cfd7997 + pub struct CreateOptions: u32 { + const FILE_DIRECTORY_FILE = 0x00000001; + const FILE_WRITE_THROUGH = 0x00000002; + const FILE_SEQUENTIAL_ONLY = 0x00000004; + const FILE_NO_INTERMEDIATE_BUFFERING = 0x00000008; + const FILE_SYNCHRONOUS_IO_ALERT = 0x00000010; + const FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020; + const FILE_NON_DIRECTORY_FILE = 0x00000040; + const FILE_COMPLETE_IF_OPLOCKED = 0x00000100; + const FILE_NO_EA_KNOWLEDGE = 0x00000200; + const FILE_RANDOM_ACCESS = 0x00000800; + const FILE_DELETE_ON_CLOSE = 0x00001000; + const FILE_OPEN_BY_FILE_ID = 0x00002000; + const FILE_OPEN_FOR_BACKUP_INTENT = 0x00004000; + const FILE_NO_COMPRESSION = 0x00008000; + const FILE_OPEN_REMOTE_INSTANCE = 0x00000400; + const FILE_OPEN_REQUIRING_OPLOCK = 0x00010000; + const FILE_DISALLOW_EXCLUSIVE = 0x00020000; + const FILE_RESERVE_OPFILTER = 0x00100000; + const FILE_OPEN_REPARSE_POINT = 0x00200000; + const FILE_OPEN_NO_RECALL = 0x00400000; + const FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000; + } +} + +bitflags! { + /// An unsigned 8-bit integer. This field indicates the success of the Device Create Request (section 2.2.1.4.1). + /// The value of the Information field depends on the value of CreateDisposition field in the Device Create Request + /// (section 2.2.1.4.1). If the IoStatus field is set to 0x00000000, this field MAY be skipped, in which case the + /// server MUST assume that the Information field is set to 0x00. + /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/99e5fca5-b37a-41e4-bc69-8d7da7860f76 + pub struct Information: u8 { + /// A new file was created. + const FILE_SUPERSEDED = 0x00000000; + /// An existing file was opened. + const FILE_OPENED = 0x00000001; + /// An existing file was overwritten. + const FILE_OVERWRITTEN = 0x00000003; + } +} + +bitflags! { + /// Specifies the types of changes to monitor. It is valid to choose multiple trigger conditions. + /// In this case, if any condition is met, the client is notified of the change and the CHANGE_NOTIFY operation is completed. + /// See CompletionFilter at: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/598f395a-e7a2-4cc8-afb3-ccb30dd2df7c + pub struct CompletionFilter: u32 { + /// The client is notified if a file-name changes. + const FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001; + /// The client is notified if a directory name changes. + const FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002; + /// The client is notified if a file's attributes change. Possible file attribute values are specified in [MS-FSCC] section 2.6. + const FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004; + /// The client is notified if a file's size changes. + const FILE_NOTIFY_CHANGE_SIZE = 0x00000008; + /// The client is notified if the last write time of a file changes. + const FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010; + /// The client is notified if the last access time of a file changes. + const FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020; + /// The client is notified if the creation time of a file changes. + const FILE_NOTIFY_CHANGE_CREATION = 0x00000040; + /// The client is notified if a file's extended attributes (EAs) change. + const FILE_NOTIFY_CHANGE_EA = 0x00000080; + /// The client is notified of a file's access control list (ACL) settings change. + const FILE_NOTIFY_CHANGE_SECURITY = 0x00000100; + /// The client is notified if a named stream is added to a file. + const FILE_NOTIFY_CHANGE_STREAM_NAME = 0x00000200; + /// The client is notified if the size of a named stream is changed. + const FILE_NOTIFY_CHANGE_STREAM_SIZE = 0x00000400; + /// The client is notified if a named stream is modified. + const FILE_NOTIFY_CHANGE_STREAM_WRITE = 0x00000800; + } +} diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs index c667141e10345..dee3923369b42 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs @@ -13,17 +13,30 @@ // limitations under the License. mod consts; +mod flags; mod scard; -use crate::errors::{invalid_data_error, NTSTATUS_OK, SPECIAL_NO_RESPONSE}; +use crate::errors::{ + invalid_data_error, not_implemented_error, rejected_by_server_error, NTSTATUS_OK, + SPECIAL_NO_RESPONSE, +}; +use crate::util; use crate::vchan; -use crate::Payload; +use crate::{Payload, SharedDirectoryAcknowledge}; + use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use consts::{ + CapabilityType, Component, DeviceType, FsInformationClassLevel, MajorFunction, MinorFunction, + PacketId, DRIVE_CAPABILITY_VERSION_02, GENERAL_CAPABILITY_VERSION_02, NTSTATUS, + SCARD_DEVICE_ID, SMARTCARD_CAPABILITY_VERSION_01, VERSION_MAJOR, VERSION_MINOR, +}; use num_traits::{FromPrimitive, ToPrimitive}; use rdp::core::mcs; use rdp::core::tpkt; use rdp::model::data::Message; +use rdp::model::error::Error as RdpError; use rdp::model::error::*; +use std::convert::{TryFrom, TryInto}; use std::io::{Read, Write}; pub use consts::CHANNEL_NAME; @@ -32,12 +45,15 @@ pub use consts::CHANNEL_NAME; /// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPEFS/%5bMS-RDPEFS%5d.pdf /// /// This client only supports a single smartcard device. -#[allow(dead_code)] pub struct Client { vchan: vchan::Client, scard: scard::Client, allow_directory_sharing: bool, + active_device_ids: Vec, + + // Functions for sending tdp messages to the browser client. + tdp_sd_acknowledge: Box RdpResult<()>>, } impl Client { @@ -46,6 +62,8 @@ impl Client { key_der: Vec, pin: String, allow_directory_sharing: bool, + + tdp_sd_acknowledge: Box RdpResult<()>>, ) -> Self { if allow_directory_sharing { debug!("creating rdpdr client with directory sharing enabled") @@ -55,8 +73,9 @@ impl Client { Client { vchan: vchan::Client::new(), scard: scard::Client::new(cert_der, key_der, pin), - + active_device_ids: vec![], allow_directory_sharing, + tdp_sd_acknowledge, } } /// Reads raw RDP messages sent on the rdpdr virtual channel and replies as necessary. @@ -67,31 +86,28 @@ impl Client { ) -> RdpResult<()> { if let Some(mut payload) = self.vchan.read(payload)? { let header = SharedHeader::decode(&mut payload)?; - if let consts::Component::RDPDR_CTYP_PRN = header.component { + if let Component::RDPDR_CTYP_PRN = header.component { warn!("got {:?} RDPDR header from RDP server, ignoring because we're not redirecting any printers", header); return Ok(()); } let responses = match header.packet_id { - consts::PacketId::PAKID_CORE_SERVER_ANNOUNCE => { + PacketId::PAKID_CORE_SERVER_ANNOUNCE => { self.handle_server_announce(&mut payload)? } - consts::PacketId::PAKID_CORE_SERVER_CAPABILITY => { + PacketId::PAKID_CORE_SERVER_CAPABILITY => { self.handle_server_capability(&mut payload)? } - consts::PacketId::PAKID_CORE_CLIENTID_CONFIRM => { + PacketId::PAKID_CORE_CLIENTID_CONFIRM => { self.handle_client_id_confirm(&mut payload)? } - consts::PacketId::PAKID_CORE_DEVICE_REPLY => { - self.handle_device_reply(&mut payload)? - } - // Device IO request is where communication with the smartcard actually happens. - // Everything up to this point was negotiation and smartcard device registration. - consts::PacketId::PAKID_CORE_DEVICE_IOREQUEST => { + PacketId::PAKID_CORE_DEVICE_REPLY => self.handle_device_reply(&mut payload)?, + // Device IO request is where communication with the smartcard and shared drive actually happens. + // Everything up to this point was negotiation (and smartcard device registration). + PacketId::PAKID_CORE_DEVICE_IOREQUEST => { self.handle_device_io_request(&mut payload)? } _ => { - // We don't implement the full set of messages. Only the ones necessary for initial - // negotiation and registration of a smartcard device. + // We don't implement the full set of messages. error!( "RDPDR packets {:?} are not implemented yet, ignoring", header.packet_id @@ -113,7 +129,7 @@ impl Client { debug!("got ServerAnnounceRequest {:?}", req); let resp = self.add_headers_and_chunkify( - consts::PacketId::PAKID_CORE_CLIENTID_CONFIRM, + PacketId::PAKID_CORE_CLIENTID_CONFIRM, ClientAnnounceReply::new(req).encode()?, )?; debug!("sending client announce reply"); @@ -125,83 +141,185 @@ impl Client { debug!("got {:?}", req); let resp = self.add_headers_and_chunkify( - consts::PacketId::PAKID_CORE_CLIENT_CAPABILITY, - ClientCoreCapabilityResponse::new_response().encode()?, + PacketId::PAKID_CORE_CLIENT_CAPABILITY, + ClientCoreCapabilityResponse::new_response(self.allow_directory_sharing).encode()?, )?; debug!("sending client core capability response"); Ok(resp) } - fn handle_client_id_confirm(&self, payload: &mut Payload) -> RdpResult>> { + fn handle_client_id_confirm(&mut self, payload: &mut Payload) -> RdpResult>> { let req = ServerClientIdConfirm::decode(payload)?; debug!("got ServerClientIdConfirm {:?}", req); - let resp = self.add_headers_and_chunkify( - consts::PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, - ClientDeviceListAnnounceRequest::new_smartcard().encode()?, - )?; - debug!("sending client device list announce request"); + // The smartcard initialization sequence that contains this message happens once at session startup, + // and once when login succeeds. We only need to announce the smartcard once. + let resp = if !self.active_device_ids.contains(&SCARD_DEVICE_ID) { + self.push_active_device_id(SCARD_DEVICE_ID)?; + self.add_headers_and_chunkify( + PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, + ClientDeviceListAnnounceRequest::new_smartcard(SCARD_DEVICE_ID).encode()?, + )? + } else { + self.add_headers_and_chunkify( + PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, + ClientDeviceListAnnounceRequest::new_empty().encode()?, + )? + }; + debug!("replying with: {:?}", resp); Ok(resp) } fn handle_device_reply(&self, payload: &mut Payload) -> RdpResult>> { let req = ServerDeviceAnnounceResponse::decode(payload)?; - debug!("got {:?}", req); + debug!("got ServerDeviceAnnounceResponse: {:?}", req); + + if self.active_device_ids.contains(&req.device_id) { + if req.device_id != self.get_scard_device_id()? { + // This was for a directory we're sharing over TDP + let mut err: u32 = 0; + if req.result_code != NTSTATUS_OK { + err = 1; + debug!("ServerDeviceAnnounceResponse for smartcard redirection failed with result code NTSTATUS({})", &req.result_code); + } else { + debug!("ServerDeviceAnnounceResponse for shared directory succeeded") + } - if req.device_id != consts::SCARD_DEVICE_ID { - Err(invalid_data_error(&format!( + (self.tdp_sd_acknowledge)(SharedDirectoryAcknowledge { + err, + directory_id: req.device_id, + })?; + } else { + // This was for the smart card + if req.result_code != NTSTATUS_OK { + // End the session, we cannot continue without + // the smart card being redirected. + return Err(rejected_by_server_error(&format!( + "ServerDeviceAnnounceResponse for smartcard redirection failed with result code NTSTATUS({})", + &req.result_code + ))); + } + debug!("ServerDeviceAnnounceResponse for smartcard redirection succeeded"); + } + } else { + return Err(invalid_data_error(&format!( "got ServerDeviceAnnounceResponse for unknown device_id {}", &req.device_id - ))) - } else if req.result_code != NTSTATUS_OK { - Err(invalid_data_error(&format!( - "got unsuccessful ServerDeviceAnnounceResponse result code NTSTATUS({})", - &req.result_code - ))) - } else { - Ok(vec![]) + ))); } + Ok(vec![]) } fn handle_device_io_request(&mut self, payload: &mut Payload) -> RdpResult>> { - let req = DeviceIoRequest::decode(payload)?; - debug!("got {:?}", req); + let device_io_request = DeviceIoRequest::decode(payload)?; + let major_function = device_io_request.major_function.clone(); - if let consts::MajorFunction::IRP_MJ_DEVICE_CONTROL = req.major_function { - let ioctl = DeviceControlRequest::decode(req, payload)?; - debug!("got {:?}", ioctl); + // Smartcard control only uses IRP_MJ_DEVICE_CONTROL; directory control uses IRP_MJ_DEVICE_CONTROL along with + // all the other MajorFunctions supported by this Client. Therefore if we receive any major function when drive + // redirection is not allowed, something has gone wrong. In such a case, we return an error as a security measure + // to ensure directories are never shared when RBAC doesn't permit it. + if major_function != MajorFunction::IRP_MJ_DEVICE_CONTROL && !self.allow_directory_sharing { + return Err(Error::TryError( + "received a drive redirection major function when drive redirection was not allowed" + .to_string(), + )); + } + + match major_function { + MajorFunction::IRP_MJ_DEVICE_CONTROL => { + let ioctl = DeviceControlRequest::decode(device_io_request, payload)?; + let is_smart_card_op = ioctl.header.device_id == self.get_scard_device_id()?; + debug!("got: {:?}", ioctl); - let (code, res) = self.scard.ioctl(ioctl.io_control_code, payload)?; - if code == SPECIAL_NO_RESPONSE { - return Ok(vec![]); + // IRP_MJ_DEVICE_CONTROL is the one major function used by both the smartcard controller (always enabled) + // and shared directory controller (potentially disabled by RBAC). Here we check that directory sharing + // is enabled here before proceeding with any shared directory controls as an additional security measure. + if !is_smart_card_op && !self.allow_directory_sharing { + return Err(Error::TryError("received a drive redirection major function when drive redirection was not allowed".to_string())); + } + let resp = if is_smart_card_op { + // Smart card control + let (code, res) = self.scard.ioctl(ioctl.io_control_code, payload)?; + if code == SPECIAL_NO_RESPONSE { + return Ok(vec![]); + } + DeviceControlResponse::new(&ioctl, code, res) + } else { + // Drive redirection, mimic FreeRDP's "no-op" + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L677-L684 + DeviceControlResponse::new( + &ioctl, + NTSTATUS::STATUS_SUCCESS.to_u32().unwrap(), + vec![], + ) + }; + debug!("replying with: {:?}", resp); + let resp = self.add_headers_and_chunkify( + PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, + resp.encode()?, + )?; + debug!("sending device IO response"); + Ok(resp) } - let resp = self.add_headers_and_chunkify( - consts::PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, - DeviceControlResponse::new(&ioctl, code, res).encode()?, - )?; - debug!("sending device IO response"); - Ok(resp) - } else { - Err(invalid_data_error(&format!( + _ => Err(invalid_data_error(&format!( + // TODO(isaiah): send back a not implemented response(?) "got unsupported major_function in DeviceIoRequest: {:?}", - &req.major_function - ))) + &major_function + ))), } } + /// This is called from Go (in effect) to announce a new directory + /// for sharing. + pub fn write_client_device_list_announce( + &mut self, + req: ClientDeviceListAnnounce, + mcs: &mut mcs::Client, + ) -> RdpResult<()> { + self.push_active_device_id(req.device_list[0].device_id)?; + debug!("sending new drive for redirection: {:?}", req); + + let responses = + self.add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, req.encode()?)?; + let chan = &CHANNEL_NAME.to_string(); + for resp in responses { + mcs.write(chan, resp)?; + } + + Ok(()) + } + /// add_headers_and_chunkify takes an encoded PDU ready to be sent over a virtual channel (payload), /// adds on the Shared Header based the passed packet_id, adds the appropriate (virtual) Channel PDU Header, /// and splits the entire payload into chunks if the payload exceeds the maximum size. fn add_headers_and_chunkify( &self, - packet_id: consts::PacketId, + packet_id: PacketId, payload: Vec, ) -> RdpResult>> { - let mut inner = - SharedHeader::new(consts::Component::RDPDR_CTYP_CORE, packet_id).encode()?; + let mut inner = SharedHeader::new(Component::RDPDR_CTYP_CORE, packet_id).encode()?; inner.extend_from_slice(&payload); self.vchan.add_header_and_chunkify(None, inner) } + + fn push_active_device_id(&mut self, device_id: u32) -> RdpResult<()> { + if self.active_device_ids.contains(&device_id) { + return Err(RdpError::TryError(format!( + "attempted to add a duplicate device_id {} to active_device_ids {:?}", + device_id, self.active_device_ids + ))); + } + self.active_device_ids.push(device_id); + Ok(()) + } + + fn get_scard_device_id(&self) -> RdpResult { + // We always push it into the list first + if !self.active_device_ids.is_empty() { + return Ok(self.active_device_ids[0]); + } + Err(RdpError::TryError("no active device ids".to_string())) + } } /// 2.2.1.1 Shared Header (RDPDR_HEADER) @@ -210,12 +328,12 @@ impl Client { /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/29d4108f-8163-4a67-8271-e48c4b9c2a7c #[derive(Debug)] struct SharedHeader { - component: consts::Component, - packet_id: consts::PacketId, + component: Component, + packet_id: PacketId, } impl SharedHeader { - fn new(component: consts::Component, packet_id: consts::PacketId) -> Self { + fn new(component: Component, packet_id: PacketId) -> Self { Self { component, packet_id, @@ -225,10 +343,10 @@ impl SharedHeader { let component = payload.read_u16::()?; let packet_id = payload.read_u16::()?; Ok(Self { - component: consts::Component::from_u16(component).ok_or_else(|| { + component: Component::from_u16(component).ok_or_else(|| { invalid_data_error(&format!("invalid component value {:#06x}", component)) })?, - packet_id: consts::PacketId::from_u16(packet_id).ok_or_else(|| { + packet_id: PacketId::from_u16(packet_id).ok_or_else(|| { invalid_data_error(&format!("invalid packet_id value {:#06x}", packet_id)) })?, }) @@ -255,8 +373,8 @@ struct ClientIdMessage { impl ClientIdMessage { fn new(req: ServerAnnounceRequest) -> Self { Self { - version_major: consts::VERSION_MAJOR, - version_minor: consts::VERSION_MINOR, + version_major: VERSION_MAJOR, + version_minor: VERSION_MINOR, client_id: req.client_id, } } @@ -286,41 +404,55 @@ struct ServerCoreCapabilityRequest { } impl ServerCoreCapabilityRequest { - fn new_response() -> Self { + fn new_response(allow_directory_sharing: bool) -> Self { // Clients are always required to send the "general" capability set. - // In addition, we also send the optional smartcard capability. - Self { - num_capabilities: 2, - padding: 0, - capabilities: vec![ - CapabilitySet { - header: CapabilityHeader { - cap_type: consts::CapabilityType::CAP_GENERAL_TYPE, - length: 8 + 36, // 8 byte header + 36 byte capability descriptor - version: consts::GENERAL_CAPABILITY_VERSION_02, - }, - data: Capability::General(GeneralCapabilitySet { - os_type: 0, - os_version: 0, - protocol_major_version: consts::VERSION_MAJOR, - protocol_minor_version: consts::VERSION_MINOR, - io_code_1: 0x00007fff, // Combination of all the required bits. - io_code_2: 0, - extended_pdu: 0x00000001 | 0x00000002, // RDPDR_DEVICE_REMOVE_PDUS | RDPDR_CLIENT_DISPLAY_NAME_PDU - extra_flags_1: 0, - extra_flags_2: 0, - special_type_device_cap: 1, // Request redirection of 1 special device - smartcard. - }), + // In addition, we also send the optional smartcard capability (CAP_SMARTCARD_TYPE) + // and drive capability (CAP_DRIVE_TYPE). + let mut capabilities = vec![ + CapabilitySet { + header: CapabilityHeader { + cap_type: CapabilityType::CAP_GENERAL_TYPE, + length: 8 + 36, // 8 byte header + 36 byte capability descriptor + version: GENERAL_CAPABILITY_VERSION_02, }, - CapabilitySet { - header: CapabilityHeader { - cap_type: consts::CapabilityType::CAP_SMARTCARD_TYPE, - length: 8, // 8 byte header + empty capability descriptor - version: consts::SMARTCARD_CAPABILITY_VERSION_01, - }, - data: Capability::Smartcard, + data: Capability::General(GeneralCapabilitySet { + os_type: 0, + os_version: 0, + protocol_major_version: VERSION_MAJOR, + protocol_minor_version: VERSION_MINOR, + io_code_1: 0x00007fff, // Combination of all the required bits. + io_code_2: 0, + extended_pdu: 0x00000001 | 0x00000002, // RDPDR_DEVICE_REMOVE_PDUS | RDPDR_CLIENT_DISPLAY_NAME_PDU + extra_flags_1: 0, + extra_flags_2: 0, + special_type_device_cap: 1, // Request redirection of 1 special device - smartcard. + }), + }, + CapabilitySet { + header: CapabilityHeader { + cap_type: CapabilityType::CAP_SMARTCARD_TYPE, + length: 8, // 8 byte header + empty capability descriptor + version: SMARTCARD_CAPABILITY_VERSION_01, + }, + data: Capability::Smartcard, + }, + ]; + + if allow_directory_sharing { + capabilities.push(CapabilitySet { + header: CapabilityHeader { + cap_type: CapabilityType::CAP_DRIVE_TYPE, + length: 8, // 8 byte header + empty capability descriptor + version: DRIVE_CAPABILITY_VERSION_02, }, - ], + data: Capability::Drive, + }); + } + + Self { + padding: 0, + num_capabilities: capabilities.len() as u16, + capabilities, } } @@ -372,7 +504,7 @@ impl CapabilitySet { #[derive(Debug)] struct CapabilityHeader { - cap_type: consts::CapabilityType, + cap_type: CapabilityType, length: u16, version: u32, } @@ -388,7 +520,7 @@ impl CapabilityHeader { fn decode(payload: &mut Payload) -> RdpResult { let cap_type = payload.read_u16::()?; Ok(Self { - cap_type: consts::CapabilityType::from_u16(cap_type).ok_or_else(|| { + cap_type: CapabilityType::from_u16(cap_type).ok_or_else(|| { invalid_data_error(&format!("invalid capability type {:#06x}", cap_type)) })?, length: payload.read_u16::()?, @@ -416,13 +548,13 @@ impl Capability { fn decode(payload: &mut Payload, header: &CapabilityHeader) -> RdpResult { match header.cap_type { - consts::CapabilityType::CAP_GENERAL_TYPE => Ok(Capability::General( + CapabilityType::CAP_GENERAL_TYPE => Ok(Capability::General( GeneralCapabilitySet::decode(payload, header.version)?, )), - consts::CapabilityType::CAP_PRINTER_TYPE => Ok(Capability::Printer), - consts::CapabilityType::CAP_PORT_TYPE => Ok(Capability::Port), - consts::CapabilityType::CAP_DRIVE_TYPE => Ok(Capability::Drive), - consts::CapabilityType::CAP_SMARTCARD_TYPE => Ok(Capability::Smartcard), + CapabilityType::CAP_PRINTER_TYPE => Ok(Capability::Printer), + CapabilityType::CAP_PORT_TYPE => Ok(Capability::Port), + CapabilityType::CAP_DRIVE_TYPE => Ok(Capability::Drive), + CapabilityType::CAP_SMARTCARD_TYPE => Ok(Capability::Smartcard), } } } @@ -468,7 +600,7 @@ impl GeneralCapabilitySet { extended_pdu: payload.read_u32::()?, extra_flags_1: payload.read_u32::()?, extra_flags_2: payload.read_u32::()?, - special_type_device_cap: if version == consts::GENERAL_CAPABILITY_VERSION_02 { + special_type_device_cap: if version == GENERAL_CAPABILITY_VERSION_02 { payload.read_u32::()? } else { 0 @@ -480,18 +612,22 @@ impl GeneralCapabilitySet { type ClientCoreCapabilityResponse = ServerCoreCapabilityRequest; #[derive(Debug)] -struct ClientDeviceListAnnounceRequest { - count: u32, - devices: Vec, +pub struct ClientDeviceListAnnounceRequest { + device_count: u32, + device_list: Vec, } +pub type ClientDeviceListAnnounce = ClientDeviceListAnnounceRequest; + impl ClientDeviceListAnnounceRequest { - fn new_smartcard() -> Self { + // We only need to announce the smartcard in this Client Device List Announce Request. + // Drives (directories) can be announced at any time with a Client Drive Device List Announce. + fn new_smartcard(device_id: u32) -> Self { Self { - count: 1, - devices: vec![DeviceAnnounceHeader { - device_type: consts::DeviceType::RDPDR_DTYP_SMARTCARD, - device_id: consts::SCARD_DEVICE_ID, + device_count: 1, + device_list: vec![DeviceAnnounceHeader { + device_type: DeviceType::RDPDR_DTYP_SMARTCARD, + device_id, // This name is a constant defined by the spec. preferred_dos_name: "SCARD".to_string(), device_data_length: 0, @@ -500,19 +636,55 @@ impl ClientDeviceListAnnounceRequest { } } + /// Creates a ClientDeviceListAnnounceRequest for announcing a new shared drive (directory). + /// A new drive can be announced at any time during RDP's operation. It is up to the caller + /// to ensure that the passed device_id is unique from that of any previously shared devices. + pub fn new_drive(device_id: u32, drive_name: String) -> Self { + // According to the spec: + // + // If the client supports DRIVE_CAPABILITY_VERSION_02 in the Drive Capability Set, + // then the full name MUST also be specified in the DeviceData field, as a null-terminated + // Unicode string. If the DeviceDataLength field is nonzero, the content of the + // PreferredDosName field is ignored. + // + // In the RDP spec, Unicode typically means null-terminated UTF-16LE, however empirically it + // appears that this field expects null-terminated UTF-8. + let device_data = util::to_utf8(&drive_name); + + Self { + device_count: 1, + device_list: vec![DeviceAnnounceHeader { + device_type: DeviceType::RDPDR_DTYP_FILESYSTEM, + device_id, + preferred_dos_name: drive_name, + device_data_length: device_data.len() as u32, + device_data, + }], + } + } + + fn new_empty() -> Self { + Self { + device_count: 0, + device_list: vec![], + } + } + fn encode(&self) -> RdpResult> { let mut w = vec![]; - w.write_u32::(self.count)?; - for dev in self.devices.iter() { + w.write_u32::(self.device_count)?; + for dev in self.device_list.iter() { w.extend_from_slice(&dev.encode()?); } Ok(w) } } +/// 2.2.1.3 Device Announce Header (DEVICE_ANNOUNCE) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/32e34332-774b-4ead-8c9d-5d64720d6bf9 #[derive(Debug)] struct DeviceAnnounceHeader { - device_type: consts::DeviceType, + device_type: DeviceType, device_id: u32, preferred_dos_name: String, device_data_length: u32, @@ -525,8 +697,10 @@ impl DeviceAnnounceHeader { w.write_u32::(self.device_type.to_u32().unwrap())?; w.write_u32::(self.device_id)?; let mut name: &str = &self.preferred_dos_name; - if name.len() > 8 { - name = &name[..8]; + // See "PreferredDosName" at + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/32e34332-774b-4ead-8c9d-5d64720d6bf9 + if name.len() > 7 { + name = &name[..7]; } w.extend_from_slice(&format!("{:\x00<8}", name).into_bytes()); w.write_u32::(self.device_data_length)?; @@ -550,14 +724,16 @@ impl ServerDeviceAnnounceResponse { } } +/// 2.2.1.4 Device I/O Request (DR_DEVICE_IOREQUEST) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a087ffa8-d0d5-4874-ac7b-0494f63e2d5d #[derive(Debug)] #[allow(dead_code)] struct DeviceIoRequest { device_id: u32, file_id: u32, completion_id: u32, - major_function: consts::MajorFunction, - minor_function: consts::MinorFunction, + major_function: MajorFunction, + minor_function: MinorFunction, } impl DeviceIoRequest { @@ -566,27 +742,44 @@ impl DeviceIoRequest { let file_id = payload.read_u32::()?; let completion_id = payload.read_u32::()?; let major_function = payload.read_u32::()?; + let major_function = MajorFunction::from_u32(major_function).ok_or_else(|| { + invalid_data_error(&format!( + "invalid major function value {:#010x}", + major_function + )) + })?; let minor_function = payload.read_u32::()?; + // From the spec (2.2.1.4 Device I/O Request (DR_DEVICE_IOREQUEST)): + // "This field [MinorFunction] is valid only when the MajorFunction field + // is set to IRP_MJ_DIRECTORY_CONTROL. If the MajorFunction field is set + // to another value, the MinorFunction field value SHOULD be 0x00000000."" + // + // SHOULD means implementations are not guaranteed to give us 0x00000000, + // so handle that possibility here. + let minor_function = if major_function == MajorFunction::IRP_MJ_DIRECTORY_CONTROL { + minor_function + } else { + 0x00000000 + }; + let minor_function = MinorFunction::from_u32(minor_function).ok_or_else(|| { + invalid_data_error(&format!( + "invalid minor function value {:#010x}", + minor_function + )) + })?; + Ok(Self { device_id, file_id, completion_id, - major_function: consts::MajorFunction::from_u32(major_function).ok_or_else(|| { - invalid_data_error(&format!( - "invalid major function value {:#010x}", - major_function - )) - })?, - minor_function: consts::MinorFunction::from_u32(minor_function).ok_or_else(|| { - invalid_data_error(&format!( - "invalid minor function value {:#010x}", - minor_function - )) - })?, + major_function, + minor_function, }) } } +/// 2.2.1.4.5 Device Control Request (DR_CONTROL_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/30662c80-ec6e-4ed1-9004-2e6e367bb59f #[derive(Debug)] #[allow(dead_code)] struct DeviceControlRequest { @@ -614,6 +807,8 @@ impl DeviceControlRequest { } } +/// 2.2.1.5 Device I/O Response (DR_DEVICE_IOCOMPLETION) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/1c412a84-0776-4984-b35c-3f0445fcae65 #[derive(Debug)] struct DeviceIoResponse { device_id: u32, @@ -655,7 +850,7 @@ impl DeviceControlResponse { } } - fn encode(&mut self) -> RdpResult> { + fn encode(&self) -> RdpResult> { let mut w = vec![]; w.extend_from_slice(&self.header.encode()?); w.write_u32::(self.output_buffer_length)?; @@ -663,3 +858,730 @@ impl DeviceControlResponse { Ok(w) } } + +/// 2.2.1.4.1 Device Create Request (DR_CREATE_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/5f71f6d2-d9ff-40c2-bdb5-a739447d3c3e +#[derive(Debug)] +#[allow(dead_code)] +struct DeviceCreateRequest { + /// The MajorFunction field in this header MUST be set to IRP_MJ_CREATE. + device_io_request: DeviceIoRequest, + desired_access: flags::DesiredAccess, + allocation_size: u64, + file_attributes: flags::FileAttributes, + shared_access: flags::SharedAccess, + create_disposition: flags::CreateDisposition, + create_options: flags::CreateOptions, + path_length: u32, + path: String, +} + +#[allow(dead_code)] +impl DeviceCreateRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + let invalid_flags = || invalid_data_error("invalid flags in Device Create Request"); + + let desired_access = flags::DesiredAccess::from_bits(payload.read_u32::()?) + .ok_or_else(invalid_flags)?; + let allocation_size = payload.read_u64::()?; + let file_attributes = flags::FileAttributes::from_bits(payload.read_u32::()?) + .ok_or_else(invalid_flags)?; + let shared_access = flags::SharedAccess::from_bits(payload.read_u32::()?) + .ok_or_else(invalid_flags)?; + let create_disposition = + flags::CreateDisposition::from_bits(payload.read_u32::()?) + .ok_or_else(invalid_flags)?; + let create_options = flags::CreateOptions::from_bits(payload.read_u32::()?) + .ok_or_else(invalid_flags)?; + let path_length = payload.read_u32::()?; + + // usize is 32 bits on a 32 bit target and 64 on a 64, so we can safely say try_into().unwrap() + // for a u32 will never panic on the machines that run teleport. + let mut path = vec![0u8; path_length.try_into().unwrap()]; + payload.read_exact(&mut path)?; + let path = util::from_unicode(path)?; + + Ok(Self { + device_io_request, + desired_access, + allocation_size, + file_attributes, + shared_access, + create_disposition, + create_options, + path_length, + path, + }) + } +} + +/// 2.2.1.5.1 Device Create Response (DR_CREATE_RSP) +/// A message with this header describes a response to a Device Create Request (section 2.2.1.4.1). +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/99e5fca5-b37a-41e4-bc69-8d7da7860f76 +#[derive(Debug)] +#[allow(dead_code)] +struct DeviceCreateResponse { + device_io_reply: DeviceIoResponse, + file_id: u32, + /// The values of the CreateDisposition field in the Device Create Request (section 2.2.1.4.1) that determine the value + /// of the Information field are associated as follows: + /// +---------------------+--------------------+ + /// | CreateDisposition | Information | + /// +---------------------+--------------------+ + /// | FILE_SUPERSEDE | FILE_SUPERSEDED | + /// | FILE_OPEN | | + /// | FILE_CREATE | | + /// | FILE_OVERWRITE | | + /// +---------------------+--------------------+ + /// | FILE_OPEN_IF | FILE_OPENED | + /// +---------------------+--------------------+ + /// | FILE_OVERWRITE_IF | FILE_OVERWRITTEN | + /// +---------------------+--------------------+ + information: flags::Information, +} + +#[allow(dead_code)] +impl DeviceCreateResponse { + fn new(device_create_request: &DeviceCreateRequest, io_status: NTSTATUS) -> Self { + let device_io_request = &device_create_request.device_io_request; + + let information: flags::Information; + if device_create_request.create_disposition.intersects( + flags::CreateDisposition::FILE_SUPERSEDE + | flags::CreateDisposition::FILE_OPEN + | flags::CreateDisposition::FILE_CREATE + | flags::CreateDisposition::FILE_OVERWRITE, + ) { + information = flags::Information::FILE_SUPERSEDED; + } else if device_create_request.create_disposition == flags::CreateDisposition::FILE_OPEN_IF + { + information = flags::Information::FILE_OPENED; + } else if device_create_request.create_disposition + == flags::CreateDisposition::FILE_OVERWRITE_IF + { + information = flags::Information::FILE_OVERWRITTEN; + } else { + panic!("program error, CreateDispositionFlags check should be exhaustive"); + } + + Self { + device_io_reply: DeviceIoResponse::new( + device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + file_id: device_io_request.file_id, // TODO(isaiah): this is false, the client should be generating the file_id here + information, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_reply.encode()?); + w.write_u32::(self.file_id)?; + w.write_u8(self.information.bits())?; + Ok(w) + } +} + +/// 2.2.3.3.8 Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946 +#[derive(Debug)] +#[allow(dead_code)] +struct ServerDriveQueryInformationRequest { + /// A DR_DEVICE_IOREQUEST (section 2.2.1.4) header. The MajorFunction field in the DR_DEVICE_IOREQUEST header MUST be set to IRP_MJ_QUERY_INFORMATION. + device_io_request: DeviceIoRequest, + /// A 32-bit unsigned integer. + /// This field MUST contain one of the following values: + /// FileBasicInformation + /// This information class is used to query a file for the times of creation, last access, last write, and change, in addition to file attribute information. The Reserved field of the FileBasicInformation structure ([MS-FSCC] section 2.4.7) MUST NOT be present. + /// + /// FileStandardInformation + /// This information class is used to query for file information such as allocation size, end-of-file position, and number of links. The Reserved field of the FileStandardInformation structure ([MS-FSCC] section 2.4.41) MUST NOT be present. + /// + /// FileAttributeTagInformation + /// This information class is used to query for file attribute and reparse tag information. + fs_information_class_lvl: FsInformationClassLevel, + // Length, Padding, and QueryBuffer appear to be vestigial fields and can safely be ignored. Their description + // is provided below for documentation purposes. + // + // Length (4 bytes): A 32-bit unsigned integer that specifies the number of bytes in the QueryBuffer field. + // + // Padding (24 bytes): An array of 24 bytes. This field is unused and MUST be ignored. + // + // QueryBuffer (variable): A variable-length array of bytes. The size of the array is specified by the Length field. + // The content of this field is based on the value of the FsInformationClass field, which determines the different + // structures that MUST be contained in the QueryBuffer field. For a complete list of these structures, see [MS-FSCC] + // section 2.4. The "File information class" table defines all the possible values for the FsInformationClass field. +} + +#[allow(dead_code)] +impl ServerDriveQueryInformationRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + if let Some(fs_information_class_lvl) = + FsInformationClassLevel::from_u32(payload.read_u32::()?) + { + Ok(Self { + device_io_request, + fs_information_class_lvl, + }) + } else { + Err(invalid_data_error( + "received invalid FsInformationClass in ServerDriveQueryInformationRequest", + )) + } + } +} + +/// 2.4 File Information Classes [MS-FSCC] +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4718fc40-e539-4014-8e33-b675af74e3e1 +#[derive(Debug)] +#[allow(dead_code, clippy::enum_variant_names)] +enum FsInformationClass { + FileBasicInformation(FileBasicInformation), + FileStandardInformation(FileStandardInformation), + FileBothDirectoryInformation(FileBothDirectoryInformation), +} + +#[allow(dead_code)] +impl FsInformationClass { + fn encode(&self) -> RdpResult> { + match self { + Self::FileBasicInformation(file_basic_info) => file_basic_info.encode(), + Self::FileStandardInformation(file_standard_info) => file_standard_info.encode(), + Self::FileBothDirectoryInformation(fil_both_dir_info) => fil_both_dir_info.encode(), // TODO(isaiah) + } + } +} + +/// 2.4.7 FileBasicInformation [MS-FSCC] +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/16023025-8a78-492f-8b96-c873b042ac50 +#[derive(Debug)] +struct FileBasicInformation { + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + file_attributes: flags::FileAttributes, + // NOTE: The `reserved` field in the spec MUST not be serialized and sent over RDP, or it will break the server implementation. + // FreeRDP does the same: https://github.com/FreeRDP/FreeRDP/blob/1adb263813ca2e76a893ef729a04db8f94b5d757/channels/drive/client/drive_file.c#L508 + //reserved: u32, +} + +#[allow(dead_code)] +/// 4 i64's and 1 u32's = (4 * 8) + 4 +const FILE_BASIC_INFORMATION_SIZE: u32 = (4 * 8) + 4; + +impl FileBasicInformation { + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_i64::(self.creation_time)?; + w.write_i64::(self.last_access_time)?; + w.write_i64::(self.last_write_time)?; + w.write_i64::(self.change_time)?; + w.write_u32::(self.file_attributes.bits())?; + Ok(w) + } +} + +/// 2.4.41 FileStandardInformation [MS-FSCC] +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/5afa7f66-619c-48f3-955f-68c4ece704ae +#[derive(Debug)] +struct FileStandardInformation { + /// A 64-bit signed integer that contains the file allocation size, in bytes. The value of this field MUST be an + /// integer multiple of the cluster size. + /// Cluster size is the size of the logical minimal unit of disk space used by the operating system. FreeRDP + /// doesn't give the actual size here, but rather just gives the file size itself, which we will mimic. + /// (ttps://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L518-L519). + /// + /// When FileStandardInformation is requested for a directory, its not entirely clear what "file size" means. + /// FreeRDP derives this value from the st_size field of a stat struct (https://linux.die.net/man/2/lstat), which says + /// "The st_size field gives the size of the file (if it is a regular file or a symbolic link) in bytes. The size of + /// a symbolic link is the length of the pathname it contains, without a terminating null byte." Since it's not + /// entirely clear what is offered here in the case of a directory, we will just use 0. + allocation_size: i64, + /// A 64-bit signed integer that contains the absolute end-of-file position as a byte offset from the start of the + /// file. EndOfFile specifies the offset to the byte immediately following the last valid byte in the file. Because + /// this value is zero-based, it actually refers to the first free byte in the file. That is, it is the offset from + /// the beginning of the file at which new bytes appended to the file will be written. The value of this field MUST + /// be greater than or equal to 0. + end_of_file: i64, + /// A 32-bit unsigned integer that contains the number of non-deleted [hard] links to this file. + /// NOTE: this information is not available to us in the browser, so we will simply set this field to 0. + number_of_links: u32, + /// Set to TRUE to indicate that a file deletion has been requested; set to FALSE + /// otherwise. + delete_pending: Boolean, + /// Set to TRUE to indicate that the file is a directory; set to FALSE otherwise. + directory: Boolean, + // NOTE: `reserved` field omitted, see NOTE in FileBasicInformation struct. + // reserved: u16, +} + +impl FileStandardInformation { + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_i64::(self.allocation_size)?; + w.write_i64::(self.end_of_file)?; + w.write_u32::(self.number_of_links)?; + w.write_u8(Boolean::to_u8(&self.delete_pending).unwrap())?; + w.write_u8(Boolean::to_u8(&self.directory).unwrap())?; + Ok(w) + } +} + +#[allow(dead_code)] +// 2 i64's + 1 u32 + 2 Boolean (u8) = (2 * 8) + 4 + 2 +const FILE_STANDARD_INFORMATION_SIZE: u32 = (2 * 8) + 4 + 2; + +/// 2.1.8 Boolean +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/8ce7b38c-d3cc-415d-ab39-944000ea77ff +#[derive(Debug, ToPrimitive)] +#[repr(u8)] +#[allow(dead_code)] +enum Boolean { + True = 1, + False = 0, +} + +/// 2.4.8 FileBothDirectoryInformation +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/270df317-9ba5-4ccb-ba00-8d22be139bc5 +/// Fields are omitted based on those omitted by FreeRDP: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L871 +#[derive(Debug)] +struct FileBothDirectoryInformation { + // next_entry_offset: u32, + // file_index: u32, + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + end_of_file: i64, + allocation_size: i64, + file_attributes: flags::FileAttributes, + file_name_length: u32, + // ea_size: u32, + // short_name_length: i8, + // reserved: u8: MUST NOT be added, + // see https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L907 + // short_name: String, // 24 bytes + file_name: String, +} + +#[allow(dead_code)] +/// Base size of the FileBothDirectoryInformation, not accounting for variably sized file_name. +/// Note that file_name's size should be calculated as if it were a Unicode string. +/// 5 u32's (including FileAttributesFlags) + 6 i64's + 1 i8 + 24 bytes +const FILE_BOTH_DIRECTORY_INFORMATION_BASE_SIZE: u32 = (5 * 4) + (6 * 8) + 1 + 24; // 93 + +#[allow(dead_code)] +impl FileBothDirectoryInformation { + fn new( + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + file_size: i64, + file_attributes: flags::FileAttributes, + file_name: String, + ) -> Self { + Self { + creation_time, + last_access_time, + last_write_time, + change_time, + end_of_file: file_size, + allocation_size: file_size, + file_attributes, + file_name_length: u32::try_from(util::to_unicode(&file_name, false).len()).unwrap(), + file_name, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + // next_entry_offset + w.write_u32::(0)?; + // file_index + w.write_u32::(0)?; + w.write_i64::(self.creation_time)?; + w.write_i64::(self.last_access_time)?; + w.write_i64::(self.last_write_time)?; + w.write_i64::(self.change_time)?; + w.write_i64::(self.end_of_file)?; + w.write_i64::(self.allocation_size)?; + w.write_u32::(self.file_attributes.bits())?; + w.write_u32::(self.file_name_length)?; + // ea_size + w.write_u32::(0)?; + // short_name_length + w.write_i8(0)?; + // reserved u8, MUST NOT be added! + // short_name + w.extend_from_slice(&[0; 24]); + // When working with this field, use file_name_length to determine the length of the file name rather + // than assuming the presence of a trailing null delimiter. Dot directory names are valid for this field. + w.extend_from_slice(&util::to_unicode(&self.file_name, false)); + Ok(w) + } +} + +/// 2.2.3.4.8 Client Drive Query Information Response (DR_DRIVE_QUERY_INFORMATION_RSP) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/37ef4fb1-6a95-4200-9fbf-515464f034a4 +#[derive(Debug)] +#[allow(dead_code)] + +struct ClientDriveQueryInformationResponse { + device_io_response: DeviceIoResponse, + length: u32, + buffer: FsInformationClass, +} + +#[allow(dead_code)] +impl ClientDriveQueryInformationResponse { + /// Constructs a ClientDriveQueryInformationResponse from a ServerDriveQueryInformationRequest and an NTSTATUS. + /// If the ServerDriveQueryInformationRequest.fs_information_class_lvl is currently unsupported, the program will panic. + /// TODO(isaiah): We will pass some sort of file structure into here. + fn new(req: &ServerDriveQueryInformationRequest, io_status: NTSTATUS) -> RdpResult { + let (length, buffer) = match req.fs_information_class_lvl { + FsInformationClassLevel::FileBasicInformation => ( + FILE_BASIC_INFORMATION_SIZE, + FsInformationClass::FileBasicInformation(FileBasicInformation { + creation_time: 1, + last_access_time: 2, + last_write_time: 3, + change_time: 4, + file_attributes: flags::FileAttributes::FILE_ATTRIBUTE_DIRECTORY, + }), + ), + FsInformationClassLevel::FileStandardInformation => ( + FILE_STANDARD_INFORMATION_SIZE, + FsInformationClass::FileStandardInformation(FileStandardInformation { + allocation_size: 0, + end_of_file: 0, + number_of_links: 0, + delete_pending: Boolean::False, + directory: Boolean::True, + }), + ), + _ => { + return Err(not_implemented_error(&format!( + "received unsupported NTSTATUS: {:?}", + io_status + ))) + } + }; + + Ok(Self { + device_io_response: DeviceIoResponse::new( + &req.device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length, + buffer, + }) + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_response.encode()?); + w.write_u32::(self.length)?; + w.extend_from_slice(&self.buffer.encode()?); + Ok(w) + } +} + +/// 2.2.1.4.2 Device Close Request (DR_CLOSE_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/3ec6627f-9e0f-4941-a828-3fc6ed63d9e7 +#[derive(Debug)] +#[allow(dead_code)] +struct DeviceCloseRequest { + device_io_request: DeviceIoRequest, + // Padding (32 bytes): An array of 32 bytes. Reserved. This field can be set to any value, and MUST be ignored. +} + +#[allow(dead_code)] +impl DeviceCloseRequest { + fn decode(device_io_request: DeviceIoRequest) -> Self { + Self { device_io_request } + } +} + +/// 2.2.1.5.2 Device Close Response (DR_CLOSE_RSP) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/0dae7031-cfd8-4f14-908c-ec06e14997b5 +#[derive(Debug)] +#[allow(dead_code)] +struct DeviceCloseResponse { + /// The CompletionId field of this header MUST match a Device I/O Request (section 2.2.1.4) message that had the MajorFunction field set to IRP_MJ_CLOSE. + device_io_response: DeviceIoResponse, + /// This field can be set to any value and MUST be ignored. + padding: u32, +} +#[allow(dead_code)] +impl DeviceCloseResponse { + fn new(device_close_request: DeviceCloseRequest, io_status: NTSTATUS) -> Self { + Self { + device_io_response: DeviceIoResponse::new( + &device_close_request.device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + padding: 0, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_response.encode()?); + w.write_u32::(self.padding)?; + Ok(w) + } +} + +/// 2.2.3.3.11 Server Drive NotifyChange Directory Request (DR_DRIVE_NOTIFY_CHANGE_DIRECTORY_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/ed05e73d-e53e-4261-a1e1-365a70ba6512 +#[derive(Debug)] +#[allow(dead_code)] +struct ServerDriveNotifyChangeDirectoryRequest { + /// The MajorFunction field in the DR_DEVICE_IOREQUEST header MUST be set to IRP_MJ_DIRECTORY_CONTROL, + /// and the MinorFunction field MUST be set to IRP_MN_NOTIFY_CHANGE_DIRECTORY. + device_io_request: DeviceIoRequest, + /// If nonzero, a change anywhere within the tree MUST trigger the notification response; otherwise, only a change in the root directory will do so. + watch_tree: u8, + completion_filter: flags::CompletionFilter, + // Padding (27 bytes): An array of 27 bytes. This field is unused and MUST be ignored. +} + +#[allow(dead_code)] +impl ServerDriveNotifyChangeDirectoryRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + let invalid_flags = + || invalid_data_error("invalid flags in Server Drive NotifyChange Directory Request"); + + let watch_tree = payload.read_u8()?; + let completion_filter = + flags::CompletionFilter::from_bits(payload.read_u32::()?) + .ok_or_else(invalid_flags)?; + + Ok(Self { + device_io_request, + watch_tree, + completion_filter, + }) + } +} + +/// 2.2.1.4.3 Device Read Request (DR_READ_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/3192516d-36a6-47c5-987a-55c214aa0441 +#[derive(Debug)] +#[allow(dead_code)] +struct DeviceReadRequest { + /// The MajorFunction field in this header MUST be set to IRP_MJ_READ. + device_io_request: DeviceIoRequest, + /// This field specifies the maximum number of bytes to be read from the device. + length: u32, + /// This field specifies the file offset where the read operation is performed. + offset: u64, + // Padding (20 bytes): An array of 20 bytes. Reserved. This field can be set to any value and MUST be ignored. +} + +#[allow(dead_code)] +impl DeviceReadRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + Ok(Self { + device_io_request, + length: payload.read_u32::()?, + offset: payload.read_u64::()?, + }) + } +} + +/// 2.2.1.5.3 Device Read Response (DR_READ_RSP) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/d35d3f91-fc5b-492b-80be-47f483ad1dc9 +#[derive(Debug)] +#[allow(dead_code)] +struct DeviceReadResponse { + /// The CompletionId field of this header MUST match a Device I/O Request (section 2.2.1.4) message that had the MajorFunction field set to IRP_MJ_READ. + device_io_reply: DeviceIoResponse, + /// Specifies the number of bytes in the ReadData field. + length: u32, + /// A variable-length array of bytes that specifies the output data from the read request. + read_data: Vec, +} + +#[allow(dead_code)] +impl DeviceReadResponse { + fn new( + device_read_request: &DeviceReadRequest, + io_status: NTSTATUS, + read_data: Vec, + ) -> Self { + let device_io_request = &device_read_request.device_io_request; + + Self { + device_io_reply: DeviceIoResponse::new( + device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length: u32::try_from(read_data.len()).unwrap(), + read_data, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_reply.encode()?); + w.write_u32::(self.length)?; + w.extend_from_slice(&self.read_data); + Ok(w) + } +} + +/// 2.2.3.3.10 Server Drive Query Directory Request (DR_DRIVE_QUERY_DIRECTORY_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/458019d2-5d5a-4fd4-92ef-8c05f8d7acb1 +#[derive(Debug)] +#[allow(dead_code)] +struct ServerDriveQueryDirectoryRequest { + /// The MajorFunction field in the DR_DEVICE_IOREQUEST header MUST be set to IRP_MJ_DIRECTORY_CONTROL, + /// and the MinorFunction field MUST be set to IRP_MN_QUERY_DIRECTORY. + device_io_request: DeviceIoRequest, + /// Must contain one of FileDirectoryInformation, FileFullDirectoryInformation, FileBothDirectoryInformation, FileNamesInformation + fs_information_class_lvl: FsInformationClassLevel, + /// If the value of this field is zero, the request is for the next file in the directory that was specified in a previous + /// Server Drive Query Directory Request. If such a file does not exist, the client MUST complete this request with STATUS_NO_MORE_FILES + /// in the IoStatus field of the Client Drive I/O Response packet (section 2.2.3.4). If the value of this field is non-zero and such a + /// file does not exist, the client MUST complete this request with STATUS_NO_SUCH_FILE in the IoStatus field of the Client Drive I/O Response. + initial_query: u8, + /// Specifies the number of bytes in the Path field, including the null-terminator. + path_length: u32, + // Padding (23 bytes): An array of 23 bytes. This field is unused and MUST be ignored. + /// A variable-length array of Unicode characters (we will store this as a regular rust String) that specifies the directory + /// on which this operation will be performed. The Path field MUST be null-terminated. If the value of the InitialQuery field + /// is zero, then the contents of the Path field MUST be ignored, irrespective of the value specified in the PathLength field. + path: String, +} + +#[allow(dead_code)] +impl ServerDriveQueryDirectoryRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + let fs_information_class_lvl = + FsInformationClassLevel::from_u32(payload.read_u32::()?) + .ok_or_else(|| invalid_data_error("failed to read FsInformationClassLevel"))?; + if fs_information_class_lvl != FsInformationClassLevel::FileDirectoryInformation + && fs_information_class_lvl != FsInformationClassLevel::FileFullDirectoryInformation + && fs_information_class_lvl != FsInformationClassLevel::FileBothDirectoryInformation + && fs_information_class_lvl != FsInformationClassLevel::FileNamesInformation + { + return Err(invalid_data_error(&format!( + "read invalid FsInformationClassLevel: {:?}, expected one of {:?}", + fs_information_class_lvl, + vec![ + FsInformationClassLevel::FileDirectoryInformation, + FsInformationClassLevel::FileFullDirectoryInformation, + FsInformationClassLevel::FileBothDirectoryInformation, + FsInformationClassLevel::FileNamesInformation + ] + ))); + } + let initial_query = payload.read_u8()?; + let mut path_length: u32 = 0; + let mut path = String::from(""); + if initial_query != 0 { + path_length = payload.read_u32::()?; + + // TODO(isaiah): make a payload.skip(n) + let mut padding: [u8; 23] = [0; 23]; + payload.read_exact(&mut padding)?; + + // TODO(isaiah): make a from_unicode_exact + let mut path_as_vec = vec![0u8; path_length.try_into().unwrap()]; + payload.read_exact(&mut path_as_vec)?; + path = util::from_unicode(path_as_vec)?; + } + + Ok(Self { + device_io_request, + fs_information_class_lvl, + initial_query, + path_length, + path, + }) + } +} + +/// 2.2.3.4.10 Client Drive Query Directory Response (DR_DRIVE_QUERY_DIRECTORY_RSP) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/9c929407-a833-4893-8f20-90c984756140 +#[derive(Debug)] +#[allow(dead_code)] +struct ClientDriveQueryDirectoryResponse { + /// The CompletionId field of the DR_DEVICE_IOCOMPLETION header MUST match a Device I/O Request (section 2.2.1.4) that + /// has the MajorFunction field set to IRP_MJ_DIRECTORY_CONTROL and the MinorFunction field set to IRP_MN_QUERY_DIRECTORY. + device_io_reply: DeviceIoResponse, + /// Specifies the number of bytes in the Buffer field. + length: u32, + /// The content of this field is based on the value of the FsInformationClass field in the Server Drive Query Directory Request + /// message, which determines the different structures that MUST be contained in the Buffer field. + buffer: Option, + // Padding (1 byte): This field is unused and MUST be ignored. +} + +#[allow(dead_code)] +impl ClientDriveQueryDirectoryResponse { + fn new( + req: &ServerDriveQueryDirectoryRequest, + io_status: NTSTATUS, + buffer: Option, + ) -> RdpResult { + let device_io_request = &req.device_io_request; + let length = match buffer { + Some(ref fs_information_class) => match fs_information_class { + FsInformationClass::FileBothDirectoryInformation( + file_both_directory_information, + ) => { + FILE_BOTH_DIRECTORY_INFORMATION_BASE_SIZE + + file_both_directory_information.file_name_length + } + _ => { + return Err(not_implemented_error(&format!("ClientDriveQueryDirectoryResponse not implemented for fs_information_class {:?}", fs_information_class))); + } + }, + None => 0, + }; + + Ok(Self { + device_io_reply: DeviceIoResponse::new( + device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length, + buffer, + }) + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_reply.encode()?); + + if self.device_io_reply.io_status == NTSTATUS::to_u32(&NTSTATUS::STATUS_SUCCESS).unwrap() { + w.write_u32::(self.length)?; + w.extend_from_slice( + &self + .buffer.as_ref() + .ok_or_else(|| invalid_data_error( + "ClientDriveQueryDirectoryResponse with NTSTATUS::STATUS_SUCCESS expects a FsInformationClass" + ))? + .encode()?, + ); + } else if self.device_io_reply.io_status + == NTSTATUS::to_u32(&NTSTATUS::STATUS_NO_MORE_FILES).unwrap() + { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L935-L937 + w.write_u32::(0)?; + w.write_u8(0)?; + } else { + return Err(invalid_data_error(&format!( + "Found ClientDriveQueryDirectoryResponse with invalid or unhandled NTSTATUS: {:?}", + self.device_io_reply.io_status + ))); + } + + Ok(w) + } +} diff --git a/lib/srv/desktop/rdp/rdpclient/src/util.rs b/lib/srv/desktop/rdp/rdpclient/src/util.rs index 087220ea1cd24..6bb326d15daf3 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/util.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/util.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::errors::invalid_data_error; +use rdp::model::error::RdpResult; +use utf16string::{WString, LE}; + /// According to [MS-RDPEFS] 1.1 Glossary: /// Unless otherwise specified, all Unicode strings follow the UTF-16LE /// encoding scheme with no Byte Order Mark (BOM). @@ -20,9 +24,48 @@ /// UTF-16LE encoded Vec, which is useful in cases where we want /// to handle some data in the code as a &str (or String), and later /// convert it to RDP's preferred format and send it over the wire. -pub fn to_nul_terminated_utf16le(s: &str) -> Vec { - s.encode_utf16() - .chain([0]) - .flat_map(|v| v.to_le_bytes()) - .collect() +pub fn to_unicode(s: &str, with_null_term: bool) -> Vec { + let mut buf = WString::::from(s).as_bytes().to_vec(); + if with_null_term { + let mut null_terminator: Vec = vec![0, 0]; + buf.append(&mut null_terminator); + } + buf +} + +#[allow(clippy::bind_instead_of_map)] +pub fn from_unicode(s: Vec) -> RdpResult { + let mut with_null_terminator = WString::from_utf16le(s) + .or_else(|_| Err(invalid_data_error("invalid Unicode")))? + .to_utf8(); + with_null_terminator.pop(); + let without_null_terminator = with_null_terminator; + Ok(without_null_terminator) +} + +/// Converts a &str into a null-terminated UTF-8 encoded Vec +pub fn to_utf8(s: &str) -> Vec { + format!("{}\x00", s).into_bytes() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_and_from() { + let hello_vec = to_unicode("hello", true); + assert_eq!( + hello_vec, + vec![104, 0, 101, 0, 108, 0, 108, 0, 111, 0, 0, 0] + ); + + let hello_string = from_unicode(hello_vec).unwrap(); + assert_eq!(hello_string, "hello"); + } + + #[test] + fn from_unicode_empty_vector() { + assert_eq!(from_unicode(vec![]).unwrap(), ""); + } } diff --git a/lib/srv/desktop/tdp/proto.go b/lib/srv/desktop/tdp/proto.go index c70a5585222f4..0069cc1608c19 100644 --- a/lib/srv/desktop/tdp/proto.go +++ b/lib/srv/desktop/tdp/proto.go @@ -45,16 +45,18 @@ type MessageType byte // For descriptions of each message type see: // https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#message-types const ( - TypeClientScreenSpec = MessageType(1) - TypePNGFrame = MessageType(2) - TypeMouseMove = MessageType(3) - TypeMouseButton = MessageType(4) - TypeKeyboardButton = MessageType(5) - TypeClipboardData = MessageType(6) - TypeClientUsername = MessageType(7) - TypeMouseWheel = MessageType(8) - TypeError = MessageType(9) - TypeMFA = MessageType(10) + TypeClientScreenSpec = MessageType(1) + TypePNGFrame = MessageType(2) + TypeMouseMove = MessageType(3) + TypeMouseButton = MessageType(4) + TypeKeyboardButton = MessageType(5) + TypeClipboardData = MessageType(6) + TypeClientUsername = MessageType(7) + TypeMouseWheel = MessageType(8) + TypeError = MessageType(9) + TypeMFA = MessageType(10) + TypeSharedDirectoryAnnounce = MessageType(11) + TypeSharedDirectoryAcknowledge = MessageType(12) ) // Message is a Go representation of a desktop protocol message. @@ -107,6 +109,10 @@ func decode(in peekReader) (Message, error) { return decodeError(in) case TypeMFA: return DecodeMFA(in) + case TypeSharedDirectoryAnnounce: + return decodeSharedDirectoryAnnounce(in) + case TypeSharedDirectoryAcknowledge: + return decodeSharedDirectoryAcknowledge(in) default: return nil, trace.BadParameter("unsupported desktop protocol message type %d", t) } @@ -597,6 +603,76 @@ func DecodeMFAChallenge(in peekReader) (*MFA, error) { }, nil } +type SharedDirectoryAnnounce struct { + DirectoryID uint32 + Name string +} + +func (s SharedDirectoryAnnounce) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryAnnounce)) + binary.Write(buf, binary.BigEndian, s.DirectoryID) + if err := encodeString(buf, s.Name); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil +} + +func decodeSharedDirectoryAnnounce(in peekReader) (SharedDirectoryAnnounce, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryAnnounce{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryAnnounce) { + return SharedDirectoryAnnounce{}, trace.BadParameter("got message type %v, expected SharedDirectoryAnnounce(%v)", t, TypeSharedDirectoryAnnounce) + } + var completionID, directoryID uint32 + err = binary.Read(in, binary.BigEndian, &completionID) + if err != nil { + return SharedDirectoryAnnounce{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &directoryID) + if err != nil { + return SharedDirectoryAnnounce{}, trace.Wrap(err) + } + name, err := decodeString(in, windowsMaxUsernameLength) + if err != nil { + return SharedDirectoryAnnounce{}, trace.Wrap(err) + } + + return SharedDirectoryAnnounce{ + DirectoryID: directoryID, + Name: name, + }, nil +} + +type SharedDirectoryAcknowledge struct { + Err uint32 + DirectoryID uint32 +} + +func decodeSharedDirectoryAcknowledge(in peekReader) (SharedDirectoryAcknowledge, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryAcknowledge{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryAcknowledge) { + return SharedDirectoryAcknowledge{}, trace.BadParameter("got message type %v, expected SharedDirectoryAcknowledge(%v)", t, TypeSharedDirectoryAnnounce) + } + + var s SharedDirectoryAcknowledge + err = binary.Read(in, binary.BigEndian, &s) + return s, trace.Wrap(err) +} + +func (s SharedDirectoryAcknowledge) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryAcknowledge)) + binary.Write(buf, binary.BigEndian, s.Err) + binary.Write(buf, binary.BigEndian, s.DirectoryID) + return buf.Bytes(), nil +} + // encodeString encodes strings for TDP. Strings are encoded as UTF-8 with // a 32-bit length prefix (in bytes): // https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#field-types diff --git a/lib/web/desktop/playback.go b/lib/web/desktop/playback.go index 79f0a7c86d1d7..1f22df9d3ed2b 100644 --- a/lib/web/desktop/playback.go +++ b/lib/web/desktop/playback.go @@ -107,8 +107,6 @@ const ( // actionPlayPause toggles the playback state // between playing and paused actionPlayPause = playbackAction("play/pause") - - // TODO(isaiah): support playbackAction("seek") ) // actionMessage is a message passed from the playback client