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,193 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
public static class RemoteEndpoint
|
||||
{
|
||||
public static void MapRemoteEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
// WebSocket-Endpoint für Remote-Clients (kein Auth)
|
||||
app.Map("/api/remote", HandleWebSocket);
|
||||
|
||||
// REST-APIs für Frontend (Auth required)
|
||||
app.MapGet("/api/remote/connections", GetConnections);
|
||||
app.MapPost("/api/remote/assign", AssignConnection);
|
||||
app.MapPost("/api/remote/unassign", UnassignConnection);
|
||||
|
||||
// PowerShell-Script ausliefern
|
||||
app.MapGet("/remote.ps1", (Delegate)ServeScript);
|
||||
}
|
||||
|
||||
private static async Task HandleWebSocket(HttpContext ctx, RemoteConnectionManager mgr)
|
||||
{
|
||||
if (!ctx.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsync("WebSocket expected");
|
||||
return;
|
||||
}
|
||||
|
||||
using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
||||
var conn = mgr.Register(ws);
|
||||
Console.WriteLine($"[Remote] Client connected: {conn.Id}");
|
||||
|
||||
try
|
||||
{
|
||||
await ReceiveLoop(ws, conn, mgr);
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{
|
||||
// Client disconnected
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Shutdown
|
||||
}
|
||||
finally
|
||||
{
|
||||
mgr.Unregister(conn.Id);
|
||||
Console.WriteLine($"[Remote] Client disconnected: {conn.Id} ({conn.Hostname})");
|
||||
|
||||
if (ws.State == WebSocketState.Open || ws.State == WebSocketState.CloseReceived)
|
||||
{
|
||||
try { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReceiveLoop(WebSocket ws, RemoteConnection conn, RemoteConnectionManager mgr)
|
||||
{
|
||||
var buffer = new byte[64 * 1024]; // 64 KB Buffer
|
||||
var messageBuffer = new MemoryStream();
|
||||
|
||||
while (ws.State == WebSocketState.Open)
|
||||
{
|
||||
WebSocketReceiveResult result;
|
||||
messageBuffer.SetLength(0);
|
||||
|
||||
do
|
||||
{
|
||||
result = await ws.ReceiveAsync(buffer, CancellationToken.None);
|
||||
if (result.MessageType == WebSocketMessageType.Close) return;
|
||||
messageBuffer.Write(buffer, 0, result.Count);
|
||||
} while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType != WebSocketMessageType.Text) continue;
|
||||
|
||||
var json = Encoding.UTF8.GetString(messageBuffer.GetBuffer(), 0, (int)messageBuffer.Length);
|
||||
JsonElement msg;
|
||||
try { msg = JsonSerializer.Deserialize<JsonElement>(json); }
|
||||
catch { continue; }
|
||||
|
||||
var type = msg.TryGetProperty("type", out var tp) ? tp.GetString() : "";
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "hello":
|
||||
// Client meldet sich mit Hostname + OS-Info
|
||||
conn.Hostname = msg.TryGetProperty("hostname", out var h) ? h.GetString() ?? "" : "";
|
||||
conn.Username = msg.TryGetProperty("username", out var u) ? u.GetString() ?? "" : "";
|
||||
conn.OsVersion = msg.TryGetProperty("os", out var o) ? o.GetString() ?? "" : "";
|
||||
Console.WriteLine($"[Remote] Client identified: {conn.Id} = {conn.Username}@{conn.Hostname} ({conn.OsVersion})");
|
||||
break;
|
||||
|
||||
case "result":
|
||||
// Ergebnis eines Befehls
|
||||
var cmdResult = new RemoteCommandResult
|
||||
{
|
||||
ExitCode = msg.TryGetProperty("exitCode", out var ec) ? ec.GetInt32() : -1,
|
||||
Output = msg.TryGetProperty("output", out var ot) ? ot.GetString() ?? "" : "",
|
||||
Error = msg.TryGetProperty("error", out var er) ? er.GetString() ?? "" : ""
|
||||
};
|
||||
mgr.HandleResult(conn.Id, cmdResult);
|
||||
break;
|
||||
|
||||
case "ping":
|
||||
// Heartbeat – antworte mit pong
|
||||
var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}");
|
||||
await ws.SendAsync(pong, WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IResult GetConnections(RemoteConnectionManager mgr)
|
||||
{
|
||||
var connections = mgr.GetAll().Select(c => new
|
||||
{
|
||||
id = c.Id,
|
||||
hostname = c.Hostname,
|
||||
username = c.Username,
|
||||
os = c.OsVersion,
|
||||
connectedAt = c.ConnectedAt.ToString("o"),
|
||||
assignedSessionId = c.AssignedSessionId
|
||||
});
|
||||
return Results.Ok(connections);
|
||||
}
|
||||
|
||||
private static IResult AssignConnection(HttpContext ctx, RemoteConnectionManager mgr)
|
||||
{
|
||||
using var reader = new StreamReader(ctx.Request.Body);
|
||||
var body = reader.ReadToEndAsync().Result;
|
||||
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var connectionId = json.TryGetProperty("connectionId", out var cid) ? cid.GetString() : "";
|
||||
var sessionId = json.TryGetProperty("sessionId", out var sid) ? sid.GetString() : "";
|
||||
|
||||
if (string.IsNullOrEmpty(connectionId) || string.IsNullOrEmpty(sessionId))
|
||||
return Results.BadRequest(new { error = "connectionId und sessionId erforderlich" });
|
||||
|
||||
if (!mgr.Assign(connectionId, sessionId))
|
||||
return Results.NotFound(new { error = "Verbindung nicht gefunden" });
|
||||
|
||||
return Results.Ok(new { ok = true });
|
||||
}
|
||||
|
||||
private static IResult UnassignConnection(HttpContext ctx, RemoteConnectionManager mgr)
|
||||
{
|
||||
using var reader = new StreamReader(ctx.Request.Body);
|
||||
var body = reader.ReadToEndAsync().Result;
|
||||
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
|
||||
var connectionId = json.TryGetProperty("connectionId", out var cid) ? cid.GetString() : "";
|
||||
|
||||
if (string.IsNullOrEmpty(connectionId))
|
||||
return Results.BadRequest(new { error = "connectionId erforderlich" });
|
||||
|
||||
if (!mgr.Unassign(connectionId))
|
||||
return Results.NotFound(new { error = "Verbindung nicht gefunden" });
|
||||
|
||||
return Results.Ok(new { ok = true });
|
||||
}
|
||||
|
||||
private static async Task<IResult> ServeScript(HttpContext ctx)
|
||||
{
|
||||
var scriptPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "remote.ps1");
|
||||
if (!File.Exists(scriptPath))
|
||||
return Results.NotFound("Script nicht gefunden");
|
||||
|
||||
// Host aus dem Request ermitteln für die WebSocket-URL
|
||||
var host = ctx.Request.Host.ToString();
|
||||
var scheme = ctx.Request.Scheme == "https" ? "wss" : "ws";
|
||||
var wsUrl = $"{scheme}://{host}/api/remote";
|
||||
|
||||
var script = await File.ReadAllTextAsync(scriptPath);
|
||||
script = script.Replace("{{WS_URL}}", wsUrl);
|
||||
|
||||
return Results.Content(script, "text/plain; charset=utf-8");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user