diff --git a/Cargo.lock b/Cargo.lock index 3aedc13fbc538..532965eda7237 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "arrayvec" @@ -72,9 +72,9 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "base64ct" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" +checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" [[package]] name = "bit_field" @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "delog" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb73cae03ad02cd38353f93fe84b288daffcb6371212226e09b9a4c7fc93b03f" +checksum = "e371811fb858c17e75e0316e7ebf8db0343b10d73038a3b9571006b0e7e5cc53" dependencies = [ "log", ] @@ -387,13 +387,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -407,15 +407,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" [[package]] name = "heapless" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983" +checksum = "065681e99f9ef7e0e813702a0326aedbcbbde7db5e55f097aedd1bf50b9dca43" dependencies = [ "atomic-polyfill", "hash32", @@ -457,9 +457,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "indexmap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", @@ -504,9 +504,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" dependencies = [ "wasm-bindgen", ] @@ -821,9 +821,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ "unicode-ident", ] @@ -885,9 +885,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] @@ -951,7 +951,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -987,7 +987,7 @@ dependencies = [ [[package]] name = "rdp-rs" version = "0.1.0" -source = "git+https://github.com/gravitational/rdp-rs?rev=17ec446ecb73c58b77ac47c6fc8598153f673076#17ec446ecb73c58b77ac47c6fc8598153f673076" +source = "git+https://github.com/gravitational/rdp-rs?rev=6e4623bd6939759e6bdd32a9806cd2579a3b5d8f#6e4623bd6939759e6bdd32a9806cd2579a3b5d8f" dependencies = [ "bufstream", "byteorder", @@ -1195,9 +1195,9 @@ checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" [[package]] name = "smallvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "cc88c725d61fc6c3132893370cac4a0200e3fedf5da8331c570664b1987f5ca2" [[package]] name = "spin" @@ -1250,9 +1250,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.96" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", @@ -1284,11 +1284,12 @@ dependencies = [ [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -1309,9 +1310,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "untrusted" @@ -1340,7 +1341,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -1378,15 +1379,21 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1394,9 +1401,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" dependencies = [ "bumpalo", "lazy_static", @@ -1409,9 +1416,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1419,9 +1426,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" dependencies = [ "proc-macro2", "quote", @@ -1432,15 +1439,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/lib/srv/desktop/rdp/rdpclient/Cargo.toml b/lib/srv/desktop/rdp/rdpclient/Cargo.toml index 2224b2ed336f2..e800bdf24251d 100644 --- a/lib/srv/desktop/rdp/rdpclient/Cargo.toml +++ b/lib/srv/desktop/rdp/rdpclient/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2.15" rand = { version = "0.8.5", features = ["getrandom"] } rand_chacha = "0.3.1" rsa = "0.6.1" -rdp-rs = { git = "https://github.com/gravitational/rdp-rs", rev = "17ec446ecb73c58b77ac47c6fc8598153f673076" } +rdp-rs = { git = "https://github.com/gravitational/rdp-rs", rev = "6e4623bd6939759e6bdd32a9806cd2579a3b5d8f" } 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 75beaaf40534c..1ded1c32a5cc4 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -433,6 +433,41 @@ func (c *Client) start() { return } } + case tdp.SharedDirectoryListResponse: + if c.cfg.AllowDirectorySharing { + fsoList := make([]C.CGOFileSystemObject, 0, len(m.FsoList)) + + for _, fso := range m.FsoList { + path := C.CString(fso.Path) + defer C.free(unsafe.Pointer(path)) + + fsoList = append(fsoList, C.CGOFileSystemObject{ + last_modified: C.uint64_t(fso.LastModified), + size: C.uint64_t(fso.Size), + file_type: fso.FileType, + path: path, + }) + } + + fsoListLen := len(fsoList) + var cgoFsoList *C.CGOFileSystemObject + + if fsoListLen > 0 { + cgoFsoList = (*C.CGOFileSystemObject)(unsafe.Pointer(&fsoList[0])) + } else { + cgoFsoList = (*C.CGOFileSystemObject)(unsafe.Pointer(&fsoList)) + } + + if errCode := C.handle_tdp_sd_list_response(c.rustClient, C.CGOSharedDirectoryListResponse{ + completion_id: C.uint32_t(m.CompletionID), + err_code: m.ErrCode, + fso_list_length: C.uint32_t(fsoListLen), + fso_list: cgoFsoList, + }); errCode != C.ErrCodeSuccess { + c.cfg.Log.Errorf("SharedDirectoryListResponse failed: %v", errCode) + return + } + } default: c.cfg.Log.Warningf("Skipping unimplemented TDP message type %T", msg) } @@ -574,6 +609,25 @@ func (c *Client) sharedDirectoryDeleteRequest(req tdp.SharedDirectoryDeleteReque return C.ErrCodeSuccess } +//export tdp_sd_list_request +func tdp_sd_list_request(handle C.uintptr_t, req *C.CGOSharedDirectoryListRequest) C.CGOErrCode { + return cgo.Handle(handle).Value().(*Client).sharedDirectoryListRequest(tdp.SharedDirectoryListRequest{ + CompletionID: uint32(req.completion_id), + DirectoryID: uint32(req.directory_id), + Path: C.GoString(req.path), + }) +} + +func (c *Client) sharedDirectoryListRequest(req tdp.SharedDirectoryListRequest) C.CGOErrCode { + if c.cfg.AllowDirectorySharing { + if err := c.cfg.Conn.OutputMessage(req); err != nil { + c.cfg.Log.Errorf("failed to send SharedDirectoryAcknowledge: %v", err) + return C.ErrCodeFailure + } + } + return C.ErrCodeSuccess +} + // close frees the memory of the cgo.Handle, // closes the RDP client connection, // and frees the Rust client. diff --git a/lib/srv/desktop/rdp/rdpclient/librdprs.h b/lib/srv/desktop/rdp/rdpclient/librdprs.h index 7d298f23bb3c8..5738f205fac68 100644 --- a/lib/srv/desktop/rdp/rdpclient/librdprs.h +++ b/lib/srv/desktop/rdp/rdpclient/librdprs.h @@ -94,10 +94,6 @@ typedef struct ClientOrError { enum CGOErrCode err; } ClientOrError; -/** - * CGOSharedDirectoryAnnounce is sent by the TDP client to the server - * to announce a new directory to be shared over TDP. - */ typedef struct CGOSharedDirectoryAnnounce { uint32_t directory_id; const char *name; @@ -129,6 +125,13 @@ typedef struct SharedDirectoryCreateResponse CGOSharedDirectoryCreateResponse; typedef struct SharedDirectoryCreateResponse CGOSharedDirectoryDeleteResponse; +typedef struct CGOSharedDirectoryListResponse { + uint32_t completion_id; + enum TdpErrCode err_code; + uint32_t fso_list_length; + struct CGOFileSystemObject *fso_list; +} CGOSharedDirectoryListResponse; + /** * 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. @@ -192,11 +195,9 @@ typedef struct CGOSharedDirectoryCreateRequest { const char *path; } CGOSharedDirectoryCreateRequest; -typedef struct CGOSharedDirectoryDeleteRequest { - uint32_t completion_id; - uint32_t directory_id; - const char *path; -} CGOSharedDirectoryDeleteRequest; +typedef struct CGOSharedDirectoryInfoRequest CGOSharedDirectoryDeleteRequest; + +typedef struct CGOSharedDirectoryInfoRequest CGOSharedDirectoryListRequest; void init(void); @@ -228,17 +229,27 @@ struct ClientOrError connect_rdp(uintptr_t go_ref, * * # Safety * - * `client_ptr` must be a valid pointer to a Client. + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) + * + * data MUST be a valid pointer. + * (validity defined by the validity of data in https://doc.rust-lang.org/std/slice/fn.from_raw_parts_mut.html) */ -enum CGOErrCode update_clipboard(struct Client *client_ptr, uint8_t *data, uint32_t len); +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. + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) + * + * sd_announce.name MUST be a non-null pointer to a C-style null terminated string. */ enum CGOErrCode handle_tdp_sd_announce(struct Client *client_ptr, struct CGOSharedDirectoryAnnounce sd_announce); @@ -249,7 +260,10 @@ enum CGOErrCode handle_tdp_sd_announce(struct Client *client_ptr, * * # Safety * - * The caller must ensure that res.fso.path points to a valid buffer. + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) + * + * res.fso.path MUST be a non-null pointer to a C-style null terminated string. */ enum CGOErrCode handle_tdp_sd_info_response(struct Client *client_ptr, struct CGOSharedDirectoryInfoResponse res); @@ -260,7 +274,8 @@ enum CGOErrCode handle_tdp_sd_info_response(struct Client *client_ptr, * * # Safety * - * client_ptr must be a valid pointer + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) */ enum CGOErrCode handle_tdp_sd_create_response(struct Client *client_ptr, CGOSharedDirectoryCreateResponse res); @@ -271,11 +286,28 @@ enum CGOErrCode handle_tdp_sd_create_response(struct Client *client_ptr, * * # Safety * - * client_ptr must be a valid pointer + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) */ enum CGOErrCode handle_tdp_sd_delete_response(struct Client *client_ptr, CGOSharedDirectoryDeleteResponse res); +/** + * handle_tdp_sd_list_response handles a TDP Shared Directory List Response message. + * + * # Safety + * + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) + * + * res.fso_list MUST be a valid pointer + * (validity defined by the validity of data in https://doc.rust-lang.org/std/slice/fn.from_raw_parts_mut.html) + * + * each res.fso_list[i].path MUST be a non-null pointer to a C-style null terminated string. + */ +enum CGOErrCode handle_tdp_sd_list_response(struct Client *client_ptr, + struct CGOSharedDirectoryListResponse res); + /** * `read_rdp_output` reads incoming RDP bitmap frames from client at client_ref and forwards them to * handle_bitmap. @@ -290,16 +322,20 @@ enum CGOErrCode read_rdp_output(struct Client *client_ptr); /** * # Safety * - * client_ptr must be a valid pointer to a Client. + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) */ -enum CGOErrCode write_rdp_pointer(struct Client *client_ptr, struct CGOMousePointerEvent pointer); +enum CGOErrCode write_rdp_pointer(struct Client *client_ptr, + struct CGOMousePointerEvent pointer); /** * # Safety * - * client_ptr must be a valid pointer to a Client. + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) */ -enum CGOErrCode write_rdp_keyboard(struct Client *client_ptr, struct CGOKeyboardEvent key); +enum CGOErrCode write_rdp_keyboard(struct Client *client_ptr, + struct CGOKeyboardEvent key); /** * # Safety @@ -313,7 +349,8 @@ enum CGOErrCode close_rdp(struct Client *client_ptr); * * # Safety * - * client_ptr must be a valid pointer to a Client. + * client_ptr MUST be a valid pointer. + * (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) */ void free_rdp(struct Client *client_ptr); @@ -330,4 +367,7 @@ extern enum CGOErrCode tdp_sd_create_request(uintptr_t client_ref, struct CGOSharedDirectoryCreateRequest *req); extern enum CGOErrCode tdp_sd_delete_request(uintptr_t client_ref, - struct CGOSharedDirectoryDeleteRequest *req); + CGOSharedDirectoryDeleteRequest *req); + +extern enum CGOErrCode tdp_sd_list_request(uintptr_t client_ref, + CGOSharedDirectoryListRequest *req); diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index 3a4ab7cf296a3..a03f404aa0e3c 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -24,6 +24,7 @@ extern crate log; #[macro_use] extern crate num_derive; +use errors::try_error; use libc::{fd_set, select, FD_SET}; use rand::Rng; use rand::SeedableRng; @@ -135,8 +136,8 @@ pub unsafe extern "C" fn connect_rdp( // Convert from C to Rust types. let addr = from_go_string(go_addr); let username = from_go_string(go_username); - let cert_der = from_go_array(cert_der_len, cert_der); - let key_der = from_go_array(key_der_len, key_der); + let cert_der = from_go_array(cert_der, cert_der_len); + let key_der = from_go_array(key_der, key_der_len); connect_rdp_inner( go_ref, @@ -255,7 +256,7 @@ fn connect_rdp_inner( let tdp_sd_acknowledge = Box::new( move |mut ack: SharedDirectoryAcknowledge| -> RdpResult<()> { - debug!("sending: {:?}", ack); + debug!("sending TDP SharedDirectoryAcknowledge: {:?}", ack); unsafe { if tdp_sd_acknowledge(go_ref, &mut ack) != CGOErrCode::ErrCodeSuccess { return Err(RdpError::TryError(String::from( @@ -268,7 +269,7 @@ fn connect_rdp_inner( ); let tdp_sd_info_request = Box::new(move |req: SharedDirectoryInfoRequest| -> RdpResult<()> { - debug!("sending: {:?}", req); + debug!("sending TDP SharedDirectoryInfoRequest: {:?}", req); // Create C compatible string from req.path match CString::new(req.path.clone()) { Ok(c_string) => { @@ -301,7 +302,7 @@ fn connect_rdp_inner( let tdp_sd_create_request = Box::new(move |req: SharedDirectoryCreateRequest| -> RdpResult<()> { - debug!("sending: {:?}", req); + debug!("sending TDP SharedDirectoryCreateRequest: {:?}", req); // Create C compatible string from req.path match CString::new(req.path.clone()) { Ok(c_string) => { @@ -335,7 +336,7 @@ fn connect_rdp_inner( let tdp_sd_delete_request = Box::new(move |req: SharedDirectoryDeleteRequest| -> RdpResult<()> { - debug!("sending: {:?}", req); + debug!("sending TDP SharedDirectoryDeleteRequest: {:?}", req); // Create C compatible string from req.path match CString::new(req.path.clone()) { Ok(c_string) => { @@ -366,6 +367,38 @@ fn connect_rdp_inner( } }); + let tdp_sd_list_request = Box::new(move |req: SharedDirectoryListRequest| -> RdpResult<()> { + debug!("sending TDP SharedDirectoryListRequest: {:?}", req); + // Create C compatible string from req.path + match CString::new(req.path.clone()) { + Ok(c_string) => { + unsafe { + let err = tdp_sd_list_request( + go_ref, + &mut CGOSharedDirectoryListRequest { + completion_id: req.completion_id, + directory_id: req.directory_id, + path: c_string.as_ptr(), + }, + ); + if err != CGOErrCode::ErrCodeSuccess { + return Err(RdpError::TryError(String::from( + "call to tdp_sd_list_request failed", + ))); + }; + } + Ok(()) + } + Err(_) => { + // TODO(isaiah): change TryError to TeleportError for a generic error caused by Teleport specific code. + return Err(RdpError::TryError(format!( + "path contained characters that couldn't be converted to a C string: {}", + req.path + ))); + } + } + }); + // Client for the "rdpdr" channel - smartcard emulation and drive redirection. let rdpdr = rdpdr::Client::new(rdpdr::Config { cert_der: params.cert_der, @@ -376,6 +409,7 @@ fn connect_rdp_inner( tdp_sd_info_request, tdp_sd_create_request, tdp_sd_delete_request, + tdp_sd_list_request, }); // Client for the "cliprdr" channel - clipboard sharing. @@ -484,6 +518,13 @@ impl RdpClient { self.rdpdr.handle_tdp_sd_delete_response(res, &mut self.mcs) } + pub fn handle_tdp_sd_list_response( + &mut self, + res: SharedDirectoryListResponse, + ) -> RdpResult<()> { + self.rdpdr.handle_tdp_sd_list_response(res, &mut self.mcs) + } + pub fn shutdown(&mut self) -> RdpResult<()> { self.mcs.shutdown() } @@ -568,7 +609,11 @@ fn wait_for_fd(fd: usize) -> bool { /// /// # Safety /// -/// `client_ptr` must be a valid pointer to a Client. +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) +/// +/// data MUST be a valid pointer. +/// (validity defined by the validity of data in https://doc.rust-lang.org/std/slice/fn.from_raw_parts_mut.html) #[no_mangle] pub unsafe extern "C" fn update_clipboard( client_ptr: *mut Client, @@ -581,7 +626,7 @@ pub unsafe extern "C" fn update_clipboard( return cgo_error; } }; - let data = from_go_array(len, data); + let data = from_go_array(data, len); let mut lock = client.rdp_client.lock().unwrap(); match lock.cliprdr { @@ -609,14 +654,26 @@ 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. +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) +/// +/// sd_announce.name MUST be a non-null pointer to a C-style null terminated string. #[no_mangle] pub unsafe extern "C" fn handle_tdp_sd_announce( client_ptr: *mut Client, sd_announce: CGOSharedDirectoryAnnounce, ) -> CGOErrCode { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + + // Clone here to ensure nothing the CGO object is passed to can hang on to it or any of its pointers. + let sd_announce = SharedDirectoryAnnounce::from(sd_announce); + let client = match Client::from_ptr(client_ptr) { Ok(client) => client, Err(cgo_error) => { @@ -624,9 +681,8 @@ pub unsafe extern "C" fn handle_tdp_sd_announce( } }; - let drive_name = from_go_string(sd_announce.name); let new_drive = - rdpdr::ClientDeviceListAnnounce::new_drive(sd_announce.directory_id, drive_name); + rdpdr::ClientDeviceListAnnounce::new_drive(sd_announce.directory_id, sd_announce.name); let mut rdp_client = client.rdp_client.lock().unwrap(); match rdp_client.write_client_device_list_announce(new_drive) { @@ -643,12 +699,23 @@ pub unsafe extern "C" fn handle_tdp_sd_announce( /// /// # Safety /// -/// The caller must ensure that res.fso.path points to a valid buffer. +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) +/// +/// res.fso.path MUST be a non-null pointer to a C-style null terminated string. #[no_mangle] pub unsafe extern "C" fn handle_tdp_sd_info_response( client_ptr: *mut Client, res: CGOSharedDirectoryInfoResponse, ) -> CGOErrCode { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + + // Clone here to ensure nothing the CGO object is passed to can hang on to it or any of its pointers. + let res = SharedDirectoryInfoResponse::from(res); + let client = match Client::from_ptr(client_ptr) { Ok(client) => client, Err(cgo_error) => { @@ -657,7 +724,7 @@ pub unsafe extern "C" fn handle_tdp_sd_info_response( }; let mut rdp_client = client.rdp_client.lock().unwrap(); - match rdp_client.handle_tdp_sd_info_response(SharedDirectoryInfoResponse::from(res)) { + match rdp_client.handle_tdp_sd_info_response(res) { Ok(()) => CGOErrCode::ErrCodeSuccess, Err(e) => { error!("failed to handle Shared Directory Info Response: {:?}", e); @@ -671,12 +738,22 @@ pub unsafe extern "C" fn handle_tdp_sd_info_response( /// /// # Safety /// -/// client_ptr must be a valid pointer +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) #[no_mangle] pub unsafe extern "C" fn handle_tdp_sd_create_response( client_ptr: *mut Client, res: CGOSharedDirectoryCreateResponse, ) -> CGOErrCode { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + + // Clone here to ensure nothing the CGO object is passed to can hang on to it or any of its pointers. + #[allow(clippy::redundant_clone)] + let res: SharedDirectoryCreateResponse = res.clone(); + let client = match Client::from_ptr(client_ptr) { Ok(client) => client, Err(cgo_error) => { @@ -699,12 +776,22 @@ pub unsafe extern "C" fn handle_tdp_sd_create_response( /// /// # Safety /// -/// client_ptr must be a valid pointer +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) #[no_mangle] pub unsafe extern "C" fn handle_tdp_sd_delete_response( client_ptr: *mut Client, res: CGOSharedDirectoryDeleteResponse, ) -> CGOErrCode { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + + // Clone here to ensure nothing the CGO object is passed to can hang on to it or any of its pointers. + #[allow(clippy::redundant_clone)] + let res: SharedDirectoryDeleteResponse = res.clone(); + let client = match Client::from_ptr(client_ptr) { Ok(client) => client, Err(cgo_error) => { @@ -722,6 +809,47 @@ pub unsafe extern "C" fn handle_tdp_sd_delete_response( } } +/// handle_tdp_sd_list_response handles a TDP Shared Directory List Response message. +/// +/// # Safety +/// +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) +/// +/// res.fso_list MUST be a valid pointer +/// (validity defined by the validity of data in https://doc.rust-lang.org/std/slice/fn.from_raw_parts_mut.html) +/// +/// each res.fso_list[i].path MUST be a non-null pointer to a C-style null terminated string. +#[no_mangle] +pub unsafe extern "C" fn handle_tdp_sd_list_response( + client_ptr: *mut Client, + res: CGOSharedDirectoryListResponse, +) -> CGOErrCode { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + + // Clone here to ensure nothing the CGO object is passed to can hang on to it or any of its pointers. + let res = SharedDirectoryListResponse::from(res); + + let client = match Client::from_ptr(client_ptr) { + Ok(client) => client, + Err(cgo_error) => { + return cgo_error; + } + }; + + let mut rdp_client = client.rdp_client.lock().unwrap(); + match rdp_client.handle_tdp_sd_list_response(res) { + Ok(()) => CGOErrCode::ErrCodeSuccess, + Err(e) => { + error!("failed to handle Shared Directory List Response: {:?}", e); + CGOErrCode::ErrCodeFailure + } + } +} + /// `read_rdp_output` reads incoming RDP bitmap frames from client at client_ref and forwards them to /// handle_bitmap. /// @@ -828,6 +956,10 @@ pub enum CGOPointerWheel { impl From for PointerEvent { fn from(p: CGOMousePointerEvent) -> PointerEvent { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. PointerEvent { x: p.x, y: p.y, @@ -850,7 +982,8 @@ impl From for PointerEvent { /// # Safety /// -/// client_ptr must be a valid pointer to a Client. +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) #[no_mangle] pub unsafe extern "C" fn write_rdp_pointer( client_ptr: *mut Client, @@ -890,6 +1023,10 @@ pub struct CGOKeyboardEvent { impl From for KeyboardEvent { fn from(k: CGOKeyboardEvent) -> KeyboardEvent { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. KeyboardEvent { code: k.code, down: k.down, @@ -899,7 +1036,8 @@ impl From for KeyboardEvent { /// # Safety /// -/// client_ptr must be a valid pointer to a Client. +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) #[no_mangle] pub unsafe extern "C" fn write_rdp_keyboard( client_ptr: *mut Client, @@ -947,7 +1085,8 @@ pub unsafe extern "C" fn close_rdp(client_ptr: *mut Client) -> CGOErrCode { /// /// # Safety /// -/// client_ptr must be a valid pointer to a Client. +/// client_ptr MUST be a valid pointer. +/// (validity defined by https://doc.rust-lang.org/nightly/core/primitive.pointer.html#method.as_ref-1) #[no_mangle] pub unsafe extern "C" fn free_rdp(client_ptr: *mut Client) { drop(Client::from_raw(client_ptr)) @@ -959,14 +1098,22 @@ pub unsafe extern "C" fn free_rdp(client_ptr: *mut Client) { /// s is cloned here, and the caller is responsible for /// ensuring its memory is freed. unsafe fn from_go_string(s: *const c_char) -> String { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. CStr::from_ptr(s).to_string_lossy().into_owned() } /// # Safety /// -/// ptr must be a valid buffer of len bytes. -unsafe fn from_go_array(len: u32, ptr: *mut u8) -> Vec { - slice::from_raw_parts(ptr, len as usize).to_vec() +/// See https://doc.rust-lang.org/std/slice/fn.from_raw_parts_mut.html +unsafe fn from_go_array(data: *mut T, len: u32) -> Vec { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + slice::from_raw_parts(data, len as usize).to_vec() } #[repr(C)] @@ -976,14 +1123,34 @@ pub enum CGOErrCode { ErrCodeFailure = 1, } -/// CGOSharedDirectoryAnnounce is sent by the TDP client to the server -/// to announce a new directory to be shared over TDP. #[repr(C)] pub struct CGOSharedDirectoryAnnounce { pub directory_id: u32, pub name: *const c_char, } +/// SharedDirectoryAnnounce is sent by the TDP client to the server +/// to announce a new directory to be shared over TDP. +pub struct SharedDirectoryAnnounce { + directory_id: u32, + name: String, +} + +impl From for SharedDirectoryAnnounce { + fn from(cgo: CGOSharedDirectoryAnnounce) -> SharedDirectoryAnnounce { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + unsafe { + SharedDirectoryAnnounce { + directory_id: cgo.directory_id, + name: from_go_string(cgo.name), + } + } + } +} + /// SharedDirectoryAcknowledge is sent by the TDP server to the client /// to acknowledge that a SharedDirectoryAnnounce was received. #[derive(Debug)] @@ -1040,6 +1207,10 @@ pub struct CGOSharedDirectoryInfoResponse { impl From for SharedDirectoryInfoResponse { fn from(cgo_res: CGOSharedDirectoryInfoResponse) -> SharedDirectoryInfoResponse { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. SharedDirectoryInfoResponse { completion_id: cgo_res.completion_id, err_code: cgo_res.err_code, @@ -1048,9 +1219,9 @@ impl From for SharedDirectoryInfoResponse { } } +#[derive(Debug, Clone)] /// FileSystemObject is a TDP structure containing the metadata /// of a file or directory. -#[derive(Debug)] #[allow(dead_code)] pub struct FileSystemObject { last_modified: u64, @@ -1059,7 +1230,21 @@ pub struct FileSystemObject { path: String, } +impl FileSystemObject { + fn name(&self) -> RdpResult { + if let Some(name) = self.path.split('/').last() { + Ok(name.to_string()) + } else { + Err(try_error(&format!( + "failed to extract name from path: {:?}", + self.path + ))) + } + } +} + #[repr(C)] +#[derive(Clone)] pub struct CGOFileSystemObject { pub last_modified: u64, pub size: u64, @@ -1069,6 +1254,10 @@ pub struct CGOFileSystemObject { impl From for FileSystemObject { fn from(cgo_fso: CGOFileSystemObject) -> FileSystemObject { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. unsafe { FileSystemObject { last_modified: cgo_fso.last_modified, @@ -1120,35 +1309,66 @@ pub struct CGOSharedDirectoryCreateRequest { /// SharedDirectoryCreateResponse is sent by the TDP client to the server /// to acknowledge a SharedDirectoryCreateRequest was received and executed. -#[derive(Debug)] +#[derive(Debug, Clone)] #[repr(C)] pub struct SharedDirectoryCreateResponse { pub completion_id: u32, pub err_code: TdpErrCode, } -type CGOSharedDirectoryCreateResponse = SharedDirectoryCreateResponse; - -/// SharedDirectoryDeleteRequest is sent by the TDP server to the client -/// to request the deletion of a file or directory at path. +/// SharedDirectoryListResponse is sent by the TDP client to the server +/// in response to a SharedDirectoryInfoRequest. +#[allow(dead_code)] #[derive(Debug)] -pub struct SharedDirectoryDeleteRequest { +pub struct SharedDirectoryListResponse { completion_id: u32, - directory_id: u32, - path: String, + err_code: TdpErrCode, + fso_list: Vec, +} + +impl From for SharedDirectoryListResponse { + fn from(cgo: CGOSharedDirectoryListResponse) -> SharedDirectoryListResponse { + // # Safety + // + // This function MUST NOT hang on to any of the pointers passed in to it after it returns. + // All passed data that needs to persist after this function MUST be copied into Rust-owned memory. + unsafe { + let cgo_fso_list = from_go_array(cgo.fso_list, cgo.fso_list_length); + let mut fso_list = vec![]; + for cgo_fso in cgo_fso_list.into_iter() { + fso_list.push(FileSystemObject::from(cgo_fso)); + } + + SharedDirectoryListResponse { + completion_id: cgo.completion_id, + err_code: cgo.err_code, + fso_list, + } + } + } } #[repr(C)] -pub struct CGOSharedDirectoryDeleteRequest { - pub completion_id: u32, - pub directory_id: u32, - pub path: *const c_char, +pub struct CGOSharedDirectoryListResponse { + completion_id: u32, + err_code: TdpErrCode, + fso_list_length: u32, + fso_list: *mut CGOFileSystemObject, } +pub type CGOSharedDirectoryCreateResponse = SharedDirectoryCreateResponse; +/// SharedDirectoryDeleteRequest is sent by the TDP server to the client +/// to request the deletion of a file or directory at path. +pub type SharedDirectoryDeleteRequest = SharedDirectoryInfoRequest; +pub type CGOSharedDirectoryDeleteRequest = CGOSharedDirectoryInfoRequest; /// SharedDirectoryDeleteResponse is sent by the TDP client to the server /// to acknowledge a SharedDirectoryDeleteRequest was received and executed. pub type SharedDirectoryDeleteResponse = SharedDirectoryCreateResponse; pub type CGOSharedDirectoryDeleteResponse = SharedDirectoryCreateResponse; +/// SharedDirectoryListRequest is sent by the TDP server to the client +/// to request the contents of a directory. +pub type SharedDirectoryListRequest = SharedDirectoryInfoRequest; +pub type CGOSharedDirectoryListRequest = CGOSharedDirectoryInfoRequest; // These functions are defined on the Go side. Look for functions with '//export funcname' // comments. @@ -1170,6 +1390,10 @@ extern "C" { client_ref: usize, req: *mut CGOSharedDirectoryDeleteRequest, ) -> CGOErrCode; + fn tdp_sd_list_request( + client_ref: usize, + req: *mut CGOSharedDirectoryListRequest, + ) -> 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 5e9a7d250a32f..518e4646dcce0 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs @@ -113,6 +113,7 @@ pub enum NTSTATUS { STATUS_ACCESS_DENIED = 0xC0000022, STATUS_NOT_A_DIRECTORY = 0xC0000103, STATUS_NO_SUCH_FILE = 0xC000000F, + STATUS_NOT_SUPPORTED = 0xC00000BB, } /// 2.4 File Information Classes [MS-FSCC] diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs index 70dd377b48b9b..44df1301b7eee 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs @@ -25,7 +25,8 @@ use crate::vchan; use crate::{ FileSystemObject, FileType, Payload, SharedDirectoryAcknowledge, SharedDirectoryCreateRequest, SharedDirectoryCreateResponse, SharedDirectoryDeleteRequest, SharedDirectoryDeleteResponse, - SharedDirectoryInfoRequest, SharedDirectoryInfoResponse, TdpErrCode, + SharedDirectoryInfoRequest, SharedDirectoryInfoResponse, SharedDirectoryListRequest, + SharedDirectoryListResponse, TdpErrCode, }; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; @@ -59,7 +60,7 @@ pub struct Client { /// FileId-indexed cache of FileCacheObjects. /// See the documentation of FileCacheObject /// for more detail on how this is used. - file_cache: HashMap, + file_cache: FileCache, next_file_id: u32, // used to generate file id's // Functions for sending tdp messages to the browser client. @@ -67,11 +68,13 @@ pub struct Client { tdp_sd_info_request: SharedDirectoryInfoRequestSender, tdp_sd_create_request: SharedDirectoryCreateRequestSender, tdp_sd_delete_request: SharedDirectoryDeleteRequestSender, + tdp_sd_list_request: SharedDirectoryListRequestSender, // CompletionId-indexed maps of handlers for tdp messages coming from the browser client. pending_sd_info_resp_handlers: HashMap, pending_sd_create_resp_handlers: HashMap, pending_sd_delete_resp_handlers: HashMap, + pending_sd_list_resp_handlers: HashMap, } pub struct Config { @@ -84,6 +87,7 @@ pub struct Config { pub tdp_sd_info_request: SharedDirectoryInfoRequestSender, pub tdp_sd_create_request: SharedDirectoryCreateRequestSender, pub tdp_sd_delete_request: SharedDirectoryDeleteRequestSender, + pub tdp_sd_list_request: SharedDirectoryListRequestSender, } impl Client { @@ -99,17 +103,19 @@ impl Client { allow_directory_sharing: cfg.allow_directory_sharing, active_device_ids: vec![], - file_cache: HashMap::new(), + file_cache: FileCache::new(), next_file_id: 0, tdp_sd_acknowledge: cfg.tdp_sd_acknowledge, tdp_sd_info_request: cfg.tdp_sd_info_request, tdp_sd_create_request: cfg.tdp_sd_create_request, tdp_sd_delete_request: cfg.tdp_sd_delete_request, + tdp_sd_list_request: cfg.tdp_sd_list_request, pending_sd_info_resp_handlers: HashMap::new(), pending_sd_create_resp_handlers: HashMap::new(), pending_sd_delete_resp_handlers: HashMap::new(), + pending_sd_list_resp_handlers: HashMap::new(), } } /// Reads raw RDP messages sent on the rdpdr virtual channel and replies as necessary. @@ -264,6 +270,9 @@ impl Client { self.process_irp_query_information(device_io_request, payload) } MajorFunction::IRP_MJ_CLOSE => self.process_irp_close(device_io_request), + MajorFunction::IRP_MJ_DIRECTORY_CONTROL => { + self.process_irp_directory_control(device_io_request, payload) + } _ => Err(invalid_data_error(&format!( // TODO(isaiah): send back a not implemented response(?) "got unsupported major_function in DeviceIoRequest: {:?}", @@ -317,7 +326,6 @@ impl Client { // Send a TDP Shared Directory Info Request // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L210 let tdp_req = SharedDirectoryInfoRequest::from(rdp_req.clone()); - debug!("sending TDP: {:?}", tdp_req); (self.tdp_sd_info_request)(tdp_req)?; // Add a TDP Shared Directory Info Response handler to the handler cache. @@ -507,7 +515,7 @@ impl Client { // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L373 let rdp_req = ServerDriveQueryInformationRequest::decode(device_io_request, payload)?; debug!("received RDP: {:?}", rdp_req); - let f = self.get_file_by_id(rdp_req.device_io_request.file_id); + let f = self.file_cache.get(rdp_req.device_io_request.file_id); let code = if f.is_some() { NTSTATUS::STATUS_SUCCESS } else { @@ -521,7 +529,7 @@ impl Client { let rdp_req = DeviceCloseRequest::decode(device_io_request); debug!("received RDP: {:?}", rdp_req); // Remove the file from our cache - if let Some(file) = self.remove_file_by_id(rdp_req.device_io_request.file_id) { + if let Some(file) = self.file_cache.remove(rdp_req.device_io_request.file_id) { if file.delete_pending { self.tdp_sd_delete(rdp_req, file) } else { @@ -532,8 +540,114 @@ impl Client { } } - /// This is called from Go (in effect) to announce a new directory - /// for sharing. + /// The IRP_MJ_DIRECTORY_CONTROL function we support is when it's sent with minor function IRP_MN_QUERY_DIRECTORY, + /// which is used to retrieve the contents of a directory. RDP does this by repeatedly sending + /// IRP_MN_QUERY_DIRECTORY, expecting to retrieve the next item in the directory in each reply. + /// (Which directory is being queried is specified by the FileId in each request). + /// + /// An idiosyncrasy of the protocol is that on the first IRP_MN_QUERY_DIRECTORY in a sequence, RDP expects back an + /// entry for the "." directory, on the second call it expects an entry for the ".." directory, and on subsequent + /// calls it expects entries for the actual contents of the directory. + /// + /// Once all of the directory's contents has been sent back, we alert RDP to stop sending IRP_MN_QUERY_DIRECTORY + /// by sending it back an NTSTATUS::STATUS_NO_MORE_FILES. + fn process_irp_directory_control( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + let minor_function = device_io_request.minor_function.clone(); + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L650 + match minor_function { + MinorFunction::IRP_MN_QUERY_DIRECTORY => { + let rdp_req = ServerDriveQueryDirectoryRequest::decode(device_io_request, payload)?; + debug!("received RDP: {:?}", rdp_req); + let file_id = rdp_req.device_io_request.file_id; + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L610 + if let Some(dir) = self.file_cache.get(file_id) { + if dir.fso.file_type != FileType::Directory { + return Err(invalid_data_error("received an IRP_MN_QUERY_DIRECTORY request for a file rather than a directory")); + } + + // On the initial query, we need to get the list of files in this directory from + // the client by sending a TDP SharedDirectoryListRequest. + if rdp_req.initial_query != 0 { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L775 + let path = dir.path.clone(); + + // Ask the client for the list of files in this directory. + (self.tdp_sd_list_request)(SharedDirectoryListRequest { + completion_id: rdp_req.device_io_request.completion_id, + directory_id: rdp_req.device_io_request.device_id, + path, + })?; + + // When we get the response for that list of files... + self.pending_sd_list_resp_handlers.insert( + rdp_req.device_io_request.completion_id, + Box::new( + move |cli: &mut Self, + res: SharedDirectoryListResponse| + -> RdpResult>> { + if res.err_code == TdpErrCode::Nil { + // If SharedDirectoryListRequest succeeded, move the + // list of FileSystemObjects that correspond to this directory's + // contents to its entry in the file cache. + if let Some(dir) = cli.file_cache.get_mut(file_id) { + dir.contents = res.fso_list; + } else { + return cli + .prep_file_cache_fail_drive_query_dir_response( + &rdp_req, + ); + } + + // And send back the "." directory over RDP + cli.prep_next_drive_query_dir_response(&rdp_req) + } else { + cli.prep_drive_query_dir_response( + &rdp_req.device_io_request, + NTSTATUS::STATUS_UNSUCCESSFUL, + None, + ) + } + }, + ), + ); + + // Return nothing yet, an RDP message will be returned when the pending_sd_list_resp_handlers + // closure gets called. + Ok(vec![]) + } else { + // This isn't the initial query, ergo we already have this dir's contents filled in. + // Just send the next item. + self.prep_next_drive_query_dir_response(&rdp_req) + } + } else { + self.prep_file_cache_fail_drive_query_dir_response(&rdp_req) + } + } + MinorFunction::IRP_MN_NOTIFY_CHANGE_DIRECTORY => { + debug!("received RDP: {:?}", device_io_request); + debug!( + "ignoring IRP_MN_NOTIFY_CHANGE_DIRECTORY: {:?}", + device_io_request + ); + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L661 + Ok(vec![]) + } + _ => { + debug!("received RDP: {:?}", device_io_request); + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L663 + self.prep_drive_query_dir_response( + &device_io_request, + NTSTATUS::STATUS_NOT_SUPPORTED, + None, + ) + } + } + } + pub fn write_client_device_list_announce( &mut self, req: ClientDeviceListAnnounce, @@ -557,7 +671,7 @@ impl Client { res: SharedDirectoryInfoResponse, mcs: &mut mcs::Client, ) -> RdpResult<()> { - debug!("received TDP: {:?}", res); + debug!("received TDP SharedDirectoryInfoResponse: {:?}", res); if let Some(tdp_resp_handler) = self .pending_sd_info_resp_handlers .remove(&res.completion_id) @@ -581,7 +695,7 @@ impl Client { res: SharedDirectoryCreateResponse, mcs: &mut mcs::Client, ) -> RdpResult<()> { - debug!("received TDP: {:?}", res); + debug!("received TDP SharedDirectoryCreateResponse: {:?}", res); if let Some(tdp_resp_handler) = self .pending_sd_create_resp_handlers .remove(&res.completion_id) @@ -605,7 +719,7 @@ impl Client { res: SharedDirectoryDeleteResponse, mcs: &mut mcs::Client, ) -> RdpResult<()> { - debug!("received TDP: {:?}", res); + debug!("received TDP SharedDirectoryDeleteResponse: {:?}", res); if let Some(tdp_resp_handler) = self .pending_sd_delete_resp_handlers .remove(&res.completion_id) @@ -624,6 +738,30 @@ impl Client { } } + pub fn handle_tdp_sd_list_response( + &mut self, + res: SharedDirectoryListResponse, + mcs: &mut mcs::Client, + ) -> RdpResult<()> { + debug!("received TDP SharedDirectoryListResponse: {:?}", res); + if let Some(tdp_resp_handler) = self + .pending_sd_list_resp_handlers + .remove(&res.completion_id) + { + let rdp_responses = tdp_resp_handler(self, res)?; + let chan = &CHANNEL_NAME.to_string(); + for resp in rdp_responses { + mcs.write(chan, resp)?; + } + Ok(()) + } else { + return Err(try_error(&format!( + "received invalid completion id: {}", + res.completion_id + ))); + } + } + fn prep_device_create_response( &mut self, req: &DeviceCreateRequest, @@ -662,6 +800,94 @@ impl Client { Ok(resp) } + fn prep_drive_query_dir_response( + &self, + device_io_request: &DeviceIoRequest, + io_status: NTSTATUS, + buffer: Option, + ) -> RdpResult>> { + let resp = ClientDriveQueryDirectoryResponse::new(device_io_request, io_status, buffer)?; + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + Ok(resp) + } + + /// prep_next_drive_query_dir_response is a helper function that takes advantage of the + /// Iterator implementation for FileCacheObject in order to respond appropriately to + /// Server Drive Query Directory Requests as they come in. + /// + /// req gives us a FileId, which we use to get the FileCacheObject for the directory that + /// this request is targeted at. We use that FileCacheObject as an iterator, grabbing the + /// next() FileSystemObject (starting with ".", then "..", then iterating through the contents + /// of the target directory), which we then convert to an RDP FsInformationClass for sending back + /// to the RDP server. + fn prep_next_drive_query_dir_response( + &mut self, + req: &ServerDriveQueryDirectoryRequest, + ) -> RdpResult>> { + if let Some(dir) = self.file_cache.get_mut(req.device_io_request.file_id) { + // Get the next FileSystemObject from the FileCacheObject for translation + // into an RDP data structure. Because of how next() is implemented for FileCacheObject, + // the first time this is called we will get an object for the "." directory, the second + // time will give us "..", and then we will iterate through any files/directories stored + // within dir. + if let Some(fso) = dir.next() { + match req.fs_information_class_lvl { + // TODO(isaiah): we should support all the fs_information_class_lvl's that FreeRDP does: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L794 + FsInformationClassLevel::FileBothDirectoryInformation => { + let buffer = FileBothDirectoryInformation::from(fso)?; + self.prep_drive_query_dir_response( + &req.device_io_request, + NTSTATUS::STATUS_SUCCESS, + Some(FsInformationClass::FileBothDirectoryInformation(buffer)) + ) + }, + FsInformationClassLevel::FileDirectoryInformation | + FsInformationClassLevel::FileFullDirectoryInformation | + FsInformationClassLevel::FileNamesInformation => { + Err(not_implemented_error(&format!( + "support for ServerDriveQueryDirectoryRequest with fs_information_class_lvl = {:?} is not implemented", + req.fs_information_class_lvl + ))) + }, + _ => { + Err(invalid_data_error("received invalid FsInformationClassLevel in ServerDriveQueryDirectoryRequest")) + } + } + } else { + // Once our iterator is exhausted, send back a NTSTATUS::STATUS_NO_MORE_FILES to alert RDP that we've listed all the + // contents of this directory. + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/winpr/libwinpr/file/generic.c#L1193 + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L114 + self.prep_drive_query_dir_response( + &req.device_io_request, + NTSTATUS::STATUS_NO_MORE_FILES, + None, + ) + } + } else { + self.prep_file_cache_fail_drive_query_dir_response(req) + } + } + + fn prep_file_cache_fail_drive_query_dir_response( + &self, + req: &ServerDriveQueryDirectoryRequest, + ) -> RdpResult>> { + debug!( + "failed to retrieve an item from the file cache with FileId = {}", + req.device_io_request.file_id + ); + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L633 + self.prep_drive_query_dir_response( + &req.device_io_request, + NTSTATUS::STATUS_UNSUCCESSFUL, + None, + ) + } + /// Helper function for sending a TDP SharedDirectoryCreateRequest based on an /// RDP DeviceCreateRequest and handling the TDP SharedDirectoryCreateResponse. fn tdp_sd_create( @@ -676,7 +902,6 @@ impl Client { file_type, path: rdp_req.path.clone(), }; - debug!("sending TDP: {:?}", tdp_req); (self.tdp_sd_create_request)(tdp_req)?; self.pending_sd_create_resp_handlers.insert( @@ -712,7 +937,6 @@ impl Client { directory_id: rdp_req.device_io_request.device_id, path: rdp_req.path.clone(), }; - debug!("sending TDP: {:?}", tdp_req); (self.tdp_sd_delete_request)(tdp_req)?; self.pending_sd_delete_resp_handlers.insert( rdp_req.device_io_request.completion_id, @@ -739,7 +963,6 @@ impl Client { directory_id: rdp_req.device_io_request.device_id, path: file.path, }; - debug!("sending TDP: {:?}", tdp_req); (self.tdp_sd_delete_request)(tdp_req)?; self.pending_sd_delete_resp_handlers.insert( rdp_req.device_io_request.completion_id, @@ -770,18 +993,6 @@ impl Client { self.vchan.add_header_and_chunkify(None, inner) } - /// Retrieves a FileCacheObject from the file cache - /// (without removing it from the cache). - fn get_file_by_id(&self, file_id: u32) -> Option<&FileCacheObject> { - self.file_cache.get(&file_id) - } - - /// Retrieves a FileCacheObject from the file cache, - /// removing it from the cache. - fn remove_file_by_id(&mut self, file_id: u32) -> Option { - self.file_cache.remove(&file_id) - } - fn push_active_device_id(&mut self, device_id: u32) -> RdpResult<()> { if self.active_device_ids.contains(&device_id) { return Err(RdpError::TryError(format!( @@ -829,11 +1040,17 @@ impl Client { struct FileCacheObject { path: String, delete_pending: bool, - /// The FileSystemObject pertaining to the file at path + /// The FileSystemObject pertaining to the file or directory at path. fso: FileSystemObject, - /// An optional list of FileSystemObjects held by the directory at path - fsos: Vec, - fsos_index: u32, + /// A vector of the contents of the directory at path. + contents: Vec, + + /// Book-keeping variable, see Iterator implementation + contents_i: usize, + /// Book-keeping variable, see Iterator implementation + dot_sent: bool, + /// Book-keeping variable, see Iterator implementation + dotdot_sent: bool, } impl FileCacheObject { @@ -842,12 +1059,107 @@ impl FileCacheObject { path, delete_pending: false, fso, - fsos: Vec::new(), - fsos_index: 0, + contents: Vec::new(), + + contents_i: 0, + dot_sent: false, + dotdot_sent: false, } } } +/// FileCacheObject is used as an iterator for the implementation of +/// IRP_MJ_DIRECTORY_CONTROL, which requires that we iterate through +/// all the files of a directory one by one. In this case, the directory +/// is the FileCacheObject itself, with it's own fso field representing +/// the directory, and its contents being represented by FileSystemObject's +/// in its contents field. +/// +/// We account for an idiosyncrasy of the RDP protocol here: when fielding an +/// IRP_MJ_DIRECTORY_CONTROL, RDP first expects to receive an entry for the "." +/// directory, and next an entry for the ".." directory. Only after those two +/// directories have been sent do we begin sending the actual contents of this +/// directory (the contents field). (This is why we maintain dot_sent and dotdot_sent +/// fields on each FileCacheObject) +/// +/// Note that this implementation only makes sense in the case that this FileCacheObject +/// is itself a directory (fso.file_type == FileType::Directory). We leave it up to the +/// caller to ensure iteration makes sense in the given context that it's used. +impl Iterator for FileCacheObject { + type Item = FileSystemObject; + + fn next(&mut self) -> Option { + // On the first call to next, return the "." directory + if !self.dot_sent { + self.dot_sent = true; + Some(FileSystemObject { + last_modified: self.fso.last_modified, + size: self.fso.size, + file_type: self.fso.file_type, + path: ".".to_string(), + }) + } else if !self.dotdot_sent { + // On the second call to next, return the ".." directory + self.dotdot_sent = true; + Some(FileSystemObject { + last_modified: self.fso.last_modified, + size: 0, + file_type: FileType::Directory, + path: "..".to_string(), + }) + } else { + // "." and ".." have been sent, now start iterating through + // the actual contents of the directory + if self.contents_i < self.contents.len() { + let i = self.contents_i; + self.contents_i += 1; + return Some(self.contents[i].clone()); + } + None + } + } +} + +struct FileCache { + cache: HashMap, +} + +impl FileCache { + fn new() -> Self { + Self { + cache: HashMap::new(), + } + } + + /// Insert a FileCacheObject into the file cache. + /// + /// If the file cache did not have this key present, [`None`] is returned. + /// + /// If the file cache did have this key present, the value is updated, and the old + /// value is returned. The key is not updated, though; this matters for + /// types that can be `==` without being identical. + fn insert(&mut self, file_id: u32, file: FileCacheObject) -> Option { + self.cache.insert(file_id, file) + } + + /// Retrieves a FileCacheObject from the file cache, + /// without removing it from the cache. + fn get(&self, file_id: u32) -> Option<&FileCacheObject> { + self.cache.get(&file_id) + } + /// Retrieves a mutable FileCacheObject from the file cache, + /// without removing it from the cache. + fn get_mut(&mut self, file_id: u32) -> Option<&mut FileCacheObject> { + self.cache.get_mut(&file_id) + } + + /// Retrieves a FileCacheObject from the file cache, + /// removing it from the cache. + fn remove(&mut self, file_id: u32) -> Option { + self.cache.remove(&file_id) + } +} + /// 2.2.1.1 Shared Header (RDPDR_HEADER) /// This header is present at the beginning of every message in sent over the rdpdr virtual channel. /// The purpose of this header is to describe the type of the message. @@ -1775,6 +2087,26 @@ impl FileBothDirectoryInformation { w.extend_from_slice(&util::to_unicode(&self.file_name, false)); Ok(w) } + + fn from(fso: FileSystemObject) -> RdpResult { + let file_attributes = if fso.file_type == FileType::Directory { + flags::FileAttributes::FILE_ATTRIBUTE_DIRECTORY + } else { + flags::FileAttributes::FILE_ATTRIBUTE_NORMAL + }; + + let last_modified = to_windows_time(fso.last_modified); + + Ok(FileBothDirectoryInformation::new( + last_modified, + last_modified, + last_modified, + last_modified, + i64::try_from(fso.size)?, + file_attributes, + fso.name()?, + )) + } } /// 2.2.3.4.8 Client Drive Query Information Response (DR_DRIVE_QUERY_INFORMATION_RSP) @@ -2067,7 +2399,6 @@ struct ServerDriveQueryDirectoryRequest { path: String, } -#[allow(dead_code)] impl ServerDriveQueryDirectoryRequest { fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { let fs_information_class_lvl = @@ -2131,14 +2462,12 @@ struct ClientDriveQueryDirectoryResponse { // Padding (1 byte): This field is unused and MUST be ignored. } -#[allow(dead_code)] impl ClientDriveQueryDirectoryResponse { fn new( - req: &ServerDriveQueryDirectoryRequest, + device_io_request: &DeviceIoRequest, 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( @@ -2164,6 +2493,7 @@ impl ClientDriveQueryDirectoryResponse { }) } + // TODO(isaiah): make this logic more sane fn encode(&self) -> RdpResult> { let mut w = vec![]; w.extend_from_slice(&self.device_io_reply.encode()?); @@ -2184,6 +2514,14 @@ impl ClientDriveQueryDirectoryResponse { // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L935-L937 w.write_u32::(0)?; w.write_u8(0)?; + } else if self.device_io_reply.io_status + == NTSTATUS::to_u32(&NTSTATUS::STATUS_NOT_SUPPORTED).unwrap() + || self.device_io_reply.io_status + == NTSTATUS::to_u32(&NTSTATUS::STATUS_UNSUCCESSFUL).unwrap() + { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L665 + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L634 + w.write_u32::(self.length)?; } else { return Err(invalid_data_error(&format!( "Found ClientDriveQueryDirectoryResponse with invalid or unhandled NTSTATUS: {:?}", @@ -2212,6 +2550,7 @@ type SharedDirectoryCreateRequestSender = Box RdpResult<()>>; type SharedDirectoryDeleteRequestSender = Box RdpResult<()>>; +type SharedDirectoryListRequestSender = Box RdpResult<()>>; type SharedDirectoryInfoResponseHandler = Box RdpResult>>>; @@ -2219,6 +2558,8 @@ type SharedDirectoryCreateResponseHandler = Box RdpResult>>>; type SharedDirectoryDeleteResponseHandler = Box RdpResult>>>; +type SharedDirectoryListResponseHandler = + Box RdpResult>>>; #[cfg(test)] mod tests { diff --git a/lib/srv/desktop/tdp/proto.go b/lib/srv/desktop/tdp/proto.go index 943c4c0268161..fb8d8f54d4181 100644 --- a/lib/srv/desktop/tdp/proto.go +++ b/lib/srv/desktop/tdp/proto.go @@ -63,6 +63,8 @@ const ( TypeSharedDirectoryCreateResponse = MessageType(16) TypeSharedDirectoryDeleteRequest = MessageType(17) TypeSharedDirectoryDeleteResponse = MessageType(18) + TypeSharedDirectoryListRequest = MessageType(25) + TypeSharedDirectoryListResponse = MessageType(26) ) // Message is a Go representation of a desktop protocol message. @@ -131,6 +133,10 @@ func decode(in peekReader) (Message, error) { return decodeSharedDirectoryDeleteRequest(in) case TypeSharedDirectoryDeleteResponse: return decodeSharedDirectoryDeleteResponse(in) + case TypeSharedDirectoryListRequest: + return decodeSharedDirectoryListRequest(in) + case TypeSharedDirectoryListResponse: + return decodeSharedDirectoryListResponse(in) default: return nil, trace.BadParameter("unsupported desktop protocol message type %d", t) } @@ -987,6 +993,115 @@ func decodeSharedDirectoryDeleteResponse(in peekReader) (SharedDirectoryDeleteRe return res, err } +type SharedDirectoryListRequest struct { + CompletionID uint32 + DirectoryID uint32 + Path string +} + +func (s SharedDirectoryListRequest) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryListRequest)) + binary.Write(buf, binary.BigEndian, s.CompletionID) + binary.Write(buf, binary.BigEndian, s.DirectoryID) + if err := encodeString(buf, s.Path); err != nil { + return nil, trace.Wrap(err) + } + + return buf.Bytes(), nil +} + +func decodeSharedDirectoryListRequest(in peekReader) (SharedDirectoryListRequest, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryListRequest{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryListRequest) { + return SharedDirectoryListRequest{}, trace.BadParameter("got message type %v, expected SharedDirectoryListRequest(%v)", t, TypeSharedDirectoryListRequest) + } + var completionId, directoryId uint32 + err = binary.Read(in, binary.BigEndian, &completionId) + if err != nil { + return SharedDirectoryListRequest{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &directoryId) + if err != nil { + return SharedDirectoryListRequest{}, trace.Wrap(err) + } + path, err := decodeString(in, tdpMaxPathLength) + if err != nil { + return SharedDirectoryListRequest{}, trace.Wrap(err) + } + + return SharedDirectoryListRequest{ + CompletionID: completionId, + DirectoryID: directoryId, + Path: path, + }, nil +} + +// | message type (26) | completion_id uint32 | err_code uint32 | fso_list_length uint32 | fso_list fso[] | +type SharedDirectoryListResponse struct { + CompletionID uint32 + ErrCode uint32 + FsoList []FileSystemObject +} + +func (s SharedDirectoryListResponse) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryListResponse)) + binary.Write(buf, binary.BigEndian, s.CompletionID) + binary.Write(buf, binary.BigEndian, s.ErrCode) + binary.Write(buf, binary.BigEndian, uint32(len(s.FsoList))) + for _, fso := range s.FsoList { + fsoEnc, err := fso.Encode() + if err != nil { + return nil, trace.Wrap(err) + } + binary.Write(buf, binary.BigEndian, fsoEnc) + } + + return buf.Bytes(), nil +} + +func decodeSharedDirectoryListResponse(in peekReader) (SharedDirectoryListResponse, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryListResponse{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryListResponse) { + return SharedDirectoryListResponse{}, trace.BadParameter("got message type %v, expected SharedDirectoryListResponse(%v)", t, TypeSharedDirectoryListResponse) + } + var completionId, errCode, fsoListLength uint32 + err = binary.Read(in, binary.BigEndian, &completionId) + if err != nil { + return SharedDirectoryListResponse{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &errCode) + if err != nil { + return SharedDirectoryListResponse{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &fsoListLength) + if err != nil { + return SharedDirectoryListResponse{}, trace.Wrap(err) + } + + var fsoList []FileSystemObject + for i := uint32(0); i < fsoListLength; i++ { + fso, err := decodeFileSystemObject(in) + if err != nil { + return SharedDirectoryListResponse{}, trace.Wrap(err) + } + fsoList = append(fsoList, fso) + } + + return SharedDirectoryListResponse{ + CompletionID: completionId, + ErrCode: errCode, + FsoList: fsoList, + }, 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