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:
@@ -27,6 +27,7 @@ public class CommandExecutor(ApiService api, string hostname, string dataDir, st
|
||||
"reboot" => await Reboot(cmd.Id),
|
||||
"upgrade_win11" => await UpgradeWin11(),
|
||||
"update_agent" => await UpdateAgent(),
|
||||
"shell_exec" => await ShellExec(cmd.Params ?? ""),
|
||||
_ => $"Unbekannter Command: {cmd.Command}"
|
||||
};
|
||||
}
|
||||
@@ -95,6 +96,33 @@ public class CommandExecutor(ApiService api, string hostname, string dataDir, st
|
||||
return Task.FromResult("running");
|
||||
}
|
||||
|
||||
private Task<string> ShellExec(string command)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(command)) return Task.FromResult("Kein Befehl angegeben");
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("powershell.exe",
|
||||
$"-NonInteractive -NoProfile -ExecutionPolicy Bypass -Command \"{command.Replace("\"", "\\\"")}\"")
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
var p = Process.Start(psi)!;
|
||||
var stdout = p.StandardOutput.ReadToEnd();
|
||||
var stderr = p.StandardError.ReadToEnd();
|
||||
p.WaitForExit(30000);
|
||||
var output = stdout;
|
||||
if (!string.IsNullOrEmpty(stderr)) output += "\n[STDERR] " + stderr;
|
||||
return Task.FromResult(string.IsNullOrEmpty(output) ? "(kein Output)" : output.Trim());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult($"Fehler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> UpdateAgent()
|
||||
{
|
||||
var bytes = await _api.DownloadSetupAsync();
|
||||
|
||||
@@ -35,6 +35,11 @@ public class NotificationService
|
||||
return [];
|
||||
}
|
||||
|
||||
public void RemoveShownId(int id)
|
||||
{
|
||||
if (_shownIds.Remove(id)) SaveShownIds();
|
||||
}
|
||||
|
||||
private void SaveShownIds()
|
||||
{
|
||||
try { File.WriteAllText(_shownIdsPath, JsonConvert.SerializeObject(_shownIds.ToList())); }
|
||||
@@ -84,8 +89,12 @@ public class NotificationService
|
||||
|
||||
// Task mit Trigger weit in der Zukunft erstellen (damit er nicht abläuft vor /run)
|
||||
var triggerTime = DateTime.Now.AddMinutes(60).ToString("HH:mm:ss");
|
||||
// AzureAD-User (Entra/Hybrid-Join) können nicht per /ru aufgelöst werden → INTERACTIVE
|
||||
var ruArg = fullUser.StartsWith("AzureAD\\", StringComparison.OrdinalIgnoreCase)
|
||||
? "/ru \"INTERACTIVE\""
|
||||
: $"/ru \"{fullUser}\"";
|
||||
var args = $"/create /tn \"{taskName}\" /tr \"\\\"{_exePath}\\\" --notify {json}\" " +
|
||||
$"/sc ONCE /st {triggerTime} /ru \"{fullUser}\" /it /f";
|
||||
$"/sc ONCE /st {triggerTime} {ruArg} /it /f";
|
||||
var p = Process.Start(new ProcessStartInfo("schtasks.exe", args)
|
||||
{ CreateNoWindow = true, RedirectStandardError = true, UseShellExecute = false });
|
||||
p?.WaitForExit();
|
||||
|
||||
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