From 361ea8ef3f28aeae783358b79ee606d3ed1bc5aa Mon Sep 17 00:00:00 2001 From: Isaiah Becker-Mayer Date: Thu, 4 Aug 2022 17:50:02 -0400 Subject: [PATCH] Windows Desktop Directory Sharing (#13630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `IRP_MJ_CREATE` (#12665) * `IRP_MJ_QUERY_INFORMATION` (#12717) * `IRP_MJ_CLOSE` (#12729) * Refactor rdpdr client (#12750) * Adding logic for `FILE_SUPERSEDE` (#12829) * Improve `process_irp_create` (#12830) * adds return statements that got lost in a merge * `IRP_MJ_DIRECTORY_CONTROL` (#12870) * `FileFullDirectoryInformation` (#12908) * Improve `ClientDriveQueryDirectoryResponse.encode()` (#12912) * `IRP_MJ_QUERY_VOLUME_INFORMATION` (#13071) * Fix Shared Directory Request handling when feature is disabled (#13439) * IRP_MJ_READ, IRP_MJ_WRITE, and IRP_MJ_SET_INFORMATION (#13995) * Adds constants for sizing calculations (#14051) Co-authored-by: Łukasz Kozłowski Co-authored-by: Zac Bergquist --- Cargo.lock | 103 +- lib/srv/desktop/rdp/rdpclient/Cargo.toml | 2 +- lib/srv/desktop/rdp/rdpclient/client.go | 223 +- lib/srv/desktop/rdp/rdpclient/src/lib.rs | 793 ++++- .../desktop/rdp/rdpclient/src/rdpdr/consts.rs | 45 +- .../desktop/rdp/rdpclient/src/rdpdr/flags.rs | 10 + .../desktop/rdp/rdpclient/src/rdpdr/mod.rs | 2750 +++++++++++++++-- lib/srv/desktop/rdp/rdpclient/src/util.rs | 6 + lib/srv/desktop/tdp/proto.go | 577 +++- 9 files changed, 4062 insertions(+), 447 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94b33f41f10..a5fb45dcd57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,9 +66,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" @@ -166,9 +166,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.8" +version = "3.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "54635806b078b7925d6e36810b1755f2a4b5b4d57560432c1ecf60bcbe10602b" dependencies = [ "atty", "bitflags", @@ -250,9 +250,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", ] @@ -378,13 +378,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]] @@ -398,15 +398,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", @@ -448,9 +448,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", @@ -492,9 +492,9 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[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", ] @@ -713,9 +713,9 @@ checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" [[package]] name = "pem-rfc7468" @@ -765,18 +765,18 @@ 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", ] [[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", ] @@ -840,7 +840,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]] @@ -878,7 +878,7 @@ dependencies = [ [[package]] name = "rdp-rs" version = "0.1.0" -source = "git+https://github.com/gravitational/rdp-rs?rev=6075679e7c9bd8e3c2136a87f097d05c7db3235f#6075679e7c9bd8e3c2136a87f097d05c7db3235f" +source = "git+https://github.com/gravitational/rdp-rs?rev=004207e2edfbc6a57fd04daf6bcade93bcc3495e#004207e2edfbc6a57fd04daf6bcade93bcc3495e" dependencies = [ "bufstream", "byteorder", @@ -1100,9 +1100,9 @@ dependencies = [ [[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" @@ -1161,9 +1161,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", @@ -1201,11 +1201,12 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[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", ] @@ -1226,9 +1227,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" @@ -1257,7 +1258,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]] @@ -1295,15 +1296,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 = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", @@ -1311,9 +1318,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", @@ -1326,9 +1333,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", @@ -1336,9 +1343,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", @@ -1349,15 +1356,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 0c13df53f4d..8bf4ac13145 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 = "6075679e7c9bd8e3c2136a87f097d05c7db3235f" } +rdp-rs = { git = "https://github.com/gravitational/rdp-rs", rev = "004207e2edfbc6a57fd04daf6bcade93bcc3495e" } 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 daba8123607..e8a99e0369e 100644 --- a/lib/srv/desktop/rdp/rdpclient/client.go +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -412,11 +412,11 @@ func (c *Client) start() { defer C.free(unsafe.Pointer(path)) if errCode := C.handle_tdp_sd_info_response(c.rustClient, C.CGOSharedDirectoryInfoResponse{ completion_id: C.uint32_t(m.CompletionID), - err_code: C.uint32_t(m.ErrCode), + err_code: m.ErrCode, fso: C.CGOFileSystemObject{ last_modified: C.uint64_t(m.Fso.LastModified), size: C.uint64_t(m.Fso.Size), - file_type: C.uint32_t(m.Fso.FileType), + file_type: m.Fso.FileType, path: path, }, }); errCode != C.ErrCodeSuccess { @@ -424,6 +424,91 @@ func (c *Client) start() { return } } + case tdp.SharedDirectoryCreateResponse: + if c.cfg.AllowDirectorySharing { + if errCode := C.handle_tdp_sd_create_response(c.rustClient, C.CGOSharedDirectoryCreateResponse{ + completion_id: C.uint32_t(m.CompletionID), + err_code: m.ErrCode, + }); errCode != C.ErrCodeSuccess { + c.cfg.Log.Errorf("SharedDirectoryCreateResponse failed: %v", errCode) + return + } + } + case tdp.SharedDirectoryDeleteResponse: + if c.cfg.AllowDirectorySharing { + if errCode := C.handle_tdp_sd_delete_response(c.rustClient, C.CGOSharedDirectoryDeleteResponse{ + completion_id: C.uint32_t(m.CompletionID), + err_code: m.ErrCode, + }); errCode != C.ErrCodeSuccess { + c.cfg.Log.Errorf("SharedDirectoryDeleteResponse failed: %v", errCode) + 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 + } + } + case tdp.SharedDirectoryReadResponse: + if c.cfg.AllowDirectorySharing { + var readData *C.uint8_t + if m.ReadDataLength > 0 { + readData = (*C.uint8_t)(unsafe.Pointer(&m.ReadData[0])) + } else { + readData = (*C.uint8_t)(unsafe.Pointer(&m.ReadData)) + } + + if errCode := C.handle_tdp_sd_read_response(c.rustClient, C.CGOSharedDirectoryReadResponse{ + completion_id: C.uint32_t(m.CompletionID), + err_code: m.ErrCode, + read_data_length: C.uint32_t(m.ReadDataLength), + read_data: readData, + }); errCode != C.ErrCodeSuccess { + c.cfg.Log.Errorf("SharedDirectoryReadResponse failed: %v", errCode) + return + } + } + case tdp.SharedDirectoryWriteResponse: + if c.cfg.AllowDirectorySharing { + if errCode := C.handle_tdp_sd_write_response(c.rustClient, C.CGOSharedDirectoryWriteResponse{ + completion_id: C.uint32_t(m.CompletionID), + err_code: m.ErrCode, + bytes_written: C.uint32_t(m.BytesWritten), + }); errCode != C.ErrCodeSuccess { + c.cfg.Log.Errorf("SharedDirectoryWriteResponse failed: %v", errCode) + return + } + } default: c.cfg.Log.Warningf("Skipping unimplemented TDP message type %T", msg) } @@ -497,15 +582,18 @@ func tdp_sd_acknowledge(handle C.uintptr_t, ack *C.CGOSharedDirectoryAcknowledge }) } -// sharedDirectoryAcknowledge acknowledges that a `Shared Directory Announce` TDP message was processed. +// sharedDirectoryAcknowledge is sent by the TDP server to the client +// to acknowledge that a SharedDirectoryAnnounce was received. func (c *Client) sharedDirectoryAcknowledge(ack tdp.SharedDirectoryAcknowledge) C.CGOErrCode { if c.cfg.AllowDirectorySharing { if err := c.cfg.Conn.OutputMessage(ack); err != nil { c.cfg.Log.Errorf("failed to send SharedDirectoryAcknowledge: %v", err) return C.ErrCodeFailure } + return C.ErrCodeSuccess } - return C.ErrCodeSuccess + + return C.ErrCodeFailure } //export tdp_sd_info_request @@ -517,14 +605,139 @@ func tdp_sd_info_request(handle C.uintptr_t, req *C.CGOSharedDirectoryInfoReques }) } +// sharedDirectoryInfoRequest is sent from the TDP server to the client +// to request information about a file or directory at a given path. func (c *Client) sharedDirectoryInfoRequest(req tdp.SharedDirectoryInfoRequest) 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 } - return C.ErrCodeSuccess + + return C.ErrCodeFailure +} + +//export tdp_sd_create_request +func tdp_sd_create_request(handle C.uintptr_t, req *C.CGOSharedDirectoryCreateRequest) C.CGOErrCode { + return cgo.Handle(handle).Value().(*Client).sharedDirectoryCreateRequest(tdp.SharedDirectoryCreateRequest{ + CompletionID: uint32(req.completion_id), + DirectoryID: uint32(req.directory_id), + FileType: uint32(req.file_type), + Path: C.GoString(req.path), + }) +} + +// sharedDirectoryCreateRequest is sent by the TDP server to +// the client to request the creation of a new file or directory. +func (c *Client) sharedDirectoryCreateRequest(req tdp.SharedDirectoryCreateRequest) C.CGOErrCode { + if c.cfg.AllowDirectorySharing { + if err := c.cfg.Conn.OutputMessage(req); err != nil { + c.cfg.Log.Errorf("failed to send SharedDirectoryCreateRequest: %v", err) + return C.ErrCodeFailure + } + return C.ErrCodeSuccess + } + + return C.ErrCodeFailure +} + +//export tdp_sd_delete_request +func tdp_sd_delete_request(handle C.uintptr_t, req *C.CGOSharedDirectoryDeleteRequest) C.CGOErrCode { + return cgo.Handle(handle).Value().(*Client).sharedDirectoryDeleteRequest(tdp.SharedDirectoryDeleteRequest{ + CompletionID: uint32(req.completion_id), + DirectoryID: uint32(req.directory_id), + Path: C.GoString(req.path), + }) +} + +// sharedDirectoryDeleteRequest is sent by the TDP server to the client +// to request the deletion of a file or directory at path. +func (c *Client) sharedDirectoryDeleteRequest(req tdp.SharedDirectoryDeleteRequest) C.CGOErrCode { + if c.cfg.AllowDirectorySharing { + if err := c.cfg.Conn.OutputMessage(req); err != nil { + c.cfg.Log.Errorf("failed to send SharedDirectoryDeleteRequest: %v", err) + return C.ErrCodeFailure + } + return C.ErrCodeSuccess + } + + return C.ErrCodeFailure +} + +//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), + }) +} + +// sharedDirectoryListRequest is sent by the TDP server to the client +// to request the contents of a directory. +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 SharedDirectoryListRequest: %v", err) + return C.ErrCodeFailure + } + return C.ErrCodeSuccess + } + + return C.ErrCodeFailure +} + +//export tdp_sd_read_request +func tdp_sd_read_request(handle C.uintptr_t, req *C.CGOSharedDirectoryReadRequest) C.CGOErrCode { + return cgo.Handle(handle).Value().(*Client).sharedDirectoryReadRequest(tdp.SharedDirectoryReadRequest{ + CompletionID: uint32(req.completion_id), + DirectoryID: uint32(req.directory_id), + Path: C.GoString(req.path), + PathLength: uint32(req.path_length), + Offset: uint64(req.offset), + Length: uint32(req.length), + }) +} + +// SharedDirectoryReadRequest is sent by the TDP server to the client +// to request the contents of a file. +func (c *Client) sharedDirectoryReadRequest(req tdp.SharedDirectoryReadRequest) C.CGOErrCode { + if c.cfg.AllowDirectorySharing { + if err := c.cfg.Conn.OutputMessage(req); err != nil { + c.cfg.Log.Errorf("failed to send SharedDirectoryReadRequest: %v", err) + return C.ErrCodeFailure + } + return C.ErrCodeSuccess + } + return C.ErrCodeFailure +} + +//export tdp_sd_write_request +func tdp_sd_write_request(handle C.uintptr_t, req *C.CGOSharedDirectoryWriteRequest) C.CGOErrCode { + return cgo.Handle(handle).Value().(*Client).sharedDirectoryWriteRequest(tdp.SharedDirectoryWriteRequest{ + CompletionID: uint32(req.completion_id), + DirectoryID: uint32(req.directory_id), + Offset: uint64(req.offset), + PathLength: uint32(req.path_length), + Path: C.GoString(req.path), + WriteDataLength: uint32(req.write_data_length), + WriteData: C.GoBytes(unsafe.Pointer(req.write_data), C.int(req.write_data_length)), + }) +} + +// SharedDirectoryWriteRequest is sent by the TDP server to the client +// to write to a file. +func (c *Client) sharedDirectoryWriteRequest(req tdp.SharedDirectoryWriteRequest) C.CGOErrCode { + if c.cfg.AllowDirectorySharing { + if err := c.cfg.Conn.OutputMessage(req); err != nil { + c.cfg.Log.Errorf("failed to send SharedDirectoryWriteRequest: %v", err) + return C.ErrCodeFailure + } + return C.ErrCodeSuccess + } + return C.ErrCodeFailure } // close closes the RDP client connection and diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index 94096195388..0e74e0852ef 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; @@ -164,8 +165,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, @@ -288,22 +289,22 @@ fn connect_rdp_inner( "rdp-rs", ); - let tdp_sd_acknowledge = Box::new(move |ack: SharedDirectoryAcknowledge| -> RdpResult<()> { - debug!("sending: {:?}", ack); - unsafe { - if tdp_sd_acknowledge(go_ref, &mut CGOSharedDirectoryAcknowledge::from(ack)) - != CGOErrCode::ErrCodeSuccess - { - return Err(RdpError::TryError(String::from( - "call to tdp_sd_acknowledge failed", - ))); + let tdp_sd_acknowledge = Box::new( + move |mut ack: SharedDirectoryAcknowledge| -> RdpResult<()> { + debug!("sending TDP SharedDirectoryAcknowledge: {:?}", ack); + unsafe { + if tdp_sd_acknowledge(go_ref, &mut ack) != CGOErrCode::ErrCodeSuccess { + return Err(RdpError::TryError(String::from( + "call to tdp_sd_acknowledge failed", + ))); + } + Ok(()) } - } - Ok(()) - }); + }, + ); 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) => { @@ -334,15 +335,188 @@ fn connect_rdp_inner( } }); + let tdp_sd_create_request = + Box::new(move |req: SharedDirectoryCreateRequest| -> RdpResult<()> { + debug!("sending TDP SharedDirectoryCreateRequest: {:?}", req); + // Create C compatible string from req.path + match CString::new(req.path.clone()) { + Ok(c_string) => { + unsafe { + let err = tdp_sd_create_request( + go_ref, + &mut CGOSharedDirectoryCreateRequest { + completion_id: req.completion_id, + directory_id: req.directory_id, + file_type: req.file_type, + path: c_string.as_ptr(), + }, + ); + if err != CGOErrCode::ErrCodeSuccess { + return Err(RdpError::TryError(String::from( + "call to tdp_sd_create_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 + ))); + } + } + }); + + let tdp_sd_delete_request = + Box::new(move |req: SharedDirectoryDeleteRequest| -> RdpResult<()> { + debug!("sending TDP SharedDirectoryDeleteRequest: {:?}", req); + // Create C compatible string from req.path + match CString::new(req.path.clone()) { + Ok(c_string) => { + unsafe { + let err = tdp_sd_delete_request( + go_ref, + &mut CGOSharedDirectoryDeleteRequest { + 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_delete_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 + ))); + } + } + }); + + 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 + ))); + } + } + }); + + let tdp_sd_read_request = Box::new(move |req: SharedDirectoryReadRequest| -> RdpResult<()> { + debug!("sending: {:?}", req); + match CString::new(req.path.clone()) { + Ok(c_string) => { + unsafe { + let err = tdp_sd_read_request( + go_ref, + &mut CGOSharedDirectoryReadRequest { + completion_id: req.completion_id, + directory_id: req.directory_id, + path: c_string.as_ptr(), + path_length: req.path.len() as u32, + offset: req.offset, + length: req.length, + }, + ); + + if err != CGOErrCode::ErrCodeSuccess { + return Err(RdpError::TryError(String::from( + "call to tdp_sd_read_request failed", + ))); + } + } + Ok(()) + } + Err(_) => { + return Err(RdpError::TryError(format!( + "path contained characters that couldn't be converted to a C string: {}", + req.path + ))); + } + } + }); + + let tdp_sd_write_request = Box::new(move |req: SharedDirectoryWriteRequest| -> RdpResult<()> { + debug!("sending: {:?}", req); + match CString::new(req.path.clone()) { + Ok(c_string) => { + unsafe { + let err = tdp_sd_write_request( + go_ref, + &mut CGOSharedDirectoryWriteRequest { + completion_id: req.completion_id, + directory_id: req.directory_id, + offset: req.offset, + path: c_string.as_ptr(), + path_length: req.path.len() as u32, + write_data_length: req.write_data.len() as u32, + write_data: req.write_data.as_ptr() as *mut u8, + }, + ); + + if err != CGOErrCode::ErrCodeSuccess { + return Err(RdpError::TryError(String::from( + "call to tdp_sd_write_failed", + ))); + } + } + Ok(()) + } + Err(_) => { + 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( - params.cert_der, - params.key_der, + let rdpdr = rdpdr::Client::new(rdpdr::Config { + cert_der: params.cert_der, + key_der: params.key_der, pin, - params.allow_directory_sharing, + allow_directory_sharing: params.allow_directory_sharing, tdp_sd_acknowledge, tdp_sd_info_request, - ); + tdp_sd_create_request, + tdp_sd_delete_request, + tdp_sd_list_request, + tdp_sd_read_request, + tdp_sd_write_request, + }); // Client for the "cliprdr" channel - clipboard sharing. let cliprdr = if params.allow_clipboard { @@ -443,6 +617,41 @@ impl RdpClient { self.rdpdr.handle_tdp_sd_info_response(res, &mut self.mcs) } + pub fn handle_tdp_sd_create_response( + &mut self, + res: SharedDirectoryCreateResponse, + ) -> RdpResult<()> { + self.rdpdr.handle_tdp_sd_create_response(res, &mut self.mcs) + } + + pub fn handle_tdp_sd_delete_response( + &mut self, + res: SharedDirectoryDeleteResponse, + ) -> RdpResult<()> { + 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 handle_tdp_sd_read_response( + &mut self, + res: SharedDirectoryReadResponse, + ) -> RdpResult<()> { + self.rdpdr.handle_tdp_sd_read_response(res, &mut self.mcs) + } + + pub fn handle_tdp_sd_write_response( + &mut self, + res: SharedDirectoryWriteResponse, + ) -> RdpResult<()> { + self.rdpdr.handle_tdp_sd_write_response(res, &mut self.mcs) + } + pub fn shutdown(&mut self) -> RdpResult<()> { self.mcs.shutdown() } @@ -527,7 +736,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, @@ -540,7 +753,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 { @@ -568,14 +781,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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. + + let sd_announce = SharedDirectoryAnnounce::from(sd_announce); + let client = match Client::from_ptr(client_ptr) { Ok(client) => client, Err(cgo_error) => { @@ -583,9 +808,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) { @@ -602,11 +826,165 @@ 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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. + + let res = SharedDirectoryInfoResponse::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_info_response(res) { + Ok(()) => CGOErrCode::ErrCodeSuccess, + Err(e) => { + error!("failed to handle Shared Directory Info Response: {:?}", e); + CGOErrCode::ErrCodeFailure + } + } +} + +/// handle_tdp_sd_create_response handles a TDP Shared Directory Create 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) +#[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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. + + let res: SharedDirectoryCreateResponse = 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_create_response(res) { + Ok(()) => CGOErrCode::ErrCodeSuccess, + Err(e) => { + error!("failed to handle Shared Directory Create Response: {:?}", e); + CGOErrCode::ErrCodeFailure + } + } +} + +/// handle_tdp_sd_delete_response handles a TDP Shared Directory Delete 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) +#[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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. + + let res: SharedDirectoryDeleteResponse = 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_delete_response(res) { + Ok(()) => CGOErrCode::ErrCodeSuccess, + Err(e) => { + error!("failed to handle Shared Directory Create Response: {:?}", e); + CGOErrCode::ErrCodeFailure + } + } +} + +/// 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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. + + 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 + } + } +} + +/// handle_tdp_sd_read_response handles a TDP Shared Directory Read Response +/// message +/// +/// # Safety +/// +/// client_ptr must be a valid pointer +#[no_mangle] +pub unsafe extern "C" fn handle_tdp_sd_read_response( + client_ptr: *mut Client, + res: CGOSharedDirectoryReadResponse, ) -> CGOErrCode { let client = match Client::from_ptr(client_ptr) { Ok(client) => client, @@ -616,10 +994,39 @@ 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_read_response(SharedDirectoryReadResponse::from(res)) { Ok(()) => CGOErrCode::ErrCodeSuccess, Err(e) => { - error!("failed to handle Shared Directory Info Response: {:?}", e); + error!("failed to handle Shared Directory Read Response: {:?}", e); + CGOErrCode::ErrCodeFailure + } + } +} + +/// handle_tdp_sd_write_response handles a TDP Shared Directory Write Response +/// message +/// +/// # Safety +/// +/// client_ptr must be a valid pointer +#[no_mangle] +pub unsafe extern "C" fn handle_tdp_sd_write_response( + client_ptr: *mut Client, + res: CGOSharedDirectoryWriteResponse, +) -> CGOErrCode { + 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_write_response(res) { + Ok(()) => CGOErrCode::ErrCodeSuccess, + Err(e) => { + error!("failed to handle Shared Directory Write Response: {:?}", e); CGOErrCode::ErrCodeFailure } } @@ -735,6 +1142,11 @@ 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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. PointerEvent { x: p.x, y: p.y, @@ -757,7 +1169,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, @@ -797,6 +1210,11 @@ 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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. KeyboardEvent { code: k.code, down: k.down, @@ -806,7 +1224,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, @@ -860,7 +1279,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)) @@ -872,14 +1292,24 @@ 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. + // In other words, all pointer data that needs to persist after this function returns 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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. + slice::from_raw_parts(data, len as usize).to_vec() } #[repr(C)] @@ -895,29 +1325,42 @@ pub struct CGOSharedDirectoryAnnounce { pub name: *const c_char, } -/// SharedDirectoryAcknowledge is a CGO-compatible version of -/// the TDP Shared Directory Knowledge message that we pass back to Go. -#[derive(Debug)] -pub struct SharedDirectoryAcknowledge { - pub err_code: u32, - pub directory_id: u32, +/// 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, } -#[repr(C)] -pub struct CGOSharedDirectoryAcknowledge { - pub err_code: u32, - pub directory_id: u32, -} - -impl From for CGOSharedDirectoryAcknowledge { - fn from(ack: SharedDirectoryAcknowledge) -> CGOSharedDirectoryAcknowledge { - CGOSharedDirectoryAcknowledge { - err_code: ack.err_code, - directory_id: ack.directory_id, +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. + // In other words, all pointer data that needs to persist after this function returns 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)] +#[repr(C)] +pub struct SharedDirectoryAcknowledge { + pub err_code: TdpErrCode, + pub directory_id: u32, +} + +pub type CGOSharedDirectoryAcknowledge = SharedDirectoryAcknowledge; + +/// SharedDirectoryInfoRequest is sent from the TDP server to the client +/// to request information about a file or directory at a given path. #[derive(Debug)] pub struct SharedDirectoryInfoRequest { completion_id: u32, @@ -942,23 +1385,29 @@ impl From for SharedDirectoryInfoRequest { } } +/// SharedDirectoryInfoResponse is sent by the TDP client to the server +/// in response to a `Shared Directory Info Request`. #[derive(Debug)] -#[allow(dead_code)] pub struct SharedDirectoryInfoResponse { completion_id: u32, - err_code: u32, + err_code: TdpErrCode, fso: FileSystemObject, } #[repr(C)] pub struct CGOSharedDirectoryInfoResponse { pub completion_id: u32, - pub err_code: u32, + pub err_code: TdpErrCode, pub fso: CGOFileSystemObject, } 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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. SharedDirectoryInfoResponse { completion_id: cgo_res.completion_id, err_code: cgo_res.err_code, @@ -967,25 +1416,45 @@ impl From for SharedDirectoryInfoResponse { } } -#[derive(Debug)] -#[allow(dead_code)] +#[derive(Debug, Clone)] +/// FileSystemObject is a TDP structure containing the metadata +/// of a file or directory. pub struct FileSystemObject { last_modified: u64, size: u64, - file_type: u32, // TODO(isaiah): make an enum + file_type: FileType, 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, - pub file_type: u32, // TODO(isaiah): make an enum + pub file_type: FileType, pub path: *const c_char, } 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. + // In other words, all pointer data that needs to persist after this function returns MUST + // be copied into Rust-owned memory. unsafe { FileSystemObject { last_modified: cgo_fso.last_modified, @@ -997,6 +1466,194 @@ impl From for FileSystemObject { } } +#[repr(C)] +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum FileType { + File = 0, + Directory = 1, +} + +#[repr(C)] +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum TdpErrCode { + /// nil (no error, operation succeeded) + Nil = 0, + /// operation failed + Failed = 1, + /// resource does not exist + DoesNotExist = 2, + /// resource already exists + AlreadyExists = 3, +} + +/// SharedDirectoryWriteRequest is sent by the TDP server to the client +/// to write to a file. +#[derive(Debug, Clone)] +pub struct SharedDirectoryWriteRequest { + completion_id: u32, + directory_id: u32, + offset: u64, + path: String, + write_data: Vec, +} + +#[derive(Debug)] +#[repr(C)] +pub struct CGOSharedDirectoryWriteRequest { + pub completion_id: u32, + pub directory_id: u32, + pub offset: u64, + pub path_length: u32, + pub path: *const c_char, + pub write_data_length: u32, + pub write_data: *mut u8, +} + +/// SharedDirectoryReadRequest is sent by the TDP server to the client +/// to request the contents of a file. +#[derive(Debug)] +pub struct SharedDirectoryReadRequest { + completion_id: u32, + directory_id: u32, + path: String, + offset: u64, + length: u32, +} + +#[repr(C)] +pub struct CGOSharedDirectoryReadRequest { + pub completion_id: u32, + pub directory_id: u32, + pub path_length: u32, + pub path: *const c_char, + pub offset: u64, + pub length: u32, +} + +/// SharedDirectoryReadResponse is sent by the TDP client to the server +/// with the data as requested by a SharedDirectoryReadRequest. +#[derive(Debug)] +#[repr(C)] +pub struct SharedDirectoryReadResponse { + pub completion_id: u32, + pub err_code: TdpErrCode, + pub read_data: Vec, +} + +impl From for SharedDirectoryReadResponse { + fn from(cgo_response: CGOSharedDirectoryReadResponse) -> SharedDirectoryReadResponse { + unsafe { + SharedDirectoryReadResponse { + completion_id: cgo_response.completion_id, + err_code: cgo_response.err_code, + read_data: from_go_array(cgo_response.read_data, cgo_response.read_data_length), + } + } + } +} + +#[derive(Debug)] +#[repr(C)] +pub struct CGOSharedDirectoryReadResponse { + pub completion_id: u32, + pub err_code: TdpErrCode, + pub read_data_length: u32, + pub read_data: *mut u8, +} + +/// SharedDirectoryWriteResponse is sent by the TDP client to the server +/// to acknowledge the completion of a SharedDirectoryWriteRequest. +#[derive(Debug)] +#[repr(C)] +pub struct SharedDirectoryWriteResponse { + pub completion_id: u32, + pub err_code: TdpErrCode, + pub bytes_written: u32, +} + +pub type CGOSharedDirectoryWriteResponse = SharedDirectoryWriteResponse; + +/// SharedDirectoryCreateRequest is sent by the TDP server to +/// the client to request the creation of a new file or directory. +#[derive(Debug)] +pub struct SharedDirectoryCreateRequest { + completion_id: u32, + directory_id: u32, + file_type: FileType, + path: String, +} + +#[repr(C)] +pub struct CGOSharedDirectoryCreateRequest { + pub completion_id: u32, + pub directory_id: u32, + pub file_type: FileType, + pub path: *const c_char, +} + +/// SharedDirectoryCreateResponse is sent by the TDP client to the server +/// to acknowledge a SharedDirectoryCreateRequest was received and executed. +#[derive(Debug, Clone)] +#[repr(C)] +pub struct SharedDirectoryCreateResponse { + pub completion_id: u32, + pub err_code: TdpErrCode, +} + +/// SharedDirectoryListResponse is sent by the TDP client to the server +/// in response to a SharedDirectoryInfoRequest. +#[derive(Debug)] +pub struct SharedDirectoryListResponse { + completion_id: u32, + 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. + // In other words, all pointer data that needs to persist after this function returns 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 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. extern "C" { @@ -1009,6 +1666,26 @@ extern "C" { client_ref: usize, req: *mut CGOSharedDirectoryInfoRequest, ) -> CGOErrCode; + fn tdp_sd_create_request( + client_ref: usize, + req: *mut CGOSharedDirectoryCreateRequest, + ) -> CGOErrCode; + fn tdp_sd_delete_request( + client_ref: usize, + req: *mut CGOSharedDirectoryDeleteRequest, + ) -> CGOErrCode; + fn tdp_sd_list_request( + client_ref: usize, + req: *mut CGOSharedDirectoryListRequest, + ) -> CGOErrCode; + fn tdp_sd_read_request( + client_ref: usize, + req: *mut CGOSharedDirectoryReadRequest, + ) -> CGOErrCode; + fn tdp_sd_write_request( + client_ref: usize, + req: *mut CGOSharedDirectoryWriteRequest, + ) -> 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 b8d572febd3..0a3eb699b94 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/consts.rs @@ -12,10 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::flags; +use super::Boolean; + pub const CHANNEL_NAME: &str = "rdpdr"; // Each redirected device requires a unique ID. We only share // one permanent smartcard device, so we can give it hardcoded ID 1. +pub const DIRECTORY_SHARE_CLIENT_NAME: &str = "teleport"; + +// Each redirected device requires a unique ID. pub const SCARD_DEVICE_ID: u32 = 1; pub const VERSION_MAJOR: u16 = 0x0001; @@ -100,7 +106,7 @@ pub enum MinorFunction { /// Windows defines an absolutely massive list of potential NTSTATUS values. /// This enum includes the basic ones we support for communicating with the windows machine. /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 -#[derive(ToPrimitive, Debug)] +#[derive(ToPrimitive, Debug, PartialEq)] #[repr(u32)] #[allow(non_camel_case_types)] #[allow(dead_code)] @@ -109,6 +115,11 @@ pub enum NTSTATUS { STATUS_UNSUCCESSFUL = 0xC0000001, STATUS_NOT_IMPLEMENTED = 0xC0000002, STATUS_NO_MORE_FILES = 0x80000006, + STATUS_OBJECT_NAME_COLLISION = 0xC0000035, + 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] @@ -116,7 +127,7 @@ pub enum NTSTATUS { #[derive(FromPrimitive, Debug, PartialEq)] #[repr(u32)] #[allow(clippy::enum_variant_names)] -pub enum FsInformationClassLevel { +pub enum FileInformationClassLevel { FileAccessInformation = 8, FileAlignmentInformation = 17, FileAllInformation = 18, @@ -165,3 +176,33 @@ pub enum FsInformationClassLevel { FileTrackingInformation = 36, FileValidDataLengthInformation = 39, } + +/// 2.5 File System Information Classes [MS-FSCC] +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ee12042a-9352-46e3-9f67-c094b75fe6c3 +#[derive(FromPrimitive, Debug, PartialEq)] +#[repr(u32)] +#[allow(clippy::enum_variant_names)] +pub enum FileSystemInformationClassLevel { + FileFsVolumeInformation = 1, + FileFsLabelInformation = 2, + FileFsSizeInformation = 3, + FileFsDeviceInformation = 4, + FileFsAttributeInformation = 5, + FileFsControlInformation = 6, + FileFsFullSizeInformation = 7, + FileFsObjectIdInformation = 8, + FileFsDriverPathInformation = 9, + FileFsVolumeFlagsInformation = 10, + FileFsSectorSizeInformation = 11, +} + +const fn size_of() -> u32 { + std::mem::size_of::() as u32 +} + +pub const U32_SIZE: u32 = size_of::(); +pub const I64_SIZE: u32 = size_of::(); +pub const I8_SIZE: u32 = size_of::(); +pub const U8_SIZE: u32 = size_of::(); +pub const FILE_ATTR_SIZE: u32 = size_of::(); +pub const BOOL_SIZE: u32 = size_of::(); diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs index 6bff374d8da..677b3bdf8a6 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/flags.rs @@ -198,3 +198,13 @@ bitflags! { const FILE_NOTIFY_CHANGE_STREAM_WRITE = 0x00000800; } } + +bitflags! { + /// Only defines the subset we require from + /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ebc7e6e5-4650-4e54-b17c-cf60f6fbeeaa + pub struct FileSystemAttributes: u32 { + const FILE_CASE_SENSITIVE_SEARCH = 0x00000001; + const FILE_CASE_PRESERVED_NAMES = 0x00000002; + const FILE_UNICODE_ON_DISK = 0x00000004; + } +} diff --git a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs index 3c40557ca2f..1872f461b00 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/rdpdr/mod.rs @@ -23,14 +23,20 @@ use crate::errors::{ use crate::util; use crate::vchan; use crate::{ - Payload, SharedDirectoryAcknowledge, SharedDirectoryInfoRequest, SharedDirectoryInfoResponse, + FileSystemObject, FileType, Payload, SharedDirectoryAcknowledge, SharedDirectoryCreateRequest, + SharedDirectoryCreateResponse, SharedDirectoryDeleteRequest, SharedDirectoryDeleteResponse, + SharedDirectoryInfoRequest, SharedDirectoryInfoResponse, SharedDirectoryListRequest, + SharedDirectoryListResponse, SharedDirectoryReadRequest, SharedDirectoryReadResponse, + SharedDirectoryWriteRequest, SharedDirectoryWriteResponse, TdpErrCode, }; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use consts::{ - CapabilityType, Component, DeviceType, FsInformationClassLevel, MajorFunction, MinorFunction, - PacketId, DRIVE_CAPABILITY_VERSION_02, GENERAL_CAPABILITY_VERSION_02, NTSTATUS, - SCARD_DEVICE_ID, SMARTCARD_CAPABILITY_VERSION_01, VERSION_MAJOR, VERSION_MINOR, + CapabilityType, Component, DeviceType, FileInformationClassLevel, + FileSystemInformationClassLevel, MajorFunction, MinorFunction, PacketId, BOOL_SIZE, + DIRECTORY_SHARE_CLIENT_NAME, DRIVE_CAPABILITY_VERSION_02, FILE_ATTR_SIZE, + GENERAL_CAPABILITY_VERSION_02, I64_SIZE, I8_SIZE, NTSTATUS, SCARD_DEVICE_ID, + SMARTCARD_CAPABILITY_VERSION_01, U32_SIZE, U8_SIZE, VERSION_MAJOR, VERSION_MINOR, }; use num_traits::{FromPrimitive, ToPrimitive}; use rdp::core::mcs; @@ -40,10 +46,12 @@ use rdp::model::error::Error as RdpError; use rdp::model::error::*; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; -use std::io::{Read, Write}; +use std::io::{Read, Seek, SeekFrom, Write}; pub use consts::CHANNEL_NAME; +use std::ffi::CString; + /// Client implements a device redirection (RDPDR) client, as defined in /// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPEFS/%5bMS-RDPEFS%5d.pdf /// @@ -54,40 +62,75 @@ pub struct Client { allow_directory_sharing: bool, active_device_ids: Vec, + /// FileId-indexed cache of FileCacheObjects. + /// See the documentation of FileCacheObject + /// for more detail on how this is used. + file_cache: FileCache, + next_file_id: u32, // used to generate file ids // Functions for sending tdp messages to the browser client. - tdp_sd_acknowledge: Box RdpResult<()>>, - tdp_sd_info_request: Box RdpResult<()>>, + tdp_sd_acknowledge: SharedDirectoryAcknowledgeSender, + tdp_sd_info_request: SharedDirectoryInfoRequestSender, + tdp_sd_create_request: SharedDirectoryCreateRequestSender, + tdp_sd_delete_request: SharedDirectoryDeleteRequestSender, + tdp_sd_list_request: SharedDirectoryListRequestSender, + tdp_sd_read_request: SharedDirectoryReadRequestSender, + tdp_sd_write_request: SharedDirectoryWriteRequestSender, - // Completion-id-indexed maps of handlers for tdp messages coming from the browser client. + // 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, + pending_sd_read_resp_handlers: HashMap, + pending_sd_write_resp_handlers: HashMap, +} + +pub struct Config { + pub cert_der: Vec, + pub key_der: Vec, + pub pin: String, + pub allow_directory_sharing: bool, + + pub tdp_sd_acknowledge: SharedDirectoryAcknowledgeSender, + pub tdp_sd_info_request: SharedDirectoryInfoRequestSender, + pub tdp_sd_create_request: SharedDirectoryCreateRequestSender, + pub tdp_sd_delete_request: SharedDirectoryDeleteRequestSender, + pub tdp_sd_list_request: SharedDirectoryListRequestSender, + pub tdp_sd_read_request: SharedDirectoryReadRequestSender, + pub tdp_sd_write_request: SharedDirectoryWriteRequestSender, } impl Client { - pub fn new( - cert_der: Vec, - key_der: Vec, - pin: String, - allow_directory_sharing: bool, - - tdp_sd_acknowledge: Box RdpResult<()>>, - tdp_sd_info_request: Box RdpResult<()>>, - ) -> Self { - if allow_directory_sharing { + pub fn new(cfg: Config) -> Self { + if cfg.allow_directory_sharing { debug!("creating rdpdr client with directory sharing enabled") } else { debug!("creating rdpdr client with directory sharing disabled") } Client { vchan: vchan::Client::new(), - scard: scard::Client::new(cert_der, key_der, pin), - active_device_ids: vec![], - allow_directory_sharing, + scard: scard::Client::new(cfg.cert_der, cfg.key_der, cfg.pin), - tdp_sd_acknowledge, - tdp_sd_info_request, + allow_directory_sharing: cfg.allow_directory_sharing, + active_device_ids: vec![], + 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, + tdp_sd_read_request: cfg.tdp_sd_read_request, + tdp_sd_write_request: cfg.tdp_sd_write_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(), + pending_sd_read_resp_handlers: HashMap::new(), + pending_sd_write_resp_handlers: HashMap::new(), } } /// Reads raw RDP messages sent on the rdpdr virtual channel and replies as necessary. @@ -138,87 +181,95 @@ impl Client { fn handle_server_announce(&self, payload: &mut Payload) -> RdpResult>> { let req = ServerAnnounceRequest::decode(payload)?; - debug!("got ServerAnnounceRequest {:?}", req); + debug!("received RDP {:?}", req); - let resp = self.add_headers_and_chunkify( - PacketId::PAKID_CORE_CLIENTID_CONFIRM, - ClientAnnounceReply::new(req).encode()?, + let resp = ClientAnnounceReply::new(req); + debug!("sending RDP {:?}", resp); + + let mut resp = + self.add_headers_and_chunkify(PacketId::PAKID_CORE_CLIENTID_CONFIRM, resp.encode()?)?; + + let client_name_request = ClientNameRequest::new( + ClientNameRequestUnicodeFlag::Ascii, + CString::new(DIRECTORY_SHARE_CLIENT_NAME.to_string()).unwrap(), + ); + + let mut client_name_response = self.add_headers_and_chunkify( + PacketId::PAKID_CORE_CLIENT_NAME, + client_name_request.encode()?, )?; - debug!("sending client announce reply"); + resp.append(&mut client_name_response); + Ok(resp) } fn handle_server_capability(&self, payload: &mut Payload) -> RdpResult>> { let req = ServerCoreCapabilityRequest::decode(payload)?; - debug!("got {:?}", req); + debug!("received RDP {:?}", req); - let resp = self.add_headers_and_chunkify( - PacketId::PAKID_CORE_CLIENT_CAPABILITY, - ClientCoreCapabilityResponse::new_response(self.allow_directory_sharing).encode()?, - )?; - debug!("sending client core capability response"); + let resp = + ClientCoreCapabilityResponse::new_response(self.allow_directory_sharing).encode()?; + debug!("sending RDP {:?}", resp); + let resp = self.add_headers_and_chunkify(PacketId::PAKID_CORE_CLIENT_CAPABILITY, resp)?; Ok(resp) } fn handle_client_id_confirm(&mut self, payload: &mut Payload) -> RdpResult>> { let req = ServerClientIdConfirm::decode(payload)?; - debug!("got ServerClientIdConfirm {:?}", req); + debug!("received RDP {:?}", req); // The smartcard initialization sequence that contains this message happens once at session startup, // and once when login succeeds. We only need to announce the smartcard once. let resp = if !self.active_device_ids.contains(&SCARD_DEVICE_ID) { self.push_active_device_id(SCARD_DEVICE_ID)?; - self.add_headers_and_chunkify( - PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, - ClientDeviceListAnnounceRequest::new_smartcard(SCARD_DEVICE_ID).encode()?, - )? + let resp = ClientDeviceListAnnounceRequest::new_smartcard(SCARD_DEVICE_ID); + debug!("sending RDP {:?}", resp); + self.add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, resp.encode()?)? } else { - self.add_headers_and_chunkify( - PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, - ClientDeviceListAnnounceRequest::new_empty().encode()?, - )? + let resp = ClientDeviceListAnnounceRequest::new_empty(); + debug!("sending RDP {:?}", resp); + self.add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, resp.encode()?)? }; - debug!("replying with: {:?}", resp); Ok(resp) } fn handle_device_reply(&self, payload: &mut Payload) -> RdpResult>> { let req = ServerDeviceAnnounceResponse::decode(payload)?; - debug!("got ServerDeviceAnnounceResponse: {:?}", req); + debug!("received RDP: {:?}", req); - if self.active_device_ids.contains(&req.device_id) { - if req.device_id != self.get_scard_device_id()? { - // This was for a directory we're sharing over TDP - let mut err_code: u32 = 0; - if req.result_code != NTSTATUS_OK { - err_code = 1; - debug!("ServerDeviceAnnounceResponse for smartcard redirection failed with result code NTSTATUS({})", &req.result_code); - } else { - debug!("ServerDeviceAnnounceResponse for shared directory succeeded") - } - - (self.tdp_sd_acknowledge)(SharedDirectoryAcknowledge { - err_code, - directory_id: req.device_id, - })?; - } else { - // This was for the smart card - if req.result_code != NTSTATUS_OK { - // End the session, we cannot continue without - // the smart card being redirected. - return Err(rejected_by_server_error(&format!( - "ServerDeviceAnnounceResponse for smartcard redirection failed with result code NTSTATUS({})", - &req.result_code - ))); - } - debug!("ServerDeviceAnnounceResponse for smartcard redirection succeeded"); - } - } else { + if !self.active_device_ids.contains(&req.device_id) { return Err(invalid_data_error(&format!( "got ServerDeviceAnnounceResponse for unknown device_id {}", &req.device_id ))); } + + if req.device_id != self.get_scard_device_id()? { + // This was for a directory we're sharing over TDP + let mut err_code = TdpErrCode::Nil; + if req.result_code != NTSTATUS_OK { + err_code = TdpErrCode::Failed; + debug!("ServerDeviceAnnounceResponse for smartcard redirection failed with result code NTSTATUS({})", &req.result_code); + } else { + debug!("ServerDeviceAnnounceResponse for shared directory succeeded") + } + + (self.tdp_sd_acknowledge)(SharedDirectoryAcknowledge { + err_code, + directory_id: req.device_id, + })?; + } else { + // This was for the smart card + if req.result_code != NTSTATUS_OK { + // End the session, we cannot continue without + // the smart card being redirected. + return Err(rejected_by_server_error(&format!( + "ServerDeviceAnnounceResponse for smartcard redirection failed with result code NTSTATUS({})", + &req.result_code + ))); + } + debug!("ServerDeviceAnnounceResponse for smartcard redirection succeeded"); + } Ok(vec![]) } @@ -239,65 +290,23 @@ impl Client { match major_function { MajorFunction::IRP_MJ_DEVICE_CONTROL => { - let ioctl = DeviceControlRequest::decode(device_io_request, payload)?; - let is_smart_card_op = ioctl.header.device_id == self.get_scard_device_id()?; - debug!("got: {:?}", ioctl); - - // IRP_MJ_DEVICE_CONTROL is the one major function used by both the smartcard controller (always enabled) - // and shared directory controller (potentially disabled by RBAC). Here we check that directory sharing - // is enabled here before proceeding with any shared directory controls as an additional security measure. - if !is_smart_card_op && !self.allow_directory_sharing { - return Err(Error::TryError("received a drive redirection major function when drive redirection was not allowed".to_string())); - } - let resp = if is_smart_card_op { - // Smart card control - let (code, res) = self.scard.ioctl(ioctl.io_control_code, payload)?; - if code == SPECIAL_NO_RESPONSE { - return Ok(vec![]); - } - DeviceControlResponse::new(&ioctl, code, res) - } else { - // Drive redirection, mimic FreeRDP's "no-op" - // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L677-L684 - DeviceControlResponse::new( - &ioctl, - NTSTATUS::STATUS_SUCCESS.to_u32().unwrap(), - vec![], - ) - }; - debug!("replying with: {:?}", resp); - let resp = self.add_headers_and_chunkify( - PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, - resp.encode()?, - )?; - debug!("sending device IO response"); - Ok(resp) + self.process_irp_device_control(device_io_request, payload) } - MajorFunction::IRP_MJ_CREATE => { - let rdp_req = ServerCreateDriveRequest::decode(device_io_request, payload)?; - debug!("got: {:?}", rdp_req); - - // Send a TDP Shared Directory Info Request - (self.tdp_sd_info_request)(SharedDirectoryInfoRequest::from(rdp_req.clone()))?; - - // Add a TDP Shared Directory Info Response handler to the handler cache. - // When we receive a TDP Shared Directory Info Response with this completion_id, - // this handler will be called. - self.pending_sd_info_resp_handlers.insert( - rdp_req.device_io_request.completion_id, - Box::new( - |_cli: &mut Self, - res: SharedDirectoryInfoResponse| - -> RdpResult>> { - let _rdp_req = rdp_req; - debug!("got {:?}", res); - // TODO(isaiah): see https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L207 - - Ok(vec![]) - }, - ), - ); - Ok(vec![]) + MajorFunction::IRP_MJ_CREATE => self.process_irp_create(device_io_request, payload), + MajorFunction::IRP_MJ_QUERY_INFORMATION => { + 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) + } + MajorFunction::IRP_MJ_QUERY_VOLUME_INFORMATION => { + self.process_irp_query_volume_information(device_io_request, payload) + } + MajorFunction::IRP_MJ_READ => self.process_irp_read(device_io_request, payload), + MajorFunction::IRP_MJ_WRITE => self.process_irp_write(device_io_request, payload), + MajorFunction::IRP_MJ_SET_INFORMATION => { + self.process_irp_set_information(device_io_request, payload) } _ => Err(invalid_data_error(&format!( // TODO(isaiah): send back a not implemented response(?) @@ -307,15 +316,488 @@ impl Client { } } - /// This is called from Go (in effect) to announce a new directory - /// for sharing. + fn process_irp_device_control( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + let ioctl = DeviceControlRequest::decode(device_io_request, payload)?; + let is_smart_card_op = ioctl.header.device_id == self.get_scard_device_id()?; + debug!("received RDP: {:?}", ioctl); + + // IRP_MJ_DEVICE_CONTROL is the one major function used by both the smartcard controller (always enabled) + // and shared directory controller (potentially disabled by RBAC). Here we check that directory sharing + // is enabled here before proceeding with any shared directory controls as an additional security measure. + if !is_smart_card_op && !self.allow_directory_sharing { + return Err(Error::TryError("received a drive redirection major function when drive redirection was not allowed".to_string())); + } + let resp = if is_smart_card_op { + // Smart card control + let (code, res) = self.scard.ioctl(ioctl.io_control_code, payload)?; + if code == SPECIAL_NO_RESPONSE { + return Ok(vec![]); + } + DeviceControlResponse::new(&ioctl, code, res) + } else { + // Drive redirection, mimic FreeRDP's "no-op" + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L677-L684 + DeviceControlResponse::new(&ioctl, NTSTATUS::STATUS_SUCCESS.to_u32().unwrap(), vec![]) + }; + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + Ok(resp) + } + + fn process_irp_create( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L207 + let rdp_req = ServerCreateDriveRequest::decode(device_io_request, payload)?; + debug!("received RDP: {:?}", rdp_req); + + // 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()); + (self.tdp_sd_info_request)(tdp_req)?; + + // Add a TDP Shared Directory Info Response handler to the handler cache. + // When we receive a TDP Shared Directory Info Response with this completion_id, + // this handler will be called. + self.pending_sd_info_resp_handlers.insert( + rdp_req.device_io_request.completion_id, + Box::new( + |cli: &mut Self, res: SharedDirectoryInfoResponse| -> RdpResult>> { + let rdp_req = rdp_req; + + match res.err_code { + TdpErrCode::Failed | TdpErrCode::AlreadyExists => { + return Err(try_error(&format!( + "received unexpected TDP error code in SharedDirectoryInfoResponse: {:?}", + res.err_code, + ))); + } + TdpErrCode::Nil => { + // The file exists + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L214 + if res.fso.file_type == FileType::Directory { + if rdp_req.create_disposition + == flags::CreateDisposition::FILE_CREATE + { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L221 + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_OBJECT_NAME_COLLISION, + 0, + ); + } + + if rdp_req + .create_options + .contains(flags::CreateOptions::FILE_NON_DIRECTORY_FILE) + { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L227 + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_ACCESS_DENIED, + 0, + ); + } + } else if rdp_req + .create_options + .contains(flags::CreateOptions::FILE_DIRECTORY_FILE) + { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L237 + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_NOT_A_DIRECTORY, + 0, + ); + } + } + TdpErrCode::DoesNotExist => { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L242 + if rdp_req + .create_options + .contains(flags::CreateOptions::FILE_DIRECTORY_FILE) + { + if rdp_req.create_disposition.intersects( + flags::CreateDisposition::FILE_OPEN_IF + | flags::CreateDisposition::FILE_CREATE, + ) { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L252 + return cli.tdp_sd_create( + rdp_req, + FileType::Directory, + res.fso, + ); + } else { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L258 + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_NO_SUCH_FILE, + 0, + ); + } + } + } + } + + // The actual creation of files and error mapping in FreeRDP happens here, for reference: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/winpr/libwinpr/file/file.c#L781 + match rdp_req.create_disposition { + flags::CreateDisposition::FILE_SUPERSEDE => { + // If the file already exists, replace it with the given file. If it does not, create the given file. + if res.err_code == TdpErrCode::Nil { + return cli.tdp_sd_overwrite(rdp_req, res.fso); + } else if res.err_code == TdpErrCode::DoesNotExist { + return cli.tdp_sd_create(rdp_req, FileType::File, res.fso); + } + } + flags::CreateDisposition::FILE_OPEN => { + // If the file already exists, open it instead of creating a new file. If it does not, fail the request and do not create a new file. + if res.err_code == TdpErrCode::Nil { + let file_id = cli.generate_file_id(); + cli.file_cache.insert( + file_id, + FileCacheObject::new(rdp_req.path.clone(), res.fso), + ); + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_SUCCESS, + file_id, + ); + } else if res.err_code == TdpErrCode::DoesNotExist { + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_NO_SUCH_FILE, + 0, + ) + } + } + flags::CreateDisposition::FILE_CREATE => { + // If the file already exists, fail the request and do not create or open the given file. If it does not, create the given file. + if res.err_code == TdpErrCode::Nil { + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_OBJECT_NAME_COLLISION, + 0, + ); + } else if res.err_code == TdpErrCode::DoesNotExist { + return cli.tdp_sd_create(rdp_req, FileType::File, res.fso); + } + } + flags::CreateDisposition::FILE_OPEN_IF => { + // If the file already exists, open it. If it does not, create the given file. + if res.err_code == TdpErrCode::Nil { + let file_id = cli.generate_file_id(); + cli.file_cache.insert( + file_id, + FileCacheObject::new(rdp_req.path.clone(), res.fso), + ); + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_SUCCESS, + file_id, + ); + } else if res.err_code == TdpErrCode::DoesNotExist { + return cli.tdp_sd_create(rdp_req, FileType::File, res.fso); + } + } + flags::CreateDisposition::FILE_OVERWRITE => { + // If the file already exists, open it and overwrite it. If it does not, fail the request. + if res.err_code == TdpErrCode::Nil { + return cli.tdp_sd_overwrite(rdp_req, res.fso); + } else if res.err_code == TdpErrCode::DoesNotExist { + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_NO_SUCH_FILE, + 0, + ) + } + } + flags::CreateDisposition::FILE_OVERWRITE_IF => { + // If the file already exists, open it and overwrite it. If it does not, create the given file. + if res.err_code == TdpErrCode::Nil { + return cli.tdp_sd_overwrite(rdp_req, res.fso); + } else if res.err_code == TdpErrCode::DoesNotExist { + return cli.tdp_sd_create(rdp_req, FileType::File, res.fso); + } + } + _ => { + return Err(invalid_data_error(&format!( + "received unknown CreateDisposition value for RDP {:?}", + rdp_req + ))); + } + } + + Err(try_error("Programmer error, this line should never be reached")) + }, + ), + ); + + Ok(vec![]) + } + + fn process_irp_query_information( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + // 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.file_cache.get(rdp_req.device_io_request.file_id); + let code = if f.is_some() { + NTSTATUS::STATUS_SUCCESS + } else { + NTSTATUS::STATUS_UNSUCCESSFUL + }; + self.prep_query_info_response(&rdp_req, f, code) + } + + fn process_irp_close(&mut self, device_io_request: DeviceIoRequest) -> RdpResult>> { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L236 + let rdp_req = DeviceCloseRequest::decode(device_io_request); + debug!("received RDP: {:?}", rdp_req); + // Remove the file from our cache + if let Some(file) = self.file_cache.remove(rdp_req.device_io_request.file_id) { + if file.delete_pending { + return self.tdp_sd_delete(rdp_req, file); + } + return self.prep_device_close_response(rdp_req, NTSTATUS::STATUS_SUCCESS); + } + + self.prep_device_close_response(rdp_req, NTSTATUS::STATUS_UNSUCCESSFUL) + } + + /// 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")); + } + + if rdp_req.initial_query == 0 { + // This isn't the initial query, ergo we already have this dir's contents filled in. + // Just send the next item. + return self.prep_next_drive_query_dir_response(&rdp_req); + } + + // On the initial query, we need to get the list of files in this directory from + // the client by sending a TDP SharedDirectoryListRequest. + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L775 + // TODO(isaiah): I'm observing that sometimes rdp_req.path will not be precisely equal to dir.path. For example, we will + // get a ServerDriveQueryDirectoryRequest where path == "\\*", whereas the corresponding entry in the file_cache will have + // path == "\\". I'm not quite sure what to do with this yet, so just leaving this as a note to self. + 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 { + // TODO(isaiah): For now any error will kill the session. + // In the future, we might want to make this send back + // an NTSTATUS::STATUS_UNSUCCESSFUL instead. + return Err(try_error(&format!( + "SharedDirectoryListRequest failed with err_code = {:?}", + res.err_code + ))); + } + + // 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; + // And send back the "." directory over RDP + return cli.prep_next_drive_query_dir_response(&rdp_req); + } + + cli.prep_file_cache_fail_drive_query_dir_response(&rdp_req) + }, + ), + ); + + // Return nothing yet, an RDP message will be returned when the pending_sd_list_resp_handlers + // closure gets called. + return Ok(vec![]); + } + + // File not found in cache, return a failure + 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, + ) + } + } + } + + /// https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L442 + fn process_irp_query_volume_information( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + let rdp_req = ServerDriveQueryVolumeInformationRequest::decode(device_io_request, payload)?; + debug!("received RDP: {:?}", rdp_req); + if let Some(dir) = self.file_cache.get(rdp_req.device_io_request.file_id) { + // TODO(isaiah): we should support all of the fs_info_class_lvls that FreeRDP does: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L468 + match rdp_req.fs_info_class_lvl { + FileSystemInformationClassLevel::FileFsVolumeInformation => { + let buffer = Some(FileSystemInformationClass::FileFsVolumeInformation( + FileFsVolumeInformation::new(dir.fso.last_modified as i64), + )); + return self.prep_query_vol_info_response( + &rdp_req.device_io_request, + NTSTATUS::STATUS_SUCCESS, + buffer, + ); + } + FileSystemInformationClassLevel::FileFsAttributeInformation => { + let buffer = Some(FileSystemInformationClass::FileFsAttributeInformation( + FileFsAttributeInformation::new(), + )); + return self.prep_query_vol_info_response( + &rdp_req.device_io_request, + NTSTATUS::STATUS_SUCCESS, + buffer, + ); + } + FileSystemInformationClassLevel::FileFsSizeInformation + | FileSystemInformationClassLevel::FileFsFullSizeInformation + | FileSystemInformationClassLevel::FileFsDeviceInformation => { + return Err(not_implemented_error(&format!( + "support for ServerDriveQueryVolumeInformationRequest with fs_info_class_lvl = {:?} is not implemented", + rdp_req.fs_info_class_lvl + ))); + } + _ => { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L574-L577 + return self.prep_query_vol_info_response( + &rdp_req.device_io_request, + NTSTATUS::STATUS_UNSUCCESSFUL, + None, + ); + } + } + } + + // File not found in cache + Err(invalid_data_error(&format!( + "failed to retrieve an item from the file cache with FileId = {}", + rdp_req.device_io_request.file_id + ))) + } + + fn process_irp_read( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L268 + let rdp_req = DeviceReadRequest::decode(device_io_request, payload)?; + debug!("received RDP: {:?}", rdp_req); + self.tdp_sd_read(rdp_req) + } + + fn process_irp_write( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + let rdp_req = DeviceWriteRequest::decode(device_io_request, payload)?; + debug!("received RDP: {:?}", rdp_req); + self.tdp_sd_write(rdp_req) + } + + fn process_irp_set_information( + &mut self, + device_io_request: DeviceIoRequest, + payload: &mut Payload, + ) -> RdpResult>> { + let rdp_req = ServerDriveSetInformationRequest::decode(device_io_request, payload)?; + + let resp = match rdp_req.file_information_class_level { + FileInformationClassLevel::FileBasicInformation + | FileInformationClassLevel::FileEndOfFileInformation + | FileInformationClassLevel::FileAllocationInformation + | FileInformationClassLevel::FileDispositionInformation => { + ClientDriveSetInformationResponse::new(&rdp_req, NTSTATUS::STATUS_SUCCESS) + } + _ => { + return Err(not_implemented_error(&format!( + "support for ServerDriveSetInformationRequest with fs_info_class_lvl = {:?} is not implemented", + rdp_req.file_information_class_level + ))); + } + }; + + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + Ok(resp) + } + pub fn write_client_device_list_announce( &mut self, req: ClientDeviceListAnnounce, mcs: &mut mcs::Client, ) -> RdpResult<()> { self.push_active_device_id(req.device_list[0].device_id)?; - debug!("sending new drive for redirection: {:?}", req); + debug!("sending new drive for redirection over RDP: {:?}", req); let responses = self.add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICELIST_ANNOUNCE, req.encode()?)?; @@ -332,6 +814,7 @@ impl Client { res: SharedDirectoryInfoResponse, mcs: &mut mcs::Client, ) -> RdpResult<()> { + debug!("received TDP SharedDirectoryInfoResponse: {:?}", res); if let Some(tdp_resp_handler) = self .pending_sd_info_resp_handlers .remove(&res.completion_id) @@ -341,13 +824,492 @@ impl Client { for resp in rdp_responses { mcs.write(chan, resp)?; } - Ok(()) - } else { - return Err(try_error(&format!( - "received invalid completion id: {}", - res.completion_id - ))); + return Ok(()); } + + Err(try_error(&format!( + "received invalid completion id: {}", + res.completion_id + ))) + } + + pub fn handle_tdp_sd_create_response( + &mut self, + res: SharedDirectoryCreateResponse, + mcs: &mut mcs::Client, + ) -> RdpResult<()> { + debug!("received TDP SharedDirectoryCreateResponse: {:?}", res); + if let Some(tdp_resp_handler) = self + .pending_sd_create_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)?; + } + return Ok(()); + } + + Err(try_error(&format!( + "received invalid completion id: {}", + res.completion_id + ))) + } + + pub fn handle_tdp_sd_delete_response( + &mut self, + res: SharedDirectoryDeleteResponse, + mcs: &mut mcs::Client, + ) -> RdpResult<()> { + debug!("received TDP SharedDirectoryDeleteResponse: {:?}", res); + if let Some(tdp_resp_handler) = self + .pending_sd_delete_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)?; + } + return Ok(()); + } + + Err(try_error(&format!( + "received invalid completion id: {}", + res.completion_id + ))) + } + + 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)?; + } + return Ok(()); + } + + Err(try_error(&format!( + "received invalid completion id: {}", + res.completion_id + ))) + } + + pub fn handle_tdp_sd_read_response( + &mut self, + res: SharedDirectoryReadResponse, + mcs: &mut mcs::Client, + ) -> RdpResult<()> { + debug!("received TDP: {:?}", res); + if let Some(tdp_resp_handler) = self + .pending_sd_read_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)?; + } + return Ok(()); + } + + Err(try_error(&format!( + "received invalid completion id: {}", + res.completion_id + ))) + } + + pub fn handle_tdp_sd_write_response( + &mut self, + res: SharedDirectoryWriteResponse, + mcs: &mut mcs::Client, + ) -> RdpResult<()> { + debug!("received TDP: {:?}", res); + if let Some(tdp_resp_handler) = self + .pending_sd_write_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)?; + } + return Ok(()); + } + + Err(try_error(&format!( + "received invalid completion id: {}", + res.completion_id + ))) + } + + fn prep_device_create_response( + &mut self, + req: &DeviceCreateRequest, + io_status: NTSTATUS, + new_file_id: u32, + ) -> RdpResult>> { + let resp = DeviceCreateResponse::new(req, io_status, new_file_id); + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + Ok(resp) + } + + fn prep_query_info_response( + &self, + req: &ServerDriveQueryInformationRequest, + file: Option<&FileCacheObject>, + io_status: NTSTATUS, + ) -> RdpResult>> { + let resp = ClientDriveQueryInformationResponse::new(req, file, io_status)?; + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + Ok(resp) + } + + fn prep_device_close_response( + &self, + req: DeviceCloseRequest, + io_status: NTSTATUS, + ) -> RdpResult>> { + let resp = DeviceCloseResponse::new(req, io_status); + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + 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 FileInformationClass 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.file_info_class_lvl { + // TODO(isaiah): we should support all the file_info_class_lvl's that FreeRDP does: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L794 + FileInformationClassLevel::FileBothDirectoryInformation => { + let buffer = Some(FileInformationClass::FileBothDirectoryInformation( + FileBothDirectoryInformation::from(fso)?, + )); + return self.prep_drive_query_dir_response( + &req.device_io_request, + NTSTATUS::STATUS_SUCCESS, + buffer, + ); + } + FileInformationClassLevel::FileFullDirectoryInformation => { + let buffer = Some(FileInformationClass::FileFullDirectoryInformation( + FileFullDirectoryInformation::from(fso)?, + )); + return self.prep_drive_query_dir_response( + &req.device_io_request, + NTSTATUS::STATUS_SUCCESS, + buffer, + ); + } + FileInformationClassLevel::FileDirectoryInformation + | FileInformationClassLevel::FileNamesInformation => { + return Err(not_implemented_error(&format!( + "support for ServerDriveQueryDirectoryRequest with file_info_class_lvl = {:?} is not implemented", + req.file_info_class_lvl + ))); + } + _ => { + return Err(invalid_data_error("received invalid FileInformationClassLevel in ServerDriveQueryDirectoryRequest")); + } + } + } + + // If we reach here it means our iterator is exhausted, + // so we 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 + return self.prep_drive_query_dir_response( + &req.device_io_request, + NTSTATUS::STATUS_NO_MORE_FILES, + None, + ); + } + + // File not found in cache + 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, + ) + } + + fn prep_query_vol_info_response( + &self, + device_io_request: &DeviceIoRequest, + io_status: NTSTATUS, + buffer: Option, + ) -> RdpResult>> { + let resp = + ClientDriveQueryVolumeInformationResponse::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) + } + + fn prep_read_response( + &self, + req: DeviceReadRequest, + io_status: NTSTATUS, + data: Vec, + ) -> RdpResult>> { + let resp = DeviceReadResponse::new(&req, io_status, data); + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + Ok(resp) + } + + fn prep_write_response( + &self, + req: DeviceIoRequest, + io_status: NTSTATUS, + length: u32, + ) -> RdpResult>> { + let resp = DeviceWriteResponse::new(&req, io_status, length); + debug!("sending RDP: {:?}", resp); + let resp = self + .add_headers_and_chunkify(PacketId::PAKID_CORE_DEVICE_IOCOMPLETION, resp.encode()?)?; + Ok(resp) + } + + /// Helper function for sending a TDP SharedDirectoryCreateRequest based on an + /// RDP DeviceCreateRequest and handling the TDP SharedDirectoryCreateResponse. + fn tdp_sd_create( + &mut self, + rdp_req: DeviceCreateRequest, + file_type: FileType, + fso: FileSystemObject, + ) -> RdpResult>> { + let tdp_req = SharedDirectoryCreateRequest { + completion_id: rdp_req.device_io_request.completion_id, + directory_id: rdp_req.device_io_request.device_id, + file_type, + path: rdp_req.path.clone(), + }; + (self.tdp_sd_create_request)(tdp_req)?; + + self.pending_sd_create_resp_handlers.insert( + rdp_req.device_io_request.completion_id, + Box::new( + move |cli: &mut Self, + res: SharedDirectoryCreateResponse| + -> RdpResult>> { + if res.err_code != TdpErrCode::Nil { + return cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_UNSUCCESSFUL, + 0, + ); + } + + let file_id = cli.generate_file_id(); + cli.file_cache + .insert(file_id, FileCacheObject::new(rdp_req.path.clone(), fso)); + cli.prep_device_create_response(&rdp_req, NTSTATUS::STATUS_SUCCESS, file_id) + }, + ), + ); + Ok(vec![]) + } + + /// Helper function for combining a TDP SharedDirectoryDeleteRequest + /// with a TDP SharedDirectoryCreateRequest to overwrite a file, based + /// on an RDP DeviceCreateRequest. + fn tdp_sd_overwrite( + &mut self, + rdp_req: DeviceCreateRequest, + fso: FileSystemObject, + ) -> RdpResult>> { + let tdp_req = SharedDirectoryDeleteRequest { + completion_id: rdp_req.device_io_request.completion_id, + directory_id: rdp_req.device_io_request.device_id, + path: rdp_req.path.clone(), + }; + (self.tdp_sd_delete_request)(tdp_req)?; + self.pending_sd_delete_resp_handlers.insert( + rdp_req.device_io_request.completion_id, + Box::new( + |cli: &mut Self, res: SharedDirectoryDeleteResponse| -> RdpResult>> { + match res.err_code { + TdpErrCode::Nil => cli.tdp_sd_create(rdp_req, FileType::File, fso), + _ => cli.prep_device_create_response( + &rdp_req, + NTSTATUS::STATUS_UNSUCCESSFUL, + 0, + ), + } + }, + ), + ); + Ok(vec![]) + } + + fn tdp_sd_delete( + &mut self, + rdp_req: DeviceCloseRequest, + file: FileCacheObject, + ) -> RdpResult>> { + let tdp_req = SharedDirectoryDeleteRequest { + completion_id: rdp_req.device_io_request.completion_id, + directory_id: rdp_req.device_io_request.device_id, + path: file.path, + }; + (self.tdp_sd_delete_request)(tdp_req)?; + self.pending_sd_delete_resp_handlers.insert( + rdp_req.device_io_request.completion_id, + Box::new( + |cli: &mut Self, res: SharedDirectoryDeleteResponse| -> RdpResult>> { + let code = if res.err_code == TdpErrCode::Nil { + NTSTATUS::STATUS_SUCCESS + } else { + NTSTATUS::STATUS_UNSUCCESSFUL + }; + cli.prep_device_close_response(rdp_req, code) + }, + ), + ); + Ok(vec![]) + } + + fn tdp_sd_read(&mut self, rdp_req: DeviceReadRequest) -> RdpResult>> { + if let Some(file) = self.file_cache.get(rdp_req.device_io_request.file_id) { + let tdp_req = SharedDirectoryReadRequest { + completion_id: rdp_req.device_io_request.completion_id, + directory_id: rdp_req.device_io_request.device_id, + path: file.path.clone(), + length: rdp_req.length, + offset: rdp_req.offset, + }; + (self.tdp_sd_read_request)(tdp_req)?; + + self.pending_sd_read_resp_handlers.insert( + rdp_req.device_io_request.completion_id, + Box::new( + move |cli: &mut Self, + res: SharedDirectoryReadResponse| + -> RdpResult>> { + match res.err_code { + TdpErrCode::Nil => cli.prep_read_response( + rdp_req, + NTSTATUS::STATUS_SUCCESS, + res.read_data, + ), + _ => cli.prep_read_response( + rdp_req, + NTSTATUS::STATUS_UNSUCCESSFUL, + vec![], + ), + } + }, + ), + ); + + return Ok(vec![]); + } + + // File not found in cache + self.prep_read_response(rdp_req, NTSTATUS::STATUS_UNSUCCESSFUL, vec![]) + } + + fn tdp_sd_write(&mut self, rdp_req: DeviceWriteRequest) -> RdpResult>> { + if let Some(file) = self.file_cache.get(rdp_req.device_io_request.file_id) { + let tdp_req = SharedDirectoryWriteRequest { + completion_id: rdp_req.device_io_request.completion_id, + directory_id: rdp_req.device_io_request.device_id, + path: file.path.clone(), + offset: rdp_req.offset, + write_data: rdp_req.write_data, + }; + (self.tdp_sd_write_request)(tdp_req)?; + + let device_io_request = rdp_req.device_io_request; + self.pending_sd_write_resp_handlers.insert( + device_io_request.completion_id, + Box::new( + move |cli: &mut Self, + res: SharedDirectoryWriteResponse| + -> RdpResult>> { + match res.err_code { + TdpErrCode::Nil => cli.prep_write_response( + device_io_request, + NTSTATUS::STATUS_SUCCESS, + res.bytes_written, + ), + _ => cli.prep_write_response( + device_io_request, + NTSTATUS::STATUS_UNSUCCESSFUL, + 0, + ), + } + }, + ), + ); + + return Ok(vec![]); + } + + // File not found in cache + self.prep_write_response(rdp_req.device_io_request, NTSTATUS::STATUS_UNSUCCESSFUL, 0) } /// add_headers_and_chunkify takes an encoded PDU ready to be sent over a virtual channel (payload), @@ -381,6 +1343,152 @@ impl Client { } Err(RdpError::TryError("no active device ids".to_string())) } + + fn generate_file_id(&mut self) -> u32 { + self.next_file_id = self.next_file_id.wrapping_add(1); + self.next_file_id + } +} + +/// FileCacheObject is an in-memory representation of +/// of a file or directory holding the metadata necessary +/// for RDP drive redirection. They are stored in map indexed +/// by their RDP FileId. +/// +/// The lifecycle for a FileCacheObject is a function of the +/// MajorFunction of RDP DeviceIoRequests: +/// +/// | Sequence | MajorFunction | results in | +/// | -------- | ------------- | ---------------------------------------------------------| +/// | 1 | IRP_MJ_CREATE | A new FileCacheObject is created and assigned a FileId | +/// | -------- | ------------- | ---------------------------------------------------------| +/// | 2 | | The FCO is retrieved from the cache by the FileId in the | +/// | | | DeviceIoRequest and metadata is used to craft a response | +/// | -------- | ------------- | ---------------------------------------------------------| +/// | 3 | IRP_MJ_CLOSE | The FCO is deleted from the cache | +/// | -------- | ------------- | ---------------------------------------------------------| +#[derive(Debug)] +struct FileCacheObject { + path: String, + delete_pending: bool, + /// The FileSystemObject pertaining to the file or directory at path. + fso: FileSystemObject, + /// 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 { + fn new(path: String, fso: FileSystemObject) -> Self { + Self { + path, + delete_pending: false, + fso, + 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) @@ -697,12 +1805,8 @@ impl ClientDeviceListAnnounceRequest { } } - /// Creates a ClientDeviceListAnnounceRequest for announcing a new shared drive (directory). /// A new drive can be announced at any time during RDP's operation. It is up to the caller - /// to ensure that the passed device_id is unique from that of any previously shared devices. pub fn new_drive(device_id: u32, drive_name: String) -> Self { - // According to the spec: - // // If the client supports DRIVE_CAPABILITY_VERSION_02 in the Drive Capability Set, // then the full name MUST also be specified in the DeviceData field, as a null-terminated // Unicode string. If the DeviceDataLength field is nonzero, the content of the @@ -785,10 +1889,57 @@ impl ServerDeviceAnnounceResponse { } } +#[derive(Debug, Clone, ToPrimitive)] +#[repr(u32)] +#[allow(non_camel_case_types)] +#[allow(dead_code)] +enum ClientNameRequestUnicodeFlag { + Ascii = 0x0, + Unicode = 0x1, +} + +/// 2.2.2.4 Client Name Request (DR_CORE_CLIENT_NAME_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/902497f1-3b1c-4aee-95f8-1668f9b7b7d2 +#[derive(Debug, Clone)] +pub struct ClientNameRequest { + unicode_flag: ClientNameRequestUnicodeFlag, + computer_name: CString, +} + +impl ClientNameRequest { + fn new(unicode_flag: ClientNameRequestUnicodeFlag, computer_name: CString) -> Self { + Self { + unicode_flag, + computer_name, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_u32::(self.unicode_flag.clone() as u32)?; + // CodePage (4 bytes): A 32-bit unsigned integer that specifies the code page of the ComputerName field; it MUST be set to 0. + w.write_u32::(0x0)?; + + let computer_name_data = match self.unicode_flag { + ClientNameRequestUnicodeFlag::Ascii => self.computer_name.to_bytes_with_nul().to_vec(), + ClientNameRequestUnicodeFlag::Unicode => util::to_unicode( + self.computer_name + .as_c_str() + .to_str() + .map_err(|err| RdpError::TryError(err.to_string()))?, + true, + ), + }; + + w.write_u32::(computer_name_data.len() as u32)?; + w.extend_from_slice(&computer_name_data); + Ok(w) + } +} + /// 2.2.1.4 Device I/O Request (DR_DEVICE_IOREQUEST) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/a087ffa8-d0d5-4874-ac7b-0494f63e2d5d #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct DeviceIoRequest { pub device_id: u32, file_id: u32, @@ -941,7 +2092,6 @@ pub struct DeviceCreateRequest { pub path: String, } -#[allow(dead_code)] impl DeviceCreateRequest { fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { let invalid_flags = || invalid_data_error("invalid flags in Device Create Request"); @@ -984,7 +2134,6 @@ impl DeviceCreateRequest { /// A message with this header describes a response to a Device Create Request (section 2.2.1.4.1). /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/99e5fca5-b37a-41e4-bc69-8d7da7860f76 #[derive(Debug)] -#[allow(dead_code)] struct DeviceCreateResponse { device_io_reply: DeviceIoResponse, file_id: u32, @@ -1005,18 +2154,21 @@ struct DeviceCreateResponse { information: flags::Information, } -#[allow(dead_code)] impl DeviceCreateResponse { - fn new(device_create_request: &DeviceCreateRequest, io_status: NTSTATUS) -> Self { + fn new(device_create_request: &DeviceCreateRequest, io_status: NTSTATUS, file_id: u32) -> Self { let device_io_request = &device_create_request.device_io_request; let information: flags::Information; - if device_create_request.create_disposition.intersects( - flags::CreateDisposition::FILE_SUPERSEDE - | flags::CreateDisposition::FILE_OPEN - | flags::CreateDisposition::FILE_CREATE - | flags::CreateDisposition::FILE_OVERWRITE, - ) { + if io_status != NTSTATUS::STATUS_SUCCESS + || device_create_request.create_disposition.intersects( + flags::CreateDisposition::FILE_SUPERSEDE + | flags::CreateDisposition::FILE_OPEN + | flags::CreateDisposition::FILE_CREATE + | flags::CreateDisposition::FILE_OVERWRITE, + ) + { + // if io_status != NTSTATUS::STATUS_SUCCESS because that's what FreeRDP sets information to in the case of failure, see + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L191 information = flags::Information::FILE_SUPERSEDED; } else if device_create_request.create_disposition == flags::CreateDisposition::FILE_OPEN_IF { @@ -1034,7 +2186,7 @@ impl DeviceCreateResponse { device_io_request, NTSTATUS::to_u32(&io_status).unwrap(), ), - file_id: device_io_request.file_id, // TODO(isaiah): this is false, the client should be generating the file_id here + file_id, information, } } @@ -1051,7 +2203,6 @@ impl DeviceCreateResponse { /// 2.2.3.3.8 Server Drive Query Information Request (DR_DRIVE_QUERY_INFORMATION_REQ) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/e43dcd68-2980-40a9-9238-344b6cf94946 #[derive(Debug)] -#[allow(dead_code)] struct ServerDriveQueryInformationRequest { /// A DR_DEVICE_IOREQUEST (section 2.2.1.4) header. The MajorFunction field in the DR_DEVICE_IOREQUEST header MUST be set to IRP_MJ_QUERY_INFORMATION. device_io_request: DeviceIoRequest, @@ -1065,7 +2216,7 @@ struct ServerDriveQueryInformationRequest { /// /// FileAttributeTagInformation /// This information class is used to query for file attribute and reparse tag information. - fs_information_class_lvl: FsInformationClassLevel, + file_info_class_lvl: FileInformationClassLevel, // Length, Padding, and QueryBuffer appear to be vestigial fields and can safely be ignored. Their description // is provided below for documentation purposes. // @@ -1074,26 +2225,25 @@ struct ServerDriveQueryInformationRequest { // Padding (24 bytes): An array of 24 bytes. This field is unused and MUST be ignored. // // QueryBuffer (variable): A variable-length array of bytes. The size of the array is specified by the Length field. - // The content of this field is based on the value of the FsInformationClass field, which determines the different + // The content of this field is based on the value of the FileInformationClass field, which determines the different // structures that MUST be contained in the QueryBuffer field. For a complete list of these structures, see [MS-FSCC] - // section 2.4. The "File information class" table defines all the possible values for the FsInformationClass field. + // section 2.4. The "File information class" table defines all the possible values for the FileInformationClass field. } -#[allow(dead_code)] impl ServerDriveQueryInformationRequest { fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { - if let Some(fs_information_class_lvl) = - FsInformationClassLevel::from_u32(payload.read_u32::()?) + if let Some(file_info_class_lvl) = + FileInformationClassLevel::from_u32(payload.read_u32::()?) { - Ok(Self { + return Ok(Self { device_io_request, - fs_information_class_lvl, - }) - } else { - Err(invalid_data_error( - "received invalid FsInformationClass in ServerDriveQueryInformationRequest", - )) + file_info_class_lvl, + }); } + + Err(invalid_data_error( + "received invalid FileInformationClass in ServerDriveQueryInformationRequest", + )) } } @@ -1101,19 +2251,81 @@ impl ServerDriveQueryInformationRequest { /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4718fc40-e539-4014-8e33-b675af74e3e1 #[derive(Debug)] #[allow(dead_code, clippy::enum_variant_names)] -enum FsInformationClass { +enum FileInformationClass { FileBasicInformation(FileBasicInformation), FileStandardInformation(FileStandardInformation), FileBothDirectoryInformation(FileBothDirectoryInformation), + FileAttributeTagInformation(FileAttributeTagInformation), + FileFullDirectoryInformation(FileFullDirectoryInformation), + FileEndOfFileInformation(FileEndOfFileInformation), + FileDispositionInformation(FileDispositionInformation), + FileRenameInformation(FileRenameInformation), + FileAllocationInformation(FileAllocationInformation), } -#[allow(dead_code)] -impl FsInformationClass { +impl FileInformationClass { fn encode(&self) -> RdpResult> { match self { - Self::FileBasicInformation(file_basic_info) => file_basic_info.encode(), - Self::FileStandardInformation(file_standard_info) => file_standard_info.encode(), - Self::FileBothDirectoryInformation(fil_both_dir_info) => fil_both_dir_info.encode(), // TODO(isaiah) + Self::FileBasicInformation(file_info_class) => file_info_class.encode(), + Self::FileStandardInformation(file_info_class) => file_info_class.encode(), + Self::FileBothDirectoryInformation(file_info_class) => file_info_class.encode(), + Self::FileAttributeTagInformation(file_info_class) => file_info_class.encode(), + Self::FileFullDirectoryInformation(file_info_class) => file_info_class.encode(), + Self::FileEndOfFileInformation(file_info_class) => file_info_class.encode(), + Self::FileDispositionInformation(file_info_class) => file_info_class.encode(), + Self::FileRenameInformation(file_info_class) => file_info_class.encode(), + Self::FileAllocationInformation(file_info_class) => file_info_class.encode(), + } + } + + fn decode( + file_information_class_level: &FileInformationClassLevel, + payload: &mut Payload, + ) -> RdpResult { + match file_information_class_level { + FileInformationClassLevel::FileBasicInformation => Ok( + FileInformationClass::FileBasicInformation(FileBasicInformation::decode(payload)?), + ), + FileInformationClassLevel::FileEndOfFileInformation => { + Ok(FileInformationClass::FileEndOfFileInformation( + FileEndOfFileInformation::decode(payload)?, + )) + } + FileInformationClassLevel::FileDispositionInformation => { + Ok(FileInformationClass::FileDispositionInformation( + FileDispositionInformation::decode(payload)?, + )) + } + FileInformationClassLevel::FileRenameInformation => { + Ok(FileInformationClass::FileRenameInformation( + FileRenameInformation::decode(payload)?, + )) + } + FileInformationClassLevel::FileAllocationInformation => { + Ok(FileInformationClass::FileAllocationInformation( + FileAllocationInformation::decode(payload)?, + )) + } + _ => { + return Err(invalid_data_error(&format!( + "decode invalid FileInformationClassLevel: {:?}", + file_information_class_level + ))) + } + } + } + + fn size(&self) -> u32 { + match self { + Self::FileBasicInformation(file_info_class) => file_info_class.size(), + Self::FileStandardInformation(file_info_class) => file_info_class.size(), + Self::FileBothDirectoryInformation(file_info_class) => file_info_class.size(), + Self::FileAttributeTagInformation(file_info_class) => file_info_class.size(), + Self::FileFullDirectoryInformation(file_info_class) => file_info_class.size(), + Self::FileEndOfFileInformation(file_info_class) => file_info_class.size(), + Self::FileDispositionInformation(file_info_class) => file_info_class.size(), + Self::FileRenameInformation(file_info_class) => file_info_class.size(), + Self::FileAllocationInformation(file_info_class) => file_info_class.size(), } } } @@ -1132,11 +2344,9 @@ struct FileBasicInformation { //reserved: u32, } -#[allow(dead_code)] -/// 4 i64's and 1 u32's = (4 * 8) + 4 -const FILE_BASIC_INFORMATION_SIZE: u32 = (4 * 8) + 4; - impl FileBasicInformation { + const BASE_SIZE: u32 = (4 * I64_SIZE) + FILE_ATTR_SIZE; + fn encode(&self) -> RdpResult> { let mut w = vec![]; w.write_i64::(self.creation_time)?; @@ -1146,6 +2356,27 @@ impl FileBasicInformation { w.write_u32::(self.file_attributes.bits())?; Ok(w) } + + fn decode(payload: &mut Payload) -> RdpResult { + let creation_time = payload.read_i64::()?; + let last_access_time = payload.read_i64::()?; + let last_write_time = payload.read_i64::()?; + let change_time = payload.read_i64::()?; + let file_attributes = flags::FileAttributes::from_bits(payload.read_u32::()?) + .ok_or_else(|| invalid_data_error("invalid flags in FileBasicInformation decode"))?; + + Ok(Self { + creation_time, + last_access_time, + last_write_time, + change_time, + file_attributes, + }) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } } /// 2.4.41 FileStandardInformation [MS-FSCC] @@ -1183,6 +2414,8 @@ struct FileStandardInformation { } impl FileStandardInformation { + const BASE_SIZE: u32 = (2 * I64_SIZE) + U32_SIZE + (2 * BOOL_SIZE); + fn encode(&self) -> RdpResult> { let mut w = vec![]; w.write_i64::(self.allocation_size)?; @@ -1192,17 +2425,39 @@ impl FileStandardInformation { w.write_u8(Boolean::to_u8(&self.directory).unwrap())?; Ok(w) } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } } -#[allow(dead_code)] -// 2 i64's + 1 u32 + 2 Boolean (u8) = (2 * 8) + 4 + 2 -const FILE_STANDARD_INFORMATION_SIZE: u32 = (2 * 8) + 4 + 2; +/// 2.4.6 FileAttributeTagInformation [MS-FSCC] +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e?redirectedfrom=MSDN +#[derive(Debug)] +struct FileAttributeTagInformation { + file_attributes: flags::FileAttributes, + reparse_tag: u32, +} + +impl FileAttributeTagInformation { + const BASE_SIZE: u32 = U32_SIZE + FILE_ATTR_SIZE; + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_u32::(self.file_attributes.bits())?; + w.write_u32::(self.reparse_tag)?; + Ok(w) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } +} /// 2.1.8 Boolean /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/8ce7b38c-d3cc-415d-ab39-944000ea77ff -#[derive(Debug, ToPrimitive)] +#[derive(Debug, FromPrimitive, ToPrimitive)] #[repr(u8)] -#[allow(dead_code)] enum Boolean { True = 1, False = 0, @@ -1210,11 +2465,10 @@ enum Boolean { /// 2.4.8 FileBothDirectoryInformation /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/270df317-9ba5-4ccb-ba00-8d22be139bc5 -/// Fields are omitted based on those omitted by FreeRDP: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L871 #[derive(Debug)] struct FileBothDirectoryInformation { - // next_entry_offset: u32, - // file_index: u32, + next_entry_offset: u32, + file_index: u32, creation_time: i64, last_access_time: i64, last_write_time: i64, @@ -1223,22 +2477,19 @@ struct FileBothDirectoryInformation { allocation_size: i64, file_attributes: flags::FileAttributes, file_name_length: u32, - // ea_size: u32, - // short_name_length: i8, + ea_size: u32, + short_name_length: i8, // reserved: u8: MUST NOT be added, // see https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L907 - // short_name: String, // 24 bytes + short_name: [u8; 24], // 24 bytes file_name: String, } -#[allow(dead_code)] -/// Base size of the FileBothDirectoryInformation, not accounting for variably sized file_name. -/// Note that file_name's size should be calculated as if it were a Unicode string. -/// 5 u32's (including FileAttributesFlags) + 6 i64's + 1 i8 + 24 bytes -const FILE_BOTH_DIRECTORY_INFORMATION_BASE_SIZE: u32 = (5 * 4) + (6 * 8) + 1 + 24; // 93 - -#[allow(dead_code)] impl FileBothDirectoryInformation { + /// Base size of the FileBothDirectoryInformation, not accounting for variably sized file_name. + /// Note that file_name's size should be calculated as if it were a Unicode string. + const BASE_SIZE: u32 = (4 * U32_SIZE) + FILE_ATTR_SIZE + (6 * I64_SIZE) + I8_SIZE + 24; // 93 + fn new( creation_time: i64, last_access_time: i64, @@ -1248,7 +2499,11 @@ impl FileBothDirectoryInformation { file_attributes: flags::FileAttributes, file_name: String, ) -> Self { + // Default field values taken from + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L871 Self { + next_entry_offset: 0, + file_index: 0, creation_time, last_access_time, last_write_time, @@ -1256,17 +2511,18 @@ impl FileBothDirectoryInformation { end_of_file: file_size, allocation_size: file_size, file_attributes, - file_name_length: u32::try_from(util::to_unicode(&file_name, false).len()).unwrap(), + file_name_length: util::unicode_size(&file_name, false), + ea_size: 0, + short_name_length: 0, + short_name: [0; 24], file_name, } } fn encode(&self) -> RdpResult> { let mut w = vec![]; - // next_entry_offset - w.write_u32::(0)?; - // file_index - w.write_u32::(0)?; + w.write_u32::(self.next_entry_offset)?; + w.write_u32::(self.file_index)?; w.write_i64::(self.creation_time)?; w.write_i64::(self.last_access_time)?; w.write_i64::(self.last_write_time)?; @@ -1275,81 +2531,620 @@ impl FileBothDirectoryInformation { w.write_i64::(self.allocation_size)?; w.write_u32::(self.file_attributes.bits())?; w.write_u32::(self.file_name_length)?; - // ea_size - w.write_u32::(0)?; - // short_name_length - w.write_i8(0)?; + w.write_u32::(self.ea_size)?; + w.write_i8(self.short_name_length)?; // reserved u8, MUST NOT be added! - // short_name - w.extend_from_slice(&[0; 24]); + w.extend_from_slice(&self.short_name); // When working with this field, use file_name_length to determine the length of the file name rather // than assuming the presence of a trailing null delimiter. Dot directory names are valid for this field. w.extend_from_slice(&util::to_unicode(&self.file_name, false)); Ok(w) } + + 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()?, + )) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + self.file_name_length + } +} + +/// 2.4.14 FileFullDirectoryInformation +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/e8d926d1-3a22-4654-be9c-58317a85540b +#[derive(Debug)] +struct FileFullDirectoryInformation { + next_entry_offset: u32, + file_index: u32, + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + end_of_file: i64, + allocation_size: i64, + file_attributes: flags::FileAttributes, + file_name_length: u32, + ea_size: u32, + file_name: String, +} + +impl FileFullDirectoryInformation { + /// Base size of the FileFullDirectoryInformation, not accounting for variably sized file_name. + /// Note that file_name's size should be calculated as if it were a Unicode string. + const BASE_SIZE: u32 = (4 * U32_SIZE) + FILE_ATTR_SIZE + (6 * I64_SIZE); // 68 + + fn new( + creation_time: i64, + last_access_time: i64, + last_write_time: i64, + change_time: i64, + file_size: i64, + file_attributes: flags::FileAttributes, + file_name: String, + ) -> Self { + // Default field values taken from + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L871 + Self { + next_entry_offset: 0, + file_index: 0, + creation_time, + last_access_time, + last_write_time, + change_time, + end_of_file: file_size, + allocation_size: file_size, + file_attributes, + file_name_length: util::unicode_size(&file_name, false), + ea_size: 0, + file_name, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_u32::(self.next_entry_offset)?; + w.write_u32::(self.file_index)?; + w.write_i64::(self.creation_time)?; + w.write_i64::(self.last_access_time)?; + w.write_i64::(self.last_write_time)?; + w.write_i64::(self.change_time)?; + w.write_i64::(self.end_of_file)?; + w.write_i64::(self.allocation_size)?; + w.write_u32::(self.file_attributes.bits())?; + w.write_u32::(self.file_name_length)?; + w.write_u32::(self.ea_size)?; + // When working with this field, use file_name_length to determine the length of the file name rather + // than assuming the presence of a trailing null delimiter. Dot directory names are valid for this field. + w.extend_from_slice(&util::to_unicode(&self.file_name, false)); + Ok(w) + } + + 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(Self::new( + last_modified, + last_modified, + last_modified, + last_modified, + i64::try_from(fso.size)?, + file_attributes, + fso.name()?, + )) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + self.file_name_length + } +} + +// 2.4.13 FileEndOfFileInformation +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/75241cca-3167-472f-8058-a52d77c6bb17 +#[derive(Debug)] +struct FileEndOfFileInformation { + end_of_file: i64, +} + +impl FileEndOfFileInformation { + const BASE_SIZE: u32 = I64_SIZE; + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_i64::(self.end_of_file)?; + Ok(w) + } + + fn decode(payload: &mut Payload) -> RdpResult { + let end_of_file = payload.read_i64::()?; + Ok(Self { end_of_file }) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } +} + +// 2.4.11 FileDispositionInformation +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/12c3dd1c-14f6-4229-9d29-75fb2cb392f6 +#[derive(Debug)] +struct FileDispositionInformation { + delete_pending: u8, +} + +impl FileDispositionInformation { + const BASE_SIZE: u32 = U8_SIZE; + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_u8(self.delete_pending)?; + Ok(w) + } + + fn decode(payload: &mut Payload) -> RdpResult { + let delete_pending = payload.read_u8()?; + Ok(Self { delete_pending }) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } +} + +// 2.4.37 FileRenameInformation +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/1d2673a8-8fb9-4868-920a-775ccaa30cf8 +#[derive(Debug)] +struct FileRenameInformation { + replace_if_exists: Boolean, + file_name: String, +} + +impl FileRenameInformation { + // This matches the FreeRDP implementation rather than Microsoft specification + // see encode method + const BASE_SIZE: u32 = (2 * U8_SIZE) + U32_SIZE; + + fn encode(&self) -> RdpResult> { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L709 + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/3668ae46-1df5-4656-b481-763877428bcb + // This matches the FreeRDP implementation rather than Microsoft specification + let mut w = vec![]; + w.write_u8(Boolean::to_u8(&self.replace_if_exists).unwrap())?; + // RootDirectory. For network operations, this value MUST be zero. + w.write_u8(0)?; + w.write_u32::(self.file_name.len() as u32)?; + w.extend_from_slice(&util::to_unicode(&self.file_name, false)); + Ok(w) + } + + fn decode(payload: &mut Payload) -> RdpResult { + let replace_if_exists = payload.read_u8()?; + // RootDirectory + payload.read_u8()?; + + let file_name_length = payload.read_u32::()?; + let mut file_name = vec![0u8; file_name_length as usize]; + payload.read_exact(&mut file_name)?; + let file_name = util::from_unicode(file_name)?; + + Ok(Self { + replace_if_exists: Boolean::from_u8(replace_if_exists).unwrap(), + file_name, + }) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + self.file_name.len() as u32 + } +} + +// 2.4.4 FileAllocationInformation +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/0201c69b-50db-412d-bab3-dd97aeede13b +#[derive(Debug)] +struct FileAllocationInformation { + allocation_size: i64, +} + +impl FileAllocationInformation { + const BASE_SIZE: u32 = I64_SIZE; + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_i64::(self.allocation_size)?; + Ok(w) + } + + fn decode(payload: &mut Payload) -> RdpResult { + let allocation_size = payload.read_i64::()?; + + Ok(Self { allocation_size }) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } +} + +/// 2.5 File System Information Classes +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ee12042a-9352-46e3-9f67-c094b75fe6c3 +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +#[allow(dead_code)] +enum FileSystemInformationClass { + FileFsVolumeInformation(FileFsVolumeInformation), + FileFsSizeInformation(FileFsSizeInformation), + FileFsAttributeInformation(FileFsAttributeInformation), + FileFsFullSizeInformation(FileFsFullSizeInformation), + FileFsDeviceInformation(FileFsDeviceInformation), +} + +impl FileSystemInformationClass { + fn encode(&self) -> RdpResult> { + match self { + Self::FileFsVolumeInformation(fs_info_class) => fs_info_class.encode(), + Self::FileFsSizeInformation(fs_info_class) => fs_info_class.encode(), + Self::FileFsAttributeInformation(fs_info_class) => fs_info_class.encode(), + Self::FileFsFullSizeInformation(fs_info_class) => fs_info_class.encode(), + Self::FileFsDeviceInformation(fs_info_class) => fs_info_class.encode(), + } + } +} + +/// 2.5.9 FileFsVolumeInformation +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/bf691378-c34e-4a13-976e-404ea1a87738 +#[derive(Debug)] +struct FileFsVolumeInformation { + volume_creation_time: i64, + volume_serial_number: u32, + volume_label_length: u32, + supports_objects: Boolean, + // reserved is omitted + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L495 + volume_label: String, +} + +impl FileFsVolumeInformation { + /// Base size of the FileFsVolumeInformation, not accounting for variably sized volume_label. + /// 1 i64, 2 u32, 1 Boolean + const BASE_SIZE: u32 = I64_SIZE + (2 * U32_SIZE) + BOOL_SIZE; // 17 + + fn new(volume_creation_time: i64) -> Self { + // volume_label can just be something we make up + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L446 + let volume_label = "TELEPORT".to_string(); + + Self { + volume_creation_time, + // Not sure why the `& 0xffff` is necessary, just copying FreeRDP + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L492 + // u32::MAX is given due to the fact that FreeRDP uses it as a fallback: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/winpr/libwinpr/file/file.c#L1018-L1021 + volume_serial_number: u32::MAX & 0xffff, + // The FreeRDP function they use to convert the volume_label to unicode is null-terminated + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/winpr/libwinpr/crt/unicode.c#L371 + volume_label_length: util::unicode_size(&volume_label, true), + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L494 + supports_objects: Boolean::False, + volume_label, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_i64::(self.volume_creation_time)?; + w.write_u32::(self.volume_serial_number)?; + w.write_u32::(self.volume_label_length)?; + w.write_u8(Boolean::to_u8(&self.supports_objects).unwrap())?; + w.extend_from_slice(&util::to_unicode(&self.volume_label, true)); + Ok(w) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + self.volume_label_length + } +} + +/// 2.5.8 FileFsSizeInformation +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/e13e068c-e3a7-4dd4-94fd-3892b492e6e7 +#[derive(Debug)] +struct FileFsSizeInformation { + total_alloc_units: i64, + available_alloc_units: i64, + sectors_per_alloc_unit: u32, + bytes_per_sector: u32, +} + +#[allow(dead_code)] +impl FileFsSizeInformation { + const BASE_SIZE: u32 = (2 * I64_SIZE) + (2 * U32_SIZE); + + fn new() -> Self { + // Fill these out with the default fallback values FreeRDP uses + // Written here: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L510-L513 + // With default fallback values ultimately found here: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/winpr/libwinpr/file/file.c#L1018-L1021 + Self { + total_alloc_units: u32::MAX as i64, + available_alloc_units: u32::MAX as i64, + sectors_per_alloc_unit: u32::MAX, + bytes_per_sector: 1, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_i64::(self.total_alloc_units)?; + w.write_i64::(self.available_alloc_units)?; + w.write_u32::(self.sectors_per_alloc_unit)?; + w.write_u32::(self.bytes_per_sector)?; + Ok(w) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } +} + +/// 2.5.1 FileFsAttributeInformation +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ebc7e6e5-4650-4e54-b17c-cf60f6fbeeaa +#[derive(Debug)] +struct FileFsAttributeInformation { + file_system_attributes: flags::FileSystemAttributes, + max_component_name_len: u32, + file_system_name_len: u32, + file_system_name: String, +} + +impl FileFsAttributeInformation { + const BASE_SIZE: u32 = (2 * U32_SIZE) + FILE_ATTR_SIZE; + + fn new() -> Self { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L447 + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L519 + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L538 + let file_system_name = "FAT32".to_string(); + + Self { + file_system_attributes: flags::FileSystemAttributes::FILE_CASE_SENSITIVE_SEARCH + | flags::FileSystemAttributes::FILE_CASE_PRESERVED_NAMES + | flags::FileSystemAttributes::FILE_UNICODE_ON_DISK, + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L536 + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/winpr/include/winpr/file.h#L36 + max_component_name_len: 260, + // The FreeRDP function they use to convert the file_system_name to unicode is null-terminated + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L519 + file_system_name_len: util::unicode_size(&file_system_name, true), + file_system_name, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_u32::(self.file_system_attributes.bits())?; + w.write_u32::(self.max_component_name_len)?; + w.write_u32::(self.file_system_name_len)?; + w.extend_from_slice(&util::to_unicode(&self.file_system_name, true)); + Ok(w) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + self.file_system_name_len + } +} + +/// 2.5.4 FileFsFullSizeInformation +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/63768db7-9012-4209-8cca-00781e7322f5 +#[derive(Debug)] +struct FileFsFullSizeInformation { + total_alloc_units: i64, + caller_available_alloc_units: i64, + actual_available_alloc_units: i64, + sectors_per_alloc_unit: u32, + bytes_per_sector: u32, +} + +#[allow(dead_code)] +impl FileFsFullSizeInformation { + const BASE_SIZE: u32 = (3 * I64_SIZE) + (2 * U32_SIZE); + + fn new() -> Self { + // Fill these out with the default fallback values FreeRDP uses + // Written here: https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L552-L557 + // With default fallback values ultimately found here: + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/winpr/libwinpr/file/file.c#L1018-L1021 + Self { + total_alloc_units: u32::MAX as i64, + caller_available_alloc_units: u32::MAX as i64, + actual_available_alloc_units: u32::MAX as i64, + sectors_per_alloc_unit: u32::MAX, + bytes_per_sector: 1, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_i64::(self.total_alloc_units)?; + w.write_i64::(self.caller_available_alloc_units)?; + w.write_i64::(self.actual_available_alloc_units)?; + w.write_u32::(self.sectors_per_alloc_unit)?; + w.write_u32::(self.bytes_per_sector)?; + Ok(w) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } +} + +/// 2.5.10 FileFsDeviceInformation +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/616b66d5-b335-4e1c-8f87-b4a55e8d3e4a +// Taking a shortcut by ignoring the bitflag typing here, since we will only ever fill this out with single values: +// https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L570-L571 +#[derive(Debug)] +struct FileFsDeviceInformation { + device_type: u32, + characteristics: u32, +} + +#[allow(dead_code)] +impl FileFsDeviceInformation { + const BASE_SIZE: u32 = 2 * U32_SIZE; + + fn new() -> Self { + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L570-L571 + Self { + device_type: 0x00000007, // FILE_DEVICE_DISK + characteristics: 0, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.write_u32::(self.device_type)?; + w.write_u32::(self.characteristics)?; + Ok(w) + } + + fn size(&self) -> u32 { + Self::BASE_SIZE + } } /// 2.2.3.4.8 Client Drive Query Information Response (DR_DRIVE_QUERY_INFORMATION_RSP) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/37ef4fb1-6a95-4200-9fbf-515464f034a4 #[derive(Debug)] -#[allow(dead_code)] - struct ClientDriveQueryInformationResponse { device_io_response: DeviceIoResponse, - length: u32, - buffer: FsInformationClass, + length: Option, + buffer: Option, } -#[allow(dead_code)] impl ClientDriveQueryInformationResponse { /// Constructs a ClientDriveQueryInformationResponse from a ServerDriveQueryInformationRequest and an NTSTATUS. - /// If the ServerDriveQueryInformationRequest.fs_information_class_lvl is currently unsupported, the program will panic. - /// TODO(isaiah): We will pass some sort of file structure into here. - fn new(req: &ServerDriveQueryInformationRequest, io_status: NTSTATUS) -> RdpResult { - let (length, buffer) = match req.fs_information_class_lvl { - FsInformationClassLevel::FileBasicInformation => ( - FILE_BASIC_INFORMATION_SIZE, - FsInformationClass::FileBasicInformation(FileBasicInformation { - creation_time: 1, - last_access_time: 2, - last_write_time: 3, - change_time: 4, - file_attributes: flags::FileAttributes::FILE_ATTRIBUTE_DIRECTORY, - }), - ), - FsInformationClassLevel::FileStandardInformation => ( - FILE_STANDARD_INFORMATION_SIZE, - FsInformationClass::FileStandardInformation(FileStandardInformation { - allocation_size: 0, - end_of_file: 0, - number_of_links: 0, - delete_pending: Boolean::False, - directory: Boolean::True, - }), - ), - _ => { - return Err(not_implemented_error(&format!( - "received unsupported NTSTATUS: {:?}", - io_status - ))) - } - }; + fn new( + req: &ServerDriveQueryInformationRequest, + file: Option<&FileCacheObject>, + io_status: NTSTATUS, + ) -> RdpResult { + // If io_status == NTSTATUS::STATUS_UNSUCCESSFUL, we can just fill out the + // device_io_response and don't need to create/encode the rest. + if io_status == NTSTATUS::STATUS_UNSUCCESSFUL { + return Ok(Self { + device_io_response: DeviceIoResponse::new( + &req.device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length: None, + buffer: None, + }); + } - Ok(Self { - device_io_response: DeviceIoResponse::new( - &req.device_io_request, - NTSTATUS::to_u32(&io_status).unwrap(), - ), - length, - buffer, - }) + if let Some(file) = file { + // We support all the FsInformationClasses that FreeRDP does here + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L482 + let (length, buffer) = match req.file_info_class_lvl { + FileInformationClassLevel::FileBasicInformation => ( + Some(FileBasicInformation::BASE_SIZE), + Some(FileInformationClass::FileBasicInformation( + FileBasicInformation { + creation_time: to_windows_time(file.fso.last_modified), + last_access_time: to_windows_time(file.fso.last_modified), + last_write_time: to_windows_time(file.fso.last_modified), + change_time: to_windows_time(file.fso.last_modified), + file_attributes: if file.fso.file_type == FileType::File { + flags::FileAttributes::FILE_ATTRIBUTE_NORMAL + } else { + flags::FileAttributes::FILE_ATTRIBUTE_DIRECTORY + }, + }, + )), + ), + FileInformationClassLevel::FileStandardInformation => ( + Some(FileStandardInformation::BASE_SIZE), + Some(FileInformationClass::FileStandardInformation( + FileStandardInformation { + allocation_size: file.fso.size as i64, + end_of_file: file.fso.size as i64, + number_of_links: 0, + delete_pending: if file.delete_pending { + Boolean::True + } else { + Boolean::False + }, + directory: if file.fso.file_type == FileType::File { + Boolean::False + } else { + Boolean::True + }, + }, + )), + ), + FileInformationClassLevel::FileAttributeTagInformation => ( + Some(FileAttributeTagInformation::BASE_SIZE), + Some(FileInformationClass::FileAttributeTagInformation( + FileAttributeTagInformation { + file_attributes: if file.fso.file_type == FileType::File { + flags::FileAttributes::FILE_ATTRIBUTE_NORMAL + } else { + flags::FileAttributes::FILE_ATTRIBUTE_DIRECTORY + }, + reparse_tag: 0, + }, + )), + ), + _ => { + return Err(not_implemented_error(&format!( + "received unsupported FileInformationClass: {:?}", + req.file_info_class_lvl + ))) + } + }; + + Ok(Self { + device_io_response: DeviceIoResponse::new( + &req.device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length, + buffer, + }) + } else { + Err(try_error( + "if io_status != NTSTATUS::STATUS_UNSUCCESSFUL a &FileCacheObject must be provided", + )) + } } fn encode(&self) -> RdpResult> { let mut w = vec![]; w.extend_from_slice(&self.device_io_response.encode()?); - w.write_u32::(self.length)?; - w.extend_from_slice(&self.buffer.encode()?); + if let Some(length) = self.length { + w.write_u32::(length)?; + } + if let Some(buffer) = &self.buffer { + w.extend_from_slice(&buffer.encode()?); + } Ok(w) } } @@ -1357,13 +3152,11 @@ impl ClientDriveQueryInformationResponse { /// 2.2.1.4.2 Device Close Request (DR_CLOSE_REQ) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/3ec6627f-9e0f-4941-a828-3fc6ed63d9e7 #[derive(Debug)] -#[allow(dead_code)] struct DeviceCloseRequest { device_io_request: DeviceIoRequest, // Padding (32 bytes): An array of 32 bytes. Reserved. This field can be set to any value, and MUST be ignored. } -#[allow(dead_code)] impl DeviceCloseRequest { fn decode(device_io_request: DeviceIoRequest) -> Self { Self { device_io_request } @@ -1373,14 +3166,12 @@ impl DeviceCloseRequest { /// 2.2.1.5.2 Device Close Response (DR_CLOSE_RSP) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/0dae7031-cfd8-4f14-908c-ec06e14997b5 #[derive(Debug)] -#[allow(dead_code)] struct DeviceCloseResponse { /// The CompletionId field of this header MUST match a Device I/O Request (section 2.2.1.4) message that had the MajorFunction field set to IRP_MJ_CLOSE. device_io_response: DeviceIoResponse, /// This field can be set to any value and MUST be ignored. padding: u32, } -#[allow(dead_code)] impl DeviceCloseResponse { fn new(device_close_request: DeviceCloseRequest, io_status: NTSTATUS) -> Self { Self { @@ -1435,19 +3226,17 @@ impl ServerDriveNotifyChangeDirectoryRequest { /// 2.2.1.4.3 Device Read Request (DR_READ_REQ) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/3192516d-36a6-47c5-987a-55c214aa0441 -#[derive(Debug)] -#[allow(dead_code)] -struct DeviceReadRequest { +#[derive(Debug, Clone)] +pub struct DeviceReadRequest { /// The MajorFunction field in this header MUST be set to IRP_MJ_READ. - device_io_request: DeviceIoRequest, + pub device_io_request: DeviceIoRequest, /// This field specifies the maximum number of bytes to be read from the device. - length: u32, + pub length: u32, /// This field specifies the file offset where the read operation is performed. - offset: u64, + pub offset: u64, // Padding (20 bytes): An array of 20 bytes. Reserved. This field can be set to any value and MUST be ignored. } -#[allow(dead_code)] impl DeviceReadRequest { fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { Ok(Self { @@ -1461,7 +3250,6 @@ impl DeviceReadRequest { /// 2.2.1.5.3 Device Read Response (DR_READ_RSP) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/d35d3f91-fc5b-492b-80be-47f483ad1dc9 #[derive(Debug)] -#[allow(dead_code)] struct DeviceReadResponse { /// The CompletionId field of this header MUST match a Device I/O Request (section 2.2.1.4) message that had the MajorFunction field set to IRP_MJ_READ. device_io_reply: DeviceIoResponse, @@ -1471,7 +3259,6 @@ struct DeviceReadResponse { read_data: Vec, } -#[allow(dead_code)] impl DeviceReadResponse { fn new( device_read_request: &DeviceReadRequest, @@ -1499,6 +3286,149 @@ impl DeviceReadResponse { } } +/// 2.2.1.4.4 Device Write Request (DR_WRITE_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/2e25f0aa-a4ce-4ff3-ad62-ab6098280a3a +#[derive(Debug, Clone)] +pub struct DeviceWriteRequest { + /// The MajorFunction field in this header MUST be set to IRP_MJ_WRITE. + pub device_io_request: DeviceIoRequest, + /// Number of bytes in the write_data field. + pub length: u32, + /// File offset at which the data must be written. + pub offset: u64, + /// Data to be written on the target device. + pub write_data: Vec, +} + +impl DeviceWriteRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + let length = payload.read_u32::()?; + let offset = payload.read_u64::()?; + + // There is a padding of 20 bytes between offset and write data so we + // must ignore it + payload.seek(SeekFrom::Current(20))?; + + let mut write_data = vec![0; length as usize]; + payload.read_exact(&mut write_data)?; + + Ok(Self { + device_io_request, + length, + offset, + write_data, + }) + } +} + +/// 2.2.1.5.4 Device Write Response (DR_WRITE_RSP) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/58160a47-2379-4c4a-a99d-24a1a666c02a +#[derive(Debug)] +pub struct DeviceWriteResponse { + /// The CompletionId field of this header MUST match a Device I/O Request (section 2.2.1.4) message that had the MajorFunction field set to IRP_MJ_WRITE. + device_io_reply: DeviceIoResponse, + /// Number of bytes written in response to the write request. + length: u32, +} + +impl DeviceWriteResponse { + fn new(device_io_request: &DeviceIoRequest, io_status: NTSTATUS, length: u32) -> Self { + Self { + device_io_reply: DeviceIoResponse::new( + device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_reply.encode()?); + w.write_u32::(self.length)?; + // 1 byte padding + w.write_u32::(0)?; + Ok(w) + } +} + +/// 2.2.3.4.9 Client Drive Set Information Response (DR_DRIVE_SET_INFORMATION_RSP) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/16b893d5-5d8b-49d1-8dcb-ee21e7612970 +#[derive(Debug)] +struct ClientDriveSetInformationResponse { + device_io_reply: DeviceIoResponse, + /// This field MUST be equal to the Length field in the Server Drive Set Information Request (section 2.2.3.3.9). + length: u32, +} + +impl ClientDriveSetInformationResponse { + fn new(req: &ServerDriveSetInformationRequest, io_status: NTSTATUS) -> Self { + Self { + device_io_reply: DeviceIoResponse::new( + &req.device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length: req.set_buffer.size() as u32, + } + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_reply.encode()?); + w.write_u32::(self.length)?; + // 1 byte padding + w.write_u32::(0)?; + Ok(w) + } +} + +/// 2.2.3.3.9 Server Drive Set Information Request (DR_DRIVE_SET_INFORMATION_REQ) +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/b5d3104b-0e42-4cf8-9059-e9fe86615e5c +#[derive(Debug)] +struct ServerDriveSetInformationRequest { + /// The MajorFunction field in the DR_DEVICE_IOREQUEST header MUST be set to IRP_MJ_SET_INFORMATION. + device_io_request: DeviceIoRequest, + file_information_class_level: FileInformationClassLevel, + set_buffer: FileInformationClass, +} + +impl ServerDriveSetInformationRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + let file_information_class_level = + FileInformationClassLevel::from_u32(payload.read_u32::()?) + .ok_or_else(|| invalid_data_error("failed to read FileInformationClassLevel"))?; + + match file_information_class_level { + FileInformationClassLevel::FileBasicInformation + | FileInformationClassLevel::FileEndOfFileInformation + | FileInformationClassLevel::FileDispositionInformation + | FileInformationClassLevel::FileRenameInformation + | FileInformationClassLevel::FileAllocationInformation => {} + _ => { + return Err(invalid_data_error(&format!( + "read invalid FileInformationClassLevel: {:?}", + file_information_class_level + ))) + } + }; + + // length, u32 + payload.seek(SeekFrom::Current(4))?; + + // There is a padding of 24 bytes between offset and write data so we + // must ignore it + payload.seek(SeekFrom::Current(24))?; + + let set_buffer = FileInformationClass::decode(&file_information_class_level, payload)?; + + Ok(Self { + device_io_request, + file_information_class_level, + set_buffer, + }) + } +} + /// 2.2.3.3.10 Server Drive Query Directory Request (DR_DRIVE_QUERY_DIRECTORY_REQ) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/458019d2-5d5a-4fd4-92ef-8c05f8d7acb1 #[derive(Debug)] @@ -1508,7 +3438,7 @@ struct ServerDriveQueryDirectoryRequest { /// and the MinorFunction field MUST be set to IRP_MN_QUERY_DIRECTORY. device_io_request: DeviceIoRequest, /// Must contain one of FileDirectoryInformation, FileFullDirectoryInformation, FileBothDirectoryInformation, FileNamesInformation - fs_information_class_lvl: FsInformationClassLevel, + file_info_class_lvl: FileInformationClassLevel, /// If the value of this field is zero, the request is for the next file in the directory that was specified in a previous /// Server Drive Query Directory Request. If such a file does not exist, the client MUST complete this request with STATUS_NO_MORE_FILES /// in the IoStatus field of the Client Drive I/O Response packet (section 2.2.3.4). If the value of this field is non-zero and such a @@ -1517,42 +3447,43 @@ struct ServerDriveQueryDirectoryRequest { /// Specifies the number of bytes in the Path field, including the null-terminator. path_length: u32, // Padding (23 bytes): An array of 23 bytes. This field is unused and MUST be ignored. + padding: [u8; 23], /// A variable-length array of Unicode characters (we will store this as a regular rust String) that specifies the directory /// on which this operation will be performed. The Path field MUST be null-terminated. If the value of the InitialQuery field /// is zero, then the contents of the Path field MUST be ignored, irrespective of the value specified in the PathLength field. path: String, } -#[allow(dead_code)] impl ServerDriveQueryDirectoryRequest { fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { - let fs_information_class_lvl = - FsInformationClassLevel::from_u32(payload.read_u32::()?) - .ok_or_else(|| invalid_data_error("failed to read FsInformationClassLevel"))?; - if fs_information_class_lvl != FsInformationClassLevel::FileDirectoryInformation - && fs_information_class_lvl != FsInformationClassLevel::FileFullDirectoryInformation - && fs_information_class_lvl != FsInformationClassLevel::FileBothDirectoryInformation - && fs_information_class_lvl != FsInformationClassLevel::FileNamesInformation - { + let file_info_class_lvl = + FileInformationClassLevel::from_u32(payload.read_u32::()?) + .ok_or_else(|| invalid_data_error("failed to read FileInformationClassLevel"))?; + + // These are all the FileInformationClass's supported for this message by FreeRDP + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L794 + static VALID_LEVELS: [FileInformationClassLevel; 4] = [ + FileInformationClassLevel::FileDirectoryInformation, + FileInformationClassLevel::FileFullDirectoryInformation, + FileInformationClassLevel::FileBothDirectoryInformation, + FileInformationClassLevel::FileNamesInformation, + ]; + + if !VALID_LEVELS.contains(&file_info_class_lvl) { return Err(invalid_data_error(&format!( - "read invalid FsInformationClassLevel: {:?}, expected one of {:?}", - fs_information_class_lvl, - vec![ - FsInformationClassLevel::FileDirectoryInformation, - FsInformationClassLevel::FileFullDirectoryInformation, - FsInformationClassLevel::FileBothDirectoryInformation, - FsInformationClassLevel::FileNamesInformation - ] + "read invalid FileInformationClassLevel: {:?}, expected one of {:?}", + file_info_class_lvl, VALID_LEVELS, ))); } + let initial_query = payload.read_u8()?; let mut path_length: u32 = 0; let mut path = String::from(""); + let mut padding: [u8; 23] = [0; 23]; if initial_query != 0 { path_length = payload.read_u32::()?; // TODO(isaiah): make a payload.skip(n) - let mut padding: [u8; 23] = [0; 23]; payload.read_exact(&mut padding)?; // TODO(isaiah): make a from_unicode_exact @@ -1563,9 +3494,10 @@ impl ServerDriveQueryDirectoryRequest { Ok(Self { device_io_request, - fs_information_class_lvl, + file_info_class_lvl, initial_query, path_length, + padding, path, }) } @@ -1574,35 +3506,64 @@ impl ServerDriveQueryDirectoryRequest { /// 2.2.3.4.10 Client Drive Query Directory Response (DR_DRIVE_QUERY_DIRECTORY_RSP) /// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/9c929407-a833-4893-8f20-90c984756140 #[derive(Debug)] -#[allow(dead_code)] struct ClientDriveQueryDirectoryResponse { /// The CompletionId field of the DR_DEVICE_IOCOMPLETION header MUST match a Device I/O Request (section 2.2.1.4) that /// has the MajorFunction field set to IRP_MJ_DIRECTORY_CONTROL and the MinorFunction field set to IRP_MN_QUERY_DIRECTORY. device_io_reply: DeviceIoResponse, /// Specifies the number of bytes in the Buffer field. length: u32, - /// The content of this field is based on the value of the FsInformationClass field in the Server Drive Query Directory Request + /// The content of this field is based on the value of the FileInformationClass field in the Server Drive Query Directory Request /// message, which determines the different structures that MUST be contained in the Buffer field. - buffer: Option, + buffer: Option, // 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, + buffer: Option, ) -> RdpResult { - let device_io_request = &req.device_io_request; + // This match block ensures that the passed parameters are in a configuration that's + // explicitly supported by the length calculation (below) and the self.encode() method. + match io_status { + NTSTATUS::STATUS_SUCCESS => { + if buffer.is_none() { + return Err(invalid_data_error( + "a ClientDriveQueryDirectoryResponse with NTSTATUS::STATUS_SUCCESS \ + should have Some(FileInformationClass) buffer, got None", + )); + } + } + NTSTATUS::STATUS_NOT_SUPPORTED + | NTSTATUS::STATUS_NO_MORE_FILES + | NTSTATUS::STATUS_UNSUCCESSFUL => { + if buffer.is_some() { + return Err(invalid_data_error(&format!( + "a ClientDriveQueryDirectoryResponse with NTSTATUS = {:?} \ + should have a None buffer, got {:?}", + io_status, buffer, + ))); + } + } + _ => { + return Err(invalid_data_error(&format!( + "received unsupported io_status for ClientDriveQueryDirectoryResponse: {:?}", + io_status + ))) + } + } + let length = match buffer { Some(ref fs_information_class) => match fs_information_class { - FsInformationClass::FileBothDirectoryInformation( - file_both_directory_information, - ) => { - FILE_BOTH_DIRECTORY_INFORMATION_BASE_SIZE - + file_both_directory_information.file_name_length + FileInformationClass::FileBothDirectoryInformation(fs_info_class) => { + fs_info_class.size() } + FileInformationClass::FileFullDirectoryInformation(fs_info_class) => { + fs_info_class.size() + } + // TODO(isaiah): add support for FileDirectoryInformation and FileNamesInformation + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L794 _ => { return Err(not_implemented_error(&format!("ClientDriveQueryDirectoryResponse not implemented for fs_information_class {:?}", fs_information_class))); } @@ -1623,33 +3584,184 @@ impl ClientDriveQueryDirectoryResponse { fn encode(&self) -> RdpResult> { let mut w = vec![]; w.extend_from_slice(&self.device_io_reply.encode()?); - - if self.device_io_reply.io_status == NTSTATUS::to_u32(&NTSTATUS::STATUS_SUCCESS).unwrap() { - w.write_u32::(self.length)?; - w.extend_from_slice( - &self - .buffer.as_ref() - .ok_or_else(|| invalid_data_error( - "ClientDriveQueryDirectoryResponse with NTSTATUS::STATUS_SUCCESS expects a FsInformationClass" - ))? - .encode()?, - ); - } else if self.device_io_reply.io_status + w.write_u32::(self.length)?; + if let Some(buffer) = &self.buffer { + w.extend_from_slice(&buffer.encode()?); + } + if self.device_io_reply.io_status == NTSTATUS::to_u32(&NTSTATUS::STATUS_NO_MORE_FILES).unwrap() { - // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L935-L937 - w.write_u32::(0)?; + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_file.c#L937 w.write_u8(0)?; - } else { - return Err(invalid_data_error(&format!( - "Found ClientDriveQueryDirectoryResponse with invalid or unhandled NTSTATUS: {:?}", - self.device_io_reply.io_status - ))); } Ok(w) } } +/// 2.2.3.3.6 Server Drive Query Volume Information Request +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpefs/484e622d-0e2b-423c-8461-7de38878effb +/// +/// We only need to read the buffer up to the FileInformationClass to get the job done, so the rest of the fields in +/// this structure are omitted. See FreeRDP: +/// https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L464 +#[derive(Debug)] +struct ServerDriveQueryVolumeInformationRequest { + device_io_request: DeviceIoRequest, + fs_info_class_lvl: FileSystemInformationClassLevel, +} + +impl ServerDriveQueryVolumeInformationRequest { + fn decode(device_io_request: DeviceIoRequest, payload: &mut Payload) -> RdpResult { + let fs_info_class_lvl = + FileSystemInformationClassLevel::from_u32(payload.read_u32::()?) + .ok_or_else(|| { + invalid_data_error("failed to read FileSystemInformationClassLevel") + })?; + + // These are all the FileInformationClass's supported for this message by FreeRDP + // https://github.com/FreeRDP/FreeRDP/blob/511444a65e7aa2f537c5e531fa68157a50c1bd4d/channels/drive/client/drive_main.c#L468 + static VALID_LEVELS: [FileSystemInformationClassLevel; 5] = [ + FileSystemInformationClassLevel::FileFsVolumeInformation, + FileSystemInformationClassLevel::FileFsSizeInformation, + FileSystemInformationClassLevel::FileFsAttributeInformation, + FileSystemInformationClassLevel::FileFsFullSizeInformation, + FileSystemInformationClassLevel::FileFsDeviceInformation, + ]; + + if !VALID_LEVELS.contains(&fs_info_class_lvl) { + return Err(invalid_data_error(&format!( + "read invalid FileInformationClassLevel: {:?}, expected one of {:?}", + fs_info_class_lvl, VALID_LEVELS, + ))); + } + + Ok(Self { + device_io_request, + fs_info_class_lvl, + }) + } +} + +/// 2.2.3.4.6 Client Drive Query Volume Information Response +#[derive(Debug)] +struct ClientDriveQueryVolumeInformationResponse { + device_io_reply: DeviceIoResponse, + /// Specifies the number of bytes in the Buffer field. + length: u32, + /// The content of this field is based on the value of the FileInformationClass field in the Server Drive Query Volume Information Request message, + /// which determines the different structures that MUST be contained in the Buffer field. + buffer: Option, +} + +impl ClientDriveQueryVolumeInformationResponse { + fn new( + device_io_request: &DeviceIoRequest, + io_status: NTSTATUS, + buffer: Option, + ) -> RdpResult { + match io_status { + NTSTATUS::STATUS_SUCCESS => { + if buffer.is_none() { + return Err(invalid_data_error( + "a ClientDriveQueryVolumeInformationResponse with NTSTATUS::STATUS_SUCCESS \ + should have Some(FileInformationClass) buffer, got None", + )); + } + } + NTSTATUS::STATUS_UNSUCCESSFUL => { + if buffer.is_some() { + return Err(invalid_data_error(&format!( + "a ClientDriveQueryVolumeInformationResponse with NTSTATUS = {:?} \ + should have a None buffer, got {:?}", + io_status, buffer, + ))); + } + } + _ => { + return Err(invalid_data_error(&format!( + "received unsupported io_status for ClientDriveQueryVolumeInformationResponse: {:?}", + io_status + ))) + } + } + + let length = match buffer { + Some(ref buf) => match buf { + FileSystemInformationClass::FileFsVolumeInformation(f) => f.size(), + FileSystemInformationClass::FileFsSizeInformation(f) => f.size(), + FileSystemInformationClass::FileFsAttributeInformation(f) => f.size(), + FileSystemInformationClass::FileFsFullSizeInformation(f) => f.size(), + FileSystemInformationClass::FileFsDeviceInformation(f) => f.size(), + }, + None => 0, + }; + + Ok(Self { + device_io_reply: DeviceIoResponse::new( + device_io_request, + NTSTATUS::to_u32(&io_status).unwrap(), + ), + length, + buffer, + }) + } + + fn encode(&self) -> RdpResult> { + let mut w = vec![]; + w.extend_from_slice(&self.device_io_reply.encode()?); + w.write_u32::(self.length)?; + if let Some(buffer) = &self.buffer { + w.extend_from_slice(&buffer.encode()?); + } + + Ok(w) + } +} + +/// TDP handles time in milliseconds since the UNIX epoch (https://en.wikipedia.org/wiki/Unix_time), +/// whereas Windows prefers 64-bit signed integers representing the number of 100-nanosecond intervals +/// that have elapsed since January 1, 1601, Coordinated Universal Time (UTC) +/// (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/a69cc039-d288-4673-9598-772b6083f8bf). +fn to_windows_time(tdp_time_ms: u64) -> i64 { + // https://stackoverflow.com/a/5471380/6277051 + // https://docs.microsoft.com/en-us/windows/win32/sysinfo/converting-a-time-t-value-to-a-file-time + let tdp_time_sec = tdp_time_ms / 1000; + ((tdp_time_sec * 10000000) + 116444736000000000) as i64 +} + +type SharedDirectoryAcknowledgeSender = Box RdpResult<()>>; +type SharedDirectoryInfoRequestSender = Box RdpResult<()>>; +type SharedDirectoryCreateRequestSender = + Box RdpResult<()>>; +type SharedDirectoryDeleteRequestSender = + Box RdpResult<()>>; +type SharedDirectoryListRequestSender = Box RdpResult<()>>; +type SharedDirectoryReadRequestSender = Box RdpResult<()>>; +type SharedDirectoryWriteRequestSender = Box RdpResult<()>>; + type SharedDirectoryInfoResponseHandler = Box RdpResult>>>; +type SharedDirectoryCreateResponseHandler = + Box RdpResult>>>; +type SharedDirectoryDeleteResponseHandler = + Box RdpResult>>>; +type SharedDirectoryListResponseHandler = + Box RdpResult>>>; +type SharedDirectoryReadResponseHandler = + Box RdpResult>>>; +type SharedDirectoryWriteResponseHandler = + Box RdpResult>>>; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_windows_time() { + // Cross checked against + // https://www.silisoftware.com/tools/date.php?inputdate=1655246166&inputformat=unix + assert_eq!(to_windows_time(1655246166 * 1000), 132997197660000000); + assert_eq!(to_windows_time(1000), 116444736010000000); + } +} diff --git a/lib/srv/desktop/rdp/rdpclient/src/util.rs b/lib/srv/desktop/rdp/rdpclient/src/util.rs index 6bb326d15da..1ef2d87999c 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/util.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/util.rs @@ -14,6 +14,7 @@ use crate::errors::invalid_data_error; use rdp::model::error::RdpResult; +use std::convert::TryFrom; use utf16string::{WString, LE}; /// According to [MS-RDPEFS] 1.1 Glossary: @@ -48,6 +49,11 @@ pub fn to_utf8(s: &str) -> Vec { format!("{}\x00", s).into_bytes() } +/// Takes a Rust string slice and calculates it's unicode size in bytes. +pub fn unicode_size(s: &str, with_null_term: bool) -> u32 { + u32::try_from(to_unicode(s, with_null_term).len()).unwrap() +} + #[cfg(test)] mod tests { use super::*; diff --git a/lib/srv/desktop/tdp/proto.go b/lib/srv/desktop/tdp/proto.go index ef623a3bd60..c37b09bcaf8 100644 --- a/lib/srv/desktop/tdp/proto.go +++ b/lib/srv/desktop/tdp/proto.go @@ -45,20 +45,30 @@ 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) + 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) + TypeSharedDirectoryListRequest = MessageType(25) + TypeSharedDirectoryListResponse = MessageType(26) ) // Message is a Go representation of a desktop protocol message. @@ -119,6 +129,26 @@ func decode(in peekReader) (Message, error) { return decodeSharedDirectoryInfoRequest(in) case TypeSharedDirectoryInfoResponse: return decodeSharedDirectoryInfoResponse(in) + case TypeSharedDirectoryCreateRequest: + return decodeSharedDirectoryCreateRequest(in) + case TypeSharedDirectoryCreateResponse: + return decodeSharedDirectoryCreateResponse(in) + case TypeSharedDirectoryDeleteRequest: + return decodeSharedDirectoryDeleteRequest(in) + case TypeSharedDirectoryDeleteResponse: + return decodeSharedDirectoryDeleteResponse(in) + case TypeSharedDirectoryListRequest: + return decodeSharedDirectoryListRequest(in) + case TypeSharedDirectoryListResponse: + return decodeSharedDirectoryListResponse(in) + case TypeSharedDirectoryReadRequest: + return decodeSharedDirectoryReadRequest(in) + case TypeSharedDirectoryReadResponse: + return decodeSharedDirectoryReadResponse(in) + case TypeSharedDirectoryWriteRequest: + return decodeSharedDirectoryWriteRequest(in) + case TypeSharedDirectoryWriteResponse: + return decodeSharedDirectoryWriteResponse(in) default: return nil, trace.BadParameter("unsupported desktop protocol message type %d", t) } @@ -821,6 +851,525 @@ func decodeFileSystemObject(in peekReader) (FileSystemObject, error) { }, nil } +type SharedDirectoryCreateRequest struct { + CompletionID uint32 + DirectoryID uint32 + FileType uint32 + Path string +} + +func (s SharedDirectoryCreateRequest) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryCreateRequest)) + binary.Write(buf, binary.BigEndian, s.CompletionID) + binary.Write(buf, binary.BigEndian, s.DirectoryID) + binary.Write(buf, binary.BigEndian, s.FileType) + if err := encodeString(buf, s.Path); err != nil { + return nil, trace.Wrap(err) + } + + return buf.Bytes(), nil +} + +func decodeSharedDirectoryCreateRequest(in peekReader) (SharedDirectoryCreateRequest, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryCreateRequest{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryCreateRequest) { + return SharedDirectoryCreateRequest{}, trace.BadParameter("got message type %v, expected SharedDirectoryCreateRequest(%v)", t, TypeSharedDirectoryCreateRequest) + } + var completionID, directoryID, fileType uint32 + err = binary.Read(in, binary.BigEndian, &completionID) + if err != nil { + return SharedDirectoryCreateRequest{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &directoryID) + if err != nil { + return SharedDirectoryCreateRequest{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &fileType) + if err != nil { + return SharedDirectoryCreateRequest{}, trace.Wrap(err) + } + path, err := decodeString(in, tdpMaxPathLength) + if err != nil { + return SharedDirectoryCreateRequest{}, trace.Wrap(err) + } + + return SharedDirectoryCreateRequest{ + CompletionID: completionID, + DirectoryID: directoryID, + FileType: fileType, + Path: path, + }, nil + +} + +type SharedDirectoryCreateResponse struct { + CompletionID uint32 + ErrCode uint32 +} + +func (s SharedDirectoryCreateResponse) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryCreateResponse)) + binary.Write(buf, binary.BigEndian, s) + return buf.Bytes(), nil +} + +func decodeSharedDirectoryCreateResponse(in peekReader) (SharedDirectoryCreateResponse, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryCreateResponse{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryCreateRequest) { + return SharedDirectoryCreateResponse{}, trace.BadParameter("got message type %v, expected SharedDirectoryCreateResponse(%v)", t, TypeSharedDirectoryCreateRequest) + } + + var res SharedDirectoryCreateResponse + err = binary.Read(in, binary.BigEndian, &res) + return res, err +} + +type SharedDirectoryDeleteRequest struct { + CompletionID uint32 + DirectoryID uint32 + Path string +} + +func (s SharedDirectoryDeleteRequest) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryDeleteRequest)) + 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 decodeSharedDirectoryDeleteRequest(in peekReader) (SharedDirectoryDeleteRequest, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryDeleteRequest{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryDeleteRequest) { + return SharedDirectoryDeleteRequest{}, trace.BadParameter("got message type %v, expected SharedDirectoryDeleteRequest(%v)", t, TypeSharedDirectoryDeleteRequest) + } + var completionID, directoryID uint32 + err = binary.Read(in, binary.BigEndian, &completionID) + if err != nil { + return SharedDirectoryDeleteRequest{}, trace.Wrap(err) + } + err = binary.Read(in, binary.BigEndian, &directoryID) + if err != nil { + return SharedDirectoryDeleteRequest{}, trace.Wrap(err) + } + path, err := decodeString(in, tdpMaxPathLength) + if err != nil { + return SharedDirectoryDeleteRequest{}, trace.Wrap(err) + } + + return SharedDirectoryDeleteRequest{ + CompletionID: completionID, + DirectoryID: directoryID, + Path: path, + }, nil +} + +type SharedDirectoryDeleteResponse struct { + CompletionID uint32 + ErrCode uint32 +} + +func (s SharedDirectoryDeleteResponse) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryDeleteResponse)) + binary.Write(buf, binary.BigEndian, s) + return buf.Bytes(), nil +} + +func decodeSharedDirectoryDeleteResponse(in peekReader) (SharedDirectoryDeleteResponse, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryDeleteResponse{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryDeleteRequest) { + return SharedDirectoryDeleteResponse{}, trace.BadParameter("got message type %v, expected SharedDirectoryDeleteResponse(%v)", t, TypeSharedDirectoryDeleteRequest) + } + + var res SharedDirectoryDeleteResponse + err = binary.Read(in, binary.BigEndian, &res) + 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 +} + +// SharedDirectoryReadRequest is a message sent by the server to the client to request +// bytes to be read from the file at the path and starting at byte offset. +type SharedDirectoryReadRequest struct { + CompletionID uint32 + DirectoryID uint32 + Path string + PathLength uint32 + Offset uint64 + Length uint32 +} + +func (s SharedDirectoryReadRequest) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + + buf.WriteByte(byte(TypeSharedDirectoryReadRequest)) + 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) + } + binary.Write(buf, binary.BigEndian, s.Offset) + binary.Write(buf, binary.BigEndian, s.Length) + + return buf.Bytes(), nil +} + +func decodeSharedDirectoryReadRequest(in peekReader) (SharedDirectoryReadRequest, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryReadRequest{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryReadRequest) { + return SharedDirectoryReadRequest{}, trace.BadParameter("got message type %v, expected TypeSharedDirectoryReadRequest(%v)", t, TypeSharedDirectoryReadRequest) + } + + var completionID, directoryID, pathLength, length uint32 + var offset uint64 + + err = binary.Read(in, binary.BigEndian, &completionID) + if err != nil { + return SharedDirectoryReadRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &directoryID) + if err != nil { + return SharedDirectoryReadRequest{}, trace.Wrap(err) + } + + path, err := decodeString(in, tdpMaxPathLength) + if err != nil { + return SharedDirectoryReadRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &pathLength) + if err != nil { + return SharedDirectoryReadRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &offset) + if err != nil { + return SharedDirectoryReadRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &length) + if err != nil { + return SharedDirectoryReadRequest{}, trace.Wrap(err) + } + + return SharedDirectoryReadRequest{ + CompletionID: completionID, + DirectoryID: directoryID, + Path: path, + PathLength: pathLength, + Offset: offset, + Length: length, + }, nil +} + +// SharedDirectoryReadResponse is a message sent by the client to the server +// in response to the SharedDirectoryReadRequest. +type SharedDirectoryReadResponse struct { + CompletionID uint32 + ErrCode uint32 + ReadDataLength uint32 + ReadData []byte +} + +func (s SharedDirectoryReadResponse) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryReadResponse)) + binary.Write(buf, binary.BigEndian, s.CompletionID) + binary.Write(buf, binary.BigEndian, s.ErrCode) + binary.Write(buf, binary.BigEndian, s.ReadDataLength) + if _, err := buf.Write(s.ReadData); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil +} + +func decodeSharedDirectoryReadResponse(in peekReader) (SharedDirectoryReadResponse, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryReadResponse{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryReadResponse) { + return SharedDirectoryReadResponse{}, trace.BadParameter("got message type %v, expected TypeSharedDirectoryReadResponse(%v)", t, TypeSharedDirectoryReadResponse) + } + + var completionID, errorCode, readDataLength uint32 + + err = binary.Read(in, binary.BigEndian, &completionID) + if err != nil { + return SharedDirectoryReadResponse{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &errorCode) + if err != nil { + return SharedDirectoryReadResponse{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &readDataLength) + if err != nil { + return SharedDirectoryReadResponse{}, trace.Wrap(err) + } + + readData := make([]byte, int(readDataLength)) + if _, err := io.ReadFull(in, readData); err != nil { + return SharedDirectoryReadResponse{}, trace.Wrap(err) + } + + return SharedDirectoryReadResponse{ + CompletionID: completionID, + ErrCode: errorCode, + ReadDataLength: readDataLength, + ReadData: readData, + }, nil +} + +// SharedDirectoryWriteRequest is a message sent by the server to the client to request +// bytes to be written the file at the path and starting at byte offset. +type SharedDirectoryWriteRequest struct { + CompletionID uint32 + DirectoryID uint32 + Offset uint64 + Path string + PathLength uint32 + WriteDataLength uint32 + WriteData []byte +} + +func (s SharedDirectoryWriteRequest) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + + buf.WriteByte(byte(TypeSharedDirectoryWriteRequest)) + binary.Write(buf, binary.BigEndian, s.CompletionID) + binary.Write(buf, binary.BigEndian, s.DirectoryID) + binary.Write(buf, binary.BigEndian, s.Offset) + if err := encodeString(buf, s.Path); err != nil { + return nil, trace.Wrap(err) + } + binary.Write(buf, binary.BigEndian, s.WriteDataLength) + if _, err := buf.Write(s.WriteData); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil + +} + +func decodeSharedDirectoryWriteRequest(in peekReader) (SharedDirectoryWriteRequest, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryWriteRequest) { + return SharedDirectoryWriteRequest{}, trace.BadParameter("got message type %v, expected TypeSharedDirectoryWriteRequest(%v)", t, TypeSharedDirectoryWriteRequest) + } + + var completionID, directoryID, pathLength, writeDataLength uint32 + var offset uint64 + + err = binary.Read(in, binary.BigEndian, &completionID) + if err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &directoryID) + if err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &offset) + if err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + + path, err := decodeString(in, tdpMaxPathLength) + if err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &pathLength) + if err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + + err = binary.Read(in, binary.BigEndian, &writeDataLength) + if err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + + writeData := make([]byte, int(writeDataLength)) + if _, err := io.ReadFull(in, writeData); err != nil { + return SharedDirectoryWriteRequest{}, trace.Wrap(err) + } + + return SharedDirectoryWriteRequest{ + CompletionID: completionID, + DirectoryID: directoryID, + Path: path, + PathLength: pathLength, + Offset: offset, + WriteDataLength: writeDataLength, + WriteData: writeData, + }, nil + +} + +// SharedDirectoryWriteResponse is a message sent by the client to the server +// in response to the SharedDirectoryWriteRequest. +type SharedDirectoryWriteResponse struct { + CompletionID uint32 + ErrCode uint32 + BytesWritten uint32 +} + +func (s SharedDirectoryWriteResponse) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeSharedDirectoryWriteResponse)) + binary.Write(buf, binary.BigEndian, s) + return buf.Bytes(), nil +} + +func decodeSharedDirectoryWriteResponse(in peekReader) (SharedDirectoryWriteResponse, error) { + t, err := in.ReadByte() + if err != nil { + return SharedDirectoryWriteResponse{}, trace.Wrap(err) + } + if t != byte(TypeSharedDirectoryWriteResponse) { + return SharedDirectoryWriteResponse{}, trace.BadParameter("got message type %v, expected SharedDirectoryWriteResponse(%v)", t, TypeSharedDirectoryWriteResponse) + } + + var res SharedDirectoryWriteResponse + 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