Add Remote Desktop (JPEG-over-WebSocket), Agent v2.2.0

- RtcService.cs: Screen capture via Graphics.CopyFromScreen, ~8fps JPEG stream over WebSocket
- shellServer.js: Separate rdp-agent/rdp types with independent socket maps (no shell collision)
- AgentDetailPage: WebSocket-based RemoteDesktop component replaces WebRTC attempt
- setup.iss: Fixed filename to include 'v' prefix (IT-Nexus-Agent-Setup-v2.2.0.exe)
- Removed SIPSorcery dependencies, added System.Drawing.Common

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 14:13:18 +02:00
parent c23091fa6e
commit 0a8b186585
7 changed files with 256 additions and 99 deletions

View File

@@ -6,7 +6,7 @@ namespace ITNexusAgent;
public class AgentWorker
{
private const string Version = "2.1.2";
private const string Version = "2.2.0";
private const string DataDir = @"C:\ProgramData\IT Nexus Agent";
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
@@ -43,6 +43,10 @@ public class AgentWorker
var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname());
_ = shellService.RunAsync(_ct);
// WebRTC Remote Desktop Service im Hintergrund starten
var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname());
_ = rtcService.RunAsync(_ct);
while (!_ct.IsCancellationRequested)
{
await RunCycleAsync();

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>IT-Nexus-Agent</AssemblyName>
@@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<Resource Include="icon.ico"/>
<Resource Include="icon.ico" />
<Content Include="icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -28,6 +28,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
</ItemGroup>

View File

@@ -0,0 +1,127 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Net.WebSockets;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ITNexusAgent.Services;
// Überträgt den Bildschirm via JPEG-Frames über WebSocket an den Browser
public class RtcService
{
private readonly string _serverUrl;
private readonly string _agentKey;
private readonly string _hostname;
private static readonly ImageCodecInfo JpegCodec =
ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
public RtcService(string serverUrl, string agentKey, string hostname)
{
_serverUrl = serverUrl;
_agentKey = agentKey;
_hostname = hostname;
}
public async Task RunAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try { await ConnectAsync(ct); }
catch (Exception ex) { AgentWorker.Log($"RDP: {ex.Message}"); }
if (!ct.IsCancellationRequested)
await Task.Delay(TimeSpan.FromSeconds(15), ct).ContinueWith(_ => { });
}
}
private async Task ConnectAsync(CancellationToken ct)
{
var wsUrl = _serverUrl
.Replace("https://", "wss://")
.Replace("http://", "ws://")
+ $"/ws?type=rdp-agent&key={Uri.EscapeDataString(_agentKey)}&hostname={Uri.EscapeDataString(_hostname)}";
using var ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri(wsUrl), ct);
AgentWorker.Log("RDP: Bereit");
CancellationTokenSource? captureCts = null;
var buf = new byte[4096];
while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested)
{
WebSocketReceiveResult res;
try { res = await ws.ReceiveAsync(new ArraySegment<byte>(buf), ct); }
catch { break; }
if (res.MessageType == WebSocketMessageType.Close) break;
var raw = Encoding.UTF8.GetString(buf, 0, res.Count);
JObject? obj;
try { obj = JObject.Parse(raw); } catch { continue; }
var type = obj["type"]?.ToString();
if (type == "rdp_start" && captureCts == null)
{
captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_ = CaptureLoopAsync(ws, captureCts.Token);
AgentWorker.Log("RDP: Screen-Capture gestartet");
}
else if (type == "rdp_stop" && captureCts != null)
{
captureCts.Cancel();
captureCts = null;
AgentWorker.Log("RDP: Screen-Capture gestoppt");
}
}
captureCts?.Cancel();
AgentWorker.Log("RDP: Getrennt");
}
private static async Task CaptureLoopAsync(ClientWebSocket ws, CancellationToken ct)
{
var encParams = new EncoderParameters(1);
encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L);
while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open)
{
try
{
var screen = System.Windows.Forms.Screen.PrimaryScreen;
if (screen == null) { await Task.Delay(500, ct).ContinueWith(_ => { }); continue; }
var bounds = screen.Bounds;
using var bmp = new Bitmap(bounds.Width, bounds.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(bmp))
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size);
byte[] jpeg;
using (var ms = new MemoryStream())
{
bmp.Save(ms, JpegCodec, encParams);
jpeg = ms.ToArray();
}
var payload = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new
{
type = "rdp_frame",
data = Convert.ToBase64String(jpeg),
w = bounds.Width,
h = bounds.Height
}));
if (ws.State == WebSocketState.Open)
await ws.SendAsync(new ArraySegment<byte>(payload), WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (OperationCanceledException) { break; }
catch (Exception ex) { AgentWorker.Log($"RDP: Capture-Fehler: {ex.Message}"); }
// ~8 fps
await Task.Delay(125, ct).ContinueWith(_ => { });
}
}
}

View File

@@ -1,5 +1,5 @@
#define MyAppName "IT Nexus Agent"
#define MyAppVersion "2.1.2"
#define MyAppVersion "2.2.0"
#define MyAppPublisher "Cereda Systems GmbH"
#define MyAppURL "https://it-nexus.cereda-systems.de"
#define MyAppExeName "IT-Nexus-Agent.exe"
@@ -19,7 +19,7 @@ DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
DisableWelcomePage=no
OutputDir=..\installer
OutputBaseFilename=IT-Nexus-Agent-Setup-{#MyAppVersion}
OutputBaseFilename=IT-Nexus-Agent-Setup-v{#MyAppVersion}
SetupIconFile=icon.ico
Compression=lzma2/max
SolidCompression=yes