WebHost.psm1

<#
 .Synopsis
  Start a HTTP server to host a web site.
 
 .Description
  Start a HTTP web server to host a web site.
 
 .Parameter Port
  The port that HTTP server will use to host the web site.
  Defaults to 2194
 
# .Parameter Host
# The host that HTTP server will use to host the web site.
# Defaults to localhost
 
# .Parameter Wwwroot
# The directory to use as root directory to serve files from.
# Defaults to the folder that command was executed from.
 
 .Example
   # Start HTTP server to host web site using default settings
   Start-WebHost
 
# .Example
# # Display a date range.
# Show-Calendar -Start "March, 2010" -End "May, 2010"
 
# .Example
# # Highlight a range of days.
# Show-Calendar -HighlightDay (1..10 + 22) -HighlightDate "December 25, 2008"
#>


Function Send {
    param (
        [parameter(Mandatory)][System.Net.HttpListenerResponse]$resp,
        [parameter(Mandatory)][byte[]]$data,
        [string]$contentType = [System.Net.Mime.MediaTypeNames+Text]::Html,
        [System.Text.Encoding]$contentEncoding = [System.Text.Encoding]::UTF8,
        [int]$statusCode = 200
    )

    try {
        $resp.StatusCode = $statusCode
        $resp.ContentType = $contentType
        $resp.ContentEncoding = $contentEncoding
        $resp.OutputStream.Write($data, 0, $data.Length)
    }
    catch {
        Write-Error $_
    }
    finally {
        $resp.Close()
    }
}

Function GetMimeType {
    param (
        [parameter(Mandatory)][string]$extension
    )

    $ext = $extension.ToLower()
    switch ($ext) {
        ".css" { "text/css"; Break }
        ".csv" { "text/csv"; Break }
        ".html" { [System.Net.Mime.MediaTypeNames+Text]::Html; Break }
        ".htm" { [System.Net.Mime.MediaTypeNames+Text]::Html; Break }
        ".js" { "text/javascript"; Break }
        ".rtf" { [System.Net.Mime.MediaTypeNames+Text]::RichText; Break }
        ".str" { [System.Net.Mime.MediaTypeNames+Text]::Html; Break }
        ".txt" { [System.Net.Mime.MediaTypeNames+Text]::Plain; Break }
        ".xhtml" { "application/xhtml+xml"; Break }
        ".xml" { [System.Net.Mime.MediaTypeNames+Text]::Xml; Break }

        ".gif" { [System.Net.Mime.MediaTypeNames+Image]::Gif; Break }
        ".ico" { "image/vnd.microsoft.icon"; Break }
        ".jpg" { [System.Net.Mime.MediaTypeNames+Image]::Jpeg; Break }
        ".jpeg" { [System.Net.Mime.MediaTypeNames+Image]::Jpeg; Break }
        ".png" { "image/png"; Break }
        ".svg" { "image/svg+xml"; Break }
        ".tiff" { [System.Net.Mime.MediaTypeNames+Image]::Tiff; Break }
        ".tif" { [System.Net.Mime.MediaTypeNames+Image]::Tiff; Break }

        ".gz" { "application/gzip"; Break }
        ".json" { "application/json"; Break }
        ".pdf" { "application/pdf"; Break }
        ".zip" { [System.Net.Mime.MediaTypeNames+Application]::Zip; Break }

        ".ttf" { "font/ttf"; Break }

        default { [System.Net.Mime.MediaTypeNames+Application]::Octet; Break }
    }

    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
    # System.Net.Mime.MediaTypeNames.Application.Octet
    # System.Net.Mime.MediaTypeNames.Application.Pdf
    # System.Net.Mime.MediaTypeNames.Application.Rtf
    # System.Net.Mime.MediaTypeNames.Application.Soap
    # System.Net.Mime.MediaTypeNames.Application.Zip
}

Function Log {
    param (
        [parameter(Mandatory)][string]$message,
        [string]$filePath = "webhost.log",
        [string]$dateTimeFormat = "u"
    )

    "$((Get-Date).ToString($dateTimeFormat)) $message" | Out-File -FilePath $filePath -Append
}

Function Start-WebHost {
    param
    (
        [ushort]$port = 2194 # Ports 2194-2196 are unassigned in IANA Service Name and Transport Protocol Port Number Registry
    )

    
    # Constants
    Set-Variable spaceByteArray -Option Constant -Value ([System.Text.Encoding]::UTF8.GetBytes(" "))
    Set-Variable waitTime -Option Constant -Value (New-Object -TypeName System.TimeSpan -ArgumentList 0,0,0,1,678)

    # Local variables
    $rootPath = (Get-Location).Path
    $prefix = "http://127.0.0.1:$port/"         # 127.0.0.1 is fairly universal
    $server = New-Object -TypeName System.Net.HttpListener
    $server.Prefixes.Add($prefix)
    Write-Debug "`$prefix is $prefix" 

    if ($Debug) {
        $DebugPreference = 'Continue'           # Start - display debug messages
    }
  
    try {
        $server.Start()
        Write-Host "Server started.`nAccess server at: $prefix"
        while ($true) {
            # The synchronous version of GetContext will block until we get a request connection
            # This unfortuntately also blocks CTRL+C termination of PowerShell
            # This is a behaviour which we do not want.
            # So we use the asynchronous version.
            Write-Host "Waiting for request"
            $ctxTask = $server.GetContextAsync()
  
            # Instead of directly busy spin, we add a blocking wait call
            while (-not $ctxTask.IsCompleted) {
                [void]$ctxTask.Wait($waitTime)
            }
  
            $ctx = $ctxTask.Result
  
            # Get corresponding requests and response objects
            [System.Net.HttpListenerRequest] $req = $ctx.Request
            [System.Net.HttpListenerResponse] $resp = $ctx.Response;
  
            # Resolve request into local path
            $localPath = "$rootPath$($req.Url.AbsolutePath.Replace('/', [System.IO.Path]::DirectorySeparatorChar))"
  
            if ([System.IO.File]::Exists($localPath)) {
                $ext = [System.IO.Path]::GetExtension($localPath)
                $mimeType = GetMimeType($ext)
  
                $data = Get-Content $localPath -AsByteStream -ReadCount 0

                # If file is empty, send a single character 'space' byte array
                if ($null -eq $data) {
                    $data = $spaceByteArray
                }

                Send $resp $data -contentType $mimeType
                Write-Host "Requesting: $($req.Url.AbsolutePath) ==> $localPath (200 $mimeType)"
            }
            else {
                $data = [System.Text.Encoding]::UTF8.GetBytes("404 - Resource not found.")
                Send $resp $data -statusCode 404
                Write-Host "Requesting: $($req.Url.AbsolutePath) ==> $localPath (404 $mimeType )"
            }
  
            # Dispose
            $ctxTask.Dispose()
        }

    }
    catch {
        Write-Error $_
    }
    finally {
        $server.Stop()
        Write-Host "Server stopped."
    }

    # End-of-script
    if ($Debug) {
        $DebugPreference = 'SilentlyContinue'   # End - display debug messages
    }
}

# Module member export definitions
Export-ModuleMember -Function Start-WebHost