100 lines
4.4 KiB
PowerShell
100 lines
4.4 KiB
PowerShell
# 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")
|
|
}
|
|
}
|