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>
This commit is contained in:
199
agent-cs/Services/ShellService.cs
Normal file
199
agent-cs/Services/ShellService.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user