- 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>
200 lines
7.1 KiB
C#
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);
|
|
}
|
|
}
|