# 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 } }