Files
IT-Nexus/agent-cs/Services/ShellService.cs
Simon Grüssing c23091fa6e Add: Agent v2.1.2, WebSocket Live Shell, WebRTC Remote Desktop (Beta), Announcement Push
- Agent v2.1.2: WebSocket ShellService, ACK nach WS-Ankündigungen, shown_announcements Fix
- Backend: shellServer.js mit WebSocket-Server (Shell + Announcement Push + RTC Signaling)
- Backend: patch.controller.js Fix (command_id=0 Falsy-Bug beim Auto-Update)
- Frontend: Remote Desktop Tab (WebRTC Beta) in AgentDetailPage für super_admin/admin
- Frontend: PatchManagementPage auf v2.1.2 aktualisiert
- WPF Notification: AllowsTransparency=False + kein DropShadowEffect (Dispatcher-Crash Fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 13:31:11 +02:00

200 lines
7.1 KiB
C#

using System.Diagnostics;
using System.Net.Http.Json;
using System.Net.WebSockets;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ITNexusAgent.Services;
public class ShellService
{
private readonly string _serverUrl;
private readonly string _agentKey;
private readonly string _hostname;
public ShellService(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($"SHELL: {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=agent&key={Uri.EscapeDataString(_agentKey)}&hostname={Uri.EscapeDataString(_hostname)}";
using var ws = new ClientWebSocket();
ws.Options.SetRequestHeader("User-Agent", "IT-Nexus-Agent/2.1.2");
await ws.ConnectAsync(new Uri(wsUrl), ct);
AgentWorker.Log("SHELL: WebSocket verbunden");
Process? shell = null;
CancellationTokenSource? shellCts = null;
async Task SendText(string text)
{
if (ws.State != WebSocketState.Open) return;
var bytes = Encoding.UTF8.GetBytes(text);
await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
}
void StartShell()
{
try { shellCts?.Cancel(); shell?.Kill(); } catch { }
shellCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
var token = shellCts.Token;
var psi = new ProcessStartInfo("powershell.exe", "-NoExit -NoLogo -NoProfile")
{
CreateNoWindow = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
};
shell = Process.Start(psi)!;
AgentWorker.Log("SHELL: PowerShell gestartet");
// stdout streamen
_ = Task.Run(async () =>
{
var buf = new char[1024];
while (!token.IsCancellationRequested)
{
try
{
int n = await shell.StandardOutput.ReadAsync(buf, 0, buf.Length);
if (n == 0) break;
await SendText(new string(buf, 0, n));
}
catch { break; }
}
}, token);
// stderr streamen (rot markiert)
_ = Task.Run(async () =>
{
var buf = new char[1024];
while (!token.IsCancellationRequested)
{
try
{
int n = await shell.StandardError.ReadAsync(buf, 0, buf.Length);
if (n == 0) break;
await SendText("\x1b[31m" + new string(buf, 0, n) + "\x1b[0m");
}
catch { break; }
}
}, token);
}
void StopShell()
{
shellCts?.Cancel();
try { shell?.Kill(); } catch { }
shell = null;
AgentWorker.Log("SHELL: PowerShell gestoppt");
}
try
{
var buffer = new byte[8192];
while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested)
{
WebSocketReceiveResult result;
try
{
result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), ct);
}
catch { break; }
if (result.MessageType == WebSocketMessageType.Close) break;
var msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
JObject? obj;
try { obj = JObject.Parse(msg); }
catch { continue; }
switch (obj["type"]?.ToString())
{
case "start_shell":
StartShell();
break;
case "input":
if (shell != null && !shell.HasExited)
{
var data = obj["data"]?.ToString() ?? "";
await shell.StandardInput.WriteAsync(data);
await shell.StandardInput.FlushAsync();
}
break;
case "stop_shell":
StopShell();
break;
case "announcements":
// Sofort-Push von Ankündigungen (ohne Warten auf Check-in)
var anns = obj["announcements"]?.ToObject<List<ITNexusAgent.Models.Announcement>>();
if (anns != null && anns.Count > 0)
{
var notifier = new NotificationService(
System.Diagnostics.Process.GetCurrentProcess().MainModule!.FileName,
@"C:\ProgramData\IT Nexus Agent");
notifier.ShowAnnouncements(anns);
AgentWorker.Log($"SHELL: {anns.Count} Ankündigung(en) via WebSocket empfangen");
// ACK senden + shown_announcements.json bereinigen damit gelöschte+neu erstellte
// Ankündigungen mit gleicher ID nicht geblockt werden
foreach (var ann in anns)
{
try
{
await AckAnnouncementAsync(ann.Id);
notifier.RemoveShownId(ann.Id);
}
catch (Exception ex) { AgentWorker.Log($"SHELL: ACK Fehler für ID {ann.Id}: {ex.Message}"); }
}
}
break;
}
}
}
finally
{
StopShell();
}
AgentWorker.Log("SHELL: WebSocket getrennt");
}
private async Task AckAnnouncementAsync(int announcementId)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-agent-key", _agentKey);
var body = JsonContent.Create(new { hostname = _hostname });
await http.PostAsync($"{_serverUrl}/api/announcements/{announcementId}/ack-agent", body);
}
}