diff --git a/Cargo.lock b/Cargo.lock
index ea165ee61..980260bba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1527,6 +1527,7 @@ dependencies = [
name = "ffi"
version = "0.0.0"
dependencies = [
+ "anyhow",
"diplomat",
"diplomat-runtime",
"embed-resource",
@@ -1534,6 +1535,7 @@ dependencies = [
"ironrdp-cliprdr-native",
"ironrdp-core",
"ironrdp-dvc-pipe-proxy",
+ "ironrdp-rdcleanpath",
"sspi",
"thiserror 2.0.17",
"tracing",
diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml
index 30d98d3db..22d5871ea 100644
--- a/ffi/Cargo.toml
+++ b/ffi/Cargo.toml
@@ -18,10 +18,12 @@ ironrdp = { path = "../crates/ironrdp", features = ["session", "connector", "dvc
ironrdp-cliprdr-native.path = "../crates/ironrdp-cliprdr-native"
ironrdp-dvc-pipe-proxy.path = "../crates/ironrdp-dvc-pipe-proxy"
ironrdp-core = { path = "../crates/ironrdp-core", features = ["alloc"] }
+ironrdp-rdcleanpath.path = "../crates/ironrdp-rdcleanpath"
sspi = { version = "0.16", features = ["network_client"] }
thiserror = "2"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+anyhow = "1.0"
[target.'cfg(windows)'.build-dependencies]
embed-resource = "3.0"
diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj
index e29164c4e..802b8ad76 100644
--- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj
+++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj
@@ -10,18 +10,18 @@
-
-
-
-
+
+
+
+
-
+
-
+
diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs
index 341485271..4ab8ef3f3 100644
--- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs
+++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs
@@ -7,6 +7,7 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
+using System.IO;
using System.Net.Security;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -22,7 +23,7 @@ public partial class MainWindow : Window
readonly InputDatabase? _inputDatabase = InputDatabase.New();
ActiveStage? _activeStage;
DecodedImage? _decodedImage;
- Framed? _framed;
+ Framed? _framed;
WinCliprdr? _cliprdr;
private readonly RendererModel _renderModel;
private Image? _imageControl;
@@ -77,18 +78,49 @@ private void OnOpened(object? sender, EventArgs e)
var username = Environment.GetEnvironmentVariable("IRONRDP_USERNAME");
var password = Environment.GetEnvironmentVariable("IRONRDP_PASSWORD");
- var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN");
+ var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN"); // Optional
var server = Environment.GetEnvironmentVariable("IRONRDP_SERVER");
+ var portEnv = Environment.GetEnvironmentVariable("IRONRDP_PORT"); // Optional
- if (username == null || password == null || domain == null || server == null)
+ // Gateway configuration (optional)
+ var gatewayUrl = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_URL");
+ var gatewayToken = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_TOKEN");
+ var tokengenUrl = Environment.GetEnvironmentVariable("IRONRDP_TOKENGEN_URL");
+
+ if (username == null || password == null || server == null)
{
var errorMessage =
- "Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, IRONRDP_DOMAIN, and RONRDP_SERVER environment variables";
+ "Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, and IRONRDP_SERVER environment variables";
Trace.TraceError(errorMessage);
Close();
throw new InvalidProgramException(errorMessage);
}
+ // Validate server is only domain or IP (no port allowed)
+ // i.e. "example.com" or "10.10.0.3" the port should go to the dedicated env var IRONRDP_PORT
+ if (server.Contains(':'))
+ {
+ var errorMessage = $"IRONRDP_SERVER must be a domain or IP address only, not '{server}'. Use IRONRDP_PORT for the port.";
+ Trace.TraceError(errorMessage);
+ Close();
+ throw new InvalidProgramException(errorMessage);
+ }
+
+ // Parse port from environment variable or use default
+ int port = 3389;
+ if (!string.IsNullOrEmpty(portEnv))
+ {
+ if (!int.TryParse(portEnv, out port) || port <= 0 || port > 65535)
+ {
+ var errorMessage = $"IRONRDP_PORT must be a valid port number (1-65535), got '{portEnv}'";
+ Trace.TraceError(errorMessage);
+ Close();
+ throw new InvalidProgramException(errorMessage);
+ }
+ }
+
+ Trace.TraceInformation($"Target server: {server}:{port}");
+
var config = BuildConfig(username, password, domain, _renderModel.Width, _renderModel.Height);
CliprdrBackendFactory? factory = null;
@@ -106,15 +138,81 @@ private void OnOpened(object? sender, EventArgs e)
BeforeConnectSetup();
Task.Run(async () =>
{
- var (res, framed) = await Connection.Connect(config, server, factory);
- this._decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(),
- res.GetDesktopSize().GetHeight());
- this._activeStage = ActiveStage.New(res);
- this._framed = framed;
- ReadPduAndProcessActiveStage();
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ try
{
- HandleClipboardEvents();
+ ConnectionResult res;
+
+ // Determine connection mode: Gateway or Direct
+ if (!string.IsNullOrEmpty(gatewayUrl))
+ {
+ Trace.TraceInformation("=== GATEWAY MODE ===");
+ Trace.TraceInformation($"Gateway URL: {gatewayUrl}");
+ Trace.TraceInformation($"Destination: {server}:{port}");
+
+ var tokenGen = new TokenGenerator(tokengenUrl ?? "http://localhost:8080");
+
+ // Generate RDP token if not provided
+ if (string.IsNullOrEmpty(gatewayToken))
+ {
+ Trace.TraceInformation("No RDP token provided, generating token...");
+
+ try
+ {
+ gatewayToken = await tokenGen.GenerateRdpTlsToken(
+ dstHost: server!,
+ proxyUser: string.IsNullOrEmpty(domain) ? username : $"{username}@{domain}",
+ proxyPassword: password!,
+ destUser: username!,
+ destPassword: password!
+ );
+ Trace.TraceInformation($"RDP token generated successfully (length: {gatewayToken.Length})");
+ }
+ catch (Exception ex)
+ {
+ Trace.TraceError($"Failed to generate RDP token: {ex.Message}");
+ Trace.TraceInformation("Make sure tokengen server is running:");
+ Trace.TraceInformation($" cargo run --manifest-path tools/tokengen/Cargo.toml -- server");
+ throw;
+ }
+ }
+
+ // Connect via gateway - destination needs "hostname:port" format for RDCleanPath
+ string destination = $"{server}:{port}";
+
+ var (gatewayRes, gatewayFramed) = await RDCleanPathConnection.ConnectRDCleanPath(
+ config, gatewayUrl, gatewayToken!, destination, null, factory);
+ res = gatewayRes;
+ this._framed = new Framed(gatewayFramed.GetInner().Item1);
+
+ Trace.TraceInformation("=== GATEWAY CONNECTION SUCCESSFUL ===");
+ }
+ else
+ {
+ Trace.TraceInformation("=== DIRECT MODE ===");
+
+ // Direct connection (original behavior)
+ var (directRes, directFramed) = await Connection.Connect(config, server, factory, port);
+ res = directRes;
+ this._framed = new Framed(directFramed.GetInner().Item1);
+
+ Trace.TraceInformation("=== DIRECT CONNECTION SUCCESSFUL ===");
+ }
+
+ this._decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(),
+ res.GetDesktopSize().GetHeight());
+ this._activeStage = ActiveStage.New(res);
+ ReadPduAndProcessActiveStage();
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ HandleClipboardEvents();
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.TraceError($"Connection failed: {ex.Message}");
+ Trace.TraceError($"Stack trace: {ex.StackTrace}");
+ throw;
}
});
}
@@ -260,12 +358,16 @@ private void HandleClipboardEvents()
});
}
- private static Config BuildConfig(string username, string password, string domain, int width, int height)
+ private static Config BuildConfig(string username, string password, string? domain, int width, int height)
{
ConfigBuilder configBuilder = ConfigBuilder.New();
configBuilder.WithUsernameAndPassword(username, password);
- configBuilder.SetDomain(domain);
+ if (domain != null)
+ {
+ configBuilder.SetDomain(domain);
+ }
+
configBuilder.SetDesktopSize((ushort)height, (ushort)width);
configBuilder.SetClientName("IronRdp");
configBuilder.SetClientDir("C:\\");
@@ -418,7 +520,7 @@ private async Task HandleActiveStageOutput(ActiveStageOutputIterator outpu
var writeBuf = WriteBuf.New();
while (true)
{
- await Connection.SingleSequenceStep(activationSequence, writeBuf,_framed!);
+ await Connection.SingleSequenceStep(activationSequence, writeBuf, _framed!);
if (activationSequence.GetState().GetType() != ConnectionActivationStateType.Finalized)
continue;
diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs
new file mode 100644
index 000000000..00946c604
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Devolutions.IronRdp.AvaloniaExample;
+
+///
+/// Client for requesting JWT tokens from a Devolutions Gateway tokengen server.
+///
+public class TokenGenerator : IDisposable
+{
+ private readonly HttpClient _client;
+ private readonly string _tokengenUrl;
+
+ ///
+ /// Creates a new TokenGenerator instance.
+ ///
+ /// The base URL of the tokengen server (e.g., "http://localhost:8080")
+ public TokenGenerator(string tokengenUrl = "http://localhost:8080")
+ {
+ _tokengenUrl = tokengenUrl;
+ _client = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(30)
+ };
+ }
+
+ ///
+ /// Generates an RDP token with credential injection for gateway-based connections.
+ ///
+ /// Destination RDP server (e.g., "10.10.0.3:3389")
+ /// Gateway proxy username
+ /// Gateway proxy password
+ /// Destination RDP server username
+ /// Destination RDP server password
+ /// Optional session UUID
+ /// Token validity in seconds (default: 3600)
+ /// A JWT token string
+ public async Task GenerateRdpTlsToken(
+ string dstHost,
+ string proxyUser,
+ string proxyPassword,
+ string destUser,
+ string destPassword,
+ string? jetAid = null,
+ int validityDuration = 3600)
+ {
+ var request = new RdpTlsTokenRequest
+ {
+ DstHst = dstHost,
+ PrxUsr = proxyUser,
+ PrxPwd = proxyPassword,
+ DstUsr = destUser,
+ DstPwd = destPassword,
+ JetAid = jetAid,
+ ValidityDuration = validityDuration
+ };
+
+ try
+ {
+ var response = await _client.PostAsJsonAsync($"{_tokengenUrl}/rdp_tls", request);
+ response.EnsureSuccessStatusCode();
+
+ var result = await response.Content.ReadFromJsonAsync();
+ if (result?.Token == null)
+ {
+ throw new Exception("Token generation failed: Empty response");
+ }
+
+ return result.Token;
+ }
+ catch (HttpRequestException ex)
+ {
+ throw new Exception($"Failed to connect to tokengen server at {_tokengenUrl}: {ex.Message}", ex);
+ }
+ catch (TaskCanceledException ex)
+ {
+ throw new Exception($"Token generation request timed out: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Generates a forward mode token for simple RDP forwarding without credential injection.
+ ///
+ /// Destination host
+ /// Application protocol (default: "rdp")
+ /// Enable recording
+ /// Token validity in seconds (default: 3600)
+ /// A JWT token string
+ public async Task GenerateForwardToken(
+ string dstHost,
+ string jetAp = "rdp",
+ bool jetRec = false,
+ int validityDuration = 3600)
+ {
+ var request = new ForwardTokenRequest
+ {
+ DstHst = dstHost,
+ JetAp = jetAp,
+ JetRec = jetRec,
+ ValidityDuration = validityDuration
+ };
+
+ try
+ {
+ var response = await _client.PostAsJsonAsync($"{_tokengenUrl}/forward", request);
+ response.EnsureSuccessStatusCode();
+
+ var result = await response.Content.ReadFromJsonAsync();
+ if (result?.Token == null)
+ {
+ throw new Exception("Token generation failed: Empty response");
+ }
+
+ return result.Token;
+ }
+ catch (HttpRequestException ex)
+ {
+ throw new Exception($"Failed to connect to tokengen server at {_tokengenUrl}: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Checks if the tokengen server is reachable.
+ ///
+ /// True if server is reachable, false otherwise
+ public async Task IsServerReachable()
+ {
+ try
+ {
+ var response = await _client.GetAsync(_tokengenUrl);
+ return response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.NotFound;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public void Dispose()
+ {
+ _client?.Dispose();
+ }
+
+ // Request/Response DTOs
+ private class RdpTlsTokenRequest
+ {
+ [JsonPropertyName("dst_hst")]
+ public string DstHst { get; set; } = string.Empty;
+
+ [JsonPropertyName("prx_usr")]
+ public string PrxUsr { get; set; } = string.Empty;
+
+ [JsonPropertyName("prx_pwd")]
+ public string PrxPwd { get; set; } = string.Empty;
+
+ [JsonPropertyName("dst_usr")]
+ public string DstUsr { get; set; } = string.Empty;
+
+ [JsonPropertyName("dst_pwd")]
+ public string DstPwd { get; set; } = string.Empty;
+
+ [JsonPropertyName("jet_aid")]
+ public string? JetAid { get; set; }
+
+ [JsonPropertyName("validity_duration")]
+ public int ValidityDuration { get; set; }
+ }
+
+ private class ForwardTokenRequest
+ {
+ [JsonPropertyName("dst_hst")]
+ public string DstHst { get; set; } = string.Empty;
+
+ [JsonPropertyName("jet_ap")]
+ public string JetAp { get; set; } = "rdp";
+
+ [JsonPropertyName("jet_rec")]
+ public bool JetRec { get; set; }
+
+ [JsonPropertyName("validity_duration")]
+ public int ValidityDuration { get; set; }
+ }
+
+ private class TokenResponse
+ {
+ [JsonPropertyName("token")]
+ public string? Token { get; set; }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj
index 1ef474af5..61014ed13 100644
--- a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj
+++ b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj
@@ -16,7 +16,7 @@
https://learn.microsoft.com/en-us/dotnet/api/system.drawing?view=net-8.0
-->
-
+
diff --git a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs
index b685455ca..3c85f2587 100644
--- a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs
+++ b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs
@@ -23,7 +23,7 @@ static async Task Main(string[] args)
try
{
- var (res, framed) = await Connection.Connect(buildConfig(serverName, username, password, domain, 1980, 1080), serverName, null);
+ var (res, framed) = await Connection.Connect(buildConfig(username, password, domain, 1980, 1080), serverName, null);
var decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(), res.GetDesktopSize().GetHeight());
var activeState = ActiveStage.New(res);
var keepLooping = true;
@@ -175,7 +175,7 @@ static void PrintHelp()
Console.WriteLine(" --help Show this message and exit.");
}
- private static Config buildConfig(string servername, string username, string password, string domain, int width, int height)
+ private static Config buildConfig(string username, string password, string domain, int width, int height)
{
ConfigBuilder configBuilder = ConfigBuilder.New();
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/CertificateChainIterator.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/CertificateChainIterator.cs
new file mode 100644
index 000000000..9830d732d
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/CertificateChainIterator.cs
@@ -0,0 +1,109 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp;
+
+#nullable enable
+
+public partial class CertificateChainIterator: IDisposable
+{
+ private unsafe Raw.CertificateChainIterator* _inner;
+
+ ///
+ /// Creates a managed CertificateChainIterator from a raw handle.
+ ///
+ ///
+ /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free).
+ ///
+ /// This constructor assumes the raw struct is allocated on Rust side.
+ /// If implemented, the custom Drop implementation on Rust side WILL run on destruction.
+ ///
+ public unsafe CertificateChainIterator(Raw.CertificateChainIterator* handle)
+ {
+ _inner = handle;
+ }
+
+ ///
+ /// A VecU8 allocated on Rust side.
+ ///
+ public VecU8? Next()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("CertificateChainIterator");
+ }
+ Raw.VecU8* retVal = Raw.CertificateChainIterator.Next(_inner);
+ if (retVal == null)
+ {
+ return null;
+ }
+ return new VecU8(retVal);
+ }
+ }
+
+ public nuint Len()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("CertificateChainIterator");
+ }
+ nuint retVal = Raw.CertificateChainIterator.Len(_inner);
+ return retVal;
+ }
+ }
+
+ public bool IsEmpty()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("CertificateChainIterator");
+ }
+ bool retVal = Raw.CertificateChainIterator.IsEmpty(_inner);
+ return retVal;
+ }
+ }
+
+ ///
+ /// Returns the underlying raw handle.
+ ///
+ public unsafe Raw.CertificateChainIterator* AsFFI()
+ {
+ return _inner;
+ }
+
+ ///
+ /// Destroys the underlying object immediately.
+ ///
+ public void Dispose()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ return;
+ }
+
+ Raw.CertificateChainIterator.Destroy(_inner);
+ _inner = null;
+
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ~CertificateChainIterator()
+ {
+ Dispose();
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs
index ec2769f3b..c0e3af52f 100644
--- a/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs
@@ -29,6 +29,13 @@ public unsafe Log(Raw.Log* handle)
_inner = handle;
}
+ ///
+ /// # Panics
+ ///
+ ///
+ /// - Panics if log directory creation fails.
+ /// - Panics if tracing initialization fails.
+ ///
public static void InitWithEnv()
{
unsafe
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathDetectionResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathDetectionResult.cs
new file mode 100644
index 000000000..644c997e6
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathDetectionResult.cs
@@ -0,0 +1,129 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp;
+
+#nullable enable
+
+public partial class RDCleanPathDetectionResult: IDisposable
+{
+ private unsafe Raw.RDCleanPathDetectionResult* _inner;
+
+ public nuint TotalLength
+ {
+ get
+ {
+ return GetTotalLength();
+ }
+ }
+
+ ///
+ /// Creates a managed RDCleanPathDetectionResult from a raw handle.
+ ///
+ ///
+ /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free).
+ ///
+ /// This constructor assumes the raw struct is allocated on Rust side.
+ /// If implemented, the custom Drop implementation on Rust side WILL run on destruction.
+ ///
+ public unsafe RDCleanPathDetectionResult(Raw.RDCleanPathDetectionResult* handle)
+ {
+ _inner = handle;
+ }
+
+ public bool IsDetected()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathDetectionResult");
+ }
+ bool retVal = Raw.RDCleanPathDetectionResult.IsDetected(_inner);
+ return retVal;
+ }
+ }
+
+ public bool IsNotEnoughBytes()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathDetectionResult");
+ }
+ bool retVal = Raw.RDCleanPathDetectionResult.IsNotEnoughBytes(_inner);
+ return retVal;
+ }
+ }
+
+ public bool IsFailed()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathDetectionResult");
+ }
+ bool retVal = Raw.RDCleanPathDetectionResult.IsFailed(_inner);
+ return retVal;
+ }
+ }
+
+ ///
+ public nuint GetTotalLength()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathDetectionResult");
+ }
+ Raw.RdcleanpathFfiResultUsizeBoxIronRdpError result = Raw.RDCleanPathDetectionResult.GetTotalLength(_inner);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ nuint retVal = result.Ok;
+ return retVal;
+ }
+ }
+
+ ///
+ /// Returns the underlying raw handle.
+ ///
+ public unsafe Raw.RDCleanPathDetectionResult* AsFFI()
+ {
+ return _inner;
+ }
+
+ ///
+ /// Destroys the underlying object immediately.
+ ///
+ public void Dispose()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ return;
+ }
+
+ Raw.RDCleanPathDetectionResult.Destroy(_inner);
+ _inner = null;
+
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ~RDCleanPathDetectionResult()
+ {
+ Dispose();
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs
new file mode 100644
index 000000000..b4c5d6278
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs
@@ -0,0 +1,424 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp;
+
+#nullable enable
+
+public partial class RDCleanPathPdu: IDisposable
+{
+ private unsafe Raw.RDCleanPathPdu* _inner;
+
+ public ushort ErrorCode
+ {
+ get
+ {
+ return GetErrorCode();
+ }
+ }
+
+ public string ErrorMessage
+ {
+ get
+ {
+ return GetErrorMessage();
+ }
+ }
+
+ public ushort HttpStatusCode
+ {
+ get
+ {
+ return GetHttpStatusCode();
+ }
+ }
+
+ public string ServerAddr
+ {
+ get
+ {
+ return GetServerAddr();
+ }
+ }
+
+ public CertificateChainIterator ServerCertChain
+ {
+ get
+ {
+ return GetServerCertChain();
+ }
+ }
+
+ public RDCleanPathResultType Type
+ {
+ get
+ {
+ return GetType();
+ }
+ }
+
+ public VecU8 X224Response
+ {
+ get
+ {
+ return GetX224Response();
+ }
+ }
+
+ ///
+ /// Creates a managed RDCleanPathPdu from a raw handle.
+ ///
+ ///
+ /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free).
+ ///
+ /// This constructor assumes the raw struct is allocated on Rust side.
+ /// If implemented, the custom Drop implementation on Rust side WILL run on destruction.
+ ///
+ public unsafe RDCleanPathPdu(Raw.RDCleanPathPdu* handle)
+ {
+ _inner = handle;
+ }
+
+ ///
+ /// Creates a new RDCleanPath request PDU
+ ///
+ ///
+ /// # Arguments
+ /// * `x224_pdu` - The X.224 Connection Request PDU bytes
+ /// * `destination` - The destination RDP server address (e.g., "10.10.0.3:3389")
+ /// * `proxy_auth` - The JWT authentication token
+ /// * `pcb` - Optional preconnection blob (for Hyper-V VM connections, empty string if not needed)
+ ///
+ ///
+ ///
+ /// A RDCleanPathPdu allocated on Rust side.
+ ///
+ public static RDCleanPathPdu NewRequest(byte[] x224Pdu, string destination, string proxyAuth, string pcb)
+ {
+ unsafe
+ {
+ byte[] destinationBuf = DiplomatUtils.StringToUtf8(destination);
+ byte[] proxyAuthBuf = DiplomatUtils.StringToUtf8(proxyAuth);
+ byte[] pcbBuf = DiplomatUtils.StringToUtf8(pcb);
+ nuint x224PduLength = (nuint)x224Pdu.Length;
+ nuint destinationBufLength = (nuint)destinationBuf.Length;
+ nuint proxyAuthBufLength = (nuint)proxyAuthBuf.Length;
+ nuint pcbBufLength = (nuint)pcbBuf.Length;
+ fixed (byte* x224PduPtr = x224Pdu)
+ {
+ fixed (byte* destinationBufPtr = destinationBuf)
+ {
+ fixed (byte* proxyAuthBufPtr = proxyAuthBuf)
+ {
+ fixed (byte* pcbBufPtr = pcbBuf)
+ {
+ Raw.RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError result = Raw.RDCleanPathPdu.NewRequest(x224PduPtr, x224PduLength, destinationBufPtr, destinationBufLength, proxyAuthBufPtr, proxyAuthBufLength, pcbBufPtr, pcbBufLength);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ Raw.RDCleanPathPdu* retVal = result.Ok;
+ return new RDCleanPathPdu(retVal);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Decodes a RDCleanPath PDU from DER-encoded bytes
+ ///
+ ///
+ ///
+ /// A RDCleanPathPdu allocated on Rust side.
+ ///
+ public static RDCleanPathPdu FromDer(byte[] bytes)
+ {
+ unsafe
+ {
+ nuint bytesLength = (nuint)bytes.Length;
+ fixed (byte* bytesPtr = bytes)
+ {
+ Raw.RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError result = Raw.RDCleanPathPdu.FromDer(bytesPtr, bytesLength);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ Raw.RDCleanPathPdu* retVal = result.Ok;
+ return new RDCleanPathPdu(retVal);
+ }
+ }
+ }
+
+ ///
+ /// Encodes the RDCleanPath PDU to DER-encoded bytes
+ ///
+ ///
+ ///
+ /// A VecU8 allocated on Rust side.
+ ///
+ public VecU8 ToDer()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RdcleanpathFfiResultBoxVecU8BoxIronRdpError result = Raw.RDCleanPathPdu.ToDer(_inner);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ Raw.VecU8* retVal = result.Ok;
+ return new VecU8(retVal);
+ }
+ }
+
+ ///
+ /// Detects if the bytes contain a valid RDCleanPath PDU and returns detection result
+ ///
+ ///
+ /// A RDCleanPathDetectionResult allocated on Rust side.
+ ///
+ public static RDCleanPathDetectionResult Detect(byte[] bytes)
+ {
+ unsafe
+ {
+ nuint bytesLength = (nuint)bytes.Length;
+ fixed (byte* bytesPtr = bytes)
+ {
+ Raw.RDCleanPathDetectionResult* retVal = Raw.RDCleanPathPdu.Detect(bytesPtr, bytesLength);
+ return new RDCleanPathDetectionResult(retVal);
+ }
+ }
+ }
+
+ ///
+ /// Gets the type of this RDCleanPath PDU
+ ///
+ ///
+ ///
+ /// A RDCleanPathResultType allocated on C# side.
+ ///
+ public RDCleanPathResultType GetType()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError result = Raw.RDCleanPathPdu.GetType(_inner);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ Raw.RDCleanPathResultType retVal = result.Ok;
+ return (RDCleanPathResultType)retVal;
+ }
+ }
+
+ ///
+ /// Gets the X.224 connection response bytes (for Response or NegotiationError variants)
+ ///
+ ///
+ ///
+ /// A VecU8 allocated on Rust side.
+ ///
+ public VecU8 GetX224Response()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RdcleanpathFfiResultBoxVecU8BoxIronRdpError result = Raw.RDCleanPathPdu.GetX224Response(_inner);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ Raw.VecU8* retVal = result.Ok;
+ return new VecU8(retVal);
+ }
+ }
+
+ ///
+ /// Gets the server certificate chain (for Response variant)
+ /// Returns a vector iterator of certificate bytes
+ ///
+ ///
+ ///
+ /// A CertificateChainIterator allocated on Rust side.
+ ///
+ public CertificateChainIterator GetServerCertChain()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError result = Raw.RDCleanPathPdu.GetServerCertChain(_inner);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ Raw.CertificateChainIterator* retVal = result.Ok;
+ return new CertificateChainIterator(retVal);
+ }
+ }
+
+ ///
+ /// Gets the server address string (for Response variant)
+ ///
+ public void GetServerAddr(DiplomatWriteable writeable)
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RDCleanPathPdu.GetServerAddr(_inner, &writeable);
+ }
+ }
+
+ ///
+ /// Gets the server address string (for Response variant)
+ ///
+ public string GetServerAddr()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ DiplomatWriteable writeable = new DiplomatWriteable();
+ Raw.RDCleanPathPdu.GetServerAddr(_inner, &writeable);
+ string retVal = writeable.ToUnicode();
+ writeable.Dispose();
+ return retVal;
+ }
+ }
+
+ ///
+ /// Gets error message (for GeneralError variant)
+ ///
+ public void GetErrorMessage(DiplomatWriteable writeable)
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RDCleanPathPdu.GetErrorMessage(_inner, &writeable);
+ }
+ }
+
+ ///
+ /// Gets error message (for GeneralError variant)
+ ///
+ public string GetErrorMessage()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ DiplomatWriteable writeable = new DiplomatWriteable();
+ Raw.RDCleanPathPdu.GetErrorMessage(_inner, &writeable);
+ string retVal = writeable.ToUnicode();
+ writeable.Dispose();
+ return retVal;
+ }
+ }
+
+ ///
+ /// Gets the error code (for GeneralError variant)
+ ///
+ ///
+ public ushort GetErrorCode()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RdcleanpathFfiResultU16BoxIronRdpError result = Raw.RDCleanPathPdu.GetErrorCode(_inner);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ ushort retVal = result.Ok;
+ return retVal;
+ }
+ }
+
+ ///
+ /// Gets the HTTP status code if present (for GeneralError variant)
+ /// Returns error if not present or not a GeneralError variant
+ ///
+ ///
+ public ushort GetHttpStatusCode()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ throw new ObjectDisposedException("RDCleanPathPdu");
+ }
+ Raw.RdcleanpathFfiResultU16BoxIronRdpError result = Raw.RDCleanPathPdu.GetHttpStatusCode(_inner);
+ if (!result.isOk)
+ {
+ throw new IronRdpException(new IronRdpError(result.Err));
+ }
+ ushort retVal = result.Ok;
+ return retVal;
+ }
+ }
+
+ ///
+ /// Returns the underlying raw handle.
+ ///
+ public unsafe Raw.RDCleanPathPdu* AsFFI()
+ {
+ return _inner;
+ }
+
+ ///
+ /// Destroys the underlying object immediately.
+ ///
+ public void Dispose()
+ {
+ unsafe
+ {
+ if (_inner == null)
+ {
+ return;
+ }
+
+ Raw.RDCleanPathPdu.Destroy(_inner);
+ _inner = null;
+
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ~RDCleanPathPdu()
+ {
+ Dispose();
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResultType.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResultType.cs
new file mode 100644
index 000000000..4145574e1
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResultType.cs
@@ -0,0 +1,20 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp;
+
+#nullable enable
+
+public enum RDCleanPathResultType
+{
+ Request = 0,
+ Response = 1,
+ GeneralError = 2,
+ NegotiationError = 3,
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCertificateChainIterator.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCertificateChainIterator.cs
new file mode 100644
index 000000000..c451aa991
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCertificateChainIterator.cs
@@ -0,0 +1,31 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct CertificateChainIterator
+{
+ private const string NativeLib = "DevolutionsIronRdp";
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_next", ExactSpelling = true)]
+ public static unsafe extern VecU8* Next(CertificateChainIterator* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_len", ExactSpelling = true)]
+ public static unsafe extern nuint Len(CertificateChainIterator* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_is_empty", ExactSpelling = true)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static unsafe extern bool IsEmpty(CertificateChainIterator* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_destroy", ExactSpelling = true)]
+ public static unsafe extern void Destroy(CertificateChainIterator* self);
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs
index e1f649b99..404827024 100644
--- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs
@@ -16,6 +16,13 @@ public partial struct Log
{
private const string NativeLib = "DevolutionsIronRdp";
+ ///
+ /// # Panics
+ ///
+ ///
+ /// - Panics if log directory creation fails.
+ /// - Panics if tracing initialization fails.
+ ///
[DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "Log_init_with_env", ExactSpelling = true)]
public static unsafe extern void InitWithEnv();
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathDetectionResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathDetectionResult.cs
new file mode 100644
index 000000000..a5f7d88d0
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathDetectionResult.cs
@@ -0,0 +1,36 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RDCleanPathDetectionResult
+{
+ private const string NativeLib = "DevolutionsIronRdp";
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_is_detected", ExactSpelling = true)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static unsafe extern bool IsDetected(RDCleanPathDetectionResult* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_is_not_enough_bytes", ExactSpelling = true)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static unsafe extern bool IsNotEnoughBytes(RDCleanPathDetectionResult* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_is_failed", ExactSpelling = true)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static unsafe extern bool IsFailed(RDCleanPathDetectionResult* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_get_total_length", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultUsizeBoxIronRdpError GetTotalLength(RDCleanPathDetectionResult* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_destroy", ExactSpelling = true)]
+ public static unsafe extern void Destroy(RDCleanPathDetectionResult* self);
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs
new file mode 100644
index 000000000..5710356a1
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs
@@ -0,0 +1,96 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RDCleanPathPdu
+{
+ private const string NativeLib = "DevolutionsIronRdp";
+
+ ///
+ /// Creates a new RDCleanPath request PDU
+ ///
+ ///
+ /// # Arguments
+ /// * `x224_pdu` - The X.224 Connection Request PDU bytes
+ /// * `destination` - The destination RDP server address (e.g., "10.10.0.3:3389")
+ /// * `proxy_auth` - The JWT authentication token
+ /// * `pcb` - Optional preconnection blob (for Hyper-V VM connections, empty string if not needed)
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_new_request", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError NewRequest(byte* x224Pdu, nuint x224PduSz, byte* destination, nuint destinationSz, byte* proxyAuth, nuint proxyAuthSz, byte* pcb, nuint pcbSz);
+
+ ///
+ /// Decodes a RDCleanPath PDU from DER-encoded bytes
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_from_der", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError FromDer(byte* bytes, nuint bytesSz);
+
+ ///
+ /// Encodes the RDCleanPath PDU to DER-encoded bytes
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_to_der", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultBoxVecU8BoxIronRdpError ToDer(RDCleanPathPdu* self);
+
+ ///
+ /// Detects if the bytes contain a valid RDCleanPath PDU and returns detection result
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_detect", ExactSpelling = true)]
+ public static unsafe extern RDCleanPathDetectionResult* Detect(byte* bytes, nuint bytesSz);
+
+ ///
+ /// Gets the type of this RDCleanPath PDU
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_type", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError GetType(RDCleanPathPdu* self);
+
+ ///
+ /// Gets the X.224 connection response bytes (for Response or NegotiationError variants)
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_x224_response", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultBoxVecU8BoxIronRdpError GetX224Response(RDCleanPathPdu* self);
+
+ ///
+ /// Gets the server certificate chain (for Response variant)
+ /// Returns a vector iterator of certificate bytes
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_server_cert_chain", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError GetServerCertChain(RDCleanPathPdu* self);
+
+ ///
+ /// Gets the server address string (for Response variant)
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_server_addr", ExactSpelling = true)]
+ public static unsafe extern void GetServerAddr(RDCleanPathPdu* self, DiplomatWriteable* writeable);
+
+ ///
+ /// Gets error message (for GeneralError variant)
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_error_message", ExactSpelling = true)]
+ public static unsafe extern void GetErrorMessage(RDCleanPathPdu* self, DiplomatWriteable* writeable);
+
+ ///
+ /// Gets the error code (for GeneralError variant)
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_error_code", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultU16BoxIronRdpError GetErrorCode(RDCleanPathPdu* self);
+
+ ///
+ /// Gets the HTTP status code if present (for GeneralError variant)
+ /// Returns error if not present or not a GeneralError variant
+ ///
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_http_status_code", ExactSpelling = true)]
+ public static unsafe extern RdcleanpathFfiResultU16BoxIronRdpError GetHttpStatusCode(RDCleanPathPdu* self);
+
+ [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_destroy", ExactSpelling = true)]
+ public static unsafe extern void Destroy(RDCleanPathPdu* self);
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResultType.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResultType.cs
new file mode 100644
index 000000000..393d65092
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResultType.cs
@@ -0,0 +1,20 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+public enum RDCleanPathResultType
+{
+ Request = 0,
+ Response = 1,
+ GeneralError = 2,
+ NegotiationError = 3,
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError.cs
new file mode 100644
index 000000000..01cb35908
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError.cs
@@ -0,0 +1,46 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError
+{
+ [StructLayout(LayoutKind.Explicit)]
+ private unsafe struct InnerUnion
+ {
+ [FieldOffset(0)]
+ internal CertificateChainIterator* ok;
+ [FieldOffset(0)]
+ internal IronRdpError* err;
+ }
+
+ private InnerUnion _inner;
+
+ [MarshalAs(UnmanagedType.U1)]
+ public bool isOk;
+
+ public unsafe CertificateChainIterator* Ok
+ {
+ get
+ {
+ return _inner.ok;
+ }
+ }
+
+ public unsafe IronRdpError* Err
+ {
+ get
+ {
+ return _inner.err;
+ }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError.cs
new file mode 100644
index 000000000..c9f33a5da
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError.cs
@@ -0,0 +1,46 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError
+{
+ [StructLayout(LayoutKind.Explicit)]
+ private unsafe struct InnerUnion
+ {
+ [FieldOffset(0)]
+ internal RDCleanPathPdu* ok;
+ [FieldOffset(0)]
+ internal IronRdpError* err;
+ }
+
+ private InnerUnion _inner;
+
+ [MarshalAs(UnmanagedType.U1)]
+ public bool isOk;
+
+ public unsafe RDCleanPathPdu* Ok
+ {
+ get
+ {
+ return _inner.ok;
+ }
+ }
+
+ public unsafe IronRdpError* Err
+ {
+ get
+ {
+ return _inner.err;
+ }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxVecU8BoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxVecU8BoxIronRdpError.cs
new file mode 100644
index 000000000..7cce2e6ad
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxVecU8BoxIronRdpError.cs
@@ -0,0 +1,46 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RdcleanpathFfiResultBoxVecU8BoxIronRdpError
+{
+ [StructLayout(LayoutKind.Explicit)]
+ private unsafe struct InnerUnion
+ {
+ [FieldOffset(0)]
+ internal VecU8* ok;
+ [FieldOffset(0)]
+ internal IronRdpError* err;
+ }
+
+ private InnerUnion _inner;
+
+ [MarshalAs(UnmanagedType.U1)]
+ public bool isOk;
+
+ public unsafe VecU8* Ok
+ {
+ get
+ {
+ return _inner.ok;
+ }
+ }
+
+ public unsafe IronRdpError* Err
+ {
+ get
+ {
+ return _inner.err;
+ }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError.cs
new file mode 100644
index 000000000..6a9c6e8a9
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError.cs
@@ -0,0 +1,46 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError
+{
+ [StructLayout(LayoutKind.Explicit)]
+ private unsafe struct InnerUnion
+ {
+ [FieldOffset(0)]
+ internal RDCleanPathResultType ok;
+ [FieldOffset(0)]
+ internal IronRdpError* err;
+ }
+
+ private InnerUnion _inner;
+
+ [MarshalAs(UnmanagedType.U1)]
+ public bool isOk;
+
+ public unsafe RDCleanPathResultType Ok
+ {
+ get
+ {
+ return _inner.ok;
+ }
+ }
+
+ public unsafe IronRdpError* Err
+ {
+ get
+ {
+ return _inner.err;
+ }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultU16BoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultU16BoxIronRdpError.cs
new file mode 100644
index 000000000..3eaf32007
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultU16BoxIronRdpError.cs
@@ -0,0 +1,46 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RdcleanpathFfiResultU16BoxIronRdpError
+{
+ [StructLayout(LayoutKind.Explicit)]
+ private unsafe struct InnerUnion
+ {
+ [FieldOffset(0)]
+ internal ushort ok;
+ [FieldOffset(0)]
+ internal IronRdpError* err;
+ }
+
+ private InnerUnion _inner;
+
+ [MarshalAs(UnmanagedType.U1)]
+ public bool isOk;
+
+ public unsafe ushort Ok
+ {
+ get
+ {
+ return _inner.ok;
+ }
+ }
+
+ public unsafe IronRdpError* Err
+ {
+ get
+ {
+ return _inner.err;
+ }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultUsizeBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultUsizeBoxIronRdpError.cs
new file mode 100644
index 000000000..eecbdaea4
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultUsizeBoxIronRdpError.cs
@@ -0,0 +1,46 @@
+// by Diplomat
+
+#pragma warning disable 0105
+using System;
+using System.Runtime.InteropServices;
+
+using Devolutions.IronRdp.Diplomat;
+#pragma warning restore 0105
+
+namespace Devolutions.IronRdp.Raw;
+
+#nullable enable
+
+[StructLayout(LayoutKind.Sequential)]
+public partial struct RdcleanpathFfiResultUsizeBoxIronRdpError
+{
+ [StructLayout(LayoutKind.Explicit)]
+ private unsafe struct InnerUnion
+ {
+ [FieldOffset(0)]
+ internal nuint ok;
+ [FieldOffset(0)]
+ internal IronRdpError* err;
+ }
+
+ private InnerUnion _inner;
+
+ [MarshalAs(UnmanagedType.U1)]
+ public bool isOk;
+
+ public unsafe nuint Ok
+ {
+ get
+ {
+ return _inner.ok;
+ }
+ }
+
+ public unsafe IronRdpError* Err
+ {
+ get
+ {
+ return _inner.err;
+ }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs
index 0a03fd6e0..497e70ab2 100644
--- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs
+++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs
@@ -11,28 +11,17 @@ public static class Connection
{
var client = await CreateTcpConnection(serverName, port);
string clientAddr = client.Client.LocalEndPoint.ToString();
- Console.WriteLine(clientAddr);
+ System.Diagnostics.Debug.WriteLine(clientAddr);
var framed = new Framed(client.GetStream());
var connector = ClientConnector.New(config, clientAddr);
- connector.WithDynamicChannelDisplayControl();
- var dvcPipeProxy = config.DvcPipeProxy;
- if (dvcPipeProxy != null)
- {
- connector.WithDynamicChannelPipeProxy(dvcPipeProxy);
- }
-
- if (factory != null)
- {
- var cliprdr = factory.BuildCliprdr();
- connector.AttachStaticCliprdr(cliprdr);
- }
+ ConnectionHelpers.SetupConnector(connector, config, factory);
await ConnectBegin(framed, connector);
var (serverPublicKey, framedSsl) = await SecurityUpgrade(framed, connector);
- var result = await ConnectFinalize(serverName, connector, serverPublicKey, framedSsl);
+ var result = await ConnectionHelpers.ConnectFinalize(serverName, connector, serverPublicKey, framedSsl);
return (result, framedSsl);
}
@@ -67,78 +56,11 @@ private static async Task ConnectBegin(Framed framed, ClientConne
}
}
-
- private static async Task ConnectFinalize(string serverName, ClientConnector connector,
- byte[] serverPubKey, Framed framedSsl)
- {
- var writeBuf2 = WriteBuf.New();
- if (connector.ShouldPerformCredssp())
- {
- await PerformCredsspSteps(connector, serverName, writeBuf2, framedSsl, serverPubKey);
- }
-
- while (!connector.GetDynState().IsTerminal())
- {
- await SingleSequenceStep(connector, writeBuf2, framedSsl);
- }
-
- ClientConnectorState state = connector.ConsumeAndCastToClientConnectorState();
-
- if (state.GetEnumType() == ClientConnectorStateType.Connected)
- {
- return state.GetConnectedResult();
- }
- else
- {
- throw new IronRdpLibException(IronRdpLibExceptionType.ConnectionFailed, "Connection failed");
- }
- }
-
- private static async Task PerformCredsspSteps(ClientConnector connector, string serverName, WriteBuf writeBuf,
- Framed framedSsl, byte[] serverpubkey)
- {
- var credsspSequenceInitResult = CredsspSequence.Init(connector, serverName, serverpubkey, null);
- var credsspSequence = credsspSequenceInitResult.GetCredsspSequence();
- var tsRequest = credsspSequenceInitResult.GetTsRequest();
- var tcpClient = new TcpClient();
- while (true)
- {
- var generator = credsspSequence.ProcessTsRequest(tsRequest);
- var clientState = await ResolveGenerator(generator, tcpClient);
- writeBuf.Clear();
- var written = credsspSequence.HandleProcessResult(clientState, writeBuf);
-
- if (written.GetSize().IsSome())
- {
- var actualSize = (int)written.GetSize().Get();
- var response = new byte[actualSize];
- writeBuf.ReadIntoBuf(response);
- await framedSsl.Write(response);
- }
-
- var pduHint = credsspSequence.NextPduHint();
- if (pduHint == null)
- {
- break;
- }
-
- var pdu = await framedSsl.ReadByHint(pduHint);
- var decoded = credsspSequence.DecodeServerMessage(pdu);
-
- // Don't remove, DecodeServerMessage is generated, and it can return null
- if (null == decoded)
- {
- break;
- }
-
- tsRequest = decoded;
- }
- }
-
- private static async Task ResolveGenerator(CredsspProcessGenerator generator, TcpClient tcpClient)
+ internal static async Task ResolveGenerator(CredsspProcessGenerator generator, TcpClient tcpClient)
{
var state = generator.Start();
NetworkStream? stream = null;
+
while (true)
{
if (state.IsSuspended())
@@ -147,16 +69,17 @@ private static async Task ResolveGenerator(CredsspProcessGenerator
var protocol = request.GetProtocol();
var url = request.GetUrl();
var data = request.GetData();
- if (null == stream)
- {
- url = url.Replace("tcp://", "");
- var split = url.Split(":");
- await tcpClient.ConnectAsync(split[0], int.Parse(split[1]));
- stream = tcpClient.GetStream();
- }
if (protocol == NetworkRequestProtocol.Tcp)
{
+ if (null == stream)
+ {
+ url = url.Replace("tcp://", "");
+ var split = url.Split(":");
+ await tcpClient.ConnectAsync(split[0], int.Parse(split[1]));
+ stream = tcpClient.GetStream();
+ }
+
stream.Write(Utils.VecU8ToByte(data));
var readBuf = new byte[8096];
var readlen = await stream.ReadAsync(readBuf, 0, readBuf.Length);
@@ -166,13 +89,29 @@ private static async Task ResolveGenerator(CredsspProcessGenerator
}
else
{
- throw new Exception("Unimplemented protocol");
+ throw new Exception($"Unimplemented protocol: {protocol}");
+ }
+ }
+ else if (state.IsCompleted())
+ {
+ try
+ {
+ var clientState = state.GetClientStateIfCompleted();
+ return clientState;
+ }
+ catch (IronRdpException ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Error getting client state: {ex.Message}");
+ System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Error kind: {ex.Inner?.Kind}");
+ System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Stack trace: {ex.StackTrace}");
+ throw;
}
}
else
{
- var clientState = state.GetClientStateIfCompleted();
- return clientState;
+ var errorMsg = $"[ResolveGenerator] Generator state is neither suspended nor completed. IsSuspended={state.IsSuspended()}, IsCompleted={state.IsCompleted()}";
+ System.Diagnostics.Debug.WriteLine(errorMsg);
+ throw new InvalidOperationException(errorMsg);
}
}
}
diff --git a/ffi/dotnet/Devolutions.IronRdp/src/ConnectionHelpers.cs b/ffi/dotnet/Devolutions.IronRdp/src/ConnectionHelpers.cs
new file mode 100644
index 000000000..d57aaaca5
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/src/ConnectionHelpers.cs
@@ -0,0 +1,121 @@
+using System.Net.Sockets;
+
+namespace Devolutions.IronRdp;
+
+///
+/// Internal helper class providing shared connection logic for both direct and RDCleanPath connections.
+///
+internal static class ConnectionHelpers
+{
+ ///
+ /// Sets up common connector configuration including dynamic channels and clipboard.
+ ///
+ internal static void SetupConnector(ClientConnector connector, Config config, CliprdrBackendFactory? factory)
+ {
+ connector.WithDynamicChannelDisplayControl();
+
+ var dvcPipeProxy = config.DvcPipeProxy;
+ if (dvcPipeProxy != null)
+ {
+ connector.WithDynamicChannelPipeProxy(dvcPipeProxy);
+ }
+
+ if (factory != null)
+ {
+ var cliprdr = factory.BuildCliprdr();
+ connector.AttachStaticCliprdr(cliprdr);
+ }
+ }
+
+ ///
+ /// Performs CredSSP authentication steps over any stream type.
+ ///
+ internal static async Task PerformCredsspSteps(
+ ClientConnector connector,
+ string serverName,
+ WriteBuf writeBuf,
+ Framed framed,
+ byte[] serverpubkey) where T : Stream
+ {
+ // Extract hostname from "hostname:port" format if needed
+ // CredSSP needs just the hostname for the service principal name (TERMSRV/hostname)
+ var hostname = serverName;
+ var colonIndex = serverName.IndexOf(':');
+ if (colonIndex > 0)
+ {
+ hostname = serverName.Substring(0, colonIndex);
+ }
+
+ var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null);
+ var credsspSequence = credsspSequenceInitResult.GetCredsspSequence();
+ var tsRequest = credsspSequenceInitResult.GetTsRequest();
+ var tcpClient = new TcpClient();
+
+ while (true)
+ {
+ var generator = credsspSequence.ProcessTsRequest(tsRequest);
+ var clientState = await Connection.ResolveGenerator(generator, tcpClient);
+ writeBuf.Clear();
+ var written = credsspSequence.HandleProcessResult(clientState, writeBuf);
+
+ if (written.GetSize().IsSome())
+ {
+ var actualSize = (int)written.GetSize().Get();
+ var response = new byte[actualSize];
+ writeBuf.ReadIntoBuf(response);
+ await framed.Write(response);
+ }
+
+ var pduHint = credsspSequence.NextPduHint();
+ if (pduHint == null)
+ {
+ break;
+ }
+
+ var pdu = await framed.ReadByHint(pduHint);
+ var decoded = credsspSequence.DecodeServerMessage(pdu);
+
+ // Don't remove, DecodeServerMessage is generated, and it can return null
+ if (null == decoded)
+ {
+ break;
+ }
+
+ tsRequest = decoded;
+ }
+ }
+
+ ///
+ /// Finalizes the RDP connection after security upgrade, performing CredSSP if needed
+ /// and completing the connection sequence.
+ ///
+ internal static async Task ConnectFinalize(
+ string serverName,
+ ClientConnector connector,
+ byte[] serverPubKey,
+ Framed framedSsl) where T : Stream
+ {
+ var writeBuf = WriteBuf.New();
+
+ if (connector.ShouldPerformCredssp())
+ {
+ await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey);
+ }
+
+ while (!connector.GetDynState().IsTerminal())
+ {
+ await Connection.SingleSequenceStep(connector, writeBuf, framedSsl);
+ }
+
+ ClientConnectorState state = connector.ConsumeAndCastToClientConnectorState();
+
+ if (state.GetEnumType() == ClientConnectorStateType.Connected)
+ {
+ return state.GetConnectedResult();
+ }
+ else
+ {
+ throw new IronRdpLibException(IronRdpLibExceptionType.ConnectionFailed, "Connection failed");
+ }
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs b/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs
index f03530f3d..2e8772e79 100644
--- a/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs
+++ b/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs
@@ -134,4 +134,45 @@ public async Task ReadByHint(PduHint pduHint)
}
}
}
+
+ ///
+ /// Reads data from the buffer based on a custom PDU hint function.
+ ///
+ /// A custom hint object implementing IPduHint interface.
+ /// An asynchronous task that represents the operation. The task result contains the read data as a byte array.
+ public async Task ReadByHint(IPduHint customHint)
+ {
+ while (true)
+ {
+ var result = customHint.FindSize(this._buffer.ToArray());
+ if (result.HasValue)
+ {
+ return await this.ReadExact((nuint)result.Value.Item2);
+ }
+ else
+ {
+ var len = await this.Read();
+ if (len == 0)
+ {
+ throw new Exception("EOF");
+ }
+ }
+ }
+ }
+}
+
+///
+/// Interface for custom PDU hint implementations.
+///
+public interface IPduHint
+{
+ ///
+ /// Finds the size of a PDU in the given byte array.
+ ///
+ /// The byte array to analyze.
+ ///
+ /// A tuple (detected, size) if PDU is detected, null if more bytes are needed.
+ /// Throws exception if invalid PDU is detected.
+ ///
+ (bool, int)? FindSize(byte[] bytes);
}
\ No newline at end of file
diff --git a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs
new file mode 100644
index 000000000..725c0525f
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs
@@ -0,0 +1,200 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Devolutions.IronRdp;
+
+///
+/// Provides methods for connecting to RDP servers through an RDCleanPath-compatible gateway
+/// (such as Devolutions Gateway or Cloudflare) using WebSocket.
+///
+public static class RDCleanPathConnection
+{
+ ///
+ /// Connects to an RDP server through an RDCleanPath-compatible gateway using WebSocket.
+ ///
+ /// The RDP connection configuration
+ /// The WebSocket URL to the RDCleanPath gateway (e.g., "ws://localhost:7171/jet/rdp")
+ /// The JWT authentication token for the RDCleanPath gateway
+ /// The destination RDP server address (e.g., "10.10.0.3:3389")
+ /// Optional preconnection blob for Hyper-V VM connections
+ /// Optional clipboard backend factory
+ /// A tuple containing the connection result and framed WebSocket stream
+ public static async Task<(ConnectionResult, Framed)> ConnectRDCleanPath(
+ Config config,
+ string gatewayUrl,
+ string authToken,
+ string destination,
+ string? pcb = null,
+ CliprdrBackendFactory? factory = null)
+ {
+ // Step 1: Connect WebSocket to gateway
+ System.Diagnostics.Debug.WriteLine($"Connecting to gateway at {gatewayUrl}...");
+ var ws = await WebSocketStream.ConnectAsync(new Uri(gatewayUrl));
+ var framed = new Framed(ws);
+
+ // Step 2: Get client local address from the WebSocket connection
+ // This mimics Rust: let client_addr = socket.local_addr()?;
+ string clientAddr = ws.ClientAddr;
+ System.Diagnostics.Debug.WriteLine($"Client local address: {clientAddr}");
+
+ // Step 3: Setup ClientConnector
+ var connector = ClientConnector.New(config, clientAddr);
+ ConnectionHelpers.SetupConnector(connector, config, factory);
+
+ // Step 4: Perform RDCleanPath handshake
+ System.Diagnostics.Debug.WriteLine("Performing RDCleanPath handshake...");
+ var (serverPublicKey, framedAfterHandshake) = await ConnectRdCleanPath(
+ framed, connector, destination, authToken, pcb ?? "");
+
+ // Step 5: Mark security upgrade as done (WebSocket already has TLS)
+ connector.MarkSecurityUpgradeAsDone();
+
+ // Step 6: Finalize connection
+ System.Diagnostics.Debug.WriteLine("Finalizing RDP connection...");
+ var result = await ConnectionHelpers.ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake);
+
+ System.Diagnostics.Debug.WriteLine("Gateway connection established successfully!");
+ return (result, framedAfterHandshake);
+ }
+
+ ///
+ /// Performs the RDCleanPath handshake with the RDCleanPath-compatible gateway.
+ ///
+ private static async Task<(byte[], Framed)> ConnectRdCleanPath(
+ Framed framed,
+ ClientConnector connector,
+ string destination,
+ string authToken,
+ string pcb)
+ {
+ var writeBuf = WriteBuf.New();
+
+ // Step 1: Generate X.224 Connection Request
+ System.Diagnostics.Debug.WriteLine("Generating X.224 Connection Request...");
+ var written = connector.StepNoInput(writeBuf);
+ var x224PduSize = (int)written.GetSize().Get();
+ var x224Pdu = new byte[x224PduSize];
+ writeBuf.ReadIntoBuf(x224Pdu);
+
+ // Step 2: Create and send RDCleanPath Request
+ System.Diagnostics.Debug.WriteLine($"Sending RDCleanPath request to {destination}...");
+ var rdCleanPathReq = RDCleanPathPdu.NewRequest(x224Pdu, destination, authToken, pcb);
+ var reqBytes = rdCleanPathReq.ToDer();
+ var reqBytesArray = new byte[reqBytes.GetSize()];
+ reqBytes.Fill(reqBytesArray);
+ await framed.Write(reqBytesArray);
+
+ // Step 3: Read RDCleanPath Response
+ System.Diagnostics.Debug.WriteLine("Waiting for RDCleanPath response...");
+ var respBytes = await framed.ReadByHint(new RDCleanPathHint());
+ var rdCleanPathResp = RDCleanPathPdu.FromDer(respBytes);
+
+ // Step 4: Determine response type and handle accordingly
+ var resultType = rdCleanPathResp.GetType();
+
+ if (resultType == RDCleanPathResultType.Response)
+ {
+ System.Diagnostics.Debug.WriteLine("RDCleanPath handshake successful!");
+
+ // Extract X.224 response
+ var x224Response = rdCleanPathResp.GetX224Response();
+ var x224ResponseBytes = new byte[x224Response.GetSize()];
+ x224Response.Fill(x224ResponseBytes);
+
+ // Process X.224 response with connector
+ writeBuf.Clear();
+ connector.Step(x224ResponseBytes, writeBuf);
+
+ // Extract server public key from certificate chain
+ var certChain = rdCleanPathResp.GetServerCertChain();
+ if (certChain.IsEmpty())
+ {
+ throw new IronRdpLibException(
+ IronRdpLibExceptionType.ConnectionFailed,
+ "Server certificate chain is empty");
+ }
+
+ var firstCert = certChain.Next();
+ if (firstCert == null)
+ {
+ throw new IronRdpLibException(
+ IronRdpLibExceptionType.ConnectionFailed,
+ "Failed to get first certificate from chain");
+ }
+
+ var certBytes = new byte[firstCert.GetSize()];
+ firstCert.Fill(certBytes);
+
+ var serverPublicKey = ExtractPublicKeyFromX509(certBytes);
+
+ System.Diagnostics.Debug.WriteLine($"Extracted server public key (length: {serverPublicKey.Length})");
+
+ return (serverPublicKey, framed);
+ }
+ else if (resultType == RDCleanPathResultType.GeneralError)
+ {
+ var errorCode = rdCleanPathResp.GetErrorCode();
+ var errorMessage = rdCleanPathResp.GetErrorMessage();
+ throw new IronRdpLibException(
+ IronRdpLibExceptionType.ConnectionFailed,
+ $"RDCleanPath error (code {errorCode}): {errorMessage}");
+ }
+ else if (resultType == RDCleanPathResultType.NegotiationError)
+ {
+ throw new IronRdpLibException(
+ IronRdpLibExceptionType.ConnectionFailed,
+ "RDCleanPath negotiation error: Server rejected connection parameters");
+ }
+ else
+ {
+ throw new IronRdpLibException(
+ IronRdpLibExceptionType.ConnectionFailed,
+ $"Unexpected RDCleanPath response type: {resultType}");
+ }
+ }
+
+ ///
+ /// Extracts the public key from an X.509 certificate in DER format.
+ ///
+ private static byte[] ExtractPublicKeyFromX509(byte[] certDer)
+ {
+ try
+ {
+ var cert = new X509Certificate2(certDer);
+ return cert.GetPublicKey();
+ }
+ catch (Exception ex)
+ {
+ throw new IronRdpLibException(
+ IronRdpLibExceptionType.ConnectionFailed,
+ $"Failed to extract public key from certificate: {ex.Message}");
+ }
+ }
+}
+
+///
+/// PDU hint for detecting RDCleanPath PDUs in the stream.
+///
+public class RDCleanPathHint : IPduHint
+{
+ public (bool, int)? FindSize(byte[] bytes)
+ {
+ var detection = RDCleanPathPdu.Detect(bytes);
+
+ if (detection.IsDetected())
+ {
+ var totalLength = (int)detection.GetTotalLength();
+ return (true, totalLength);
+ }
+
+ if (detection.IsNotEnoughBytes())
+ {
+ return null; // Need more bytes
+ }
+
+ // Detection failed
+ throw new IronRdpLibException(
+ IronRdpLibExceptionType.ConnectionFailed,
+ "Invalid RDCleanPath PDU detected");
+ }
+}
diff --git a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs
new file mode 100644
index 000000000..5f5281011
--- /dev/null
+++ b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Buffers;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+public sealed class WebSocketStream : Stream
+{
+ private readonly ClientWebSocket _ws;
+ private readonly byte[] _recvBuf;
+ private int _recvPos;
+ private int _recvLen;
+ private bool _remoteClosed;
+ private bool _disposed;
+ private readonly string _clientAddr;
+
+ private const int DefaultRecvBufferSize = 64 * 1024;
+ private const int MaxSendFrame = 16 * 1024; // send in chunks
+
+ private WebSocketStream(ClientWebSocket ws, int receiveBufferSize, string clientAddr)
+ {
+ _ws = ws ?? throw new ArgumentNullException(nameof(ws));
+ _recvBuf = ArrayPool.Shared.Rent(Math.Max(1024, receiveBufferSize));
+ _clientAddr = clientAddr;
+ }
+
+ public static async Task ConnectAsync(
+ Uri uri,
+ ClientWebSocket? ws = null,
+ int receiveBufferSize = DefaultRecvBufferSize,
+ CancellationToken ct = default)
+ {
+ // Capture the local endpoint from the socket using SocketsHttpHandler.ConnectCallback
+ // This follows the Rust approach: socket.local_addr()
+ IPEndPoint? localEndPoint = null;
+
+ var handler = new SocketsHttpHandler
+ {
+ ConnectCallback = async (context, cancellationToken) =>
+ {
+ var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
+
+ // Set TCP_NODELAY (matching Rust: socket.set_nodelay(true))
+ socket.NoDelay = true;
+
+ // Connect to the endpoint
+ await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
+
+ // Capture the local endpoint after connection
+ localEndPoint = socket.LocalEndPoint as IPEndPoint;
+
+ return new NetworkStream(socket, ownsSocket: true);
+ }
+ };
+
+ var invoker = new HttpMessageInvoker(handler);
+
+ ws ??= new ClientWebSocket();
+ await ws.ConnectAsync(uri, invoker, ct).ConfigureAwait(false);
+
+ string clientAddr = localEndPoint?.ToString() ?? "127.0.0.1:0";
+
+ return new WebSocketStream(ws, receiveBufferSize, clientAddr);
+ }
+
+ public ClientWebSocket Socket => _ws;
+
+ ///
+ /// Gets the local client address in "IP:port" format.
+ /// This is the address that was determined when establishing the TCP connection.
+ ///
+ public string ClientAddr => _clientAddr;
+
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => true;
+ public override long Length => throw new NotSupportedException();
+ public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
+
+ public override void Flush() { /* no-op */ }
+ public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public override int Read(byte[] buffer, int offset, int count) =>
+ ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult();
+
+ public override void Write(byte[] buffer, int offset, int count) =>
+ WriteAsync(buffer.AsMemory(offset, count)).GetAwaiter().GetResult();
+
+ public override async ValueTask ReadAsync(
+ Memory destination, CancellationToken cancellationToken = default)
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(WebSocketStream));
+ if (_remoteClosed) return 0;
+ if (destination.Length == 0) return 0;
+
+ // Fill local buffer if empty
+ if (_recvLen == 0)
+ {
+ var mem = _recvBuf.AsMemory();
+ while (true)
+ {
+ var result = await _ws.ReceiveAsync(mem, cancellationToken).ConfigureAwait(false);
+
+ // Close frame → signal EOF
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ _remoteClosed = true;
+ try { await _ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "OK", cancellationToken).ConfigureAwait(false); }
+ catch { /* ignore */ }
+ return 0;
+ }
+
+ if (result.MessageType == WebSocketMessageType.Text)
+ throw new InvalidOperationException("Received TEXT frame; this stream expects BINARY.");
+
+ // Some data arrived
+ if (result.Count > 0)
+ {
+ _recvPos = 0;
+ _recvLen = result.Count;
+ break;
+ }
+
+ // Keep looping if Count == 0 (can happen with pings/keepers)
+ }
+ }
+
+ var toCopy = Math.Min(destination.Length, _recvLen);
+ new ReadOnlySpan(_recvBuf, _recvPos, toCopy).CopyTo(destination.Span);
+ _recvPos += toCopy;
+ _recvLen -= toCopy;
+
+ // If we've drained local buffer, try to prefetch next chunk (non-blocking behavior not guaranteed)
+ if (_recvLen == 0 && _ws.State == WebSocketState.Open)
+ {
+ // optional prefetch: not strictly necessary—kept simple
+ }
+
+ return toCopy;
+ }
+
+ public override async Task WriteAsync(
+ byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => await WriteAsync(buffer.AsMemory(offset, count), cancellationToken);
+
+ public override async ValueTask WriteAsync(
+ ReadOnlyMemory source, CancellationToken cancellationToken = default)
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(WebSocketStream));
+ if (_ws.State != WebSocketState.Open) throw new IOException("WebSocket is not open.");
+
+ // Treat each Write* as one complete WebSocket message (Binary).
+ // Chunk large payloads as continuation frames and set EndOfMessage on the last chunk.
+ int sent = 0;
+ while (sent < source.Length)
+ {
+ var chunkLen = Math.Min(MaxSendFrame, source.Length - sent);
+ var chunk = source.Slice(sent, chunkLen);
+ sent += chunkLen;
+
+ bool end = (sent == source.Length);
+ await _ws.SendAsync(chunk, WebSocketMessageType.Binary, end, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+ public override void SetLength(long value) => throw new NotSupportedException();
+
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed) return;
+ if (disposing)
+ {
+ try
+ {
+ if (_ws.State == WebSocketState.Open)
+ {
+ _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposing", CancellationToken.None)
+ .GetAwaiter().GetResult();
+ }
+ }
+ catch { /* ignore on dispose */ }
+ _ws.Dispose();
+ ArrayPool.Shared.Return(_recvBuf);
+ }
+ _disposed = true;
+ base.Dispose(disposing);
+ }
+
+#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER
+ public override async ValueTask DisposeAsync()
+ {
+ if (!_disposed)
+ {
+ try
+ {
+ if (_ws.State == WebSocketState.Open)
+ await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposing", CancellationToken.None).ConfigureAwait(false);
+ }
+ catch { /* ignore */ }
+ _ws.Dispose();
+ ArrayPool.Shared.Return(_recvBuf);
+ _disposed = true;
+ }
+ await base.DisposeAsync().ConfigureAwait(false);
+ }
+#endif
+}
diff --git a/ffi/src/clipboard/mod.rs b/ffi/src/clipboard/mod.rs
index e50aa1fec..386e7b855 100644
--- a/ffi/src/clipboard/mod.rs
+++ b/ffi/src/clipboard/mod.rs
@@ -34,8 +34,8 @@ pub struct FfiClipbarodMessageProxy {
impl ironrdp::cliprdr::backend::ClipboardMessageProxy for FfiClipbarodMessageProxy {
fn send_clipboard_message(&self, message: ironrdp::cliprdr::backend::ClipboardMessage) {
- if let Err(err) = self.sender.send(message) {
- error!("Failed to send clipboard message: {:?}", err);
+ if let Err(error) = self.sender.send(message) {
+ error!(?error, "Failed to send clipboard message");
}
}
}
diff --git a/ffi/src/connector/mod.rs b/ffi/src/connector/mod.rs
index e9226467c..332177af9 100644
--- a/ffi/src/connector/mod.rs
+++ b/ffi/src/connector/mod.rs
@@ -97,7 +97,7 @@ pub mod ffi {
pub fn with_dynamic_channel_display_control(&mut self) -> Result<(), Box> {
self.with_dvc(DisplayControlClient::new(|c| {
- info!(DisplayCountrolCapabilities = ?c, "DisplayControl capabilities received");
+ info!(display_control_capabilities = ?c, "DisplayControl capabilities received");
Ok(Vec::new())
}))
}
@@ -185,7 +185,7 @@ pub mod ffi {
let Some(connector) = self.0.as_ref() else {
return Err(ValueConsumedError::for_item("connector").into());
};
- tracing::trace!(pduhint=?connector.next_pdu_hint(), "Reading next PDU hint");
+ tracing::trace!(pdu_hint=?connector.next_pdu_hint(), "Reading next PDU hint");
Ok(connector.next_pdu_hint().map(PduHint).map(Box::new))
}
diff --git a/ffi/src/error.rs b/ffi/src/error.rs
index 9312ba45f..d21c3893e 100644
--- a/ffi/src/error.rs
+++ b/ffi/src/error.rs
@@ -6,6 +6,7 @@ use ironrdp::connector::ConnectorError;
use ironrdp::session::SessionError;
#[cfg(target_os = "windows")]
use ironrdp_cliprdr_native::WinCliprdrError;
+use ironrdp_rdcleanpath::der;
use self::ffi::IronRdpErrorKind;
@@ -93,6 +94,32 @@ impl From for IronRdpErrorKind {
}
}
+impl From for IronRdpErrorKind {
+ fn from(_val: der::Error) -> Self {
+ IronRdpErrorKind::DecodeError
+ }
+}
+
+impl From for IronRdpErrorKind {
+ fn from(_val: ironrdp_rdcleanpath::MissingRDCleanPathField) -> Self {
+ IronRdpErrorKind::Generic
+ }
+}
+
+pub struct GenericError(pub anyhow::Error);
+
+impl Display for GenericError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ write!(f, "{:#}", self.0)
+ }
+}
+
+impl From for IronRdpErrorKind {
+ fn from(_val: GenericError) -> Self {
+ IronRdpErrorKind::Generic
+ }
+}
+
impl From for Box
where
T: Into + ToString,
@@ -221,11 +248,7 @@ impl IncorrectEnumTypeErrorBuilder {
impl Display for IncorrectEnumTypeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- write!(
- f,
- "expected enum variable {}, of enum {}",
- self.expected, self.enum_name
- )
+ write!(f, "expected enum variable {} of enum {}", self.expected, self.enum_name)
}
}
diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs
index 407847ce4..44009cd5a 100644
--- a/ffi/src/lib.rs
+++ b/ffi/src/lib.rs
@@ -11,6 +11,7 @@ pub mod graphics;
pub mod input;
pub mod log;
pub mod pdu;
+pub mod rdcleanpath;
pub mod session;
pub mod svc;
pub mod utils;
diff --git a/ffi/src/log.rs b/ffi/src/log.rs
index 7f7750a27..f050b1503 100644
--- a/ffi/src/log.rs
+++ b/ffi/src/log.rs
@@ -23,7 +23,7 @@ pub mod ffi {
INIT_LOG.call_once(|| {
let log_file = std::env::var(IRONRDP_LOG_PATH).ok();
let log_file = log_file.as_deref();
- setup_logging(log_file).expect("Failed to setup logging");
+ setup_logging(log_file).expect("failed to setup logging");
});
}
}
diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs
new file mode 100644
index 000000000..a842c747b
--- /dev/null
+++ b/ffi/src/rdcleanpath.rs
@@ -0,0 +1,268 @@
+#[diplomat::bridge]
+pub mod ffi {
+ use core::fmt::Write as _;
+
+ use anyhow::Context as _;
+ use diplomat_runtime::DiplomatWriteable;
+
+ use crate::error::ffi::IronRdpError;
+ use crate::error::GenericError;
+ use crate::utils::ffi::VecU8;
+
+ #[diplomat::opaque]
+ pub struct RDCleanPathPdu(pub ironrdp_rdcleanpath::RDCleanPathPdu);
+
+ impl RDCleanPathPdu {
+ /// Creates a new RDCleanPath request PDU
+ ///
+ /// # Arguments
+ /// * `x224_pdu` - The X.224 Connection Request PDU bytes
+ /// * `destination` - The destination RDP server address (e.g., "10.10.0.3:3389")
+ /// * `proxy_auth` - The JWT authentication token
+ /// * `pcb` - Optional preconnection blob (for Hyper-V VM connections, empty string if not needed)
+ pub fn new_request(
+ x224_pdu: &[u8],
+ destination: &str,
+ proxy_auth: &str,
+ pcb: &str,
+ ) -> Result, Box> {
+ let pcb_opt = if pcb.is_empty() { None } else { Some(pcb.to_owned()) };
+
+ let pdu = ironrdp_rdcleanpath::RDCleanPathPdu::new_request(
+ x224_pdu.to_vec(),
+ destination.to_owned(),
+ proxy_auth.to_owned(),
+ pcb_opt,
+ )
+ .context("failed to create RDCleanPath request")
+ .map_err(GenericError)?;
+
+ Ok(Box::new(RDCleanPathPdu(pdu)))
+ }
+
+ /// Decodes a RDCleanPath PDU from DER-encoded bytes
+ pub fn from_der(bytes: &[u8]) -> Result, Box> {
+ let pdu = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(bytes)
+ .context("failed to decode RDCleanPath PDU")
+ .map_err(GenericError)?;
+
+ Ok(Box::new(RDCleanPathPdu(pdu)))
+ }
+
+ /// Encodes the RDCleanPath PDU to DER-encoded bytes
+ pub fn to_der(&self) -> Result, Box> {
+ let bytes = self
+ .0
+ .to_der()
+ .context("failed to encode RDCleanPath PDU")
+ .map_err(GenericError)?;
+
+ Ok(Box::new(VecU8(bytes)))
+ }
+
+ /// Detects if the bytes contain a valid RDCleanPath PDU and returns detection result
+ pub fn detect(bytes: &[u8]) -> Box {
+ let result = ironrdp_rdcleanpath::RDCleanPathPdu::detect(bytes);
+ Box::new(RDCleanPathDetectionResult(result))
+ }
+
+ /// Gets the type of this RDCleanPath PDU
+ pub fn get_type(&self) -> Result> {
+ if self.0.destination.is_some() {
+ if self.0.proxy_auth.is_none() {
+ return Err(Self::missing_field("proxy_auth"));
+ }
+
+ if self.0.x224_connection_pdu.is_none() {
+ return Err(Self::missing_field("x224_connection_pdu"));
+ }
+
+ Ok(RDCleanPathResultType::Request)
+ } else if self.0.server_addr.is_some() {
+ if self.0.x224_connection_pdu.is_none() {
+ return Err(Self::missing_field("x224_connection_pdu"));
+ }
+
+ if self.0.server_cert_chain.is_none() {
+ return Err(Self::missing_field("server_cert_chain"));
+ }
+
+ Ok(RDCleanPathResultType::Response)
+ } else if let Some(error) = &self.0.error {
+ if error.error_code == ironrdp_rdcleanpath::NEGOTIATION_ERROR_CODE {
+ if self.0.x224_connection_pdu.is_none() {
+ return Err(Self::missing_field("x224_connection_pdu"));
+ }
+
+ Ok(RDCleanPathResultType::NegotiationError)
+ } else {
+ Ok(RDCleanPathResultType::GeneralError)
+ }
+ } else {
+ Err(Self::missing_field("error"))
+ }
+ }
+
+ /// Gets the X.224 connection response bytes (for Response or NegotiationError variants)
+ pub fn get_x224_response(&self) -> Result, Box> {
+ if self.0.server_addr.is_some() {
+ let x224 = self
+ .0
+ .x224_connection_pdu
+ .as_ref()
+ .ok_or_else(|| Self::missing_field("x224_connection_pdu"))?;
+ self.0
+ .server_cert_chain
+ .as_ref()
+ .ok_or_else(|| Self::missing_field("server_cert_chain"))?;
+
+ Ok(Box::new(VecU8(x224.as_bytes().to_vec())))
+ } else if let Some(error) = &self.0.error {
+ if error.error_code == ironrdp_rdcleanpath::NEGOTIATION_ERROR_CODE {
+ let x224 = self
+ .0
+ .x224_connection_pdu
+ .as_ref()
+ .ok_or_else(|| Self::missing_field("x224_connection_pdu"))?;
+
+ Ok(Box::new(VecU8(x224.as_bytes().to_vec())))
+ } else {
+ Err(GenericError(anyhow::anyhow!("RDCleanPath variant does not contain X.224 response")).into())
+ }
+ } else {
+ Err(GenericError(anyhow::anyhow!("RDCleanPath variant does not contain X.224 response")).into())
+ }
+ }
+
+ /// Gets the server certificate chain (for Response variant)
+ /// Returns a vector iterator of certificate bytes
+ pub fn get_server_cert_chain(&self) -> Result, Box> {
+ if self.0.server_addr.is_some() {
+ self.0
+ .x224_connection_pdu
+ .as_ref()
+ .ok_or_else(|| Self::missing_field("x224_connection_pdu"))?;
+ let certs = self
+ .0
+ .server_cert_chain
+ .as_ref()
+ .ok_or_else(|| Self::missing_field("server_cert_chain"))?;
+
+ let certs: Vec> = certs.iter().map(|cert| cert.as_bytes().to_vec()).collect();
+ Ok(Box::new(CertificateChainIterator { certs, index: 0 }))
+ } else {
+ Err(GenericError(anyhow::anyhow!(
+ "RDCleanPath variant does not contain certificate chain"
+ ))
+ .into())
+ }
+ }
+
+ /// Gets the server address string (for Response variant)
+ pub fn get_server_addr<'a>(&'a self, writeable: &'a mut DiplomatWriteable) {
+ if self.0.server_addr.is_some()
+ && self.0.server_cert_chain.is_some()
+ && self.0.x224_connection_pdu.is_some()
+ {
+ if let Some(server_addr) = &self.0.server_addr {
+ let _ = write!(writeable, "{server_addr}");
+ }
+ }
+ }
+
+ /// Gets error message (for GeneralError variant)
+ pub fn get_error_message<'a>(&'a self, writeable: &'a mut DiplomatWriteable) {
+ if let Ok(err) = self.general_error() {
+ let _ = write!(writeable, "{err}");
+ }
+ }
+
+ /// Gets the error code (for GeneralError variant)
+ pub fn get_error_code(&self) -> Result> {
+ let err = self.general_error()?;
+ Ok(err.error_code)
+ }
+
+ /// Gets the HTTP status code if present (for GeneralError variant)
+ /// Returns error if not present or not a GeneralError variant
+ pub fn get_http_status_code(&self) -> Result> {
+ let err = self.general_error()?;
+
+ err.http_status_code
+ .ok_or_else(|| GenericError(anyhow::anyhow!("HTTP status code not present")).into())
+ }
+
+ fn missing_field(field: &'static str) -> Box {
+ GenericError(anyhow::anyhow!("RDCleanPath is missing {field} field")).into()
+ }
+
+ fn general_error(&self) -> Result<&ironrdp_rdcleanpath::RDCleanPathErr, Box> {
+ let error = self.0.error.as_ref().ok_or_else(|| Self::missing_field("error"))?;
+
+ if error.error_code == ironrdp_rdcleanpath::NEGOTIATION_ERROR_CODE {
+ Err(GenericError(anyhow::anyhow!("not a GeneralError variant")).into())
+ } else {
+ Ok(error)
+ }
+ }
+ }
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ pub enum RDCleanPathResultType {
+ Request,
+ Response,
+ GeneralError,
+ NegotiationError,
+ }
+
+ #[diplomat::opaque]
+ pub struct RDCleanPathDetectionResult(pub ironrdp_rdcleanpath::DetectionResult);
+
+ impl RDCleanPathDetectionResult {
+ pub fn is_detected(&self) -> bool {
+ matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Detected { .. })
+ }
+
+ pub fn is_not_enough_bytes(&self) -> bool {
+ matches!(self.0, ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes)
+ }
+
+ pub fn is_failed(&self) -> bool {
+ matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Failed)
+ }
+
+ pub fn get_total_length(&self) -> Result> {
+ if let ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } = self.0 {
+ Ok(total_length)
+ } else {
+ Err(GenericError(anyhow::anyhow!("detection result is not Detected variant")).into())
+ }
+ }
+ }
+
+ #[diplomat::opaque]
+ pub struct CertificateChainIterator {
+ certs: Vec>,
+ index: usize,
+ }
+
+ impl CertificateChainIterator {
+ pub fn next(&mut self) -> Option> {
+ if self.index < self.certs.len() {
+ let cert = self.certs[self.index].clone();
+ self.index += 1;
+ Some(Box::new(VecU8(cert)))
+ } else {
+ None
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ self.certs.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.certs.is_empty()
+ }
+ }
+}