Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
<PackageReference Include="Avalonia" Version="11.3.7" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.7" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.7" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.7" />
<!--Condition
below is needed to remove Avalonia.Diagnostics package from build output in Release
configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.7" />
<ProjectReference Include="../Devolutions.IronRdp/Devolutions.IronRdp.csproj" />
</ItemGroup>

<ItemGroup Condition="'$([System.OperatingSystem]::IsWindows())'">
<PackageReference Include="Avalonia.Win32" Version="11.0.10" />
<PackageReference Include="Avalonia.Win32" Version="11.3.7" />
</ItemGroup>
</Project>
132 changes: 117 additions & 15 deletions ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,7 +23,7 @@ public partial class MainWindow : Window
readonly InputDatabase? _inputDatabase = InputDatabase.New();
ActiveStage? _activeStage;
DecodedImage? _decodedImage;
Framed<SslStream>? _framed;
Framed<Stream>? _framed;
WinCliprdr? _cliprdr;
private readonly RendererModel _renderModel;
private Image? _imageControl;
Expand Down Expand Up @@ -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;
Expand All @@ -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<Stream>(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<Stream>(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;
}
});
}
Expand Down Expand Up @@ -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:\\");
Expand Down Expand Up @@ -418,7 +520,7 @@ private async Task<bool> 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;
Expand Down
Loading
Loading