- 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
165 lines
6.4 KiB
PowerShell
165 lines
6.4 KiB
PowerShell
# GOTT Remote Agent - Windows PowerShell Client
|
||
# Verbindet sich per WebSocket zum GOTT-Server und fuehrt Befehle aus.
|
||
#
|
||
# Verwendung:
|
||
# irm https://gott.example.com/remote.ps1 | iex
|
||
# oder:
|
||
# .\remote.ps1 -Url "wss://gott.example.com/api/remote"
|
||
|
||
param(
|
||
[string]$Url = "{{WS_URL}}"
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
Write-Host ""
|
||
Write-Host " ============================" -ForegroundColor Cyan
|
||
Write-Host " GOTT Remote Agent" -ForegroundColor Cyan
|
||
Write-Host " ============================" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
Write-Host " Verbinde zu: $Url" -ForegroundColor Gray
|
||
Write-Host ""
|
||
|
||
try {
|
||
$ws = New-Object System.Net.WebSockets.ClientWebSocket
|
||
$ct = New-Object System.Threading.CancellationToken
|
||
|
||
# Verbindung aufbauen
|
||
$ws.ConnectAsync([Uri]$Url, $ct).GetAwaiter().GetResult()
|
||
Write-Host " [OK] Verbunden!" -ForegroundColor Green
|
||
|
||
# Hello-Message senden
|
||
$hostname = $env:COMPUTERNAME
|
||
$username = $env:USERNAME
|
||
$osVersion = [System.Environment]::OSVersion.VersionString
|
||
$hello = @{
|
||
type = "hello"
|
||
hostname = $hostname
|
||
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()
|
||
|
||
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
|
||
|
||
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) {
|
||
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)
|
||
}
|
||
|
||
$json = [System.Text.Encoding]::UTF8.GetString($messageBuffer.ToArray())
|
||
$msg = $json | ConvertFrom-Json
|
||
|
||
switch ($msg.type) {
|
||
"execute" {
|
||
$command = $msg.command
|
||
$timestamp = Get-Date -Format "HH:mm:ss"
|
||
Write-Host " [$timestamp] Ausfuehren: " -NoNewline -ForegroundColor Cyan
|
||
Write-Host $command -ForegroundColor White
|
||
|
||
$exitCode = 0
|
||
$stdout = ""
|
||
$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"
|
||
$exitCode = $LASTEXITCODE
|
||
if ($null -eq $exitCode) { $exitCode = 0 }
|
||
} catch {
|
||
$stderr = $_.Exception.Message
|
||
$exitCode = 1
|
||
}
|
||
|
||
# Ergebnis senden
|
||
$response = @{
|
||
type = "result"
|
||
exitCode = $exitCode
|
||
output = if ($stdout) { $stdout } else { "" }
|
||
error = if ($stderr) { $stderr } else { "" }
|
||
} | ConvertTo-Json -Compress
|
||
|
||
$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()
|
||
|
||
# Kurzausgabe
|
||
if ($stdout) {
|
||
$preview = if ($stdout.Length -gt 200) { $stdout.Substring(0, 200) + "..." } else { $stdout }
|
||
Write-Host " -> $preview" -ForegroundColor Gray
|
||
}
|
||
if ($stderr) {
|
||
Write-Host " [stderr] $stderr" -ForegroundColor Red
|
||
}
|
||
Write-Host " [Exit: $exitCode]" -ForegroundColor $(if ($exitCode -eq 0) { "Green" } else { "Red" })
|
||
Write-Host ""
|
||
}
|
||
"pong" {
|
||
# Heartbeat-Antwort – ignorieren
|
||
}
|
||
default {
|
||
Write-Host " Unbekannte Nachricht: $($msg.type)" -ForegroundColor Yellow
|
||
}
|
||
}
|
||
}
|
||
} catch [System.Net.WebSockets.WebSocketException] {
|
||
Write-Host " Verbindung verloren." -ForegroundColor Red
|
||
} catch {
|
||
Write-Host " Fehler: $_" -ForegroundColor Red
|
||
} finally {
|
||
if ($ws -and $ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
|
||
try {
|
||
$ws.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "bye", $ct).GetAwaiter().GetResult()
|
||
} catch { }
|
||
}
|
||
if ($ws) { $ws.Dispose() }
|
||
Write-Host ""
|
||
Write-Host " Verbindung beendet." -ForegroundColor Yellow
|
||
}
|