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:
1 parent
10da2bd964
commit
0eb9022803
3 files changed
+91
-72
No files matched your search
+29
-16
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user