Skip to content

Commit d3e0cb1

Browse files
feat(ffi): expose RDCleanPath (#1014)
Add RDCleanPath support for Devolutions.IronRDP .NET package
1 parent 2cedc05 commit d3e0cb1

34 files changed

+2385
-127
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ffi/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ ironrdp = { path = "../crates/ironrdp", features = ["session", "connector", "dvc
1818
ironrdp-cliprdr-native.path = "../crates/ironrdp-cliprdr-native"
1919
ironrdp-dvc-pipe-proxy.path = "../crates/ironrdp-dvc-pipe-proxy"
2020
ironrdp-core = { path = "../crates/ironrdp-core", features = ["alloc"] }
21+
ironrdp-rdcleanpath.path = "../crates/ironrdp-rdcleanpath"
2122
sspi = { version = "0.16", features = ["network_client"] }
2223
thiserror = "2"
2324
tracing = { version = "0.1", features = ["log"] }
2425
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
26+
anyhow = "1.0"
2527

2628
[target.'cfg(windows)'.build-dependencies]
2729
embed-resource = "3.0"

ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Avalonia" Version="11.0.10" />
14-
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
15-
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
16-
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
13+
<PackageReference Include="Avalonia" Version="11.3.7" />
14+
<PackageReference Include="Avalonia.Desktop" Version="11.3.7" />
15+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.7" />
16+
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.7" />
1717
<!--Condition
1818
below is needed to remove Avalonia.Diagnostics package from build output in Release
1919
configuration.-->
20-
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
20+
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.7" />
2121
<ProjectReference Include="../Devolutions.IronRdp/Devolutions.IronRdp.csproj" />
2222
</ItemGroup>
2323

2424
<ItemGroup Condition="'$([System.OperatingSystem]::IsWindows())'">
25-
<PackageReference Include="Avalonia.Win32" Version="11.0.10" />
25+
<PackageReference Include="Avalonia.Win32" Version="11.3.7" />
2626
</ItemGroup>
2727
</Project>

ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs

Lines changed: 117 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System;
88
using System.ComponentModel;
99
using System.Diagnostics;
10+
using System.IO;
1011
using System.Net.Security;
1112
using System.Runtime.CompilerServices;
1213
using System.Runtime.InteropServices;
@@ -22,7 +23,7 @@ public partial class MainWindow : Window
2223
readonly InputDatabase? _inputDatabase = InputDatabase.New();
2324
ActiveStage? _activeStage;
2425
DecodedImage? _decodedImage;
25-
Framed<SslStream>? _framed;
26+
Framed<Stream>? _framed;
2627
WinCliprdr? _cliprdr;
2728
private readonly RendererModel _renderModel;
2829
private Image? _imageControl;
@@ -77,18 +78,49 @@ private void OnOpened(object? sender, EventArgs e)
7778

7879
var username = Environment.GetEnvironmentVariable("IRONRDP_USERNAME");
7980
var password = Environment.GetEnvironmentVariable("IRONRDP_PASSWORD");
80-
var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN");
81+
var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN"); // Optional
8182
var server = Environment.GetEnvironmentVariable("IRONRDP_SERVER");
83+
var portEnv = Environment.GetEnvironmentVariable("IRONRDP_PORT"); // Optional
8284

83-
if (username == null || password == null || domain == null || server == null)
85+
// Gateway configuration (optional)
86+
var gatewayUrl = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_URL");
87+
var gatewayToken = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_TOKEN");
88+
var tokengenUrl = Environment.GetEnvironmentVariable("IRONRDP_TOKENGEN_URL");
89+
90+
if (username == null || password == null || server == null)
8491
{
8592
var errorMessage =
86-
"Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, IRONRDP_DOMAIN, and RONRDP_SERVER environment variables";
93+
"Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, and IRONRDP_SERVER environment variables";
8794
Trace.TraceError(errorMessage);
8895
Close();
8996
throw new InvalidProgramException(errorMessage);
9097
}
9198

99+
// Validate server is only domain or IP (no port allowed)
100+
// i.e. "example.com" or "10.10.0.3" the port should go to the dedicated env var IRONRDP_PORT
101+
if (server.Contains(':'))
102+
{
103+
var errorMessage = $"IRONRDP_SERVER must be a domain or IP address only, not '{server}'. Use IRONRDP_PORT for the port.";
104+
Trace.TraceError(errorMessage);
105+
Close();
106+
throw new InvalidProgramException(errorMessage);
107+
}
108+
109+
// Parse port from environment variable or use default
110+
int port = 3389;
111+
if (!string.IsNullOrEmpty(portEnv))
112+
{
113+
if (!int.TryParse(portEnv, out port) || port <= 0 || port > 65535)
114+
{
115+
var errorMessage = $"IRONRDP_PORT must be a valid port number (1-65535), got '{portEnv}'";
116+
Trace.TraceError(errorMessage);
117+
Close();
118+
throw new InvalidProgramException(errorMessage);
119+
}
120+
}
121+
122+
Trace.TraceInformation($"Target server: {server}:{port}");
123+
92124
var config = BuildConfig(username, password, domain, _renderModel.Width, _renderModel.Height);
93125

94126
CliprdrBackendFactory? factory = null;
@@ -106,15 +138,81 @@ private void OnOpened(object? sender, EventArgs e)
106138
BeforeConnectSetup();
107139
Task.Run(async () =>
108140
{
109-
var (res, framed) = await Connection.Connect(config, server, factory);
110-
this._decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(),
111-
res.GetDesktopSize().GetHeight());
112-
this._activeStage = ActiveStage.New(res);
113-
this._framed = framed;
114-
ReadPduAndProcessActiveStage();
115-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
141+
try
116142
{
117-
HandleClipboardEvents();
143+
ConnectionResult res;
144+
145+
// Determine connection mode: Gateway or Direct
146+
if (!string.IsNullOrEmpty(gatewayUrl))
147+
{
148+
Trace.TraceInformation("=== GATEWAY MODE ===");
149+
Trace.TraceInformation($"Gateway URL: {gatewayUrl}");
150+
Trace.TraceInformation($"Destination: {server}:{port}");
151+
152+
var tokenGen = new TokenGenerator(tokengenUrl ?? "http://localhost:8080");
153+
154+
// Generate RDP token if not provided
155+
if (string.IsNullOrEmpty(gatewayToken))
156+
{
157+
Trace.TraceInformation("No RDP token provided, generating token...");
158+
159+
try
160+
{
161+
gatewayToken = await tokenGen.GenerateRdpTlsToken(
162+
dstHost: server!,
163+
proxyUser: string.IsNullOrEmpty(domain) ? username : $"{username}@{domain}",
164+
proxyPassword: password!,
165+
destUser: username!,
166+
destPassword: password!
167+
);
168+
Trace.TraceInformation($"RDP token generated successfully (length: {gatewayToken.Length})");
169+
}
170+
catch (Exception ex)
171+
{
172+
Trace.TraceError($"Failed to generate RDP token: {ex.Message}");
173+
Trace.TraceInformation("Make sure tokengen server is running:");
174+
Trace.TraceInformation($" cargo run --manifest-path tools/tokengen/Cargo.toml -- server");
175+
throw;
176+
}
177+
}
178+
179+
// Connect via gateway - destination needs "hostname:port" format for RDCleanPath
180+
string destination = $"{server}:{port}";
181+
182+
var (gatewayRes, gatewayFramed) = await RDCleanPathConnection.ConnectRDCleanPath(
183+
config, gatewayUrl, gatewayToken!, destination, null, factory);
184+
res = gatewayRes;
185+
this._framed = new Framed<Stream>(gatewayFramed.GetInner().Item1);
186+
187+
Trace.TraceInformation("=== GATEWAY CONNECTION SUCCESSFUL ===");
188+
}
189+
else
190+
{
191+
Trace.TraceInformation("=== DIRECT MODE ===");
192+
193+
// Direct connection (original behavior)
194+
var (directRes, directFramed) = await Connection.Connect(config, server, factory, port);
195+
res = directRes;
196+
this._framed = new Framed<Stream>(directFramed.GetInner().Item1);
197+
198+
Trace.TraceInformation("=== DIRECT CONNECTION SUCCESSFUL ===");
199+
}
200+
201+
this._decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(),
202+
res.GetDesktopSize().GetHeight());
203+
this._activeStage = ActiveStage.New(res);
204+
ReadPduAndProcessActiveStage();
205+
206+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
207+
{
208+
HandleClipboardEvents();
209+
}
210+
}
211+
catch (Exception ex)
212+
{
213+
Trace.TraceError($"Connection failed: {ex.Message}");
214+
Trace.TraceError($"Stack trace: {ex.StackTrace}");
215+
throw;
118216
}
119217
});
120218
}
@@ -260,12 +358,16 @@ private void HandleClipboardEvents()
260358
});
261359
}
262360

263-
private static Config BuildConfig(string username, string password, string domain, int width, int height)
361+
private static Config BuildConfig(string username, string password, string? domain, int width, int height)
264362
{
265363
ConfigBuilder configBuilder = ConfigBuilder.New();
266364

267365
configBuilder.WithUsernameAndPassword(username, password);
268-
configBuilder.SetDomain(domain);
366+
if (domain != null)
367+
{
368+
configBuilder.SetDomain(domain);
369+
}
370+
269371
configBuilder.SetDesktopSize((ushort)height, (ushort)width);
270372
configBuilder.SetClientName("IronRdp");
271373
configBuilder.SetClientDir("C:\\");
@@ -418,7 +520,7 @@ private async Task<bool> HandleActiveStageOutput(ActiveStageOutputIterator outpu
418520
var writeBuf = WriteBuf.New();
419521
while (true)
420522
{
421-
await Connection.SingleSequenceStep(activationSequence, writeBuf,_framed!);
523+
await Connection.SingleSequenceStep(activationSequence, writeBuf, _framed!);
422524

423525
if (activationSequence.GetState().GetType() != ConnectionActivationStateType.Finalized)
424526
continue;

0 commit comments

Comments
 (0)