Skip to content

Instantly share code, notes, and snippets.

@anonhostpi
Last active September 1, 2025 10:05
Show Gist options
  • Select an option

  • Save anonhostpi/1cc0084b959a9ea9e97dca9dce414e1f to your computer and use it in GitHub Desktop.

Select an option

Save anonhostpi/1cc0084b959a9ea9e97dca9dce414e1f to your computer and use it in GitHub Desktop.

Revisions

  1. anonhostpi revised this gist Aug 31, 2025. 1 changed file with 29 additions and 269 deletions.
    298 changes: 29 additions & 269 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -38,7 +38,7 @@ function New-Webserver {

    $Server = New-Object psobject -Property @{
    Binding = $Binding
    BaseDirectory = "$(Resolve-Path $BaseDirectory)".TrimEnd('\/')
    BaseDirectory = "$(Resolve-Path $BaseDirectory -ErrorAction SilentlyContinue)".TrimEnd('\/')
    Name = $Name
    Routes = $Routes
    Caching = [bool]$Cache
    @@ -100,7 +100,7 @@ function New-Webserver {
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Read -Value (& {
    If( $Reader -ne $null ) {
    If( $null -ne $Reader ) {
    return $Reader
    }

    @@ -133,254 +133,11 @@ function New-Webserver {
    throw "Invalid path '$Path'."
    }

    return (Get-Content "$root\$Path" -Raw -ErrorAction SilentlyContinue)
    }
    })

    $Server | Add-Member -MemberType ScriptMethod -Name Serve -Value {
    param(
    [string] $File,
    $Response
    )

    If( $null -ne $this.Cached[$File] ) {
    $Response.ContentType = $this.Cached[$File].Type
    return $this.Cached[$File].Content
    }

    Try {
    $content = $this.Read($File)
    $mimetype = $this.ConvertExtension( $extension )

    If( $this.Caching -and -not [string]::IsNullOrWhitespace($content) ) {
    $this.Cached[$File] = @{
    Content = $content
    Type = $mimetype
    }
    }

    $Response.ContentType = $mimetype
    return $content
    } Catch {
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name ParseQuery -Value {
    param( $Request )

    return [System.Web.HttpUtility]::ParseQueryString($Request.Url.Query)
    }

    $Server | Add-Member -MemberType ScriptMethod -Name ParseBody -Value {
    param( $Request )

    If( -not $Request.HasEntityBody -or $Request.ContentLength64 -le 0 ) {
    return $null
    }

    $stream = $Request.InputStream
    $encoding = $Request.ContentEncoding
    $reader = New-Object System.IO.StreamReader( $stream, $encoding )
    $body = $reader.ReadToEnd()

    $reader.Close()
    $stream.Close()

    switch -Wildcard ( $Request.ContentType ) {
    "application/x-www-form-urlencoded*" {
    return [System.Web.HttpUtility]::ParseQueryString($body)
    }
    "application/json*" {
    return $body | ConvertFrom-Json
    }
    "text/xml*" {
    return [xml]$body
    }
    default {
    return $body
    return @{
    Path = $file
    Content = (Get-Content "$root\$Path" -Raw -ErrorAction SilentlyContinue)
    }
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Stop -Value {
    param()

    If( $null -ne $this.Listener -and $this.Listener.IsListening ) {
    $this.Listener.Stop()
    $this.Listener.Close()
    $this.Listener = $null
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Start -Value {
    param()

    $this.Listener = New-Object System.Net.HttpListener
    $this.Listener.Prefixes.Add($this.Binding)
    $this.Listener.Start()

    (& {
    Try {
    While ( $this.Listener.IsListening ) {
    <# $task = $this.Listener.GetContextAsync()
    if (-not ($task.Wait(300))) {
    Write-Host "Polling..."
    continue
    } #>

    # $context = $this.Listener.GetContext() # $task.Result

    $task = $this.Listener.GetContextAsync()
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $this.Listener.IsListening ) { return }
    }

    $context = $task.GetAwaiter().GetResult()
    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath

    If( $this.Routes.Before -is [scriptblock] ) {
    $continue = & $this.Routes.Before $Server $command $this.Listener $context
    If( -not $continue ) {
    continue
    }
    function New-Webserver {
    param(
    [string] $Binding = "http://localhost:8080/",
    [string] $BaseDirectory = "$(Get-Location -PSProvider FileSystem)",
    [string] $Name = "PowerShell Web Server",

    [System.Collections.IDictionary] $Routes = @{
    Before = { param( $Server, $Command, $Listener, $Context ) return $true }
    After = { param( $Server, $Command, $Listener, $Context ) return $true }

    # "GET /" = { param( $Server, $Command, $request, $response )... }
    # "GET /hello" = "./path/to/static/file.html"

    Default = {
    param( $Server, $Command, $Request, $Response )
    $Command = $Command -split " ", 2
    $path = $Command | Select-Object -Index 1

    return $Server.Serve( $path, $Response )
    }
    },
    [switch] $Cache,
    [scriptblock] $Reader
    )

    If( $Routes -eq $null ) {
    $Routes = [ordered]@{}
    }
    If( $Routes.Default -eq $null ) {
    $Routes.Default = {
    param( $Server, $Command, $Request, $Response )
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    }

    $Server = New-Object psobject -Property @{
    Binding = $Binding
    BaseDirectory = "$(Resolve-Path $BaseDirectory)".TrimEnd('\/')
    Name = $Name
    Routes = $Routes
    Caching = [bool]$Cache
    Cached = @{}
    Listener = $null
    }

    $Server | Add-Member -MemberType ScriptMethod -Name ConvertExtension -Value {
    param( [string] $Extension )

    switch( $Extension.ToLower() ) {
    ".html" { "text/html; charset=utf-8" }
    ".htm" { "text/html; charset=utf-8" }
    ".css" { "text/css; charset=utf-8" }
    ".js" { "application/javascript; charset=utf-8" }

    ".json" { "application/json; charset=utf-8" }
    ".txt" { "text/plain; charset=utf-8" }
    ".xml" { "application/xml; charset=utf-8" }
    ".csv" { "text/csv; charset=utf-8" }
    ".tsv" { "text/tab-separated-values; charset=utf-8" }
    ".md" { "text/markdown; charset=utf-8" }

    ".png" { "image/png" }
    ".jpg" { "image/jpeg" }
    ".jpeg" { "image/jpeg" }
    ".gif" { "image/gif" }
    ".svg" { "image/svg+xml" }
    ".ico" { "image/x-icon" }
    ".bmp" { "image/bmp" }
    ".avif" { "image/avif" }
    ".webp" { "image/webp" }

    ".mp4" { "video/mp4" }
    ".webm" { "video/webm" }
    ".avi" { "video/x-msvideo" }
    ".mov" { "video/quicktime" }

    ".mp3" { "audio/mpeg" }
    ".wav" { "audio/wav" }
    ".ogg" { "audio/ogg" }

    ".pdf" { "application/pdf" }

    ".zip" { "application/zip" }
    ".tar" { "application/x-tar" }
    ".gz" { "application/gzip" }
    ".7z" { "application/x-7z-compressed" }
    ".bz2" { "application/x-bzip2" }

    ".woff" { "font/woff" }
    ".woff2" { "font/woff2" }
    ".ttf" { "font/ttf" }
    ".eot" { "application/vnd.ms-fontobject" }
    ".otf" { "font/otf" }

    default { "application/octet-stream" }
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Read -Value (& {
    If( $Reader -ne $null ) {
    return $Reader
    }

    return {
    param( [string] $Path )

    $root = $this.BaseDirectory

    $Path = $Path.TrimStart('\/')
    $file = "$root\$Path".TrimEnd('\/')
    $file = Try {
    Resolve-Path $file -ErrorAction Stop
    } Catch {
    Try {
    Resolve-Path "$file.html" -ErrorAction Stop
    } Catch {
    Resolve-Path "$file\index.html" -ErrorAction SilentlyContinue
    }
    }
    $file = "$file"

    # Throw on directory traversal attacks and invalid paths
    $bad = @(
    [string]::IsNullOrWhitespace($file),
    -not (Test-Path $file -PathType Leaf -ErrorAction SilentlyContinue),
    -not ($file -like "$root*")
    )

    if ( $bad -contains $true ) {
    throw "Invalid path '$Path'."
    }

    return (Get-Content "$root\$Path" -Raw -ErrorAction SilentlyContinue)
    }
    })

    $Server | Add-Member -MemberType ScriptMethod -Name Serve -Value {
    @@ -395,7 +152,10 @@ function New-Webserver {
    }

    Try {
    $content = $this.Read($File)
    $result = $this.Read($File)
    $content = $result.Content

    $extension = [System.IO.Path]::GetExtension($result.Path)
    $mimetype = $this.ConvertExtension( $extension )

    If( $this.Caching -and -not [string]::IsNullOrWhitespace($content) ) {
    @@ -451,8 +211,6 @@ function New-Webserver {
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Stop -Value {
    param()

    If( $null -ne $this.Listener -and $this.Listener.IsListening ) {
    $this.Listener.Stop()
    $this.Listener.Close()
    @@ -461,8 +219,6 @@ function New-Webserver {
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Start -Value {
    param()

    $this.Listener = New-Object System.Net.HttpListener
    $this.Listener.Prefixes.Add($this.Binding)
    $this.Listener.Start()
    @@ -479,7 +235,7 @@ function New-Webserver {
    # $context = $this.Listener.GetContext() # $task.Result

    $task = $this.Listener.GetContextAsync()
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    While( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $this.Listener.IsListening ) { return }
    }

    @@ -488,8 +244,8 @@ function New-Webserver {
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath

    If( $this.Routes.Before -is [scriptblock] ) {
    $continue = & $this.Routes.Before $Server $command $this.Listener $context
    If( -not $continue ) {
    $allow = & $this.Routes.Before $Server $command $this.Listener $context
    If( -not $allow ) {
    continue
    }
    }
    @@ -507,28 +263,32 @@ function New-Webserver {
    If( $route -is [scriptblock] ) {
    & $route $this $command $request $response
    } Else {
    & $this.Serve $route $response
    $this.Serve( $route, $response )
    }
    } Catch {
    $response.StatusCode = 500
    "500 Internal Server Error`n`n$($_.Exception.Message)"
    }

    if( -not [string]::IsNullOrWhiteSpace($result) ) {
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
    $response.ContentLength64 = $buffer.Length

    If( [string]::IsNullOrWhiteSpace($response.Headers["Last-Modified"]) ){
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $Name)
    }

    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
    Try {
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
    $response.ContentLength64 = $buffer.Length

    If( [string]::IsNullOrWhiteSpace($response.Headers["Last-Modified"]) ){
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $Name)
    }

    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
    } Catch {}
    }

    $response.Close()
    Try {
    $response.Close()
    } Catch {}

    If( $this.Routes.After -is [scriptblock] ) {
    & $this.Routes.After $Server $command $this.Listener $context
  2. anonhostpi revised this gist Aug 31, 2025. 1 changed file with 247 additions and 1 deletion.
    248 changes: 247 additions & 1 deletion webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -214,6 +214,252 @@ function New-Webserver {
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Start -Value {
    param()

    $this.Listener = New-Object System.Net.HttpListener
    $this.Listener.Prefixes.Add($this.Binding)
    $this.Listener.Start()

    (& {
    Try {
    While ( $this.Listener.IsListening ) {
    <# $task = $this.Listener.GetContextAsync()
    if (-not ($task.Wait(300))) {
    Write-Host "Polling..."
    continue
    } #>

    # $context = $this.Listener.GetContext() # $task.Result

    $task = $this.Listener.GetContextAsync()
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $this.Listener.IsListening ) { return }
    }

    $context = $task.GetAwaiter().GetResult()
    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath

    If( $this.Routes.Before -is [scriptblock] ) {
    $continue = & $this.Routes.Before $Server $command $this.Listener $context
    If( -not $continue ) {
    continue
    }
    function New-Webserver {
    param(
    [string] $Binding = "http://localhost:8080/",
    [string] $BaseDirectory = "$(Get-Location -PSProvider FileSystem)",
    [string] $Name = "PowerShell Web Server",

    [System.Collections.IDictionary] $Routes = @{
    Before = { param( $Server, $Command, $Listener, $Context ) return $true }
    After = { param( $Server, $Command, $Listener, $Context ) return $true }

    # "GET /" = { param( $Server, $Command, $request, $response )... }
    # "GET /hello" = "./path/to/static/file.html"

    Default = {
    param( $Server, $Command, $Request, $Response )
    $Command = $Command -split " ", 2
    $path = $Command | Select-Object -Index 1

    return $Server.Serve( $path, $Response )
    }
    },
    [switch] $Cache,
    [scriptblock] $Reader
    )

    If( $Routes -eq $null ) {
    $Routes = [ordered]@{}
    }
    If( $Routes.Default -eq $null ) {
    $Routes.Default = {
    param( $Server, $Command, $Request, $Response )
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    }

    $Server = New-Object psobject -Property @{
    Binding = $Binding
    BaseDirectory = "$(Resolve-Path $BaseDirectory)".TrimEnd('\/')
    Name = $Name
    Routes = $Routes
    Caching = [bool]$Cache
    Cached = @{}
    Listener = $null
    }

    $Server | Add-Member -MemberType ScriptMethod -Name ConvertExtension -Value {
    param( [string] $Extension )

    switch( $Extension.ToLower() ) {
    ".html" { "text/html; charset=utf-8" }
    ".htm" { "text/html; charset=utf-8" }
    ".css" { "text/css; charset=utf-8" }
    ".js" { "application/javascript; charset=utf-8" }

    ".json" { "application/json; charset=utf-8" }
    ".txt" { "text/plain; charset=utf-8" }
    ".xml" { "application/xml; charset=utf-8" }
    ".csv" { "text/csv; charset=utf-8" }
    ".tsv" { "text/tab-separated-values; charset=utf-8" }
    ".md" { "text/markdown; charset=utf-8" }

    ".png" { "image/png" }
    ".jpg" { "image/jpeg" }
    ".jpeg" { "image/jpeg" }
    ".gif" { "image/gif" }
    ".svg" { "image/svg+xml" }
    ".ico" { "image/x-icon" }
    ".bmp" { "image/bmp" }
    ".avif" { "image/avif" }
    ".webp" { "image/webp" }

    ".mp4" { "video/mp4" }
    ".webm" { "video/webm" }
    ".avi" { "video/x-msvideo" }
    ".mov" { "video/quicktime" }

    ".mp3" { "audio/mpeg" }
    ".wav" { "audio/wav" }
    ".ogg" { "audio/ogg" }

    ".pdf" { "application/pdf" }

    ".zip" { "application/zip" }
    ".tar" { "application/x-tar" }
    ".gz" { "application/gzip" }
    ".7z" { "application/x-7z-compressed" }
    ".bz2" { "application/x-bzip2" }

    ".woff" { "font/woff" }
    ".woff2" { "font/woff2" }
    ".ttf" { "font/ttf" }
    ".eot" { "application/vnd.ms-fontobject" }
    ".otf" { "font/otf" }

    default { "application/octet-stream" }
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Read -Value (& {
    If( $Reader -ne $null ) {
    return $Reader
    }

    return {
    param( [string] $Path )

    $root = $this.BaseDirectory

    $Path = $Path.TrimStart('\/')
    $file = "$root\$Path".TrimEnd('\/')
    $file = Try {
    Resolve-Path $file -ErrorAction Stop
    } Catch {
    Try {
    Resolve-Path "$file.html" -ErrorAction Stop
    } Catch {
    Resolve-Path "$file\index.html" -ErrorAction SilentlyContinue
    }
    }
    $file = "$file"

    # Throw on directory traversal attacks and invalid paths
    $bad = @(
    [string]::IsNullOrWhitespace($file),
    -not (Test-Path $file -PathType Leaf -ErrorAction SilentlyContinue),
    -not ($file -like "$root*")
    )

    if ( $bad -contains $true ) {
    throw "Invalid path '$Path'."
    }

    return (Get-Content "$root\$Path" -Raw -ErrorAction SilentlyContinue)
    }
    })

    $Server | Add-Member -MemberType ScriptMethod -Name Serve -Value {
    param(
    [string] $File,
    $Response
    )

    If( $null -ne $this.Cached[$File] ) {
    $Response.ContentType = $this.Cached[$File].Type
    return $this.Cached[$File].Content
    }

    Try {
    $content = $this.Read($File)
    $mimetype = $this.ConvertExtension( $extension )

    If( $this.Caching -and -not [string]::IsNullOrWhitespace($content) ) {
    $this.Cached[$File] = @{
    Content = $content
    Type = $mimetype
    }
    }

    $Response.ContentType = $mimetype
    return $content
    } Catch {
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name ParseQuery -Value {
    param( $Request )

    return [System.Web.HttpUtility]::ParseQueryString($Request.Url.Query)
    }

    $Server | Add-Member -MemberType ScriptMethod -Name ParseBody -Value {
    param( $Request )

    If( -not $Request.HasEntityBody -or $Request.ContentLength64 -le 0 ) {
    return $null
    }

    $stream = $Request.InputStream
    $encoding = $Request.ContentEncoding
    $reader = New-Object System.IO.StreamReader( $stream, $encoding )
    $body = $reader.ReadToEnd()

    $reader.Close()
    $stream.Close()

    switch -Wildcard ( $Request.ContentType ) {
    "application/x-www-form-urlencoded*" {
    return [System.Web.HttpUtility]::ParseQueryString($body)
    }
    "application/json*" {
    return $body | ConvertFrom-Json
    }
    "text/xml*" {
    return [xml]$body
    }
    default {
    return $body
    }
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Stop -Value {
    param()

    If( $null -ne $this.Listener -and $this.Listener.IsListening ) {
    $this.Listener.Stop()
    $this.Listener.Close()
    $this.Listener = $null
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Start -Value {
    param()

    @@ -276,7 +522,7 @@ function New-Webserver {
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $ServerName)
    $response.Headers.Add("Server", $Name)
    }

    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
  3. anonhostpi revised this gist Aug 31, 2025. 1 changed file with 7 additions and 8 deletions.
    15 changes: 7 additions & 8 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,5 @@
    # iex (iwr "https://gist.github.com/anonhostpi/1cc0084b959a9ea9e97dca9dce414e1f/raw/webserver.ps1").Content

    # An improved version of https://github.com/MScholtes/WebServer/blob/master/Module/Start-Webserver.ps1

    function New-Webserver {
    param(
    [string] $Binding = "http://localhost:8080/",
    @@ -28,12 +26,13 @@ function New-Webserver {
    )

    If( $Routes -eq $null ) {
    $Routes = @{
    Default = {
    param( $Server, $Command, $Request, $Response )
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    $Routes = [ordered]@{}
    }
    If( $Routes.Default -eq $null ) {
    $Routes.Default = {
    param( $Server, $Command, $Request, $Response )
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    }

  4. anonhostpi revised this gist Aug 31, 2025. 1 changed file with 59 additions and 57 deletions.
    116 changes: 59 additions & 57 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -126,7 +126,7 @@ function New-Webserver {
    # Throw on directory traversal attacks and invalid paths
    $bad = @(
    [string]::IsNullOrWhitespace($file),
    -not (Test-Path $file -ErrorAction SilentlyContinue),
    -not (Test-Path $file -PathType Leaf -ErrorAction SilentlyContinue),
    -not ($file -like "$root*")
    )

    @@ -222,78 +222,80 @@ function New-Webserver {
    $this.Listener.Prefixes.Add($this.Binding)
    $this.Listener.Start()

    Try {
    While ( $this.Listener.IsListening ) {
    <# $task = $this.Listener.GetContextAsync()
    if (-not ($task.Wait(300))) {
    Write-Host "Polling..."
    continue
    } #>

    # $context = $this.Listener.GetContext() # $task.Result

    $task = $this.Listener.GetContextAsync()
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $this.Listener.IsListening ) { return }
    }
    (& {
    Try {
    While ( $this.Listener.IsListening ) {
    <# $task = $this.Listener.GetContextAsync()
    if (-not ($task.Wait(300))) {
    Write-Host "Polling..."
    continue
    } #>

    $context = $task.GetAwaiter().GetResult()
    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath
    # $context = $this.Listener.GetContext() # $task.Result

    If( $this.Routes.Before -is [scriptblock] ) {
    $continue = & $this.Routes.Before $this $command $this.Listener $context
    If( -not $continue ) {
    continue
    $task = $this.Listener.GetContextAsync()
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $this.Listener.IsListening ) { return }
    }
    }

    $response = $context.Response
    $response.ContentType = "text/plain; charset=utf-8"
    $context = $task.GetAwaiter().GetResult()
    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath

    $result = Try {
    $route = If( $this.Routes[$command] ) {
    $this.Routes[$command]
    } Else {
    $this.Routes.Default
    If( $this.Routes.Before -is [scriptblock] ) {
    $continue = & $this.Routes.Before $Server $command $this.Listener $context
    If( -not $continue ) {
    continue
    }
    }

    If( $route -is [scriptblock] ) {
    & $route $this $command $request $response
    } Else {
    & $this.Serve $route $response
    $response = $context.Response
    $response.ContentType = "text/plain; charset=utf-8"

    $result = Try {
    $route = If( $this.Routes[$command] ) {
    $this.Routes[$command]
    } Else {
    $this.Routes.Default
    }

    If( $route -is [scriptblock] ) {
    & $route $this $command $request $response
    } Else {
    & $this.Serve $route $response
    }
    } Catch {
    $response.StatusCode = 500
    "500 Internal Server Error`n`n$($_.Exception.Message)"
    }
    } Catch {
    $response.StatusCode = 500
    "500 Internal Server Error`n`n$($_.Exception.Message)"
    }

    if( -not [string]::IsNullOrWhiteSpace($result) ) {
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
    $response.ContentLength64 = $buffer.Length
    if( -not [string]::IsNullOrWhiteSpace($result) ) {
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
    $response.ContentLength64 = $buffer.Length

    If( [string]::IsNullOrWhiteSpace($response.Headers["Last-Modified"]) ){
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $ServerName)
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Last-Modified"]) ){
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $ServerName)
    }

    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
    }
    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
    }

    $response.Close()
    $response.Close()

    If( $this.Routes.After -is [scriptblock] ) {
    & $this.Routes.After $server $command $this.Listener $context
    If( $this.Routes.After -is [scriptblock] ) {
    & $this.Routes.After $Server $command $this.Listener $context
    }
    }
    } finally {
    $this.Stop()
    }
    } finally {
    $this.Stop()
    }
    }) | Out-Null
    }
    return $server

    return $Server
    }

    # $server = New-Webserver
  5. anonhostpi revised this gist Aug 31, 2025. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -292,6 +292,8 @@ function New-Webserver {
    $this.Stop()
    }
    }

    return $server
    }

    # $server = New-Webserver
  6. anonhostpi revised this gist Aug 31, 2025. 1 changed file with 48 additions and 46 deletions.
    94 changes: 48 additions & 46 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -9,16 +9,18 @@ function New-Webserver {
    [string] $Name = "PowerShell Web Server",

    [System.Collections.IDictionary] $Routes = @{
    Before = { param( $server, $command, $listener, $context ) return $true }
    After = { param( $server, $command, $listener, $context ) return $true }
    Before = { param( $Server, $Command, $Listener, $Context ) return $true }
    After = { param( $Server, $Command, $Listener, $Context ) return $true }

    # "GET /" = { param( $server, $command, $request, $response )... }
    # "GET /" = { param( $Server, $Command, $request, $response )... }
    # "GET /hello" = "./path/to/static/file.html"

    Default = {
    param( $server, $command, $request, $response )
    $response.StatusCode = 404
    return "404 Not Found"
    param( $Server, $Command, $Request, $Response )
    $Command = $Command -split " ", 2
    $path = $Command | Select-Object -Index 1

    return $Server.Serve( $path, $Response )
    }
    },
    [switch] $Cache,
    @@ -28,8 +30,8 @@ function New-Webserver {
    If( $Routes -eq $null ) {
    $Routes = @{
    Default = {
    param( $server, $command, $request, $response )
    $response.StatusCode = 404
    param( $Server, $Command, $Request, $Response )
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    }
    @@ -45,44 +47,6 @@ function New-Webserver {
    Listener = $null
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Read -Value (& {
    If( $Reader -ne $null ) {
    return $Reader
    }

    return {
    param( [string] $Path )

    $root = $this.BaseDirectory

    $Path = $Path.TrimStart('\/')
    $file = "$root\$Path"
    $file = Try {
    Resolve-Path $file -ErrorAction Stop
    } Catch {
    Try {
    Resolve-Path "$file.html" -ErrorAction Stop
    } Catch {
    Resolve-Path "$file\index.html" -ErrorAction SilentlyContinue
    }
    }
    $file = "$file"

    # Throw on directory traversal attacks and invalid paths
    $bad = @(
    [string]::IsNullOrWhitespace($file),
    -not (Test-Path $file -ErrorAction SilentlyContinue),
    -not ($file -like "$root*")
    )

    if ( $bad -contains $true ) {
    throw "Invalid path '$Path'."
    }

    return (Get-Content "$root\$Path" -Raw -ErrorAction SilentlyContinue)
    }
    })

    $Server | Add-Member -MemberType ScriptMethod -Name ConvertExtension -Value {
    param( [string] $Extension )

    @@ -136,6 +100,44 @@ function New-Webserver {
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Read -Value (& {
    If( $Reader -ne $null ) {
    return $Reader
    }

    return {
    param( [string] $Path )

    $root = $this.BaseDirectory

    $Path = $Path.TrimStart('\/')
    $file = "$root\$Path".TrimEnd('\/')
    $file = Try {
    Resolve-Path $file -ErrorAction Stop
    } Catch {
    Try {
    Resolve-Path "$file.html" -ErrorAction Stop
    } Catch {
    Resolve-Path "$file\index.html" -ErrorAction SilentlyContinue
    }
    }
    $file = "$file"

    # Throw on directory traversal attacks and invalid paths
    $bad = @(
    [string]::IsNullOrWhitespace($file),
    -not (Test-Path $file -ErrorAction SilentlyContinue),
    -not ($file -like "$root*")
    )

    if ( $bad -contains $true ) {
    throw "Invalid path '$Path'."
    }

    return (Get-Content "$root\$Path" -Raw -ErrorAction SilentlyContinue)
    }
    })

    $Server | Add-Member -MemberType ScriptMethod -Name Serve -Value {
    param(
    [string] $File,
  7. anonhostpi revised this gist Aug 31, 2025. 1 changed file with 247 additions and 94 deletions.
    341 changes: 247 additions & 94 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -1,144 +1,297 @@
    # iex (iwr "https://gist.github.com/anonhostpi/1cc0084b959a9ea9e97dca9dce414e1f/raw/webserver.ps1").Content

    function Start-Webserver {
    # An improved version of https://github.com/MScholtes/WebServer/blob/master/Module/Start-Webserver.ps1

    function New-Webserver {
    param(
    [string] $Binding = "http://localhost:8080/",
    [string] $BaseDirectory = "$(Get-Location -PSProvider FileSystem)",
    [string] $ServerName = "PowerShell Webserver",
    [string] $Name = "PowerShell Web Server",

    [System.Collections.IDictionary] $Routes = @{
    Before = { param( $command, $listener, $context ) return $true }
    After = { param( $command, $listener, $context ) return $true }
    # "GET /" = { ... }
    Before = { param( $server, $command, $listener, $context ) return $true }
    After = { param( $server, $command, $listener, $context ) return $true }

    # "GET /" = { param( $server, $command, $request, $response )... }
    # "GET /hello" = "./path/to/static/file.html"

    Default = {
    param( $command, $request, $response )
    $msg = "Hello, World!"
    # $response.OutputStream.Write( [System.Text.Encoding]::UTF8.GetBytes($msg), 0, $msg.Length )
    return $msg
    param( $server, $command, $request, $response )
    $response.StatusCode = 404
    return "404 Not Found"
    }
    }
    },
    [switch] $Cache,
    [scriptblock] $Reader
    )

    $BaseDirectory = "$(Resolve-Path $BaseDirectory)"
    If( $Routes -eq $null ) {
    $Routes = @{
    Default = {
    param( $server, $command, $request, $response )
    $response.StatusCode = 404
    return "404 Not Found"
    }
    }
    }

    $Server = New-Object psobject -Property @{
    Binding = $Binding
    BaseDirectory = "$(Resolve-Path $BaseDirectory)".TrimEnd('\/')
    Name = $Name
    Routes = $Routes
    Caching = [bool]$Cache
    Cached = @{}
    Listener = $null
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Read -Value (& {
    If( $Reader -ne $null ) {
    return $Reader
    }

    return {
    param( [string] $Path )

    $root = $this.BaseDirectory

    $Path = $Path.TrimStart('\/')
    $file = "$root\$Path"
    $file = Try {
    Resolve-Path $file -ErrorAction Stop
    } Catch {
    Try {
    Resolve-Path "$file.html" -ErrorAction Stop
    } Catch {
    Resolve-Path "$file\index.html" -ErrorAction SilentlyContinue
    }
    }
    $file = "$file"

    # Throw on directory traversal attacks and invalid paths
    $bad = @(
    [string]::IsNullOrWhitespace($file),
    -not (Test-Path $file -ErrorAction SilentlyContinue),
    -not ($file -like "$root*")
    )

    if ( $bad -contains $true ) {
    throw "Invalid path '$Path'."
    }

    return (Get-Content "$root\$Path" -Raw -ErrorAction SilentlyContinue)
    }
    })

    $Server | Add-Member -MemberType ScriptMethod -Name ConvertExtension -Value {
    param( [string] $Extension )

    switch( $Extension.ToLower() ) {
    ".html" { "text/html; charset=utf-8" }
    ".htm" { "text/html; charset=utf-8" }
    ".css" { "text/css; charset=utf-8" }
    ".js" { "application/javascript; charset=utf-8" }

    ".json" { "application/json; charset=utf-8" }
    ".txt" { "text/plain; charset=utf-8" }
    ".xml" { "application/xml; charset=utf-8" }
    ".csv" { "text/csv; charset=utf-8" }
    ".tsv" { "text/tab-separated-values; charset=utf-8" }
    ".md" { "text/markdown; charset=utf-8" }

    ".png" { "image/png" }
    ".jpg" { "image/jpeg" }
    ".jpeg" { "image/jpeg" }
    ".gif" { "image/gif" }
    ".svg" { "image/svg+xml" }
    ".ico" { "image/x-icon" }
    ".bmp" { "image/bmp" }
    ".avif" { "image/avif" }
    ".webp" { "image/webp" }

    $serve_static = {
    ".mp4" { "video/mp4" }
    ".webm" { "video/webm" }
    ".avi" { "video/x-msvideo" }
    ".mov" { "video/quicktime" }

    ".mp3" { "audio/mpeg" }
    ".wav" { "audio/wav" }
    ".ogg" { "audio/ogg" }

    ".pdf" { "application/pdf" }

    ".zip" { "application/zip" }
    ".tar" { "application/x-tar" }
    ".gz" { "application/gzip" }
    ".7z" { "application/x-7z-compressed" }
    ".bz2" { "application/x-bzip2" }

    ".woff" { "font/woff" }
    ".woff2" { "font/woff2" }
    ".ttf" { "font/ttf" }
    ".eot" { "application/vnd.ms-fontobject" }
    ".otf" { "font/otf" }

    default { "application/octet-stream" }
    }
    }

    $Server | Add-Member -MemberType ScriptMethod -Name Serve -Value {
    param(
    [string] $path,
    $response
    [string] $File,
    $Response
    )

    $file = "$BaseDirectory\$($path.TrimStart('/').Replace('/','\'))"
    $file = "$(Resolve-Path $file)"
    If( Test-Path $file ){

    # Prevent directory traversal attacks
    If( -not $file.StartsWith($BaseDirectory) ){
    $response.StatusCode = 403
    return "403 Forbidden"

    If( $null -ne $this.Cached[$File] ) {
    $Response.ContentType = $this.Cached[$File].Type
    return $this.Cached[$File].Content
    }

    Try {
    $content = $this.Read($File)
    $mimetype = $this.ConvertExtension( $extension )

    If( $this.Caching -and -not [string]::IsNullOrWhitespace($content) ) {
    $this.Cached[$File] = @{
    Content = $content
    Type = $mimetype
    }
    }

    return (Get-Content $file -Raw)
    } Else {
    $response.StatusCode = 404
    $Response.ContentType = $mimetype
    return $content
    } Catch {
    $Response.StatusCode = 404
    return "404 Not Found"
    }
    }

    If( [string]::IsNullOrWhitespace( $Routes.Default ) ) {
    If( [string]::IsNullOrWhitespace( $Routes["GET /"] ) ) {
    $Routes["GET /"] = { return "Hello, World!" }
    $Server | Add-Member -MemberType ScriptMethod -Name ParseQuery -Value {
    param( $Request )

    return [System.Web.HttpUtility]::ParseQueryString($Request.Url.Query)
    }

    $Server | Add-Member -MemberType ScriptMethod -Name ParseBody -Value {
    param( $Request )

    If( -not $Request.HasEntityBody -or $Request.ContentLength64 -le 0 ) {
    return $null
    }

    # Simple static webfile server
    $Routes.Default = {
    param( $command, $request, $response )

    & $serve_static $request.Url.AbsolutePath $response
    $stream = $Request.InputStream
    $encoding = $Request.ContentEncoding
    $reader = New-Object System.IO.StreamReader( $stream, $encoding )
    $body = $reader.ReadToEnd()

    $reader.Close()
    $stream.Close()

    switch -Wildcard ( $Request.ContentType ) {
    "application/x-www-form-urlencoded*" {
    return [System.Web.HttpUtility]::ParseQueryString($body)
    }
    "application/json*" {
    return $body | ConvertFrom-Json
    }
    "text/xml*" {
    return [xml]$body
    }
    default {
    return $body
    }
    }
    }

    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add($Binding)
    $listener.Start()

    $Error.Clear()
    $Server | Add-Member -MemberType ScriptMethod -Name Stop -Value {
    param()

    Try {
    If( $null -ne $this.Listener -and $this.Listener.IsListening ) {
    $this.Listener.Stop()
    $this.Listener.Close()
    $this.Listener = $null
    }
    }

    While ( $listener.IsListening ) {
    <# $task = $listener.GetContextAsync()
    if (-not ($task.Wait(300))) {
    Write-Host "Polling..."
    continue
    } #>
    $Server | Add-Member -MemberType ScriptMethod -Name Start -Value {
    param()

    # $context = $listener.GetContext() # $task.Result
    $this.Listener = New-Object System.Net.HttpListener
    $this.Listener.Prefixes.Add($this.Binding)
    $this.Listener.Start()

    $task = $listener.GetContextAsync()
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $listener.IsListening ) { return }
    }
    Try {
    While ( $this.Listener.IsListening ) {
    <# $task = $this.Listener.GetContextAsync()
    if (-not ($task.Wait(300))) {
    Write-Host "Polling..."
    continue
    } #>

    $context = $task.GetAwaiter().GetResult()
    # $context = $this.Listener.GetContext() # $task.Result

    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath

    If( $Routes.Before ) {
    $continue = & $Routes.Before $command $listener $context
    If( -not $continue ) {
    continue
    $task = $this.Listener.GetContextAsync()
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $this.Listener.IsListening ) { return }
    }
    }
    $response = $context.Response
    $response.ContentType = "text/plain; charset=utf-8"

    $result = try {
    If( $Routes[$command] ) {
    If ( $Routes[$command] -is [scriptblock] ) {
    & $Routes[$command] $command $request $response
    $context = $task.GetAwaiter().GetResult()
    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath

    If( $this.Routes.Before -is [scriptblock] ) {
    $continue = & $this.Routes.Before $this $command $this.Listener $context
    If( -not $continue ) {
    continue
    }
    }

    $response = $context.Response
    $response.ContentType = "text/plain; charset=utf-8"

    $result = Try {
    $route = If( $this.Routes[$command] ) {
    $this.Routes[$command]
    } Else {
    & $serve_static $Routes[$command] $response
    $this.Routes.Default
    }
    } Else {
    If( $Routes.Default -is [scriptblock] ) {
    & $Routes.Default $command $request $response

    If( $route -is [scriptblock] ) {
    & $route $this $command $request $response
    } Else {
    & $serve_static $Routes.Default $response
    & $this.Serve $route $response
    }
    } Catch {
    $response.StatusCode = 500
    "500 Internal Server Error`n`n$($_.Exception.Message)"
    }
    } catch {
    $response.StatusCode = 500
    "500 Internal Server Error`n`n$($_.Exception.Message)"
    }

    if( -not [string]::IsNullOrWhiteSpace($result) ) {
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
    $response.ContentLength64 = $buffer.Length
    if( -not [string]::IsNullOrWhiteSpace($result) ) {
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
    $response.ContentLength64 = $buffer.Length

    If( [string]::IsNullOrWhiteSpace($response.Headers["Last-Modified"]) ){
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $ServerName)
    If( [string]::IsNullOrWhiteSpace($response.Headers["Last-Modified"]) ){
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $ServerName)
    }

    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
    }

    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
    }
    $response.Close()

    $response.Close()

    If( $Routes.After ) {
    $null = & $Routes.After $command $listener $context
    If( $this.Routes.After -is [scriptblock] ) {
    & $this.Routes.After $server $command $this.Listener $context
    }
    }
    } finally {
    $this.Stop()
    }
    } finally {
    $listener.Stop()
    $listener.Close()
    }
    }

    # $server = New-Webserver
    # Start "http://localhost:8080"
    # Start-WebServer
    # $server.Start()
  8. anonhostpi revised this gist Aug 30, 2025. 1 changed file with 12 additions and 3 deletions.
    15 changes: 12 additions & 3 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -68,11 +68,20 @@ function Start-Webserver {
    Try {

    While ( $listener.IsListening ) {
    <# $task = $listener.GetContextAsync()
    if (-not ($task.Wait(300))) {
    Write-Host "Polling..."
    continue
    } #>

    # $context = $listener.GetContext() # $task.Result

    $task = $listener.GetContextAsync()
    $ready = $task.Wait(100) # 100 ms timeout
    if (-not $ready) { continue }
    while( -not $task.AsyncWaitHandle.WaitOne(300) ) {
    if( -not $listener.IsListening ) { return }
    }

    $context = $task.Result
    $context = $task.GetAwaiter().GetResult()

    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath
  9. anonhostpi revised this gist Aug 30, 2025. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -131,5 +131,5 @@ function Start-Webserver {
    }
    }

    Start "http://localhost:8080"
    Start-WebServer
    # Start "http://localhost:8080"
    # Start-WebServer
  10. anonhostpi revised this gist Aug 30, 2025. 1 changed file with 48 additions and 26 deletions.
    74 changes: 48 additions & 26 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -23,29 +23,39 @@ function Start-Webserver {

    $BaseDirectory = "$(Resolve-Path $BaseDirectory)"

    If( [string]::IsNullOrWhiteSpace($Routes.Default) ) {
    If( [string]::IsNullOrWhiteSpace($Routes["GET /"]) ) {
    $Routes["GET /"] = {
    param( $request, $response )
    $msg = "Hello, World!"
    return $msg
    $serve_static = {
    param(
    [string] $path,
    $response
    )

    $file = "$BaseDirectory\$($path.TrimStart('/').Replace('/','\'))"
    $file = "$(Resolve-Path $file)"
    If( Test-Path $file ){

    # Prevent directory traversal attacks
    If( -not $file.StartsWith($BaseDirectory) ){
    $response.StatusCode = 403
    return "403 Forbidden"
    }

    return (Get-Content $file -Raw)
    } Else {
    $response.StatusCode = 404
    return "404 Not Found"
    }
    }

    If( [string]::IsNullOrWhitespace( $Routes.Default ) ) {
    If( [string]::IsNullOrWhitespace( $Routes["GET /"] ) ) {
    $Routes["GET /"] = { return "Hello, World!" }
    }

    # Simple static webfile server
    $Routes.Default = {
    param( $command, $request, $response )
    $file = "$BaseDirectory\$($request.Url.AbsolutePath.TrimStart('/').Replace('/','\'))"
    If( Test-Path $file ) {
    # Prevent escaping the base directory
    If( -not $file.StartsWith($BaseDirectory) ) {
    $response.StatusCode = 403
    return "403 Forbidden"
    }
    return Get-Content $file -Raw
    } Else {
    $response.StatusCode = 404
    return "404 Not Found"
    }

    & $serve_static $request.Url.AbsolutePath $response
    }
    }

    @@ -65,26 +75,34 @@ function Start-Webserver {
    $context = $task.Result

    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.LocalPath
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.AbsolutePath

    If( $routes.Before ) {
    $continue = & $routes.Before $command $listener $context
    If( $Routes.Before ) {
    $continue = & $Routes.Before $command $listener $context
    If( -not $continue ) {
    continue
    }
    }
    $response = $context.Response
    $response.ContentType = "text/plain; charset=utf-8"

    try {
    $result = If( $routes[$command] ) {
    & $routes[$command] $request $response
    $result = try {
    If( $Routes[$command] ) {
    If ( $Routes[$command] -is [scriptblock] ) {
    & $Routes[$command] $command $request $response
    } Else {
    & $serve_static $Routes[$command] $response
    }
    } Else {
    & $routes.Default $command $request $response
    If( $Routes.Default -is [scriptblock] ) {
    & $Routes.Default $command $request $response
    } Else {
    & $serve_static $Routes.Default $response
    }
    }
    } catch {
    $response.StatusCode = 500
    $result = "500 Internal Server Error`n`n$($_.Exception.Message)"
    "500 Internal Server Error`n`n$($_.Exception.Message)"
    }

    if( -not [string]::IsNullOrWhiteSpace($result) ) {
    @@ -102,6 +120,10 @@ function Start-Webserver {
    }

    $response.Close()

    If( $Routes.After ) {
    $null = & $Routes.After $command $listener $context
    }
    }
    } finally {
    $listener.Stop()
  11. anonhostpi revised this gist Aug 30, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # iex (iwr "https://gist.github.com/anonhostpi/1cc0084b959a9ea9e97dca9dce414e1f/raw/webserver.ps1").Contents
    # iex (iwr "https://gist.github.com/anonhostpi/1cc0084b959a9ea9e97dca9dce414e1f/raw/webserver.ps1").Content

    function Start-Webserver {
    param(
  12. anonhostpi revised this gist Aug 30, 2025. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    # iex (iwr "https://gist.github.com/anonhostpi/1cc0084b959a9ea9e97dca9dce414e1f/raw/webserver.ps1").Contents

    function Start-Webserver {
    param(
    [string] $Binding = "http://localhost:8080/",
  13. anonhostpi created this gist Aug 30, 2025.
    111 changes: 111 additions & 0 deletions webserver.ps1
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,111 @@
    function Start-Webserver {
    param(
    [string] $Binding = "http://localhost:8080/",
    [string] $BaseDirectory = "$(Get-Location -PSProvider FileSystem)",
    [string] $ServerName = "PowerShell Webserver",
    [System.Collections.IDictionary] $Routes = @{
    Before = { param( $command, $listener, $context ) return $true }
    After = { param( $command, $listener, $context ) return $true }

    # "GET /" = { ... }
    # "GET /hello" = "./path/to/static/file.html"

    Default = {
    param( $command, $request, $response )
    $msg = "Hello, World!"
    # $response.OutputStream.Write( [System.Text.Encoding]::UTF8.GetBytes($msg), 0, $msg.Length )
    return $msg
    }
    }
    )

    $BaseDirectory = "$(Resolve-Path $BaseDirectory)"

    If( [string]::IsNullOrWhiteSpace($Routes.Default) ) {
    If( [string]::IsNullOrWhiteSpace($Routes["GET /"]) ) {
    $Routes["GET /"] = {
    param( $request, $response )
    $msg = "Hello, World!"
    return $msg
    }
    }

    $Routes.Default = {
    param( $command, $request, $response )
    $file = "$BaseDirectory\$($request.Url.AbsolutePath.TrimStart('/').Replace('/','\'))"
    If( Test-Path $file ) {
    # Prevent escaping the base directory
    If( -not $file.StartsWith($BaseDirectory) ) {
    $response.StatusCode = 403
    return "403 Forbidden"
    }
    return Get-Content $file -Raw
    } Else {
    $response.StatusCode = 404
    return "404 Not Found"
    }
    }
    }

    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add($Binding)
    $listener.Start()

    $Error.Clear()

    Try {

    While ( $listener.IsListening ) {
    $task = $listener.GetContextAsync()
    $ready = $task.Wait(100) # 100 ms timeout
    if (-not $ready) { continue }

    $context = $task.Result

    $request = $context.Request
    $command = "{0} {1}" -f $request.HttpMethod, $request.Url.LocalPath

    If( $routes.Before ) {
    $continue = & $routes.Before $command $listener $context
    If( -not $continue ) {
    continue
    }
    }
    $response = $context.Response
    $response.ContentType = "text/plain; charset=utf-8"

    try {
    $result = If( $routes[$command] ) {
    & $routes[$command] $request $response
    } Else {
    & $routes.Default $command $request $response
    }
    } catch {
    $response.StatusCode = 500
    $result = "500 Internal Server Error`n`n$($_.Exception.Message)"
    }

    if( -not [string]::IsNullOrWhiteSpace($result) ) {
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
    $response.ContentLength64 = $buffer.Length

    If( [string]::IsNullOrWhiteSpace($response.Headers["Last-Modified"]) ){
    $response.Headers.Add("Last-Modified", (Get-Date).ToString("r"))
    }
    If( [string]::IsNullOrWhiteSpace($response.Headers["Server"]) ){
    $response.Headers.Add("Server", $ServerName)
    }

    $response.OutputStream.Write( $buffer, 0, $buffer.Length )
    }

    $response.Close()
    }
    } finally {
    $listener.Stop()
    $listener.Close()
    }
    }

    Start "http://localhost:8080"
    Start-WebServer