http.server.psm1
function Start-HttpServer { param( [int]$Port = 8000, [string]$Root = (Get-Location), [switch]$Public, [Alias('if')][string]$IndexFile ) if (-not (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue)) { Write-Error "Start-ThreadJob is not available. Run: Install-Module ThreadJob" return } if ($Global:HttpServerJob -and ($Global:HttpServerJob.State -eq 'Running')) { Write-Host "[-] Server already running (Job ID: $($Global:HttpServerJob.Id)). Use Stop-HttpServer to stop it." return } $bind = if ($Public) { "*" } else { "localhost" } $prefix = "http://$bind`:$Port/" $rootFull = (Resolve-Path $Root).Path # Only serve a file if explicitly provided, no default $ServeFile = if ($IndexFile) { (Resolve-Path (Join-Path $rootFull $IndexFile)).Path } else { $null } # MIME type lookup table $mimeMap = @{ '.html' = 'text/html' '.htm' = 'text/html' '.png' = 'image/png' '.jpg' = 'image/jpeg' '.jpeg' = 'image/jpeg' '.svg' = 'image/svg+xml' '.js' = 'application/javascript' '.css' = 'text/css' '.json' = 'application/json' } $job = Start-ThreadJob -Name 'PowerShellHttpServer' -ScriptBlock { param($prefix, $rootFull, $ServeFile, $mimeMap) try { $listener = [System.Net.HttpListener]::new() $listener.Prefixes.Add($prefix) $listener.Start() Write-Output "[+] Serving at $prefix" while ($listener.IsListening) { $ctx = $listener.GetContext() $req = $ctx.Request $res = $ctx.Response # Serve specified file at root if ($ServeFile -and $req.Url.AbsolutePath -eq '/') { if (Test-Path $ServeFile) { $bytes = [System.IO.File]::ReadAllBytes($ServeFile) $ext = [IO.Path]::GetExtension($ServeFile).ToLower() $type = $mimeMap[$ext] ? $mimeMap[$ext] : 'application/octet-stream' $res.ContentType = $type $res.ContentLength64 = $bytes.Length $res.OutputStream.Write($bytes, 0, $bytes.Length) $res.OutputStream.Close() continue } } # Fallback plain-text response $message = 'Hello from PowerShell HTTP server!' $buf = [System.Text.Encoding]::UTF8.GetBytes($message) $res.ContentType = 'text/plain' $res.ContentLength64 = $buf.Length $res.OutputStream.Write($buf, 0, $buf.Length) $res.OutputStream.Close() } } catch { Write-Output "[-] Error: $($_.Exception.Message)" } finally { $listener?.Stop() } } -ArgumentList $prefix, $rootFull, $ServeFile, $mimeMap $Global:HttpServerJob = $job Start-Sleep -Milliseconds 500 Receive-Job -Id $job.Id | Write-Host Write-Host "[*] Server running. Use Stop-HttpServer to stop it." } function Stop-HttpServer { $job = $Global:HttpServerJob if (-not $job -or $job.State -ne 'Running') { $job = Get-Job -Name 'PowerShellHttpServer' -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Running' } } if ($job) { Stop-Job $job.Id -ErrorAction SilentlyContinue while ((Get-Job -Id $job.Id).State -ne 'Stopped') { Start-Sleep -Milliseconds 100 } Remove-Job $job.Id -ErrorAction SilentlyContinue Remove-Variable -Name HttpServerJob -Scope Global -ErrorAction SilentlyContinue Write-Host "[*] HTTP server stopped and cleaned up." } else { Write-Host "[-] No running HTTP server job found." } } Export-ModuleMember -Function Start-HttpServer, Stop-HttpServer function Stop-HttpServer { $job = $Global:HttpServerJob if (-not $job -or $job.State -ne 'Running') { # Fallback: try to find the job by name $job = Get-Job -Name 'PowerShellHttpServer' -ErrorAction SilentlyContinue | Where-Object { $_.State -eq 'Running' } } if ($job) { try { Stop-Job $job.Id -ErrorAction SilentlyContinue while ((Get-Job -Id $job.Id).State -ne 'Stopped') { Start-Sleep -Milliseconds 100 } Remove-Job $job.Id -ErrorAction SilentlyContinue Remove-Variable -Name HttpServerJob -Scope Global -ErrorAction SilentlyContinue Write-Host "[*] HTTP server stopped and cleaned up." } catch { Write-Host "[-] Error stopping job: $_" } } else { Write-Host "[-] No running HTTP server job found." } } Export-ModuleMember -Function Start-HttpServer, Stop-HttpServer |