Files
gott/Endpoints/RemoteEndpoint.cs
T
Meleki 10da2bd964 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
2026-03-11 07:04:24 +01:00

194 lines
7.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}
}