From 3d429af41dfee1c16e92cb3280f8ba9f8b6ce5e2 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Sat, 30 Mar 2024 13:41:33 -0700 Subject: [PATCH 1/2] Adds support for SharedDirectoryTruncateRequest and SharedDirectoryTruncateResponse messages --- lib/srv/desktop/rdp/rdpclient/client.go | 36 +++++ lib/srv/desktop/rdp/rdpclient/src/client.rs | 36 +++++ lib/srv/desktop/rdp/rdpclient/src/lib.rs | 36 ++++- lib/srv/desktop/rdp/rdpclient/src/rdpdr.rs | 7 + .../rdp/rdpclient/src/rdpdr/filesystem.rs | 101 ++++++++++++- .../desktop/rdp/rdpclient/src/rdpdr/tdp.rs | 38 ++++- lib/srv/desktop/tdp/proto.go | 138 ++++++++++++++---- ...0067-desktop-access-file-system-sharing.md | 34 ++++- web/packages/teleport/src/lib/tdp/client.ts | 23 +++ web/packages/teleport/src/lib/tdp/codec.ts | 106 ++++++++++---- .../src/lib/tdp/sharedDirectoryManager.ts | 69 +++++---- 11 files changed, 530 insertions(+), 94 deletions(-) diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go index 920c8ba3c29e5..69aebd3fd85f7 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -622,6 +622,15 @@ func (c *Client) startInputStreaming(stopCh chan struct{}) error { return trace.Errorf("SharedDirectoryMoveResponse failed: %v", errCode) } } + case tdp.SharedDirectoryTruncateResponse: + if c.cfg.AllowDirectorySharing { + if errCode := C.client_handle_tdp_sd_truncate_response(C.ulong(c.handle), C.CGOSharedDirectoryTruncateResponse{ + completion_id: C.uint32_t(m.CompletionID), + err_code: m.ErrCode, + }); errCode != C.ErrCodeSuccess { + return trace.Errorf("SharedDirectoryTruncateResponse failed: %v", errCode) + } + } case tdp.RDPResponsePDU: pduLen := uint32(len(m)) if pduLen == 0 { @@ -963,6 +972,33 @@ func (c *Client) sharedDirectoryMoveRequest(req tdp.SharedDirectoryMoveRequest) } +//export cgo_tdp_sd_truncate_request +func cgo_tdp_sd_truncate_request(handle C.uintptr_t, req *C.CGOSharedDirectoryTruncateRequest) C.CGOErrCode { + client, err := toClient(handle) + if err != nil { + return C.ErrCodeFailure + } + return client.sharedDirectoryTruncateRequest(tdp.SharedDirectoryTruncateRequest{ + CompletionID: uint32(req.completion_id), + DirectoryID: uint32(req.directory_id), + Path: C.GoString(req.path), + EndOfFile: uint32(req.end_of_file), + }) +} + +func (c *Client) sharedDirectoryTruncateRequest(req tdp.SharedDirectoryTruncateRequest) C.CGOErrCode { + if !c.cfg.AllowDirectorySharing { + return C.ErrCodeFailure + } + + if err := c.cfg.Conn.WriteMessage(req); err != nil { + c.cfg.Log.Errorf("failed to send SharedDirectoryTruncateRequest: %v", err) + return C.ErrCodeFailure + } + return C.ErrCodeSuccess + +} + // GetClientLastActive returns the time of the last recorded activity. // For RDP, "activity" is defined as user-input messages // (mouse move, button press, etc.) diff --git a/lib/srv/desktop/rdp/rdpclient/src/client.rs b/lib/srv/desktop/rdp/rdpclient/src/client.rs index 3c394fa2c7022..e34f0ab70475c 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/client.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/client.rs @@ -400,6 +400,10 @@ impl Client { Client::handle_tdp_sd_move_response(x224_processor.clone(), res) .await?; } + ClientFunction::HandleTdpSdTruncateResponse(res) => { + Client::handle_tdp_sd_truncate_response(x224_processor.clone(), res) + .await?; + } ClientFunction::WriteCliprdr(f) => { Client::write_cliprdr(x224_processor.clone(), &mut write_stream, f) .await?; @@ -699,6 +703,21 @@ impl Client { .await? } + async fn handle_tdp_sd_truncate_response( + x224_processor: Arc>, + res: tdp::SharedDirectoryTruncateResponse, + ) -> ClientResult<()> { + global::TOKIO_RT + .spawn_blocking(move || { + debug!("received tdp: {:?}", res); + let mut x224_processor = Self::x224_lock(&x224_processor)?; + let rdpdr = Self::rdpdr_backend(&mut x224_processor)?; + rdpdr.handle_tdp_sd_truncate_response(res)?; + Ok(()) + }) + .await? + } + async fn add_drive( x224_processor: Arc>, sda: tdp::SharedDirectoryAnnounce, @@ -864,6 +883,8 @@ enum ClientFunction { HandleTdpSdWriteResponse(tdp::SharedDirectoryWriteResponse), /// Corresponds to [`Client::handle_tdp_sd_move_response`] HandleTdpSdMoveResponse(tdp::SharedDirectoryMoveResponse), + /// Corresponds to [`Client::handle_tdp_sd_truncate_response`] + HandleTdpSdTruncateResponse(tdp::SharedDirectoryTruncateResponse), /// Corresponds to [`Client::write_cliprdr`] WriteCliprdr(Box), /// Corresponds to [`Client::update_clipboard`] @@ -1042,6 +1063,21 @@ impl ClientHandle { .await } + pub fn handle_tdp_sd_truncate_response( + &self, + res: tdp::SharedDirectoryTruncateResponse, + ) -> ClientResult<()> { + self.blocking_send(ClientFunction::HandleTdpSdTruncateResponse(res)) + } + + pub async fn handle_tdp_sd_truncate_response_async( + &self, + res: tdp::SharedDirectoryTruncateResponse, + ) -> ClientResult<()> { + self.send(ClientFunction::HandleTdpSdTruncateResponse(res)) + .await + } + pub fn write_cliprdr(&self, f: Box) -> ClientResult<()> { self.blocking_send(ClientFunction::WriteCliprdr(f)) } diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index 8203f03150c26..54b4ad31e5c37 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -33,8 +33,8 @@ use rdpdr::path::UnixPath; use rdpdr::tdp::{ FileSystemObject, FileType, SharedDirectoryAcknowledge, SharedDirectoryCreateResponse, SharedDirectoryDeleteResponse, SharedDirectoryInfoResponse, SharedDirectoryListResponse, - SharedDirectoryMoveResponse, SharedDirectoryReadResponse, SharedDirectoryWriteResponse, - TdpErrCode, + SharedDirectoryMoveResponse, SharedDirectoryReadResponse, SharedDirectoryTruncateResponse, + SharedDirectoryWriteResponse, TdpErrCode, }; use std::ffi::CString; use std::fmt::Debug; @@ -347,6 +347,24 @@ pub unsafe extern "C" fn client_handle_tdp_sd_move_response( ) } +/// client_handle_tdp_sd_truncate_response handles a TDP Shared Directory Truncate Response +/// message +/// +/// # Safety +/// +/// `cgo_handle` must be a valid handle. +#[no_mangle] +pub unsafe extern "C" fn client_handle_tdp_sd_truncate_response( + cgo_handle: CgoHandle, + res: CGOSharedDirectoryTruncateResponse, +) -> CGOErrCode { + handle_operation( + cgo_handle, + "client_handle_tdp_sd_truncate_response", + move |client_handle| client_handle.handle_tdp_sd_truncate_response(res), + ) +} + /// client_handle_tdp_rdp_response_pdu handles a TDP RDP Response PDU message. It takes a raw encoded RDP PDU /// created by the ironrdp client on the frontend and sends it directly to the RDP server. /// @@ -651,6 +669,16 @@ pub struct CGOSharedDirectoryListRequest { pub path: *const c_char, } +#[repr(C)] +pub struct CGOSharedDirectoryTruncateRequest { + pub completion_id: u32, + pub directory_id: u32, + pub path: *const c_char, + pub end_of_file: u32, +} + +pub type CGOSharedDirectoryTruncateResponse = SharedDirectoryTruncateResponse; + // These functions are defined on the Go side. // Look for functions with '//export funcname' comments. extern "C" { @@ -695,6 +723,10 @@ extern "C" { cgo_handle: CgoHandle, req: *mut CGOSharedDirectoryMoveRequest, ) -> CGOErrCode; + fn cgo_tdp_sd_truncate_request( + cgo_handle: CgoHandle, + req: *mut CGOSharedDirectoryTruncateRequest, + ) -> CGOErrCode; } /// A [cgo.Handle] passed to us by Go. diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr.rs index 11b0517fe5b8a..1dbeaa24f8239 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr.rs @@ -157,6 +157,13 @@ impl TeleportRdpdrBackend { ) -> PduResult<()> { self.fs.handle_tdp_sd_move_response(tdp_resp) } + + pub fn handle_tdp_sd_truncate_response( + &mut self, + tdp_resp: tdp::SharedDirectoryTruncateResponse, + ) -> PduResult<()> { + self.fs.handle_tdp_sd_truncate_response(tdp_resp) + } } /// A generic error type for the TeleportRdpdrBackend that can contain any arbitrary error message. diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/filesystem.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/filesystem.rs index 1fcd404d11dcb..2069b0b04f0d0 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/filesystem.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/filesystem.rs @@ -19,7 +19,8 @@ use super::{ use crate::{ cgo_tdp_sd_acknowledge, cgo_tdp_sd_create_request, cgo_tdp_sd_delete_request, cgo_tdp_sd_info_request, cgo_tdp_sd_list_request, cgo_tdp_sd_move_request, - cgo_tdp_sd_read_request, cgo_tdp_sd_write_request, client::ClientHandle, CGOErrCode, CgoHandle, + cgo_tdp_sd_read_request, cgo_tdp_sd_truncate_request, cgo_tdp_sd_write_request, + client::ClientHandle, CGOErrCode, CgoHandle, }; use ironrdp_pdu::{cast_length, custom_err, other_err, PduResult}; use ironrdp_rdpdr::pdu::{ @@ -27,7 +28,7 @@ use ironrdp_rdpdr::pdu::{ efs::{self, NtStatus}, esc, }; -use log::debug; +use log::{debug, warn}; use std::collections::HashMap; use std::convert::TryInto; @@ -51,6 +52,7 @@ pub struct FilesystemBackend { pending_sd_read_resp_handlers: ResponseCache, pending_sd_write_resp_handlers: ResponseCache, pending_sd_move_resp_handlers: ResponseCache, + pending_sd_truncate_resp_handlers: ResponseCache, } impl FilesystemBackend { @@ -66,6 +68,7 @@ impl FilesystemBackend { pending_sd_read_resp_handlers: ResponseCache::new(), pending_sd_write_resp_handlers: ResponseCache::new(), pending_sd_move_resp_handlers: ResponseCache::new(), + pending_sd_truncate_resp_handlers: ResponseCache::new(), } } @@ -595,9 +598,10 @@ impl FilesystemBackend { } } } - efs::FileInformationClass::Basic(_) - | efs::FileInformationClass::EndOfFile(_) - | efs::FileInformationClass::Allocation(_) => { + efs::FileInformationClass::EndOfFile(ref eof) => { + self.tdp_sd_truncate(rdp_req.clone(), eof, io_status) + } + efs::FileInformationClass::Basic(_) | efs::FileInformationClass::Allocation(_) => { // Each of these ask us to change something we don't have control over at the browser // level, so we just do nothing and send back a success. // https://github.com/FreeRDP/FreeRDP/blob/dfa231c0a55b005af775b833f92f6bcd30363d77/channels/drive/client/drive_file.c#L579 @@ -844,6 +848,59 @@ impl FilesystemBackend { } } + /// Helper function for sending a [`tdp::SharedDirectoryMoveRequest`] to the browser + /// and handling the [`tdp::SharedDirectoryMoveResponse`] that is received in response. + fn tdp_sd_truncate( + &mut self, + rdp_req: efs::ServerDriveSetInformationRequest, + eof: &efs::FileEndOfFileInformation, + io_status: NtStatus, + ) -> PduResult<()> { + if let Some(file) = self.file_cache.get(rdp_req.device_io_request.file_id) { + let end_of_file = eof.end_of_file; + self.send_tdp_truncate_request(tdp::SharedDirectoryTruncateRequest { + completion_id: rdp_req.device_io_request.completion_id, + directory_id: rdp_req.device_io_request.device_id, + path: file.path.clone(), + end_of_file: cast_length!("end_of_file", end_of_file)?, + })?; + + self.pending_sd_truncate_resp_handlers.insert( + rdp_req.device_io_request.completion_id, + SharedDirectoryTruncateResponseHandler::new( + move |this: &mut FilesystemBackend, + res: tdp::SharedDirectoryTruncateResponse| + -> PduResult<()> { + if res.err_code != TdpErrCode::Nil { + return this + .send_rdp_set_info_response(&rdp_req, NtStatus::UNSUCCESSFUL); + } + + let io_status = if let Some(file) = + this.file_cache.get_mut(rdp_req.device_io_request.file_id) + { + // Truncate succeeded, update our internal books to reflect the new size. + file.fso.size = cast_length!("end_of_file", end_of_file)?; + io_status + } else { + // This shouldn't happen. + warn!("file unexpectedly not found in cache after truncate"); + NtStatus::UNSUCCESSFUL + }; + + this.send_rdp_set_info_response(&rdp_req, io_status) + }, + ), + ); + + return Ok(()); + } + + // This shouldn't happen. + warn!("attempted to truncate a file that wasn't in the file cache"); + self.send_rdp_set_info_response(&rdp_req, NtStatus::UNSUCCESSFUL) + } + /// Helper function for sending a [`tdp::SharedDirectoryMoveRequest`] to the browser /// and handling the [`tdp::SharedDirectoryMoveResponse`] that is received in response. fn tdp_sd_move( @@ -913,6 +970,23 @@ impl FilesystemBackend { } } + /// Sends a [`tdp::SharedDirectoryTruncateRequest`] to the browser. + fn send_tdp_truncate_request( + &self, + tdp_req: tdp::SharedDirectoryTruncateRequest, + ) -> PduResult<()> { + debug!("sending tdp: {:?}", tdp_req); + let mut req = tdp_req.into_cgo()?; + let err = unsafe { cgo_tdp_sd_truncate_request(self.cgo_handle, req.cgo()) }; + match err { + CGOErrCode::ErrCodeSuccess => Ok(()), + _ => Err(custom_err!(FilesystemBackendError(format!( + "call to tdp_sd_truncate_request failed: {:?}", + err + )))), + } + } + /// Sends a [`tdp::SharedDirectoryCreateRequest`] to the browser. fn send_tdp_sd_create_request( &self, @@ -1142,6 +1216,22 @@ impl FilesystemBackend { } } + pub fn handle_tdp_sd_truncate_response( + &mut self, + tdp_resp: tdp::SharedDirectoryTruncateResponse, + ) -> PduResult<()> { + match self + .pending_sd_truncate_resp_handlers + .remove(&tdp_resp.completion_id) + { + Some(handler) => handler.call(self, tdp_resp), + None => Err(custom_err!(FilesystemBackendError(format!( + "received invalid completion id: {}", + tdp_resp.completion_id + )))), + } + } + /// Helper function for sending an RDP [`efs::DeviceCreateResponse`] based on an RDP [`efs::DeviceCreateRequest`]. fn send_rdp_device_create_response( &self, @@ -1690,6 +1780,7 @@ type SharedDirectoryListResponseHandler = ResponseHandler; type SharedDirectoryWriteResponseHandler = ResponseHandler; type SharedDirectoryMoveResponseHandler = ResponseHandler; +type SharedDirectoryTruncateResponseHandler = ResponseHandler; type CompletionId = u32; diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/tdp.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/tdp.rs index aecfffe795226..e146dbe2fcb0e 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/tdp.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/tdp.rs @@ -18,7 +18,8 @@ use crate::{ CGOSharedDirectoryAnnounce, CGOSharedDirectoryCreateRequest, CGOSharedDirectoryCreateResponse, CGOSharedDirectoryDeleteRequest, CGOSharedDirectoryInfoRequest, CGOSharedDirectoryInfoResponse, CGOSharedDirectoryListRequest, CGOSharedDirectoryListResponse, CGOSharedDirectoryMoveRequest, - CGOSharedDirectoryReadRequest, CGOSharedDirectoryReadResponse, CGOSharedDirectoryWriteRequest, + CGOSharedDirectoryReadRequest, CGOSharedDirectoryReadResponse, + CGOSharedDirectoryTruncateRequest, CGOSharedDirectoryWriteRequest, }; use ironrdp_pdu::{cast_length, custom_err, PduResult}; use ironrdp_rdpdr::pdu::efs::{ @@ -558,6 +559,41 @@ impl SharedDirectoryListRequest { } } +/// SharedDirectoryTruncateRequest is sent by the TDP server to the client +/// to truncate a file at path to end_of_file bytes. +#[derive(Debug)] +pub struct SharedDirectoryTruncateRequest { + pub completion_id: u32, + pub directory_id: u32, + pub path: UnixPath, + pub end_of_file: u32, +} + +impl SharedDirectoryTruncateRequest { + /// See [`CGOWithData`]. + pub fn into_cgo(self) -> PduResult> { + let path = self.path.to_cstring()?; + Ok(CGOWithData { + cgo: CGOSharedDirectoryTruncateRequest { + completion_id: self.completion_id, + directory_id: self.directory_id, + path: path.as_ptr(), + end_of_file: self.end_of_file, + }, + _data: vec![path.into()], + }) + } +} + +/// SharedDirectoryTruncateResponse is sent by the TDP client to the server +/// to acknowledge a SharedDirectoryTruncateRequest was received and executed. +#[derive(Debug)] +#[repr(C)] +pub struct SharedDirectoryTruncateResponse { + pub completion_id: u32, + pub err_code: TdpErrCode, +} + #[repr(C)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum TdpErrCode { diff --git a/lib/srv/desktop/tdp/proto.go b/lib/srv/desktop/tdp/proto.go index 85091f1150d92..e9258174071f2 100644 --- a/lib/srv/desktop/tdp/proto.go +++ b/lib/srv/desktop/tdp/proto.go @@ -48,38 +48,40 @@ 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) - TypeSharedDirectoryAnnounce = MessageType(11) - TypeSharedDirectoryAcknowledge = MessageType(12) - TypeSharedDirectoryInfoRequest = MessageType(13) - TypeSharedDirectoryInfoResponse = MessageType(14) - TypeSharedDirectoryCreateRequest = MessageType(15) - TypeSharedDirectoryCreateResponse = MessageType(16) - TypeSharedDirectoryDeleteRequest = MessageType(17) - TypeSharedDirectoryDeleteResponse = MessageType(18) - TypeSharedDirectoryReadRequest = MessageType(19) - TypeSharedDirectoryReadResponse = MessageType(20) - TypeSharedDirectoryWriteRequest = MessageType(21) - TypeSharedDirectoryWriteResponse = MessageType(22) - TypeSharedDirectoryMoveRequest = MessageType(23) - TypeSharedDirectoryMoveResponse = MessageType(24) - TypeSharedDirectoryListRequest = MessageType(25) - TypeSharedDirectoryListResponse = MessageType(26) - TypePNG2Frame = MessageType(27) - TypeNotification = MessageType(28) - TypeRDPFastPathPDU = MessageType(29) - TypeRDPResponsePDU = MessageType(30) - TypeRDPConnectionInitialized = MessageType(31) - TypeSyncKeys = MessageType(32) + 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) + TypeSharedDirectoryInfoRequest = MessageType(13) + TypeSharedDirectoryInfoResponse = MessageType(14) + TypeSharedDirectoryCreateRequest = MessageType(15) + TypeSharedDirectoryCreateResponse = MessageType(16) + TypeSharedDirectoryDeleteRequest = MessageType(17) + TypeSharedDirectoryDeleteResponse = MessageType(18) + TypeSharedDirectoryReadRequest = MessageType(19) + TypeSharedDirectoryReadResponse = MessageType(20) + TypeSharedDirectoryWriteRequest = MessageType(21) + TypeSharedDirectoryWriteResponse = MessageType(22) + TypeSharedDirectoryMoveRequest = MessageType(23) + TypeSharedDirectoryMoveResponse = MessageType(24) + TypeSharedDirectoryListRequest = MessageType(25) + TypeSharedDirectoryListResponse = MessageType(26) + TypePNG2Frame = MessageType(27) + TypeNotification = MessageType(28) + TypeRDPFastPathPDU = MessageType(29) + TypeRDPResponsePDU = MessageType(30) + TypeRDPConnectionInitialized = MessageType(31) + TypeSyncKeys = MessageType(32) + TypeSharedDirectoryTruncateRequest = MessageType(33) + TypeSharedDirectoryTruncateResponse = MessageType(34) ) // Message is a Go representation of a desktop protocol message. @@ -176,6 +178,10 @@ func decodeMessage(firstByte byte, in byteReader) (Message, error) { return decodeSharedDirectoryMoveRequest(in) case TypeSharedDirectoryMoveResponse: return decodeSharedDirectoryMoveResponse(in) + case TypeSharedDirectoryTruncateRequest: + return decodeSharedDirectoryTruncateRequest(in) + case TypeSharedDirectoryTruncateResponse: + return decodeSharedDirectoryTruncateResponse(in) default: return nil, trace.BadParameter("unsupported desktop protocol message type %d", firstByte) } @@ -1549,6 +1555,74 @@ func decodeSharedDirectoryMoveResponse(in io.Reader) (SharedDirectoryMoveRespons return res, err } +// | message type (33) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | end_of_file uint32 | +type SharedDirectoryTruncateRequest struct { + CompletionID uint32 + DirectoryID uint32 + Path string + EndOfFile uint32 +} + +func (s SharedDirectoryTruncateRequest) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryTruncateRequest)) + writeUint32(buf, s.CompletionID) + writeUint32(buf, s.DirectoryID) + if err := encodeString(buf, s.Path); err != nil { + return nil, trace.Wrap(err) + } + writeUint32(buf, s.EndOfFile) + return buf.Bytes(), nil +} + +func decodeSharedDirectoryTruncateRequest(in io.Reader) (SharedDirectoryTruncateRequest, error) { + var completionID, directoryID, endOfFile uint32 + err := binary.Read(in, binary.BigEndian, &completionID) + if err != nil { + return SharedDirectoryTruncateRequest{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &directoryID) + if err != nil { + return SharedDirectoryTruncateRequest{}, trace.Wrap(err) + } + path, err := decodeString(in, tdpMaxPathLength) + if err != nil { + return SharedDirectoryTruncateRequest{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &endOfFile) + if err != nil { + return SharedDirectoryTruncateRequest{}, trace.Wrap(err) + } + return SharedDirectoryTruncateRequest{ + CompletionID: completionID, + DirectoryID: directoryID, + Path: path, + EndOfFile: endOfFile, + }, nil +} + +// SharedDirectoryTruncateResponse is sent from the TDP client to the server +// to acknowledge a SharedDirectoryTruncateRequest was executed. +// | message type (34) | completion_id uint32 | err_code uint32 | +type SharedDirectoryTruncateResponse struct { + CompletionID uint32 + ErrCode uint32 +} + +func (s SharedDirectoryTruncateResponse) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryTruncateResponse)) + writeUint32(buf, s.CompletionID) + writeUint32(buf, s.ErrCode) + return buf.Bytes(), nil +} + +func decodeSharedDirectoryTruncateResponse(in io.Reader) (SharedDirectoryTruncateResponse, error) { + var res SharedDirectoryTruncateResponse + err := binary.Read(in, binary.BigEndian, &res) + return res, err +} + // 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/rfd/0067-desktop-access-file-system-sharing.md b/rfd/0067-desktop-access-file-system-sharing.md index 219e460465b9a..1af652944d8ad 100644 --- a/rfd/0067-desktop-access-file-system-sharing.md +++ b/rfd/0067-desktop-access-file-system-sharing.md @@ -420,7 +420,7 @@ This message is sent by the server to the client to request the contents of a di `directory_id` is the `directory_id` of the top level directory being shared, as specified in a previous `Announce Shared Directory` message. -`path_length` is the length in bytes of the `path` +`path_length` is the length in bytes of the `path`. `path` is the unix-style relative path (from the root-level directory specified by `directory_id`) to the file or directory, excepting special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, will result in an error. Info can be requested for the root-level @@ -443,6 +443,36 @@ This message is sent by the client to the server in response to a `Shared Direct `fso_list` is a list of `fso` objects representing all the files and directories in the directory specified by the `path` variable in the originating `Shared Directory List Request`. +#### 33 - Shared Directory Truncate Request + +``` +| message type (33) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | end_of_file uint32 | +``` + +This message is sent by the server to the client to request a file be truncated to `end_of_file` bytes. + +`completion_id` is generated by the server and must be returned by the client in its corresponding `Shared Directory Truncate Response` or `Shared Directory Error` + +`directory_id` is the `directory_id` of the top level directory being shared, as specified in a previous `Announce Shared Directory` message. + +`path_length` is the length in bytes of the `path`. + +`path` is the unix-style relative path (from the root-level directory specified by `directory_id`) to the file which should be truncated. + +`end_of_file` is the new size of the file in bytes. + +#### 34 - Shared Directory Truncate Response + +``` +| message type (34) | completion_id uint32 | err_code uint32 | +``` + +This message is sent by the client to the server in response to a `Shared Directory Truncate Request` to alert the server of a successful truncate operation. + +`completion_id` must match the `completion_id` of the `Shared Directory Truncate Request` that this message is responding to. + +`err_code` is an error code. If the file does not exist or the path is to a directory, this field should be set to `2` ("resource does not exist"). For any other error, this field should be set to `1` ("operation failed"). + ##### File System Object (fso) ``` @@ -471,7 +501,7 @@ types available in RDP's [File Attributes](https://docs.microsoft.com/en-us/open `is_empty` says whether or not a directory is empty. For objects where `file_type = 0` (files), this field should be ignored. -`path_length` is the length in bytes of the `path` +`path_length` is the length in bytes of the `path`. `path` is the unix-style relative path (from the root-level directory specified by `directory_id`) to the file or directory, excluding special path characters ".", "..". Absolute paths, or those containing path elements with either of the special characters, are considered an error. A root-level shared directory is specified diff --git a/web/packages/teleport/src/lib/tdp/client.ts b/web/packages/teleport/src/lib/tdp/client.ts index 8781b4c1d7c00..ddd89262bf09b 100644 --- a/web/packages/teleport/src/lib/tdp/client.ts +++ b/web/packages/teleport/src/lib/tdp/client.ts @@ -55,6 +55,7 @@ import type { SharedDirectoryDeleteResponse, FileSystemObject, SyncKeys, + SharedDirectoryTruncateResponse, } from './codec'; import type { WebauthnAssertionResponse } from 'teleport/services/auth'; @@ -254,6 +255,9 @@ export default class Client extends EventEmitterWebAuthnSender { case MessageType.SHARED_DIRECTORY_LIST_REQUEST: this.handleSharedDirectoryListRequest(buffer); break; + case MessageType.SHARED_DIRECTORY_TRUNCATE_REQUEST: + this.handleSharedDirectoryTruncateRequest(buffer); + break; default: this.logger.warn(`received unsupported message type ${messageType}`); } @@ -563,6 +567,19 @@ export default class Client extends EventEmitterWebAuthnSender { } } + async handleSharedDirectoryTruncateRequest(buffer: ArrayBuffer) { + const req = this.codec.decodeSharedDirectoryTruncateRequest(buffer); + try { + await this.sdManager.truncateFile(req.path, req.endOfFile); + this.sendSharedDirectoryTruncateResponse({ + completionId: req.completionId, + errCode: SharedDirectoryErrCode.Nil, + }); + } catch (e) { + this.handleError(e, TdpClientEvent.CLIENT_ERROR); + } + } + private toFso(info: FileOrDirInfo): FileSystemObject { return { lastModified: BigInt(info.lastModified), @@ -684,6 +701,12 @@ export default class Client extends EventEmitterWebAuthnSender { this.send(this.codec.encodeSharedDirectoryDeleteResponse(response)); } + sendSharedDirectoryTruncateResponse( + response: SharedDirectoryTruncateResponse + ) { + this.send(this.codec.encodeSharedDirectoryTruncateResponse(response)); + } + resize(spec: ClientScreenSpec) { this.send(this.codec.encodeClientScreenSpec(spec)); } diff --git a/web/packages/teleport/src/lib/tdp/codec.ts b/web/packages/teleport/src/lib/tdp/codec.ts index bfca1ed008c53..ff32bc83eb6c9 100644 --- a/web/packages/teleport/src/lib/tdp/codec.ts +++ b/web/packages/teleport/src/lib/tdp/codec.ts @@ -53,6 +53,8 @@ export enum MessageType { RDP_RESPONSE_PDU = 30, RDP_CONNECTION_INITIALIZED = 31, SYNC_KEYS = 32, + SHARED_DIRECTORY_TRUNCATE_REQUEST = 33, + SHARED_DIRECTORY_TRUNCATE_RESPONSE = 34, __LAST, // utility value } @@ -276,6 +278,20 @@ export type SharedDirectoryListResponse = { fsoList: FileSystemObject[]; }; +// | message type (33) | completion_id uint32 | directory_id uint32 | path_length uint32 | path []byte | end_of_file uint32 | +export type SharedDirectoryTruncateRequest = { + completionId: number; + directoryId: number; + path: string; + endOfFile: number; +}; + +// | message type (34) | completion_id uint32 | err_code uint32 | +export type SharedDirectoryTruncateResponse = { + completionId: number; + errCode: SharedDirectoryErrCode; +}; + // | last_modified uint64 | size uint64 | file_type uint32 | is_empty bool | path_length uint32 | path byte[] | export type FileSystemObject = { lastModified: bigint; @@ -556,19 +572,10 @@ export default class Codec { encodeSharedDirectoryDeleteResponse( res: SharedDirectoryDeleteResponse ): Message { - const bufLen = BYTE_LEN + 2 * UINT_32_LEN; - const buffer = new ArrayBuffer(bufLen); - const view = new DataView(buffer); - let offset = 0; - - view.setUint8(offset, MessageType.SHARED_DIRECTORY_DELETE_RESPONSE); - offset += BYTE_LEN; - view.setUint32(offset, res.completionId); - offset += UINT_32_LEN; - view.setUint32(offset, res.errCode); - offset += UINT_32_LEN; - - return buffer; + return this.encodeGenericResponse( + MessageType.SHARED_DIRECTORY_DELETE_RESPONSE, + res + ); } // | message type (20) | completion_id uint32 | err_code uint32 | read_data_length uint32 | read_data []byte | @@ -616,19 +623,10 @@ export default class Codec { // | message type (24) | completion_id uint32 | err_code uint32 | encodeSharedDirectoryMoveResponse(res: SharedDirectoryMoveResponse): Message { - const bufLen = BYTE_LEN + 2 * UINT_32_LEN; - const buffer = new ArrayBuffer(bufLen); - const view = new DataView(buffer); - let offset = 0; - - view.setUint8(offset, MessageType.SHARED_DIRECTORY_MOVE_RESPONSE); - offset += BYTE_LEN; - view.setUint32(offset, res.completionId); - offset += UINT_32_LEN; - view.setUint32(offset, res.errCode); - offset += UINT_32_LEN; - - return buffer; + return this.encodeGenericResponse( + MessageType.SHARED_DIRECTORY_MOVE_RESPONSE, + res + ); } // | message type (26) | completion_id uint32 | err_code uint32 | fso_list_length uint32 | fso_list fso[] | @@ -660,6 +658,37 @@ export default class Codec { return withFsoList.buffer; } + encodeSharedDirectoryTruncateResponse( + res: SharedDirectoryTruncateResponse + ): Message { + return this.encodeGenericResponse( + MessageType.SHARED_DIRECTORY_TRUNCATE_RESPONSE, + res + ); + } + + private encodeGenericResponse( + type: MessageType, + res: { + completionId: number; + errCode: SharedDirectoryErrCode; + } + ): Message { + const bufLen = BYTE_LEN + 2 * UINT_32_LEN; + const buffer = new ArrayBuffer(bufLen); + const view = new DataView(buffer); + let offset = 0; + + view.setUint8(offset, type); + offset += BYTE_LEN; + view.setUint32(offset, res.completionId); + offset += UINT_32_LEN; + view.setUint32(offset, res.errCode); + offset += UINT_32_LEN; + + return buffer; + } + // | last_modified uint64 | size uint64 | file_type uint32 | is_empty bool | path_length uint32 | path byte[] | encodeFileSystemObject(fso: FileSystemObject): Message { const dataUtf8array = this.encoder.encode(fso.path); @@ -1057,6 +1086,31 @@ export default class Codec { return this.decodeSharedDirectoryInfoRequest(buffer); } + decodeSharedDirectoryTruncateRequest( + buffer: ArrayBuffer + ): SharedDirectoryTruncateRequest { + const dv = new DataView(buffer); + let bufOffset = BYTE_LEN; // eat message type + const completionId = dv.getUint32(bufOffset); + bufOffset += UINT_32_LEN; // eat completion_id + const directoryId = dv.getUint32(bufOffset); + bufOffset += UINT_32_LEN; // eat directory_id + const pathLength = dv.getUint32(bufOffset); + bufOffset += UINT_32_LEN; // eat path_length + const path = this.decoder.decode( + new Uint8Array(buffer, bufOffset, pathLength) + ); + bufOffset += pathLength; // eat path + const endOfFile = dv.getUint32(bufOffset); + + return { + completionId, + directoryId, + path, + endOfFile, + }; + } + // asBase64Url creates a data:image uri from the png data part of a PNG_FRAME tdp message. private asBase64Url(buffer: ArrayBuffer, offset: number): string { return `data:image/png;base64,${arrayBufferToBase64(buffer.slice(offset))}`; diff --git a/web/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts b/web/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts index 637b8f91536cd..53f700974424e 100644 --- a/web/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts +++ b/web/packages/teleport/src/lib/tdp/sharedDirectoryManager.ts @@ -127,14 +127,8 @@ export class SharedDirectoryManager { length: number ): Promise { this.checkReady(); - - const fileHandle = await this.walkPath(path); - if (fileHandle.kind !== 'file') { - throw new Error('cannot read the bytes of a directory'); - } - + const fileHandle = await this.getFileHandle(path); const file = await fileHandle.getFile(); - return new Uint8Array( await file.slice(Number(offset), Number(offset) + length).arrayBuffer() ); @@ -152,11 +146,7 @@ export class SharedDirectoryManager { ): Promise { this.checkReady(); - const fileHandle = await this.walkPath(path); - if (fileHandle.kind !== 'file') { - throw new Error('cannot read the bytes of a directory'); - } - + const fileHandle = await this.getFileHandle(path); const file = await fileHandle.createWritable({ keepExistingData: true }); await file.write({ type: 'write', position: Number(offset), data }); await file.close(); // Needed to actually write data to disk. @@ -164,6 +154,19 @@ export class SharedDirectoryManager { return data.length; } + /** + * Truncates the file at path to size bytes. + * @throws Will throw an error if a directory has not already been initialized via add(). + * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory + */ + async truncateFile(path: string, size: number): Promise { + this.checkReady(); + const fileHandle = await this.getFileHandle(path); + const file = await fileHandle.createWritable({ keepExistingData: true }); + await file.truncate(size); + await file.close(); + } + /** * Creates a new file or directory (determined by fileType) at path. * If the path already exists for the given fileType, this operation is effectively ignored. @@ -175,14 +178,7 @@ export class SharedDirectoryManager { let splitPath = path.split('/'); const fileOrDirName = splitPath.pop(); const dirPath = splitPath.join('/'); - - const dirHandle = await this.walkPath(dirPath); - if (dirHandle.kind !== 'directory') { - throw new PathDoesNotExistError( - 'destination was a file, not a directory' - ); - } - + const dirHandle = await this.getDirectoryHandle(dirPath); if (fileType === FileType.File) { await dirHandle.getFileHandle(fileOrDirName, { create: true }); } else { @@ -200,15 +196,36 @@ export class SharedDirectoryManager { let splitPath = path.split('/'); const fileOrDirName = splitPath.pop(); const dirPath = splitPath.join('/'); + const dirHandle = await this.getDirectoryHandle(dirPath); + await dirHandle.removeEntry(fileOrDirName, { recursive: true }); + } - const dirHandle = await this.walkPath(dirPath); - if (dirHandle.kind !== 'directory') { - throw new PathDoesNotExistError( - 'destination was a file, not a directory' - ); + /** + * Returns the FileSystemFileHandle for the file at path. + * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory + * @throws {Error} if the pathstr points to a directory + */ + private async getFileHandle(pathstr: string): Promise { + const fileHandle = await this.walkPath(pathstr); + if (fileHandle.kind !== 'file') { + throw new Error('cannot read the bytes of a directory'); } + return fileHandle; + } - await dirHandle.removeEntry(fileOrDirName, { recursive: true }); + /** + * Returns the FileSystemDirectoryHandle for the directory at path. + * @throws {PathDoesNotExistError} if the pathstr isn't a valid path in the shared directory + * @throws {Error} if the pathstr points to a file + */ + private async getDirectoryHandle( + pathstr: string + ): Promise { + const dirHandle = await this.walkPath(pathstr); + if (dirHandle.kind !== 'directory') { + throw new Error('cannot list the contents of a file'); + } + return dirHandle; } /** From 197a123e8788b121e9131878a509f7f2452c7e92 Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Mon, 1 Apr 2024 10:24:54 -0700 Subject: [PATCH 2/2] Adds note on terminology --- rfd/0067-desktop-access-file-system-sharing.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rfd/0067-desktop-access-file-system-sharing.md b/rfd/0067-desktop-access-file-system-sharing.md index 1af652944d8ad..f1870de298c93 100644 --- a/rfd/0067-desktop-access-file-system-sharing.md +++ b/rfd/0067-desktop-access-file-system-sharing.md @@ -173,6 +173,11 @@ we must extend the [TDP protocol](https://github.com/gravitational/teleport/blob Each `* Request` (such as `Shared Directory Info Request`, `Shared Directory Create Request`, etc.) and `* Response` (`Shared Directory Info Response`, `Shared Directory Create Response`, etc.) TDP message contains a `completion_id` field, with `* Request`s being responsible for generating the `completion_id`s, and `* Response`s being responsible for including the correct `completion_id` to signify which `* Request` the response is intended for. +##### Note on terminology + +From here on out, the term "client" will refer to the browser-based TDP client, and the term "server" will refer to the Windows Desktop Service based TDP server, unless otherwise +specified. + #### 11 - Shared Directory Announce ```