From eae5cb14aacf472ee3506e01369dc12648601e17 Mon Sep 17 00:00:00 2001 From: Stephan Yelle Date: Tue, 9 Dec 2025 23:03:36 -0500 Subject: [PATCH] Add test/Samy.Http.ps1 --- test/Samy.Http.ps1 | 220 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 test/Samy.Http.ps1 diff --git a/test/Samy.Http.ps1 b/test/Samy.Http.ps1 new file mode 100644 index 0000000..b7da726 --- /dev/null +++ b/test/Samy.Http.ps1 @@ -0,0 +1,220 @@ +# Samy.Http.ps1 +# HTTP helpers, listener, and dispatcher + +function Send-Text { + param($Context, $Text) + if (-not $Context -or -not $Context.Response) { + return + } + $bytes = [Text.Encoding]::UTF8.GetBytes([string]$Text) + $Context.Response.ContentType = 'text/plain' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) + $Context.Response.OutputStream.Close() +} + +function Send-HTML { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][object] $Context, + [Parameter(Mandatory = $true)][string] $Html + ) + if (-not $Context -or -not $Context.Response) { + return + } + $bytes = [Text.Encoding]::UTF8.GetBytes($Html) + $Context.Response.ContentType = 'text/html' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) + $Context.Response.OutputStream.Close() +} + +function Send-JSON { + [CmdletBinding()] + param( + $Context, + $Object + ) + + if (-not $Context -or -not $Context.Response) { + return + } + + try { + if ($null -eq $Object) { + Write-LogHybrid "Send-JSON called with `$null object; returning empty JSON array." Warning Printers -LogToEvent + $json = '[]' + } + else { + try { + $json = $Object | ConvertTo-Json -Depth 5 -ErrorAction Stop + } + catch { + Write-LogHybrid "Send-JSON serialization failed: $($_.Exception.Message); returning empty JSON array." Error Printers -LogToEvent + $json = '[]' + } + } + + $json = [string]$json + $bytes = [Text.Encoding]::UTF8.GetBytes($json) + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) + $Context.Response.OutputStream.Close() + } + catch { + Write-LogHybrid "Send-JSON fatal error: $($_.Exception.Message)" Error Printers -LogToEvent + try { + $fallback = '[]' + $bytes = [Text.Encoding]::UTF8.GetBytes($fallback) + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) + $Context.Response.OutputStream.Close() + } + catch { + } + } +} + +function Invoke-TasksCompleted { + param($Context) + + Write-LogHybrid "All UI-selected tasks processed" Info UI -LogToEvent + Send-Text $Context "Tasks completion acknowledged." +} + +function Get-NextFreePort { + param([int]$Start) + + for ($p = [Math]::Max(1024,$Start); $p -lt 65535; $p++) { + $l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p) + try { + $l.Start() + $l.Stop() + return $p + } + catch { + } + } + throw "No free TCP port available." +} + +function Dispatch-Request { + param($Context) + + $path = $Context.Request.Url.AbsolutePath.TrimStart('/') + + if ($path -eq 'quit') { + Write-LogHybrid "Shutdown requested" Info Server -LogToEvent + Send-Text $Context "Server shutting down." + $Global:Listener.Stop() + return + } + + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'tasksCompleted') { + Invoke-TasksCompleted $Context + return + } + + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') { + Invoke-FetchSites $Context + return + } + + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'renameComputer') { + Invoke-RenameComputer $Context + return + } + + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getprinters') { + Invoke-GetPrinters $Context + return + } + + if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'installprinters') { + Invoke-InstallPrinters $Context + return + } + + if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) { + $page = if ($path -eq '') { 'onboard' } else { $path } + $html = Get-UIHtml -Page $page + Send-HTML $Context $html + return + } + + $task = $Global:SamyTasks | Where-Object Name -EQ $path + if ($task) { + & $task.HandlerFn $Context + return + } + + $Context.Response.StatusCode = 404 + Send-Text $Context '404 - Not Found' +} + +function Start-SamyHttpServer { + param( + [Parameter(Mandatory)][int]$Port + ) + + $Global:Listener = [System.Net.HttpListener]::new() + $primaryPrefix = "http://localhost:$Port/" + $wildcardPrefix = "http://+:$Port/" + + try { + $Global:Listener.Prefixes.Add($primaryPrefix) + $Global:Listener.Start() + Write-LogHybrid "Listening on $primaryPrefix" Info Server -LogToEvent + } + catch [System.Net.HttpListenerException] { + if ($_.Exception.ErrorCode -eq 5) { + Write-LogHybrid "Access denied on $primaryPrefix. Attempting URL ACL..." Warning Server -LogToEvent + try { + $user = "$env:USERDOMAIN\$env:USERNAME" + if (-not $user.Trim()) { + $user = $env:USERNAME + } + Start-Process -FilePath "netsh" -ArgumentList "http add urlacl url=$wildcardPrefix user=`"$user`" listen=yes" -Verb RunAs -WindowStyle Hidden -Wait + $Global:Listener = [System.Net.HttpListener]::new() + $Global:Listener.Prefixes.Add($wildcardPrefix) + $Global:Listener.Start() + Write-LogHybrid "Listening on $wildcardPrefix (URL ACL added for $user)" Success Server -LogToEvent + } + catch { + Write-LogHybrid "URL ACL registration failed: $($_.Exception.Message)" Error Server -LogToEvent + return + } + } + elseif ($_.Exception.NativeErrorCode -in 32,183) { + $old = $Port + $Port = Get-NextFreePort -Start ($Port + 1) + $Global:Listener = [System.Net.HttpListener]::new() + $primaryPrefix = "http://localhost:$Port/" + $Global:Listener.Prefixes.Add($primaryPrefix) + $Global:Listener.Start() + Write-LogHybrid "Port $old busy. Listening on $primaryPrefix" Warning Server -LogToEvent + } + else { + Write-LogHybrid "HttpListener start failed: $($_.Exception.Message)" Error Server -LogToEvent + return + } + } + + try { + while ($Global:Listener.IsListening) { + $ctx = $Global:Listener.GetContext() + try { + Dispatch-Request $ctx + } + catch { + Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent + } + } + } + finally { + $Global:Listener.Close() + Write-LogHybrid "Listener closed." Info Server -LogToEvent + } +}