diff --git a/Endpoints/RemoteEndpoint.cs b/Endpoints/RemoteEndpoint.cs index 960b04f..d01d5d5 100644 --- a/Endpoints/RemoteEndpoint.cs +++ b/Endpoints/RemoteEndpoint.cs @@ -12,6 +12,8 @@ using Gott.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Gott.Endpoints; @@ -40,13 +42,16 @@ public static class RemoteEndpoint return; } + // App-Shutdown-Token verwenden damit WebSocket-Loops sauber beendet werden + var appLifetime = ctx.RequestServices.GetRequiredService(); + var ct = appLifetime.ApplicationStopping; + 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); + await ReceiveLoop(ws, conn, mgr, ct); } catch (WebSocketException) { @@ -54,7 +59,7 @@ public static class RemoteEndpoint } catch (OperationCanceledException) { - // Shutdown + // Shutdown oder Ctrl+C } finally { @@ -63,25 +68,29 @@ public static class RemoteEndpoint 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 { } } } } - 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 messageBuffer = new MemoryStream(); - while (ws.State == WebSocketState.Open) + while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested) { WebSocketReceiveResult result; messageBuffer.SetLength(0); do { - result = await ws.ReceiveAsync(buffer, CancellationToken.None); + result = await ws.ReceiveAsync(buffer, ct); if (result.MessageType == WebSocketMessageType.Close) return; messageBuffer.Write(buffer, 0, result.Count); } while (!result.EndOfMessage); @@ -89,6 +98,7 @@ public static class RemoteEndpoint if (result.MessageType != WebSocketMessageType.Text) continue; var json = Encoding.UTF8.GetString(messageBuffer.GetBuffer(), 0, (int)messageBuffer.Length); + JsonElement msg; try { msg = JsonSerializer.Deserialize(json); } catch { continue; } @@ -98,15 +108,13 @@ public static class RemoteEndpoint 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})"); + Console.WriteLine($"[Remote] Client connected: {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, @@ -117,9 +125,8 @@ public static class RemoteEndpoint break; case "ping": - // Heartbeat – antworte mit 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; } } @@ -180,10 +187,16 @@ public static class RemoteEndpoint 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"; + // Host aus X-Forwarded-Host (Reverse-Proxy) oder Request.Host + var host = ctx.Request.Headers["X-Forwarded-Host"].FirstOrDefault() + ?? ctx.Request.Host.ToString(); + + // 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); script = script.Replace("{{WS_URL}}", wsUrl); diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index dcf4a95..3dcfb9d 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -1516,7 +1516,7 @@ public class AnthropicService { var hasWorkspace = !string.IsNullOrEmpty(workspace); var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId); - var hasTools = hasWorkspace || includeMemory; + var hasTools = hasWorkspace || includeMemory || includeRemote; var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion); if (!hasTools && !hasWebSearch) return; diff --git a/wwwroot/remote.ps1 b/wwwroot/remote.ps1 index 59bdaff..25325f8 100644 --- a/wwwroot/remote.ps1 +++ b/wwwroot/remote.ps1 @@ -20,12 +20,48 @@ Write-Host "" Write-Host " Verbinde zu: $Url" -ForegroundColor Gray 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 { $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 - $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 # Hello-Message senden @@ -38,61 +74,28 @@ try { username = $username os = $osVersion } | ConvertTo-Json -Compress - - $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() + Send-WsMessage $ws $hello $ct Write-Host " Host: $username@$hostname ($osVersion)" -ForegroundColor Gray Write-Host "" Write-Host " Warte auf Befehle... (Strg+C zum Beenden)" -ForegroundColor Yellow Write-Host "" - # Empfangs-Loop - $buffer = New-Object byte[] 65536 - $messageBuffer = New-Object System.IO.MemoryStream - - # Heartbeat-Timer (alle 30 Sekunden) - $lastPing = [DateTime]::UtcNow - + # Synchroner Empfangs-Loop while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) { - # Heartbeat senden - if (([DateTime]::UtcNow - $lastPing).TotalSeconds -gt 30) { - $pingBytes = [System.Text.Encoding]::UTF8.GetBytes('{"type":"ping"}') - $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) { + # Blockierend auf naechste Nachricht warten + $json = Receive-WsMessage $ws $ct + if ($null -eq $json) { Write-Host " Server hat Verbindung geschlossen." -ForegroundColor Yellow break } - $messageBuffer.SetLength(0) - $messageBuffer.Write($buffer, 0, $result.Count) - - # Mehrteilige Nachrichten zusammenbauen - while (-not $result.EndOfMessage) { - $result = $ws.ReceiveAsync((New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer)), $ct).GetAwaiter().GetResult() - $messageBuffer.Write($buffer, 0, $result.Count) + try { + $msg = $json | ConvertFrom-Json + } catch { + continue } - $json = [System.Text.Encoding]::UTF8.GetString($messageBuffer.ToArray()) - $msg = $json | ConvertFrom-Json - switch ($msg.type) { "execute" { $command = $msg.command @@ -105,11 +108,12 @@ try { $stderr = "" try { - # PowerShell-Befehl ausfuehren $output = & powershell.exe -NoProfile -NonInteractive -Command $command 2>&1 - $stdout = ($output | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }) -join "`n" - $stderrLines = ($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }) | ForEach-Object { $_.ToString() } - $stderr = $stderrLines -join "`n" + $stdoutLines = @($output | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }) + $stderrLines = @($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }) | + ForEach-Object { $_.ToString() } + $stdout = ($stdoutLines | Out-String).TrimEnd() + $stderr = ($stderrLines | Out-String).TrimEnd() $exitCode = $LASTEXITCODE if ($null -eq $exitCode) { $exitCode = 0 } } catch { @@ -123,13 +127,11 @@ try { exitCode = $exitCode output = if ($stdout) { $stdout } else { "" } error = if ($stderr) { $stderr } else { "" } - } | ConvertTo-Json -Compress + } | ConvertTo-Json -Compress -Depth 3 - $responseBytes = [System.Text.Encoding]::UTF8.GetBytes($response) - $responseSegment = New-Object System.ArraySegment[byte] -ArgumentList @(,$responseBytes) - $ws.SendAsync($responseSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult() + Send-WsMessage $ws $response $ct - # Kurzausgabe + # Kurzausgabe lokal if ($stdout) { $preview = if ($stdout.Length -gt 200) { $stdout.Substring(0, 200) + "..." } else { $stdout } Write-Host " -> $preview" -ForegroundColor Gray @@ -141,13 +143,15 @@ try { Write-Host "" } "pong" { - # Heartbeat-Antwort – ignorieren + # Heartbeat-Antwort - ignorieren } default { Write-Host " Unbekannte Nachricht: $($msg.type)" -ForegroundColor Yellow } } } +} catch [System.OperationCanceledException] { + # Ctrl+C } catch [System.Net.WebSockets.WebSocketException] { Write-Host " Verbindung verloren." -ForegroundColor Red } catch { @@ -155,9 +159,11 @@ try { } finally { if ($ws -and $ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) { 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 { } } + if ($cts) { $cts.Dispose() } if ($ws) { $ws.Dispose() } Write-Host "" Write-Host " Verbindung beendet." -ForegroundColor Yellow