diff --git a/docs/overview.md b/docs/overview.md index 22cc17f..ef6a7cf 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -144,7 +144,7 @@ Use Nexus Terminal when you need to: - Native RDP viewer (IronRDP) with Network Level Authentication (CredSSP/NTLM) and optional TLS-only logon for servers such as xrdp. - Server certificate pinned by fingerprint in Known Hosts, checked before credentials are sent. -- Direct, SOCKS5, HTTP CONNECT or SSH-tunnelled connections. +- Direct, SOCKS5, HTTP CONNECT, SSH-tunnelled or (experimental) RD Gateway connections. - Scale-to-fit or 1:1, a fixed resolution or one that follows the tab (Display Control). - Keyboard (scancodes for keys and shortcuts, Unicode for text), mouse and wheel; Ctrl+Alt+Del; text clipboard sharing in both directions. - Password storage modes and domain logon. diff --git a/docs/user-manual.md b/docs/user-manual.md index cd2f8e1..b167e9f 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -166,6 +166,7 @@ Use SSH tunnelling for VNC whenever possible. If a VNC profile has no tunnel, Ne - **RDP**: domain (optional), password storage mode, **Certificate Policy** and **Allow servers without NLA**. The username, host and port (3389) are the profile's own. - **Display**: **Scale to Fit**, a fixed **Width** and **Height** (0 × 0 fits the tab) and **Resize With Tab**. +- **RD Gateway** (experimental): reach the desktop through a Remote Desktop Gateway (`host` or `host:port`) with its own username and password. The gateway's certificate must be valid for its name (public CA); only gateways that accept Basic authentication over HTTPS work. - **Proxy**: **SOCKS5** or **HTTP CONNECT**, with an optional username and password. - **Devices**: **Play Remote Audio**; **Redirect Printer**, which adds a *Nexus Terminal* printer on the remote desktop whose jobs are saved as PostScript files in your Downloads folder (up to 256 MiB per job); and on Linux and macOS a **Shared Folder** that appears on the remote desktop as a drive named Nexus. The server can read and change everything in that folder and nothing outside it: paths that try to leave it (`..`, or symlinks pointing elsewhere) are refused. - **SSH Tunnel**: carry RDP through SSH, as for VNC. Use either a proxy or a tunnel, not both. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1082905..22b7d3e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2631,6 +2631,29 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -2655,6 +2678,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -2978,6 +3034,27 @@ dependencies = [ "smallvec", ] +[[package]] +name = "ironrdp-mstsgu" +version = "0.0.1" +dependencies = [ + "base64 0.22.1", + "bitflags 2.13.2", + "futures-util", + "http-body-util", + "hyper", + "hyper-util", + "ironrdp-core", + "ironrdp-error", + "log", + "tokio", + "tokio-rustls", + "tokio-tungstenite", + "tokio-util", + "uuid", + "webpki-roots", +] + [[package]] name = "ironrdp-pdu" version = "0.9.0" @@ -3651,6 +3728,7 @@ dependencies = [ "image", "ironrdp", "ironrdp-blocking", + "ironrdp-mstsgu", "ironrdp-rdpdr-native", "ironrdp-rdpsnd-native", "keyring", @@ -6366,6 +6444,28 @@ dependencies = [ "syn 3.0.7", ] +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.20" @@ -6479,6 +6579,28 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "sha1 0.10.7", + "thiserror 2.0.21", +] + [[package]] name = "type-map" version = "0.5.1" @@ -6698,6 +6820,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 51ecaf2..4b940cc 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,3 +22,4 @@ lto = "thin" picky = { path = "vendor/picky" } sspi = { path = "vendor/sspi" } ironrdp-session = { path = "vendor/ironrdp-session" } +ironrdp-mstsgu = { path = "vendor/ironrdp-mstsgu" } diff --git a/rust/app/src/profile_editor.rs b/rust/app/src/profile_editor.rs index 09ec01e..acc71e5 100644 --- a/rust/app/src/profile_editor.rs +++ b/rust/app/src/profile_editor.rs @@ -739,6 +739,19 @@ impl ProfileEditor { check(ui, "Resize With Tab", &mut c.dynamic_resolution, "Ask the server to match the tab's size as it changes"); } }); + section(ui, "RD Gateway", |ui| { + text(ui, "Gateway", &mut c.gateway_host, "gateway.example.com (empty: none)"); + err(ui, errs, "rdp.gatewayHost"); + if !c.gateway_host.trim().is_empty() { + text(ui, "Gateway Username", &mut c.gateway_username, "DOMAIN\\user"); + err(ui, errs, "rdp.gatewayUsername"); + mode(ui, "Gateway Password Storage", &mut c.gateway_secret_mode); + if c.gateway_secret_mode == SecretMode::Saved { + secret(ui, "Gateway Password", secrets, SecretKind::RdpGatewayPassword, "Leave blank to keep the saved password"); + } + widgets::hint(ui, "Experimental: gateways that accept Basic authentication over HTTPS (the gateway's certificate is verified)."); + } + }); section(ui, "Proxy", |ui| { combo_s(ui, "Proxy Type", &mut c.proxy_type, &[("none", "None (direct connection)"), ("socks5", "SOCKS5"), ("http", "HTTP CONNECT")]); err(ui, errs, "rdp.proxyType"); diff --git a/rust/core/Cargo.toml b/rust/core/Cargo.toml index 021056c..13a5d98 100644 --- a/rust/core/Cargo.toml +++ b/rust/core/Cargo.toml @@ -39,6 +39,7 @@ webpki-roots = "1" ironrdp = { version = "0.17", features = ["connector", "session", "graphics", "pdu", "svc", "dvc", "displaycontrol", "input", "cliprdr", "rdpsnd", "rdpdr"] } ironrdp-rdpsnd-native = { version = "0.7", default-features = false } ironrdp-blocking = "0.10" +ironrdp-mstsgu = "0.0.1" x509-cert = { version = "0.2", default-features = false, features = ["std"] } [target.'cfg(unix)'.dependencies] diff --git a/rust/core/src/clients/rdp.rs b/rust/core/src/clients/rdp.rs index 7465826..20189f6 100644 --- a/rust/core/src/clients/rdp.rs +++ b/rust/core/src/clients/rdp.rs @@ -467,6 +467,34 @@ fn copy_rect(image: &DecodedImage, fb: &mut Framebuffer, left: u16, top: u16, ri fb.dirty = true; } +/// A transport through an RD Gateway (MS-TSGU over HTTPS WebSocket, Basic authentication). The +/// gateway's certificate is verified against the web PKI. The async gateway stream is bridged to +/// a loopback socket so the blocking RDP client can use it like any other connection. +pub fn gateway_dial(endpoint: String, user: String, password: zeroize::Zeroizing, server: String, port: u16) -> Dial { + Arc::new(move || { + let endpoint = if endpoint.contains(':') { endpoint.clone() } else { format!("{endpoint}:443") }; + let target = ironrdp_mstsgu::GwConnectTarget { + gw_endpoint: endpoint, + gw_user: user.clone(), + gw_pass: password.to_string(), + server: server.clone(), + server_port: port, + }; + let (gw, _) = super::rt() + .block_on(ironrdp_mstsgu::GwClient::connect(&target, "Nexus Terminal")) + .map_err(|e| format!("RD Gateway {}: {}", target.gw_endpoint, error_chain(&e)))?; + let (client, server_end) = super::ssh::loopback_pair()?; + server_end.set_nonblocking(true)?; + super::rt().spawn(async move { + if let Ok(mut local) = tokio::net::TcpStream::from_std(server_end) { + let mut gw = gw; + let _ = tokio::io::copy_bidirectional(&mut local, &mut gw).await; + } + }); + Ok(client) + }) +} + /// `SHA256:` of the certificate, the format SSH host keys use. pub fn fingerprint(cert_der: &[u8]) -> String { use base64::Engine; @@ -544,10 +572,14 @@ impl connector::sspi::network_client::NetworkClient for NoNetwork { } } -/// Connector errors carry their cause in a chain; surface the useful part. +/// IronRDP errors carry their cause in a chain; surface the useful part. fn describe(e: &connector::ConnectorError) -> String { + error_chain(e) +} + +fn error_chain(e: &dyn std::error::Error) -> String { let mut s = e.to_string(); - let mut src = std::error::Error::source(e); + let mut src = e.source(); while let Some(c) = src { s = format!("{s}: {c}"); src = c.source(); diff --git a/rust/core/src/models.rs b/rust/core/src/models.rs index 8c25d45..2fdbf5b 100644 --- a/rust/core/src/models.rs +++ b/rust/core/src/models.rs @@ -418,6 +418,10 @@ pub struct RdpConfig { /// Trust policy for the server's TLS certificate (pinned like an SSH host key). #[serde(deserialize_with = "lenient")] pub host_key_policy: HostKeyPolicy, + /// RD Gateway (`host` or `host:port`, HTTPS); empty connects without one. Basic auth only. + pub gateway_host: String, + pub gateway_username: String, + pub gateway_secret_mode: SecretMode, /// "none", "socks5" or "http"; the password is a stored secret. pub proxy_type: String, pub proxy_host: String, diff --git a/rust/core/src/secrets.rs b/rust/core/src/secrets.rs index f4ecab9..40d4fc5 100644 --- a/rust/core/src/secrets.rs +++ b/rust/core/src/secrets.rs @@ -34,12 +34,13 @@ pub enum SecretKind { RdpPassword, RdpSshPassword, RdpSshPassphrase, + RdpGatewayPassword, } impl SecretKind { /// Every kind, so deleting a profile can purge them all from the keychain. A new variant /// must be added here as well as to `as_str`. - pub const ALL: [SecretKind; 15] = [ + pub const ALL: [SecretKind; 16] = [ Self::Password, Self::Passphrase, Self::Totp, @@ -55,6 +56,7 @@ impl SecretKind { Self::RdpPassword, Self::RdpSshPassword, Self::RdpSshPassphrase, + Self::RdpGatewayPassword, ]; /// The storage name, shared with the Go app. @@ -75,6 +77,7 @@ impl SecretKind { Self::RdpPassword => "rdp-password", Self::RdpSshPassword => "rdp-ssh-password", Self::RdpSshPassphrase => "rdp-ssh-passphrase", + Self::RdpGatewayPassword => "rdp-gateway-password", } } diff --git a/rust/core/src/sessions/protocols.rs b/rust/core/src/sessions/protocols.rs index cb6d6f9..02446a4 100644 --- a/rust/core/src/sessions/protocols.rs +++ b/rust/core/src/sessions/protocols.rs @@ -315,12 +315,17 @@ impl Manager { ) -> Result> { let timeout = Duration::from_secs(p.connect_timeout.max(1) as u64); let host = p.host.clone(); - let dial: clients::rdp::Dial = match tunnel { - Some(t) => { + let gateway = (!rc.gateway_host.trim().is_empty()).then(|| rc.gateway_host.trim().to_string()); + let dial: clients::rdp::Dial = match (tunnel, gateway) { + (_, Some(gw)) => { + let pw = self.secret_fn(id, &p.id, SecretKind::RdpGatewayPassword, rc.gateway_secret_mode, prompt.clone())()?; + clients::rdp::gateway_dial(gw, rc.gateway_username.clone(), pw, host.clone(), port) + } + (Some(t), None) => { let h = host.clone(); Arc::new(move || t.open_tunnel(&h, port)) } - None => { + (None, None) => { let proxy = clients::proxy::Proxy { kind: rc.proxy_type.clone(), host: rc.proxy_host.clone(), diff --git a/rust/core/src/validation.rs b/rust/core/src/validation.rs index acc24e4..3178ef5 100644 --- a/rust/core/src/validation.rs +++ b/rust/core/src/validation.rs @@ -334,6 +334,16 @@ pub fn validate_rdp_config(cfg: &RdpConfig) -> Result<(), ValidationErrors> { } t => errs.add("rdp.proxyType", format!("unsupported proxy type {t:?}")), } + if !cfg.gateway_host.trim().is_empty() { + let host = cfg.gateway_host.trim().rsplit_once(':').filter(|(_, p)| p.parse::().is_ok()).map_or(cfg.gateway_host.trim(), |(h, _)| h); + errs.check("rdp.gatewayHost", validate_host(host)); + if cfg.gateway_username.trim().is_empty() { + errs.add("rdp.gatewayUsername", "the gateway needs a username"); + } + if cfg.ssh_tunnel || !matches!(cfg.proxy_type.as_str(), "" | "none") { + errs.add("rdp.gatewayHost", "use a gateway, a proxy or an SSH tunnel — only one"); + } + } let share = cfg.shared_folder.trim(); if !share.is_empty() && !std::path::Path::new(share).is_dir() { errs.add("rdp.sharedFolder", "the shared folder must be an existing folder"); diff --git a/rust/fuzz/Cargo.toml b/rust/fuzz/Cargo.toml index 4c6b379..ef20ef4 100644 --- a/rust/fuzz/Cargo.toml +++ b/rust/fuzz/Cargo.toml @@ -20,6 +20,7 @@ members = ["."] picky = { path = "../vendor/picky" } sspi = { path = "../vendor/sspi" } ironrdp-session = { path = "../vendor/ironrdp-session" } +ironrdp-mstsgu = { path = "../vendor/ironrdp-mstsgu" } [profile.release] debug = 1 diff --git a/rust/vendor/README.md b/rust/vendor/README.md index 62b6e5d..9a7e5e8 100644 --- a/rust/vendor/README.md +++ b/rust/vendor/README.md @@ -7,6 +7,7 @@ IronRDP release includes the fix it carries. |---|---|---| | `picky` | 7.0.0-rc.25 (crates.io) | `=x.y.z-rc.N` RustCrypto pins relaxed to `^x.y.z-rc.N` (so the stable releases russh uses satisfy them); one unused import removed. | | `sspi` | 0.21.3 (crates.io) | Same pin relaxation. | +| `ironrdp-mstsgu` | 0.0.1 (crates.io) | RD Gateway client. Upstream connects with `ironrdp-tls`, which accepts any certificate, and then sends Basic credentials; the patch verifies the gateway against the web PKI (rustls/ring + webpki-roots). Its `assert!`s and unchecked length subtractions on gateway packets become errors, and the RDP server's port is passed through instead of a fixed 3389. | | `ironrdp-session` | 0.11.0 (crates.io) | Upstream fixes not yet released: #1486 "preserve bitmap source stride" (commit `80bb81b344`, applied as-is) and the RDP6 orientation flip from #1524 (`8f1737a287`). Without them xrdp desktops render sheared and upside-down tile by tile. | Why: IronRDP pins `picky = "=7.0.0-rc.25"`, which pins `curve25519-dalek = "=5.0.0-rc.1"` and diff --git a/rust/vendor/ironrdp-mstsgu/Cargo.lock b/rust/vendor/ironrdp-mstsgu/Cargo.lock new file mode 100644 index 0000000..dddebae --- /dev/null +++ b/rust/vendor/ironrdp-mstsgu/Cargo.lock @@ -0,0 +1,1242 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "ironrdp-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0875b98275068b88652e49ba2e0a1150a1390aad69d26c942c8c59e8ec918e" +dependencies = [ + "ironrdp-error", +] + +[[package]] +name = "ironrdp-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd344ce9518ab83f6f7568ca4f1bc6dc8c55bd2da04cb5ee7b3e8740d5041734" + +[[package]] +name = "ironrdp-mstsgu" +version = "0.0.1" +dependencies = [ + "base64", + "bitflags", + "futures-util", + "http-body-util", + "hyper", + "hyper-util", + "ironrdp-core", + "ironrdp-error", + "ironrdp-tls", + "log", + "tokio", + "tokio-tungstenite", + "tokio-util", + "uuid", +] + +[[package]] +name = "ironrdp-tls" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5f0bb732e33159834f06a687f5415dae1a82c71c123c1c3a3689d41ed47645" +dependencies = [ + "tokio", + "tokio-native-tls", + "tokio-rustls", + "x509-cert", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rust/vendor/ironrdp-mstsgu/Cargo.toml b/rust/vendor/ironrdp-mstsgu/Cargo.toml new file mode 100644 index 0000000..1985789 --- /dev/null +++ b/rust/vendor/ironrdp-mstsgu/Cargo.toml @@ -0,0 +1,223 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.89" +name = "ironrdp-mstsgu" +version = "0.0.1" +authors = [ + "Devolutions Inc. ", + "Teleport ", +] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Terminal Services Gateway Server Protocol" +homepage = "https://github.com/Devolutions/IronRDP" +readme = "README.md" +keywords = [ + "rdp", + "remote-desktop", + "network", + "client", + "protocol", +] +categories = ["network-programming"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/Devolutions/IronRDP" +resolver = "2" + +# Nexus patch: TLS is always rustls (ring) with web PKI verification; see src/lib.rs. +[features] +default = [] + +[lib] +name = "ironrdp_mstsgu" +path = "src/lib.rs" +test = false +doctest = false + +[dependencies.base64] +version = "0.22" + +[dependencies.bitflags] +version = "2.11" + +[dependencies.futures-util] +version = "0.3" + +[dependencies.http-body-util] +version = "0.1" + +[dependencies.hyper] +version = "1.9" +features = [ + "client", + "http1", +] + +[dependencies.hyper-util] +version = "0.1" +features = ["tokio"] + +[dependencies.ironrdp-core] +version = "0.2" +features = ["std"] + +[dependencies.ironrdp-error] +version = "0.2" + +[dependencies.tokio-rustls] +version = "0.26" +default-features = false +features = ["ring", "tls12"] + +[dependencies.webpki-roots] +version = "1" + +[dependencies.log] +version = "0.4" + +[dependencies.tokio] +version = "1.52" +features = [ + "macros", + "rt", +] + +[dependencies.tokio-tungstenite] +version = "0.29" + +[dependencies.tokio-util] +version = "0.7" + +[dependencies.uuid] +version = "1" +features = ["v4"] + +[lints.clippy] +alloc_instead_of_core = "warn" +allow_attributes = "warn" +as_conversions = "warn" +as_pointer_underscore = "warn" +as_ptr_cast_mut = "warn" +as_underscore = "warn" +cast_lossless = "warn" +cast_possible_truncation = "warn" +cast_possible_wrap = "warn" +cast_ptr_alignment = "warn" +cast_sign_loss = "warn" +cfg_not_test = "warn" +checked_conversions = "warn" +clone_on_ref_ptr = "warn" +cloned_instead_of_copied = "warn" +collection_is_never_read = "warn" +copy_iterator = "warn" +dbg_macro = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +doc_include_without_cfg = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +expl_impl_clone_on_copy = "warn" +filetype_is_file = "warn" +float_cmp = "warn" +float_cmp_const = "warn" +fn_to_numeric_cast_any = "warn" +get_unwrap = "warn" +implicit_clone = "warn" +infinite_loop = "warn" +inline_always = "warn" +large_futures = "warn" +large_include_file = "warn" +large_stack_frames = "warn" +large_types_passed_by_value = "warn" +lossy_float_literal = "warn" +map_with_unused_argument_over_ranges = "warn" +mem_forget = "warn" +missing_panics_doc = "warn" +missing_safety_doc = "warn" +mixed_read_write_in_expression = "warn" +multiple_inherent_impl = "warn" +multiple_unsafe_ops_per_block = "warn" +needless_raw_strings = "warn" +non_ascii_literal = "warn" +non_zero_suggestions = "warn" +or_fun_call = "warn" +panic = "warn" +partial_pub_fields = "warn" +precedence_bits = "warn" +print_stderr = "warn" +print_stdout = "warn" +ptr_cast_constness = "warn" +pub_without_shorthand = "warn" +range_plus_one = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +redundant_clone = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +rest_pat_in_fully_bound_structs = "warn" +return_self_not_must_use = "warn" +same_name_method = "warn" +self_named_module_files = "warn" +semicolon_outside_block = "warn" +separated_literal_suffix = "warn" +similar_names = "warn" +std_instead_of_core = "warn" +str_to_string = "warn" +string_add = "warn" +string_lit_chars_any = "warn" +string_slice = "warn" +suspicious_xor_used_as_pow = "warn" +todo = "warn" +trait_duplication_in_bounds = "warn" +transmute_ptr_to_ptr = "warn" +try_err = "warn" +type_repetition_in_bounds = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_box_returns = "warn" +unnecessary_safety_comment = "warn" +unnecessary_self_imports = "warn" +unused_result_ok = "warn" +unused_self = "warn" +unused_trait_names = "warn" +unwrap_used = "warn" +useless_let_if_seq = "warn" +wildcard_dependencies = "warn" +wildcard_imports = "warn" + +[lints.rust] +absolute_paths_not_starting_with_crate = "warn" +ambiguous_negative_literals = "warn" +elided_lifetimes_in_paths = "warn" +invalid_reference_casting = "warn" +keyword_idents = "warn" +macro_use_extern_crate = "warn" +missing_unsafe_on_extern = "warn" +noop_method_call = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +single_use_lifetimes = "warn" +trivial_numeric_casts = "warn" +unit_bindings = "warn" +unreachable_pub = "warn" +unsafe_attr_outside_unsafe = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_crate_dependencies = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" +unused_unsafe = "warn" diff --git a/rust/vendor/ironrdp-mstsgu/Cargo.toml.orig b/rust/vendor/ironrdp-mstsgu/Cargo.toml.orig new file mode 100644 index 0000000..b32451e --- /dev/null +++ b/rust/vendor/ironrdp-mstsgu/Cargo.toml.orig @@ -0,0 +1,41 @@ +[package] +name = "ironrdp-mstsgu" +version = "0.0.1" +readme = "README.md" +description = "Terminal Services Gateway Server Protocol" +edition.workspace = true +rust-version = "1.89" +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"] +native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"] + +[dependencies] +base64 = "0.22" +bitflags = "2.11" +futures-util = "0.3" +http-body-util = { version = "0.1" } +hyper-util = { version = "0.1", features = ["tokio"] } +hyper = { version = "1.9", features = ["client", "http1"] } +ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["std"] } +ironrdp-error = { path = "../ironrdp-error", version = "0.2" } +ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" } +log = "0.4" +tokio-tungstenite = { version = "0.29" } +tokio-util = { version = "0.7" } +tokio = { version = "1.52", features = ["macros", "rt"] } +uuid = { version = "1", features = ["v4"] } + +[lints] +workspace = true diff --git a/rust/vendor/ironrdp-mstsgu/README.md b/rust/vendor/ironrdp-mstsgu/README.md new file mode 100644 index 0000000..8b1d171 --- /dev/null +++ b/rust/vendor/ironrdp-mstsgu/README.md @@ -0,0 +1,11 @@ +# IronRDP MS-TSGU + +[Terminal Services Gateway Server Protocol][MS-TSGU] implementation for IronRDP. + +This crate +- implements an MVP state needed to connect through Microsoft RD Gateway, +- only supports the HTTPS protocol with WebSocket (and not the legacy HTTP, HTTP-RPC or UDP protocols), +- does not implement reconnection/reauthentication, and +- only supports basic auth. + +[MS-TSGU]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/0007d661-a86d-4e8f-89f7-7f77f8824188 diff --git a/rust/vendor/ironrdp-mstsgu/src/lib.rs b/rust/vendor/ironrdp-mstsgu/src/lib.rs new file mode 100644 index 0000000..0fe64a0 --- /dev/null +++ b/rust/vendor/ironrdp-mstsgu/src/lib.rs @@ -0,0 +1,487 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] + +#[macro_use] +mod macros; + +mod proto; + +use core::fmt; +use core::fmt::Display; +use core::pin::Pin; +use core::task::Poll; +use core::time::Duration; +use std::io; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD; +use futures_util::stream::{SplitSink, SplitStream}; +use futures_util::{FutureExt as _, SinkExt as _, StreamExt as _}; +use hyper::body::Bytes; +use ironrdp_core::{Decode as _, Encode, ReadCursor, WriteCursor}; +type TlsStream = tokio_rustls::client::TlsStream; +use log::{error, warn}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; +use tokio::sync::oneshot; +use tokio_tungstenite::WebSocketStream; +use tokio_tungstenite::tungstenite::handshake::client::generate_key; +use tokio_tungstenite::tungstenite::protocol::Role; +use tokio_tungstenite::tungstenite::{Message, http}; +use tokio_util::sync::PollSender; + +use self::proto::{ + ChannelPkt, ChannelResp, DataPkt, HandshakeReqPkt, HandshakeRespPkt, HttpCapsTy, KeepalivePkt, PktHdr, PktTy, + TunnelAuthPkt, TunnelAuthRespPkt, TunnelReqPkt, TunnelRespPkt, +}; + +#[derive(Clone, Debug)] +pub struct GwConnectTarget { + pub gw_endpoint: String, + pub gw_user: String, + pub gw_pass: String, + + pub server: String, + /// The RDP server's port behind the gateway (Nexus patch: was always 3389). + pub server_port: u16, +} + +type Error = ironrdp_error::Error; + +#[derive(Debug)] +#[non_exhaustive] +pub enum GwErrorKind { + InvalidGwTarget, + Connect, + PacketEof, + UnsupportedFeature, + Custom, + Encode, + Decode, +} + +trait GwErrorExt { + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl GwErrorExt for ironrdp_error::Error { + #[track_caller] + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + Self::new(context, GwErrorKind::Custom).with_source(e) + } +} + +impl Display for GwErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let x = match self { + GwErrorKind::InvalidGwTarget => "invalid GW Target", + GwErrorKind::Connect => "connection error", + GwErrorKind::PacketEof => "PacketEOF", + GwErrorKind::UnsupportedFeature => "unsupported feature", + GwErrorKind::Custom => "custom", + GwErrorKind::Encode => "encode", + GwErrorKind::Decode => "decode", + }; + f.write_str(x) + } +} + +impl core::error::Error for GwErrorKind {} + +struct GwConn { + client_name: String, + target: GwConnectTarget, + ws_sink: SplitSink>, Message>, + ws_stream: SplitStream>>, +} + +pub struct GwClient { + work: tokio::task::JoinHandle>, + rx: tokio::sync::mpsc::Receiver, + rx_bufs: Vec, + tx: PollSender, +} + +impl Drop for GwClient { + fn drop(&mut self) { + self.work.abort(); + } +} + +impl GwClient { + pub async fn connect( + target: &GwConnectTarget, + client_name: &str, + ) -> Result<(GwClient, core::net::SocketAddr), Error> { + let gw_host = target + .gw_endpoint + .split(":") + .nth(0) + .ok_or_else(|| Error::new("Connect", GwErrorKind::InvalidGwTarget))?; + + let stream = TcpStream::connect(&target.gw_endpoint) + .await + .map_err(|e| custom_err!("TCP connect", e))?; + let client_addr = stream + .local_addr() + .map_err(|e| custom_err!("get socket local address", e))?; + + // Nexus patch: the gateway is a public HTTPS endpoint, so its certificate is verified + // against the web PKI (upstream used ironrdp-tls, which accepts any certificate, before + // sending Basic credentials). + let mut roots = tokio_rustls::rustls::RootCertStore::empty(); + roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let config = tokio_rustls::rustls::ClientConfig::builder_with_provider(std::sync::Arc::new( + tokio_rustls::rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .map_err(|e| custom_err!("TLS config", e))? + .with_root_certificates(roots) + .with_no_client_auth(); + let name = tokio_rustls::rustls::pki_types::ServerName::try_from(gw_host.to_owned()) + .map_err(|e| custom_err!("gateway host name", e))?; + let stream = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)) + .connect(name, stream) + .await + .map_err(|e| custom_err!("TLS connect", e))?; + + let auth_val: String = STANDARD.encode(format!("{}:{}", target.gw_user, target.gw_pass)); + let req = http::Request::builder() + .method("RDG_OUT_DATA") + .header(hyper::header::HOST, gw_host) + .header("Rdg-Connection-Id", format!("{{{}}}", uuid::Uuid::new_v4())) + .uri("/remoteDesktopGateway/") + .header(hyper::header::AUTHORIZATION, format!("Basic {auth_val}")) + .header(hyper::header::CONNECTION, "Upgrade") + .header(hyper::header::UPGRADE, "websocket") + .header(hyper::header::SEC_WEBSOCKET_VERSION, "13") + .header(hyper::header::SEC_WEBSOCKET_KEY, generate_key()) + .body(http_body_util::Empty::::new()) + .map_err(|e| custom_err!("failed to build request", e))?; + + let stream = hyper_util::rt::tokio::TokioIo::new(stream); + let (mut sender, mut conn) = hyper::client::conn::http1::handshake(stream) + .await + .map_err(|e| custom_err!("H1 Handshake", e))?; + let (tx, rx) = oneshot::channel(); + + let jh = tokio::task::spawn(async move { + tokio::select! { + Err(e) = &mut conn => error!("Handshake error: {:?}", e), + _ = rx => (), + } + conn.into_parts() + }); + let resp = sender + .send_request(req) + .await + .map_err(|e| custom_err!("WS Upgrade Send error", e))?; + + if resp.status() != http::StatusCode::SWITCHING_PROTOCOLS { + return Err(Error::new("WS Upgrade", GwErrorKind::Connect)); + } + + let _ = tx.send(()); // TODO: Not needed since it doesnt keep alive conn? + let stream = jh.await.map_err(|e| custom_err!("WS join", e))?.io.into_inner(); + + Self::connect_ws(target.clone(), client_name, stream) + .await + .map(|x| (x, client_addr)) + } + + async fn connect_ws( + target: GwConnectTarget, + client_name: &str, + tls_stream: TlsStream, + ) -> Result { + let ws_stream: WebSocketStream<_> = WebSocketStream::from_raw_socket(tls_stream, Role::Client, None).await; + let (ws_sink, ws_stream) = ws_stream.split(); + let mut gw = GwConn { + client_name: client_name.to_owned(), + target, + ws_sink, + ws_stream, + }; + + gw.handshake().await?; + gw.tunnel().await?; + gw.tunnel_auth().await?; + gw.channel().await?; + + let (in_tx, in_rx) = tokio::sync::mpsc::channel(4); + let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::(4); + + let work = tokio::spawn(async move { + let iv = Duration::from_secs(15 * 60); + let mut keepalive_interval = tokio::time::interval_at(tokio::time::Instant::now() + iv, iv); + + loop { + let mut wsbuf = [0u8; 8192]; + + tokio::select!( + _ = keepalive_interval.tick() => { + let pos = { + let mut cur = WriteCursor::new(&mut wsbuf); + KeepalivePkt.encode(&mut cur).map_err(|e| custom_err!("PktEncode", e))?; + cur.pos() + }; + + gw.ws_sink.send(Message::Binary(Bytes::copy_from_slice(&wsbuf[..pos]))).await.map_err(|e| custom_err!("ws send", e))?; + }, + next = gw.ws_stream.next() => { + let tmp = next.ok_or_else(|| Error::new("WS Stream Dead", GwErrorKind::Connect))?; + let msg = tmp.map_err(|e| custom_err!("Stream", e))?.into_data(); + let mut cur = ReadCursor::new(&msg); + let hdr = PktHdr::decode(&mut cur).map_err(|e| custom_err!("Header Decode", e))?; + + let header_length = usize::try_from(hdr.length).map_err(|_| Error::new("PktHdr too big", GwErrorKind::Decode))?; + // Nexus patch: a short or inconsistent packet is an error, not a panic. + if header_length.checked_sub(hdr.size()).is_none_or(|body| cur.len() < body) { + return Err(Error::new("PktHdr length", GwErrorKind::Decode)); + } + match hdr.ty { + PktTy::Keepalive => { + continue; + }, + PktTy::Data => { + let p = DataPkt::decode(&mut cur).map_err(|e| custom_err!("PktDecode", e))?; + in_tx.send(Bytes::from(p.data.to_vec())).await.map_err(|e| custom_err!("in_tx dead", e))?; + }, + x => { + warn!("Unhandled gw packet type {x:?}"); + } + } + }, + next = out_rx.recv() => { + let next = next.ok_or_else(|| Error::new("WS Sink Dead", GwErrorKind::Connect))?; + let pkt = DataPkt { data: &next }; + + let pos = { + let mut cur = WriteCursor::new(&mut wsbuf); + pkt.encode(&mut cur).map_err(|e| custom_err!("PktEncode", e))?; + cur.pos() + }; + gw.ws_sink.send(Message::Binary(Bytes::copy_from_slice(&wsbuf[..pos]))).await.map_err(|e| custom_err!("ws send", e))?; + } + ); + } + }); + + Ok(GwClient { + work, + rx: in_rx, + rx_bufs: vec![], + tx: PollSender::new(out_tx), + }) + } +} + +impl GwConn { + async fn send_packet(&mut self, payload: &E) -> Result<(), Error> { + let mut buf = [0u8; 4096]; + let pos = { + let mut cur = WriteCursor::new(&mut buf); + payload + .encode(&mut cur) + .map_err(|e| Error::new("packet encode", GwErrorKind::Encode).with_source(e))?; + cur.pos() + }; + self.ws_sink + .send(Message::Binary(Bytes::copy_from_slice(&buf[..pos]))) + .await + .map_err(|e| custom_err!("WebSocket send error", e))?; + Ok(()) + } + + async fn read_packet(&mut self) -> Result<(PktHdr, Bytes), Error> { + let mut msg = self + .ws_stream + .next() + .await + .ok_or_else(|| Error::new("Stream closed", GwErrorKind::Connect))? + .map_err(|e| custom_err!("WS err", e))? + .into_data(); + let mut cur = ReadCursor::new(&msg); + + let hdr = PktHdr::decode(&mut cur).map_err(|_| Error::new("PktHdr", GwErrorKind::Decode))?; + + let header_length = + usize::try_from(hdr.length).map_err(|_| Error::new("PktHdr too big", GwErrorKind::Decode))?; + if header_length.checked_sub(hdr.size()) != Some(cur.len()) { + return Err(Error::new("read_packet", GwErrorKind::PacketEof)); + } + + Ok((hdr, msg.split_off(cur.pos()))) + } + + async fn handshake(&mut self) -> Result<(), Error> { + // For NTLM we would include extended_auth: NTLM_SSPI in this handshake req here. + let hs = HandshakeReqPkt { + ver_major: 1, + ver_minor: 0, + ..HandshakeReqPkt::default() + }; + self.send_packet(&hs).await?; + let (_hdr, bytes) = self.read_packet().await?; + + let mut cur = ReadCursor::new(&bytes); + let resp = HandshakeRespPkt::decode(&mut cur).map_err(|_| Error::new("Handshake", GwErrorKind::Decode))?; + if resp.error_code != 0 || resp.ver_major != 1 || resp.ver_minor != 0 || resp.server_version != 0 { + return Err(Error::new("Handshake", GwErrorKind::Connect)); + } + Ok(()) + } + + async fn tunnel(&mut self) -> Result<(), Error> { + let req = TunnelReqPkt { + // Havent seen any server working without this. + caps: HttpCapsTy::MessagingConsentSign.as_u32(), + fields_present: 0, + ..TunnelReqPkt::default() + }; + self.send_packet(&req).await?; + + let (_hdr, bytes) = self.read_packet().await?; + let mut cur = ReadCursor::new(&bytes); + + let resp = TunnelRespPkt::decode(&mut cur).map_err(|_| Error::new("TunnelDecode", GwErrorKind::Decode))?; + if resp.status_code != 0 { + return Err(Error::new("Tunnel", GwErrorKind::Connect)); + } + if !cur.eof() { + return Err(Error::new("Tunnel", GwErrorKind::Decode)); + } + if !resp.consent_msg.is_empty() { + return Err(Error::new( + "Received consent message but showing it not implemented", + GwErrorKind::UnsupportedFeature, + )); + } + Ok(()) + } + + async fn tunnel_auth(&mut self) -> Result<(), Error> { + let req = TunnelAuthPkt { + fields_present: 0, + client_name: self.client_name.clone(), + }; + self.send_packet(&req).await?; + + let (_hdr, bytes) = self.read_packet().await?; + let mut cur = ReadCursor::new(&bytes); + let resp: TunnelAuthRespPkt = + TunnelAuthRespPkt::decode(&mut cur).map_err(|_| Error::new("TunnelAuth", GwErrorKind::Decode))?; + + if resp.error_code() != 0 { + return Err(Error::new("TunnelAuth", GwErrorKind::Connect)); + } + Ok(()) + } + + async fn channel(&mut self) -> Result { + let req = ChannelPkt { + resources: vec![self.target.server.clone()], + port: self.target.server_port, + protocol: 3, + }; + self.send_packet(&req).await?; + + let (hdr, bytes) = self.read_packet().await?; + if hdr.ty != PktTy::ChannelResp { + return Err(Error::new("ChannelResp", GwErrorKind::Decode)); + } + let mut cur: ReadCursor<'_> = ReadCursor::new(&bytes); + let resp: ChannelResp = + ChannelResp::decode(&mut cur).map_err(|_| Error::new("ChannelResp", GwErrorKind::Decode))?; + if resp.error_code() != 0 { + return Err(Error::new("ChannelCreate", GwErrorKind::Connect)); + } + if !cur.eof() { + return Err(Error::new("ChannelResp", GwErrorKind::Decode)); + } + Ok(resp) + } +} + +impl AsyncRead for GwClient { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut core::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + // Propagate error or premature exit (?) + match self.work.poll_unpin(cx) { + Poll::Ready(Err(e)) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(Ok(Err(e))) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(_) => return Poll::Ready(Err(io::Error::other("Premature Work Task end?"))), + _ => (), + } + + // Get new bufs + if let Poll::Ready(Some(new_buf)) = self.rx.poll_recv(cx) { + self.rx_bufs.push(new_buf); + } + + // Read from all queued bufs + let mut n = 0; + self.rx_bufs.retain_mut(|rx_buf| { + let rem = buf.remaining(); + if rem == 0 { + return true; + } + let max = core::cmp::min(rem, rx_buf.len()); + buf.put_slice(&rx_buf[..max]); + n += max; + let _ = rx_buf.split_to(max); + + !rx_buf.is_empty() + }); + + if n > 0 { Poll::Ready(Ok(())) } else { Poll::Pending } + } +} + +impl AsyncWrite for GwClient { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut core::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + // Propagate error or premature exit (?) + match self.work.poll_unpin(cx) { + Poll::Ready(Err(e)) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(Ok(Err(e))) => return Poll::Ready(Err(io::Error::other(e))), + Poll::Ready(_) => return Poll::Ready(Err(io::Error::other("Premature Work Task end?"))), + Poll::Pending => (), + } + + match self.tx.poll_reserve(cx) { + Poll::Ready(Ok(())) => { + if self.tx.send_item(Bytes::from(buf.to_vec())).is_err() { + return Poll::Ready(Err(io::Error::other("Sender closed"))); + } + return Poll::Ready(Ok(buf.len())); + } + Poll::Ready(Err(err)) => { + return Poll::Ready(Err(io::Error::other(err))); + } + Poll::Pending => (), + } + + Poll::Pending + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut core::task::Context<'_>) -> Poll> { + // TODO: call flush on the backing sink (e.g. websocket, but atleast for that backend doesnt seem to matter)? + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut core::task::Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/rust/vendor/ironrdp-mstsgu/src/macros.rs b/rust/vendor/ironrdp-mstsgu/src/macros.rs new file mode 100644 index 0000000..489cad9 --- /dev/null +++ b/rust/vendor/ironrdp-mstsgu/src/macros.rs @@ -0,0 +1,5 @@ +/// Creates a [`crate::Error`] with `Custom` kind and a source error attached to it +#[macro_export] +macro_rules! custom_err { + ( $context:expr, $source:expr $(,)? ) => {{ <$crate::Error as $crate::GwErrorExt>::custom($context, $source) }}; +} diff --git a/rust/vendor/ironrdp-mstsgu/src/proto.rs b/rust/vendor/ironrdp-mstsgu/src/proto.rs new file mode 100644 index 0000000..983ae29 --- /dev/null +++ b/rust/vendor/ironrdp-mstsgu/src/proto.rs @@ -0,0 +1,601 @@ +use bitflags::bitflags; +use ironrdp_core::{ + Decode, Encode, ReadCursor, WriteCursor, cast_int, cast_length, ensure_fixed_part_size, ensure_size, + unsupported_value_err, +}; + +bitflags! { + /// 2.2.5.3.2 HTTP_EXTENDED_AUTH Enumeration + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub(crate) struct HttpExtendedAuth: u16 { + const HTTP_EXTENDED_AUTH_NONE = 0x01; + const HTTP_EXTENDED_AUTH_SC = 0x01; + const HTTP_EXTENDED_AUTH_PAA = 0x02; + const HTTP_EXTENDED_AUTH_SSPI_NTLM = 0x04; + } +} + +/// 2.2.5.3.3 HTTP_PACKET_TYPE Enumeration +#[repr(u16)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, Default)] +pub(crate) enum PktTy { + #[default] + Invalid, + HandshakeReq = 0x01, + HandshakeResp = 0x02, + ExtendedAuth = 0x03, + TunnelCreate = 0x04, + TunnelResp = 0x05, + TunnelAuth = 0x06, + TunnelAuthResponse = 0x07, + ChannelCreate = 0x08, + ChannelResp = 0x09, + ChannelClose = 0x10, + Data = 0x0A, + ServiceMessage = 0x0B, + ReauthMessage = 0x0C, + Keepalive = 0x0D, +} + +impl PktTy { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +impl TryFrom for PktTy { + type Error = (); + + fn try_from(val: u16) -> Result { + let mapped = match val { + 0x01 => PktTy::HandshakeReq, + 0x02 => PktTy::HandshakeResp, + 0x03 => PktTy::ExtendedAuth, + 0x04 => PktTy::TunnelCreate, + 0x05 => PktTy::TunnelResp, + 0x06 => PktTy::TunnelAuth, + 0x07 => PktTy::TunnelAuthResponse, + 0x08 => PktTy::ChannelCreate, + 0x09 => PktTy::ChannelResp, + 0x0A => PktTy::Data, + 0x0B => PktTy::ServiceMessage, + 0x0C => PktTy::ReauthMessage, + 0x0D => PktTy::Keepalive, + 0x10 => PktTy::ChannelClose, + _ => return Err(()), + }; + Ok(mapped) + } +} + +/// 2.2.10.9 HTTP_PACKET_HEADER Structure +#[derive(Default, Debug)] +pub(crate) struct PktHdr { + pub ty: PktTy, + pub _reserved: u16, + pub length: u32, +} + +impl PktHdr { + const FIXED_PART_SIZE: usize = 4 /* ty */ + 2 /* _reserved */ + 2 /* length */; +} + +impl Encode for PktHdr { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + dst.write_u16(self.ty.as_u16()); + dst.write_u16(self._reserved); + dst.write_u32(self.length); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_PACKET_HEADER" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl<'a> Decode<'a> for PktHdr { + fn decode(src: &mut ReadCursor<'a>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + let ty = src.read_u16(); + let mty = PktTy::try_from(ty).map_err(|_| unsupported_value_err("PktHdr::ty", "ty", format!("0x{ty:x}")))?; + + Ok(PktHdr { + ty: mty, + _reserved: src.read_u16(), + length: src.read_u32(), + }) + } +} + +/// 2.2.10.10 HTTP_HANDSHAKE_REQUEST_PACKET Structure +#[derive(Default)] +pub(crate) struct HandshakeReqPkt { + pub ver_major: u8, + pub ver_minor: u8, + pub client_version: u16, + pub extended_auth: HttpExtendedAuth, +} + +impl Encode for HandshakeReqPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::HandshakeReq, + length: u32::try_from(self.size()).expect("handshake packet size fits in u32"), + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u8(self.ver_major); + dst.write_u8(self.ver_minor); + dst.write_u16(self.client_version); + dst.write_u16(self.extended_auth.bits()); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_HANDSHAKE_REQUEST_PACKET" + } + + fn size(&self) -> usize { + PktHdr::FIXED_PART_SIZE + 6 + } +} + +/// 2.2.10.11 HTTP_HANDSHAKE_RESPONSE_PACKET Structure +#[derive(Debug)] +pub(crate) struct HandshakeRespPkt { + pub error_code: u32, + pub ver_major: u8, + pub ver_minor: u8, + pub server_version: u16, + pub _extended_auth: HttpExtendedAuth, +} + +impl HandshakeRespPkt { + const FIXED_PART_SIZE: usize = 4 /* error_code */ + 1 /* ver_major */ + 1 /* ver_minor */ + 2 /* server_auth */ + 1 /*extended_auth*/; +} + +impl Decode<'_> for HandshakeRespPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + Ok(HandshakeRespPkt { + error_code: src.read_u32(), + ver_major: src.read_u8(), + ver_minor: src.read_u8(), + server_version: src.read_u16(), + _extended_auth: { + let raw = src.read_u16(); + HttpExtendedAuth::from_bits(raw) + .ok_or_else(|| unsupported_value_err("HandshakeResp", "extended_auth", format!("0x{raw:x}")))? + }, + }) + } +} + +/// 2.2.10.18 HTTP_TUNNEL_PACKET +#[derive(Default)] +pub(crate) struct TunnelReqPkt { + pub caps: u32, + pub fields_present: u16, + pub _reserved: u16, +} + +impl Encode for TunnelReqPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::TunnelCreate, + length: u32::try_from(self.size()).expect("tunnel request packet size fits in u32"), + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u32(self.caps); + dst.write_u16(self.fields_present); + dst.write_u16(self._reserved); + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_TUNNEL_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 8 + } +} + +/// 2.2.5.3.9 HTTP_CAPABILITY_TYPE Enumeration +#[repr(u32)] +#[expect(dead_code)] +#[derive(Copy, Clone)] +pub(crate) enum HttpCapsTy { + QuarSOH = 1, + IdleTimeout = 2, + MessagingConsentSign = 4, + MessagingServiceMsg = 8, + Reauth = 0x10, + UdpTransport = 0x20, +} + +impl HttpCapsTy { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + pub(crate) fn as_u32(self) -> u32 { + self as u32 + } +} + +/// 2.2.5.3.8 HTTP_TUNNEL_RESPONSE_FIELDS_PRESENT_FLAGS +#[repr(u16)] +#[derive(Copy, Clone)] +enum HttpTunnelResponseFields { + TunnelID = 1, + Caps = 2, + /// nonce & server_cert + Soh = 4, + Consent = 0x10, +} + +impl HttpTunnelResponseFields { + #[expect( + clippy::as_conversions, + reason = "guarantees discriminant layout, and as is the only way to cast enum -> primitive" + )] + fn as_u16(self) -> u16 { + self as u16 + } +} + +/// 2.2.10.20 HTTP_TUNNEL_RESPONSE Structure +#[derive(Debug, Default)] +pub(crate) struct TunnelRespPkt { + pub _server_version: u16, + pub status_code: u32, + pub fields_present: u16, + pub _reserved: u16, + + // 2.2.10.21 HTTP_TUNNEL_RESPONSE_OPTIONAL + pub tunnel_id: Option, + pub caps_flags: Option, + pub nonce: Option, + pub server_cert: Vec, + pub consent_msg: Vec, +} + +impl TunnelRespPkt { + const FIXED_PART_SIZE: usize = 2 /* server_version */ + 4 /* status_code */ + 2 /* fields_present */ + 2 /* reserved */; +} + +impl Decode<'_> for TunnelRespPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + let mut pkt = TunnelRespPkt { + _server_version: src.read_u16(), + status_code: src.read_u32(), + fields_present: src.read_u16(), + _reserved: src.read_u16(), + ..TunnelRespPkt::default() + }; + + if pkt.fields_present & (HttpTunnelResponseFields::TunnelID.as_u16()) != 0 { + ensure_size!(in: src, size: 4); + pkt.tunnel_id = Some(src.read_u32()); + } + if pkt.fields_present & (HttpTunnelResponseFields::Caps.as_u16()) != 0 { + ensure_size!(in: src, size: 4); + pkt.caps_flags = Some(src.read_u32()); + } + if pkt.fields_present & (HttpTunnelResponseFields::Soh.as_u16()) != 0 { + ensure_size!(in: src, size: 2 + 2); + pkt.nonce = Some(src.read_u16()); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + pkt.server_cert = src.read_slice(len).to_vec(); + } + if pkt.fields_present & (HttpTunnelResponseFields::Consent.as_u16()) != 0 { + ensure_size!(in: src, size: 2); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + pkt.consent_msg = src.read_slice(len).to_vec(); + } + + Ok(pkt) + } +} + +/// 2.2.10.7 HTTP_EXTENDED_AUTH_PACKET Structure +#[expect(dead_code, reason = "defined for completeness per spec; not yet used")] +pub(crate) struct ExtendedAuthPkt { + error_code: u32, + blob: Vec, +} + +impl Encode for ExtendedAuthPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::ExtendedAuth, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u32(self.error_code); + let blob_len: u16 = cast_int!("blob length", self.blob.len())?; + dst.write_u16(blob_len); + dst.write_slice(&self.blob); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_EXTENDED_AUTH_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 6 + self.blob.len() + } +} + +impl Decode<'_> for ExtendedAuthPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_size!(in: src, size: 4 + 2); + let error_code = src.read_u32(); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + + Ok(ExtendedAuthPkt { + error_code, + blob: src.read_slice(len).to_vec(), + }) + } +} + +/// 2.2.10.14 HTTP_TUNNEL_AUTH_PACKET Structure +pub(crate) struct TunnelAuthPkt { + pub fields_present: u16, + pub client_name: String, +} + +impl Encode for TunnelAuthPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::TunnelAuth, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u16(self.fields_present); + + let client_name_len = self.client_name.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000). + let client_name_len: u16 = cast_int!("client name length", client_name_len)?; + dst.write_u16(client_name_len); + + for c in self.client_name.encode_utf16() { + dst.write_u16(c); + } + + dst.write_u16(0); + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_TUNNEL_AUTH_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 4 + 2 * (self.client_name.len() + 1) + } +} + +/// 2.2.10.16 HTTP_TUNNEL_AUTH_RESPONSE Structure +#[derive(Debug)] +pub(crate) struct TunnelAuthRespPkt { + error_code: u32, + _fields_present: u16, + _reserved: u16, +} + +impl TunnelAuthRespPkt { + const FIXED_PART_SIZE: usize = 4 /* error_code */ + 2 /* fields_present */ + 2 /* _reserved */; + + pub(crate) fn error_code(&self) -> u32 { + self.error_code + } +} + +impl Decode<'_> for TunnelAuthRespPkt { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + Ok(TunnelAuthRespPkt { + error_code: src.read_u32(), + _fields_present: src.read_u16(), + _reserved: src.read_u16(), + }) + } +} + +/// 2.2.10.2 HTTP_CHANNEL_PACKET +pub(crate) struct ChannelPkt { + pub resources: Vec, + pub port: u16, + pub protocol: u16, +} + +impl Encode for ChannelPkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::ChannelCreate, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + + let resources_count: u8 = cast_length!("resources count", self.resources.len())?; + dst.write_u8(resources_count); + dst.write_u8(0); // alt_names + dst.write_u16(self.port); + dst.write_u16(self.protocol); + + // 2.2.10.3 HTTP_CHANNEL_PACKET_VARIABLE + for res in &self.resources { + let res_utf16_len = res.encode_utf16().count() * 2 + 2; // Add 2 to account for a null terminator (0x0000). + let res_len: u16 = cast_int!("resource name UTF-16 length", res_utf16_len)?; + dst.write_u16(res_len); + for b in res.encode_utf16() { + dst.write_u16(b); + } + dst.write_u16(0); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_CHANNEL_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 6 + self.resources.iter().map(|x| 2 + 2 * (x.len() + 1)).sum::() + } +} + +/// 2.2.10.4 HTTP_CHANNEL_RESPONSE +#[derive(Default, Debug)] +pub(crate) struct ChannelResp { + error_code: u32, + fields_present: u16, + _reserved: u16, + + /// 2.2.10.5 HTTP_CHANNEL_RESPONSE_OPTIONAL + chan_id: Option, + udp_port: u16, + authn_cookie: Vec, +} + +impl ChannelResp { + const FIXED_PART_SIZE: usize = 4 /* error_code */ + 2 /* fields_present */ + 2 /* _reserved */; + + pub(crate) fn error_code(&self) -> u32 { + self.error_code + } +} + +impl Decode<'_> for ChannelResp { + fn decode(src: &mut ReadCursor<'_>) -> ironrdp_core::DecodeResult { + ensure_fixed_part_size!(in: src); + + let mut resp = ChannelResp { + error_code: src.read_u32(), + fields_present: src.read_u16(), + _reserved: src.read_u16(), + ..ChannelResp::default() + }; + if resp.fields_present & 1 != 0 { + ensure_size!(in: src, size: 4); + resp.chan_id = Some(src.read_u32()); + } + if resp.fields_present & 2 != 0 { + ensure_size!(in: src, size: 2); + resp.udp_port = src.read_u16(); + } + if resp.fields_present & 4 != 0 { + ensure_size!(in: src, size: 2); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + resp.authn_cookie = src.read_slice(len).to_vec(); + } + Ok(resp) + } +} + +/// 2.2.10.6 HTTP_DATA_PACKET +pub(crate) struct DataPkt<'a> { + pub data: &'a [u8], +} + +impl Encode for DataPkt<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let hdr = PktHdr { + ty: PktTy::Data, + length: cast_int!("packet length", self.size())?, + ..PktHdr::default() + }; + hdr.encode(dst)?; + let data_len: u16 = cast_int!("data payload length", self.data.len())?; + dst.write_u16(data_len); + dst.write_slice(self.data); + Ok(()) + } + + fn name(&self) -> &'static str { + "HTTP_DATA_PACKET" + } + + fn size(&self) -> usize { + PktHdr::default().size() + 2 + self.data.len() + } +} + +impl<'a> Decode<'a> for DataPkt<'a> { + fn decode(src: &mut ReadCursor<'a>) -> ironrdp_core::DecodeResult { + ensure_size!(in: src, size: 2); + let len = usize::from(src.read_u16()); + ensure_size!(in: src, size: len); + Ok(DataPkt { + data: src.read_slice(len), + }) + } +} + +pub(crate) struct KeepalivePkt; + +impl Encode for KeepalivePkt { + fn encode(&self, dst: &mut WriteCursor<'_>) -> ironrdp_core::EncodeResult<()> { + let hdr = PktHdr { + ty: PktTy::Keepalive, + length: u32::try_from(self.size()).expect("keepalive packet size fits in u32"), + ..PktHdr::default() + }; + hdr.encode(dst) + } + + fn name(&self) -> &'static str { + "KEEPALIVE" + } + + fn size(&self) -> usize { + PktHdr::default().size() + } +}