Private/PodeServer.ps1

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

    # setup the callback for sockets
    $PodeContext.Server.Sockets.Ssl.Callback = [System.Net.Security.RemoteCertificateValidationCallback]{
        param(
            [Parameter()]
            [object]
            $Sender,

            [Parameter()]
            [X509Certificate]
            $Certificate,

            [Parameter()]
            [System.Security.Cryptography.X509Certificates.X509Chain]
            $Chain,

            [Parameter()]
            [System.Net.Security.SslPolicyErrors]
            $SslPolicyErrors
        )

        # if there is no client cert, just allow it
        if ($null -eq $Certificate) {
            return $true
        }

        # if we have a cert, but there are errors, fail
        return ($SslPolicyErrors -ne [System.Net.Security.SslPolicyErrors]::None)
    }

    # 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 = @()
    $PodeContext.Server.Endpoints | ForEach-Object {
        # get the protocol
        $_protocol = (Resolve-PodeValue -Check $_.Ssl -TrueValue 'https' -FalseValue 'http')

        # get the ip address
        $_ip = [string]($_.Address)
        $_ip = (Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1)
        $_ip = (Get-PodeIPAddress $_ip)

        # get the port
        $_port = [int]($_.Port)
        if ($_port -eq 0) {
            $_port = (Resolve-PodeValue $_.Ssl -TrueValue 8443 -FalseValue 8080)
        }

        # add endpoint to list
        $endpoints += @{
            Address = $_ip
            Port = $_port
            Certificate = $_.Certificate.Raw
            HostName = "$($_protocol)://$($_.HostName):$($_port)/"
        }
    }

    try
    {
        # register endpoints on the listener
        $endpoints | ForEach-Object {
            Initialize-PodeSocketListenerEndpoint -Address $_.Address -Port $_.Port -Certificate $_.Certificate
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeSocketListener
        throw $_.Exception
    }

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

        try
        {
            Start-PodeSocketListener

            [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 | 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
        }
    }

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

    # state where we're running
    Write-Host 'Note: This server type is experimental' -ForegroundColor Magenta
    Write-Host "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads) thread(s)]:" -ForegroundColor Yellow

    $endpoints | ForEach-Object {
        Write-Host "`t- $($_.HostName)" -ForegroundColor Yellow
    }

    # browse to the first endpoint, if flagged
    if ($Browse) {
        Start-Process $endpoints[0].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
        }

        # 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 = $req_info.Uri.AbsolutePath
        $WebEvent.Method = $req_info.Method.ToLowerInvariant()
        $WebEvent.Endpoint = $req_info.Headers['Host']
        $WebEvent.ContentType = $req_info.Headers['Content-Type']

        $WebEvent.Query = [System.Web.HttpUtility]::ParseQueryString($req_info.Query)
        if ($null -eq $WebEvent.Query) {
            $WebEvent.Query = @{}
        }

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

        # invoke middleware
        if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
            # get the route logic
            $route = Get-PodeRoute -Method $WebEvent.Method -Route $WebEvent.Path -Protocol $WebEvent.Protocol `
                -Endpoint $WebEvent.Endpoint -CheckWildMethod

            # invoke route and custom middleware
            if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $route.Middleware)) {
                if ($null -ne $route.Logic) {
                    Invoke-PodeScriptBlock -ScriptBlock $route.Logic -Arguments (@($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 $_
    }

    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'
}

function Get-PodeServerRequestDetails
{
    param(
        [Parameter()]
        [byte[]]
        $Bytes,

        [Parameter(Mandatory=$true)]
        [string]
        $Protocol
    )

    # convert array to string
    $Content = $PodeContext.Server.Encoding.GetString($bytes, 0, $bytes.Length)

    # parse the request headers
    $newLine = "`r`n"
    if (!$Content.Contains($newLine)) {
        $newLine = "`n"
    }

    $req_lines = ($Content -isplit $newLine)

    # first line is the request info
    $req_line_info = ($req_lines[0].Trim() -isplit '\s+')
    if ($req_line_info.Length -ne 3) {
        throw [System.Net.Http.HttpRequestException]::new("Invalid request line: $($req_lines[0]) [$($req_line_info.Length)]")
    }

    $req_method = $req_line_info[0].Trim()
    if (@('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE') -inotcontains $req_method) {
        throw [System.Net.Http.HttpRequestException]::new("Invalid request HTTP method: $($req_method)")
    }

    $req_query = $req_line_info[1].Trim()
    $req_proto = $req_line_info[2].Trim()
    if (!$req_proto.StartsWith('HTTP/')) {
        throw [System.Net.Http.HttpRequestException]::new("Invalid request version: $($req_proto)")
    }

    # then, read the headers
    $req_headers = @{}
    $req_body_index = 0
    for ($i = 1; $i -le $req_lines.Length -1; $i++) {
        $line = $req_lines[$i].Trim()
        if ([string]::IsNullOrWhiteSpace($line)) {
            $req_body_index = $i + 1
            break
        }

        $index = $line.IndexOf(':')
        $name = $line.Substring(0, $index).Trim()
        $value = $line.Substring($index + 1).Trim()
        $req_headers[$name] = $value
    }

    # then set the request body
    $req_body = ($req_lines[($req_body_index)..($req_lines.Length - 1)] -join $newLine)
    $req_body_bytes = $bytes[($bytes.Length - $req_body.Length)..($bytes.Length - 1)]

    # build required URI details
    $req_uri = [uri]::new("$($Protocol)://$($req_headers['Host'])$($req_query)")

    # return the details
    return @{
        Method = $req_method
        Query = $req_query
        Protocol = $req_proto
        Headers = $req_headers
        Body = $req_body
        RawBody = $req_body_bytes
        Uri = $req_uri
    }
}