Public/Start-Stagecoach.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Start-Stagecoach { <# .SYNOPSIS Starts the local Stagecoach web server and opens the operator interface in the default browser. .DESCRIPTION Starts a lightweight localhost web server on 127.0.0.1 serving stagecoach.html and bridging web UI button clicks to AzureStagecoach PowerShell cmdlets. .PARAMETER Port The local TCP port to bind (default: 8085). .PARAMETER NoBrowser If specified, prevents automatically opening the default web browser. .EXAMPLE Start-Stagecoach #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $false)] [int]$Port = 8085, [Parameter(Mandatory = $false)] [switch]$NoBrowser ) $uiUrl = "http://127.0.0.1:$Port/" if (-not $PSCmdlet.ShouldProcess($uiUrl, 'Start Stagecoach Local Web Server')) { return } $webPath = Join-Path -Path $PSScriptRoot -ChildPath '..\Web\stagecoach.html' if (-not (Test-Path $webPath)) { throw "Could not locate web UI file at '$webPath'." } Write-Information "[Stagecoach] Initializing listener on $uiUrl..." -InformationAction Continue $listener = [System.Net.HttpListener]::new() $listener.Prefixes.Add($uiUrl) $listener.Start() Write-Information "[Stagecoach] Server active. Press Ctrl+C in terminal to stop." -InformationAction Continue if (-not $NoBrowser) { Start-Process $uiUrl } try { while ($listener.IsListening) { $context = $listener.GetContext() $request = $context.Request $response = $context.Response # Enable CORS for localhost $response.Headers.Add('Access-Control-Allow-Origin', '*') $response.Headers.Add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') $response.Headers.Add('Access-Control-Allow-Headers', 'Content-Type, Authorization') if ($request.HttpMethod -eq 'OPTIONS') { $response.StatusCode = 204 $response.Close() continue } $rawPath = $request.Url.AbsolutePath # Route: Static UI if ($rawPath -eq '/' -or $rawPath -eq '/index.html' -or $rawPath -eq '/stagecoach.html') { $htmlContent = [System.IO.File]::ReadAllBytes($webPath) $response.ContentType = 'text/html; charset=utf-8' $response.ContentLength64 = $htmlContent.Length $response.OutputStream.Write($htmlContent, 0, $htmlContent.Length) $response.Close() continue } # Route: GET /api/inventory (Scan Azure Resource Graph) if ($rawPath -eq '/api/inventory' -and $request.HttpMethod -eq 'GET') { try { $inventory = @(Get-StagecoachInventory) $json = $inventory | ConvertTo-Json -Depth 5 $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) $response.ContentType = 'application/json; charset=utf-8' $response.ContentLength64 = $bytes.Length $response.OutputStream.Write($bytes, 0, $bytes.Length) } catch { $errObj = @{ error = $_.Exception.Message } | ConvertTo-Json $bytes = [System.Text.Encoding]::UTF8.GetBytes($errObj) $response.StatusCode = 500 $response.ContentType = 'application/json' $response.ContentLength64 = $bytes.Length $response.OutputStream.Write($bytes, 0, $bytes.Length) } $response.Close() continue } # Route: POST /api/credentials (Resolve LAPS / Domain / Key Vault) if ($rawPath -eq '/api/credentials' -and $request.HttpMethod -eq 'POST') { try { $reader = [System.IO.StreamReader]::new($request.InputStream, $request.ContentEncoding) $bodyJson = $reader.ReadToEnd() $targetData = $bodyJson | ConvertFrom-Json $target = [StagecoachTarget]::new() $target.Id = $targetData.Id $target.Name = $targetData.Name $target.ResourceGroup = $targetData.ResourceGroup $target.SubscriptionId = $targetData.SubscriptionId $target.Kind = [StagecoachTargetKind]::$($targetData.Kind) $target.DomainName = $targetData.DomainName $target.DomainType = [StagecoachDomainType]::$($targetData.DomainType) if ($targetData.Tags) { foreach ($prop in $targetData.Tags.PSObject.Properties) { $target.Tags[$prop.Name] = [string]$prop.Value } } $cred = Get-StagecoachCredential -Target $target $result = if ($cred) { $cred } else { @{ Source = 'None'; Username = ''; Password = '' } } $json = $result | ConvertTo-Json $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) $response.ContentType = 'application/json; charset=utf-8' $response.ContentLength64 = $bytes.Length $response.OutputStream.Write($bytes, 0, $bytes.Length) } catch { $errObj = @{ error = $_.Exception.Message } | ConvertTo-Json $bytes = [System.Text.Encoding]::UTF8.GetBytes($errObj) $response.StatusCode = 500 $response.ContentType = 'application/json' $response.ContentLength64 = $bytes.Length $response.OutputStream.Write($bytes, 0, $bytes.Length) } $response.Close() continue } # Route: POST /api/connect (Launch Session via PowerShell Cmdlet) if ($rawPath -eq '/api/connect' -and $request.HttpMethod -eq 'POST') { try { $reader = [System.IO.StreamReader]::new($request.InputStream, $request.ContentEncoding) $bodyJson = $reader.ReadToEnd() $reqData = $bodyJson | ConvertFrom-Json $target = [StagecoachTarget]::new() $target.Id = $reqData.Target.Id $target.Name = $reqData.Target.Name $target.ResourceGroup = $reqData.Target.ResourceGroup $target.Kind = [StagecoachTargetKind]::$($reqData.Target.Kind) $target.DomainName = $reqData.Target.DomainName $target.DomainType = [StagecoachDomainType]::$($reqData.Target.DomainType) $session = Connect-StagecoachVM -Target $target -LocalUser $reqData.Username -Rdp $true $json = $session | ConvertTo-Json $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) $response.ContentType = 'application/json; charset=utf-8' $response.ContentLength64 = $bytes.Length $response.OutputStream.Write($bytes, 0, $bytes.Length) } catch { $errObj = @{ error = $_.Exception.Message } | ConvertTo-Json $bytes = [System.Text.Encoding]::UTF8.GetBytes($errObj) $response.StatusCode = 500 $response.ContentType = 'application/json' $response.ContentLength64 = $bytes.Length $response.OutputStream.Write($bytes, 0, $bytes.Length) } $response.Close() continue } # Fallback 404 $response.StatusCode = 404 $response.Close() } } finally { if ($listener -and $listener.IsListening) { $listener.Stop() $listener.Close() Write-Information "[Stagecoach] Server listener stopped." -InformationAction Continue } } } |