- WLAN-Tab: SSIDs (WPA2/WPA3/offen, versteckt) in Blaupausen speichern, fuer alle Benutzer anlegen, Profile eines Referenzgeraets uebernehmen - OpenVPN portabel: offizielles MSI per msiexec /a entpackt + Microsoft- signierter TAP-Treiber in tools\openvpn; Treiber wird nur temporaer geladen und danach entfernt (--disable-dco, --dev-node) - .ovpn/Zertifikate je Kunde hochladen, Benutzer/Kennwort optional - AD-Join mit DC-Test (SRV+LDAP), OU, Umbenennung, lokalen Admins, optional ueber VPN; VPN optional als Dienst fuer die erste Anmeldung - Fehlende Kennwoerter werden beim Start abgefragt und nicht gespeichert Co-Authored-By: Claude Opus 5.5 <[email protected]>
349 lines
19 KiB
PowerShell
349 lines
19 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
KDS Install Agent - Einrichtung von Windows-11-Notebooks per Web-/Desktop-Oberflaeche.
|
|
.PARAMETER Port
|
|
HTTP-Port (Standard 8090).
|
|
.PARAMETER Remote
|
|
Oberflaeche zusaetzlich im Netzwerk bereitstellen (Token-geschuetzt, Firewall-Regel wird angelegt).
|
|
.PARAMETER NoWindow
|
|
Kein Desktop-Fenster oeffnen (nur Web-Server).
|
|
#>
|
|
param(
|
|
[int]$Port = 8090,
|
|
[switch]$Remote,
|
|
[switch]$NoWindow
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$global:KdsRoot = $PSScriptRoot
|
|
|
|
# --- Adminrechte sicherstellen ----------------------------------------------------
|
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
if (-not $isAdmin) {
|
|
$a = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-Port', $Port)
|
|
if ($Remote) { $a += '-Remote' }
|
|
if ($NoWindow) { $a += '-NoWindow' }
|
|
Start-Process powershell.exe -Verb RunAs -ArgumentList $a
|
|
exit
|
|
}
|
|
|
|
. (Join-Path $PSScriptRoot 'lib\Functions.ps1')
|
|
$Host.UI.RawUI.WindowTitle = 'KDS Install Agent'
|
|
|
|
$LogDir = Get-KdsDataDir "logs\$env:COMPUTERNAME"
|
|
$global:KdsLogFile = Join-Path $LogDir 'agent.log'
|
|
$Token = -join ((48..57) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ })
|
|
|
|
# --- Job-Verwaltung (sequenzielle Queue, damit sich Installer nicht blockieren) ---
|
|
$Jobs = [ordered]@{}
|
|
$Queue = New-Object System.Collections.Queue
|
|
$JobSeq = 0
|
|
|
|
$JobRunner = {
|
|
param($Root, $Fn, $Json, $LogFile, $ResultFile)
|
|
$ErrorActionPreference = 'Stop'
|
|
$global:KdsRoot = $Root
|
|
$global:KdsLogFile = $LogFile
|
|
. (Join-Path $Root 'lib\Functions.ps1')
|
|
$data = $Json | ConvertFrom-Json
|
|
try {
|
|
$r = & $Fn $data
|
|
if ($null -ne $r) { ConvertTo-Json -InputObject $r -Depth 8 | Set-Content -Path $ResultFile -Encoding UTF8 }
|
|
Write-Log 'Aufgabe beendet.' 'OK'
|
|
} catch {
|
|
Write-Log "ABBRUCH: $($_.Exception.Message)" 'ERROR'
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Add-KdsJob {
|
|
param([string]$Title, [string]$Fn, $Data)
|
|
$script:JobSeq++
|
|
$id = '{0:D3}' -f $script:JobSeq
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$entry = [pscustomobject]@{
|
|
id = $id; title = $Title; fn = $Fn; state = 'Wartet'; created = (Get-Date).ToString('HH:mm:ss')
|
|
log = (Join-Path $LogDir "$stamp-$id-$Fn.log"); result = (Join-Path $LogDir "$stamp-$id-$Fn.json")
|
|
json = (ConvertTo-Json -InputObject $Data -Depth 10 -Compress); job = $null
|
|
}
|
|
$script:Jobs[$id] = $entry
|
|
$script:Queue.Enqueue($id)
|
|
Write-Log "Aufgabe $id eingereiht: $Title"
|
|
Update-KdsJobs
|
|
return $id
|
|
}
|
|
|
|
function Update-KdsJobs {
|
|
$running = $false
|
|
foreach ($e in $script:Jobs.Values) {
|
|
if ($e.job) {
|
|
switch ($e.job.State) {
|
|
'Running' { $e.state = 'Laeuft'; $running = $true }
|
|
'Completed' { $e.state = if ((Test-Path $e.log) -and (Select-String -Path $e.log -Pattern '\[ERROR\]' -Quiet)) { 'Fertig (mit Fehlern)' } else { 'Fertig' } }
|
|
'Failed' { $e.state = 'Fehlgeschlagen' }
|
|
'Stopped' { $e.state = 'Abgebrochen' }
|
|
}
|
|
}
|
|
}
|
|
if (-not $running -and $script:Queue.Count -gt 0) {
|
|
$e = $script:Jobs[$script:Queue.Dequeue()]
|
|
Set-Content -Path $e.log -Value "=== $($e.title) ($env:COMPUTERNAME, $(Get-Date)) ===" -Encoding UTF8
|
|
$e.job = Start-Job -ScriptBlock $JobRunner -ArgumentList $global:KdsRoot, $e.fn, $e.json, $e.log, $e.result
|
|
$e.state = 'Laeuft'
|
|
}
|
|
}
|
|
|
|
function Read-SharedText {
|
|
# Liest Dateien, die ein Job gerade beschreibt (Get-Content wuerde ETS-Properties anhaengen, die ConvertTo-Json aufblaehen)
|
|
param([string]$Path)
|
|
if (-not (Test-Path $Path)) { return '' }
|
|
$fs = [IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite')
|
|
try { $sr = New-Object IO.StreamReader($fs, [Text.Encoding]::UTF8); return [string]$sr.ReadToEnd() } finally { $fs.Close() }
|
|
}
|
|
|
|
function Get-KdsJobView {
|
|
param($e, [switch]$Full)
|
|
$o = [ordered]@{ id = $e.id; title = $e.title; state = $e.state; created = $e.created }
|
|
if ($Full) {
|
|
$o.log = Read-SharedText $e.log
|
|
$r = Read-SharedText $e.result
|
|
# Elemente entpacken, sonst serialisiert PS 5.1 das Array als {value, Count}
|
|
$o.result = if ($r) { @(($r | ConvertFrom-Json) | ForEach-Object { $_ }) } else { $null }
|
|
}
|
|
[pscustomobject]$o
|
|
}
|
|
|
|
# --- HTTP-Helfer -----------------------------------------------------------------
|
|
$Mime = @{ '.html' = 'text/html; charset=utf-8'; '.js' = 'application/javascript; charset=utf-8'; '.css' = 'text/css; charset=utf-8'
|
|
'.json' = 'application/json; charset=utf-8'; '.png' = 'image/png'; '.ico' = 'image/x-icon'; '.svg' = 'image/svg+xml'; '.xml' = 'application/xml; charset=utf-8' }
|
|
|
|
function Send-Response {
|
|
param($Ctx, [byte[]]$Bytes, [string]$Type = 'application/json; charset=utf-8', [int]$Code = 200)
|
|
$r = $Ctx.Response
|
|
$r.StatusCode = $Code
|
|
$r.ContentType = $Type
|
|
$r.Headers['Cache-Control'] = 'no-store'
|
|
$r.ContentLength64 = $Bytes.Length
|
|
$r.OutputStream.Write($Bytes, 0, $Bytes.Length)
|
|
$r.OutputStream.Close()
|
|
}
|
|
|
|
function Send-Json {
|
|
param($Ctx, $Obj, [int]$Code = 200)
|
|
$json = if ($null -eq $Obj) { 'null' } else { ConvertTo-Json -InputObject $Obj -Depth 10 -Compress }
|
|
Send-Response $Ctx ([Text.Encoding]::UTF8.GetBytes($json)) -Code $Code
|
|
}
|
|
|
|
function Read-Body {
|
|
param($Ctx)
|
|
$sr = New-Object IO.StreamReader($Ctx.Request.InputStream, [Text.Encoding]::UTF8)
|
|
$txt = $sr.ReadToEnd(); $sr.Close()
|
|
if ($txt) { return $txt | ConvertFrom-Json }
|
|
return $null
|
|
}
|
|
|
|
function Get-JsonFile($rel) {
|
|
$p = Join-Path $global:KdsRoot $rel
|
|
if (Test-Path $p) { return [IO.File]::ReadAllText($p, [Text.Encoding]::UTF8) | ConvertFrom-Json }
|
|
return $null
|
|
}
|
|
|
|
# --- Routing ---------------------------------------------------------------------
|
|
function Invoke-Api {
|
|
param($Ctx, [string]$Method, [string]$Path)
|
|
$q = $Ctx.Request.QueryString
|
|
$route = "$Method $Path"
|
|
switch -Regex ($route) {
|
|
'^GET /api/status$' { return Send-Json $Ctx (Get-SystemInfo) }
|
|
'^GET /api/catalog$' { return Send-Json $Ctx (Get-JsonFile 'config\apps.json') }
|
|
'^GET /api/presets$' {
|
|
return Send-Json $Ctx ([pscustomobject]@{ shortcuts = @((Get-JsonFile 'config\shortcuts.json') | ForEach-Object { $_ }); timezones = @(Get-TimeZone -ListAvailable | ForEach-Object { @{ id = $_.Id; name = $_.DisplayName } }) })
|
|
}
|
|
'^GET /api/winget/search$' { return Send-Json $Ctx @(Search-Winget -Query $q['q'] -Source $(if ($q['source']) { $q['source'] } else { 'winget' })) }
|
|
'^GET /api/local-installers$' { return Send-Json $Ctx @(Get-LocalInstallers) }
|
|
'^POST /api/local-installers$' { $b = Read-Body $Ctx; Set-InstallerMeta $b; return Send-Json $Ctx @{ ok = $true } }
|
|
|
|
# Datei-Upload: Body = Rohdaten, ?kind=installers|icons|rdp&name=Datei.ext
|
|
'^POST /api/upload$' {
|
|
$dir, $name = Resolve-UploadTarget $q['kind'] $q['name']
|
|
New-Item $dir -ItemType Directory -Force | Out-Null
|
|
$target = Join-Path $dir $name
|
|
$fs = [IO.File]::Create($target)
|
|
try { $Ctx.Request.InputStream.CopyTo($fs, 1MB) } finally { $fs.Close() }
|
|
Write-Log "Upload: $($q['kind'])\$name ($([math]::Round((Get-Item $target).Length / 1MB, 1)) MB)"
|
|
return Send-Json $Ctx @{ path = $target.Substring($global:KdsRoot.Length + 1); name = $name }
|
|
}
|
|
'^GET /api/files$' {
|
|
$dir = (Resolve-UploadTarget $q['kind'] 'x.ico')[0]
|
|
return Send-Json $Ctx @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object Name -notlike '.*' | Where-Object Name -ne 'installers.json' |
|
|
ForEach-Object { [pscustomobject]@{ name = $_.Name; sizeMB = [math]::Round($_.Length / 1MB, 2) } })
|
|
}
|
|
'^DELETE /api/files$' {
|
|
$dir, $name = Resolve-UploadTarget $q['kind'] $q['name']
|
|
Remove-Item (Join-Path $dir $name) -Force -ErrorAction SilentlyContinue
|
|
if ($q['kind'] -eq 'installers') { Set-InstallerMeta ([pscustomobject]@{ file = "installers\$name"; remove = $true }) }
|
|
return Send-Json $Ctx @{ ok = $true }
|
|
}
|
|
'^POST /api/apps/install$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Programme installieren ($(@($b.apps).Count))" 'Install-Apps' $b) } }
|
|
'^POST /api/apps/upgrade-all$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Alle Programme aktualisieren (winget)' 'Update-AllApps' @{}) } }
|
|
'^POST /api/winget/install$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'winget bereitstellen' 'Install-Winget' @{}) } }
|
|
|
|
'^POST /api/office/xml$' { $b = Read-Body $Ctx; return Send-Response $Ctx ([Text.Encoding]::UTF8.GetBytes((New-OfficeXml $b))) 'application/xml; charset=utf-8' }
|
|
'^POST /api/office/install$' {
|
|
$b = Read-Body $Ctx
|
|
$t = @{ online = 'Office installieren'; download = 'Office-Build auf Stick erstellen'; offline = 'Office offline installieren' }[$b.mode]
|
|
return Send-Json $Ctx @{ job = (Add-KdsJob $t 'Install-Office' $b) }
|
|
}
|
|
'^GET /api/office/configs$' {
|
|
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'office\source') -Directory -ErrorAction SilentlyContinue | ForEach-Object Name)
|
|
}
|
|
|
|
'^GET /api/shortcuts$' { return Send-Json $Ctx @(Get-PublicShortcuts) }
|
|
'^POST /api/shortcuts$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ file = (New-PublicShortcut $b) } }
|
|
'^DELETE /api/shortcuts$' { Remove-PublicShortcut $q['file']; return Send-Json $Ctx @{ ok = $true } }
|
|
'^GET /api/icons$' {
|
|
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'icons') -Include *.ico, *.exe, *.dll -Recurse -ErrorAction SilentlyContinue | ForEach-Object Name)
|
|
}
|
|
'^GET /api/rdpfiles$' {
|
|
return Send-Json $Ctx @(Get-ChildItem (Join-Path $global:KdsRoot 'rdp') -Filter *.rdp -ErrorAction SilentlyContinue | ForEach-Object Name)
|
|
}
|
|
'^POST /api/rdp/preview$' { $b = Read-Body $Ctx; return Send-Response $Ctx ([Text.Encoding]::UTF8.GetBytes((New-RdpContent $b))) 'text/plain; charset=utf-8' }
|
|
|
|
'^POST /api/system$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Systemeinstellungen' 'Set-SystemConfig' $b) } }
|
|
'^POST /api/security$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Sicherheitseinstellungen' 'Set-SecurityConfig' $b) } }
|
|
'^POST /api/power$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Energieeinstellungen' 'Set-PowerConfig' $b) } }
|
|
'^GET /api/power$' {
|
|
$active = (powercfg /getactivescheme) -join ''
|
|
return Send-Json $Ctx @{ active = $active; hibernate = [bool](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue).HibernateEnabled }
|
|
}
|
|
# WLAN
|
|
'^GET /api/wifi$' { return Send-Json $Ctx (Get-WifiState) }
|
|
'^POST /api/wifi/apply$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "WLAN-Profile anlegen ($(@($b.wifi).Count))" 'Add-WifiProfiles' $b) } }
|
|
'^POST /api/wifi/import$' { return Send-Json $Ctx @(Export-WifiProfiles) }
|
|
'^POST /api/wifi/connect$' { $b = Read-Body $Ctx; $o = netsh wlan connect name="$($b.ssid)" 2>&1; return Send-Json $Ctx @{ ok = ($LASTEXITCODE -eq 0); message = ($o -join ' ') } }
|
|
'^DELETE /api/wifi$' { netsh wlan delete profile name="$($q['ssid'])" | Out-Null; return Send-Json $Ctx @{ ok = $true } }
|
|
|
|
# OpenVPN (portabel) + Domaene
|
|
'^GET /api/vpn$' {
|
|
$files = @(Get-ChildItem (Join-Path $global:KdsRoot 'vpn') -File -ErrorAction SilentlyContinue | Where-Object Name -notlike '.*' | ForEach-Object Name)
|
|
return Send-Json $Ctx ([pscustomobject]@{ status = (Get-VpnStatus); portable = (Get-OpenVpnPortableInfo); files = $files })
|
|
}
|
|
'^POST /api/vpn/build$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Portables OpenVPN erstellen' 'New-OpenVpnPortable' @{}) } }
|
|
'^POST /api/vpn/connect$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "VPN verbinden ($($b.vpn.config))" 'Connect-KdsVpn' $b) } }
|
|
'^POST /api/vpn/disconnect$' { $n = Disconnect-KdsVpn; Remove-PortableTap; return Send-Json $Ctx @{ ok = $true; stopped = $n } }
|
|
'^POST /api/vpn/service$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'VPN als Dienst einrichten' 'Install-KdsVpnService' $b) } }
|
|
'^DELETE /api/vpn/service$' { Remove-KdsVpnService; return Send-Json $Ctx @{ ok = $true } }
|
|
'^POST /api/domain/test$' { $b = Read-Body $Ctx; return Send-Json $Ctx (Test-KdsDomain $b.name) }
|
|
'^POST /api/domain/join$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Domaenenbeitritt $($b.domain.name)" 'Join-KdsDomain' $b) } }
|
|
|
|
'^GET /api/bloatware$' { return Send-Json $Ctx @(Get-BloatwareInventory) }
|
|
'^POST /api/bloatware$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob 'Bloatware entfernen' 'Remove-Bloatware' $b) } }
|
|
'^GET /api/bitlocker$' { return Send-Json $Ctx (Get-BitLockerInfo) }
|
|
'^POST /api/bitlocker$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "BitLocker: $($b.bitlocker.action)" 'Set-BitLockerConfig' $b) } }
|
|
|
|
'^POST /api/updates/scan$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Update-Suche ($($b.type))" 'Find-WindowsUpdates' $b) } }
|
|
'^POST /api/updates/install$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Updates installieren ($(if($b.all){'alle'}else{@($b.ids).Count}))" 'Install-WindowsUpdates' $b) } }
|
|
'^POST /api/updates/vendor$' { return Send-Json $Ctx @{ job = (Add-KdsJob 'Hersteller-Treiber-Update' 'Invoke-VendorDriverUpdate' @{}) } }
|
|
'^POST /api/reboot$' { Restart-Computer -Force; return Send-Json $Ctx @{ ok = $true } }
|
|
|
|
# Blaupausen (Standard-Einrichtungen je Kunde) unter blueprints\<id>.json
|
|
'^GET /api/blueprints$' { return Send-Json $Ctx @(Get-Blueprints) }
|
|
'^GET /api/blueprints/(?<n>[^/\\]+)$' {
|
|
$p = Get-BlueprintPath $Matches.n
|
|
if (-not (Test-Path $p)) { return Send-Json $Ctx @{ error = 'Blaupause nicht gefunden' } 404 }
|
|
return Send-Response $Ctx ([IO.File]::ReadAllBytes($p))
|
|
}
|
|
'^POST /api/blueprints/(?<n>[^/\\]+)$' {
|
|
$sr = New-Object IO.StreamReader($Ctx.Request.InputStream, [Text.Encoding]::UTF8); $txt = $sr.ReadToEnd(); $sr.Close()
|
|
$null = $txt | ConvertFrom-Json # nur gueltiges JSON speichern
|
|
$p = Get-BlueprintPath $Matches.n
|
|
New-Item (Split-Path $p) -ItemType Directory -Force | Out-Null
|
|
[IO.File]::WriteAllText($p, $txt, (New-Object Text.UTF8Encoding($false)))
|
|
return Send-Json $Ctx @{ ok = $true }
|
|
}
|
|
'^DELETE /api/blueprints/(?<n>[^/\\]+)$' { Remove-Item (Get-BlueprintPath $Matches.n) -ErrorAction SilentlyContinue; return Send-Json $Ctx @{ ok = $true } }
|
|
'^POST /api/run-profile$' { $b = Read-Body $Ctx; return Send-Json $Ctx @{ job = (Add-KdsJob "Einrichtung: $(@($b.customer, $b.name) -ne '' -join ' / ')" 'Invoke-FullProfile' $b) } }
|
|
|
|
'^GET /api/jobs$' { Update-KdsJobs; return Send-Json $Ctx @($script:Jobs.Values | ForEach-Object { Get-KdsJobView $_ }) }
|
|
'^GET /api/jobs/(?<id>\d+)$' {
|
|
Update-KdsJobs
|
|
$e = $script:Jobs[$Matches.id]
|
|
if (-not $e) { return Send-Json $Ctx @{ error = 'unbekannt' } 404 }
|
|
return Send-Json $Ctx (Get-KdsJobView $e -Full)
|
|
}
|
|
'^DELETE /api/jobs/(?<id>\d+)$' {
|
|
$e = $script:Jobs[$Matches.id]
|
|
if ($e -and $e.job) { Stop-Job $e.job }
|
|
elseif ($e) { $e.state = 'Abgebrochen'; $script:Queue = New-Object System.Collections.Queue (@($script:Queue.ToArray() | Where-Object { $_ -ne $e.id })) }
|
|
return Send-Json $Ctx @{ ok = $true }
|
|
}
|
|
'^POST /api/shutdown$' { Send-Json $Ctx @{ ok = $true }; $script:Running = $false; return }
|
|
default { return Send-Json $Ctx @{ error = "Unbekannte Route: $route" } 404 }
|
|
}
|
|
}
|
|
|
|
# --- Server starten --------------------------------------------------------------
|
|
$listener = New-Object System.Net.HttpListener
|
|
if ($Remote) {
|
|
$listener.Prefixes.Add("http://+:$Port/")
|
|
if (-not (Get-NetFirewallRule -DisplayName "KDS Install Agent $Port" -ErrorAction SilentlyContinue)) {
|
|
New-NetFirewallRule -DisplayName "KDS Install Agent $Port" -Direction Inbound -Protocol TCP -LocalPort $Port -Action Allow -Profile Domain, Private | Out-Null
|
|
}
|
|
} else {
|
|
$listener.Prefixes.Add("http://localhost:$Port/")
|
|
$listener.Prefixes.Add("http://127.0.0.1:$Port/")
|
|
}
|
|
$listener.Start()
|
|
|
|
$localUrl = "http://localhost:$Port/?token=$Token"
|
|
Write-Host ''
|
|
Write-Host ' KDS Install Agent laeuft' -ForegroundColor Cyan
|
|
Write-Host " Lokal: $localUrl"
|
|
if ($Remote) {
|
|
Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' } | ForEach-Object {
|
|
Write-Host " Netzwerk: http://$($_.IPAddress):$Port/?token=$Token" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
Write-Host " Logs: $LogDir"
|
|
Write-Host ' Beenden: Button "Beenden" in der Oberflaeche oder dieses Fenster schliessen.' -ForegroundColor DarkGray
|
|
Write-Host ''
|
|
|
|
if (-not $NoWindow) {
|
|
$edge = @("${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
if ($edge) {
|
|
$profileDir = Join-Path $env:TEMP 'KDS-Agent-Edge'
|
|
Start-Process $edge -ArgumentList "--app=$localUrl", '--window-size=1400,920', "--user-data-dir=`"$profileDir`"", '--no-first-run', '--no-default-browser-check'
|
|
} else { Start-Process $localUrl }
|
|
}
|
|
|
|
$WebRoot = Join-Path $PSScriptRoot 'web'
|
|
$script:Running = $true
|
|
while ($script:Running -and $listener.IsListening) {
|
|
$async = $listener.BeginGetContext($null, $null)
|
|
while (-not $async.AsyncWaitHandle.WaitOne(1000)) { Update-KdsJobs }
|
|
$ctx = $listener.EndGetContext($async)
|
|
$path = [Uri]::UnescapeDataString($ctx.Request.Url.AbsolutePath)
|
|
try {
|
|
if ($path.StartsWith('/api/')) {
|
|
$t = $ctx.Request.Headers['X-Token']
|
|
if (-not $t) { $t = $ctx.Request.QueryString['token'] }
|
|
if ($t -ne $Token) { Send-Json $ctx @{ error = 'Ungueltiges Token' } 401; continue }
|
|
Invoke-Api $ctx $ctx.Request.HttpMethod $path
|
|
} else {
|
|
if ($path -eq '/') { $path = '/index.html' }
|
|
$file = [IO.Path]::GetFullPath((Join-Path $WebRoot $path.TrimStart('/')))
|
|
if ($file.StartsWith($WebRoot) -and (Test-Path $file -PathType Leaf)) {
|
|
$type = $Mime[[IO.Path]::GetExtension($file)]; if (-not $type) { $type = 'application/octet-stream' }
|
|
Send-Response $ctx ([IO.File]::ReadAllBytes($file)) $type
|
|
} else { Send-Response $ctx ([Text.Encoding]::UTF8.GetBytes('Not found')) 'text/plain' 404 }
|
|
}
|
|
} catch {
|
|
Write-Log "API-Fehler $path : $($_.Exception.Message)" 'ERROR'
|
|
try { Send-Json $ctx @{ error = $_.Exception.Message } 500 } catch { }
|
|
}
|
|
}
|
|
|
|
$listener.Stop()
|
|
Get-Job | Stop-Job -ErrorAction SilentlyContinue
|
|
Write-Host 'Agent beendet.'
|