mirror of
https://github.com/Devolutions/IronRDP.git
synced 2025-08-04 15:18:17 +00:00
559 lines
19 KiB
C#
559 lines
19 KiB
C#
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Media.Imaging;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Threading;
|
|
using System;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Net.Security;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Markup.Xaml;
|
|
using Avalonia.Interactivity;
|
|
using Image = Avalonia.Controls.Image;
|
|
|
|
namespace Devolutions.IronRdp.AvaloniaExample;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
readonly InputDatabase? _inputDatabase = InputDatabase.New();
|
|
ActiveStage? _activeStage;
|
|
DecodedImage? _decodedImage;
|
|
Framed<Stream>? _framed;
|
|
WinCliprdr? _cliprdr;
|
|
private readonly RendererModel _renderModel;
|
|
private Image? _imageControl;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
Opened += OnOpened;
|
|
|
|
_renderModel = new RendererModel()
|
|
{
|
|
Width = 980,
|
|
Height = 780
|
|
};
|
|
|
|
this.DataContext = _renderModel;
|
|
|
|
Closing += (sender, e) => { Environment.Exit(1); };
|
|
}
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
AvaloniaXamlLoader.Load(this);
|
|
}
|
|
|
|
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
|
{
|
|
base.OnSizeChanged(e);
|
|
_renderModel.Width = (int)e.NewSize.Width;
|
|
_renderModel.Height = (int)e.NewSize.Height - 100;
|
|
}
|
|
|
|
private void Resize(double updatedWidth, double updatedHeight)
|
|
{
|
|
if (_activeStage != null)
|
|
{
|
|
var output = _activeStage.EncodedResize((uint)updatedWidth, (uint)updatedHeight);
|
|
if (output == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Task.Run(async () => { await HandleActiveStageOutput(output); });
|
|
}
|
|
}
|
|
|
|
private void OnOpened(object? sender, EventArgs e)
|
|
{
|
|
Log.InitWithEnv();
|
|
|
|
WindowState = WindowState.Maximized;
|
|
|
|
var username = Environment.GetEnvironmentVariable("IRONRDP_USERNAME");
|
|
var password = Environment.GetEnvironmentVariable("IRONRDP_PASSWORD");
|
|
var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN");
|
|
var server = Environment.GetEnvironmentVariable("IRONRDP_SERVER");
|
|
var wsProxy = Environment.GetEnvironmentVariable("IRONRDP_PROXY");
|
|
var wsProxyToken = Environment.GetEnvironmentVariable("IRONRDP_PROXY_TOKEN");
|
|
|
|
if (username == null || password == null || server == null)
|
|
{
|
|
var errorMessage =
|
|
"Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, IRONRDP_DOMAIN, and RONRDP_SERVER environment variables";
|
|
Trace.TraceError(errorMessage);
|
|
Close();
|
|
throw new InvalidProgramException(errorMessage);
|
|
}
|
|
|
|
var config = BuildConfig(username, password, domain, _renderModel.Width, _renderModel.Height);
|
|
|
|
CliprdrBackendFactory? factory = null;
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
_cliprdr = WinCliprdr.New();
|
|
|
|
if (_cliprdr != null)
|
|
{
|
|
factory = _cliprdr.BackendFactory();
|
|
}
|
|
}
|
|
|
|
BeforeConnectSetup();
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
|
|
ConnectionResult res;
|
|
Framed<Stream> framed;
|
|
//wsProxy = null;
|
|
if (wsProxy != null && wsProxyToken != null)
|
|
{
|
|
Debug.WriteLine("Connecting via WebSocket proxy");
|
|
(res, framed) = await Connection.ConnectWs(
|
|
config,
|
|
new RdCleanPathConfig(new Uri(wsProxy), wsProxyToken),
|
|
server,
|
|
factory);
|
|
}
|
|
else
|
|
{
|
|
Debug.WriteLine("Connecting directly to server");
|
|
(res, framed) = await Connection.Connect(config, server, factory);
|
|
}
|
|
Debug.WriteLine("Connection success");
|
|
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))
|
|
{
|
|
HandleClipboardEvents();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.WriteLine(e);
|
|
this.Close();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void BeforeConnectSetup()
|
|
{
|
|
_imageControl = this.FindControl<Image>("RdpImage");
|
|
if (_imageControl == null)
|
|
{
|
|
Trace.TraceError("Error finding Image control");
|
|
throw new NullReferenceException("image control not found");
|
|
}
|
|
|
|
var bitmap = new WriteableBitmap(new PixelSize(_renderModel.Width, _renderModel.Height),
|
|
new Vector(96, 96),
|
|
Avalonia.Platform.PixelFormat.Rgba8888, AlphaFormat.Opaque
|
|
);
|
|
_imageControl.Source = bitmap;
|
|
_imageControl.SizeChanged += (sender, e) => { Resize(e.NewSize.Width, e.NewSize.Height); };
|
|
}
|
|
|
|
private void Render()
|
|
{
|
|
Dispatcher.UIThread.Invoke(() =>
|
|
{
|
|
try
|
|
{
|
|
var data = _decodedImage!.GetData();
|
|
var bufferSize = (int)data.GetSize();
|
|
|
|
var buffer = new byte[bufferSize];
|
|
data.Fill(buffer);
|
|
|
|
if (_imageControl is not { Source: WriteableBitmap writableBitmap })
|
|
{
|
|
return;
|
|
}
|
|
|
|
var currentBitmapSize = writableBitmap.Size.Width * writableBitmap.Size.Height * 4;
|
|
if (Math.Abs(bufferSize - currentBitmapSize) > 1)
|
|
{
|
|
var newBitmap =
|
|
new WriteableBitmap(new PixelSize(_decodedImage.GetWidth(), _decodedImage.GetHeight()),
|
|
new Vector(96, 96), Avalonia.Platform.PixelFormat.Rgba8888, AlphaFormat.Opaque);
|
|
_imageControl.Source = newBitmap;
|
|
writableBitmap = newBitmap;
|
|
}
|
|
|
|
using (var bitmap = writableBitmap.Lock())
|
|
{
|
|
unsafe
|
|
{
|
|
var bitmapSpan = new Span<byte>((void*)bitmap.Address,
|
|
bitmap.Size.Width * bitmap.Size.Height * (bitmap.Format.BitsPerPixel / 8));
|
|
bitmapSpan.Clear();
|
|
var bufferSpan = new Span<byte>(buffer);
|
|
if (bufferSize > bitmapSpan.Length)
|
|
{
|
|
throw new InvalidOperationException("buffer size does not match");
|
|
}
|
|
else
|
|
{
|
|
bufferSpan.CopyTo(bitmapSpan);
|
|
}
|
|
}
|
|
}
|
|
|
|
_imageControl!.InvalidateVisual();
|
|
}
|
|
catch
|
|
{
|
|
Trace.TraceError("error rendering");
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
private void ReadPduAndProcessActiveStage()
|
|
{
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var keepLooping = true;
|
|
while (keepLooping)
|
|
{
|
|
var (action, payload) = await _framed!.ReadPdu();
|
|
var outputIterator = _activeStage!.Process(_decodedImage!, action, payload);
|
|
keepLooping = await HandleActiveStageOutput(outputIterator);
|
|
}
|
|
|
|
Trace.TraceInformation("ReadPduAndProcessActiveStage loop ended");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Trace.TraceError("Error reading PDU: " + e.Message);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void HandleClipboardEvents()
|
|
{
|
|
Task.Run(async () =>
|
|
{
|
|
while (true)
|
|
{
|
|
if (_cliprdr == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var message = _cliprdr.NextClipboardMessageBlocking();
|
|
|
|
VecU8 frame;
|
|
var messageType = message.GetMessageType();
|
|
Trace.TraceInformation("Clipboard message type: " + messageType);
|
|
if (messageType == ClipboardMessageType.SendFormatData)
|
|
{
|
|
var formatData = message.GetSendFormatData()!;
|
|
frame = _activeStage!.SubmitClipboardFormatData(formatData);
|
|
}
|
|
else if (messageType == ClipboardMessageType.SendInitiateCopy)
|
|
{
|
|
var initiateCopy = message.GetSendInitiateCopy()!;
|
|
frame = _activeStage!.InitiateClipboardCopy(initiateCopy);
|
|
}
|
|
else if (messageType == ClipboardMessageType.SendInitiatePaste)
|
|
{
|
|
var initiatePaste = message.GetSendInitiatePaste()!;
|
|
frame = _activeStage!.InitiateClipboardPaste(initiatePaste);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Error in clipboard");
|
|
break;
|
|
}
|
|
|
|
var toWriteBack = new byte[frame.GetSize()];
|
|
frame.Fill(toWriteBack);
|
|
|
|
await _framed!.Write(toWriteBack);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static Config BuildConfig(string username, string password, string? domain, int width, int height)
|
|
{
|
|
ConfigBuilder configBuilder = ConfigBuilder.New();
|
|
|
|
configBuilder.WithUsernameAndPassword(username, password);
|
|
if (domain != null)
|
|
{
|
|
configBuilder.SetDomain(domain);
|
|
}
|
|
configBuilder.SetEnableCredssp(true);
|
|
configBuilder.SetEnableTls(true);
|
|
configBuilder.SetDesktopSize((ushort)height, (ushort)width);
|
|
configBuilder.SetClientName("IronRdp");
|
|
configBuilder.SetClientDir("C:\\");
|
|
configBuilder.SetPerformanceFlags(PerformanceFlags.NewDefault());
|
|
|
|
return configBuilder.Build();
|
|
}
|
|
|
|
private void OnPointerPressed(object sender, PointerPressedEventArgs e)
|
|
{
|
|
PointerUpdateKind mouseButton = e.GetCurrentPoint((Visual?)sender).Properties.PointerUpdateKind;
|
|
|
|
MouseButtonType buttonType = mouseButton switch
|
|
{
|
|
PointerUpdateKind.LeftButtonPressed => MouseButtonType.Left,
|
|
PointerUpdateKind.RightButtonPressed => MouseButtonType.Right,
|
|
PointerUpdateKind.MiddleButtonPressed => MouseButtonType.Middle,
|
|
PointerUpdateKind.XButton1Pressed => MouseButtonType.X1,
|
|
PointerUpdateKind.XButton2Pressed => MouseButtonType.X2,
|
|
PointerUpdateKind.LeftButtonReleased => MouseButtonType.Left,
|
|
PointerUpdateKind.MiddleButtonReleased => MouseButtonType.Middle,
|
|
PointerUpdateKind.RightButtonReleased => MouseButtonType.Right,
|
|
PointerUpdateKind.XButton1Released => MouseButtonType.X1,
|
|
PointerUpdateKind.XButton2Released => MouseButtonType.X2,
|
|
PointerUpdateKind.Other => throw new NotImplementedException(),
|
|
_ => throw new NotImplementedException(),
|
|
};
|
|
|
|
var buttonOperation = MouseButton.New(buttonType).AsOperationMouseButtonPressed();
|
|
var fastpath = _inputDatabase!.Apply(buttonOperation);
|
|
var output = _activeStage!.ProcessFastpathInput(_decodedImage!, fastpath);
|
|
var _ = HandleActiveStageOutput(output);
|
|
}
|
|
|
|
private void OnPointerMoved(object sender, PointerEventArgs e)
|
|
{
|
|
if (this._activeStage == null || this._decodedImage == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var position = e.GetPosition((Visual?)sender);
|
|
var x = (ushort)position.X;
|
|
var y = (ushort)position.Y;
|
|
var mouseMovedEvent = MousePosition.New(x, y).AsMoveOperation();
|
|
var fastpath = _inputDatabase!.Apply(mouseMovedEvent);
|
|
var output = _activeStage.ProcessFastpathInput(_decodedImage, fastpath);
|
|
var _ = HandleActiveStageOutput(output);
|
|
}
|
|
|
|
private void OnPointerReleased(object sender, PointerReleasedEventArgs e)
|
|
{
|
|
PointerUpdateKind mouseButton = e.GetCurrentPoint((Visual?)sender).Properties.PointerUpdateKind;
|
|
|
|
MouseButtonType buttonType = mouseButton switch
|
|
{
|
|
PointerUpdateKind.LeftButtonPressed => MouseButtonType.Left,
|
|
PointerUpdateKind.RightButtonPressed => MouseButtonType.Right,
|
|
PointerUpdateKind.MiddleButtonPressed => MouseButtonType.Middle,
|
|
PointerUpdateKind.XButton1Pressed => MouseButtonType.X1,
|
|
PointerUpdateKind.XButton2Pressed => MouseButtonType.X2,
|
|
PointerUpdateKind.LeftButtonReleased => MouseButtonType.Left,
|
|
PointerUpdateKind.MiddleButtonReleased => MouseButtonType.Middle,
|
|
PointerUpdateKind.RightButtonReleased => MouseButtonType.Right,
|
|
PointerUpdateKind.XButton1Released => MouseButtonType.X1,
|
|
PointerUpdateKind.XButton2Released => MouseButtonType.X2,
|
|
PointerUpdateKind.Other => throw new NotImplementedException(),
|
|
_ => throw new NotImplementedException(),
|
|
};
|
|
|
|
var buttonOperation = MouseButton.New(buttonType).AsOperationMouseButtonReleased();
|
|
var fastpath = _inputDatabase!.Apply(buttonOperation);
|
|
var output = _activeStage!.ProcessFastpathInput(_decodedImage!, fastpath);
|
|
var _ = HandleActiveStageOutput(output);
|
|
}
|
|
|
|
private void OnKeyDown(object? sender, KeyEventArgs? e)
|
|
{
|
|
if (_activeStage == null || _decodedImage == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PhysicalKey physicalKey = e!.PhysicalKey;
|
|
|
|
var keycode = KeyCodeMapper.GetScancode(physicalKey);
|
|
|
|
if (keycode == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var keyOperation = Scancode.FromU16(keycode.Value).AsOperationKeyPressed();
|
|
var fastpath = _inputDatabase!.Apply(keyOperation);
|
|
var output = _activeStage.ProcessFastpathInput(_decodedImage, fastpath);
|
|
var _ = HandleActiveStageOutput(output);
|
|
}
|
|
|
|
private void OnKeyUp(object? sender, KeyEventArgs? e)
|
|
{
|
|
if (_activeStage == null || _decodedImage == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PhysicalKey physicalKey = e!.PhysicalKey;
|
|
|
|
var keycode = KeyCodeMapper.GetScancode(physicalKey);
|
|
|
|
if (keycode == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var keyOperation = Scancode.FromU16(keycode.Value).AsOperationKeyReleased();
|
|
var fastpath = _inputDatabase!.Apply(keyOperation);
|
|
var output = _activeStage.ProcessFastpathInput(_decodedImage, fastpath);
|
|
var _ = HandleActiveStageOutput(output);
|
|
}
|
|
|
|
private async Task<bool> HandleActiveStageOutput(ActiveStageOutputIterator outputIterator)
|
|
{
|
|
while (!outputIterator.IsEmpty())
|
|
{
|
|
try
|
|
{
|
|
var output =
|
|
outputIterator
|
|
.Next()!; // outputIterator.Next() is not null since outputIterator.IsEmpty() is false
|
|
Debug.WriteLine($"Output type: {output.GetType()}, Output enum type : {output.GetEnumType()}");
|
|
if (output.GetEnumType() == ActiveStageOutputType.Terminate)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (output.GetEnumType() == ActiveStageOutputType.ResponseFrame)
|
|
{
|
|
// Send the response frame to the server
|
|
var responseFrame = output.GetResponseFrame();
|
|
byte[] responseFrameBytes = new byte[responseFrame.GetSize()];
|
|
responseFrame.Fill(responseFrameBytes);
|
|
await _framed!.Write(responseFrameBytes);
|
|
}
|
|
else if (output.GetEnumType() == ActiveStageOutputType.GraphicsUpdate)
|
|
{
|
|
Render();
|
|
}
|
|
else if (output.GetEnumType() == ActiveStageOutputType.DeactivateAll)
|
|
{
|
|
var activationSequence = output.GetDeactivateAll();
|
|
var writeBuf = WriteBuf.New();
|
|
while (true)
|
|
{
|
|
await Connection.SingleSequenceStep(activationSequence, writeBuf, _framed!);
|
|
|
|
if (activationSequence.GetState().GetType() != ConnectionActivationStateType.Finalized)
|
|
continue;
|
|
|
|
var finalized = activationSequence.GetState().GetFinalized();
|
|
var desktopSize = finalized.GetDesktopSize();
|
|
var ioChannelId = finalized.GetIoChannelId();
|
|
var userChannelId = finalized.GetUserChannelId();
|
|
var noServerPointer = finalized.GetNoServerPointer();
|
|
var pointerSoftwareRendering = finalized.GetPointerSoftwareRendering();
|
|
|
|
_decodedImage = DecodedImage.New(PixelFormat.RgbA32, desktopSize.GetWidth(),
|
|
desktopSize.GetHeight());
|
|
|
|
_activeStage!.SetFastpathProcessor(
|
|
ioChannelId,
|
|
userChannelId,
|
|
noServerPointer,
|
|
pointerSoftwareRendering
|
|
);
|
|
|
|
_activeStage.SetNoServerPointer(noServerPointer);
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Trace.TraceError("Unhandled ActiveStageOutputType: " + output.GetEnumType());
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Trace.TraceError("Error processing active stage output: " + e.Message);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
IntPtr? GetWindowHandle()
|
|
{
|
|
var handle = TryGetPlatformHandle();
|
|
if (handle == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return handle.Handle;
|
|
}
|
|
|
|
public void OnDisconnectClick(object? sender, RoutedEventArgs e)
|
|
{
|
|
var output = this._activeStage!.GracefulShutdown();
|
|
|
|
HandleActiveStageOutput(output).ContinueWith(t =>
|
|
{
|
|
if (t.IsFaulted)
|
|
{
|
|
Trace.TraceError("Error processing active stage: " + t.Exception!.Message);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public sealed class RendererModel : INotifyPropertyChanged
|
|
{
|
|
private int _width;
|
|
private int _height;
|
|
|
|
public int Width
|
|
{
|
|
get => _width;
|
|
set
|
|
{
|
|
if (_width != value)
|
|
{
|
|
_width = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public int Height
|
|
{
|
|
get { return _height; }
|
|
set
|
|
{
|
|
if (_height != value)
|
|
{
|
|
_height = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
}
|