diff --git a/agent-cs/CaptureModeRunner.cs b/agent-cs/CaptureModeRunner.cs new file mode 100644 index 0000000..4442a5e --- /dev/null +++ b/agent-cs/CaptureModeRunner.cs @@ -0,0 +1,62 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.IO.Pipes; + +namespace ITNexusAgent; + +// Läuft als User-Prozess (via schtasks), nicht als SYSTEM-Service +// Captured den Desktop und schickt JPEG-Frames via Named Pipe an den Service +public static class CaptureModeRunner +{ + private static readonly ImageCodecInfo JpegCodec = + ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid); + + public static void Run(string pipeName) + { + if (string.IsNullOrEmpty(pipeName)) return; + + try + { + using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.Out); + pipe.Connect(5000); // 5s timeout + + var encParams = new EncoderParameters(1); + encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L); + + // Alle Screens zusammenführen (Multi-Monitor-Support) + var allScreens = System.Windows.Forms.Screen.AllScreens; + int left = allScreens.Min(s => s.Bounds.X); + int top = allScreens.Min(s => s.Bounds.Y); + int right = allScreens.Max(s => s.Bounds.X + s.Bounds.Width); + int bottom = allScreens.Max(s => s.Bounds.Y + s.Bounds.Height); + int totalW = right - left; + int totalH = bottom - top; + + while (pipe.IsConnected) + { + try + { + using var bmp = new Bitmap(totalW, totalH, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using (var g = Graphics.FromImage(bmp)) + g.CopyFromScreen(left, top, 0, 0, new Size(totalW, totalH)); + + byte[] jpeg; + using (var ms = new MemoryStream()) + { + bmp.Save(ms, JpegCodec, encParams); + jpeg = ms.ToArray(); + } + + // Länge (4 Bytes LE) + JPEG-Daten + pipe.Write(BitConverter.GetBytes(jpeg.Length)); + pipe.Write(jpeg); + pipe.Flush(); + } + catch { break; } + + Thread.Sleep(150); // ~6-7 fps + } + } + catch { /* Pipe nicht verfügbar oder Timeout → Exit */ } + } +} diff --git a/agent-cs/Program.cs b/agent-cs/Program.cs index 2f4d42c..d6a94aa 100644 --- a/agent-cs/Program.cs +++ b/agent-cs/Program.cs @@ -19,6 +19,10 @@ internal class Program RunNotification(args.Length > 1 ? args[1] : ""); return; + case "--rdp-capture": + CaptureModeRunner.Run(args.Length > 1 ? args[1] : ""); + return; + case "--dashboard": RunDashboard(); return; diff --git a/agent-cs/Services/RtcService.cs b/agent-cs/Services/RtcService.cs index 4aaaa09..790f5de 100644 --- a/agent-cs/Services/RtcService.cs +++ b/agent-cs/Services/RtcService.cs @@ -1,5 +1,5 @@ -using System.Drawing; -using System.Drawing.Imaging; +using System.Diagnostics; +using System.IO.Pipes; using System.Net.WebSockets; using System.Text; using Newtonsoft.Json; @@ -7,16 +7,14 @@ using Newtonsoft.Json.Linq; namespace ITNexusAgent.Services; -// Überträgt den Bildschirm via JPEG-Frames über WebSocket an den Browser +// WebSocket-Brücke: empfängt rdp_start vom Browser, spawnt Screen-Capture-Helper +// in der User-Session (via schtasks), leitet JPEG-Frames als rdp_frame weiter 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; @@ -67,7 +65,7 @@ public class RtcService if (type == "rdp_start" && captureCts == null) { captureCts = CancellationTokenSource.CreateLinkedTokenSource(ct); - _ = CaptureLoopAsync(ws, captureCts.Token); + _ = CapturePipeLoopAsync(ws, captureCts.Token); AgentWorker.Log("RDP: Screen-Capture gestartet"); } else if (type == "rdp_stop" && captureCts != null) @@ -82,46 +80,131 @@ public class RtcService AgentWorker.Log("RDP: Getrennt"); } - private static async Task CaptureLoopAsync(ClientWebSocket ws, CancellationToken ct) + private async Task CapturePipeLoopAsync(ClientWebSocket ws, CancellationToken ct) { - var encParams = new EncoderParameters(1); - encParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L); + var pipeName = $"it-nexus-rdp-{Guid.NewGuid():N}"; + var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName; - while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open) + using var pipeServer = new NamedPipeServerStream( + pipeName, PipeDirection.In, 1, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + + // Helper via schtasks in User-Session starten + if (!SpawnCaptureHelper(exePath, pipeName)) + { + AgentWorker.Log("RDP: Helper-Start fehlgeschlagen"); + return; + } + + try + { + using var connectCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, connectCts.Token); + await pipeServer.WaitForConnectionAsync(linked.Token); + } + catch + { + AgentWorker.Log("RDP: Helper hat sich nicht verbunden"); + return; + } + + AgentWorker.Log("RDP: Helper verbunden, sende Frames..."); + + var lenBuf = new byte[4]; + while (!ct.IsCancellationRequested && pipeServer.IsConnected && 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()) + // 4-Byte Länge lesen + var read = 0; + while (read < 4) { - bmp.Save(ms, JpegCodec, encParams); - jpeg = ms.ToArray(); + var n = await pipeServer.ReadAsync(lenBuf.AsMemory(read, 4 - read), ct); + if (n == 0) goto done; + read += n; + } + var jpegLen = BitConverter.ToInt32(lenBuf); + if (jpegLen <= 0 || jpegLen > 5_000_000) continue; + + // JPEG-Daten lesen + var jpeg = new byte[jpegLen]; + read = 0; + while (read < jpegLen) + { + var n = await pipeServer.ReadAsync(jpeg.AsMemory(read, jpegLen - read), ct); + if (n == 0) goto done; + read += n; } var payload = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { type = "rdp_frame", - data = Convert.ToBase64String(jpeg), - w = bounds.Width, - h = bounds.Height + data = Convert.ToBase64String(jpeg) })); if (ws.State == WebSocketState.Open) await ws.SendAsync(new ArraySegment(payload), WebSocketMessageType.Text, true, CancellationToken.None); } catch (OperationCanceledException) { break; } - catch (Exception ex) { AgentWorker.Log($"RDP: Capture-Fehler: {ex.Message}"); } + catch (Exception ex) { AgentWorker.Log($"RDP: Pipe-Fehler: {ex.Message}"); break; } + } - // ~8 fps - await Task.Delay(125, ct).ContinueWith(_ => { }); + done: + AgentWorker.Log("RDP: Frame-Loop beendet"); + } + + private static bool SpawnCaptureHelper(string exePath, string pipeName) + { + try + { + var fullUser = NotificationService.GetLoggedOnUser(); + if (string.IsNullOrEmpty(fullUser)) + { + AgentWorker.Log("RDP: Kein eingeloggter User gefunden"); + return false; + } + + var taskName = $"ITNexus-RDP-{pipeName[^8..]}"; + + Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") + { CreateNoWindow = true })?.WaitForExit(); + + var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss"); + var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase) + ? "/ru \"INTERACTIVE\"" + : $"/ru \"{fullUser}\""; + + var args = $"/create /tn \"{taskName}\" /tr \"\\\"{exePath}\\\" --rdp-capture {pipeName}\" " + + $"/sc ONCE /st {triggerTime} {ruArg} /it /f"; + + var p = Process.Start(new ProcessStartInfo("schtasks.exe", args) + { CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false }); + p?.WaitForExit(); + + if (p?.ExitCode != 0) + { + AgentWorker.Log($"RDP: schtasks create fehlgeschlagen (ExitCode={p?.ExitCode})"); + return false; + } + + Process.Start(new ProcessStartInfo("schtasks.exe", $"/run /tn \"{taskName}\"") + { CreateNoWindow = true })?.WaitForExit(); + + // Aufräumen nach kurzer Zeit + _ = Task.Run(async () => + { + await Task.Delay(5000); + Process.Start(new ProcessStartInfo("schtasks.exe", $"/delete /tn \"{taskName}\" /f") + { CreateNoWindow = true })?.WaitForExit(); + }); + + AgentWorker.Log($"RDP: Helper gestartet als '{fullUser}' (Task: {taskName})"); + return true; + } + catch (Exception ex) + { + AgentWorker.Log($"RDP: SpawnHelper Fehler: {ex.Message}"); + return false; } } }