feat: Remote-Verbindungen – Windows-Workstations per WebSocket steuern
- RemoteConnectionManager: Singleton-Service verwaltet WebSocket-Verbindungen mit Command-Execution (Send/Receive mit Timeout + SemaphoreSlim-Locking) - RemoteEndpoint: WebSocket-Endpoint /api/remote (auth-frei), REST-APIs für Connections/Assign/Unassign, PowerShell-Script-Auslieferung mit dynamischer URL - remote.ps1: PowerShell-Client für Windows – verbindet per WebSocket, sendet Hostname/User/OS, empfängt und führt Befehle aus, Heartbeat - remote_execute Tool: Neues Agent-Tool, nur verfügbar wenn Session eine zugeordnete Remote-Verbindung hat, führt PowerShell-Befehle remote aus - Frontend: Menüpunkt 'Remote-Verbindungen' im Chat-Dropdown, Modal zeigt alle aktiven WebSocket-Verbindungen (Hostname, User, OS, Verbindungszeit), Verbinden/Trennen-Buttons pro Verbindung und Session - Auth: /api/remote und /remote.ps1 von Authentifizierung ausgenommen Architektur: Windows-Client → WebSocket → GOTT-Server → Agent-Tool Session-gebunden: Eine Verbindung wird einer Session zugeordnet
This commit is contained in:
1 parent
9f4746c2e7
commit
10da2bd964
14 files changed
+706
-18
No files matched your search
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Gott.Services;
|
||||
|
||||
public class RemoteConnection
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Hostname { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string OsVersion { get; set; }
|
||||
public DateTime ConnectedAt { get; set; }
|
||||
public string AssignedSessionId { get; set; }
|
||||
public WebSocket Socket { get; set; }
|
||||
|
||||
// Für synchrone Command-Execution: ein Befehl wird gesendet, Ergebnis abgewartet
|
||||
public SemaphoreSlim CommandLock { get; } = new SemaphoreSlim(1, 1);
|
||||
public TaskCompletionSource<RemoteCommandResult> PendingResult { get; set; }
|
||||
}
|
||||
|
||||
public class RemoteCommandResult
|
||||
{
|
||||
public int ExitCode { get; set; }
|
||||
public string Output { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
public class RemoteConnectionManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, RemoteConnection> _connections = new();
|
||||
|
||||
public RemoteConnection Register(WebSocket socket)
|
||||
{
|
||||
var conn = new RemoteConnection
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N")[..12],
|
||||
ConnectedAt = DateTime.UtcNow,
|
||||
Socket = socket
|
||||
};
|
||||
_connections[conn.Id] = conn;
|
||||
return conn;
|
||||
}
|
||||
|
||||
public void Unregister(string connectionId)
|
||||
{
|
||||
if (_connections.TryRemove(connectionId, out var conn))
|
||||
{
|
||||
// Falls ein Befehl auf Antwort wartet, abbrechen
|
||||
conn.PendingResult?.TrySetCanceled();
|
||||
conn.AssignedSessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<RemoteConnection> GetAll()
|
||||
{
|
||||
return _connections.Values.ToList();
|
||||
}
|
||||
|
||||
public RemoteConnection GetById(string connectionId)
|
||||
{
|
||||
_connections.TryGetValue(connectionId, out var conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
public RemoteConnection GetBySessionId(string sessionId)
|
||||
{
|
||||
return _connections.Values.FirstOrDefault(c => c.AssignedSessionId == sessionId);
|
||||
}
|
||||
|
||||
public bool Assign(string connectionId, string sessionId)
|
||||
{
|
||||
if (!_connections.TryGetValue(connectionId, out var conn)) return false;
|
||||
conn.AssignedSessionId = sessionId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Unassign(string connectionId)
|
||||
{
|
||||
if (!_connections.TryGetValue(connectionId, out var conn)) return false;
|
||||
conn.AssignedSessionId = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sendet einen Befehl an den Remote-Client und wartet auf das Ergebnis.
|
||||
/// </summary>
|
||||
public async Task<RemoteCommandResult> ExecuteCommand(string connectionId, string command, int timeoutSeconds = 180, CancellationToken ct = default)
|
||||
{
|
||||
if (!_connections.TryGetValue(connectionId, out var conn))
|
||||
return new RemoteCommandResult { ExitCode = -1, Error = "Verbindung nicht gefunden" };
|
||||
|
||||
if (conn.Socket.State != WebSocketState.Open)
|
||||
return new RemoteCommandResult { ExitCode = -1, Error = "WebSocket nicht verbunden" };
|
||||
|
||||
await conn.CommandLock.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
conn.PendingResult = new TaskCompletionSource<RemoteCommandResult>();
|
||||
|
||||
// Befehl senden
|
||||
var msg = JsonSerializer.Serialize(new { type = "execute", command });
|
||||
var bytes = Encoding.UTF8.GetBytes(msg);
|
||||
await conn.Socket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
|
||||
|
||||
// Auf Ergebnis warten (mit Timeout)
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||
|
||||
try
|
||||
{
|
||||
var registration = cts.Token.Register(() => conn.PendingResult.TrySetCanceled());
|
||||
var result = await conn.PendingResult.Task;
|
||||
registration.Dispose();
|
||||
return result;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return new RemoteCommandResult { ExitCode = -1, Error = $"Timeout nach {timeoutSeconds} Sekunden" };
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
conn.PendingResult = null;
|
||||
conn.CommandLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wird vom WebSocket-Receiver aufgerufen wenn ein Ergebnis vom Client kommt.
|
||||
/// </summary>
|
||||
public void HandleResult(string connectionId, RemoteCommandResult result)
|
||||
{
|
||||
if (_connections.TryGetValue(connectionId, out var conn))
|
||||
{
|
||||
conn.PendingResult?.TrySetResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user