# HTTP-Router: leitet Requests an statische Dateien oder API-Endpoints weiter. function Invoke-Router { param([Parameter(Mandatory=$true)]$Context) $request = $Context.Request $path = $request.Url.AbsolutePath.TrimEnd('/') if ([string]::IsNullOrEmpty($path)) { $path = "/" } $method = $request.HttpMethod $isApi = $path.StartsWith("/api/") if ($isApi) { Write-Host (">>> [{0}] {1} {2}" -f (Get-Date -Format "HH:mm:ss.fff"), $method, $path) -ForegroundColor DarkCyan } $reqStart = Get-Date # API-Endpoints if ($isApi) { $body = $null if ($method -in @("POST","PUT","PATCH")) { $body = Read-RequestBody -Context $Context } $query = @{} foreach ($key in $request.QueryString.AllKeys) { if ($null -ne $key) { $query[$key] = $request.QueryString[$key] } } try { $result = Invoke-ApiHandler -Path $path -Method $method -Body $body -Query $query $handlerMs = [int]((Get-Date) - $reqStart).TotalMilliseconds if ($null -eq $result) { Send-JsonResponse -Context $Context -StatusCode 404 -Body @{ error = "Unknown endpoint: $method $path" } Write-Host ("<<< [{0}] {1} {2} -> 404 ({3}ms)" -f (Get-Date -Format "HH:mm:ss.fff"), $method, $path, $handlerMs) -ForegroundColor DarkYellow } else { $statusCode = 200 if ($result -is [hashtable] -and $result.ContainsKey('__status')) { $statusCode = $result['__status'] $result.Remove('__status') } Send-JsonResponse -Context $Context -StatusCode $statusCode -Body $result $totalMs = [int]((Get-Date) - $reqStart).TotalMilliseconds Write-Host ("<<< [{0}] {1} {2} -> {3} (handler={4}ms total={5}ms)" -f (Get-Date -Format "HH:mm:ss.fff"), $method, $path, $statusCode, $handlerMs, $totalMs) -ForegroundColor DarkCyan } } catch { $msg = $_.Exception.Message # Client-Disconnects (AbortController) sind harmlos und werden nicht geloggt if ($msg -match "Netzwerkname.*nicht.*verfügbar|connection was forcibly closed|aborted by the software|response has been submitted") { Write-Host " [client disconnect] $path" -ForegroundColor DarkGray } else { Write-Host "<<< [API ERROR] $path -> $msg" -ForegroundColor Red Write-Host " Stack: $($_.ScriptStackTrace)" -ForegroundColor DarkRed try { Send-JsonResponse -Context $Context -StatusCode 500 -Body @{ error = $msg stack = $_.ScriptStackTrace } } catch {} } } return } # Assets aus dem Projekt-Root (Logos etc.) if ($path.StartsWith("/assets/")) { $name = $path.Substring("/assets/".Length) $name = $name -replace "[^a-zA-Z0-9._-]", "" $rootDir = Split-Path -Parent $script:Config.WebRoot $file = Join-Path $rootDir $name # Branding-Logos NIE cachen — sonst sieht der User nach dem Upload # weiter die alte Version, auch wenn die URL gleich bleibt. $extraHeaders = $null if ($name -like 'branding-logo.*') { $extraHeaders = @{ 'Cache-Control' = 'no-store, max-age=0' } } Send-FileResponse -Context $Context -FilePath $file -ExtraHeaders $extraHeaders return } # Reports (HTML-Reports werden in Temp gespeichert) if ($path.StartsWith("/reports/")) { $name = $path.Substring("/reports/".Length) $name = $name -replace "[^a-zA-Z0-9._-]", "" $file = Join-Path $script:Config.ReportDir $name Send-FileResponse -Context $Context -FilePath $file return } # Statische Dateien aus www/ $relativePath = if ($path -eq "/") { "index.html" } else { $path.TrimStart('/') } $relativePath = $relativePath -replace '\.\.', '' # path traversal blocken $file = Join-Path $script:Config.WebRoot $relativePath if (Test-Path $file -PathType Leaf) { Send-FileResponse -Context $Context -FilePath $file } else { # SPA fallback - alle nicht erkannten Pfade auf index.html Send-FileResponse -Context $Context -FilePath (Join-Path $script:Config.WebRoot "index.html") } }