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() + } +}