From 7d28ef83a67afa8f69a7170ff47f4547a5d01b1e Mon Sep 17 00:00:00 2001 From: Steffen Butzer Date: Mon, 18 Aug 2025 12:11:24 +0200 Subject: [PATCH] feat: add MS-TSGU (Microsoft RD Gateway) support (#913) This adds a working state to connect with the ironrdp-client CLI against a server behind a microsoft remote desktop gateway. During my testing this was robust enough to work with sessions for more than 30 minutes. CLI Flags and prompts are implemented and can be mixed, so the following would prompt only for 2 passwords: > ironrdp-client --gw-user username@domain --gw-endpoint rdp.gw.host:443 -u username@domain rdp.internal.host [MS-TSGU] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/0007d661-a86d-4e8f-89f7-7f77f8824188 * This implements a MVP (in terms of recentness) state needed to connect through microsoft rdp gateway. * This only supports the HTTPS protocol with Websocket (and not the legacy HTTP, HTTP-RPC or UDP protocols). * This does not implement reconnection/reauthentication. * This only supports basic auth. Mostly looking for rough initial feedback (e.g. in terms of if there are parts that dont align with projects architecture or other areas needing major rework) as well as if the implemented scope would be considered complete enough to land this in the first place. --- Cargo.lock | 155 ++++++-- crates/ironrdp-client/Cargo.toml | 6 +- crates/ironrdp-client/src/config.rs | 51 ++- crates/ironrdp-client/src/rdp.rs | 24 +- crates/ironrdp-mstsgu/Cargo.toml | 40 ++ crates/ironrdp-mstsgu/src/lib.rs | 466 ++++++++++++++++++++++++ crates/ironrdp-mstsgu/src/proto.rs | 547 ++++++++++++++++++++++++++++ 7 files changed, 1240 insertions(+), 49 deletions(-) create mode 100644 crates/ironrdp-mstsgu/Cargo.toml create mode 100644 crates/ironrdp-mstsgu/src/lib.rs create mode 100644 crates/ironrdp-mstsgu/src/proto.rs diff --git a/Cargo.lock b/Cargo.lock index 8c67fdb4..549a5ed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,7 +1510,7 @@ dependencies = [ "embed-resource", "ironrdp", "ironrdp-cliprdr-native", - "ironrdp-core", + "ironrdp-core 0.1.5", "sspi", "thiserror 2.0.14", "tracing", @@ -2332,7 +2332,7 @@ dependencies = [ "ironrdp-cliprdr", "ironrdp-cliprdr-native", "ironrdp-connector", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-graphics", @@ -2359,7 +2359,7 @@ version = "0.6.0" dependencies = [ "ironrdp-async", "ironrdp-connector", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -2370,7 +2370,7 @@ name = "ironrdp-ainput" version = "0.3.0" dependencies = [ "bitflags 2.9.1", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-dvc", "num-derive", "num-traits", @@ -2382,7 +2382,7 @@ version = "0.6.0" dependencies = [ "bytes", "ironrdp-connector", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "tracing", ] @@ -2403,7 +2403,7 @@ version = "0.6.0" dependencies = [ "bytes", "ironrdp-connector", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "tracing", ] @@ -2426,13 +2426,14 @@ dependencies = [ "ironrdp", "ironrdp-cfg", "ironrdp-cliprdr-native", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-dvc-pipe-proxy", + "ironrdp-mstsgu", "ironrdp-propertyset", "ironrdp-rdcleanpath", "ironrdp-rdpfile", "ironrdp-rdpsnd-native", - "ironrdp-tls", + "ironrdp-tls 0.1.3", "ironrdp-tokio", "proc-exit", "raw-window-handle", @@ -2441,7 +2442,8 @@ dependencies = [ "softbuffer", "tap", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.27.0", + "tokio-util", "tracing", "tracing-subscriber", "transport", @@ -2458,7 +2460,7 @@ name = "ironrdp-cliprdr" version = "0.3.0" dependencies = [ "bitflags 2.9.1", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -2468,7 +2470,7 @@ dependencies = [ name = "ironrdp-cliprdr-format" version = "0.1.3" dependencies = [ - "ironrdp-core", + "ironrdp-core 0.1.5", "png", ] @@ -2477,7 +2479,7 @@ name = "ironrdp-cliprdr-native" version = "0.3.0" dependencies = [ "ironrdp-cliprdr", - "ironrdp-core", + "ironrdp-core 0.1.5", "tracing", "windows 0.61.3", ] @@ -2487,8 +2489,8 @@ name = "ironrdp-connector" version = "0.6.0" dependencies = [ "arbitrary", - "ironrdp-core", - "ironrdp-error", + "ironrdp-core 0.1.5", + "ironrdp-error 0.1.3", "ironrdp-pdu", "ironrdp-svc", "picky", @@ -2504,14 +2506,23 @@ dependencies = [ name = "ironrdp-core" version = "0.1.5" dependencies = [ - "ironrdp-error", + "ironrdp-error 0.1.3", +] + +[[package]] +name = "ironrdp-core" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2db60a59716a84d09040d29c9e75e81545842510fccb0934c09b28e78b46680" +dependencies = [ + "ironrdp-error 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "ironrdp-displaycontrol" version = "0.3.0" dependencies = [ - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -2522,7 +2533,7 @@ dependencies = [ name = "ironrdp-dvc" version = "0.3.1" dependencies = [ - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "ironrdp-svc", "slab", @@ -2534,7 +2545,7 @@ name = "ironrdp-dvc-pipe-proxy" version = "0.1.0" dependencies = [ "async-trait", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -2546,6 +2557,12 @@ dependencies = [ name = "ironrdp-error" version = "0.1.3" +[[package]] +name = "ironrdp-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9d7794e854eef2f13fdf79c8502bcc567a75a15fd0522885f37739386a4cef" + [[package]] name = "ironrdp-futures" version = "0.4.0" @@ -2562,7 +2579,7 @@ dependencies = [ "arbitrary", "ironrdp-cliprdr", "ironrdp-cliprdr-format", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-displaycontrol", "ironrdp-graphics", "ironrdp-pdu", @@ -2582,7 +2599,7 @@ dependencies = [ "bytemuck", "byteorder", "expect-test", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "lazy_static", "num-derive", @@ -2599,6 +2616,26 @@ dependencies = [ "smallvec", ] +[[package]] +name = "ironrdp-mstsgu" +version = "0.0.1" +dependencies = [ + "base64", + "bitflags 2.9.1", + "futures-util", + "http-body-util", + "hyper", + "hyper-util", + "ironrdp-core 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ironrdp-error 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ironrdp-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "tokio", + "tokio-tungstenite 0.26.2", + "tokio-util", + "uuid", +] + [[package]] name = "ironrdp-pdu" version = "0.5.0" @@ -2608,8 +2645,8 @@ dependencies = [ "byteorder", "der-parser", "expect-test", - "ironrdp-core", - "ironrdp-error", + "ironrdp-core 0.1.5", + "ironrdp-error 0.1.3", "lazy_static", "md-5", "num-bigint", @@ -2646,8 +2683,8 @@ name = "ironrdp-rdpdr" version = "0.3.0" dependencies = [ "bitflags 2.9.1", - "ironrdp-core", - "ironrdp-error", + "ironrdp-core 0.1.5", + "ironrdp-error 0.1.3", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -2657,7 +2694,7 @@ dependencies = [ name = "ironrdp-rdpdr-native" version = "0.3.0" dependencies = [ - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "ironrdp-rdpdr", "ironrdp-svc", @@ -2677,7 +2714,7 @@ name = "ironrdp-rdpsnd" version = "0.5.0" dependencies = [ "bitflags 2.9.1", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -2707,7 +2744,7 @@ dependencies = [ "ironrdp-ainput", "ironrdp-async", "ironrdp-cliprdr", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-graphics", @@ -2731,10 +2768,10 @@ name = "ironrdp-session" version = "0.5.0" dependencies = [ "ironrdp-connector", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-displaycontrol", "ironrdp-dvc", - "ironrdp-error", + "ironrdp-error 0.1.3", "ironrdp-graphics", "ironrdp-pdu", "ironrdp-svc", @@ -2752,7 +2789,7 @@ name = "ironrdp-svc" version = "0.4.1" dependencies = [ "bitflags 2.9.1", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-pdu", ] @@ -2767,7 +2804,7 @@ dependencies = [ "ironrdp-cliprdr", "ironrdp-cliprdr-format", "ironrdp-connector", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-fuzzing", @@ -2796,7 +2833,7 @@ dependencies = [ "async-trait", "ironrdp", "ironrdp-async", - "ironrdp-tls", + "ironrdp-tls 0.1.3", "ironrdp-tokio", "semver", "tokio", @@ -2814,6 +2851,18 @@ dependencies = [ "x509-cert", ] +[[package]] +name = "ironrdp-tls" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc807c143533f41e19bf323e8c7f78995953ce8427d37220dde53906cbd48a3" +dependencies = [ + "tokio", + "tokio-native-tls", + "tokio-rustls", + "x509-cert", +] + [[package]] name = "ironrdp-tokio" version = "0.6.0" @@ -2843,7 +2892,7 @@ dependencies = [ "iron-remote-desktop", "ironrdp", "ironrdp-cliprdr-format", - "ironrdp-core", + "ironrdp-core 0.1.5", "ironrdp-futures", "ironrdp-propertyset", "ironrdp-rdcleanpath", @@ -5474,6 +5523,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite 0.26.2", +] + [[package]] name = "tokio-tungstenite" version = "0.27.0" @@ -5489,7 +5556,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tungstenite", + "tungstenite 0.27.0", ] [[package]] @@ -5709,6 +5776,26 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.27.0" diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index af7d4e02..5495031b 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -25,8 +25,8 @@ test = false [features] default = ["rustls"] -rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"] -native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"] +rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots", "ironrdp-mstsgu/rustls"] +native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls", "ironrdp-mstsgu/native-tls"] qoi = ["ironrdp/qoi"] qoiz = ["ironrdp/qoiz"] @@ -48,6 +48,7 @@ ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.3" } ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.3" } ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" } +ironrdp-mstsgu = { path = "../ironrdp-mstsgu" } ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.6", features = ["reqwest"] } ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" ironrdp-dvc-pipe-proxy.path = "../ironrdp-dvc-pipe-proxy" @@ -70,6 +71,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Async, futures tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7" } tokio-tungstenite = "0.27" transport = { git = "https://github.com/Devolutions/devolutions-gateway", rev = "06e91dfe82751a6502eaf74b6a99663f06f0236d" } futures-util = { version = "0.3", features = ["sink"] } diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index bbf56c0d..1fbf8323 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -10,6 +10,7 @@ use clap::Parser; use ironrdp::connector::{self, Credentials}; use ironrdp::pdu::rdp::capability_sets::{client_codecs_capabilities, MajorPlatformType}; use ironrdp::pdu::rdp::client_info::PerformanceFlags; +use ironrdp_mstsgu::GwConnectTarget; use tap::prelude::*; use url::Url; @@ -19,6 +20,7 @@ const DEFAULT_HEIGHT: u16 = 1080; #[derive(Clone, Debug)] pub struct Config { pub log_file: Option, + pub gw: Option, pub destination: Destination, pub connector: connector::Config, pub clipboard_type: ClipboardType, @@ -178,6 +180,17 @@ impl FromStr for DvcProxyInfo { #[clap(author = "Devolutions", about = "Devolutions-IronRDP client")] #[clap(version, long_about = None)] struct Args { + /// A file with IronRDP client logs + #[clap(short, long, value_parser)] + log_file: Option, + + #[clap(long, value_parser)] + gw_endpoint: Option, + #[clap(long, value_parser)] + gw_user: Option, + #[clap(long, value_parser)] + gw_pass: Option, + /// An address on which the client will connect. destination: Option, @@ -185,10 +198,6 @@ struct Args { #[clap(long)] rdp_file: Option, - /// A file with IronRDP client logs - #[clap(short, long)] - log_file: Option, - /// A target RDP server user name #[clap(short, long)] username: Option, @@ -310,6 +319,35 @@ impl Config { } } + let mut gw: Option = None; + if let Some(gw_addr) = args.gw_endpoint { + gw = Some(GwConnectTarget { + gw_endpoint: gw_addr, + gw_user: String::new(), + gw_pass: String::new(), + server: String::new(), // TODO non-standard port? also dont use here? + }); + } + + if let Some(ref mut gw) = gw { + gw.gw_user = if let Some(gw_user) = args.gw_user { + gw_user + } else { + inquire::Text::new("Gateway username:") + .prompt() + .context("Username prompt")? + }; + + gw.gw_pass = if let Some(gw_pass) = args.gw_pass { + gw_pass + } else { + inquire::Password::new("Gateway password:") + .without_confirmation() + .prompt() + .context("Password prompt")? + }; + }; + let destination = if let Some(destination) = args.destination { destination } else if let Some(destination) = properties.full_address() { @@ -326,6 +364,10 @@ impl Config { .pipe(Destination::new)? }; + if let Some(ref mut gw) = gw { + gw.server = destination.name.clone(); // TODO + } + let username = if let Some(username) = args.username { username } else if let Some(username) = properties.username() { @@ -430,6 +472,7 @@ impl Config { Ok(Self { log_file: args.log_file, + gw, destination, connector, clipboard_type, diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index 769694dd..2c3d997c 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -171,15 +171,21 @@ async fn connect( ) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> { let dest = format!("{}:{}", config.destination.name(), config.destination.port()); - let socket = TcpStream::connect(dest) - .await - .map_err(|e| connector::custom_err!("TCP connect", e))?; - - let client_addr = socket - .local_addr() - .map_err(|e| connector::custom_err!("get socket local address", e))?; - - let mut framed = ironrdp_tokio::TokioFramed::new(socket); + let (client_addr, stream) = if let Some(ref gw_config) = config.gw { + let (gw, client_addr) = ironrdp_mstsgu::GwClient::connect(gw_config, &config.connector.client_name) + .await + .map_err(|e| connector::custom_err!("GW Connect", e))?; + (client_addr, tokio_util::either::Either::Left(gw)) + } else { + let stream = TcpStream::connect(dest) + .await + .map_err(|e| connector::custom_err!("TCP connect", e))?; + let client_addr = stream + .local_addr() + .map_err(|e| connector::custom_err!("get socket local address", e))?; + (client_addr, tokio_util::either::Either::Right(stream)) + }; + let mut framed = ironrdp_tokio::TokioFramed::new(stream); let mut drdynvc = ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))); diff --git a/crates/ironrdp-mstsgu/Cargo.toml b/crates/ironrdp-mstsgu/Cargo.toml new file mode 100644 index 00000000..32f00afd --- /dev/null +++ b/crates/ironrdp-mstsgu/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "ironrdp-mstsgu" +version = "0.0.1" +readme = "README.md" +description = "Terminal Services Gateway Server Protocol" +edition.workspace = true +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] +bitflags = "2.9" +ironrdp-core = { version = "0.1", features = ["std"] } +ironrdp-error = { version = "0.1" } +tokio = { version = "1.43", features = ["macros", "rt"] } +tokio-util = { version = "0.7" } +tokio-tungstenite = { version = "0.26" } +ironrdp-tls = { "version" = "0.1.3" } +hyper = { version = "1.6", features = ["client", "http1"] } +hyper-util = { version = "0.1", features = ["tokio"] } +http-body-util = { version = "0.1" } +futures-util = "0.3" +log = "0.4" +base64 = "0.22" +uuid = { version = "1.16.0", features = ["v4"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-mstsgu/src/lib.rs b/crates/ironrdp-mstsgu/src/lib.rs new file mode 100644 index 00000000..9f38db8d --- /dev/null +++ b/crates/ironrdp-mstsgu/src/lib.rs @@ -0,0 +1,466 @@ +//! [MS-TSGU] https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/0007d661-a86d-4e8f-89f7-7f77f8824188 +//! * This implements a MVP (in terms of recentness) state needed to connect through microsoft rdp gateway. +//! * This only supports the HTTPS protocol with Websocket (and not the legacy HTTP, HTTP-RPC or UDP protocols). +//! * This does not implement reconnection/reauthentication. +//! * This only supports basic auth. +use core::pin::Pin; +use core::time::Duration; +use core::{fmt, fmt::Display, task::Poll}; +use std::io; + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use futures_util::{ + stream::{SplitSink, SplitStream}, + FutureExt as _, SinkExt as _, StreamExt as _, +}; +use hyper::body::Bytes; +use ironrdp_core::{Decode as _, Encode, ReadCursor, WriteCursor}; +use ironrdp_tls::TlsStream; +use log::{error, warn}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, + sync::oneshot, +}; +use tokio_tungstenite::{ + tungstenite::{ + handshake::client::generate_key, + http::{self}, + protocol::Role, + Message, + }, + WebSocketStream, +}; + +mod proto; +use proto::*; +use tokio_util::sync::PollSender; + +#[derive(Clone, Debug)] +pub struct GwConnectTarget { + pub gw_endpoint: String, + pub gw_user: String, + pub gw_pass: String, + + pub server: String, +} + +type Error = ironrdp_error::Error; + +#[derive(Debug)] +#[non_exhaustive] +pub enum GwErrorKind { + InvalidGwTarget, + Connect, + PacketEOF, + UnsupportedFeature, + Custom, + 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 { + 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::Decode => "Decode", + }; + f.write_str(x) + } +} + +impl core::error::Error for GwErrorKind {} + +/// Creates a `ConnectorError` 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) + }}; +} + +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))?; + + let (stream, _) = ironrdp_tls::upgrade(stream, gw_host) + .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()) + .expect("Failed to build request"); + + 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 = + 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))?; + + assert!(cur.len() >= hdr.length as usize - hdr.size()); + 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).unwrap(); + cur.pos() + }; + self.ws_sink + .send(Message::Binary(Bytes::copy_from_slice(&buf[..pos]))) + .await + .map_err(|e| custom_err!("WS 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))?; + if cur.len() != hdr.length as usize - hdr.size() { + 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)); + } + assert!(cur.eof()); + 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: 3389, + protocol: 3, + }; + self.send_packet(&req).await?; + + let (hdr, bytes) = self.read_packet().await?; + assert!(hdr.ty == PktTy::ChannelResp); + 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)); + } + assert!(cur.eof()); + 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/crates/ironrdp-mstsgu/src/proto.rs b/crates/ironrdp-mstsgu/src/proto.rs new file mode 100644 index 00000000..69a0950a --- /dev/null +++ b/crates/ironrdp-mstsgu/src/proto.rs @@ -0,0 +1,547 @@ +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, ensure_size, unsupported_value_err, Decode, Encode, ReadCursor, WriteCursor, +}; + +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 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, + _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 { + 8 + } +} + +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()).unwrap(), + ..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::default().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(crate) _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()).unwrap(), + ..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)] +pub(crate) enum HttpCapsTy { + QuarSOH = 1, + IdleTimeout = 2, + MessagingConsentSign = 4, + MessagingServiceMsg = 8, + Reauth = 0x10, + UdpTransport = 0x20, +} + +/// 2.2.5.3.8 HTTP_TUNNEL_RESPONSE_FIELDS_PRESENT_FLAGS +#[repr(u16)] +enum HttpTunnelResponseFields { + TunnelID = 1, + Caps = 2, + /// nonce & server_cert + Soh = 4, + Consent = 0x10, +} + +/// 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 = src.read_u16(); + ensure_size!(in: src, size: len as usize); + pkt.server_cert = src.read_slice(len as usize).to_vec(); + } + if pkt.fields_present & (HttpTunnelResponseFields::Consent as u16) != 0 { + ensure_size!(in: src, size: 2); + let len = src.read_u16(); + ensure_size!(in: src, size: len as usize); + pkt.consent_msg = src.read_slice(len as usize).to_vec(); + } + + Ok(pkt) + } +} + +/// 2.2.10.7 HTTP_EXTENDED_AUTH_PACKET Structure +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: u32::try_from(self.size()).unwrap(), + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u32(self.error_code); + dst.write_u16(u16::try_from(self.blob.len()).unwrap()); + 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 = src.read_u16(); + ensure_size!(in: src, size: len as usize); + + Ok(ExtendedAuthPkt { + error_code, + blob: src.read_slice(len as usize).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: u32::try_from(self.size()).unwrap(), + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u16(self.fields_present); + dst.write_u16(u16::try_from(2 * (self.client_name.len() + 1)).unwrap()); + 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 { + pub error_code: u32, + _fields_present: u16, + _reserved: u16, +} + +impl TunnelAuthRespPkt { + const FIXED_PART_SIZE: usize = 4 /* error_code */ + 2 /* fields_present */ + 2 /* _reserved */; +} + +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: u32::try_from(self.size()).unwrap(), + ..PktHdr::default() + }; + hdr.encode(dst)?; + + dst.write_u8(u8::try_from(self.resources.len()).unwrap()); + 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 { + dst.write_u16(u16::try_from(2 * (res.len() + 1)).unwrap()); + 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 { + pub 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 */; +} + +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 = src.read_u16(); + ensure_size!(in: src, size: len as usize); + resp.authn_cookie = src.read_slice(len as usize).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: u32::try_from(self.size()).unwrap(), + ..PktHdr::default() + }; + hdr.encode(dst)?; + dst.write_u16(u16::try_from(self.data.len()).unwrap()); + 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 = src.read_u16(); + ensure_size!(in: src, size: len as usize); + Ok(DataPkt { + data: src.read_slice(len as usize), + }) + } +} + +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()).unwrap(), + ..PktHdr::default() + }; + hdr.encode(dst) + } + + fn name(&self) -> &'static str { + "KEEPALIVE" + } + + fn size(&self) -> usize { + PktHdr::default().size() + } +}