Private/PodeServer.ps1

function Start-PodeSocketServer
{
    param (
        [switch]
        $Browse
    )

    # setup the callback for sockets
    $PodeContext.Server.Sockets.Ssl.Callback = Get-PodeSocketCertifcateCallback

    # setup any inbuilt middleware
    $inbuilt_middleware = @(
        (Get-PodeAccessMiddleware),
        (Get-PodeLimitMiddleware),
        (Get-PodePublicMiddleware),
        (Get-PodeRouteValidateMiddleware),
        (Get-PodeBodyMiddleware),
        (Get-PodeCookieMiddleware)
    )

    $PodeContext.Server.Middleware = ($inbuilt_middleware + $PodeContext.Server.Middleware)

    # work out which endpoints to listen on
    $endpoints = @()
    @(Get-PodeEndpoints -Type Http) | ForEach-Object {
        # get the ip address
        $_ip = [string]($_.Address)
        $_ip = (Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1)
        $_ip = (Get-PodeIPAddress $_ip)

        # add endpoint to list
        $endpoints += @{
            Address = $_ip
            Port = $_.Port
            Certificate = $_.Certificate.Raw
            HostName = $_.Url
        }
    }

    try
    {
        # register endpoints on the listener
        $endpoints | ForEach-Object {
            $PodeContext.Server.Sockets.Listeners += (Initialize-PodeSocketListenerEndpoint `
                -Type Sockets `
                -Address $_.Address `
                -Port $_.Port `
                -Certificate $_.Certificate)
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeSocketListener -Type Sockets
        throw $_.Exception
    }

    # script for listening out for incoming requests
    $listenScript = {
        param (
            [Parameter(Mandatory=$true)]
            [int]
            $ThreadId
        )

        try
        {
            Start-PodeSocketListener -Listeners $PodeContext.Server.Sockets.Listeners

            [System.Threading.Thread]::CurrentThread.IsBackground = $true
            [System.Threading.Thread]::CurrentThread.Priority = [System.Threading.ThreadPriority]::Lowest

            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                Wait-PodeTask ([System.Threading.Tasks.Task]::Delay(60))
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    # start the runspace for listening on x-number of threads
    1..$PodeContext.Threads.Web | ForEach-Object {
        Add-PodeRunspace -Type 'Main' -ScriptBlock $listenScript `
            -Parameters @{ 'ThreadId' = $_ }
    }

    # script to keep web server listening until cancelled
    $waitScript = {
        try {
            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
        finally {
            Close-PodeSocketListener -Type Sockets
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript

    # browse to the first endpoint, if flagged
    if ($Browse) {
        Start-Process $endpoints[0].HostName
    }

    return @($endpoints.HostName)
}

function Invoke-PodeSocketHandler
{
    param(
        [Parameter(Mandatory)]
        [hashtable]
        $Context
    )

    try
    {
        # reset with basic event data
        $WebEvent = @{
            OnEnd = @()
            Auth = @{}
            Response = @{
                Headers = @{}
                ContentLength64 = 0
                ContentType = $null
                OutputStream = New-Object -TypeName System.IO.MemoryStream
                StatusCode = 200
                StatusDescription = 'OK'
            }
            Request = @{}
            Lockable = $PodeContext.Lockable
            Path = $null
            Method = $null
            Query = $null
            Protocol = $Context.Protocol
            Endpoint = $null
            ContentType = $null
            ErrorType = $null
            Cookies = @{}
            PendingCookies = @{}
            Parameters = $null
            Data = $null
            Files = $null
            Streamed = $true
            Route = $null
            StaticContent = $null
            Timestamp = [datetime]::UtcNow
            TransferEncoding = $null
        }

        # set pode in server response header
        Set-PodeServerHeader

        # make the stream (use an ssl stream if we have a cert)
        $stream = [System.Net.Sockets.NetworkStream]::new($Context.Socket, $true)

        if ($null -ne $Context.Certificate) {
            try {
                $stream = [System.Net.Security.SslStream]::new($stream, $false, $PodeContext.Server.Sockets.Ssl.Callback)
                $stream.AuthenticateAsServer($Context.Certificate, $true, $PodeContext.Server.Sockets.Ssl.Protocols, $false)
            }
            catch {
                # immediately close http connections
                Close-PodeSocket -Socket $Context.Socket -Shutdown
                return
            }
        }

        # read the request headers - prepare for the dodgest of hacks ever. I apologise profusely.
        try {
            $bytes = New-Object byte[] 0
            $Context.Socket.Receive($bytes) | Out-Null
        }
        catch {
            $err = [System.Net.Http.HttpRequestException]::new()
            $err.Data.Add('PodeStatusCode', 408)
            throw $err
        }

        $bytes = New-Object byte[] $Context.Socket.Available
        (Wait-PodeTask -Task $stream.ReadAsync($bytes, 0, $Context.Socket.Available)) | Out-Null
        $req_info = Get-PodeServerRequestDetails -Bytes $bytes -Protocol $Context.Protocol

        # set the rest of the event data
        $WebEvent.Request = @{
            Body = @{
                Value = $req_info.Body
                Bytes = $req_info.RawBody
            }
            Headers = $req_info.Headers
            Url = $req_info.Uri
            UrlReferrer = $req_info.Headers['Referer']
            UserAgent = $req_info.Headers['User-Agent']
            HttpMethod = $req_info.Method
            RemoteEndPoint = $Context.Socket.RemoteEndPoint
            Protocol = $req_info.Protocol
            ProtocolVersion = ($req_info.Protocol -isplit '/')[1]
            ContentEncoding = (Get-PodeEncodingFromContentType -ContentType $req_info.Headers['Content-Type'])
        }

        $WebEvent.Path = [System.Web.HttpUtility]::UrlDecode($req_info.Uri.AbsolutePath)
        $WebEvent.Method = $req_info.Method.ToLowerInvariant()
        $WebEvent.Endpoint = $req_info.Headers['Host']
        $WebEvent.ContentType = $req_info.Headers['Content-Type']
        $WebEvent.AcceptEncoding = (Get-PodeAcceptEncoding -AcceptEncoding $req_info.Headers['Accept-Encoding'] -ThrowError)

        # transfer encoding
        $WebEvent.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding $req_info.Headers['Transfer-Encoding'] -ThrowError)
        if ([string]::IsNullOrWhiteSpace($WebEvent.TransferEncoding)) {
            $WebEvent.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding $req_info.Headers['X-Transfer-Encoding'] -ThrowError)
        }

        # parse the query string and convert it to a hashtable
        $WebEvent.Query = (Convert-PodeQueryStringToHashTable -Uri $req_info.Query)

        # add logging endware for post-request
        Add-PodeRequestLogEndware -WebEvent $WebEvent

        # invoke global and route middleware
        if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
            if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $WebEvent.Route.Middleware))
            {
                # invoke the route
                if ($null -ne $WebEvent.StaticContent) {
                    if ($WebEvent.StaticContent.IsDownload) {
                        Set-PodeResponseAttachment -Path $e.Path
                    }
                    else {
                        $cachable = $WebEvent.StaticContent.IsCachable
                        Write-PodeFileResponse -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable
                    }
                }
                else {
                    Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments (@($WebEvent) + @($WebEvent.Route.Arguments)) -Scoped -Splat
                }
            }
        }
    }
    catch [System.OperationCanceledException] {}
    catch [System.Net.Http.HttpRequestException] {
        $code = [int]($_.Exception.Data['PodeStatusCode'])
        if ($code -le 0) {
            $code = 400
        }

        Set-PodeResponseStatus -Code $code -Exception $_
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Set-PodeResponseStatus -Code 500 -Exception $_
    }
    finally {
        Update-PodeServerRequestMetrics -WebEvent $WebEvent
    }

    try {
        # invoke endware specifc to the current web event
        $_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware))
        Invoke-PodeEndware -WebEvent $WebEvent -Endware $_endware

        # write the response line
        $protocol = $req_info.Protocol
        if ([string]::IsNullOrWhiteSpace($protocol)) {
            $protocol = 'HTTP/1.1'
        }

        $newLine = "`r`n"
        $res_msg = "$($protocol) $($WebEvent.Response.StatusCode) $($WebEvent.Response.StatusDescription)$($newLine)"

        # set response headers before adding
        Set-PodeServerResponseHeaders -WebEvent $WebEvent

        # write the response headers
        if ($WebEvent.Response.Headers.Count -gt 0) {
            foreach ($key in $WebEvent.Response.Headers.Keys) {
                foreach ($value in $WebEvent.Response.Headers[$key]) {
                    $res_msg += "$($key): $($value)$($newLine)"
                }
            }
        }

        $res_msg += $newLine

        # stream response output
        $buffer = $PodeContext.Server.Encoding.GetBytes($res_msg)
        Wait-PodeTask -Task $stream.WriteAsync($buffer, 0, $buffer.Length)
        $WebEvent.Response.OutputStream.WriteTo($stream)
        $stream.Flush()
    }
    catch [System.Management.Automation.MethodInvocationException] { }
    finally {
        # close socket stream
        if ($null -ne $WebEvent.Response.OutputStream) {
            Close-PodeDisposable -Disposable $WebEvent.Response.OutputStream -Close -CheckNetwork
        }

        Close-PodeSocket -Socket $Context.Socket -Shutdown
    }
}

function Set-PodeServerResponseHeaders
{
    param(
        [Parameter(Mandatory=$true)]
        $WebEvent
    )

    # add content-type
    if (![string]::IsNullOrWhiteSpace($WebEvent.Response.ContentType)) {
        Set-PodeHeader -Name 'Content-Type' -Value $WebEvent.Response.ContentType
    }
    else {
        $WebEvent.Response.Headers.Remove('Content-Type')
    }

    # add content-length
    if (($WebEvent.Response.ContentLength64 -eq 0) -and ($WebEvent.Response.OutputStream.Length -gt 0)) {
        $WebEvent.Response.ContentLength64 = $WebEvent.Response.OutputStream.Length
    }

    if ($WebEvent.Response.ContentLength64 -gt 0) {
        Set-PodeHeader -Name 'Content-Length' -Value $WebEvent.Response.ContentLength64
    }
    else {
        $WebEvent.Response.Headers.Remove('Content-Length')
    }

    # add the date of the response
    Set-PodeHeader -Name 'Date' -Value ([DateTime]::UtcNow.ToString("r", [CultureInfo]::InvariantCulture))

    # state to close the connection (no support for keep-alive yet)
    Set-PodeHeader -Name 'Connection' -Value 'close'
}