fix: Remote-Verbindungen – Reverse-Proxy-Support, Shutdown-Handling, Script-Stabilität

- X-Forwarded-Host/Proto Header für korrekte WebSocket-URL hinter Reverse-Proxy
- CancellationToken aus IHostApplicationLifetime für sauberen App-Shutdown
- WebSocket-Close mit 3s Timeout statt endlosem Warten
- PowerShell-Script: Synchroner Empfangs-Loop (Send/Receive-Funktionen)
- TLS 1.2 erzwungen für ältere Windows-Versionen
- remote_execute Tool auch ohne Workspace-Pfad verfügbar (includeRemote)
- Debug-Logs entfernt
This commit is contained in:
Meleki committed 2026-03-11 07:41:24 +01:00
1 parent 10da2bd964
commit 0eb9022803
3 files changed
+91 -72

No files matched your search

+29 -16
View File
@@ -12,6 +12,8 @@ using Gott.Services;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Gott.Endpoints; namespace Gott.Endpoints;
@@ -40,13 +42,16 @@ public static class RemoteEndpoint
return; return;
} }
// App-Shutdown-Token verwenden damit WebSocket-Loops sauber beendet werden
var appLifetime = ctx.RequestServices.GetRequiredService<IHostApplicationLifetime>();
var ct = appLifetime.ApplicationStopping;
using var ws = await ctx.WebSockets.AcceptWebSocketAsync(); using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
var conn = mgr.Register(ws); var conn = mgr.Register(ws);
Console.WriteLine($"[Remote] Client connected: {conn.Id}");
try try
{ {
await ReceiveLoop(ws, conn, mgr); await ReceiveLoop(ws, conn, mgr, ct);
} }
catch (WebSocketException) catch (WebSocketException)
{ {
@@ -54,7 +59,7 @@ public static class RemoteEndpoint
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Shutdown // Shutdown oder Ctrl+C
} }
finally finally
{ {
@@ -63,25 +68,29 @@ public static class RemoteEndpoint
if (ws.State == WebSocketState.Open || ws.State == WebSocketState.CloseReceived) if (ws.State == WebSocketState.Open || ws.State == WebSocketState.CloseReceived)
{ {
try { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None); } try
{
using var closeCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", closeCts.Token);
}
catch { } catch { }
} }
} }
} }
private static async Task ReceiveLoop(WebSocket ws, RemoteConnection conn, RemoteConnectionManager mgr) private static async Task ReceiveLoop(WebSocket ws, RemoteConnection conn, RemoteConnectionManager mgr, CancellationToken ct)
{ {
var buffer = new byte[64 * 1024]; // 64 KB Buffer var buffer = new byte[64 * 1024]; // 64 KB Buffer
var messageBuffer = new MemoryStream(); var messageBuffer = new MemoryStream();
while (ws.State == WebSocketState.Open) while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested)
{ {
WebSocketReceiveResult result; WebSocketReceiveResult result;
messageBuffer.SetLength(0); messageBuffer.SetLength(0);
do do
{ {
result = await ws.ReceiveAsync(buffer, CancellationToken.None); result = await ws.ReceiveAsync(buffer, ct);
if (result.MessageType == WebSocketMessageType.Close) return; if (result.MessageType == WebSocketMessageType.Close) return;
messageBuffer.Write(buffer, 0, result.Count); messageBuffer.Write(buffer, 0, result.Count);
} while (!result.EndOfMessage); } while (!result.EndOfMessage);
@@ -89,6 +98,7 @@ public static class RemoteEndpoint
if (result.MessageType != WebSocketMessageType.Text) continue; if (result.MessageType != WebSocketMessageType.Text) continue;
var json = Encoding.UTF8.GetString(messageBuffer.GetBuffer(), 0, (int)messageBuffer.Length); var json = Encoding.UTF8.GetString(messageBuffer.GetBuffer(), 0, (int)messageBuffer.Length);
JsonElement msg; JsonElement msg;
try { msg = JsonSerializer.Deserialize<JsonElement>(json); } try { msg = JsonSerializer.Deserialize<JsonElement>(json); }
catch { continue; } catch { continue; }
@@ -98,15 +108,13 @@ public static class RemoteEndpoint
switch (type) switch (type)
{ {
case "hello": case "hello":
// Client meldet sich mit Hostname + OS-Info
conn.Hostname = msg.TryGetProperty("hostname", out var h) ? h.GetString() ?? "" : ""; conn.Hostname = msg.TryGetProperty("hostname", out var h) ? h.GetString() ?? "" : "";
conn.Username = msg.TryGetProperty("username", out var u) ? u.GetString() ?? "" : ""; conn.Username = msg.TryGetProperty("username", out var u) ? u.GetString() ?? "" : "";
conn.OsVersion = msg.TryGetProperty("os", out var o) ? o.GetString() ?? "" : ""; conn.OsVersion = msg.TryGetProperty("os", out var o) ? o.GetString() ?? "" : "";
Console.WriteLine($"[Remote] Client identified: {conn.Id} = {conn.Username}@{conn.Hostname} ({conn.OsVersion})"); Console.WriteLine($"[Remote] Client connected: {conn.Username}@{conn.Hostname} ({conn.OsVersion})");
break; break;
case "result": case "result":
// Ergebnis eines Befehls
var cmdResult = new RemoteCommandResult var cmdResult = new RemoteCommandResult
{ {
ExitCode = msg.TryGetProperty("exitCode", out var ec) ? ec.GetInt32() : -1, ExitCode = msg.TryGetProperty("exitCode", out var ec) ? ec.GetInt32() : -1,
@@ -117,9 +125,8 @@ public static class RemoteEndpoint
break; break;
case "ping": case "ping":
// Heartbeat – antworte mit pong
var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}"); var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}");
await ws.SendAsync(pong, WebSocketMessageType.Text, true, CancellationToken.None); await ws.SendAsync(pong, WebSocketMessageType.Text, true, ct);
break; break;
} }
} }
@@ -180,10 +187,16 @@ public static class RemoteEndpoint
if (!File.Exists(scriptPath)) if (!File.Exists(scriptPath))
return Results.NotFound("Script nicht gefunden"); return Results.NotFound("Script nicht gefunden");
// Host aus dem Request ermitteln für die WebSocket-URL // Host aus X-Forwarded-Host (Reverse-Proxy) oder Request.Host
var host = ctx.Request.Host.ToString(); var host = ctx.Request.Headers["X-Forwarded-Host"].FirstOrDefault()
var scheme = ctx.Request.Scheme == "https" ? "wss" : "ws"; ?? ctx.Request.Host.ToString();
var wsUrl = $"{scheme}://{host}/api/remote";
// Schema aus X-Forwarded-Proto oder Request.Scheme
var httpScheme = ctx.Request.Headers["X-Forwarded-Proto"].FirstOrDefault()
?? ctx.Request.Scheme;
var wsScheme = httpScheme == "https" ? "wss" : "ws";
var wsUrl = $"{wsScheme}://{host}/api/remote";
var script = await File.ReadAllTextAsync(scriptPath); var script = await File.ReadAllTextAsync(scriptPath);
script = script.Replace("{{WS_URL}}", wsUrl); script = script.Replace("{{WS_URL}}", wsUrl);
+1 -1
View File
@@ -1516,7 +1516,7 @@ public class AnthropicService
{ {
var hasWorkspace = !string.IsNullOrEmpty(workspace); var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId); var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
var hasTools = hasWorkspace || includeMemory; var hasTools = hasWorkspace || includeMemory || includeRemote;
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion); var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
if (!hasTools && !hasWebSearch) return; if (!hasTools && !hasWebSearch) return;
+61 -55
View File
@@ -20,12 +20,48 @@ Write-Host ""
Write-Host " Verbinde zu: $Url" -ForegroundColor Gray Write-Host " Verbinde zu: $Url" -ForegroundColor Gray
Write-Host "" Write-Host ""
function Send-WsMessage {
param([System.Net.WebSockets.ClientWebSocket]$ws, [string]$msg, [System.Threading.CancellationToken]$ct)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)
$ws.SendAsync((New-Object System.ArraySegment[byte] -ArgumentList @(,$bytes)),
[System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult() | Out-Null
}
function Receive-WsMessage {
param([System.Net.WebSockets.ClientWebSocket]$ws, [System.Threading.CancellationToken]$ct)
$buf = New-Object byte[] 65536
$ms = New-Object System.IO.MemoryStream
try {
do {
$seg = New-Object System.ArraySegment[byte] -ArgumentList @(,$buf)
$result = $ws.ReceiveAsync($seg, $ct).GetAwaiter().GetResult()
if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
return $null
}
$ms.Write($buf, 0, $result.Count)
} while (-not $result.EndOfMessage)
return [System.Text.Encoding]::UTF8.GetString($ms.ToArray())
} finally {
$ms.Dispose()
}
}
try { try {
$ws = New-Object System.Net.WebSockets.ClientWebSocket $ws = New-Object System.Net.WebSockets.ClientWebSocket
$ct = New-Object System.Threading.CancellationToken $cts = New-Object System.Threading.CancellationTokenSource
$ct = $cts.Token
# TLS 1.2 erzwingen (fuer aeltere Windows-Versionen)
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
# Verbindung aufbauen # Verbindung aufbauen
$ws.ConnectAsync([Uri]$Url, $ct).GetAwaiter().GetResult() try {
$ws.ConnectAsync([Uri]$Url, $ct).GetAwaiter().GetResult() | Out-Null
} catch {
Write-Host " [FEHLER] Verbindung fehlgeschlagen: $_" -ForegroundColor Red
Write-Host " URL: $Url" -ForegroundColor Gray
return
}
Write-Host " [OK] Verbunden!" -ForegroundColor Green Write-Host " [OK] Verbunden!" -ForegroundColor Green
# Hello-Message senden # Hello-Message senden
@@ -38,61 +74,28 @@ try {
username = $username username = $username
os = $osVersion os = $osVersion
} | ConvertTo-Json -Compress } | ConvertTo-Json -Compress
Send-WsMessage $ws $hello $ct
$helloBytes = [System.Text.Encoding]::UTF8.GetBytes($hello)
$segment = New-Object System.ArraySegment[byte] -ArgumentList @(,$helloBytes)
$ws.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult()
Write-Host " Host: $username@$hostname ($osVersion)" -ForegroundColor Gray Write-Host " Host: $username@$hostname ($osVersion)" -ForegroundColor Gray
Write-Host "" Write-Host ""
Write-Host " Warte auf Befehle... (Strg+C zum Beenden)" -ForegroundColor Yellow Write-Host " Warte auf Befehle... (Strg+C zum Beenden)" -ForegroundColor Yellow
Write-Host "" Write-Host ""
# Empfangs-Loop # Synchroner Empfangs-Loop
$buffer = New-Object byte[] 65536
$messageBuffer = New-Object System.IO.MemoryStream
# Heartbeat-Timer (alle 30 Sekunden)
$lastPing = [DateTime]::UtcNow
while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) { while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
# Heartbeat senden # Blockierend auf naechste Nachricht warten
if (([DateTime]::UtcNow - $lastPing).TotalSeconds -gt 30) { $json = Receive-WsMessage $ws $ct
$pingBytes = [System.Text.Encoding]::UTF8.GetBytes('{"type":"ping"}') if ($null -eq $json) {
$pingSegment = New-Object System.ArraySegment[byte] -ArgumentList @(,$pingBytes)
try {
$ws.SendAsync($pingSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult()
} catch {
break
}
$lastPing = [DateTime]::UtcNow
}
# Auf Nachricht warten (mit Timeout fuer Heartbeat)
$receiveTask = $ws.ReceiveAsync((New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer)), $ct)
if (-not $receiveTask.Wait(5000)) {
continue
}
$result = $receiveTask.Result
if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
Write-Host " Server hat Verbindung geschlossen." -ForegroundColor Yellow Write-Host " Server hat Verbindung geschlossen." -ForegroundColor Yellow
break break
} }
$messageBuffer.SetLength(0) try {
$messageBuffer.Write($buffer, 0, $result.Count) $msg = $json | ConvertFrom-Json
} catch {
# Mehrteilige Nachrichten zusammenbauen continue
while (-not $result.EndOfMessage) {
$result = $ws.ReceiveAsync((New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer)), $ct).GetAwaiter().GetResult()
$messageBuffer.Write($buffer, 0, $result.Count)
} }
$json = [System.Text.Encoding]::UTF8.GetString($messageBuffer.ToArray())
$msg = $json | ConvertFrom-Json
switch ($msg.type) { switch ($msg.type) {
"execute" { "execute" {
$command = $msg.command $command = $msg.command
@@ -105,11 +108,12 @@ try {
$stderr = "" $stderr = ""
try { try {
# PowerShell-Befehl ausfuehren
$output = & powershell.exe -NoProfile -NonInteractive -Command $command 2>&1 $output = & powershell.exe -NoProfile -NonInteractive -Command $command 2>&1
$stdout = ($output | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }) -join "`n" $stdoutLines = @($output | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] })
$stderrLines = ($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }) | ForEach-Object { $_.ToString() } $stderrLines = @($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }) |
$stderr = $stderrLines -join "`n" ForEach-Object { $_.ToString() }
$stdout = ($stdoutLines | Out-String).TrimEnd()
$stderr = ($stderrLines | Out-String).TrimEnd()
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
if ($null -eq $exitCode) { $exitCode = 0 } if ($null -eq $exitCode) { $exitCode = 0 }
} catch { } catch {
@@ -123,13 +127,11 @@ try {
exitCode = $exitCode exitCode = $exitCode
output = if ($stdout) { $stdout } else { "" } output = if ($stdout) { $stdout } else { "" }
error = if ($stderr) { $stderr } else { "" } error = if ($stderr) { $stderr } else { "" }
} | ConvertTo-Json -Compress } | ConvertTo-Json -Compress -Depth 3
$responseBytes = [System.Text.Encoding]::UTF8.GetBytes($response) Send-WsMessage $ws $response $ct
$responseSegment = New-Object System.ArraySegment[byte] -ArgumentList @(,$responseBytes)
$ws.SendAsync($responseSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult()
# Kurzausgabe # Kurzausgabe lokal
if ($stdout) { if ($stdout) {
$preview = if ($stdout.Length -gt 200) { $stdout.Substring(0, 200) + "..." } else { $stdout } $preview = if ($stdout.Length -gt 200) { $stdout.Substring(0, 200) + "..." } else { $stdout }
Write-Host " -> $preview" -ForegroundColor Gray Write-Host " -> $preview" -ForegroundColor Gray
@@ -141,13 +143,15 @@ try {
Write-Host "" Write-Host ""
} }
"pong" { "pong" {
# Heartbeat-Antwort – ignorieren # Heartbeat-Antwort - ignorieren
} }
default { default {
Write-Host " Unbekannte Nachricht: $($msg.type)" -ForegroundColor Yellow Write-Host " Unbekannte Nachricht: $($msg.type)" -ForegroundColor Yellow
} }
} }
} }
} catch [System.OperationCanceledException] {
# Ctrl+C
} catch [System.Net.WebSockets.WebSocketException] { } catch [System.Net.WebSockets.WebSocketException] {
Write-Host " Verbindung verloren." -ForegroundColor Red Write-Host " Verbindung verloren." -ForegroundColor Red
} catch { } catch {
@@ -155,9 +159,11 @@ try {
} finally { } finally {
if ($ws -and $ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) { if ($ws -and $ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
try { try {
$ws.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "bye", $ct).GetAwaiter().GetResult() $closeCt = (New-Object System.Threading.CancellationTokenSource(3000)).Token
$ws.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "bye", $closeCt).GetAwaiter().GetResult() | Out-Null
} catch { } } catch { }
} }
if ($cts) { $cts.Dispose() }
if ($ws) { $ws.Dispose() } if ($ws) { $ws.Dispose() }
Write-Host "" Write-Host ""
Write-Host " Verbindung beendet." -ForegroundColor Yellow Write-Host " Verbindung beendet." -ForegroundColor Yellow