Unpacking Software Livestream

Join our monthly Unpacking Software livestream to hear about the latest news, chat and opinion on packaging, software deployment and lifecycle management!

Learn More

Chocolatey Product Spotlight

Join the Chocolatey Team on our regular monthly stream where we put a spotlight on the most recent Chocolatey product releases. You'll have a chance to have your questions answered in a live Ask Me Anything format.

Learn More

Chocolatey Coding Livestream

Join us for the Chocolatey Coding Livestream, where members of our team dive into the heart of open source development by coding live on various Chocolatey projects. Tune in to witness real-time coding, ask questions, and gain insights into the world of package management. Don't miss this opportunity to engage with our team and contribute to the future of Chocolatey!

Learn More

Calling All Chocolatiers! Whipping Up Windows Automation with Chocolatey Central Management

Webinar from
Wednesday, 17 January 2024

We are delighted to announce the release of Chocolatey Central Management v0.12.0, featuring seamless Deployment Plan creation, time-saving duplications, insightful Group Details, an upgraded Dashboard, bug fixes, user interface polishing, and refined documentation. As an added bonus we'll have members of our Solutions Engineering team on-hand to dive into some interesting ways you can leverage the new features available!

Watch On-Demand
Chocolatey Community Coffee Break

Join the Chocolatey Team as we discuss all things Community, what we do, how you can get involved and answer your Chocolatey questions.

Watch The Replays
Chocolatey and Intune Overview

Webinar Replay from
Wednesday, 30 March 2022

At Chocolatey Software we strive for simple, and teaching others. Let us teach you just how simple it could be to keep your 3rd party applications updated across your devices, all with Intune!

Watch On-Demand
Chocolatey For Business. In Azure. In One Click.

Livestream from
Thursday, 9 June 2022

Join James and Josh to show you how you can get the Chocolatey For Business recommended infrastructure and workflow, created, in Azure, in around 20 minutes.

Watch On-Demand
The Future of Chocolatey CLI

Livestream from
Thursday, 04 August 2022

Join Paul and Gary to hear more about the plans for the Chocolatey CLI in the not so distant future. We'll talk about some cool new features, long term asks from Customers and Community and how you can get involved!

Watch On-Demand
Hacktoberfest Tuesdays 2022

Livestreams from
October 2022

For Hacktoberfest, Chocolatey ran a livestream every Tuesday! Re-watch Cory, James, Gary, and Rain as they share knowledge on how to contribute to open-source projects such as Chocolatey CLI.

Watch On-Demand

Pode 1.2.0

This is not the latest version of Pode available.

  • 1
  • 2
  • 3

All Checks are Passing

3 Passing Tests


Validation Testing Passed


Verification Testing Passed

Details

Scan Testing Successful:

No detections found in any package files

Details
Learn More

Deployment Method: Individual Install, Upgrade, & Uninstall

To install Pode, run the following command from the command line or from PowerShell:

>

To upgrade Pode, run the following command from the command line or from PowerShell:

>

To uninstall Pode, run the following command from the command line or from PowerShell:

>

Deployment Method:

NOTE

This applies to both open source and commercial editions of Chocolatey.

1. Enter Your Internal Repository Url

(this should look similar to https://community.chocolatey.org/api/v2/)


2. Setup Your Environment

1. Ensure you are set for organizational deployment

Please see the organizational deployment guide

2. Get the package into your environment

  • Open Source or Commercial:
    • Proxy Repository - Create a proxy nuget repository on Nexus, Artifactory Pro, or a proxy Chocolatey repository on ProGet. Point your upstream to https://community.chocolatey.org/api/v2/. Packages cache on first access automatically. Make sure your choco clients are using your proxy repository as a source and NOT the default community repository. See source command for more information.
    • You can also just download the package and push it to a repository Download

3. Copy Your Script

choco upgrade pode -y --source="'INTERNAL REPO URL'" --version="'1.2.0'" [other options]

See options you can pass to upgrade.

See best practices for scripting.

Add this to a PowerShell script or use a Batch script with tools and in places where you are calling directly to Chocolatey. If you are integrating, keep in mind enhanced exit codes.

If you do use a PowerShell script, use the following to ensure bad exit codes are shown as failures:


choco upgrade pode -y --source="'INTERNAL REPO URL'" --version="'1.2.0'" 
$exitCode = $LASTEXITCODE

Write-Verbose "Exit code was $exitCode"
$validExitCodes = @(0, 1605, 1614, 1641, 3010)
if ($validExitCodes -contains $exitCode) {
  Exit 0
}

Exit $exitCode

- name: Install pode
  win_chocolatey:
    name: pode
    version: '1.2.0'
    source: INTERNAL REPO URL
    state: present

See docs at https://docs.ansible.com/ansible/latest/modules/win_chocolatey_module.html.


chocolatey_package 'pode' do
  action    :install
  source   'INTERNAL REPO URL'
  version  '1.2.0'
end

See docs at https://docs.chef.io/resource_chocolatey_package.html.


cChocoPackageInstaller pode
{
    Name     = "pode"
    Version  = "1.2.0"
    Source   = "INTERNAL REPO URL"
}

Requires cChoco DSC Resource. See docs at https://github.com/chocolatey/cChoco.


package { 'pode':
  ensure   => '1.2.0',
  provider => 'chocolatey',
  source   => 'INTERNAL REPO URL',
}

Requires Puppet Chocolatey Provider module. See docs at https://forge.puppet.com/puppetlabs/chocolatey.


4. If applicable - Chocolatey configuration/installation

See infrastructure management matrix for Chocolatey configuration elements and examples.

Package Approved

This package was approved as a trusted package on 13 Nov 2019.

Description

Pode is a Cross-Platform framework, completely written in PowerShell, for creating web servers to host REST APIs and Websites. Pode also has support for being used in Azure Functions and AWS Lambda.

Features

  • Cross-platform using PowerShell Core (with support for PS5)
  • Docker support, including images for ARM/Raspberry Pi
  • Azure Functions and AWS Lambda support
  • Listen on a single or multiple IP address/hostnames
  • Cross-platform support for HTTP, HTTPS, TCP and SMTP
  • Cross-platform support for server-to-client WebSockets, including secure WebSockets
  • Host REST APIs, Web Pages, and Static Content (with caching)
  • Support for custom error pages
  • Multi-thread support for incoming requests
  • Inbuilt template engine, with support for third-parties
  • Async timers for short-running repeatable processes
  • Async scheduled tasks using cron expressions for short/long-running processes
  • Supports logging to CLI, Files, and custom logic for other services like LogStash
  • Cross-state variable access across multiple runspaces
  • Restart the server via file monitoring, or defined periods/times
  • Ability to allow/deny requests from certain IP addresses and subnets
  • Basic rate limiting for IP addresses and subnets
  • Middleware and Sessions on web servers, with Flash message and CSRF support
  • Authentication on requests, such as Basic and Windows Active Directory
  • Support for dynamically building Routes from Functions and Modules
  • (Windows) Generate/bind self-signed certificates, and signed certificates
  • (Windows) Open the hosted server as a desktop application

src\LICENSE.txt
The MIT License (MIT)

Copyright (c) [2017-2019] [Matthew Kelly (Badgerati)]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
src\Misc\default-error-page.html.pode
 
src\Misc\default-error-page.json.pode
 
src\Misc\default-error-page.xml.pode
 
src\Pode.psd1
#
# Module manifest for module 'Pode'
#
# Generated by: Matthew Kelly (Badgerati)
#
# Generated on: 28/11/2017
#

@{
    # Script module or binary module file associated with this manifest.
    RootModule = 'Pode.psm1'

    # Version number of this module.
    ModuleVersion = '1.2.0'

    # ID used to uniquely identify this module
    GUID = 'e3ea217c-fc3d-406b-95d5-4304ab06c6af'

    # Author of this module
    Author = 'Matthew Kelly (Badgerati)'

    # Copyright statement for this module
    Copyright = 'Copyright (c) 2017-2019 Matthew Kelly (Badgerati), licensed under the MIT License.'

    # Description of the functionality provided by this module
    Description = 'A Cross-Platform PowerShell framework for creating web servers to host REST APIs and Websites. Pode also has support for being used in Azure Functions and AWS Lambda.'

    # Minimum version of the Windows PowerShell engine required by this module
    PowerShellVersion = '5.0'

    # Assemblies that must be loaded prior to importing this module
    RequiredAssemblies = @('System.Web', 'System.Net.Http')

    # Functions to export from this Module
    FunctionsToExport = @(
        # cookies
        'Get-PodeCookie',
        'Get-PodeCookieSecret',
        'Remove-PodeCookie',
        'Set-PodeCookie',
        'Set-PodeCookieSecret',
        'Test-PodeCookie',
        'Test-PodeCookieSigned',
        'Update-PodeCookieExpiry',

        # flash
        'Add-PodeFlashMessage',
        'Clear-PodeFlashMessages',
        'Get-PodeFlashMessage',
        'Get-PodeFlashMessageNames',
        'Remove-PodeFlashMessage',
        'Test-PodeFlashMessage',

        # headers
        'Add-PodeHeader',
        'Test-PodeHeader',
        'Get-PodeHeader',
        'Set-PodeHeader',

        # state
        'Set-PodeState',
        'Get-PodeState',
        'Remove-PodeState',
        'Save-PodeState',
        'Restore-PodeState',
        'Test-PodeState',

        # response helpers
        'Set-PodeResponseAttachment',
        'Write-PodeTextResponse',
        'Write-PodeFileResponse',
        'Write-PodeCsvResponse',
        'Write-PodeHtmlResponse',
        'Write-PodeMarkdownResponse',
        'Write-PodeJsonResponse',
        'Write-PodeXmlResponse',
        'Write-PodeViewResponse',
        'Set-PodeResponseStatus',
        'Move-PodeResponseUrl',
        'Write-PodeTcpClient',
        'Read-PodeTcpClient',
        'Save-PodeRequestFile',
        'Set-PodeViewEngine',
        'Use-PodePartialView',
        'Send-PodeSignal',

        # utility helpers
        'Wait-PodeTask',
        'Close-PodeDisposable',
        'Lock-PodeObject',
        'Get-PodeServerPath',
        'Start-PodeStopwatch',
        'Use-PodeStream',
        'Use-PodeScript',
        'Get-PodeConfig',
        'Add-PodeEndware',
        'Import-PodeModule',
        'Protect-PodeValue',
        'Resolve-PodeValue',
        'Invoke-PodeScriptBlock',
        'Test-IsUnix',
        'Test-IsWindows',
        'Test-IsPSCore',
        'Test-IsEmpty',
        'Out-PodeHost',

        # routes
        'Add-PodeRoute',
        'Add-PodeStaticRoute',
        'Remove-PodeRoute',
        'Remove-PodeStaticRoute',
        'Clear-PodeRoutes',
        'Clear-PodeStaticRoutes',
        'ConvertTo-PodeRoute',
        'Add-PodePage',

        # handlers
        'Add-PodeHandler',
        'Remove-PodeHandler',
        'Clear-PodeHandlers',

        # schedules
        'Add-PodeSchedule',
        'Remove-PodeSchedule',
        'Clear-PodeSchedule',

        # timers
        'Add-PodeTimer',
        'Remove-PodeTimer',
        'Clear-PodeTimers',

        # middleware
        'Add-PodeMiddleware',
        'Remove-PodeMiddleware',
        'Clear-PodeMiddleware',
        'Add-PodeAccessRule',
        'Add-PodeLimitRule',
        'Enable-PodeSessionMiddleware',
        'New-PodeCsrfToken',
        'Get-PodeCsrfMiddleware',
        'Initialize-PodeCsrf',
        'Enable-PodeCsrfMiddleware',
        'Remove-PodeSession',
        'Save-PodeSession',

        # auth
        'New-PodeAuthType',
        'Add-PodeAuth',
        'Add-PodeAuthWindowsAd',
        'Remove-PodeAuth',
        'Get-PodeAuthMiddleware',

        # logging
        'New-PodeLoggingMethod',
        'Enable-PodeRequestLogging',
        'Enable-PodeErrorLogging',
        'Disable-PodeRequestLogging',
        'Disable-PodeErrorLogging',
        'Add-PodeLogger',
        'Remove-PodeLogger',
        'Clear-PodeLoggers',
        'Write-PodeErrorLog',
        'Write-PodeLog',
        'Protect-PodeLogItem',

        # core
        'Start-PodeServer',
        'Show-PodeGui',
        'Add-PodeEndpoint',
        'Pode'
    )

    # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
    PrivateData = @{
        PSData = @{

            # Tags applied to this module. These help with module discovery in online galleries.
            Tags = @('powershell', 'web', 'server', 'http', 'listener', 'rest', 'api', 'tcp', 'smtp', 'websites',
                'powershell-core', 'windows', 'unix', 'linux', 'pode', 'PSEdition_Core', 'cross-platform', 'access-control',
                'file-monitoring', 'multithreaded', 'rate-limiting', 'cron', 'schedule', 'middleware', 'session',
                'authentication', 'active-directory', 'caching', 'csrf', 'arm', 'raspberry-pi', 'aws-lambda',
                'azure-functions', 'websockets')

            # A URL to the license for this module.
            LicenseUri = 'https://raw.githubusercontent.com/Badgerati/Pode/master/LICENSE.txt'

            # A URL to the main website for this project.
            ProjectUri = 'https://github.com/Badgerati/Pode'

            # A URL to an icon representing this module.
            IconUri = 'https://cdn.rawgit.com/Badgerati/Pode/master/images/icon.png'

            # Release notes for this particular version of the module
            ReleaseNotes = 'https://github.com/Badgerati/Pode/releases/tag/v1.2.0'

        }
    }
}
src\Pode.psm1
Add-Type -AssemblyName System.Web
Add-Type -AssemblyName System.Net.Http

# import everything if in a runspace
if ($PODE_SCOPE_RUNSPACE) {
    $sysfuncs = Get-ChildItem Function:
}

# load private functions
$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
Get-ChildItem "$($root)/Private/*.ps1" | Resolve-Path | ForEach-Object { . $_ }

# only import public functions if not in a runspace
if (!$PODE_SCOPE_RUNSPACE) {
    $sysfuncs = Get-ChildItem Function:
}

# load public functions
Get-ChildItem "$($root)/Public/*.ps1" | Resolve-Path | ForEach-Object { . $_ }

# get functions from memory and compare to existing to find new functions added
$funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }

# export the module's public functions
Export-ModuleMember -Function ($funcs.Name)
src\Private\Authentication.ps1
function Get-PodeAuthBasicType
{
    return {
        param($e, $options)

        # get the auth header
        $header = (Get-PodeHeader -Name 'Authorization')
        if ($null -eq $header) {
            return @{
                Message = 'No Authorization header found'
                Code = 401
            }
        }

        # ensure the first atom is basic (or opt override)
        $atoms = $header -isplit '\s+'
        if ($atoms[0] -ine $options.HeaderTag) {
            return @{
                Message = "Header is not $($options.HeaderTag) Authorization"
                Code = 400
            }
        }

        # decode the aut header
        try {
            $enc = [System.Text.Encoding]::GetEncoding($options.Encoding)
        }
        catch {
            return @{
                Message = 'Invalid encoding specified for Authorization'
                Code = 400
            }
        }

        try {
            $decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1]))
        }
        catch {
            return @{
                Message = 'Invalid Base64 string found in Authorization header'
                Code = 400
            }
        }

        # validate and return user/result
        $index = $decoded.IndexOf(':')
        $username = $decoded.Substring(0, $index)
        $password = $decoded.Substring($index + 1)

        # return data for calling validator
        return @($username, $password)
    }
}

function Get-PodeAuthFormType
{
    return {
        param($e, $options)

        # get user/pass keys to get from payload
        $userField = $options.Fields.Username
        $passField = $options.Fields.Password

        # get the user/pass
        $username = $e.Data.$userField
        $password = $e.Data.$passField

        # if either are empty, fail auth
        if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
            return @{
                Message = 'Username or Password not supplied'
                Code = 401
            }
        }

        # return data for calling validator
        return @($username, $password)
    }
}

function Get-PodeAuthInbuiltMethod
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('WindowsAd')]
        [string]
        $Type
    )

    switch ($Type.ToLowerInvariant())
    {
        'windowsad' {
            $script = {
                param($username, $password, $options)

                # validate and retrieve the AD user
                $noGroups = $options.NoGroups
                $result = Get-PodeAuthADUser -Fqdn $options.Fqdn -Username $username -Password $password -NoGroups:$noGroups

                # if there's a message, fail and return the message
                if (!(Test-IsEmpty $result.Message)) {
                    return $result
                }

                # if there's no user, then, err, oops
                if (Test-IsEmpty $result.User) {
                    return @{ Message = 'An unexpected error occured' }
                }

                # if there are no groups/users supplied, return the user
                if ((Test-IsEmpty $options.Users) -and (Test-IsEmpty $options.Groups)){
                    return $result
                }

                # before checking supplied groups, is the user in the supplied list of authorised users?
                if (!(Test-IsEmpty $options.Users) -and (@($options.Users) -icontains $result.User.Username)) {
                    return $result
                }

                # if there are groups supplied, check the user is a member of one
                if (!(Test-IsEmpty $options.Groups)) {
                    foreach ($group in $options.Groups) {
                        if (@($result.User.Groups) -icontains $group) {
                            return $result
                        }
                    }
                }

                # else, they shall not pass!
                return @{ Message = 'You are not authorised to access this website' }
            }
        }
    }

    return $script
}

function Get-PodeAuthMiddlewareScript
{
    return {
        param($e, $opts)

        # route options for using sessions
        $storeInSession = !$opts.Sessionless
        $usingSessions = (!(Test-IsEmpty $e.Session))

        # check for logout command
        if ($opts.Logout) {
            Remove-PodeAuthSession -Event $e
            $opts.Failure.Url = (Protect-PodeValue -Value $opts.Failure.Url -Default $e.Request.Url.AbsolutePath)
            return (Set-PodeAuthStatus -StatusCode 302 -Options $opts)
        }

        # if the session already has a user/isAuth'd, then skip auth
        if ($usingSessions -and !(Test-IsEmpty $e.Session.Data.Auth.User) -and $e.Session.Data.Auth.IsAuthenticated) {
            $e.Auth = $e.Session.Data.Auth
            return (Set-PodeAuthStatus -Options $opts)
        }

        # check if the auto-login flag is set, in which case just return
        if ($opts.AutoLogin) {
            if (!(Test-IsEmpty $e.Session.Data.Auth)) {
                Remove-PodeSessionCookie -Session $e.Session
            }

            return $true
        }

        # get the auth method
        $auth = $PodeContext.Server.Authentications[$opts.Name]

        try {
            # run auth type script to parse request for data
            $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Type.ScriptBlock -Arguments (@($e) + @($auth.Type.Arguments)) -Return -Splat)

            # if data is a hashtable, then don't call validator (parser either failed, or forced a success)
            if ($result -isnot [hashtable]) {
                $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments (@($result) + @($auth.Arguments)) -Return -Splat)
            }
        }
        catch {
            $_ | Write-PodeErrorLog
            return (Set-PodeAuthStatus -StatusCode 500 -Description $_.Exception.Message -Options $opts)
        }

        # if there is no result, return false (failed auth)
        if ((Test-IsEmpty $result) -or (Test-IsEmpty $result.User)) {
            return (Set-PodeAuthStatus `
                -StatusCode (Protect-PodeValue -Value $result.Code -Default 401) `
                -Description $result.Message `
                -Options $opts)
        }

        # assign the user to the session, and wire up a quick method
        $e.Auth = @{}
        $e.Auth.User = $result.User
        $e.Auth.IsAuthenticated = $true
        $e.Auth.Store = $storeInSession

        # continue
        return (Set-PodeAuthStatus -Options $opts)
    }
}

function Remove-PodeAuthSession
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Event
    )

    # blank out the auth
    $Event.Auth = @{}

    # if a session auth is found, blank it
    if (!(Test-IsEmpty $Event.Session.Data.Auth)) {
        $Event.Session.Data.Remove('Auth')
    }

    # Delete the session (remove from store, blank it, and remove from Response)
    Remove-PodeSessionCookie -Session $Event.Session
}

function Set-PodeAuthStatus
{
    param (
        [Parameter()]
        [int]
        $StatusCode = 0,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [hashtable]
        $Options
    )

    # if a statuscode supplied, assume failure
    if ($StatusCode -gt 0)
    {
        # override description with the failureMessage if supplied
        $Description = (Protect-PodeValue -Value $Options.Failure.Message -Default $Description)

        # add error to flash if flagged
        if ($Options.Failure.FlashEnabled) {
            Add-PodeFlashMessage -Name 'auth-error' -Message $Description
        }

        # check if we have a failure url redirect
        if (![string]::IsNullOrWhiteSpace($Options.Failure.Url)) {
            Move-PodeResponseUrl -Url $Options.Failure.Url
        }
        else {
            Set-PodeResponseStatus -Code $StatusCode -Description $Description
        }

        return $false
    }

    # if no statuscode, success, so check if we have a success url redirect
    if (![string]::IsNullOrWhiteSpace($Options.Success.Url)) {
        Move-PodeResponseUrl -Url $Options.Success.Url
        return $false
    }

    return $true
}

function Get-PodeAuthADUser
{
    param (
        [Parameter()]
        [string]
        $Fqdn,

        [Parameter()]
        [string]
        $Username,

        [Parameter()]
        [string]
        $Password,

        [switch]
        $NoGroups
    )

    try
    {
        # setup the dns domain
        $Fqdn = Protect-PodeValue -Value $Fqdn -Default $env:USERDNSDOMAIN

        # validate the user's AD creds
        $ad = (New-Object System.DirectoryServices.DirectoryEntry "LDAP://$($Fqdn)", "$($Username)", "$($Password)")
        if (Test-IsEmpty $ad.distinguishedName) {
            return @{ Message = 'Invalid credentials supplied' }
        }

        # generate query to find user/groups
        $query = New-Object System.DirectoryServices.DirectorySearcher $ad
        $query.filter = "(&(objectCategory=person)(samaccountname=$($Username)))"

        $user = $query.FindOne().Properties
        if (Test-IsEmpty $user) {
            return @{ Message = 'User not found in Active Directory' }
        }

        # get the users groups
        $groups =@()
        if (!$NoGroups) {
            $groups = Get-PodeAuthADGroups -Query $query -CategoryName $Username -CategoryType 'person'
        }

        # return the user
        return @{
            User = @{
                Username = $Username
                Name = @($user.name)[0]
                Fqdn = $Fqdn
                Groups = $groups
            }
        }
    }
    finally {
        if (!(Test-IsEmpty $ad.distinguishedName)) {
            Close-PodeDisposable -Disposable $query
            Close-PodeDisposable -Disposable $ad -Close
        }
    }
}

function Get-PodeAuthADGroups
{
    param (
        [Parameter(Mandatory=$true)]
        [System.DirectoryServices.DirectorySearcher]
        $Query,

        [Parameter(Mandatory=$true)]
        [string]
        $CategoryName,

        [Parameter(Mandatory=$true)]
        [ValidateSet('group', 'person')]
        [string]
        $CategoryType,

        [Parameter()]
        [hashtable]
        $GroupsFound = $null
    )

    # setup found groups
    if ($null -eq $GroupsFound) {
        $GroupsFound = @{}
    }

    # get the groups for the category
    $Query.filter = "(&(objectCategory=$($CategoryType))(samaccountname=$($CategoryName)))"

    $groups = @{}
    foreach ($member in $Query.FindOne().Properties.memberof) {
        if ($member -imatch '^CN=(?<group>.+?),') {
            $g = $Matches['group']
            $groups[$g] = ($member -imatch '=builtin,')
        }
    }

    foreach ($group in $groups.Keys) {
        # don't bother if we've already looked up the group
        if ($GroupsFound.ContainsKey($group)) {
            continue
        }

        # add group to checked groups
        $GroupsFound[$group] = $true

        # don't bother if it's inbuilt
        if ($groups[$group]) {
            continue
        }

        # get the groups
        Get-PodeAuthADGroups -Query $Query -CategoryName $group -CategoryType 'group' -GroupsFound $GroupsFound
    }

    if ($CategoryType -ieq 'person') {
        return $GroupsFound.Keys
    }
}
src\Private\Context.ps1
function New-PodeContext
{
    param (
        [scriptblock]
        $ScriptBlock,

        [int]
        $Threads = 1,

        [int]
        $Interval = 0,

        [string]
        $ServerRoot,

        [string]
        $Name = $null,

        [string]
        $ServerType
    )

    # set a random server name if one not supplied
    if (Test-IsEmpty $Name) {
        $Name = Get-PodeRandomName
    }

    # are we running in a serverless context
    $isServerless = (@('AzureFunctions', 'AwsLambda') -icontains $ServerType)

    # ensure threads are always >0, for to 1 if we're serverless
    if (($Threads -le 0) -or $isServerless) {
        $Threads = 1
    }

    # basic context object
    $ctx = New-Object -TypeName psobject |
        Add-Member -MemberType NoteProperty -Name Threads -Value $Threads -PassThru |
        Add-Member -MemberType NoteProperty -Name Timers -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name Schedules -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name RunspacePools -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name Runspaces -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name RunspaceState -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name Tokens -Value @{} -PassThru |
        Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name Lockable -Value $null -PassThru |
        Add-Member -MemberType NoteProperty -Name Server -Value @{} -PassThru

    # set the server name, logic and root
    $ctx.Server.Name = $Name
    $ctx.Server.Logic = $ScriptBlock
    $ctx.Server.Interval = $Interval
    $ctx.Server.PodeModulePath = (Get-PodeModulePath)

    # basic logging setup
    $ctx.Server.Logging = @{
        Enabled = $true
        Types = @{}
    }

    # set socket details for pode server
    $ctx.Server.Sockets = @{
        Listeners = @()
        MaxConnections = 0
        Ssl = @{
            Callback = $null
            Protocols = (ConvertTo-PodeSslProtocols -Protocols @('Ssl3', 'Tls12'))
        }
        ReceiveTimeout = 100
        Queues = @{
            Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new()
        }
    }

    $ctx.Server.WebSockets = @{
        Enabled = $false
        Listeners = @()
        MaxConnections = 0
        Ssl = @{
            Callback = $null
            Protocols = (ConvertTo-PodeSslProtocols -Protocols @('Ssl3', 'Tls12'))
        }
        ReceiveTimeout = 100
        Queues = @{
            Sockets = @{}
            Messages = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new()
            Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new()
        }
    }

    # check if there is any global configuration
    $ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx

    # configure the server's root path
    $ctx.Server.Root = $ServerRoot
    if (!(Test-IsEmpty $ctx.Server.Configuration.Server.Root)) {
        $ctx.Server.Root = Get-PodeRelativePath -Path $ctx.Server.Configuration.Server.Root -RootPath $ctx.Server.Root -JoinRoot -Resolve -TestPath
    }

    # set the server default type
    $ctx.Server.Type = $ServerType.ToUpperInvariant()
    if ($Interval -gt 0) {
        $ctx.Server.Type = 'SERVICE'
    }

    if ($isServerless) {
        $ctx.Server.IsServerless = $isServerless
    }

    # set the IP address details
    $ctx.Server.Endpoints = @()

    # general encoding for the server
    $ctx.Server.Encoding = New-Object System.Text.UTF8Encoding

    # setup gui details
    $ctx.Server.Gui = @{}

    # shared temp drives
    $ctx.Server.Drives = @{}
    $ctx.Server.InbuiltDrives = @{}

    # shared state between runspaces
    $ctx.Server.State = @{}

    # view engine for rendering pages
    $ctx.Server.ViewEngine = @{
        Type = 'html'
        Extension = 'html'
        Script = $null
        IsDynamic = $false
    }

    # routes for pages and api
    $ctx.Server.Routes = @{
        'delete' = @{};
        'get' = @{};
        'head' = @{};
        'merge' = @{};
        'options' = @{};
        'patch' = @{};
        'post' = @{};
        'put' = @{};
        'trace' = @{};
        'static' = @{};
        '*' = @{};
    }

    # handlers for tcp
    $ctx.Server.Handlers = @{
        'tcp' = @{};
        'smtp' = @{};
        'service' = @{};
    }

    # setup basic access placeholders
    $ctx.Server.Access = @{
        Allow = @{}
        Deny = @{}
    }

    # setup basic limit rules
    $ctx.Server.Limits = @{
        Rules = @{}
        Active = @{}
    }

    # cookies and session logic
    $ctx.Server.Cookies = @{
        Session = @{}
        Csrf = @{}
        Secrets = @{}
    }

    # authnetication methods
    $ctx.Server.Authentications = @{}

    # create new cancellation tokens
    $ctx.Tokens = @{
        Cancellation = New-Object System.Threading.CancellationTokenSource
        Restart = New-Object System.Threading.CancellationTokenSource
    }

    # requests that should be logged
    $ctx.LogsToProcess = New-Object System.Collections.ArrayList

    # middleware that needs to run
    $ctx.Server.Middleware = @()
    $ctx.Server.BodyParsers = @{}

    # endware that needs to run
    $ctx.Server.Endware = @()

    # runspace pools
    $ctx.RunspacePools = @{
        Main = $null
        Signals = $null
        Schedules = $null
        Gui = $null
    }

    # session state
    $ctx.Lockable = [hashtable]::Synchronized(@{})

    # setup runspaces
    $ctx.Runspaces = @()

    # return the new context
    return $ctx
}

function New-PodeRunspaceState
{
    $state = [initialsessionstate]::CreateDefault()
    $state.ImportPSModule($PodeContext.Server.PodeModulePath)

    $session = New-PodeStateContext -Context $PodeContext

    $variables = @(
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PodeContext', $session, $null),
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'Console', $Host, $null),
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PODE_SCOPE_RUNSPACE', $true, $null)
    )

    foreach ($var in $variables) {
        $state.Variables.Add($var)
    }

    $PodeContext.RunspaceState = $state
}

function New-PodeRunspacePools
{
    if ($PodeContext.Server.IsServerless) {
        return
    }

    # setup main runspace pool
    $threadsCounts = @{
        Default = 3
        Timer = 1
        Log = 1
        Schedule = 1
        Misc = 1
    }

    $totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum + $PodeContext.Threads
    $PodeContext.RunspacePools.Main = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $PodeContext.RunspaceState, $Host)
    $PodeContext.RunspacePools.Main.Open()

    # setup signal runspace pool
    $PodeContext.RunspacePools.Signals = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads + 2), $PodeContext.RunspaceState, $Host)
    $PodeContext.RunspacePools.Signals.Open()

    # setup schedule runspace pool
    $PodeContext.RunspacePools.Schedules = [runspacefactory]::CreateRunspacePool(1, 2, $PodeContext.RunspaceState, $Host)
    $PodeContext.RunspacePools.Schedules.Open()

    # setup gui runspace pool (only for non-ps-core)
    if (!((Test-IsPSCore) -and ($PSVersionTable.PSVersion.Major -eq 6))) {
        $PodeContext.RunspacePools.Gui = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host)
        $PodeContext.RunspacePools.Gui.ApartmentState = 'STA'
        $PodeContext.RunspacePools.Gui.Open()
    }
}

function New-PodeStateContext
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Context
    )

    return (New-Object -TypeName psobject |
        Add-Member -MemberType NoteProperty -Name Threads -Value $Context.Threads -PassThru |
        Add-Member -MemberType NoteProperty -Name Timers -Value $Context.Timers -PassThru |
        Add-Member -MemberType NoteProperty -Name Schedules -Value $Context.Schedules -PassThru |
        Add-Member -MemberType NoteProperty -Name RunspacePools -Value $Context.RunspacePools -PassThru |
        Add-Member -MemberType NoteProperty -Name Tokens -Value $Context.Tokens -PassThru |
        Add-Member -MemberType NoteProperty -Name LogsToProcess -Value $Context.LogsToProcess -PassThru |
        Add-Member -MemberType NoteProperty -Name Lockable -Value $Context.Lockable -PassThru |
        Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru)
}

function Open-PodeConfiguration
{
    param (
        [Parameter()]
        [string]
        $ServerRoot = $null,

        [Parameter()]
        $Context
    )

    $config = @{}

    # set the path to the root config file
    $configPath = (Join-PodeServerRoot -Folder '.' -FilePath 'server.psd1' -Root $ServerRoot)

    # check to see if an environmental config exists (if the env var is set)
    if (!(Test-IsEmpty $env:PODE_ENVIRONMENT)) {
        $_path = (Join-PodeServerRoot -Folder '.' -FilePath "server.$($env:PODE_ENVIRONMENT).psd1" -Root $ServerRoot)
        if (Test-PodePath -Path $_path -NoStatus) {
            $configPath = $_path
        }
    }

    # check the path exists, and load the config
    if (Test-PodePath -Path $configPath -NoStatus) {
        $config = Import-PowerShellDataFile -Path $configPath -ErrorAction Stop
        Set-PodeServerConfiguration -Configuration $config.Server -Context $Context
        Set-PodeWebConfiguration -Configuration $config.Web -Context $Context
    }

    return $config
}

function Set-PodeServerConfiguration
{
    param (
        [Parameter()]
        [hashtable]
        $Configuration,

        [Parameter()]
        $Context
    )

    # file monitoring
    $Context.Server.FileMonitor = @{
        Enabled = ([bool]$Configuration.FileMonitor.Enable)
        Exclude = (Convert-PodePathPatternsToRegex -Paths @($Configuration.FileMonitor.Exclude))
        Include = (Convert-PodePathPatternsToRegex -Paths @($Configuration.FileMonitor.Include))
        ShowFiles = ([bool]$Configuration.FileMonitor.ShowFiles)
        Files = @()
    }

    # logging
    $Context.Server.Logging = @{
        Enabled = !([bool]$Configuration.Logging.Enable)
        Masking = @{
            Patterns = (Remove-PodeEmptyItemsFromArray -Array @($Configuration.Logging.Masking.Patterns))
            Mask = (Protect-PodeValue -Value $Configuration.Logging.Masking.Mask -Default '********')
        }
        Types = @{}
    }

    # sockets (pode)
    if (!(Test-IsEmpty $Configuration.Pode.Ssl.Protocols)) {
        $Context.Server.Sockets.Ssl.Protocols = (ConvertTo-PodeSslProtocols -Protocols $Configuration.Pode.Ssl.Protocols)
    }

    if ([int]$Configuration.Pode.ReceiveTimeout -gt 0) {
        $Context.Server.Sockets.ReceiveTimeout = (Protect-PodeValue -Value $Configuration.Pode.ReceiveTimeout $Context.Server.Sockets.ReceiveTimeout)
    }
}

function Set-PodeWebConfiguration
{
    param (
        [Parameter()]
        [hashtable]
        $Configuration,

        [Parameter()]
        $Context
    )

    # setup the main web config
    $Context.Server.Web = @{
        Static = @{
            Defaults = $Configuration.Static.Defaults
            Cache = @{
                Enabled = [bool]$Configuration.Static.Cache.Enable
                MaxAge = [int](Protect-PodeValue -Value $Configuration.Static.Cache.MaxAge -Default 3600)
                Include = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Include) -NotSlashes)
                Exclude = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Exclude) -NotSlashes)
            }
        }
        ErrorPages = @{
            ShowExceptions = [bool]$Configuration.ErrorPages.ShowExceptions
            StrictContentTyping = [bool]$Configuration.ErrorPages.StrictContentTyping
            Default = $Configuration.ErrorPages.Default
            Routes = @{}
        }
        ContentType = @{
            Default = $Configuration.ContentType.Default
            Routes = @{}
        }
    }

    # setup content type route patterns for forced content types
    $Configuration.ContentType.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
        $_type = $Configuration.ContentType.Routes[$_]
        $_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
        $Context.Server.Web.ContentType.Routes[$_pattern] = $_type
    }

    # setup content type route patterns for error pages
    $Configuration.ErrorPages.Routes.Keys | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | ForEach-Object {
        $_type = $Configuration.ErrorPages.Routes[$_]
        $_pattern = (Convert-PodePathPatternToRegex -Path $_ -NotSlashes)
        $Context.Server.Web.ErrorPages.Routes[$_pattern] = $_type
    }
}

function New-PodeAutoRestartServer
{
    # don't configure if not supplied, or running as serverless
    $config = (Get-PodeConfig)
    if (($null -eq $config) -or ($null -eq $config.Server.Restart) -or $PodeContext.Server.IsServerless)  {
        return
    }

    $restart = $config.Server.Restart

    # period - setup a timer
    $period = [int]$restart.period
    if ($period -gt 0) {
        Add-PodeTimer -Name '__pode_restart_period__' -Interval ($period * 60) -ScriptBlock {
            $PodeContext.Tokens.Restart.Cancel()
        }
    }

    # times - convert into cron expressions
    $times = @(@($restart.times) -ne $null)
    if (($times | Measure-Object).Count -gt 0) {
        $crons = @()

        @($times) | ForEach-Object {
            $atoms = $_ -split '\:'
            $crons += "$([int]$atoms[1]) $([int]$atoms[0]) * * *"
        }

        Add-PodeSchedule -Name '__pode_restart_times__' -Cron @($crons) -ScriptBlock {
            $PodeContext.Tokens.Restart.Cancel()
        }
    }

    # crons - setup schedules
    $crons = @(@($restart.crons) -ne $null)
    if (($crons | Measure-Object).Count -gt 0) {
        Add-PodeSchedule -Name '__pode_restart_crons__' -Cron @($crons) -ScriptBlock {
            $PodeContext.Tokens.Restart.Cancel()
        }
    }
}

function Get-PodeEndpoints
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet('Http', 'Ws')]
        [string]
        $Type
    )

    $endpoints = $null

    switch ($Type.ToLowerInvariant()) {
        'http' {
            $endpoints = @($PodeContext.Server.Endpoints | Where-Object { @('http', 'https') -icontains $_.Protocol })
        }

        'ws' {
            $endpoints = @($PodeContext.Server.Endpoints | Where-Object { @('ws', 'wss') -icontains $_.Protocol })
        }
    }

    return $endpoints
}
src\Private\Cookies.ps1
function Invoke-PodeCookieSign
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Secret
    )

    return "s:$($Value).$(Invoke-PodeHMACSHA256Hash -Value $Value -Secret $Secret)"
}

function Invoke-PodeCookieUnsign
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Signature,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Secret
    )

    # the signed cookie value must start with "s:"
    if (!$Signature.StartsWith('s:')) {
        return $null
    }

    $Signature = $Signature.Substring(2)
    $periodIndex = $Signature.LastIndexOf('.')
    if ($periodIndex -eq -1) {
        return $null
    }

    $value = $Signature.Substring(0, $periodIndex)
    $sig = $Signature.Substring($periodIndex + 1)

    if ((Invoke-PodeHMACSHA256Hash -Value $value -Secret $Secret) -ne $sig) {
        return $null
    }

    return $value
}

function ConvertTo-PodeCookie
{
    param (
        [Parameter()]
        [System.Net.Cookie]
        $Cookie
    )

    if ($null -eq $Cookie) {
        return @{}
    }

    return @{
        Name = $Cookie.Name
        Value = $Cookie.Value
        Expires = $Cookie.Expires
        Expired = $Cookie.Expired
        Discard = $Cookie.Discard
        HttpOnly = $Cookie.HttpOnly
        Secure = $Cookie.Secure
        Path = $Cookie.Path
        TimeStamp = $Cookie.TimeStamp
        Signed = $Cookie.Value.StartsWith('s:')
    }
}

function ConvertTo-PodeCookieString
{
    param (
        [Parameter(Mandatory=$true)]
        $Cookie
    )

    $str = "$($Cookie.Name)=$($Cookie.Value)"

    if ($Cookie.Discard) {
        $str += '; Discard'
    }

    if ($Cookie.HttpOnly) {
        $str += '; HttpOnly'
    }

    if ($Cookie.Secure) {
        $str += '; Secure'
    }

    if (![string]::IsNullOrWhiteSpace($Cookie.Domain)) {
        $str += "; Domain=$($Cookie.Domain)"
    }

    if (![string]::IsNullOrWhiteSpace($Cookie.Path)) {
        $str += "; Path=$($Cookie.Path)"
    }

    if ($null -ne $Cookie.Expires -and $Cookie.Expires -ne [datetime]::MinValue) {
        $secs = ($Cookie.Expires.ToLocalTime() - [datetime]::Now).TotalSeconds
        if ($secs -lt 0) {
            $secs = 0
        }

        $str += "; Max-Age=$($secs)"
    }

    if ($str -eq '=') {
        return $null
    }

    return $str
}
src\Private\CronParser.ps1
function Get-PodeCronFields
{
    return @(
        'Minute',
        'Hour',
        'DayOfMonth',
        'Month',
        'DayOfWeek'
    )
}

function Get-PodeCronFieldConstraints
{
    return @{
        'MinMax' = @(
            @(0, 59),
            @(0, 23),
            @(1, 31),
            @(1, 12),
            @(0, 6)
        );
        'DaysInMonths' = @(
            31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        );
        'Months' = @(
            'January', 'February', 'March', 'April', 'May', 'June', 'July',
            'August', 'September', 'October', 'November', 'December'
        );
    }
}

function Get-PodeCronPredefined
{
    return @{
        # normal
        '@minutely' = '* * * * *';
        '@hourly' = '0 * * * *';
        '@daily' = '0 0 * * *';
        '@weekly' = '0 0 * * 0';
        '@monthly' = '0 0 1 * *';
        '@quarterly' = '0 0 1 1,4,7,10';
        '@yearly' = '0 0 1 1 *';
        '@annually' = '0 0 1 1 *';

        # twice
        '@twice-hourly' = '0,30 * * * *';
        '@twice-daily' = '0 0,12 * * *';
        '@twice-weekly' = '0 0 * * 0,4';
        '@twice-monthly' = '0 0 1,15 * *';
        '@twice-yearly' = '0 0 1 1,6 *';
        '@twice-annually' = '0 0 1 1,6 *';
    }
}

function Get-PodeCronFieldAliases
{
    return @{
        'Month' = @{
            'Jan' = 1;
            'Feb' = 2;
            'Mar' = 3;
            'Apr' = 4;
            'May' = 5;
            'Jun' = 6;
            'Jul' = 7;
            'Aug' = 8;
            'Sep' = 9;
            'Oct' = 10;
            'Nov' = 11;
            'Dec' = 12;
        };
        'DayOfWeek' = @{
            'Sun' = 0;
            'Mon' = 1;
            'Tue' = 2;
            'Wed' = 3;
            'Thu' = 4;
            'Fri' = 5;
            'Sat' = 6;
        };
    }
}

function ConvertFrom-PodeCronExpressions
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Expressions
    )

    return @(@($Expressions) | ForEach-Object {
        ConvertFrom-PodeCronExpression -Expression $_
    })
}

function ConvertFrom-PodeCronExpression
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Expression
    )

    $Expression = $Expression.Trim()

    # check predefineds
    $predef = Get-PodeCronPredefined
    if (!(Test-IsEmpty $predef[$Expression])) {
        $Expression = $predef[$Expression]
    }

    # split and check atoms length
    $atoms = @($Expression -isplit '\s+')
    if ($atoms.Length -ne 5) {
        throw "Cron expression should only consist of 5 parts: $($Expression)"
    }

    # basic variables
    $aliasRgx = '(?<tag>[a-z]{3})'

    # get cron obj and validate atoms
    $fields = Get-PodeCronFields
    $constraints = Get-PodeCronFieldConstraints
    $aliases = Get-PodeCronFieldAliases
    $cron = @{}

    for ($i = 0; $i -lt $atoms.Length; $i++)
    {
        $_cronExp = @{
            'Range' = $null;
            'Values' = $null;
            'Constraints' = $null;
            'Random' = $false;
        }

        $_atom = $atoms[$i]
        $_field = $fields[$i]
        $_constraint = $constraints.MinMax[$i]
        $_aliases = $aliases[$_field]

        # replace day of week and months with numbers
        switch ($_field)
        {
            { $_field -ieq 'month' -or $_field -ieq 'dayofweek' }
                {
                    while ($_atom -imatch $aliasRgx) {
                        $_alias = $_aliases[$Matches['tag']]
                        if ($null -eq $_alias) {
                            throw "Invalid $($_field) alias found: $($Matches['tag'])"
                        }

                        $_atom = $_atom -ireplace $Matches['tag'], $_alias
                        $_atom -imatch $aliasRgx | Out-Null
                    }
                }
        }

        # ensure atom is a valid value
        if (!($_atom -imatch '^[\d|/|*|\-|,r]+$')) {
            throw "Invalid atom character: $($_atom)"
        }

        # replace * with min/max constraint
        $_atom = $_atom -ireplace '\*', ($_constraint -join '-')

        # parse the atom for either a literal, range, array, or interval
        # literal
        if ($_atom -imatch '^(\d+|r)$') {
            # check if it's random
            if ($_atom -ieq 'r') {
                $_cronExp.Values = @(Get-Random -Minimum $_constraint[0] -Maximum ($_constraint[1] + 1))
                $_cronExp.Random = $true
            }
            else {
                $_cronExp.Values = @([int]$_atom)
            }
        }

        # range
        elseif ($_atom -imatch '^(?<min>\d+)\-(?<max>\d+)$') {
            $_cronExp.Range = @{ 'Min' = [int]($Matches['min'].Trim()); 'Max' = [int]($Matches['max'].Trim()); }
        }

        # array
        elseif ($_atom -imatch '^[\d,]+$') {
            $_cronExp.Values = [int[]](@($_atom -split ',').Trim())
        }

        # interval
        elseif ($_atom -imatch '(?<start>(\d+|\*))\/(?<interval>(\d+|r))$') {
            $start = $Matches['start']
            $interval = $Matches['interval']

            if ($interval -ieq '0') {
                $interval = '1'
            }

            if ([string]::IsNullOrWhiteSpace($start) -or $start -ieq '*') {
                $start = '0'
            }

            # set the initial trigger value
            $_cronExp.Values = @([int]$start)

            # check if it's random
            if ($interval -ieq 'r') {
                $_cronExp.Random = $true
            }
            else {
                # loop to get all next values
                $next = [int]$start + [int]$interval
                while ($next -le $_constraint[1]) {
                    $_cronExp.Values += $next
                    $next += [int]$interval
                }
            }
        }

        # error
        else {
            throw "Invalid cron atom format found: $($_atom)"
        }

        # ensure cron expression values are valid
        if ($null -ne $_cronExp.Range) {
            if ($_cronExp.Range.Min -gt $_cronExp.Range.Max) {
                throw "Min value for $($_field) should not be greater than the max value"
            }

            if ($_cronExp.Range.Min -lt $_constraint[0]) {
                throw "Min value '$($_cronExp.Range.Min)' for $($_field) is invalid, should be greater than/equal to $($_constraint[0])"
            }

            if ($_cronExp.Range.Max -gt $_constraint[1]) {
                throw "Max value '$($_cronExp.Range.Max)' for $($_field) is invalid, should be less than/equal to $($_constraint[1])"
            }
        }

        if ($null -ne $_cronExp.Values) {
            $_cronExp.Values | ForEach-Object {
                if ($_ -lt $_constraint[0] -or $_ -gt $_constraint[1]) {
                    throw "Value '$($_)' for $($_field) is invalid, should be between $($_constraint[0]) and $($_constraint[1])"
                }
            }
        }

        # assign value
        $_cronExp.Constraints = $_constraint
        $cron[$_field] = $_cronExp
    }

    # post validation for month/days in month
    if ($null -ne $cron['Month'].Values -and $null -ne $cron['DayOfMonth'].Values)
    {
        foreach ($mon in $cron['Month'].Values) {
            foreach ($day in $cron['DayOfMonth'].Values) {
                if ($day -gt $constraints.DaysInMonths[$mon - 1]) {
                    throw "$($constraints.Months[$mon - 1]) only has $($constraints.DaysInMonths[$mon - 1]) days, but $($day) was supplied"
                }
            }
        }
    }

    # flag if this cron contains a random atom
    $cron['Random'] = (($cron.Values | Where-Object { $_.Random } | Measure-Object).Count -gt 0)

    # return the parsed cron expression
    return $cron
}

function Reset-PodeRandomCronExpressions
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Expressions
    )

    return @(@($Expressions) | ForEach-Object {
        Reset-PodeRandomCronExpression -Expression $_
    })
}

function Reset-PodeRandomCronExpression
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Expression
    )

    function Reset-Atom($Atom) {
        if (!$Atom.Random) {
            return $Atom
        }

        if ($Atom.Random) {
            $Atom.Values = @(Get-Random -Minimum $Atom.Constraints[0] -Maximum ($Atom.Constraints[1] + 1))
        }

        return $Atom
    }

    if (!$Expression.Random) {
        return $Expression
    }

    $Expression.Minute = (Reset-Atom -Atom $Expression.Minute)
    $Expression.Hour = (Reset-Atom -Atom $Expression.Hour)
    $Expression.DayOfMonth = (Reset-Atom -Atom $Expression.DayOfMonth)
    $Expression.Month = (Reset-Atom -Atom $Expression.Month)
    $Expression.DayOfWeek = (Reset-Atom -Atom $Expression.DayOfWeek)

    return $Expression
}

function Test-PodeCronExpressions
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Expressions,

        [Parameter()]
        $DateTime = $null
    )

    return ((@($Expressions) | Where-Object {
        Test-PodeCronExpression -Expression $_ -DateTime $DateTime
    } | Measure-Object).Count -gt 0)
}

function Test-PodeCronExpression
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Expression,

        [Parameter()]
        $DateTime = $null
    )

    function Test-RangeAndValue($AtomContraint, $NowValue) {
        if ($null -ne $AtomContraint.Range) {
            if ($NowValue -lt $AtomContraint.Range.Min -or $NowValue -gt $AtomContraint.Range.Max) {
                return $false
            }
        }
        elseif ($AtomContraint.Values -inotcontains $NowValue) {
            return $false
        }

        return $true
    }

    # current time
    if ($null -eq $DateTime) {
        $DateTime = [datetime]::Now
    }

    # check day of month
    if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfMonth -NowValue $DateTime.Day)) {
        return $false
    }

    # check day of week
    if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfWeek -NowValue ([int]$DateTime.DayOfWeek))) {
        return $false
    }

    # check month
    if (!(Test-RangeAndValue -AtomContraint $Expression.Month -NowValue $DateTime.Month)) {
        return $false
    }

    # check hour
    if (!(Test-RangeAndValue -AtomContraint $Expression.Hour -NowValue $DateTime.Hour)) {
        return $false
    }

    # check minute
    if (!(Test-RangeAndValue -AtomContraint $Expression.Minute -NowValue $DateTime.Minute)) {
        return $false
    }

    # date is valid
    return $true
}
src\Private\Cryptography.ps1
function Invoke-PodeHMACSHA256Hash
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Secret
    )

    $crypto = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($Secret))
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function Invoke-PodeSHA256Hash
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value
    )

    $crypto = [System.Security.Cryptography.SHA256]::Create()
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function Invoke-PodeSHA1Hash
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Value
    )

    $crypto = [System.Security.Cryptography.SHA1]::Create()
    return [System.Convert]::ToBase64String($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)))
}

function Get-PodeRandomBytes
{
    param (
        [Parameter()]
        [int]
        $Length = 16
    )

    return (Use-PodeStream -Stream ([System.Security.Cryptography.RandomNumberGenerator]::Create()) {
        param($p)
        $bytes = [byte[]]::new($Length)
        $p.GetBytes($bytes)
        return $bytes
    })
}

function New-PodeSalt
{
    param (
        [Parameter()]
        [int]
        $Length = 8
    )

    $bytes = [byte[]](Get-PodeRandomBytes -Length $Length)
    return [System.Convert]::ToBase64String($bytes)
}

function New-PodeGuid
{
    param (
        [Parameter()]
        [int]
        $Length = 16,

        [switch]
        $Secure
    )

    # generate a cryptographically secure guid
    if ($Secure) {
        $bytes = [byte[]](Get-PodeRandomBytes -Length $Length)
        return ([guid]::new($bytes)).ToString()
    }

    # return a normal guid
    return ([guid]::NewGuid()).ToString()
}
src\Private\Endware.ps1
function Invoke-PodeEndware
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $WebEvent,

        [Parameter()]
        $Endware
    )

    # if there's no endware, do nothing
    if (($null -eq $Endware) -or ($Endware.Length -eq 0)) {
        return
    }

    # loop through each of the endware, invoking the next if it returns true
    foreach ($eware in @($Endware))
    {
        try {
            Invoke-PodeScriptBlock -ScriptBlock $eware.Logic -Arguments (@($WebEvent) + @($eware.Arguments)) -Scoped -Splat | Out-Null
        }
        catch {
            $_ | Write-PodeErrorLog
        }
    }
}
src\Private\FileMonitor.ps1
function Start-PodeFileMonitor
{
    # don't configure if not supplied, or we're running as serverless
    if (!$PodeContext.Server.FileMonitor.Enabled -or $PodeContext.Server.IsServerless) {
        return
    }

    # what folder and filter are we moitoring?
    $folder = $PodeContext.Server.Root
    $filter = '*.*'

    # setup the file monitor
    $watcher = New-Object System.IO.FileSystemWatcher $folder, $filter -Property @{
        IncludeSubdirectories = $true;
        NotifyFilter = [System.IO.NotifyFilters]'FileName,LastWrite,CreationTime';
    }

    $watcher.EnableRaisingEvents = $true

    # setup the monitor timer - only restart server after changes + 2s of no changes
    $timer = New-Object System.Timers.Timer
    $timer.AutoReset = $false
    $timer.Interval = 2000

    # setup the message data for the events
    $msgData = @{
        Timer = $timer
        Settings = $PodeContext.Server.FileMonitor
    }

    # setup the events script logic
    $action = {
        # if there are exclusions, and one matches, return
        if (($null -ne $Event.MessageData.Settings.Exclude) -and ($Event.SourceEventArgs.Name -imatch $Event.MessageData.Settings.Exclude)) {
            return
        }

        # if there are inclusions, and none match, return
        if (($null -ne $Event.MessageData.Settings.Include) -and ($Event.SourceEventArgs.Name -inotmatch $Event.MessageData.Settings.Include)) {
            return
        }

        # if enabled, add the file to the list of files that trigggered the restart
        if ($Event.MessageData.Settings.ShowFiles) {
            $name = "[$($Event.SourceEventArgs.ChangeType)] $($Event.SourceEventArgs.Name)"
            
            if ($Event.MessageData.Settings.Files -inotcontains $name) {
                $Event.MessageData.Settings.Files += $name
            }
        }

        # restart the timer
        $Event.MessageData.Timer.Stop()
        $Event.MessageData.Timer.Start()
    }

    # listen out of file created, changed, deleted events
    Register-ObjectEvent -InputObject $watcher -EventName 'Created' `
        -SourceIdentifier (Get-PodeFileMonitorName Create) -Action $action -MessageData $msgData -SupportEvent

    Register-ObjectEvent -InputObject $watcher -EventName 'Changed' `
        -SourceIdentifier (Get-PodeFileMonitorName Update) -Action $action -MessageData $msgData -SupportEvent

    Register-ObjectEvent -InputObject $watcher -EventName 'Deleted' `
        -SourceIdentifier (Get-PodeFileMonitorName Delete) -Action $action -MessageData $msgData -SupportEvent

    # listen out for timer ticks to reset server
    Register-ObjectEvent -InputObject $timer -EventName 'Elapsed' -SourceIdentifier (Get-PodeFileMonitorTimerName) -Action {
        # if enabled, show the files that triggered the restart
        if ($Event.MessageData.FileSettings.ShowFiles) {
            Write-Host 'The following files have changed:' -ForegroundColor Magenta

            foreach ($file in $Event.MessageData.FileSettings.Files) {
                Write-Host "> $($file)" -ForegroundColor Magenta
            }

            $Event.MessageData.FileSettings.Files = @()
        }

        # trigger the restart
        $Event.MessageData.Tokens.Restart.Cancel()
        $Event.Sender.Stop()
    } -MessageData @{
        Tokens = $PodeContext.Tokens
        FileSettings = $PodeContext.Server.FileMonitor
    } -SupportEvent
}

function Stop-PodeFileMonitor
{
    if ($PodeContext.Server.IsServerless) {
        return
    }

    if ($PodeContext.Server.FileMonitor.Enabled) {
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Create) -Force
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Delete) -Force
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorName Update) -Force
        Unregister-Event -SourceIdentifier (Get-PodeFileMonitorTimerName) -Force
    }
}

function Get-PodeFileMonitorName
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Create', 'Delete', 'Update')]
        [string]
        $Type
    )

    return "PodeFileMonitor$($Type)"
}

function Get-PodeFileMonitorTimerName
{
    return 'PodeFileMonitorTimer'
}
src\Private\Gui.ps1
function Start-PodeGuiRunspace
{
    # do nothing if gui not enabled, or running as serverless
    if (!$PodeContext.Server.Gui.Enabled -or $PodeContext.Server.IsServerless) {
        return
    }

    $script = {
        try
        {
            # if there are multiple endpoints, flag warning we're only using the first - unless explicitly set
            if ($null -eq $PodeContext.Server.Gui.Endpoint)
            {
                if (($PodeContext.Server.Endpoints | Measure-Object).Count -gt 1) {
                    Write-Host "Multiple endpoints defined, only the first will be used for the GUI" -ForegroundColor Yellow
                }
            }

            # get the endpoint on which we're currently listening, or use explicitly passed one
            $endpoint = (Get-PodeEndpointUrl -Endpoint $PodeContext.Server.Gui.Endpoint)

            # poll the server for a response
            $count = 0

            while ($true) {
                try {
                    Invoke-WebRequest -Method Get -Uri $endpoint -UseBasicParsing -ErrorAction Stop | Out-Null
                    if (!$?) {
                        throw
                    }

                    break
                }
                catch {
                    $count++
                    if ($count -le 50) {
                        Start-Sleep -Milliseconds 200
                    }
                    else {
                        throw "Failed to connect to URL: $($endpoint)"
                    }
                }
            }

            # import the WPF assembly
            [System.Reflection.Assembly]::LoadWithPartialName('PresentationFramework') | Out-Null
            [System.Reflection.Assembly]::LoadWithPartialName('PresentationCore') | Out-Null

            # setup the WPF XAML for the server
            $gui_browser = "
                <Window
                    xmlns=`"http://schemas.microsoft.com/winfx/2006/xaml/presentation`"
                    xmlns:x=`"http://schemas.microsoft.com/winfx/2006/xaml`"
                    Title=`"$($PodeContext.Server.Gui.Title)`"
                    Height=`"$($PodeContext.Server.Gui.Height)`"
                    Width=`"$($PodeContext.Server.Gui.Width)`"
                    ResizeMode=`"$($PodeContext.Server.Gui.ResizeMode)`"
                    WindowStartupLocation=`"CenterScreen`"
                    ShowInTaskbar = `"$($PodeContext.Server.Gui.ShowInTaskbar)`"
                    WindowStyle = `"$($PodeContext.Server.Gui.WindowStyle)`">
                        <Window.TaskbarItemInfo>
                            <TaskbarItemInfo />
                        </Window.TaskbarItemInfo>
                        <WebBrowser Name=`"WebBrowser`"></WebBrowser>
                </Window>"

            # read in the XAML
            $reader = [System.Xml.XmlNodeReader]::new([xml]$gui_browser)
            $form = [Windows.Markup.XamlReader]::Load($reader)

            # set other options
            $form.TaskbarItemInfo.Description = $form.Title

            # add the icon to the form
            if (!(Test-IsEmpty $PodeContext.Server.Gui.Icon)) {
                $icon = [Uri]::new($PodeContext.Server.Gui.Icon)
                $form.Icon = [Windows.Media.Imaging.BitmapFrame]::Create($icon)
            }

            # set the state of the window onload
            if (!(Test-IsEmpty $PodeContext.Server.Gui.WindowState)) {
                $form.WindowState = $PodeContext.Server.Gui.WindowState
            }

            # get the browser object from XAML and navigate to base page
            $form.FindName("WebBrowser").Navigate($endpoint)

            # display the form
            $form.ShowDialog() | Out-Null
            Start-Sleep -Seconds 1
        }
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
        finally {
            # invoke the cancellation token to close the server
            $PodeContext.Tokens.Cancellation.Cancel()
        }
    }

    Add-PodeRunspace -Type 'Gui' -ScriptBlock $script
}
src\Private\Helpers.ps1

# read in the content from a dynamic pode file and invoke its content
function ConvertFrom-PodeFile
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Content,

        [Parameter()]
        $Data = @{}
    )

    # if we have data, then setup the data param
    if ($null -ne $Data -and $Data.Count -gt 0) {
        $Content = "param(`$data)`nreturn `"$($Content -replace '"', '``"')`""
    }
    else {
        $Content = "return `"$($Content -replace '"', '``"')`""
    }

    # invoke the content as a script to generate the dynamic content
    return (Invoke-PodeScriptBlock -ScriptBlock ([scriptblock]::Create($Content)) -Arguments $Data -Return)
}

function Get-PodeViewEngineType
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    # work out the engine to use when parsing the file
    $type = $PodeContext.Server.ViewEngine.Type

    $ext = Get-PodeFileExtension -Path $Path -TrimPeriod
    if (![string]::IsNullOrWhiteSpace($ext) -and ($ext -ine $PodeContext.Server.ViewEngine.Extension)) {
        $type = $ext
    }

    return $type
}

function Get-PodeFileContentUsingViewEngine
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [hashtable]
        $Data
    )

    # work out the engine to use when parsing the file
    $engine = Get-PodeViewEngineType -Path $Path

    # setup the content
    $content = [string]::Empty

    # run the relevant engine logic
    switch ($engine.ToLowerInvariant())
    {
        'html' {
            $content = Get-Content -Path $Path -Raw -Encoding utf8
        }

        'md' {
            $content = Get-Content -Path $Path -Raw -Encoding utf8
        }

        'pode' {
            $content = Get-Content -Path $Path -Raw -Encoding utf8
            $content = ConvertFrom-PodeFile -Content $content -Data $Data
        }

        default {
            if ($null -ne $PodeContext.Server.ViewEngine.Script) {
                if ($null -eq $Data -or $Data.Count -eq 0) {
                    $content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.Script -Arguments $Path -Return)
                }
                else {
                    $content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.Script -Arguments @($Path, $Data) -Return -Splat)
                }
            }
        }
    }

    return $content
}

function Get-PodeFileContent
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    return (Get-Content -Path $Path -Raw -Encoding utf8)
}

function Get-PodeType
{
    param (
        [Parameter()]
        $Value
    )

    if ($null -eq $Value) {
        return $null
    }

    $type = $Value.GetType()
    return @{
        'Name' = $type.Name.ToLowerInvariant();
        'BaseName' = $type.BaseType.Name.ToLowerInvariant();
    }
}

function Get-PodePSVersionTable
{
    return $PSVersionTable
}

function Test-IsAdminUser
{
    # check the current platform, if it's unix then return true
    if (Test-IsUnix) {
        return $true
    }

    try {
        $principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
        if ($null -eq $principal) {
            return $false
        }

        return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }
    catch [exception] {
        Write-Host 'Error checking user administrator priviledges' -ForegroundColor Red
        Write-Host $_.Exception.Message -ForegroundColor Red
        return $false
    }
}

function New-PodeSelfSignedCertificate
{
    # generate the cert -- has to call "powershell.exe" for ps-core on windows
    $cert = (PowerShell.exe -NoProfile -Command {
        $expire = (Get-Date).AddYears(1)

        $c = New-SelfSignedCertificate -DnsName 'localhost' -CertStoreLocation 'Cert:\LocalMachine\My' -NotAfter $expire `
                -KeyAlgorithm RSA -HashAlgorithm SHA256 -KeyLength 4096 -Subject 'CN=localhost';

        if ($null -eq $c.Thumbprint) {
            return $c
        }

        return $c.Thumbprint
    })

    if ($LASTEXITCODE -ne 0 -or !$?) {
        throw "Failed to generate self-signed certificte:`n$($cert)"
    }

    return $cert
}

function Get-PodeCertificate
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Certificate
    )

    # ensure the certificate exists, and get its thumbprint
    $cert = (Get-ChildItem 'Cert:\LocalMachine\My' | Where-Object { $_.Subject -imatch [regex]::Escape($Certificate) })
    if (Test-IsEmpty $cert) {
        throw "Failed to find the $($Certificate) certificate at LocalMachine\My"
    }

    $cert = @($cert)[0].Thumbprint
    return $cert
}

function Set-PodeCertificate
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Address,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Port,

        [Parameter()]
        [string]
        $Certificate,

        [Parameter()]
        [string]
        $Thumbprint,

        [switch]
        $SelfSigned
    )

    $addrport = "$($Address):$($Port)"

    # only bind if windows at the moment
    if (!(Test-IsWindows)) {
        Write-Host "Certificates are currently only supported on Windows" -ForegroundColor Yellow
        return
    }

    # check if this addr/port is already bound
    $sslPortInUse = (netsh http show sslcert) | Where-Object {
        ($_ -ilike "*IP:port*" -or $_ -ilike "*Hostname:port*") -and $_ -ilike "*$($addrport)"
    }

    if ($sslPortInUse) {
        Write-Host "$($addrport) already has a certificate bound" -ForegroundColor Green
        return
    }

    # ensure a cert, or thumbprint, has been supplied
    if (!$SelfSigned -and (Test-IsEmpty $Certificate) -and (Test-IsEmpty $Thumbprint)) {
        throw "A certificate name, or thumbprint, is required for ssl connections. For the name, either 'self' or '*.example.com' can be supplied to the 'listen' function"
    }

    # use the cert specified from the thumbprint
    if (!(Test-IsEmpty $Thumbprint)) {
        $cert = $Thumbprint
    }

    # otherwise, generate/find a certificate
    else
    {
        # generate a self-signed cert
        if ($SelfSigned) {
            Write-Host "Generating self-signed certificate for $($addrport)..." -NoNewline -ForegroundColor Cyan
            $cert = (New-PodeSelfSignedCertificate)
        }

        # ensure a given cert exists for binding
        else {
            Write-Host "Binding $($Certificate) to $($addrport)..." -NoNewline -ForegroundColor Cyan
            $cert = (Get-PodeCertificate -Certificate $Certificate)
        }
    }

    # bind the cert to the ip:port or hostname:port
    if (Test-PodeIPAddress -IP $Address -IPOnly) {
        $result = netsh http add sslcert ipport=$addrport certhash=$cert appid=`{e3ea217c-fc3d-406b-95d5-4304ab06c6af`}
        if ($LASTEXITCODE -ne 0 -or !$?) {
            throw "Failed to attach certificate against ipport:`n$($result)"
        }
    }
    else {
        $result = netsh http add sslcert hostnameport=$addrport certhash=$cert certstorename=MY appid=`{e3ea217c-fc3d-406b-95d5-4304ab06c6af`}
        if ($LASTEXITCODE -ne 0 -or !$?) {
            throw "Failed to attach certificate against hostnameport:`n$($result)"
        }
    }

    Write-Host " Done" -ForegroundColor Green
}

function Get-PodeHostIPRegex
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Both', 'Hostname', 'IP')]
        [string]
        $Type
    )

    $ip_rgx = '\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d+|\*|all'
    $host_rgx = '([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+'

    switch ($Type.ToLowerInvariant())
    {
        'both' {
            return "(?<host>($($ip_rgx)|$($host_rgx)))"
        }

        'hostname' {
            return "(?<host>($($host_rgx)))"
        }

        'ip' {
            return "(?<host>($($ip_rgx)))"
        }
    }
}

function Get-PortRegex
{
    return '(?<port>\d+)'
}

function Get-PodeEndpointInfo
{
    param (
        [Parameter()]
        [string]
        $Endpoint,

        [switch]
        $AnyPortOnZero
    )

    if ([string]::IsNullOrWhiteSpace($Endpoint)) {
        return $null
    }

    $hostRgx = Get-PodeHostIPRegex -Type Both
    $portRgx = Get-PortRegex
    $cmbdRgx = "$($hostRgx)\:$($portRgx)"

    # validate that we have a valid ip/host:port address
    if (!(($Endpoint -imatch "^$($cmbdRgx)$") -or ($Endpoint -imatch "^$($hostRgx)[\:]{0,1}") -or ($Endpoint -imatch "[\:]{0,1}$($portRgx)$"))) {
        throw "Failed to parse '$($Endpoint)' as a valid IP/Host:Port address"
    }

    # grab the ip address/hostname
    $_host = $Matches['host']
    if ([string]::IsNullOrWhiteSpace($_host)) {
        $_host = '*'
    }

    # ensure we have a valid ip address/hostname
    if (!(Test-PodeIPAddress -IP $_host)) {
        throw "The IP address supplied is invalid: $($_host)"
    }

    # grab the port
    $_port = $Matches['port']
    if ([string]::IsNullOrWhiteSpace($_port)) {
        $_port = 0
    }

    # ensure the port is valid
    if ($_port -lt 0) {
        throw "The port cannot be negative: $($_port)"
    }

    # return the info
    return @{
        'Host' = $_host;
        'Port' = (Resolve-PodeValue -Check ($AnyPortOnZero -and $_port -eq 0) -TrueValue '*' -FalseValue $_port);
    }
}

function Test-PodeIPAddress
{
    param (
        [Parameter()]
        [string]
        $IP,

        [switch]
        $IPOnly
    )

    if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -ieq '*') -or ($IP -ieq 'all')) {
        return $true
    }

    if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") {
        return (!$IPOnly)
    }

    try {
        [System.Net.IPAddress]::Parse($IP) | Out-Null
        return $true
    }
    catch [exception] {
        return $false
    }
}

function Test-PodeHostname
{
    param (
        [Parameter()]
        [string]
        $Hostname
    )

    return ($Hostname -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$")
}

function ConvertTo-PodeIPAddress
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Endpoint
    )

    return [System.Net.IPAddress]::Parse(([System.Net.IPEndPoint]$Endpoint).Address.ToString())
}

function Get-PodeIPAddressesForHostname
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Hostname,

        [Parameter(Mandatory=$true)]
        [ValidateSet('All', 'IPv4', 'IPv6')]
        [string]
        $Type
    )

    if (!(Test-PodeHostname -Hostname $Hostname)) {
        return $Hostname
    }

    # get the ip addresses for the hostname
    $ips = @([System.Net.Dns]::GetHostAddresses($Hostname))

    # return ips based on type
    switch ($Type.ToLowerInvariant())
    {
        'ipv4' {
            $ips = @(foreach ($ip in $ips) {
                if ($ip.AddressFamily -ieq 'InterNetwork') {
                    $ip
                }
            })
        }

        'ipv6' {
            $ips = @(foreach ($ip in $ips) {
                if ($ip.AddressFamily -ieq 'InterNetworkV6') {
                    $ip
                }
            })
        }
    }

    return (@($ips)).IPAddressToString
}

function Test-PodeIPAddressLocal
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $IP
    )

    return (@('127.0.0.1', '::1', '[::1]', 'localhost') -icontains $IP)
}

function Test-PodeIPAddressAny
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $IP
    )

    return (@('0.0.0.0', '*', 'all', '::', '[::]') -icontains $IP)
}

function Test-PodeIPAddressLocalOrAny
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $IP
    )

    return ((Test-PodeIPAddressLocal -IP $IP) -or (Test-PodeIPAddressAny -IP $IP))
}

function Get-PodeIPAddress
{
    param (
        [Parameter()]
        [string]
        $IP
    )

    if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -ieq '*') -or ($IP -ieq 'all')) {
        return [System.Net.IPAddress]::Any
    }

    if (($IP -ieq '::') -or ($IP -ieq '[::]')) {
        return [System.Net.IPAddress]::IPv6Any
    }

    if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") {
        return $IP
    }

    return [System.Net.IPAddress]::Parse($IP)
}

function Test-PodeIPAddressInRange
{
    param (
        [Parameter(Mandatory=$true)]
        $IP,

        [Parameter(Mandatory=$true)]
        $LowerIP,

        [Parameter(Mandatory=$true)]
        $UpperIP
    )

    if ($IP.Family -ine $LowerIP.Family) {
        return $false
    }

    $valid = $true

    foreach ($i in 0..3) {
        if (($IP.Bytes[$i] -lt $LowerIP.Bytes[$i]) -or ($IP.Bytes[$i] -gt $UpperIP.Bytes[$i])) {
            $valid = $false
            break
        }
    }

    return $valid
}

function Test-PodeIPAddressIsSubnetMask
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $IP
    )

    return (($IP -split '/').Length -gt 1)
}

function Get-PodeSubnetRange
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SubnetMask
    )

    # split for ip and number of 1 bits
    $split = $SubnetMask -split '/'
    if ($split.Length -le 1) {
        return $null
    }

    $ip_parts = $split[0] -isplit '\.'
    $bits = [int]$split[1]

    # generate the netmask
    $network = @("", "", "", "")
    $count = 0

    foreach ($i in 0..3) {
        foreach ($b in 1..8) {
            $count++

            if ($count -le $bits) {
                $network[$i] += "1"
            }
            else {
                $network[$i] += "0"
            }
        }
    }

    # covert netmask to bytes
    foreach ($i in 0..3) {
        $network[$i] = [Convert]::ToByte($network[$i], 2)
    }

    # calculate the bottom range
    $bottom = @(foreach ($i in 0..3) {
        [byte]([byte]$network[$i] -band [byte]$ip_parts[$i])
    })

    # calculate the range
    $range = @(foreach ($i in 0..3) {
        256 + (-bnot [byte]$network[$i])
    })

    # calculate the top range
    $top = @(foreach ($i in 0..3) {
        [byte]([byte]$ip_parts[$i] + [byte]$range[$i])
    })

    return @{
        'Lower' = ($bottom -join '.');
        'Upper' = ($top -join '.');
        'Range' = ($range -join '.');
        'Netmask' = ($network -join '.');
        'IP' = ($ip_parts -join '.');
    }
}

function Add-PodeRunspace
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Main', 'Signals', 'Schedules', 'Gui')]
        [string]
        $Type,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        $Parameters,

        [switch]
        $Forget
    )

    try
    {
        $ps = [powershell]::Create()
        $ps.RunspacePool = $PodeContext.RunspacePools[$Type]
        $ps.AddScript({ Add-PodePSDrives }) | Out-Null
        $ps.AddScript($ScriptBlock) | Out-Null

        if (!(Test-IsEmpty $Parameters)) {
            $Parameters.Keys | ForEach-Object {
                $ps.AddParameter($_, $Parameters[$_]) | Out-Null
            }
        }

        if ($Forget) {
            $ps.BeginInvoke() | Out-Null
        }
        else {
            $PodeContext.Runspaces += @{
                Pool = $Type;
                Runspace = $ps;
                Status = $ps.BeginInvoke();
                Stopped = $false;
            }
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

function Close-PodeRunspaces
{
    param (
        [switch]
        $ClosePool
    )

    if ($PodeContext.Server.IsServerless) {
        return
    }

    try {
        if (!(Test-IsEmpty $PodeContext.Runspaces)) {
            # sleep for 1s before doing this, to let listeners dispose
            Start-Sleep -Seconds 1

            # now dispose runspaces
            $PodeContext.Runspaces | Where-Object { !$_.Stopped } | ForEach-Object {
                Close-PodeDisposable -Disposable $_.Runspace
                $_.Stopped = $true
            }

            $PodeContext.Runspaces = @()
        }

        # dispose the runspace pools
        if ($ClosePool -and $null -ne $PodeContext.RunspacePools) {
            $PodeContext.RunspacePools.Values | Where-Object { $null -ne $_ -and !$_.IsDisposed } | ForEach-Object {
                Close-PodeDisposable -Disposable $_ -Close
            }
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

function Get-PodeConsoleKey
{
    if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) {
        return $null
    }

    return [Console]::ReadKey($true)
}

function Test-PodeTerminationPressed
{
    param (
        [Parameter()]
        $Key = $null
    )

    if ($PodeContext.DisableTermination) {
        return $false
    }

    if ($null -eq $Key) {
        $Key = Get-PodeConsoleKey
    }

    return (($null -ne $Key) -and ($Key.Key -ieq 'c') -and
        (($Key.Modifiers -band [ConsoleModifiers]::Control) -or ((Test-IsUnix) -and ($Key.Modifiers -band [ConsoleModifiers]::Shift))))
}

function Test-PodeRestartPressed
{
    param (
        [Parameter()]
        $Key = $null
    )

    if ($null -eq $Key) {
        $Key = Get-PodeConsoleKey
    }

    return (($null -ne $Key) -and ($Key.Key -ieq 'r') -and
        (($Key.Modifiers -band [ConsoleModifiers]::Control) -or ((Test-IsUnix) -and ($Key.Modifiers -band [ConsoleModifiers]::Shift))))
}

function Start-PodeTerminationListener
{
    Add-PodeRunspace -Type 'Main' {
        # default variables
        $options = "AllowCtrlC,IncludeKeyUp,NoEcho"
        $ctrlState = "LeftCtrlPressed"
        $char = 'c'
        $cancel = $false

        # are we on ps-core?
        $onCore = ($PSVersionTable.PSEdition -ieq 'core')

        while ($true) {
            if ($Console.UI.RawUI.KeyAvailable) {
                $key = $Console.UI.RawUI.ReadKey($options)

                if ([char]$key.VirtualKeyCode -ieq $char) {
                    if ($onCore) {
                        $cancel = ($key.Character -ine $char)
                    }
                    else {
                        $cancel = (($key.ControlKeyState -band $ctrlState) -ieq $ctrlState)
                    }
                }

                if ($cancel) {
                    Write-Host 'Terminating...' -NoNewline
                    $PodeContext.Tokens.Cancellation.Cancel()
                    break
                }
            }

            Start-Sleep -Milliseconds 10
        }
    }
}

function Close-PodeServer
{
    param (
        [switch]
        $ShowDoneMessage
    )

    # stpo all current runspaces
    Close-PodeRunspaces -ClosePool

    # stop the file monitor if it's running
    Stop-PodeFileMonitor

    try {
        # remove all the cancellation tokens
        Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation
        Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart
    }
    catch {
        $_ | Write-PodeErrorLog
    }

    # remove all of the pode temp drives
    Remove-PodePSDrives

    if ($ShowDoneMessage -and ![string]::IsNullOrWhiteSpace($PodeContext.Server.Type) -and !$PodeContext.Server.IsServerless) {
        Write-Host " Done" -ForegroundColor Green
    }
}

function New-PodePSDrive
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Name
    )

    # if the path is a share, do nothing
    if ($Path.StartsWith('\\')) {
        return $Path
    }

    # if no name is passed, used a randomly generated one
    if ([string]::IsNullOrWhiteSpace($Name)) {
        $Name = "PodeDir$(New-PodeGuid)"
    }

    # if the path supplied doesn't exist, error
    if (!(Test-Path $Path)) {
        throw "Path does not exist: $($Path)"
    }

    # create the temp drive
    $drive = (New-PSDrive -Name $Name -PSProvider FileSystem -Root $Path -Scope Global)

    # store internally, and return the drive's name
    if (!$PodeContext.Server.Drives.ContainsKey($drive.Name)) {
        $PodeContext.Server.Drives[$drive.Name] = $Path
    }

    return "$($drive.Name):"
}

function Add-PodePSDrives
{
    $PodeContext.Server.Drives.Keys | ForEach-Object {
        New-PodePSDrive -Path $PodeContext.Server.Drives[$_] -Name $_ | Out-Null
    }
}

function Add-PodePSInbuiltDrives
{
    # create drive for views, if path exists
    $path = (Join-PodeServerRoot 'views')
    if (Test-Path $path) {
        $PodeContext.Server.InbuiltDrives['views'] = (New-PodePSDrive -Path $path)
    }

    # create drive for public content, if path exists
    $path = (Join-PodeServerRoot 'public')
    if (Test-Path $path) {
        $PodeContext.Server.InbuiltDrives['public'] = (New-PodePSDrive -Path $path)
    }

    # create drive for errors, if path exists
    $path = (Join-PodeServerRoot 'errors')
    if (Test-Path $path) {
        $PodeContext.Server.InbuiltDrives['errors'] = (New-PodePSDrive -Path $path)
    }
}

function Remove-PodePSDrives
{
    Get-PSDrive PodeDir* | Remove-PSDrive | Out-Null
}

function Join-PodeServerRoot
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Folder,

        [Parameter()]
        [string]
        $FilePath,

        [Parameter()]
        [string]
        $Root
    )

    # use the root path of the server
    if ([string]::IsNullOrWhiteSpace($Root)) {
        $Root = $PodeContext.Server.Root
    }

    # join the folder/file to the root path
    return (Join-PodePaths @($Root, $Folder, $FilePath))
}

function Remove-PodeEmptyItemsFromArray
{
    param (
        [Parameter()]
        $Array
    )

    if ($null -eq $Array) {
        return @()
    }

    return @(@($Array -ne ([string]::Empty)) -ne $null)
}

function Join-PodePaths
{
    param (
        [Parameter()]
        [string[]]
        $Paths
    )

    # remove any empty/null paths
    $Paths = @(Remove-PodeEmptyItemsFromArray $Paths)

    # if there are no paths, return blank
    if ($null -eq $Paths -or $Paths.Length -eq 0) {
        return ([string]::Empty)
    }

    # return the first path if singular
    if ($Paths.Length -eq 1) {
        return $Paths[0]
    }

    # join the first two paths
    $_path = Join-Path $Paths[0] $Paths[1]

    # if there are any more, add them on
    if ($Paths.Length -gt 2) {
        foreach ($p in $Paths[2..($Paths.Length - 1)]) {
            $_path = Join-Path $_path $p
        }
    }

    return $_path
}

function Get-PodeFileExtension
{
    param (
        [Parameter()]
        [string]
        $Path,

        [switch]
        $TrimPeriod
    )

    $ext = [System.IO.Path]::GetExtension($Path)
    if ($TrimPeriod) {
        $ext = $ext.Trim('.')
    }

    return $ext
}

function Get-PodeFileName
{
    param (
        [Parameter()]
        [string]
        $Path,

        [switch]
        $WithoutExtension
    )

    if ($WithoutExtension) {
        return [System.IO.Path]::GetFileNameWithoutExtension($Path)
    }

    return [System.IO.Path]::GetFileName($Path)
}

function Test-PodeValidNetworkFailure
{
    param (
        [Parameter()]
        $Exception
    )

    $msgs = @(
        '*network name is no longer available*',
        '*nonexistent network connection*',
        '*broken pipe*'
    )

    $match = @(foreach ($msg in $msgs) {
        if ($Exception.Message -ilike $msg) {
            $msg
        }
    })[0]

    return ($null -ne $match)
}

function Get-PodeEncodingFromContentType
{
    param(
        [Parameter()]
        [string]
        $ContentType
    )

    if ([string]::IsNullOrWhiteSpace($ContentType)) {
        return [System.Text.Encoding]::UTF8
    }

    $parts = $ContentType -isplit ';'

    foreach ($part in $parts) {
        if ($part.Trim().StartsWith('charset')) {
            return [System.Text.Encoding]::GetEncoding(($part -isplit '=')[1].Trim())
        }
    }

    return [System.Text.Encoding]::UTF8
}

function ConvertFrom-PodeRequestContent
{
    param (
        [Parameter()]
        $Request,

        [Parameter()]
        [string]
        $ContentType
    )

    # get the requests content type and boundary
    $MetaData = Get-PodeContentTypeAndBoundary -ContentType $ContentType
    $Encoding = $Request.ContentEncoding

    # result object for data/files
    $Result = @{
        Data = @{}
        Files = @{}
    }

    # if there is no content-type then do nothing
    if ([string]::IsNullOrWhiteSpace($MetaData.ContentType)) {
        return $Result
    }

    # if the content-type is not multipart/form-data, get the string data
    if ($MetaData.ContentType -ine 'multipart/form-data') {
        # get the content based on server type
        switch ($PodeContext.Server.Type.ToLowerInvariant()) {
            'awslambda' {
                $Content = $Request.body
            }

            'azurefunctions' {
                $Content = $Request.RawBody
            }

            'pode' {
                $Content = $Request.Body.Value
            }

            default {
                $Content = Read-PodeStreamToEnd -Stream $Request.InputStream -Encoding $Encoding
            }
        }

        # if there is no content then do nothing
        if ([string]::IsNullOrWhiteSpace($Content)) {
            return $Result
        }

        # check if there is a defined custom body parser
        if ($PodeContext.Server.BodyParsers.ContainsKey($MetaData.ContentType)) {
            $Result.Data = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.BodyParsers[$MetaData.ContentType] -Arguments $Content -Return)
            return $Result
        }
    }

    # run action for the content type
    switch ($MetaData.ContentType) {
        { $_ -ilike '*/json' } {
            if (Test-IsPSCore) {
                $Result.Data = ($Content | ConvertFrom-Json -AsHashtable)
            }
            else {
                $Result.Data = ($Content | ConvertFrom-Json)
            }
        }

        { $_ -ilike '*/xml' } {
            $Result.Data = [xml]($Content)
        }

        { $_ -ilike '*/csv' } {
            $Result.Data = ($Content | ConvertFrom-Csv)
        }

        { $_ -ilike '*/x-www-form-urlencoded' } {
            $Result.Data = (ConvertFrom-PodeNameValueToHashTable -Collection ([System.Web.HttpUtility]::ParseQueryString($Content)))
        }

        { $_ -ieq 'multipart/form-data' } {
            # convert the stream to bytes
            if ($PodeContext.Server.Type -ieq 'pode') {
                $Content = $Request.Body.Bytes
            }
            else {
                $Content = ConvertFrom-PodeStreamToBytes -Stream $Request.InputStream
            }

            $Lines = Get-PodeByteLinesFromByteArray -Bytes $Content -Encoding $Encoding -IncludeNewLine

            # get the indexes for boundary lines (start and end)
            $boundaryIndexes = @()
            for ($i = 0; $i -lt $Lines.Length; $i++) {
                if ((Test-PodeByteArrayIsBoundary -Bytes $Lines[$i] -Boundary $MetaData.Boundary.Start -Encoding $Encoding) -or
                    (Test-PodeByteArrayIsBoundary -Bytes $Lines[$i] -Boundary $MetaData.Boundary.End -Encoding $Encoding)) {
                    $boundaryIndexes += $i
                }
            }

            # loop through the boundary indexes (exclude last, as it's the end boundary)
            for ($i = 0; $i -lt ($boundaryIndexes.Length - 1); $i++)
            {
                $bIndex = $boundaryIndexes[$i]

                # the next line contains the key-value field names (content-disposition)
                $fields = @{}
                $disp = ConvertFrom-PodeBytesToString -Bytes $Lines[$bIndex+1] -Encoding $Encoding -RemoveNewLine

                foreach ($line in @($disp -isplit ';')) {
                    $atoms = @($line -isplit '=')
                    if ($atoms.Length -eq 2) {
                        $fields[$atoms[0].Trim()] = $atoms[1].Trim(' "')
                    }
                }

                # use the next line to work out field values
                if (!$fields.ContainsKey('filename')) {
                    $value = ConvertFrom-PodeBytesToString -Bytes $Lines[$bIndex+3] -Encoding $Encoding -RemoveNewLine
                    $Result.Data.Add($fields.name, $value)
                }

                # if we have a file, work out file and content type
                if ($fields.ContainsKey('filename')) {
                    $Result.Data.Add($fields.name, $fields.filename)

                    if (![string]::IsNullOrWhiteSpace($fields.filename)) {
                        $type = ConvertFrom-PodeBytesToString -Bytes $Lines[$bIndex+2] -Encoding $Encoding -RemoveNewLine

                        $Result.Files.Add($fields.filename, @{
                            'ContentType' = (@($type -isplit ':')[1].Trim());
                            'Bytes' = $null;
                        })

                        $bytes = @()
                        foreach ($b in ($Lines[($bIndex+4)..($boundaryIndexes[$i+1]-1)])) {
                            $bytes += $b
                        }

                        $Result.Files[$fields.filename].Bytes = (Remove-PodeNewLineBytesFromArray $bytes $Encoding)
                    }
                }
            }
        }

        default {
            $Result.Data = $Content
        }
    }

    return $Result
}

function Get-PodeContentTypeAndBoundary
{
    param (
        [Parameter()]
        [string]
        $ContentType
    )

    $obj = @{
        ContentType = [string]::Empty;
        Boundary = @{
            Start = [string]::Empty;
            End = [string]::Empty;
        }
    }

    if ([string]::IsNullOrWhiteSpace($ContentType)) {
        return $obj
    }

    $split = @($ContentType -isplit ';')
    $obj.ContentType = $split[0].Trim()

    if ($split.Length -gt 1) {
        $obj.Boundary.Start = "--$(($split[1] -isplit '=')[1].Trim())"
        $obj.Boundary.End = "$($obj.Boundary.Start)--"
    }

    return $obj
}

function ConvertFrom-PodeNameValueToHashTable
{
    param (
        [Parameter()]
        $Collection
    )

    if ($null -eq $Collection) {
        return $null
    }

    $ht = @{}
    foreach ($key in $Collection.Keys) {
        $ht[$key] = $Collection[$key]
    }

    return $ht
}

function Get-PodeCount
{
    param (
        [Parameter()]
        $Object
    )

    if ($null -eq $Object) {
        return 0
    }

    if ($Object -is [string]){
        return $Object.Length
    }

    return $Object.Count
}

function Test-PodePathAccess
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path
    )

    try {
        Get-Item $Path | Out-Null
    }
    catch [System.UnauthorizedAccessException] {
        return $false
    }

    return $true
}

function Test-PodePath
{
    param (
        [Parameter()]
        $Path,

        [switch]
        $NoStatus,

        [switch]
        $FailOnDirectory
    )

    # if the file doesnt exist then fail on 404
    if ([string]::IsNullOrWhiteSpace($Path) -or !(Test-Path $Path)) {
        if (!$NoStatus) {
            Set-PodeResponseStatus -Code 404
        }

        return $false
    }

    # if the file isn't accessible then fail 401
    if (!(Test-PodePathAccess $Path)) {
        if (!$NoStatus) {
            Set-PodeResponseStatus -Code 401
        }

        return $false
    }

    # if we're failing on a directory then fail on 404
    if ($FailOnDirectory -and (Test-PodePathIsDirectory $Path)) {
        if (!$NoStatus) {
            Set-PodeResponseStatus -Code 404
        }

        return $false
    }

    return $true
}

function Test-PodePathIsFile
{
    param (
        [Parameter()]
        [string]
        $Path,

        [switch]
        $FailOnWildcard
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $false
    }

    if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) {
        return $false
    }

    return (![string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}

function Test-PodePathIsWildcard
{
    param (
        [Parameter()]
        [string]
        $Path
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $false
    }

    return $Path.Contains('*')
}

function Test-PodePathIsDirectory
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path,

        [switch]
        $FailOnWildcard
    )

    if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) {
        return $false
    }

    return ([string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path)))
}

function Convert-PodePathSeparators
{
    param (
        [Parameter()]
        $Paths
    )

    return @($Paths | ForEach-Object {
        if (![string]::IsNullOrWhiteSpace($_)) {
            $_ -ireplace '[\\/]', [System.IO.Path]::DirectorySeparatorChar
        }
    })
}

function Convert-PodePathPatternToRegex
{
    param (
        [Parameter()]
        [string]
        $Path,

        [switch]
        $NotSlashes,

        [switch]
        $NotStrict
    )    

    if (!$NotSlashes) {
        if ($Path -match '[\\/]\*$') {
            $Path = $Path -replace '[\\/]\*$', '/{0,1}*'
        }

        $Path = $Path -ireplace '[\\/]', '[\\/]'
    }

    $Path = $Path -ireplace '\.', '\.'

    $Path = $Path -ireplace '\*', '.*?'

    if ($NotStrict) {
        return $Path
    }

    return "^$($Path)$"
}

function Convert-PodePathPatternsToRegex
{
    param (
        [Parameter()]
        [string[]]
        $Paths,

        [switch]
        $NotSlashes,

        [switch]
        $NotStrict
    )

    # remove any empty entries
    $Paths = @($Paths | Where-Object {
        !(Test-IsEmpty $_)
    })

    # if no paths, return null
    if (Test-IsEmpty $Paths) {
        return $null
    }

    # replace certain chars
    $Paths = @($Paths | ForEach-Object {
        if (!(Test-IsEmpty $_)) {
            Convert-PodePathPatternToRegex -Path $_ -NotStrict -NotSlashes:$NotSlashes
        }
    })

    # join them all together
    $joined = "($($Paths -join '|'))"

    if ($NotStrict) {
        return "$($joined)"
    }

    return "^$($joined)$"
}

function ConvertTo-PodeSslProtocols
{
    param(
        [Parameter()]
        [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')]
        [string[]]
        $Protocols
    )

    $protos = 0
    foreach ($protocol in $Protocols) {
        $protos = [int]($protos -bor [System.Security.Authentication.SslProtocols]::$protocol)
    }

    return [System.Security.Authentication.SslProtocols]($protos)
}

function Get-PodeModulePath
{
    # if there's 1 module imported already, use that
    $importedModule = @(Get-Module -Name Pode)
    if (($importedModule | Measure-Object).Count -eq 1) {
        return (@($importedModule)[0]).Path
    }

    # if there's none or more, attempt to get the module used for 'engine'
    try {
        $usedModule = (Get-Command -Name 'Set-PodeViewEngine').Module
        if (($usedModule | Measure-Object).Count -eq 1) {
            return $usedModule.Path
        }
    }
    catch { }

    # if there were multiple to begin with, use the newest version
    if (($importedModule | Measure-Object).Count -gt 1) {
        return (@($importedModule | Sort-Object -Property Version)[-1]).Path
    }

    # otherwise there were none, use the latest installed
    return (@(Get-Module -ListAvailable -Name Pode | Sort-Object -Property Version)[-1]).Path
}

function Get-PodeModuleRootPath
{
    return (Split-Path -Parent -Path $PodeContext.Server.PodeModulePath)
}

function Get-PodeUrl
{
    return "$($WebEvent.Protocol)://$($WebEvent.Endpoint)$($WebEvent.Path)"
}

function Find-PodeErrorPage
{
    param (
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $ContentType
    )

    # if a defined content type is supplied, attempt to find an error page for that first
    if (![string]::IsNullOrWhiteSpace($ContentType)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $ContentType
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $ContentType }
        }
    }

    # if a defined route error page content type is supplied, attempt to find an error page for that
    if (![string]::IsNullOrWhiteSpace($WebEvent.ErrorType)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ErrorType
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $WebEvent.ErrorType }
        }
    }

    # if route patterns have been defined, see if an error content type matches and attempt that
    if (!(Test-IsEmpty $PodeContext.Server.Web.ErrorPages.Routes)) {
        # find type by pattern
        $matched = @(foreach ($key in $PodeContext.Server.Web.ErrorPages.Routes.Keys) {
            if ($WebEvent.Path -imatch $key) {
                $key
            }
        })[0]

        # if we have a match, see if a page exists
        if (!(Test-IsEmpty $matched)) {
            $type = $PodeContext.Server.Web.ErrorPages.Routes[$matched]
            $path = Get-PodeErrorPage -Code $Code -ContentType $type
            if (![string]::IsNullOrWhiteSpace($path)) {
                return @{ 'Path' = $path; 'ContentType' = $type }
            }
        }
    }

    # if we're using strict typing, attempt that, if we have a content type
    if ($PodeContext.Server.Web.ErrorPages.StrictContentTyping -and ![string]::IsNullOrWhiteSpace($WebEvent.ContentType)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ContentType
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $WebEvent.ContentType }
        }
    }

    # if we have a default defined, attempt that
    if (!(Test-IsEmpty $PodeContext.Server.Web.ErrorPages.Default)) {
        $path = Get-PodeErrorPage -Code $Code -ContentType $PodeContext.Server.Web.ErrorPages.Default
        if (![string]::IsNullOrWhiteSpace($path)) {
            return @{ 'Path' = $path; 'ContentType' = $PodeContext.Server.Web.ErrorPages.Default }
        }
    }

    # if there's still no error page, use default HTML logic
    $type = Get-PodeContentType -Extension 'html'
    $path = (Get-PodeErrorPage -Code $Code -ContentType $type)

    if (![string]::IsNullOrWhiteSpace($path)) {
        return @{ 'Path' = $path; 'ContentType' = $type }
    }

    return $null
}

function Get-PodeErrorPage
{
    param (
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $ContentType
    )

    # parse the passed content type
    $ContentType = (Get-PodeContentTypeAndBoundary -ContentType $ContentType).ContentType

    # object for the page path
    $path = $null

    # attempt to find a custom error page
    $path = Find-PodeCustomErrorPage -Code $Code -ContentType $ContentType

    # if there's no custom page found, attempt to find an inbuilt page
    if ([string]::IsNullOrWhiteSpace($path)) {
        $podeRoot = Join-Path (Get-PodeModuleRootPath) 'Misc'
        $path = Find-PodeFileForContentType -Path $podeRoot -Name 'default-error-page' -ContentType $ContentType -Engine 'pode'
    }

    # if there's no path found, or it's inaccessible, return null
    if (!(Test-PodePath $path -NoStatus)) {
        return $null
    }

    return $path
}

function Find-PodeCustomErrorPage
{
    param (
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $ContentType
    )

    # get the custom errors path
    $customErrPath = $PodeContext.Server.InbuiltDrives['errors']

    # if there's no custom error path, return
    if ([string]::IsNullOrWhiteSpace($customErrPath)) {
        return $null
    }

    # retrieve a status code page
    $path = (Find-PodeFileForContentType -Path $customErrPath -Name "$($Code)" -ContentType $ContentType)
    if (![string]::IsNullOrWhiteSpace($path)) {
        return $path
    }

    # retrieve default page
    $path = (Find-PodeFileForContentType -Path $customErrPath -Name 'default' -ContentType $ContentType)
    if (![string]::IsNullOrWhiteSpace($path)) {
        return $path
    }

    # no file was found
    return $null
}

function Find-PodeFileForContentType
{
    param (
        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [string]
        $Engine = $null
    )

    # get all files at the path that start with the name
    $files = @(Get-ChildItem -Path (Join-Path $Path "$($Name).*"))

    # if there are no files, return
    if ($null -eq $files -or $files.Length -eq 0) {
        return $null
    }

    # filter the files by the view engine extension (but only if the current engine is dynamic - non-html)
    if ([string]::IsNullOrWhiteSpace($Engine) -and $PodeContext.Server.ViewEngine.IsDynamic) {
        $Engine = $PodeContext.Server.ViewEngine.Extension
    }

    $Engine = (Protect-PodeValue -Value $Engine -Default 'pode')
    if ($Engine -ine 'pode') {
        $Engine = "($($Engine)|pode)"
    }

    $engineFiles = @(foreach ($file in $files) {
        if ($file.Name -imatch "\.$($Engine)$") {
            $file
        }
    })

    $files = @(foreach ($file in $files) {
        if ($file.Name -inotmatch "\.$($Engine)$") {
            $file
        }
    })

    # only attempt static files if we still have files after any engine filtering
    if ($null -ne $files -and $files.Length -gt 0)
    {
        # get files of the format '<name>.<type>'
        $file = @(foreach ($f in $files) {
            if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)$") {
                if (($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext']))) {
                    $f.FullName
                }
            }
        })[0]

        if (![string]::IsNullOrWhiteSpace($file)) {
            return $file
        }
    }

    # only attempt these formats if we have a files for the view engine
    if ($null -ne $engineFiles -and $engineFiles.Length -gt 0)
    {
        # get files of the format '<name>.<type>.<engine>'
        $file = @(foreach ($f in $engineFiles) {
            if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)\.$($engine)$") {
                if ($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext'])) {
                    $f.FullName
                }
            }
        })[0]

        if (![string]::IsNullOrWhiteSpace($file)) {
            return $file
        }

        # get files of the format '<name>.<engine>'
        $file = @(foreach ($f in $engineFiles) {
            if ($f.Name -imatch "^$($Name)\.$($engine)$") {
                $f.FullName
            }
        })[0]

        if (![string]::IsNullOrWhiteSpace($file)) {
            return $file
        }
    }

    # no file was found
    return $null
}

function Test-PodePathIsRelative
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    if (@('.', '..') -contains $Path) {
        return $true
    }

    return ($Path -match '^\.{1,2}[\\/]')
}

function Get-PodeRelativePath
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $RootPath,

        [switch]
        $JoinRoot,

        [switch]
        $Resolve,

        [switch]
        $TestPath
    )

    # if the path is relative, join to root if flagged
    if ($JoinRoot -and (Test-PodePathIsRelative -Path $Path)) {
        if ([string]::IsNullOrWhiteSpace($RootPath)) {
            $RootPath = $PodeContext.Server.Root
        }

        $Path = Join-Path $RootPath $Path
    }

    # if flagged, resolve the path
    if ($Resolve) {
        $_rawPath = $Path
        $Path = (Resolve-Path -Path $Path -ErrorAction Ignore).Path
    }

    # if flagged, test the path and throw error if it doesn't exist
    if ($TestPath -and !(Test-PodePath $Path -NoStatus)) {
        throw "The path does not exist: $(Protect-PodeValue -Value $Path -Default $_rawPath)"
    }

    return $Path
}

function Get-PodeWildcardFiles
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Wildcard = '*.*'
    )

    # if the OriginalPath is a directory, add wildcard
    if (Test-PodePathIsDirectory -Path $Path) {
        $Path = (Join-Path $Path $Wildcard)
    }

    # if path has a *, assume wildcard
    if (Test-PodePathIsWildcard -Path $Path) {
        $Path = Get-PodeRelativePath -Path $Path -JoinRoot
        return @((Get-ChildItem $Path -Recurse -Force).FullName)
    }

    return $null
}

function Test-PodeIsServerless
{
    param (
        [Parameter()]
        [string]
        $FunctionName,

        [switch]
        $ThrowError
    )

    if ($PodeContext.Server.IsServerless -and $ThrowError) {
        throw "The $($FunctionName) function is not supported in a serverless context"
    }

    if (!$ThrowError) {
        return $PodeContext.Server.IsServerless
    }
}

function Get-PodeEndpointUrl
{
    param (
        [Parameter()]
        $Endpoint
    )

    # get the endpoint on which we're currently listening - use first if there are many
    if ($null -eq $Endpoint) {
        $Endpoint = $PodeContext.Server.Endpoints[0]
    }

    # work out the protocol
    $protocol = (Resolve-PodeValue -Check $Endpoint.Ssl -TrueValue 'https' -FalseValue 'http')

    # grab the port number
    $port = $Endpoint.Port
    if ($port -eq 0) {
        $port = (Resolve-PodeValue -Check $Endpoint.Ssl -TrueValue 8443 -FalseValue 8080)
    }

    return "$($protocol)://$($Endpoint.HostName):$($port)"
}

function Set-PodeServerHeader
{
    param (
        [Parameter()]
        [string]
        $Type,

        [switch]
        $AllowEmptyType
    )

    $name = 'Pode'
    if (![string]::IsNullOrWhiteSpace($Type) -or $AllowEmptyType) {
        $name += " - $($Type)"
    }

    Set-PodeHeader -Name 'Server' -Value $name
}

function Get-PodeHandler
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Service', 'Smtp', 'Tcp')]
        [string]
        $Type,

        [Parameter()]
        [string]
        $Name
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return $PodeContext.Server.Handlers[$Type]
    }

    return $PodeContext.Server.Handlers[$Type][$Name]
}
src\Private\Logging.ps1
function Get-PodeLoggingTerminalMethod
{
    return {
        param($item, $options)
        $item = ($item | Protect-PodeLogItem)
        $item.ToString() | Out-Default
    }
}

function Get-PodeLoggingFileMethod
{
    return {
        param($item, $options)

        # mask values
        $item = ($item | Protect-PodeLogItem)

        # variables
        $date = [DateTime]::Now.ToString('yyyy-MM-dd')

        # do we need to reset the fileId?
        if ($options.Date -ine $date) {
            $options.Date = $date
            $options.FileId = 0
        }

        # get the fileId
        if ($options.FileId -eq 0) {
            $path = (Join-Path $options.Path "$($options.Name)_$($date)_*.log")
            $options.FileId = (@(Get-ChildItem -Path $path)).Length
            if ($options.FileId -eq 0) {
                $options.FileId = 1
            }
        }

        $id = "$($options.FileId)".PadLeft(3, '0')
        if ($options.MaxSize -gt 0) {
            $path = (Join-Path $options.Path "$($options.Name)_$($date)_$($id).log")
            if ((Get-Item -Path $path -Force).Length -ge $options.MaxSize) {
                $options.FileId++
                $id = "$($options.FileId)".PadLeft(3, '0')
            }
        }

        # get the file to write to
        $path = (Join-Path $options.Path "$($options.Name)_$($date)_$($id).log")

        # write the item to the file
        $item.ToString() | Out-File -FilePath $path -Encoding utf8 -Append -Force

        # if set, remove log files beyond days set (ensure this is only run once a day)
        if (($options.MaxDays -gt 0) -and ($options.NextClearDown -lt [DateTime]::Now.Date)) {
            $date = [DateTime]::Now.Date.AddDays(-$options.MaxDays)

            Get-ChildItem -Path $options.Path -Filter '*.log' -Force |
                Where-Object { $_.CreationTime -lt $date } |
                Remove-Item $_ -Force | Out-Null

            $options.NextClearDown = [DateTime]::Now.Date.AddDays(1)
        }
    }
}

function Get-PodeLoggingInbuiltType
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Errors', 'Requests')]
        [string]
        $Type
    )

    switch ($Type.ToLowerInvariant())
    {
        'requests' {
            $script = {
                param($item, $options)

                # just return the item if Raw is set
                if ($options.Raw) {
                    return $item
                }

                function sg($value) {
                    if ([string]::IsNullOrWhiteSpace($value)) {
                        return '-'
                    }

                    return $value
                }

                # build the url with http method
                $url = "$(sg $item.Request.Method) $(sg $item.Request.Resource) $(sg $item.Request.Protocol)"

                # build and return the request row
                return "$(sg $item.Host) $(sg $item.RfcUserIdentity) $(sg $item.User) [$(sg $item.Date)] `"$($url)`" $(sg $item.Response.StatusCode) $(sg $item.Response.Size) `"$(sg $item.Request.Referrer)`" `"$(sg $item.Request.Agent)`""
            }
        }

        'errors' {
            $script = {
                param($item, $options)

                # do nothing if the error level isn't present
                if (@($options.Levels) -inotcontains $item.Level) {
                    return
                }

                # just return the item if Raw is set
                if ($options.Raw) {
                    return $item
                }

                # build the exception details
                $row = @(
                    "Date: $($item.Date.ToString('yyyy-MM-dd HH:mm:ss'))",
                    "Level: $($item.Level)",
                    "ThreadId: $($item.ThreadId)",
                    "Server: $($item.Server)",
                    "Category: $($item.Category)",
                    "Message: $($item.Message)",
                    "StackTrace: $($item.StackTrace)"
                )

                # join the details and return
                return "$($row -join "`n")`n"
            }
        }
    }

    return $script
}

function Get-PodeRequestLoggingName
{
    return '__pode_log_requests__'
}

function Get-PodeErrorLoggingName
{
    return '__pode_log_errors__'
}

function Get-PodeLogger
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    return $PodeContext.Server.Logging.Types[$Name]
}

function Test-PodeLoggerEnabled
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    return ($PodeContext.Server.Logging.Enabled -and $PodeContext.Server.Logging.Types.ContainsKey($Name))
}

function Write-PodeRequestLog
{
    param (
        [Parameter(Mandatory=$true)]
        $Request,

        [Parameter(Mandatory=$true)]
        $Response,

        [Parameter()]
        [string]
        $Path
    )

    # do nothing if logging is disabled, or request logging isn't setup
    $name = Get-PodeRequestLoggingName
    if (!(Test-PodeLoggerEnabled -Name $name)) {
        return
    }

    # build a request object
    $item = @{
        Host = $Request.RemoteEndPoint.Address.IPAddressToString
        RfcUserIdentity = '-'
        User = '-'
        Date = [DateTime]::Now.ToString('dd/MMM/yyyy:HH:mm:ss zzz')
        Request = @{
            Method = $Request.HttpMethod.ToUpperInvariant()
            Resource = $Path
            Protocol = "HTTP/$($Request.ProtocolVersion)"
            Referrer = $Request.UrlReferrer
            Agent = $Request.UserAgent
        }
        Response = @{
            StatusCode = $Response.StatusCode
            StatusDescription = $Response.StatusDescription
            Size = '-'
        }
    }

    if ($Response.ContentLength64 -gt 0) {
        $item.Response.Size = $Response.ContentLength64
    }

    # add the item to be processed
    $PodeContext.LogsToProcess.Add(@{
        Name = $name
        Item = $item
    }) | Out-Null
}

function Add-PodeRequestLogEndware
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $WebEvent
    )

    # do nothing if logging is disabled, or request logging isn't setup
    $name = Get-PodeRequestLoggingName
    if (!(Test-PodeLoggerEnabled -Name $name)) {
        return
    }

    # add the request logging endware
    $WebEvent.OnEnd += @{
        Logic = {
            param($e)
            Write-PodeRequestLog -Request $e.Request -Response $e.Response -Path $e.Path
        }
    }
}

function Start-PodeLoggingRunspace
{
    # skip if there are no loggers configured, or logging is disabled
    if (($PodeContext.Server.Logging.Types.Count -eq 0) -or (!$PodeContext.Server.Logging.Enabled)) {
        return
    }

    $script = {
        while ($true)
        {
            # if there are no logs to process, just sleep few a few seconds
            if ($PodeContext.LogsToProcess.Count -eq 0) {
                Start-Sleep -Seconds 5
                continue
            }

            # safetly pop off the first log from the array
            $log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock {
                $log = $PodeContext.LogsToProcess[0]
                $PodeContext.LogsToProcess.RemoveAt(0) | Out-Null
                return $log
            })

            if ($null -ne $log) {
                # run the log item through the appropriate method, then through the storage script
                $logger = Get-PodeLogger -Name $log.Name

                $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments (@($log.Item) + @($logger.Arguments)) -Return -Splat)
                Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments (@($result) + @($logger.Method.Arguments)) -Splat
            }

            # small sleep to lower cpu usage
            Start-Sleep -Milliseconds 100
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $script
}
src\Private\Mappers.ps1
function Get-PodeContentType
{
    param (
        [Parameter()]
        [string]
        $Extension,

        [switch]
        $DefaultIsNull
    )

    if ([string]::IsNullOrWhiteSpace($Extension)) {
        $Extension = [string]::Empty
    }

    if (!$Extension.StartsWith('.')) {
        $Extension = ".$($Extension)"
    }

    # Sourced from https://github.com/samuelneff/MimeTypeMap
    switch ($Extension.ToLowerInvariant())
    {
        '.323' { return 'text/h323' }
        '.3g2' { return 'video/3gpp2' }
        '.3gp' { return 'video/3gpp' }
        '.3gp2' { return 'video/3gpp2' }
        '.3gpp' { return 'video/3gpp' }
        '.7z' { return 'application/x-7z-compressed' }
        '.aa' { return 'audio/audible' }
        '.aac' { return 'audio/aac' }
        '.aaf' { return 'application/octet-stream' }
        '.aax' { return 'audio/vnd.audible.aax' }
        '.ac3' { return 'audio/ac3' }
        '.aca' { return 'application/octet-stream' }
        '.accda' { return 'application/msaccess.addin' }
        '.accdb' { return 'application/msaccess' }
        '.accdc' { return 'application/msaccess.cab' }
        '.accde' { return 'application/msaccess' }
        '.accdr' { return 'application/msaccess.runtime' }
        '.accdt' { return 'application/msaccess' }
        '.accdw' { return 'application/msaccess.webapplication' }
        '.accft' { return 'application/msaccess.ftemplate' }
        '.acx' { return 'application/internet-property-stream' }
        '.addin' { return 'text/xml' }
        '.ade' { return 'application/msaccess' }
        '.adobebridge' { return 'application/x-bridge-url' }
        '.adp' { return 'application/msaccess' }
        '.adt' { return 'audio/vnd.dlna.adts' }
        '.adts' { return 'audio/aac' }
        '.afm' { return 'application/octet-stream' }
        '.ai' { return 'application/postscript' }
        '.aif' { return 'audio/aiff' }
        '.aifc' { return 'audio/aiff' }
        '.aiff' { return 'audio/aiff' }
        '.air' { return 'application/vnd.adobe.air-application-installer-package+zip' }
        '.amc' { return 'application/mpeg' }
        '.anx' { return 'application/annodex' }
        '.apk' { return 'application/vnd.android.package-archive' }
        '.application' { return 'application/x-ms-application' }
        '.art' { return 'image/x-jg' }
        '.asa' { return 'application/xml' }
        '.asax' { return 'application/xml' }
        '.ascx' { return 'application/xml' }
        '.asd' { return 'application/octet-stream' }
        '.asf' { return 'video/x-ms-asf' }
        '.ashx' { return 'application/xml' }
        '.asi' { return 'application/octet-stream' }
        '.asm' { return 'text/plain' }
        '.asmx' { return 'application/xml' }
        '.aspx' { return 'application/xml' }
        '.asr' { return 'video/x-ms-asf' }
        '.asx' { return 'video/x-ms-asf' }
        '.atom' { return 'application/atom+xml' }
        '.au' { return 'audio/basic' }
        '.avi' { return 'video/x-msvideo' }
        '.axa' { return 'audio/annodex' }
        '.axs' { return 'application/olescript' }
        '.axv' { return 'video/annodex' }
        '.bas' { return 'text/plain' }
        '.bcpio' { return 'application/x-bcpio' }
        '.bin' { return 'application/octet-stream' }
        '.bmp' { return 'image/bmp' }
        '.c' { return 'text/plain' }
        '.cab' { return 'application/octet-stream' }
        '.caf' { return 'audio/x-caf' }
        '.calx' { return 'application/vnd.ms-office.calx' }
        '.cat' { return 'application/vnd.ms-pki.seccat' }
        '.cc' { return 'text/plain' }
        '.cd' { return 'text/plain' }
        '.cdda' { return 'audio/aiff' }
        '.cdf' { return 'application/x-cdf' }
        '.cer' { return 'application/x-x509-ca-cert' }
        '.cfg' { return 'text/plain' }
        '.chm' { return 'application/octet-stream' }
        '.class' { return 'application/x-java-applet' }
        '.clp' { return 'application/x-msclip' }
        '.cmd' { return 'text/plain' }
        '.cmx' { return 'image/x-cmx' }
        '.cnf' { return 'text/plain' }
        '.cod' { return 'image/cis-cod' }
        '.config' { return 'application/xml' }
        '.contact' { return 'text/x-ms-contact' }
        '.coverage' { return 'application/xml' }
        '.cpio' { return 'application/x-cpio' }
        '.cpp' { return 'text/plain' }
        '.crd' { return 'application/x-mscardfile' }
        '.crl' { return 'application/pkix-crl' }
        '.crt' { return 'application/x-x509-ca-cert' }
        '.cs' { return 'text/plain' }
        '.csdproj' { return 'text/plain' }
        '.csh' { return 'application/x-csh' }
        '.csproj' { return 'text/plain' }
        '.css' { return 'text/css' }
        '.csv' { return 'text/csv' }
        '.cur' { return 'application/octet-stream' }
        '.cxx' { return 'text/plain' }
        '.dat' { return 'application/octet-stream' }
        '.datasource' { return 'application/xml' }
        '.dbproj' { return 'text/plain' }
        '.dcr' { return 'application/x-director' }
        '.def' { return 'text/plain' }
        '.deploy' { return 'application/octet-stream' }
        '.der' { return 'application/x-x509-ca-cert' }
        '.dgml' { return 'application/xml' }
        '.dib' { return 'image/bmp' }
        '.dif' { return 'video/x-dv' }
        '.dir' { return 'application/x-director' }
        '.disco' { return 'text/xml' }
        '.divx' { return 'video/divx' }
        '.dll' { return 'application/x-msdownload' }
        '.dll.config' { return 'text/xml' }
        '.dlm' { return 'text/dlm' }
        '.doc' { return 'application/msword' }
        '.docm' { return 'application/vnd.ms-word.document.macroEnabled.12' }
        '.docx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }
        '.dot' { return 'application/msword' }
        '.dotm' { return 'application/vnd.ms-word.template.macroEnabled.12' }
        '.dotx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' }
        '.dsp' { return 'application/octet-stream' }
        '.dsw' { return 'text/plain' }
        '.dtd' { return 'text/xml' }
        '.dtsconfig' { return 'text/xml' }
        '.dv' { return 'video/x-dv' }
        '.dvi' { return 'application/x-dvi' }
        '.dwf' { return 'drawing/x-dwf' }
        '.dwg' { return 'application/acad' }
        '.dwp' { return 'application/octet-stream' }
        '.dxf' { return 'application/x-dxf' }
        '.dxr' { return 'application/x-director' }
        '.eml' { return 'message/rfc822' }
        '.emz' { return 'application/octet-stream' }
        '.eot' { return 'application/vnd.ms-fontobject' }
        '.eps' { return 'application/postscript' }
        '.etl' { return 'application/etl' }
        '.etx' { return 'text/x-setext' }
        '.evy' { return 'application/envoy' }
        '.exe' { return 'application/octet-stream' }
        '.exe.config' { return 'text/xml' }
        '.fdf' { return 'application/vnd.fdf' }
        '.fif' { return 'application/fractals' }
        '.filters' { return 'application/xml' }
        '.fla' { return 'application/octet-stream' }
        '.flac' { return 'audio/flac' }
        '.flr' { return 'x-world/x-vrml' }
        '.flv' { return 'video/x-flv' }
        '.fsscript' { return 'application/fsharp-script' }
        '.fsx' { return 'application/fsharp-script' }
        '.generictest' { return 'application/xml' }
        '.gif' { return 'image/gif' }
        '.gpx' { return 'application/gpx+xml' }
        '.group' { return 'text/x-ms-group' }
        '.gsm' { return 'audio/x-gsm' }
        '.gtar' { return 'application/x-gtar' }
        '.gz' { return 'application/x-gzip' }
        '.h' { return 'text/plain' }
        '.hdf' { return 'application/x-hdf' }
        '.hdml' { return 'text/x-hdml' }
        '.hhc' { return 'application/x-oleobject' }
        '.hhk' { return 'application/octet-stream' }
        '.hhp' { return 'application/octet-stream' }
        '.hlp' { return 'application/winhlp' }
        '.hpp' { return 'text/plain' }
        '.hqx' { return 'application/mac-binhex40' }
        '.hta' { return 'application/hta' }
        '.htc' { return 'text/x-component' }
        '.htm' { return 'text/html' }
        '.html' { return 'text/html' }
        '.htt' { return 'text/webviewhtml' }
        '.hxa' { return 'application/xml' }
        '.hxc' { return 'application/xml' }
        '.hxd' { return 'application/octet-stream' }
        '.hxe' { return 'application/xml' }
        '.hxf' { return 'application/xml' }
        '.hxh' { return 'application/octet-stream' }
        '.hxi' { return 'application/octet-stream' }
        '.hxk' { return 'application/xml' }
        '.hxq' { return 'application/octet-stream' }
        '.hxr' { return 'application/octet-stream' }
        '.hxs' { return 'application/octet-stream' }
        '.hxt' { return 'text/html' }
        '.hxv' { return 'application/xml' }
        '.hxw' { return 'application/octet-stream' }
        '.hxx' { return 'text/plain' }
        '.i' { return 'text/plain' }
        '.ico' { return 'image/x-icon' }
        '.ics' { return 'application/octet-stream' }
        '.idl' { return 'text/plain' }
        '.ief' { return 'image/ief' }
        '.iii' { return 'application/x-iphone' }
        '.inc' { return 'text/plain' }
        '.inf' { return 'application/octet-stream' }
        '.ini' { return 'text/plain' }
        '.inl' { return 'text/plain' }
        '.ins' { return 'application/x-internet-signup' }
        '.ipa' { return 'application/x-itunes-ipa' }
        '.ipg' { return 'application/x-itunes-ipg' }
        '.ipproj' { return 'text/plain' }
        '.ipsw' { return 'application/x-itunes-ipsw' }
        '.iqy' { return 'text/x-ms-iqy' }
        '.isp' { return 'application/x-internet-signup' }
        '.ite' { return 'application/x-itunes-ite' }
        '.itlp' { return 'application/x-itunes-itlp' }
        '.itms' { return 'application/x-itunes-itms' }
        '.itpc' { return 'application/x-itunes-itpc' }
        '.ivf' { return 'video/x-ivf' }
        '.jar' { return 'application/java-archive' }
        '.java' { return 'application/octet-stream' }
        '.jck' { return 'application/liquidmotion' }
        '.jcz' { return 'application/liquidmotion' }
        '.jfif' { return 'image/pjpeg' }
        '.jnlp' { return 'application/x-java-jnlp-file' }
        '.jpb' { return 'application/octet-stream' }
        '.jpe' { return 'image/jpeg' }
        '.jpeg' { return 'image/jpeg' }
        '.jpg' { return 'image/jpeg' }
        '.js' { return 'application/javascript' }
        '.json' { return 'application/json' }
        '.jsx' { return 'text/jscript' }
        '.jsxbin' { return 'text/plain' }
        '.latex' { return 'application/x-latex' }
        '.library-ms' { return 'application/windows-library+xml' }
        '.lit' { return 'application/x-ms-reader' }
        '.loadtest' { return 'application/xml' }
        '.lpk' { return 'application/octet-stream' }
        '.lsf' { return 'video/x-la-asf' }
        '.lst' { return 'text/plain' }
        '.lsx' { return 'video/x-la-asf' }
        '.lzh' { return 'application/octet-stream' }
        '.m13' { return 'application/x-msmediaview' }
        '.m14' { return 'application/x-msmediaview' }
        '.m1v' { return 'video/mpeg' }
        '.m2t' { return 'video/vnd.dlna.mpeg-tts' }
        '.m2ts' { return 'video/vnd.dlna.mpeg-tts' }
        '.m2v' { return 'video/mpeg' }
        '.m3u' { return 'audio/x-mpegurl' }
        '.m3u8' { return 'audio/x-mpegurl' }
        '.m4a' { return 'audio/m4a' }
        '.m4b' { return 'audio/m4b' }
        '.m4p' { return 'audio/m4p' }
        '.m4r' { return 'audio/x-m4r' }
        '.m4v' { return 'video/x-m4v' }
        '.mac' { return 'image/x-macpaint' }
        '.mak' { return 'text/plain' }
        '.man' { return 'application/x-troff-man' }
        '.manifest' { return 'application/x-ms-manifest' }
        '.map' { return 'text/plain' }
        '.markdown' { return 'text/markdown' }
        '.master' { return 'application/xml' }
        '.mbox' { return 'application/mbox' }
        '.md' { return 'text/markdown' }
        '.mda' { return 'application/msaccess' }
        '.mdb' { return 'application/x-msaccess' }
        '.mde' { return 'application/msaccess' }
        '.mdp' { return 'application/octet-stream' }
        '.me' { return 'application/x-troff-me' }
        '.mfp' { return 'application/x-shockwave-flash' }
        '.mht' { return 'message/rfc822' }
        '.mhtml' { return 'message/rfc822' }
        '.mid' { return 'audio/mid' }
        '.midi' { return 'audio/mid' }
        '.mix' { return 'application/octet-stream' }
        '.mk' { return 'text/plain' }
        '.mk3d' { return 'video/x-matroska-3d' }
        '.mka' { return 'audio/x-matroska' }
        '.mkv' { return 'video/x-matroska' }
        '.mmf' { return 'application/x-smaf' }
        '.mno' { return 'text/xml' }
        '.mny' { return 'application/x-msmoney' }
        '.mod' { return 'video/mpeg' }
        '.mov' { return 'video/quicktime' }
        '.movie' { return 'video/x-sgi-movie' }
        '.mp2' { return 'video/mpeg' }
        '.mp2v' { return 'video/mpeg' }
        '.mp3' { return 'audio/mpeg' }
        '.mp4' { return 'video/mp4' }
        '.mp4v' { return 'video/mp4' }
        '.mpa' { return 'video/mpeg' }
        '.mpe' { return 'video/mpeg' }
        '.mpeg' { return 'video/mpeg' }
        '.mpf' { return 'application/vnd.ms-mediapackage' }
        '.mpg' { return 'video/mpeg' }
        '.mpp' { return 'application/vnd.ms-project' }
        '.mpv2' { return 'video/mpeg' }
        '.mqv' { return 'video/quicktime' }
        '.ms' { return 'application/x-troff-ms' }
        '.msg' { return 'application/vnd.ms-outlook' }
        '.msi' { return 'application/octet-stream' }
        '.mso' { return 'application/octet-stream' }
        '.mts' { return 'video/vnd.dlna.mpeg-tts' }
        '.mtx' { return 'application/xml' }
        '.mvb' { return 'application/x-msmediaview' }
        '.mvc' { return 'application/x-miva-compiled' }
        '.mxp' { return 'application/x-mmxp' }
        '.nc' { return 'application/x-netcdf' }
        '.nsc' { return 'video/x-ms-asf' }
        '.nws' { return 'message/rfc822' }
        '.ocx' { return 'application/octet-stream' }
        '.oda' { return 'application/oda' }
        '.odb' { return 'application/vnd.oasis.opendocument.database' }
        '.odc' { return 'application/vnd.oasis.opendocument.chart' }
        '.odf' { return 'application/vnd.oasis.opendocument.formula' }
        '.odg' { return 'application/vnd.oasis.opendocument.graphics' }
        '.odh' { return 'text/plain' }
        '.odi' { return 'application/vnd.oasis.opendocument.image' }
        '.odl' { return 'text/plain' }
        '.odm' { return 'application/vnd.oasis.opendocument.text-master' }
        '.odp' { return 'application/vnd.oasis.opendocument.presentation' }
        '.ods' { return 'application/vnd.oasis.opendocument.spreadsheet' }
        '.odt' { return 'application/vnd.oasis.opendocument.text' }
        '.oga' { return 'audio/ogg' }
        '.ogg' { return 'audio/ogg' }
        '.ogv' { return 'video/ogg' }
        '.ogx' { return 'application/ogg' }
        '.one' { return 'application/onenote' }
        '.onea' { return 'application/onenote' }
        '.onepkg' { return 'application/onenote' }
        '.onetmp' { return 'application/onenote' }
        '.onetoc' { return 'application/onenote' }
        '.onetoc2' { return 'application/onenote' }
        '.opus' { return 'audio/ogg' }
        '.orderedtest' { return 'application/xml' }
        '.osdx' { return 'application/opensearchdescription+xml' }
        '.otf' { return 'application/font-sfnt' }
        '.otg' { return 'application/vnd.oasis.opendocument.graphics-template' }
        '.oth' { return 'application/vnd.oasis.opendocument.text-web' }
        '.otp' { return 'application/vnd.oasis.opendocument.presentation-template' }
        '.ots' { return 'application/vnd.oasis.opendocument.spreadsheet-template' }
        '.ott' { return 'application/vnd.oasis.opendocument.text-template' }
        '.oxt' { return 'application/vnd.openofficeorg.extension' }
        '.p10' { return 'application/pkcs10' }
        '.p12' { return 'application/x-pkcs12' }
        '.p7b' { return 'application/x-pkcs7-certificates' }
        '.p7c' { return 'application/pkcs7-mime' }
        '.p7m' { return 'application/pkcs7-mime' }
        '.p7r' { return 'application/x-pkcs7-certreqresp' }
        '.p7s' { return 'application/pkcs7-signature' }
        '.pbm' { return 'image/x-portable-bitmap' }
        '.pcast' { return 'application/x-podcast' }
        '.pct' { return 'image/pict' }
        '.pcx' { return 'application/octet-stream' }
        '.pcz' { return 'application/octet-stream' }
        '.pdf' { return 'application/pdf' }
        '.pfb' { return 'application/octet-stream' }
        '.pfm' { return 'application/octet-stream' }
        '.pfx' { return 'application/x-pkcs12' }
        '.pgm' { return 'image/x-portable-graymap' }
        '.pic' { return 'image/pict' }
        '.pict' { return 'image/pict' }
        '.pkgdef' { return 'text/plain' }
        '.pkgundef' { return 'text/plain' }
        '.pko' { return 'application/vnd.ms-pki.pko' }
        '.pls' { return 'audio/scpls' }
        '.pma' { return 'application/x-perfmon' }
        '.pmc' { return 'application/x-perfmon' }
        '.pml' { return 'application/x-perfmon' }
        '.pmr' { return 'application/x-perfmon' }
        '.pmw' { return 'application/x-perfmon' }
        '.png' { return 'image/png' }
        '.pnm' { return 'image/x-portable-anymap' }
        '.pnt' { return 'image/x-macpaint' }
        '.pntg' { return 'image/x-macpaint' }
        '.pnz' { return 'image/png' }
        '.pode' { return 'application/PowerShell' }
        '.pot' { return 'application/vnd.ms-powerpoint' }
        '.potm' { return 'application/vnd.ms-powerpoint.template.macroEnabled.12' }
        '.potx' { return 'application/vnd.openxmlformats-officedocument.presentationml.template' }
        '.ppa' { return 'application/vnd.ms-powerpoint' }
        '.ppam' { return 'application/vnd.ms-powerpoint.addin.macroEnabled.12' }
        '.ppm' { return 'image/x-portable-pixmap' }
        '.pps' { return 'application/vnd.ms-powerpoint' }
        '.ppsm' { return 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' }
        '.ppsx' { return 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' }
        '.ppt' { return 'application/vnd.ms-powerpoint' }
        '.pptm' { return 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' }
        '.pptx' { return 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }
        '.prf' { return 'application/pics-rules' }
        '.prm' { return 'application/octet-stream' }
        '.prx' { return 'application/octet-stream' }
        '.ps' { return 'application/postscript' }
        '.ps1' { return 'application/PowerShell' }
        '.psc1' { return 'application/PowerShell' }
        '.psd1' { return 'application/PowerShell' }
        '.psm1' { return 'application/PowerShell' }
        '.psd' { return 'application/octet-stream' }
        '.psess' { return 'application/xml' }
        '.psm' { return 'application/octet-stream' }
        '.psp' { return 'application/octet-stream' }
        '.pst' { return 'application/vnd.ms-outlook' }
        '.pub' { return 'application/x-mspublisher' }
        '.pwz' { return 'application/vnd.ms-powerpoint' }
        '.qht' { return 'text/x-html-insertion' }
        '.qhtm' { return 'text/x-html-insertion' }
        '.qt' { return 'video/quicktime' }
        '.qti' { return 'image/x-quicktime' }
        '.qtif' { return 'image/x-quicktime' }
        '.qtl' { return 'application/x-quicktimeplayer' }
        '.qxd' { return 'application/octet-stream' }
        '.ra' { return 'audio/x-pn-realaudio' }
        '.ram' { return 'audio/x-pn-realaudio' }
        '.rar' { return 'application/x-rar-compressed' }
        '.ras' { return 'image/x-cmu-raster' }
        '.rat' { return 'application/rat-file' }
        '.rc' { return 'text/plain' }
        '.rc2' { return 'text/plain' }
        '.rct' { return 'text/plain' }
        '.rdlc' { return 'application/xml' }
        '.reg' { return 'text/plain' }
        '.resx' { return 'application/xml' }
        '.rf' { return 'image/vnd.rn-realflash' }
        '.rgb' { return 'image/x-rgb' }
        '.rgs' { return 'text/plain' }
        '.rm' { return 'application/vnd.rn-realmedia' }
        '.rmi' { return 'audio/mid' }
        '.rmp' { return 'application/vnd.rn-rn_music_package' }
        '.roff' { return 'application/x-troff' }
        '.rpm' { return 'audio/x-pn-realaudio-plugin' }
        '.rqy' { return 'text/x-ms-rqy' }
        '.rtf' { return 'application/rtf' }
        '.rtx' { return 'text/richtext' }
        '.rvt' { return 'application/octet-stream' }
        '.ruleset' { return 'application/xml' }
        '.s' { return 'text/plain' }
        '.safariextz' { return 'application/x-safari-safariextz' }
        '.scd' { return 'application/x-msschedule' }
        '.scr' { return 'text/plain' }
        '.sct' { return 'text/scriptlet' }
        '.sd2' { return 'audio/x-sd2' }
        '.sdp' { return 'application/sdp' }
        '.sea' { return 'application/octet-stream' }
        '.searchconnector-ms' { return 'application/windows-search-connector+xml' }
        '.setpay' { return 'application/set-payment-initiation' }
        '.setreg' { return 'application/set-registration-initiation' }
        '.settings' { return 'application/xml' }
        '.sgimb' { return 'application/x-sgimb' }
        '.sgml' { return 'text/sgml' }
        '.sh' { return 'application/x-sh' }
        '.shar' { return 'application/x-shar' }
        '.shtml' { return 'text/html' }
        '.sit' { return 'application/x-stuffit' }
        '.sitemap' { return 'application/xml' }
        '.skin' { return 'application/xml' }
        '.skp' { return 'application/x-koan' }
        '.sldm' { return 'application/vnd.ms-powerpoint.slide.macroEnabled.12' }
        '.sldx' { return 'application/vnd.openxmlformats-officedocument.presentationml.slide' }
        '.slk' { return 'application/vnd.ms-excel' }
        '.sln' { return 'text/plain' }
        '.slupkg-ms' { return 'application/x-ms-license' }
        '.smd' { return 'audio/x-smd' }
        '.smi' { return 'application/octet-stream' }
        '.smx' { return 'audio/x-smd' }
        '.smz' { return 'audio/x-smd' }
        '.snd' { return 'audio/basic' }
        '.snippet' { return 'application/xml' }
        '.snp' { return 'application/octet-stream' }
        '.sol' { return 'text/plain' }
        '.sor' { return 'text/plain' }
        '.spc' { return 'application/x-pkcs7-certificates' }
        '.spl' { return 'application/futuresplash' }
        '.spx' { return 'audio/ogg' }
        '.src' { return 'application/x-wais-source' }
        '.srf' { return 'text/plain' }
        '.ssisdeploymentmanifest' { return 'text/xml' }
        '.ssm' { return 'application/streamingmedia' }
        '.sst' { return 'application/vnd.ms-pki.certstore' }
        '.stl' { return 'application/vnd.ms-pki.stl' }
        '.sv4cpio' { return 'application/x-sv4cpio' }
        '.sv4crc' { return 'application/x-sv4crc' }
        '.svc' { return 'application/xml' }
        '.svg' { return 'image/svg+xml' }
        '.swf' { return 'application/x-shockwave-flash' }
        '.step' { return 'application/step' }
        '.stp' { return 'application/step' }
        '.t' { return 'application/x-troff' }
        '.tar' { return 'application/x-tar' }
        '.tcl' { return 'application/x-tcl' }
        '.testrunconfig' { return 'application/xml' }
        '.testsettings' { return 'application/xml' }
        '.tex' { return 'application/x-tex' }
        '.texi' { return 'application/x-texinfo' }
        '.texinfo' { return 'application/x-texinfo' }
        '.tgz' { return 'application/x-compressed' }
        '.thmx' { return 'application/vnd.ms-officetheme' }
        '.thn' { return 'application/octet-stream' }
        '.tif' { return 'image/tiff' }
        '.tiff' { return 'image/tiff' }
        '.tlh' { return 'text/plain' }
        '.tli' { return 'text/plain' }
        '.toc' { return 'application/octet-stream' }
        '.tr' { return 'application/x-troff' }
        '.trm' { return 'application/x-msterminal' }
        '.trx' { return 'application/xml' }
        '.ts' { return 'video/vnd.dlna.mpeg-tts' }
        '.tsv' { return 'text/tab-separated-values' }
        '.ttf' { return 'application/font-sfnt' }
        '.tts' { return 'video/vnd.dlna.mpeg-tts' }
        '.txt' { return 'text/plain' }
        '.u32' { return 'application/octet-stream' }
        '.uls' { return 'text/iuls' }
        '.user' { return 'text/plain' }
        '.ustar' { return 'application/x-ustar' }
        '.vb' { return 'text/plain' }
        '.vbdproj' { return 'text/plain' }
        '.vbk' { return 'video/mpeg' }
        '.vbproj' { return 'text/plain' }
        '.vbs' { return 'text/vbscript' }
        '.vcf' { return 'text/x-vcard' }
        '.vcproj' { return 'application/xml' }
        '.vcs' { return 'text/plain' }
        '.vcxproj' { return 'application/xml' }
        '.vddproj' { return 'text/plain' }
        '.vdp' { return 'text/plain' }
        '.vdproj' { return 'text/plain' }
        '.vdx' { return 'application/vnd.ms-visio.viewer' }
        '.vml' { return 'text/xml' }
        '.vscontent' { return 'application/xml' }
        '.vsct' { return 'text/xml' }
        '.vsd' { return 'application/vnd.visio' }
        '.vsi' { return 'application/ms-vsi' }
        '.vsix' { return 'application/vsix' }
        '.vsixlangpack' { return 'text/xml' }
        '.vsixmanifest' { return 'text/xml' }
        '.vsmdi' { return 'application/xml' }
        '.vspscc' { return 'text/plain' }
        '.vss' { return 'application/vnd.visio' }
        '.vsscc' { return 'text/plain' }
        '.vssettings' { return 'text/xml' }
        '.vssscc' { return 'text/plain' }
        '.vst' { return 'application/vnd.visio' }
        '.vstemplate' { return 'text/xml' }
        '.vsto' { return 'application/x-ms-vsto' }
        '.vsw' { return 'application/vnd.visio' }
        '.vsx' { return 'application/vnd.visio' }
        '.vtx' { return 'application/vnd.visio' }
        '.wasm' { return 'application/wasm' }
        '.wav' { return 'audio/wav' }
        '.wave' { return 'audio/wav' }
        '.wax' { return 'audio/x-ms-wax' }
        '.wbk' { return 'application/msword' }
        '.wbmp' { return 'image/vnd.wap.wbmp' }
        '.wcm' { return 'application/vnd.ms-works' }
        '.wdb' { return 'application/vnd.ms-works' }
        '.wdp' { return 'image/vnd.ms-photo' }
        '.webarchive' { return 'application/x-safari-webarchive' }
        '.webm' { return 'video/webm' }
        '.webp' { return 'image/webp' }
        '.webtest' { return 'application/xml' }
        '.wiq' { return 'application/xml' }
        '.wiz' { return 'application/msword' }
        '.wks' { return 'application/vnd.ms-works' }
        '.wlmp' { return 'application/wlmoviemaker' }
        '.wlpginstall' { return 'application/x-wlpg-detect' }
        '.wlpginstall3' { return 'application/x-wlpg3-detect' }
        '.wm' { return 'video/x-ms-wm' }
        '.wma' { return 'audio/x-ms-wma' }
        '.wmd' { return 'application/x-ms-wmd' }
        '.wmf' { return 'application/x-msmetafile' }
        '.wml' { return 'text/vnd.wap.wml' }
        '.wmlc' { return 'application/vnd.wap.wmlc' }
        '.wmls' { return 'text/vnd.wap.wmlscript' }
        '.wmlsc' { return 'application/vnd.wap.wmlscriptc' }
        '.wmp' { return 'video/x-ms-wmp' }
        '.wmv' { return 'video/x-ms-wmv' }
        '.wmx' { return 'video/x-ms-wmx' }
        '.wmz' { return 'application/x-ms-wmz' }
        '.woff' { return 'application/font-woff' }
        '.woff2' { return 'application/font-woff2' }
        '.wpl' { return 'application/vnd.ms-wpl' }
        '.wps' { return 'application/vnd.ms-works' }
        '.wri' { return 'application/x-mswrite' }
        '.wrl' { return 'x-world/x-vrml' }
        '.wrz' { return 'x-world/x-vrml' }
        '.wsc' { return 'text/scriptlet' }
        '.wsdl' { return 'text/xml' }
        '.wvx' { return 'video/x-ms-wvx' }
        '.x' { return 'application/directx' }
        '.xaf' { return 'x-world/x-vrml' }
        '.xaml' { return 'application/xaml+xml' }
        '.xap' { return 'application/x-silverlight-app' }
        '.xbap' { return 'application/x-ms-xbap' }
        '.xbm' { return 'image/x-xbitmap' }
        '.xdr' { return 'text/plain' }
        '.xht' { return 'application/xhtml+xml' }
        '.xhtml' { return 'application/xhtml+xml' }
        '.xla' { return 'application/vnd.ms-excel' }
        '.xlam' { return 'application/vnd.ms-excel.addin.macroEnabled.12' }
        '.xlc' { return 'application/vnd.ms-excel' }
        '.xld' { return 'application/vnd.ms-excel' }
        '.xlk' { return 'application/vnd.ms-excel' }
        '.xll' { return 'application/vnd.ms-excel' }
        '.xlm' { return 'application/vnd.ms-excel' }
        '.xls' { return 'application/vnd.ms-excel' }
        '.xlsb' { return 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' }
        '.xlsm' { return 'application/vnd.ms-excel.sheet.macroEnabled.12' }
        '.xlsx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
        '.xlt' { return 'application/vnd.ms-excel' }
        '.xltm' { return 'application/vnd.ms-excel.template.macroEnabled.12' }
        '.xltx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' }
        '.xlw' { return 'application/vnd.ms-excel' }
        '.xml' { return 'text/xml' }
        '.xmp' { return 'application/octet-stream' }
        '.xmta' { return 'application/xml' }
        '.xof' { return 'x-world/x-vrml' }
        '.xoml' { return 'text/plain' }
        '.xpm' { return 'image/x-xpixmap' }
        '.xps' { return 'application/vnd.ms-xpsdocument' }
        '.xrm-ms' { return 'text/xml' }
        '.xsc' { return 'application/xml' }
        '.xsd' { return 'text/xml' }
        '.xsf' { return 'text/xml' }
        '.xsl' { return 'text/xml' }
        '.xslt' { return 'text/xml' }
        '.xsn' { return 'application/octet-stream' }
        '.xss' { return 'application/xml' }
        '.xspf' { return 'application/xspf+xml' }
        '.xtp' { return 'application/octet-stream' }
        '.xwd' { return 'image/x-xwindowdump' }
        '.yaml' { return 'application/x-yaml' }
        '.yml' { return 'application/x-yaml' }
        '.z' { return 'application/x-compress' }
        '.zip' { return 'application/zip' }
        default { return (Resolve-PodeValue -Check $DefaultIsNull -TrueValue $null -FalseValue 'text/plain') }
    }
}

function Get-PodeStatusDescription
{
    param (
        [Parameter()]
        [int]
        $StatusCode
    )

    switch ($StatusCode)
    {
        100 { return 'Continue' }
        101 { return 'Switching Protocols' }
        102 { return 'Processing' }
        103 { return 'Early Hints' }
        200 { return 'OK' }
        201 { return 'Created' }
        202 { return 'Accepted' }
        203 { return 'Non-Authoritative Information' }
        204 { return 'No Content' }
        205 { return 'Reset Content' }
        206 { return 'Partial Content' }
        207 { return 'Multi-Status' }
        208 { return 'Already Reported' }
        226 { return 'IM Used' }
        300 { return 'Multiple Choices' }
        301 { return 'Moved Permanently' }
        302 { return 'Found' }
        303 { return 'See Other' }
        304 { return 'Not Modified' }
        305 { return 'Use Proxy' }
        306 { return 'Switch Proxy' }
        307 { return 'Temporary Redirect' }
        308 { return 'Permanent Redirect' }
        400 { return 'Bad Request' }
        401 { return 'Unauthorized' }
        402 { return 'Payment Required' }
        403 { return 'Forbidden' }
        404 { return 'Not Found' }
        405 { return 'Method Not Allowed' }
        406 { return 'Not Acceptable' }
        407 { return 'Proxy Authentication Required' }
        408 { return 'Request Timeout' }
        409 { return 'Conflict' }
        410 { return 'Gone' }
        411 { return 'Length Required' }
        412 { return 'Precondition Failed' }
        413 { return 'Payload Too Large' }
        414 { return 'URI Too Long' }
        415 { return 'Unsupported Media Type' }
        416 { return 'Range Not Satisfiable' }
        417 { return 'Expectation Failed' }
        418 { return "I'm a Teapot" }
        419 { return 'Page Expired' }
        420 { return 'Enhance Your Calm' }
        421 { return 'Misdirected Request' }
        422 { return 'Unprocessable Entity' }
        423 { return 'Locked' }
        424 { return 'Failed Dependency' }
        426 { return 'Upgrade Required' }
        428 { return 'Precondition Required' }
        429 { return 'Too Many Requests' }
        431 { return 'Request Header Fields Too Large' }
        440 { return 'Login Time-out' }
        450 { return 'Blocked by Windows Parental Controls' }
        451 { return 'Unavailable For Legal Reasons' }
        500 { return 'Internal Server Error' }
        501 { return 'Not Implemented' }
        502 { return 'Bad Gateway' }
        503 { return 'Service Unavailable' }
        504 { return 'Gateway Timeout' }
        505 { return 'HTTP Version Not Supported' }
        506 { return 'Variant Also Negotiates' }
        507 { return 'Insufficient Storage' }
        508 { return 'Loop Detected' }
        510 { return 'Not Extended' }
        511 { return 'Network Authentication Required' }
        526 { return 'Invalid SSL Certificate' }
        default { return ([string]::Empty) }
    }
}
src\Private\Middleware.ps1
function Invoke-PodeMiddleware
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $WebEvent,

        [Parameter()]
        $Middleware,

        [Parameter()]
        [string]
        $Route
    )

    # if there's no middleware, do nothing
    if ($null -eq $Middleware -or $Middleware.Length -eq 0) {
        return $true
    }

    # filter the middleware down by route (retaining order)
    if (![string]::IsNullOrWhiteSpace($Route))
    {
        $Middleware = @(foreach ($mware in $Middleware) {
            if ([string]::IsNullOrWhiteSpace($mware.Route) -or ($mware.Route -ieq '/') -or ($mware.Route -ieq $Route) -or ($Route -imatch "^$($mware.Route)$")) {
                $mware
            }
        })
    }

    # continue or halt?
    $continue = $true

    # loop through each of the middleware, invoking the next if it returns true
    foreach ($midware in @($Middleware))
    {
        try {
            $continue = Invoke-PodeScriptBlock -ScriptBlock $midware.Logic -Arguments (@($WebEvent) + @($midware.Arguments)) -Return -Scoped -Splat
        }
        catch {
            Set-PodeResponseStatus -Code 500 -Exception $_
            $continue = $false
            $_ | Write-PodeErrorLog
        }

        if (!$continue) {
            break
        }
    }

    return $continue
}

function Get-PodeInbuiltMiddleware
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [scriptblock]
        $ScriptBlock
    )

    # check if middleware contains an override
    $override = ($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name })

    # if override there, remove it from middleware
    if ($override) {
        $PodeContext.Server.Middleware = @($PodeContext.Server.Middleware | Where-Object { $_.Name -ine $Name })
        $ScriptBlock = $override.Logic
    }

    # return the script
    return @{
        Name = $Name
        Logic = $ScriptBlock
    }
}

function Get-PodeAccessMiddleware
{
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_access__' -ScriptBlock {
        param($e)

        # ensure the request IP address is allowed
        if (!(Test-PodeIPAccess -IP $e.Request.RemoteEndPoint.Address)) {
            Set-PodeResponseStatus -Code 403
            return $false
        }

        # IP address is allowed
        return $true
    })
}

function Get-PodeLimitMiddleware
{
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_rate_limit__' -ScriptBlock {
        param($e)

        # ensure the request IP address has not hit a rate limit
        if (!(Test-PodeIPLimit -IP $e.Request.RemoteEndPoint.Address)) {
            Set-PodeResponseStatus -Code 429
            return $false
        }

        # IP address is allowed
        return $true
    })
}

function Get-PodePublicMiddleware
{
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_static_content__' -ScriptBlock {
        param($e)

        # get the static file path
        $info = Get-PodeStaticRoutePath -Route $e.Path -Protocol $e.Protocol -Endpoint $e.Endpoint
        if ([string]::IsNullOrWhiteSpace($info.Path)) {
            return $true
        }

        # check current state of caching
        $config = $PodeContext.Server.Web.Static.Cache
        $caching = $config.Enabled

        # if caching, check include/exclude
        if ($caching) {
            if (($null -ne $config.Exclude) -and ($e.Path -imatch $config.Exclude)) {
                $caching = $false
            }

            if (($null -ne $config.Include) -and ($e.Path -inotmatch $config.Include)) {
                $caching = $false
            }
        }

        # write, or attach, the file to the response
        if ($info.Download) {
            Set-PodeResponseAttachment -Path $e.Path
        }
        else {
            Write-PodeFileResponse -Path $info.Path -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$caching
        }

        # static content found, stop
        return $false
    })
}

function Get-PodeRouteValidateMiddleware
{
    return @{
        Name = '__pode_mw_route_validation__'
        Logic = {
            param($e)

            # ensure the path has a route
            $route = Get-PodeRoute -Method $e.Method -Route $e.Path -Protocol $e.Protocol -Endpoint $e.Endpoint -CheckWildMethod

            # if there's no route defined, it's a 404 - or a 405 if a route exists for any other method
            if ($null -eq $route) {
                # check if a route exists for another method
                $methods = @('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE')
                $routes = @(foreach ($method in $methods) {
                    $r = Get-PodeRoute -Method $method -Route $e.Path -Protocol $e.Protocol -Endpoint $e.Endpoint
                    if ($null -ne $r) {
                        $r
                    }
                })

                if (($null -ne $routes) -and ($routes.Length -gt 0)) {
                    Set-PodeResponseStatus -Code 405
                    return $false
                }

                # otheriwse, it's a 404
                Set-PodeResponseStatus -Code 404
                return $false
            }

            # set the route parameters
            $e.Parameters = $route.Parameters

            # override the content type from the route if it's not empty
            if (![string]::IsNullOrWhiteSpace($route.ContentType)) {
                $e.ContentType = $route.ContentType
            }

            # set the content type for any pages for the route if it's not empty
            $e.ErrorType = $route.ErrorType

            # route exists
            return $true
        }
    }
}

function Get-PodeBodyMiddleware
{
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_body_parsing__' -ScriptBlock {
        param($e)

        try {
            # attempt to parse that data
            $result = ConvertFrom-PodeRequestContent -Request $e.Request -ContentType $e.ContentType

            # set session data
            $e.Data = $result.Data
            $e.Files = $result.Files

            # payload parsed
            return $true
        }
        catch {
            Set-PodeResponseStatus -Code 400 -Exception $_
            return $false
        }
    })
}

function Get-PodeQueryMiddleware
{
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_query_parsing__' -ScriptBlock {
        param($e)

        try {
            # set the query string from the request
            $e.Query = (ConvertFrom-PodeNameValueToHashTable -Collection $e.Request.QueryString)
            return $true
        }
        catch {
            Set-PodeResponseStatus -Code 400 -Exception $_
            return $false
        }
    })
}

function Get-PodeCookieMiddleware
{
    return (Get-PodeInbuiltMiddleware -Name '__pode_mw_cookie_parsing__' -ScriptBlock {
        param($e)

        # if it's not serverless or pode, return
        if (!$PodeContext.Server.IsServerless -and ($PodeContext.Server.Type -ine 'pode')) {
            return $true
        }

        # if cookies already set, return
        if ($e.Cookies.Count -gt 0) {
            return $true
        }

        # if the request's header has no cookies, return
        $h_cookie = (Get-PodeHeader -Name 'Cookie')
        if ([string]::IsNullOrWhiteSpace($h_cookie)) {
            return $true
        }

        # parse the cookies from the header
        $cookies = @($h_cookie -split '; ')
        $e.Cookies = @{}

        foreach ($cookie in $cookies) {
            $atoms = $cookie.Split('=', 2)

            $value = [string]::Empty
            if ($atoms.Length -gt 1) {
                $value = ($atoms[1..($atoms.Length - 1)] -join ([string]::Empty))
            }

            $e.Cookies[$atoms[0]] = [System.Net.Cookie]::new($atoms[0], $value)
        }

        return $true
    })
}
src\Private\NameGenerator.ps1
function Get-PodeRandomName
{
    $adjs = @(
        "admiring",
        "agitated",
        "blissful",
        "dazzling",
        "ecstatic",
        "eloquent",
        "friendly",
        "gracious",
        "hardcore",
        "laughing",
        "peaceful",
        "pedantic",
        "reverent",
        "romantic",
        "trusting",
        "vigilant",
        "vigorous",
        "wizardly",
        "youthful"
    )

    $names = @(
        "almeida",
        "babbage",
        "bardeen",
        "shannon",
        "davinci",
        "feynman",
        "galileo",
        "goodall",
        "hawking",
        "hermann",
        "hodgkin",
        "hypatia",
        "jackson",
        "johnson",
        "kapitsa",
        "keldysh",
        "khorana",
        "lalande",
        "lamport",
        "leavitt",
        "lumiere",
        "mcnulty",
        "meitner",
        "mestorf",
        "murdock",
        "neumann",
        "noether",
        "pasteur",
        "perlman",
        "poitras",
        "ptolemy",
        "ritchie",
        "shirley",
        "swanson",
        "swirles",
        "vaughan",
        "volhard",
        "villani",
        "wescoff",
        "wozniak"
    )

    $adjsRand = (Get-Random -Minimum 0 -Maximum $adjs.Length)
    $namesRand = (Get-Random -Minimum 0 -Maximum $names.Length)

    return "$($adjs[$adjsRand])_$($names[$namesRand])"
}
src\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 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 {
            $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 | 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
        }

        # 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'
}
src\Private\Responses.ps1
function Show-PodeErrorPage
{
    param (
        [Parameter()]
        [int]
        $Code,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        $Exception,

        [Parameter()]
        [string]
        $ContentType
    )

    # error page info
    $errorPage = Find-PodeErrorPage -Code $Code -ContentType $ContentType

    # if no page found, return
    if (Test-IsEmpty $errorPage) {
        return
    }

    # if exception trace showing enabled then build the exception details object
    $ex = $null
    if (!(Test-IsEmpty $Exception) -and $PodeContext.Server.Web.ErrorPages.ShowExceptions) {
        $ex = @{
            'Message' = [System.Web.HttpUtility]::HtmlEncode($Exception.Exception.Message);
            'StackTrace' = [System.Web.HttpUtility]::HtmlEncode($Exception.ScriptStackTrace);
            'Line' = [System.Web.HttpUtility]::HtmlEncode($Exception.InvocationInfo.PositionMessage);
            'Category' = [System.Web.HttpUtility]::HtmlEncode($Exception.CategoryInfo.ToString());
        }
    }

    # setup the data object for dynamic pages
    $data = @{
        'Url' = (Get-PodeUrl);
        'Status' = @{
            'Code' = $Code;
            'Description' = $Description;
        };
        'Exception' = $ex;
        'ContentType' = $errorPage.ContentType;
    }

    # write the error page to the stream
    Write-PodeFileResponse -Path $errorPage.Path -Data $data -ContentType $errorPage.ContentType
}

function Close-PodeTcpConnection
{
    param (
        [Parameter()]
        $Client,

        [switch]
        $Quit
    )

    if ($null -eq $Client) {
        $Client = $TcpEvent.Client
    }

    if ($null -ne $Client) {
        if ($Quit -and $Client.Connected) {
            Write-PodeTcpClient -Message '221 Bye'
        }

        Close-PodeDisposable -Disposable $Client -Close
    }
}
src\Private\Routes.ps1
function Get-PodeRoute
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', '*')]
        [string]
        $Method,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Route,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint,

        [switch]
        $CheckWildMethod
    )

    # first, if supplied, check the wildcard method
    if ($CheckWildMethod -and $PodeContext.Server.Routes['*'].Count -ne 0) {
        $found = Get-PodeRoute -Method '*' -Route $Route -Protocol $Protocol -Endpoint $Endpoint
        if ($null -ne $found) {
            return $found
        }
    }

    # is this a static route?
    $isStatic = ($Method -ieq 'static')

    # first ensure we have the method
    $_method = $PodeContext.Server.Routes[$Method]
    if ($null -eq $_method) {
        return $null
    }

    # if we have a perfect match for the route, return it if the protocol is right
    $found = Get-PodeRouteByUrl -Routes $_method[$Route] -Protocol $Protocol -Endpoint $Endpoint
    if (!$isStatic -and $null -ne $found) {
        return @{
            Logic = $found.Logic
            Middleware = $found.Middleware
            Protocol = $found.Protocol
            Endpoint = $found.Endpoint
            ContentType = $found.ContentType
            ErrorType = $found.ErrorType
            Parameters = $null
            Arguments = $found.Arguments
        }
    }

    # otherwise, attempt to match on regex parameters
    else {
        $valid = @(foreach ($key in $_method.Keys) {
            if ($Route -imatch "^$($key)$") {
                $key
            }
        })[0]

        if ($null -eq $valid) {
            return $null
        }

        $found = Get-PodeRouteByUrl -Routes $_method[$valid] -Protocol $Protocol -Endpoint $Endpoint
        if ($null -eq $found) {
            return $null
        }

        $Route -imatch "$($valid)$" | Out-Null

        if ($isStatic) {
            return @{
                Path = $found.Path
                Defaults = $found.Defaults
                Protocol = $found.Protocol
                Endpoint = $found.Endpoint
                Download = $found.Download
                File = $Matches['file']
            }
        }
        else {
            return @{
                Logic = $found.Logic
                Middleware = $found.Middleware
                Protocol = $found.Protocol
                Endpoint = $found.Endpoint
                ContentType = $found.ContentType
                ErrorType = $found.ErrorType
                Parameters = $Matches
                Arguments = $found.Arguments
            }
        }
    }
}

function Get-PodeStaticRoutePath
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Route,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint
    )

    # attempt to get a static route for the path
    $found = Get-PodeRoute -Method 'static' -Route $Route -Protocol $Protocol -Endpoint $Endpoint
    $path = $null
    $download = $false

    # if we have a defined static route, use that
    if ($null -ne $found) {
        # is the found route set as download only?
        if ($found.Download) {
            $download = $true
            $path = (Join-Path $found.Path (Protect-PodeValue -Value $found.File -Default ([string]::Empty)))
        }

        # if there's no file, we need to check defaults
        elseif (!(Test-PodePathIsFile $found.File) -and (Get-PodeCount @($found.Defaults)) -gt 0)
        {
            $found.File = (Protect-PodeValue -Value $found.File -Default ([string]::Empty))

            if ((Get-PodeCount @($found.Defaults)) -eq 1) {
                $found.File = Join-PodePaths @($found.File, @($found.Defaults)[0])
            }
            else {
                foreach ($def in $found.Defaults) {
                    if (Test-PodePath (Join-Path $found.Path $def) -NoStatus) {
                        $found.File = Join-PodePaths @($found.File, $def)
                        break
                    }
                }
            }
        }

        $path = (Join-Path $found.Path $found.File)
    }

    # else, use the public static directory (but only if path is a file, and a public dir is present)
    elseif ((Test-PodePathIsFile $Route) -and ![string]::IsNullOrWhiteSpace($PodeContext.Server.InbuiltDrives['public'])) {
        $path = (Join-Path $PodeContext.Server.InbuiltDrives['public'] $Route)
    }

    # return the route details
    return @{
        Path = $path;
        Download = $download;
    }
}

function Get-PodeRouteByUrl
{
    param (
        [Parameter()]
        [object[]]
        $Routes,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint
    )

    # get the value routes
    $rs = @(foreach ($route in $Routes) {
        if (
            (($route.Protocol -ieq $Protocol) -or [string]::IsNullOrWhiteSpace($route.Protocol)) -and
            ([string]::IsNullOrWhiteSpace($route.Endpoint) -or ($Endpoint -ilike $route.Endpoint))
        ) {
            $route
        }
    })

    if ($null -eq $rs[0]) {
        return $null
    }

    return @($rs | Sort-Object -Property { $_.Protocol }, { $_.Endpoint } -Descending)[0]
}

function Update-PodeRoutePlaceholders
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    # replace placeholder parameters with regex
    $placeholder = '\:(?<tag>[\w]+)'
    if ($Path -imatch $placeholder) {
        $Path = [regex]::Escape($Path)
    }

    while ($Path -imatch $placeholder) {
        $Path = ($Path -ireplace $Matches[0], "(?<$($Matches['tag'])>[^\/]+?)")
    }

    return $Path
}

function Update-PodeRouteSlashes
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [switch]
        $Static
    )

    # ensure route starts with a '/'
    if (!$Path.StartsWith('/')) {
        $Path = "/$($Path)"
    }

    if ($Static) {
        # ensure the static route ends with '/{0,1}.*'
        $Path = $Path.TrimEnd('/*')
        $Path = "$($Path)[/]{0,1}(?<file>*)"
    }

    # replace * with .*
    $Path = ($Path -ireplace '\*', '.*')
    return $Path
}

function Split-PodeRouteQuery
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    return ($Path -isplit "\?")[0]
}

function ConvertTo-PodeRouteRegex
{
    param (
        [Parameter()]
        [string]
        $Path
    )

    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = Split-PodeRouteQuery -Path $Path
    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = Update-PodeRouteSlashes -Path $Path
    $Path = Update-PodeRoutePlaceholders -Path $Path

    return $Path
}

function Get-PodeStaticRouteDefaults
{
    if (!(Test-IsEmpty $PodeContext.Server.Web.Static.Defaults)) {
        return @($PodeContext.Server.Web.Static.Defaults)
    }

    return @(
        'index.html',
        'index.htm',
        'default.html',
        'default.htm'
    )
}

function Test-PodeRouteAndError
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Method,

        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint
    )

    $found = @($PodeContext.Server.Routes[$Method][$Path])

    if (($found | Where-Object { $_.Protocol -ieq $Protocol -and $_.Endpoint -ieq $Endpoint } | Measure-Object).Count -eq 0) {
        return
    }

    $_url = $Protocol
    if (![string]::IsNullOrEmpty($_url) -and ![string]::IsNullOrWhiteSpace($Endpoint)) {
        $_url = "$($_url)://$($Endpoint)"
    }
    elseif (![string]::IsNullOrWhiteSpace($Endpoint)) {
        $_url = $Endpoint
    }

    if ([string]::IsNullOrEmpty($_url)) {
        throw "[$($Method)] $($Path): Already defined"
    }
    else {
        throw "[$($Method)] $($Path): Already defined for $($_url)"
    }
}

function Get-PodeEndpointByName
{
    param (
        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $ThrowError
    )

    # if an EndpointName was supplied, find it and use it
    if ([string]::IsNullOrWhiteSpace($EndpointName)) {
        return $null
    }

    # ensure it exists
    $found = ($PodeContext.Server.Endpoints | Where-Object {
        $_.Name -ieq $EndpointName
    } | Select-Object -First 1)

    if ($null -eq $found) {
        if ($ThrowError) {
            throw "Endpoint with name '$($EndpointName)' does not exist"
        }

        return $null
    }

    return $found
}

function Convert-PodeFunctionVerbToHttpMethod
{
    param (
        [Parameter()]
        [string]
        $Verb
    )

    # if empty, just return default
    $DefaultMethod = 'POST'
    if ([string]::IsNullOrWhiteSpace($Verb)) {
        return $DefaultMethod
    }

    # GET method
    if (@('Find', 'Format', 'Get', 'Join', 'Search', 'Select', 'Split', 'Measure', 'Ping', 'Test', 'Trace') -icontains $Verb) {
        return 'GET'
    }

    # PUT method
    if (@('Set') -icontains $Verb) {
        return 'PUT'
    }

    # PATCH method
    if (@('Rename', 'Edit', 'Update') -icontains $Verb) {
        return 'PATCH'
    }

    # DELETE method
    if (@('Clear', 'Close', 'Exit', 'Hide', 'Remove', 'Undo', 'Dismount', 'Unpublish', 'Disable', 'Uninstall', 'Unregister') -icontains $Verb) {
        return 'DELETE'
    }

    # default method is POST
    return $DefaultMethod
}
src\Private\Schedules.ps1
function Get-PodeSchedule
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeContext.Schedules[$Name]
}

function Start-PodeScheduleRunspace
{
    if ((Get-PodeCount $PodeContext.Schedules) -eq 0) {
        return
    }

    $script = {
        function Invoke-PodeInternalSchedule($Schedule, $Now)
        {
            $Schedule.OnStart = $false
            $remove = $false

            # increment total number of triggers for the schedule
            if ($Schedule.Countable) {
                $Schedule.Count++
                $Schedule.Countable = ($Schedule.Count -lt $Schedule.Limit)
            }

            # check if we have hit the limit, and remove
            if (($Schedule.Limit -ne 0) -and ($Schedule.Count -ge $Schedule.Limit)) {
                $remove = $true
            }

            # trigger the schedules logic
            try {
                $parameters = @{
                    Event = @{
                        Lockable = $PodeContext.Lockable
                    }
                }

                foreach ($key in $Schedule.Arguments.Keys) {
                    $parameters[$key] = $Schedule.Arguments[$key]
                }

                Add-PodeRunspace -Type Schedules -ScriptBlock (($Schedule.Script).GetNewClosure()) -Parameters $parameters -Forget
            }
            catch {
                $_ | Write-PodeErrorLog
            }

            # reset the cron if it's random
            $Schedule.Crons = Reset-PodeRandomCronExpressions -Expressions $Schedule.Crons
            return $remove
        }

        function Remove-PodeInternalSchedules([string[]] $Schedules)
        {
            # add any schedules to remove that have exceeded their end time
            $Schedules += @($PodeContext.Schedules.Values |
                Where-Object { (($null -ne $_.EndTime) -and ($_.EndTime -lt $_now)) }).Name

            if (($null -eq $Schedules) -or ($Schedules.Length -eq 0)) {
                return
            }

            # remove any schedules
            foreach ($name in $Schedules) {
                if ($PodeContext.Schedules.ContainsKey($name)) {
                    $PodeContext.Schedules.Remove($name) | Out-Null
                }
            }
        }

        # select the schedules that trigger on-start
        $_remove = @()
        $_now = [DateTime]::Now

        $PodeContext.Schedules.Values |
            Where-Object {
                $_.OnStart
            } | ForEach-Object {
                if (Invoke-PodeInternalSchedule -Schedule $_ -Now $_now) {
                    $_remove += $_.Name
                }
            }

        # remove any schedules
        Remove-PodeInternalSchedules -Schedules $_remove

        # first, sleep for a period of time to get to 00 seconds (start of minute)
        Start-Sleep -Seconds (60 - [DateTime]::Now.Second)

        while ($true)
        {
            $_remove = @()
            $_now = [DateTime]::Now

            # select the schedules that need triggering
            $PodeContext.Schedules.Values |
                Where-Object {
                    (($null -eq $_.StartTime) -or ($_.StartTime -le $_now)) -and
                    (($null -eq $_.EndTime) -or ($_.EndTime -ge $_now)) -and
                    (Test-PodeCronExpressions -Expressions $_.Crons -DateTime $_now)
                } | ForEach-Object {
                    if (Invoke-PodeInternalSchedule -Schedule $_ -Now $_now) {
                        $_remove += $_.Name
                    }
                }

            # remove any schedules
            Remove-PodeInternalSchedules -Schedules $_remove

            # cron expression only goes down to the minute, so sleep for 1min
            Start-Sleep -Seconds (60 - [DateTime]::Now.Second)
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $script
}
src\Private\Security.ps1
function Test-PodeIPLimit
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $IP
    )

    $type = 'IP'

    # get the ip address in bytes
    $IP = @{
        'String' = $IP.IPAddressToString;
        'Family' = $IP.AddressFamily;
        'Bytes' = $IP.GetAddressBytes();
    }

    # get the limit rules and active list
    $rules = $PodeContext.Server.Limits.Rules[$type]
    $active = $PodeContext.Server.Limits.Active[$type]
    $now = [DateTime]::UtcNow

    # if there are no rules, it's valid
    if (Test-IsEmpty $rules) {
        return $true
    }

    # is the ip active? (get a direct match, then try grouped subnets)
    $_active_ip = $active[$IP.String]
    if ($null -eq $_active_ip) {
        $_groups = @(foreach ($key in $active.Keys) {
            if ($active[$key].Rule.Grouped) {
                $active[$key]
            }
        })

        $_active_ip = @(foreach ($_group in $_groups) {
            if (Test-PodeIPAddressInRange -IP $IP -LowerIP $_group.Rule.Lower -UpperIP $_group.Rule.Upper) {
                $_group
            }
        })[0]
    }

    # the ip is active, or part of a grouped subnet
    if ($null -ne $_active_ip) {
        # if limit is -1, always allowed
        if ($_active_ip.Rule.Limit -eq -1) {
            return $true
        }

        # check expire time, a reset if needed
        if ($now -ge $_active_ip.Expire) {
            $_active_ip.Rate = 0
            $_active_ip.Expire = $now.AddSeconds($_active_ip.Rule.Seconds)
        }

        # are we over the limit?
        if ($_active_ip.Rate -ge $_active_ip.Rule.Limit) {
            return $false
        }

        # increment the rate
        $_active_ip.Rate++
        return $true
    }

    # the ip isn't active
    else {
        # get the ip's rule
        $_rule_ip = @(foreach ($rule in $rules.Values) {
            if (Test-PodeIPAddressInRange -IP $IP -LowerIP $rule.Lower -UpperIP $rule.Upper) {
                $rule
            }
        })[0]

        # if ip not in rules, it's valid
        # (add to active list as always allowed - saves running where search everytime)
        if ($null -eq $_rule_ip) {
            $active.Add($IP.String, @{
                'Rule' = @{
                    'Limit' = -1
                }
            })

            return $true
        }

        # add ip to active list (ip if not grouped, else the subnet if it's grouped)
        $_ip = (Resolve-PodeValue -Check $_rule_ip.Grouped -TrueValue $_rule_ip.IP -FalseValue $IP.String)

        $active.Add($_ip, @{
            'Rule' = $_rule_ip;
            'Rate' = 1;
            'Expire' = $now.AddSeconds($_rule_ip.Seconds);
        })

        # if limit is 0, it's never allowed
        return ($_rule_ip -ne 0)
    }
}

function Test-PodeIPAccess
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $IP
    )

    $type = 'IP'

    # get the ip address in bytes
    $IP = @{
        'Family' = $IP.AddressFamily;
        'Bytes' = $IP.GetAddressBytes();
    }

    # get permission lists for ip
    $allow = $PodeContext.Server.Access.Allow[$type]
    $deny = $PodeContext.Server.Access.Deny[$type]

    # are they empty?
    $alEmpty = (Test-IsEmpty $allow)
    $dnEmpty = (Test-IsEmpty $deny)

    # if both are empty, value is valid
    if ($alEmpty -and $dnEmpty) {
        return $true
    }

    # if value in allow, it's allowed
    if (!$alEmpty) {
        $match = @(foreach ($value in $allow.Values) {
            if (Test-PodeIPAddressInRange -IP $IP -LowerIP $value.Lower -UpperIP $value.Upper) {
                $value
            }
        })[0]

        if ($null -ne $match) {
            return $true
        }
    }

    # if value in deny, it's disallowed
    if (!$dnEmpty) {
        $match = @(foreach ($value in $deny.Values) {
            if (Test-PodeIPAddressInRange -IP $IP -LowerIP $value.Lower -UpperIP $value.Upper) {
                $value
            }
        })[0]

        if ($null -ne $match) {
            return $false
        }
    }

    # if we have an allow, it's disallowed (because it's not in there)
    if (!$alEmpty) {
        return $false
    }

    # otherwise it's allowed (because it's not in the deny)
    return $true
}

function Add-PodeIPLimit
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [string]
        $IP,

        [Parameter(Mandatory=$true)]
        [int]
        $Limit,

        [Parameter(Mandatory=$true)]
        [int]
        $Seconds,

        [switch]
        $Group
    )

    # current limit type
    $type = 'IP'

    # ensure limit and seconds are non-zero and negative
    if ($Limit -le 0) {
        throw "Limit value cannot be 0 or less for $($IP)"
    }

    if ($Seconds -le 0) {
        throw "Seconds value cannot be 0 or less for $($IP)"
    }

    # get current rules
    $rules = $PodeContext.Server.Limits.Rules[$type]

    # setup up perm type
    if ($null -eq $rules) {
        $PodeContext.Server.Limits.Rules[$type] = @{}
        $PodeContext.Server.Limits.Active[$type] = @{}
        $rules = $PodeContext.Server.Limits.Rules[$type]
    }

    # have we already added the ip?
    elseif ($rules.ContainsKey($IP)) {
        return
    }

    # calculate the lower/upper ip bounds
    if (Test-PodeIPAddressIsSubnetMask -IP $IP) {
        $_tmp = Get-PodeSubnetRange -SubnetMask $IP
        $_tmpLo = Get-PodeIPAddress -IP $_tmp.Lower
        $_tmpHi = Get-PodeIPAddress -IP $_tmp.Upper
    }
    elseif (Test-PodeIPAddressAny -IP $IP) {
        $_tmpLo = Get-PodeIPAddress -IP '0.0.0.0'
        $_tmpHi = Get-PodeIPAddress -IP '255.255.255.255'
    }
    else {
        $_tmpLo = Get-PodeIPAddress -IP $IP
        $_tmpHi = $_tmpLo
    }

    # add limit rule for ip
    $rules.Add($IP, @{
        Limit = $Limit
        Seconds = $Seconds
        Grouped = [bool]$Group
        IP = $IP
        Lower = @{
            Family = $_tmpLo.AddressFamily
            Bytes = $_tmpLo.GetAddressBytes()
        }
        Upper = @{
            Family = $_tmpHi.AddressFamily
            Bytes = $_tmpHi.GetAddressBytes()
        }
    })
}

function Add-PodeIPAccess
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Allow', 'Deny')]
        [string]
        $Access,

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

    # current access type
    $type = 'IP'

    # get opposite permission
    $opp = "$(if ($Access -ieq 'allow') { 'Deny' } else { 'Allow' })"

    # get permission lists for type
    $permType = $PodeContext.Server.Access[$Access][$type]
    $oppType = $PodeContext.Server.Access[$opp][$type]

    # setup up perm type
    if ($null -eq $permType) {
        $PodeContext.Server.Access[$Access][$type] = @{}
        $permType = $PodeContext.Server.Access[$Access][$type]
    }

    # have we already added the ip?
    elseif ($permType.ContainsKey($IP)) {
        return
    }

    # remove from opp type
    if ($null -ne $oppType -and $oppType.ContainsKey($IP)) {
        $oppType.Remove($IP)
    }

    # calculate the lower/upper ip bounds
    if (Test-PodeIPAddressIsSubnetMask -IP $IP) {
        $_tmp = Get-PodeSubnetRange -SubnetMask $IP
        $_tmpLo = Get-PodeIPAddress -IP $_tmp.Lower
        $_tmpHi = Get-PodeIPAddress -IP $_tmp.Upper
    }
    elseif (Test-PodeIPAddressAny -IP $IP) {
        $_tmpLo = Get-PodeIPAddress -IP '0.0.0.0'
        $_tmpHi = Get-PodeIPAddress -IP '255.255.255.255'
    }
    else {
        $_tmpLo = Get-PodeIPAddress -IP $IP
        $_tmpHi = $_tmpLo
    }

    # add access rule for ip
    $permType.Add($IP, @{
        Lower = @{
            Family = $_tmpLo.AddressFamily
            Bytes = $_tmpLo.GetAddressBytes()
        }
        Upper = @{
            Family = $_tmpHi.AddressFamily
            Bytes = $_tmpHi.GetAddressBytes()
        }
    })
}

function Get-PodeCsrfToken
{
    # key name to search
    $key = $PodeContext.Server.Cookies.Csrf.Name

    # check the payload
    if (!(Test-IsEmpty $WebEvent.Data[$key])) {
        return $WebEvent.Data[$key]
    }

    # check the query string
    if (!(Test-IsEmpty $WebEvent.Query[$key])) {
        return $WebEvent.Query[$key]
    }

    # check the headers
    $value = (Get-PodeHeader -Name $key)
    if (!(Test-IsEmpty $value)) {
        return $value
    }

    return $null
}

function Test-PodeCsrfToken
{
    param (
        [Parameter()]
        [string]
        $Secret,

        [Parameter()]
        [string]
        $Token
    )

    # if there's no token/secret, fail
    if ((Test-IsEmpty $Secret) -or (Test-IsEmpty $Token)) {
        return $false
    }

    # the token must start with "t:"
    if (!$Token.StartsWith('t:')) {
        return $false
    }

    # get the salt from the token
    $_token = $Token.Substring(2)
    $periodIndex = $_token.LastIndexOf('.')
    if ($periodIndex -eq -1) {
        return $false
    }

    $salt = $_token.Substring(0, $periodIndex)

    # ensure the token is valid
    if ((Restore-PodeCsrfToken -Secret $Secret -Salt $salt) -ne $Token) {
        return $false
    }

    return $true
}

function New-PodeCsrfSecret
{
    # see if there's already a secret in session/cookie
    $secret = (Get-PodeCsrfSecret)
    if (!(Test-IsEmpty $secret)) {
        return $secret
    }

    # otherwise, make a new secret and cache it
    $secret = (New-PodeGuid -Secure -Length 16)
    Set-PodeCsrfSecret -Secret $secret
    return $secret
}

function Get-PodeCsrfSecret
{
    # key name to get secret
    $key = $PodeContext.Server.Cookies.Csrf.Name

    # are we getting it from a cookie, or session?
    if ($PodeContext.Server.Cookies.Csrf.UseCookies) {
        return (Get-PodeCookie `
            -Name $PodeContext.Server.Cookies.Csrf.Name `
            -Secret $PodeContext.Server.Cookies.Csrf.Secret).Value
    }

    # on session
    else {
        return $WebEvent.Session.Data[$key]
    }
}

function Set-PodeCsrfSecret
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Secret
    )

    # key name to set secret under
    $key = $PodeContext.Server.Cookies.Csrf.Name

    # are we setting this on a cookie, or session?
    if ($PodeContext.Server.Cookies.Csrf.UseCookies) {
        (Set-PodeCookie `
            -Name $PodeContext.Server.Cookies.Csrf.Name `
            -Value $Secret `
            -Secret $PodeContext.Server.Cookies.Csrf.Secret) | Out-Null
    }

    # on session
    else {
        $WebEvent.Session.Data[$key] = $Secret
    }
}

function Restore-PodeCsrfToken
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Secret,

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

    return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
}

function Test-PodeCsrfConfigured
{
    return (!(Test-IsEmpty $PodeContext.Server.Cookies.Csrf))
}
src\Private\Server.ps1
function Start-PodeInternalServer
{
    param (
        [Parameter()]
        $Request,

        [switch]
        $Browse
    )

    try
    {
        # setup temp drives for internal dirs
        Add-PodePSInbuiltDrives

        # create the runspace state, execute the server logic, and start the runspaces
        New-PodeRunspaceState
        Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Logic -NoNewClosure
        New-PodeRunspacePools

        # create timer/schedules for auto-restarting
        New-PodeAutoRestartServer

        $_type = $PodeContext.Server.Type.ToUpperInvariant()
        if (![string]::IsNullOrWhiteSpace($_type) -and !$PodeContext.Server.IsServerless)
        {
            # start runspace for loggers
            Start-PodeLoggingRunspace

            # start runspace for timers
            Start-PodeTimerRunspace

            # start runspace for schedules
            Start-PodeScheduleRunspace

            # start runspace for gui
            Start-PodeGuiRunspace
        }

        # start the appropriate server
        $endpoints = @()

        switch ($_type)
        {
            'SMTP' {
                $endpoints += (Start-PodeSmtpServer)
            }

            'TCP' {
                $endpoints += (Start-PodeTcpServer)
            }

            { ($_ -ieq 'HTTP') -or ($_ -ieq 'HTTPS') } {
                $endpoints += (Start-PodeWebServer -Browse:$Browse)
            }

            'PODE' {
                $endpoints += (Start-PodeSocketServer -Browse:$Browse)
            }

            'SERVICE' {
                Start-PodeServiceServer
            }

            'AZUREFUNCTIONS' {
                Start-PodeAzFuncServer -Data $Request
            }

            'AWSLAMBDA' {
                Start-PodeAwsLambdaServer -Data $Request
            }
        }

        # start web sockets if enabled
        if ($PodeContext.Server.WebSockets.Enabled) {
            $endpoints += (Start-PodeSignalServer)
        }

        # state what endpoints are being listened on
        if ($endpoints.Length -gt 0) {
            Write-Host "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads) thread(s)]:" -ForegroundColor Yellow
            $endpoints | ForEach-Object {
                Write-Host "`t- $($_)" -ForegroundColor Yellow
            }
        }
    }
    catch {
        throw $_.Exception
    }
}

function Restart-PodeInternalServer
{
    try
    {
        # inform restart
        Write-Host 'Restarting server...' -NoNewline -ForegroundColor Cyan

        # cancel the session token
        $PodeContext.Tokens.Cancellation.Cancel()

        # close all current runspaces
        Close-PodeRunspaces -ClosePool

        # remove all of the pode temp drives
        Remove-PodePSDrives

        # clear up timers, schedules and loggers
        $PodeContext.Server.Routes.Keys.Clone() | ForEach-Object {
            $PodeContext.Server.Routes[$_].Clear()
        }

        $PodeContext.Server.Handlers.Keys.Clone() | ForEach-Object {
            $PodeContext.Server.Handlers[$_].Clear()
        }

        $PodeContext.Timers.Clear()
        $PodeContext.Schedules.Clear()
        $PodeContext.Server.Logging.Types.Clear()

        # clear middle/endware
        $PodeContext.Server.Middleware = @()
        $PodeContext.Server.Endware = @()

        # clear misc
        $PodeContext.Server.BodyParsers.Clear()

        # clear endpoints
        $PodeContext.Server.Endpoints = @()

        # clear the sockets
        $PodeContext.Server.Sockets.Listeners = @()
        $PodeContext.Server.Sockets.Queues.Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new()

        # clear the websockets
        $PodeContext.Server.WebSockets.Listeners = @()
        $PodeContext.Server.WebSockets.Queues.Sockets.Clear()
        $PodeContext.Server.WebSockets.Queues.Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new()

        # set view engine back to default
        $PodeContext.Server.ViewEngine = @{
            Type = 'html'
            Extension = 'html'
            Script = $null
            IsDynamic = $false
        }

        # clear up cookie sessions
        $PodeContext.Server.Cookies.Session.Clear()

        # clear up authentication methods
        $PodeContext.Server.Authentications.Clear()

        # clear up shared state
        $PodeContext.Server.State.Clear()

        # recreate the session tokens
        Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation
        $PodeContext.Tokens.Cancellation = New-Object System.Threading.CancellationTokenSource

        Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart
        $PodeContext.Tokens.Restart = New-Object System.Threading.CancellationTokenSource

        # reload the configuration
        $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext

        Write-Host " Done" -ForegroundColor Green

        # restart the server
        Start-PodeInternalServer
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}
src\Private\Serverless.ps1
function Start-PodeAzFuncServer
{
    param (
        [Parameter(Mandatory=$true)]
        $Data
    )

    # setup any inbuilt middleware that works for azure functions
    $inbuilt_middleware = @(
        (Get-PodePublicMiddleware),
        (Get-PodeRouteValidateMiddleware),
        (Get-PodeBodyMiddleware),
        (Get-PodeCookieMiddleware)
    )

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

    try
    {
        try
        {
            # get the request
            $request = $Data.Request

            # setup the response
            $response = New-Object -TypeName HttpResponseContext
            $response.StatusCode = 200
            $response.Headers = @{}

            # reset event data
            $WebEvent = @{
                OnEnd = @()
                Auth = @{}
                Response = $response
                Request = $request
                Lockable = $PodeContext.Lockable
                Method = $request.Method.ToLowerInvariant()
                Query = $request.Query
                Protocol = ($request.Url -split '://')[0]
                Endpoint = ((Get-PodeHeader -Name 'host') -split ':')[0]
                ContentType = (Get-PodeHeader -Name 'content-type')
                ErrorType = $null
                Cookies = @{}
                PendingCookies = @{}
                Path = [string]::Empty
                Streamed = $false
            }

            # set the path, using static content query parameter if passed
            if (![string]::IsNullOrWhiteSpace($request.Query['static-file'])) {
                $WebEvent.Path = $request.Query['static-file']
            }
            else {
                $WebEvent.Path = "/api/$($Data.sys.MethodName)"
            }

            # set pode in server response header
            Set-PodeServerHeader -Type 'Kestrel'

            # 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 -Scoped
                    }
                }
            }
        }
        catch {
            Set-PodeResponseStatus -Code 500 -Exception $_
            Write-Host $Error[0]
        }

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

        # close and send the response
        Push-OutputBinding -Name Response -Value $response
    }
    catch {
        Write-Host $Error[0]
        throw $_.Exception
    }
}

function Start-PodeAwsLambdaServer
{
    param (
        [Parameter(Mandatory=$true)]
        $Data
    )

    # setup any inbuilt middleware that works for aws lambda
    $inbuilt_middleware = @(
        (Get-PodePublicMiddleware),
        (Get-PodeRouteValidateMiddleware),
        (Get-PodeBodyMiddleware),
        (Get-PodeCookieMiddleware)
    )

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

    try
    {
        try
        {
            # get the request
            $request = $Data

            # setup the response
            $response = @{
                StatusCode = 200
                Headers = @{}
                Body = [string]::Empty
            }

            # reset event data
            $WebEvent = @{
                OnEnd = @()
                Auth = @{}
                Response = $response
                Request = $request
                Lockable = $PodeContext.Lockable
                Path = $request.path
                Method = $request.httpMethod.ToLowerInvariant()
                Query = $request.queryStringParameters
                Protocol = (Get-PodeHeader -Name 'X-Forwarded-Proto')
                Endpoint = ((Get-PodeHeader -Name 'Host') -split ':')[0]
                ContentType = (Get-PodeHeader -Name 'Content-Type')
                ErrorType = $null
                Cookies = @{}
                PendingCookies = @{}
                Streamed = $false
            }

            # set pode in server response header
            Set-PodeServerHeader -Type 'Lambda'

            # 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 -Scoped
                    }
                }
            }
        }
        catch {
            Set-PodeResponseStatus -Code 500 -Exception $_
            Write-Host $Error[0]
        }

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

        # close and send the response
        if (![string]::IsNullOrWhiteSpace($response.ContentType)) {
            Set-PodeHeader -Name 'Content-Type' -Value $response.ContentType
        }

        return (@{
            'statusCode' = $response.StatusCode;
            'headers' = $response.Headers;
            'body' = $response.Body;
        } | ConvertTo-Json -Depth 10 -Compress) 
    }
    catch {
        Write-Host $Error[0]
        throw $_.Exception
    }
}
src\Private\ServiceServer.ps1
function Start-PodeServiceServer
{
    # ensure we have service handlers
    if (Test-IsEmpty (Get-PodeHandler -Type Service)) {
        throw 'No Service handlers have been defined'
    }

    # state we're running
    Write-Host "Server looping every $($PodeContext.Server.Interval)secs" -ForegroundColor Yellow

    # script for the looping server
    $serverScript = {
        try
        {
            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                # the event object
                $ServiceEvent = @{
                    Lockable = $PodeContext.Lockable
                }

                # invoke the service handlers
                $handlers = Get-PodeHandler -Type Service
                foreach ($name in $handlers.Keys) {
                    $handler = $handlers[$name]
                    Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments (@($ServiceEvent) + @($handler.Arguments)) -Scoped -Splat
                }

                # sleep before next run
                Start-Sleep -Seconds $PodeContext.Server.Interval
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
    }

    # start the runspace for the server
    Add-PodeRunspace -Type 'Main' -ScriptBlock $serverScript
}
src\Private\Sessions.ps1
function New-PodeSessionCookie
{
    $sid = @{
        Name = $PodeContext.Server.Cookies.Session.Name
        Id = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cookies.Session.GenerateId -Return)
        Cookie = $PodeContext.Server.Cookies.Session.Info
        Data = @{}
    }

    Set-PodeSessionCookieDataHash -Session $sid

    $sid.Cookie.TimeStamp = [DateTime]::UtcNow
    return $sid
}

function Set-PodeSessionCookie
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    $secure = [bool]($Session.Cookie.Secure)
    $discard = [bool]($Session.Cookie.Discard)
    $httpOnly = [bool]($Session.Cookie.HttpOnly)

    (Set-PodeCookie `
        -Name $Session.Name `
        -Value $Session.Id `
        -Secret $PodeContext.Server.Cookies.Session.Secret `
        -ExpiryDate (Get-PodeSessionCookieExpiry -Session $Session) `
        -HttpOnly:$httpOnly `
        -Discard:$discard `
        -Secure:$secure) | Out-Null
}

function Get-PodeSessionCookie
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret
    )

    # check that the cookie is validly signed
    if (!(Test-PodeCookieSigned -Name $Name -Secret $Secret)) {
        return $null
    }

    # get the cookie from the request
    $cookie = Get-PodeCookie -Name $Name -Secret $Secret
    if (Test-IsEmpty $cookie) {
        return $null
    }

    # generate the session from the cookie
    $data = @{
        Name = $cookie.Name
        Id = $cookie.Value
        Cookie = $PodeContext.Server.Cookies.Session.Info
        Data = @{}
    }

    $data.Cookie.TimeStamp = $cookie.TimeStamp
    return $data
}

function Remove-PodeSessionCookie
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    # remove the cookie from the response
    Remove-PodeCookie -Name $Session.Name

    # remove session from store
    Invoke-PodeScriptBlock -ScriptBlock $Session.Delete -Arguments @($Session) -Splat

    # blank the session
    $Session.Clear()
}

function Set-PodeSessionCookieDataHash
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    $Session.Data = (Protect-PodeValue -Value $Session.Data -Default @{})
    $Session.DataHash = (Invoke-PodeSHA256Hash -Value ($Session.Data | ConvertTo-Json -Depth 10 -Compress))
}

function Test-PodeSessionCookieDataHash
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    if (Test-IsEmpty $Session.DataHash) {
        return $false
    }

    $Session.Data = (Protect-PodeValue -Value $Session.Data -Default @{})
    $hash = (Invoke-PodeSHA256Hash -Value ($Session.Data | ConvertTo-Json -Depth 10 -Compress))
    return ($Session.DataHash -eq $hash)
}

function Get-PodeSessionCookieExpiry
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    if ($null -eq $Session.Cookie) {
        return [DateTime]::MinValue
    }

    $expiry = (Resolve-PodeValue -Check ([bool]$Session.Cookie.Extend) -TrueValue ([DateTime]::UtcNow) -FalseValue $Session.Cookie.TimeStamp)
    $expiry = $expiry.AddSeconds($Session.Cookie.Duration)
    return $expiry
}

function Set-PodeSessionCookieHelpers
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        $Session
    )

    # force save a session's data to the store
    $Session | Add-Member -MemberType NoteProperty -Name Save -Value {
        param($session, $check)

        # do nothing if session has no ID
        if ([string]::IsNullOrWhiteSpace($session.Id)) {
            return
        }

        # only save if check and hashes different
        if ($check -and (Test-PodeSessionCookieDataHash -Session $session)) {
            return
        }

        # generate the expiry
        $expiry = (Get-PodeSessionCookieExpiry -Session $session)

        # save session data to store
        $PodeContext.Server.Cookies.Session.Store.Set($session.Id, $session.Data, $expiry)

        # update session's data hash
        Set-PodeSessionCookieDataHash -Session $session
    }

    # delete the current session
    $Session | Add-Member -MemberType NoteProperty -Name Delete -Value {
        param($session)

        # remove data from store
        $PodeContext.Server.Cookies.Session.Store.Delete($session.Id)

        # clear session
        $session.Clear()
    }
}

function Get-PodeSessionCookieInMemStore
{
    $store = New-Object -TypeName psobject

    # add in-mem storage
    $store | Add-Member -MemberType NoteProperty -Name Memory -Value @{}

    # delete a sessionId and data
    $store | Add-Member -MemberType ScriptMethod -Name Delete -Value {
        param($sessionId)
        $this.Memory.Remove($sessionId) | Out-Null
    }

    # get a sessionId's data
    $store | Add-Member -MemberType ScriptMethod -Name Get -Value {
        param($sessionId)

        $s = $this.Memory[$sessionId]

        # if expire, remove
        if ($null -ne $s -and $s.Expiry -lt [DateTime]::UtcNow) {
            $this.Memory.Remove($sessionId) | Out-Null
            return $null
        }

        return $s.Data
    }

    # update/insert a sessionId and data
    $store | Add-Member -MemberType ScriptMethod -Name Set -Value {
        param($sessionId, $data, $expiry)

        $this.Memory[$sessionId] = @{
            Data = $data
            Expiry = $expiry
        }
    }

    return $store
}

function Set-PodeSessionCookieInMemClearDown
{
    # don't setup if serverless - as memory is short lived anyway
    if ($PodeContext.Server.IsServerless) {
        return
    }

    # cleardown expired inmem session every 10 minutes
    Add-PodeSchedule -Name '__pode_session_inmem_cleanup__' -Cron '0/10 * * * *' -ScriptBlock {
        $store = $PodeContext.Server.Cookies.Session.Store
        if (Test-IsEmpty $store.Memory) {
            return
        }

        # remove sessions that have expired
        $now = [DateTime]::UtcNow
        foreach ($key in $store.Memory.Keys) {
            if ($store.Memory[$key].Expiry -lt $now) {
                $store.Memory.Remove($key)
            }
        }
    }
}

function Test-PodeSessionsConfigured
{
    return (!(Test-IsEmpty $PodeContext.Server.Cookies.Session))
}
src\Private\Setup.ps1
function Invoke-PodePackageScript
{
    param (
        [Parameter()]
        [string]
        $ActionScript
    )

    if ([string]::IsNullOrWhiteSpace($ActionScript)) {
        return
    }

    if (Test-IsPSCore) {
        pwsh.exe /c "$($ActionScript)"
    }
    else {
        powershell.exe /c "$($ActionScript)"
    }
}

function Install-PodeLocalModules
{
    param (
        [Parameter()]
        $Modules = $null
    )

    if ($null -eq $Modules) {
        return
    }

    $psModules = './ps_modules'

    # download modules to ps_modules
    $Modules.psobject.properties.name | ForEach-Object {
        $_name = $_
        $_version = $Modules.$_name

        try {
            # if version is latest, retrieve current
            if ($_version -ieq 'latest') {
                $_version = [string]((Find-Module $_name -ErrorAction Ignore).Version)
            }

            Write-Host "=> Downloading $($_name)@$($_version)... " -NoNewline -ForegroundColor Cyan

            # if the current version exists, do nothing
            if (!(Test-Path (Join-Path $psModules "$($_name)/$($_version)"))) {
                # remove other versions
                if (Test-Path (Join-Path $psModules "$($_name)")) {
                    Remove-Item -Path (Join-Path $psModules "$($_name)") -Force -Recurse | Out-Null
                }

                # download the module
                Save-Module -Name $_name -RequiredVersion $_version -Path $psModules -Force -ErrorAction Stop | Out-Null
            }

            Write-Host 'Success' -ForegroundColor Green
        }
        catch {
            Write-Host 'Failed' -ForegroundColor Red
            throw "Module or version not found: $($_name)@$($_version)"
        }
    }
}
src\Private\SignalServer.ps1
function Start-PodeSignalServer
{
    # setup the callback for sockets
    $PodeContext.Server.WebSockets.Ssl.Callback = Get-PodeSocketCertifcateCallback

    # work out which endpoints to listen on
    $endpoints = @()
    @(Get-PodeEndpoints -Type Ws) | ForEach-Object {
        # get the protocol
        $_protocol = (Resolve-PodeValue -Check $_.Ssl -TrueValue 'wss' -FalseValue 'ws')

        # 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 9443 -FalseValue 9080)
        }

        # 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 {
            $PodeContext.Server.WebSockets.Listeners += (Initialize-PodeSocketListenerEndpoint `
                -Type WebSockets `
                -Address $_.Address `
                -Port $_.Port `
                -Certificate $_.Certificate)
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        $_.Exception | Write-PodeErrorLog -CheckInnerException
        Close-PodeSocketListener -Type WebSockets
        throw $_.Exception
    }

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

        try
        {
            Start-PodeSocketListener -Listeners $PodeContext.Server.WebSockets.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 | ForEach-Object {
        Add-PodeRunspace -Type 'Signals' -ScriptBlock $listenScript `
            -Parameters @{ 'ThreadId' = $_ }
    }

    # script to write messages back to the client(s)
    $signalScript = {
        try {
            [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(100))

                # check if we have any messages to send
                $message = $null
                if (!$PodeContext.Server.WebSockets.Queues.Messages.TryDequeue([ref]$message)) {
                    $message = $null
                }

                if ($null -eq $message) {
                    continue
                }

                # get the sockets for the message
                $sockets = @()

                # by clientId
                if (![string]::IsNullOrWhiteSpace($message.ClientId)) {
                    $sockets = @($PodeContext.Server.WebSockets.Queues.Sockets[$message.ClientId])
                }
                else {
                    $sockets = @($PodeContext.Server.WebSockets.Queues.Sockets.Values)

                    # by path
                    if (![string]::IsNullOrWhiteSpace($message.Path)) {
                        $sockets = @(foreach ($socket in $sockets) {
                            if ($socket.Path -ieq $message.Path) {
                                $socket
                                break
                            }
                        })
                    }
                }

                # do nothing if no socket found
                if (($null -eq $sockets) -or ($sockets.Length -eq 0)) {
                    continue
                }

                # frame the message
                $buffer = [byte[]]@()
                $buffer += [byte]([byte]0x80 -bor [byte]1)

                $payload = $PodeContext.Server.Encoding.GetBytes($message.Value)
                if ($payload.Length -lt 126) {
                    $buffer += [byte]([byte]0x00 -bor [byte]$payload.Length)
                }
                elseif ($payload.Length -le [uint16]::MaxValue) {
                    $buffer += [byte]([byte]0x00 -bor [byte]126)
                }
                else {
                    $buffer += [byte]([byte]0x00 -bor [byte]127)
                }

                $buffer += $payload

                # send the message to all found sockets
                foreach ($socket in $sockets) {
                    try {
                        Wait-PodeTask -Task $socket.Stream.WriteAsync($buffer, 0, $buffer.Length)
                    }
                    catch {
                        $PodeContext.Server.WebSockets.Queues.Sockets.Remove($socket.ClientId) | Out-Null
                    }
                }
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            $_.Exception | Write-PodeErrorLog -CheckInnerException
            throw $_.Exception
        }
    }

    Add-PodeRunspace -Type 'Signals' -ScriptBlock $signalScript

    # 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 WebSockets
        }
    }

    Add-PodeRunspace -Type 'Signals' -ScriptBlock $waitScript
    return @($endpoints.HostName)
}

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

    try
    {
        # 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.WebSockets.Ssl.Callback)
                $stream.AuthenticateAsServer($Context.Certificate, $false, $PodeContext.Server.WebSockets.Ssl.Protocols, $false)
            }
            catch {
                # immediately close http connections
                Close-PodeSocket -Socket $Context.Socket -Shutdown
                return
            }
        }

        # read the request headers - once again, 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

        # if the method is not a GET, close immediately
        if ($req_info.Method -ine 'GET') {
            Close-PodeSocket -Socket $Context.Socket -Shutdown
            return
        }
    }
    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 {
        # get the path, and generate a clientId
        $path = $req_info.Uri.AbsolutePath
        $clientId = New-PodeGuid -Secure

        # send back headers to upgrade the client to a websocket
        $magicGuid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
        $secSocketKey = $req_info.Headers['Sec-WebSocket-Key'].Trim()

        $resHeaders = @{
            'Connection' = 'Upgrade'
            'Upgrade' = 'websocket'
            'Sec-WebSocket-Accept' = Invoke-PodeSHA1Hash -Value "$($secSocketKey)$($magicGuid)"
            'X-Pode-ClientId' = $clientId
        }

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

        $newLine = "`r`n"
        $res_msg = "$($protocol) 101 Switching Protocols$($newLine)"

        # write the response headers
        foreach ($key in $resHeaders.Keys) {
            $res_msg += "$($key): $($resHeaders[$key])$($newLine)"
        }

        $res_msg += $newLine

        # stream response output
        $buffer = $PodeContext.Server.Encoding.GetBytes($res_msg)
        Wait-PodeTask -Task $stream.WriteAsync($buffer, 0, $buffer.Length)

        # add the socket/stream to all sockets, and path sockets (and clientId)
        $PodeContext.Server.WebSockets.Queues.Sockets[$clientId] = @{
            Socket = $Context.Socket
            Stream = $stream
            Path = $path
            ClientId = $clientId
        }
    }
    catch [System.Management.Automation.MethodInvocationException] { }
}
src\Private\SmtpServer.ps1
function Start-PodeSmtpServer
{
    # ensure we have smtp handlers
    if (Test-IsEmpty (Get-PodeHandler -Type Smtp)) {
        throw 'No SMTP handlers have been defined'
    }

    # grab the relavant port
    $port = $PodeContext.Server.Endpoints[0].Port
    if ($port -eq 0) {
        $port = 25
    }

    # get the IP address for the server
    $ipAddress = $PodeContext.Server.Endpoints[0].Address
    if (Test-PodeHostname -Hostname $ipAddress) {
        $ipAddress = (Get-PodeIPAddressesForHostname -Hostname $ipAddress -Type All | Select-Object -First 1)
        $ipAddress = (Get-PodeIPAddress $ipAddress)
    }

    try
    {
        # create the listener for smtp
        $endpoint = New-Object System.Net.IPEndPoint($ipAddress, $port)
        $listener = New-Object System.Net.Sockets.TcpListener -ArgumentList $endpoint

        # start listener
        $listener.Start()
    }
    catch {
        if ($null -ne $listener) {
            $listener.Stop()
        }

        throw $_.Exception
    }

    # script for listening out of for incoming requests
    $listenScript = {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener,

            [Parameter(Mandatory=$true)]
            [int]
            $ThreadId
        )

        # scriptblock for the core smtp message processing logic
        $process = {
            # if there's no client, just return
            if ($null -eq $SmtpEvent.Client) {
                return
            }

            # variables to store data for later processing
            $mail_from = [string]::Empty
            $rcpt_tos = @()
            $data = [string]::Empty

            # open response to smtp request
            Write-PodeTcpClient -Message "220 $($PodeContext.Server.Endpoints[0].HostName) -- Pode Proxy Server"
            $msg = [string]::Empty

            # respond to smtp request
            while ($true)
            {
                try { $msg = (Read-PodeTcpClient) }
                catch {
                    $_ | Write-PodeErrorLog
                    break
                }

                try {
                    if (!(Test-IsEmpty $msg)) {
                        if ($msg.StartsWith('QUIT')) {
                            Write-PodeTcpClient -Message '221 Bye'
                            Close-PodeTcpConnection
                            break
                        }

                        if ($msg.StartsWith('EHLO') -or $msg.StartsWith('HELO')) {
                            Write-PodeTcpClient -Message '250 OK'
                        }

                        if ($msg.StartsWith('RCPT TO')) {
                            Write-PodeTcpClient -Message '250 OK'
                            $rcpt_tos += (Get-PodeSmtpEmail $msg)
                        }

                        if ($msg.StartsWith('MAIL FROM')) {
                            Write-PodeTcpClient -Message '250 OK'
                            $mail_from = (Get-PodeSmtpEmail $msg)
                        }

                        if ($msg.StartsWith('DATA'))
                        {
                            Write-PodeTcpClient -Message '354 Start mail input; end with <CR><LF>.<CR><LF>'
                            $data = (Read-PodeTcpClient)
                            Write-PodeTcpClient -Message '250 OK'

                            # set event data/headers
                            $SmtpEvent.Email.From = $mail_from
                            $SmtpEvent.Email.To = $rcpt_tos
                            $SmtpEvent.Email.Data = $data
                            $SmtpEvent.Email.Headers = (Get-PodeSmtpHeadersFromData $data)

                            # set the subject/priority/content-types
                            $SmtpEvent.Email.Subject = $SmtpEvent.Headers['Subject']
                            $SmtpEvent.Email.IsUrgent = (($SmtpEvent.Headers['Priority'] -ieq 'urgent') -or ($SmtpEvent.Headers['Importance'] -ieq 'high'))
                            $SmtpEvent.Email.ContentType = $SmtpEvent.Headers['Content-Type']
                            $SmtpEvent.Email.ContentEncoding = $SmtpEvent.Headers['Content-Transfer-Encoding']

                            # set the email body
                            $SmtpEvent.Email.Body = (Get-PodeSmtpBody -Data $data -ContentType $SmtpEvent.ContentType -ContentEncoding $SmtpEvent.ContentEncoding)

                            # call user handlers for processing smtp data
                            $handlers = Get-PodeHandler -Type Smtp
                            foreach ($name in $handlers.Keys) {
                                $handler = $handlers[$name]
                                Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments (@($SmtpEvent) + @($handler.Arguments)) -Scoped -Splat
                            }

                            # reset the to list
                            $rcpt_tos = @()
                        }
                    }
                }
                catch [exception] {
                    $_ | Write-PodeErrorLog
                    throw $_.exception
                }
            }
        }

        try
        {
            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                # get an incoming request
                $client = (Wait-PodeTask -Task $Listener.AcceptTcpClientAsync())

                # convert the ip
                $ip = (ConvertTo-PodeIPAddress -Endpoint $client.Client.RemoteEndPoint)

                # ensure the request ip is allowed
                if (!(Test-PodeIPAccess -IP $ip) -or !(Test-PodeIPLimit -IP $ip)) {
                    Close-PodeTcpConnection -Quit
                }

                # deal with smtp call
                else {
                    $SmtpEvent = @{
                        Client = $client
                        Lockable = $PodeContext.Lockable
                        Email = $null
                    }

                    Invoke-PodeScriptBlock -ScriptBlock $process
                    Close-PodeTcpConnection -Quit
                }
            }
        }
        catch [System.OperationCanceledException] {
            Close-PodeTcpConnection -Quit
        }
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
    }

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

    # script to keep smtp server listening until cancelled
    $waitScript = {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener
        )

        try
        {
            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
        finally {
            if ($null -ne $Listener) {
                $Listener.Stop()
            }
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }

    # state where we're running
    return @("smtp://$($PodeContext.Server.Endpoints[0].HostName):$($port)")
}


function Get-PodeSmtpEmail
{
    param (
        [Parameter()]
        [string]
        $Value
    )

    $tmp = @($Value -isplit ':')
    if ($tmp.Length -gt 1) {
        return $tmp[1].Trim().Trim(' <>')
    }

    return [string]::Empty
}

function Get-PodeSmtpBody
{
    param (
        [Parameter()]
        [string]
        $Data,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [string]
        $ContentEncoding
    )

    # split the message up
    $dataSplit = @($Data -isplit [System.Environment]::NewLine)

    # get the index of the first blank line, and last dot
    $indexOfBlankLine = $dataSplit.IndexOf([string]::Empty)
    $indexOfLastDot = [array]::LastIndexOf($dataSplit, '.')

    # get the body
    $body = ($dataSplit[($indexOfBlankLine + 1)..($indexOfLastDot - 2)] -join [System.Environment]::NewLine)

    # if there's no body, just return
    if (($indexOfLastDot -eq -1) -or (Test-IsEmpty $body)) {
        return $body
    }

    # decode body based on encoding
    switch ($ContentEncoding.ToLowerInvariant()) {
        'base64' {
            $body = [System.Convert]::FromBase64String($body)
        }
    }

    # only if body is bytes, first decode based on type
    switch ($ContentType) {
        { $_ -ilike '*utf-7*' } {
            $body = [System.Text.Encoding]::UTF7.GetString($body)
        }

        { $_ -ilike '*utf-8*' } {
            $body = [System.Text.Encoding]::UTF8.GetString($body)
        }

        { $_ -ilike '*utf-16*' } {
            $body = [System.Text.Encoding]::Unicode.GetString($body)
        }

        { $_ -ilike '*utf-32*' } {
            $body = [System.Text.Encoding]::UTF32.GetString($body)
        }
    }

    return $body
}

function Get-PodeSmtpHeadersFromData
{
    param (
        [Parameter()]
        [string]
        $Data
    )

    $headers = @{}
    $lines = @($Data -isplit [System.Environment]::NewLine)

    foreach ($line in $lines) {
        if ([string]::IsNullOrWhiteSpace($line)) {
            break
        }

        if ($line -imatch '^(?<name>.*?)\:\s+(?<value>.*?)$') {
            $headers[$Matches['name'].Trim()] = $Matches['value'].Trim()
        }
    }

    return $headers
}
src\Private\Sockets.ps1
function Initialize-PodeSocketListenerEndpoint
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet('Sockets', 'WebSockets')]
        [string]
        $Type,

        [Parameter(Mandatory=$true)]
        [ipaddress]
        $Address,

        [Parameter(Mandatory=$true)]
        [int]
        $Port,

        [Parameter()]
        [X509Certificate]
        $Certificate
    )

    $endpoint = [IPEndpoint]::new($Address, $Port)
    $socket = [System.Net.Sockets.Socket]::new($endpoint.AddressFamily, [System.Net.Sockets.SocketType]::Stream, [System.Net.Sockets.ProtocolType]::Tcp)
    $socket.SetSocketOption([System.Net.Sockets.SocketOptionLevel]::Socket, [System.Net.Sockets.SocketOptionName]::KeepAlive, $false)
    $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout
    $socket.Bind($endpoint)
    $socket.Listen([int]::MaxValue)

    $protocol = (Resolve-PodeValue -Check ($null -eq $Certificate) -TrueValue 'http' -FalseValue 'https')
    if ($Type -ieq 'WebSockets') {
        $protocol = (Resolve-PodeValue -Check ($null -eq $Certificate) -TrueValue 'ws' -FalseValue 'wss')
    }

    return @{
        Type = $Type
        Socket = $socket
        Certificate = $Certificate
        Protocol = $protocol
    }
}

function New-PodeSocketListenerEvent
{
    param(
        [Parameter(Mandatory=$true)]
        $Listener,

        [Parameter()]
        [int]
        $Index = 0
    )

    Lock-PodeObject -Object $PodeContext.Server[$Listener.Type] -Return -ScriptBlock {
        $socketArgs = [System.Net.Sockets.SocketAsyncEventArgs]::new()

        if ($Index -eq 0) {
            $PodeContext.Server[$Listener.Type].MaxConnections++
            $Index = $PodeContext.Server[$Listener.Type].MaxConnections
        }

        $name = (Get-PodeSocketListenerConnectionEventName -Type $Listener.Type -Id $Index)
        Register-ObjectEvent -InputObject $socketArgs -EventName 'Completed' -SourceIdentifier $name -Action {
            Invoke-PodeSocketProcessAccept -Arguments $Event.SourceEventArgs
        } | Out-Null

        return $socketArgs
    }
}

function Start-PodeSocketListener
{
    param(
        [Parameter(Mandatory=$true)]
        [hashtable[]]
        $Listeners
    )

    foreach ($listener in $Listeners) {
        Invoke-PodeSocketAccept -Listener $listener
    }
}

function Close-PodeSocket
{
    param(
        [Parameter()]
        [System.Net.Sockets.Socket]
        $Socket,

        [switch]
        $Shutdown
    )

    if ($null -eq $Socket) {
        return
    }

    if ($Shutdown -and $Socket.Connected) {
        $Socket.Shutdown([System.Net.Sockets.SocketShutdown]::Both)
    }

    Close-PodeDisposable -Disposable $Socket -Close
}

function Close-PodeSocketListener
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet('Sockets', 'WebSockets')]
        [string]
        $Type
    )

    try {
        # close all open sockets
        if ($Type -ieq 'WebSockets') {
            for ($i = $PodeContext.Server[$Type].Queues.Sockets.Count - 1; $i -ge 0; $i--) {
                Close-PodeSocket -Socket $PodeContext.Server[$Type].Queues.Sockets[$i].Socket -Shutdown
            }

            $PodeContext.Server[$Type].Queues.Sockets.Clear()
        }

        # close all open listeners and unbind events
        for ($i = $PodeContext.Server[$Type].Listeners.Length - 1; $i -ge 0; $i--) {
            Close-PodeSocket -Socket $PodeContext.Server[$Type].Listeners[$i].Socket -Shutdown
        }

        $PodeContext.Server[$Type].Listeners = @()
    }
    catch {
        $_.Exception | Out-Default
    }
}

function Invoke-PodeSocketAccept
{
    param(
        [Parameter(Mandatory=$true)]
        $Listener
    )

    # pop args from queue (or create a new one)
    $arguments = $null
    if (!$PodeContext.Server[$Listener.Type].Queues.Connections.TryDequeue([ref]$arguments)) {
        $arguments = New-PodeSocketListenerEvent -Listener $Listener
    }

    $arguments.AcceptSocket = $null
    $arguments.UserToken = $Listener
    $raised = $false

    try {
        $raised = $arguments.UserToken.Socket.AcceptAsync($arguments)
    }
    catch [System.ObjectDisposedException] {
        return
    }

    if (!$raised) {
        Invoke-PodeSocketProcessAccept -Arguments $arguments
    }
}

function Invoke-PodeSocketProcessAccept
{
    param(
        [Parameter(Mandatory=$true)]
        [System.Net.Sockets.SocketAsyncEventArgs]
        $Arguments
    )

    # get the socket and listener
    $accepted = $Arguments.AcceptSocket
    $listener = $Arguments.UserToken
    $errors = $Arguments.SocketError

    # reset the socket args
    $Arguments.AcceptSocket = $null
    $Arguments.UserToken = $null

    # start accepting connections again for the listener
    Invoke-PodeSocketAccept -Listener $listener

    # if not success, close this accept socket and accept again
    if (($null -eq $accepted) -or ($errors -ne [System.Net.Sockets.SocketError]::Success)) {
        # close socket
        if ($null -ne $accepted) {
            $accepted.Close()
        }

        # add args back to pool
        $PodeContext.Server[$listener.Type].Queues.Connections.Enqueue($Arguments)
        return
    }

    # add args back to pool
    $PodeContext.Server[$listener.Type].Queues.Connections.Enqueue($Arguments)

    switch ($listener.Type.ToLowerInvariant()) {
        'sockets' {
            Invoke-PodeSocketHandler -Context @{
                Socket = $accepted
                Certificate = $listener.Certificate
                Protocol = $listener.Protocol
            }
        }

        'websockets' {
            Invoke-PodeWebSocketHandler -Context @{
                Socket = $accepted
                Certificate = $listener.Certificate
                Protocol = $listener.Protocol
            }
        }
    }
}

function Get-PodeSocketCertifcateCallback
{
    return ([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)
    })
}

function Get-PodeSocketListenerConnectionEventName
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Sockets', 'WebSockets')]
        [string]
        $Type,

        [Parameter(Mandatory=$true)]
        [int]
        $Id
    )

    return "PodeListenerConnection$($Type)Completed_$($Id)"
}

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
    }
}
src\Private\Streams.ps1
function Read-PodeStreamToEnd
{
    param (
        [Parameter()]
        $Stream,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    if ($null -eq $Stream) {
        return [string]::Empty
    }

    return (Use-PodeStream -Stream ([System.IO.StreamReader]::new($Stream, $Encoding)) {
        return $args[0].ReadToEnd()
    })
}

function Read-PodeByteLineFromByteArray
{
    param (
        [Parameter(Mandatory=$true)]
        [byte[]]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8,

        [Parameter()]
        [int]
        $StartIndex = 0,

        [switch]
        $IncludeNewLine
    )

    $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding

    # attempt to find \n
    $index = [array]::IndexOf($Bytes, $nlBytes.NewLine, $StartIndex)
    $fIndex = $index

    # if not including new line, remove any trailing \r and \n
    if (!$IncludeNewLine) {
        $fIndex--

        if ($Bytes[$fIndex] -eq $nlBytes.Return) {
            $fIndex--
        }
    }

    # grab the portion of the bytes array - which is our line
    return @{
        'Bytes' = $Bytes[$StartIndex..$fIndex];
        'StartIndex' = $StartIndex;
        'EndIndex' = $index;
    }
}

function Get-PodeByteLinesFromByteArray
{
    param (
        [Parameter(Mandatory=$true)]
        [byte[]]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8,

        [switch]
        $IncludeNewLine
    )

    # lines
    $lines = @()
    $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding

    # attempt to find \n
    $index = 0
    while (($nextIndex = [array]::IndexOf($Bytes, $nlBytes.NewLine, $index)) -gt 0) {
        $fIndex = $nextIndex

        # if not including new line, remove any trailing \r and \n
        if (!$IncludeNewLine) {
            $fIndex--
            if ($Bytes[$fIndex] -eq $nlBytes.Return) {
                $fIndex--
            }
        }

        # add the line, and get the next one
        $lines += ,$Bytes[$index..$fIndex]
        $index = $nextIndex + 1
    }

    return $lines
}

function ConvertFrom-PodeStreamToBytes
{
    param (
        [Parameter(Mandatory=$true)]
        $Stream
    )

    $buffer = [byte[]]::new(64 * 1024)
    $ms = New-Object -TypeName System.IO.MemoryStream
    $read = 0

    while (($read = $Stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
        $ms.Write($buffer, 0, $read)
    }

    $ms.Close()
    return $ms.ToArray()
}

function ConvertFrom-PodeValueToBytes
{
    param (
        [Parameter()]
        [object]
        $Value,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    if ($Value.GetType().Name -ieq 'string') {
        $Value = $Encoding.GetBytes($Value)
    }

    return $Value
}

function ConvertFrom-PodeBytesToString
{
    param (
        [Parameter()]
        [byte[]]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8,

        [switch]
        $RemoveNewLine
    )

    if (Test-IsEmpty $Bytes) {
        return $Bytes
    }

    $value = $Encoding.GetString($Bytes)
    if ($RemoveNewLine) {
        $value = $value.Trim("`r`n")
    }

    return $value
}

function Get-PodeNewLineBytes
{
    param (
        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    return @{
        'NewLine' = @($Encoding.GetBytes("`n"))[0];
        'Return' = @($Encoding.GetBytes("`r"))[0];
    }
}

function Test-PodeByteArrayIsBoundary
{
    param (
        [Parameter()]
        [byte[]]
        $Bytes,

        [Parameter()]
        [string]
        $Boundary,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    # if no bytes, return
    if ($Bytes.Length -eq 0) {
        return $false
    }

    # if length difference >3, return (ie, 2 offset for `r`n)
    if (($Bytes.Length - $Boundary.Length) -gt 3) {
        return $false
    }

    # check if bytes starts with the boundary
    return (ConvertFrom-PodeBytesToString $Bytes $Encoding).StartsWith($Boundary)
}

function Remove-PodeNewLineBytesFromArray
{
    param (
        [Parameter()]
        $Bytes,

        [Parameter()]
        $Encoding = [System.Text.Encoding]::UTF8
    )

    $nlBytes = Get-PodeNewLineBytes -Encoding $Encoding
    $length = $Bytes.Length

    if ($Bytes[$length] -eq $nlBytes.NewLine) {
        $length--
    }

    if ($Bytes[$length] -eq $nlBytes.Return) {
        $length--
    }

    return $Bytes[0..$length]
}
src\Private\TcpServer.ps1
function Start-PodeTcpServer
{
    # ensure we have service handlers
    if (Test-IsEmpty (Get-PodeHandler -Type Tcp)) {
        throw 'No TCP handlers have been defined'
    }

    # grab the relavant port
    $port = $PodeContext.Server.Endpoints[0].Port
    if ($port -eq 0) {
        $port = 9001
    }

    # get the IP address for the server
    $ipAddress = $PodeContext.Server.Endpoints[0].Address
    if (Test-PodeHostname -Hostname $ipAddress) {
        $ipAddress = (Get-PodeIPAddressesForHostname -Hostname $ipAddress -Type All | Select-Object -First 1)
        $ipAddress = (Get-PodeIPAddress $ipAddress)
    }

    try
    {
        # create the listener for tcp
        $endpoint = New-Object System.Net.IPEndPoint($ipAddress, $port)
        $listener = New-Object System.Net.Sockets.TcpListener -ArgumentList $endpoint

        # start listener
        $listener.Start()
    }
    catch {
        if ($null -ne $listener) {
            $listener.Stop()
        }

        throw $_.Exception
    }

    # script for listening out of for incoming requests
    $listenScript = {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener,

            [Parameter(Mandatory=$true)]
            [int]
            $ThreadId
        )

        try
        {
            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                # get an incoming request
                $client = (Wait-PodeTask -Task $Listener.AcceptTcpClientAsync())

                # convert the ip
                $ip = (ConvertTo-PodeIPAddress -Endpoint $client.Client.RemoteEndPoint)

                # ensure the request ip is allowed and deal with the tcp call
                if ((Test-PodeIPAccess -IP $ip) -and (Test-PodeIPLimit -IP $ip)) {
                    $TcpEvent = @{
                        Client = $client
                        Lockable = $PodeContext.Lockable
                    }

                    # invoke the tcp handlers
                    $handlers = Get-PodeHandler -Type Tcp
                    foreach ($name in $handlers.Keys) {
                        $handler = $handlers[$name]
                        Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments (@($TcpEvent) + @($handler.Arguments)) -Scoped -Splat
                    }
                }

                # close the connection
                Close-PodeTcpConnection
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
    }

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

    # script to keep tcp server listening until cancelled
    $waitScript = {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener
        )

        try
        {
            while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
        finally {
            if ($null -ne $Listener) {
                $Listener.Stop()
            }
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }

    # state where we're running
    return @("tcp://$($PodeContext.Server.Endpoints[0].HostName):$($port)")
}
src\Private\Timers.ps1
function Get-PodeTimer
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    return $PodeContext.Timers[$Name]
}

function Start-PodeTimerRunspace
{
    if ((Get-PodeCount $PodeContext.Timers) -eq 0) {
        return
    }

    $script = {
        while ($true)
        {
            $_remove = @()
            $_now = [DateTime]::Now

            $PodeContext.Timers.Values | Where-Object { $_.OnStart -or ($_.NextTick -le $_now) } | ForEach-Object {
                $run = $true
                $_.OnStart = $false

                # increment total number of runs for timer (do we still need to count?)
                if ($_.Countable) {
                    $_.Count++
                    $_.Countable = ($_.Count -le $_.Limit)
                }

                # check if we have hit the limit, and remove
                if ($run -and ($_.Limit -ne 0) -and ($_.Count -gt $_.Limit)) {
                    $run = $false
                    $_remove += $_.Name
                }

                if ($run) {
                    try {
                        $_event = @{ Lockable = $PodeContext.Lockable }
                        $_args = @($_event) + @($_.Arguments)
                        Invoke-PodeScriptBlock -ScriptBlock $_.Script -Arguments $_args -Scoped -Splat
                    }
                    catch {
                        $Error[0]
                    }
                }

                $_.NextTick = $_now.AddSeconds($_.Interval)
            }

            # remove any timers
            $_remove | ForEach-Object {
                $PodeContext.Timers.Remove($_)
            }

            Start-Sleep -Seconds 1
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $script
}
src\Private\WebServer.ps1
function Start-PodeWebServer
{
    param (
        [switch]
        $Browse
    )

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

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

    # work out which endpoints to listen on
    $endpoints = @()
    @(Get-PodeEndpoints -Type Http) | ForEach-Object {
        # get the protocol
        $_protocol = (Resolve-PodeValue -Check $_.Ssl -TrueValue 'https' -FalseValue 'http')

        # get the ip address
        $_ip = "$($_.Address)"
        if ($_ip -ieq '0.0.0.0') {
            $_ip = '*'
        }

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

        # if this endpoint is https, generate a self-signed cert or bind an existing one
        if ($_.Ssl) {
            $addr = (Resolve-PodeValue -Check $_.IsIPAddress -TrueValue $_.Address -FalseValue $_.HostName)
            $selfSigned = $_.Certificate.SelfSigned
            Set-PodeCertificate -Address $addr -Port $_port -Certificate $_.Certificate.Name -Thumbprint $_.Certificate.Thumbprint -SelfSigned:$selfSigned
        }

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

    # create the listener on http and/or https
    $listener = New-Object System.Net.HttpListener

    try
    {
        # start listening on defined endpoints
        $endpoints | ForEach-Object {
            $listener.Prefixes.Add($_.Prefix)
        }

        $listener.Start()
    }
    catch {
        $_ | Write-PodeErrorLog

        if ($null -ne $Listener) {
            if ($Listener.IsListening) {
                $Listener.Stop()
            }

            Close-PodeDisposable -Disposable $Listener -Close
        }

        throw $_.Exception
    }

    # script for listening out for incoming requests
    $listenScript = {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener,

            [Parameter(Mandatory=$true)]
            [int]
            $ThreadId
        )

        try
        {
            while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                # get request and response
                $context = (Wait-PodeTask -Task $Listener.GetContextAsync())

                try
                {
                    $request = $context.Request
                    $response = $context.Response

                    # reset event data
                    $WebEvent = @{
                        OnEnd = @()
                        Auth = @{}
                        Response = $response
                        Request = $request
                        Lockable = $PodeContext.Lockable
                        Path = ($request.RawUrl -isplit "\?")[0]
                        Method = $request.HttpMethod.ToLowerInvariant()
                        Protocol = $request.Url.Scheme
                        Endpoint = $request.Url.Authority
                        ContentType = $request.ContentType
                        ErrorType = $null
                        Cookies = $request.Cookies
                        PendingCookies = @{}
                        Streamed = $true
                    }

                    # set pode in server response header
                    Set-PodeServerHeader -AllowEmptyType

                    # 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 {
                    Set-PodeResponseStatus -Code 500 -Exception $_
                    $_ | Write-PodeErrorLog
                }

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

                # close response stream (check if exists, as closing the writer closes this stream on unix)
                if ($response.OutputStream) {
                    Close-PodeDisposable -Disposable $response.OutputStream -Close -CheckNetwork
                }
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
    }

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

    # script to keep web server listening until cancelled
    $waitScript = {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Listener
        )

        try
        {
            while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested)
            {
                Start-Sleep -Seconds 1
            }
        }
        catch [System.OperationCanceledException] {}
        catch {
            $_ | Write-PodeErrorLog
            throw $_.Exception
        }
        finally {
            if ($null -ne $Listener) {
                if ($Listener.IsListening) {
                    $Listener.Stop()
                }

                Close-PodeDisposable -Disposable $Listener -Close
            }
        }
    }

    Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }

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

    return @($endpoints.HostName)
}
src\Public\Authentication.ps1
<#
.SYNOPSIS
Create a new type of Authentication.

.DESCRIPTION
Create a new type of Authentication, which is used to parse the Request for user credentials for validating.

.PARAMETER Basic
If supplied, will use the inbuilt Basic Authentication credentials retriever.

.PARAMETER Encoding
The Encoding to use when decoding the Basic Authorization header.

.PARAMETER HeaderTag
The name of the type of Basic Authentication.

.PARAMETER Form
If supplied, will use the inbuilt Form Authentication credentials retriever.

.PARAMETER UsernameField
The name of the Username Field in the payload to retrieve the username.

.PARAMETER PasswordField
The name of the Password Field in the payload to retrieve the password.

.PARAMETER Custom
If supplied, will allow you to create a Custom Authentication credentials retriever.

.PARAMETER ScriptBlock
The ScriptBlock to retrieve user credentials.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Authentication type's ScriptBlock.

.EXAMPLE
$basic_auth = New-PodeAuthType -Basic

.EXAMPLE
$form_auth = New-PodeAuthType -Form -UsernameField 'Email'

.EXAMPLE
$custom_auth = New-PodeAuthType -Custom -ScriptBlock { /* logic */ }
#>
function New-PodeAuthType
{
    [CmdletBinding(DefaultParameterSetName='Basic')]
    [OutputType([hashtable])]
    param (
        [Parameter(ParameterSetName='Basic')]
        [switch]
        $Basic,

        [Parameter(ParameterSetName='Basic')]
        [string]
        $Encoding = 'ISO-8859-1',

        [Parameter(ParameterSetName='Basic')]
        [string]
        $HeaderTag = 'Basic',

        [Parameter(ParameterSetName='Form')]
        [switch]
        $Form,

        [Parameter(ParameterSetName='Form')]
        [string]
        $UsernameField = 'username',

        [Parameter(ParameterSetName='Form')]
        [string]
        $PasswordField = 'password',

        [Parameter(ParameterSetName='Custom')]
        [switch]
        $Custom,

        [Parameter(Mandatory=$true, ParameterSetName='Custom')]
        [ValidateScript({
            if (Test-IsEmpty $_) {
                throw "A non-empty ScriptBlock is required for the Custom authentication type"
            }

            return $true
        })]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName='Custom')]
        [hashtable]
        $ArgumentList
    )

    # configure the auth type
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'basic' {
            return @{
                ScriptBlock = (Get-PodeAuthBasicType)
                Arguments = @{
                    HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Basic')
                    Encoding = (Protect-PodeValue -Value $Encoding -Default 'ISO-8859-1')
                }
            }
        }

        'form' {
            return @{
                ScriptBlock = (Get-PodeAuthFormType)
                Arguments = @{
                    Fields = @{
                        Username = (Protect-PodeValue -Value $UsernameField -Default 'username')
                        Password = (Protect-PodeValue -Value $PasswordField -Default 'password')
                    }
                }
            }
        }

        'custom' {
            return @{
                ScriptBlock = $ScriptBlock
                Arguments = $ArgumentList
            }
        }
    }
}

<#
.SYNOPSIS
Adds a custom Authentication method for verifying users.

.DESCRIPTION
Adds a custom Authentication method for verifying users.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Type
The Type to use for retrieving credentials (From New-PodeAuthType).

.PARAMETER ScriptBlock
The ScriptBlock defining logic that retrieves and verifys a user.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Authentication's ScriptBlock.

.EXAMPLE
New-PodeAuthType -Form | Add-PodeAuth -Name 'Main' -ScriptBlock { /* logic */ }
#>
function Add-PodeAuth
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $Type,

        [Parameter(Mandatory=$true)]
        [ValidateScript({
            if (Test-IsEmpty $_) {
                throw "A non-empty ScriptBlock is required for the authentication method"
            }

            return $true
        })]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # ensure the name doesn't already exist
    if ($PodeContext.Server.Authentications.ContainsKey($Name)) {
        throw "Authentication method already defined: $($Name)"
    }

    # ensure the Type contains a scriptblock
    if (Test-IsEmpty $Type.ScriptBlock) {
        throw "The supplied Type for the '$($Name)' authentication method requires a valid ScriptBlock"
    }

    # add auth method to server
    $PodeContext.Server.Authentications[$Name] = @{
        Type = $Type
        ScriptBlock = $ScriptBlock
        Arguments = $ArgumentList
    }
}

<#
.SYNOPSIS
Adds the inbuilt Windows AD Authentication method for verifying users.

.DESCRIPTION
Adds the inbuilt Windows AD Authentication method for verifying users.

.PARAMETER Name
A unique Name for the Authentication method.

.PARAMETER Type
The Type to use for retrieving credentials (From New-PodeAuthType).

.PARAMETER Fqdn
A custom FQDN for the DNS of the AD you wish to authenticate against.

.PARAMETER Groups
An array of Group names to only allow access.

.PARAMETER Users
An array of Usernames to only allow access.

.PARAMETER NoGroups
If supplied, groups will not be retrieved for the user in AD.

.EXAMPLE
New-PodeAuthType -Form | Add-PodeAuthWindowsAd -Name 'WinAuth'

.EXAMPLE
New-PodeAuthType -Basic | Add-PodeAuthWindowsAd -Name 'WinAuth' -Groups @('Developers')

.EXAMPLE
New-PodeAuthType -Form | Add-PodeAuthWindowsAd -Name 'WinAuth' -NoGroups
#>
function Add-PodeAuthWindowsAd
{
    [CmdletBinding(DefaultParameterSetName='Groups')]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $Type,

        [Parameter()]
        [string]
        $Fqdn = $env:USERDNSDOMAIN,

        [Parameter(ParameterSetName='Groups')]
        [string[]]
        $Groups,

        [Parameter()]
        [string[]]
        $Users,

        [Parameter(ParameterSetName='NoGroups')]
        [switch]
        $NoGroups
    )

    # Check PowerShell/OS version
    $version = $PSVersionTable.PSVersion
    if ((Test-IsUnix) -or ($version.Major -eq 6 -and $version.Minor -eq 0)) {
        throw 'Windows AD authentication is currently only supported on Windows PowerShell, and Windows PowerShell Core v6.1+'
    }

    # ensure the name doesn't already exist
    if ($PodeContext.Server.Authentications.ContainsKey($Name)) {
        throw "Windows AD Authentication method already defined: $($Name)"
    }

    # ensure the Type contains a scriptblock
    if (Test-IsEmpty $Type.ScriptBlock) {
        throw "The supplied Type for the '$($Name)' Windows AD authentication method requires a valid ScriptBlock"
    }

    # add Windows AD auth method to server
    $PodeContext.Server.Authentications[$Name] = @{
        Type = $Type
        ScriptBlock = (Get-PodeAuthInbuiltMethod -Type WindowsAd)
        Arguments = @{
            Fqdn = $Fqdn
            Users = $Users
            Groups = $Groups
            NoGroups = $NoGroups
        }
    }
}

<#
.SYNOPSIS
Remove a specific Authentication method.

.DESCRIPTION
Remove a specific Authentication method.

.PARAMETER Name
The Name of the Authentication method.

.EXAMPLE
Remove-PodeAuth -Name 'Login'
#>
function Remove-PodeAuth
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name
    )

    $PodeContext.Server.Authentications.Remove($Name) | Out-Null
}

<#
.SYNOPSIS
Clear all defined Authentication methods.

.DESCRIPTION
Clear all defined Authentication methods.

.EXAMPLE
Clear-PodeAuth
#>
function Clear-PodeAuth
{
    [CmdletBinding()]
    param()

    $PodeContext.Server.Authentications.Clear()
}

<#
.SYNOPSIS
Returns Authentication Middleware that can be used globally, or an Routes.

.DESCRIPTION
Returns Authentication Middleware that can be used globally, or an Routes.

.PARAMETER Name
The Name of the Authentication method.

.PARAMETER FailureUrl
The URL to redirect to when authentication fails.

.PARAMETER FailureMessage
An override Message to throw when authentication fails.

.PARAMETER SuccessUrl
The URL to redirect to when authentication succeeds.

.PARAMETER EnableFlash
If supplied, error messages will be added as Flash messages.

.PARAMETER Sessionless
If supplied, authenticated users will not be stored in sessions, and sessions will not be used.

.PARAMETER AutoLogin
If supplied, navigating to a login page with a valid session will redirect to the SuccessUrl. Otherwise the login page will be displayed.

.PARAMETER Logout
If supplied, the current session will be purged, and the user will be redirected to the FailureUrl.

.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -Middleware (Get-PodeAuthMiddleware -Name 'Main') -ScriptBlock { /* logic */ }

.EXAMPLE
Get-PodeAuthMiddleware -Name 'BasicAuth' -Sessionless | Add-PodeMiddeware -Name 'GlobalAuth'

.EXAMPLE
Add-PodeRoute -Method Get -Path '/login' -Middleware (Get-PodeAuthMiddleware -Name 'Main' -SuccessUrl '/' -AutoLogin) -ScriptBlock { /* logic */ }
#>
function Get-PodeAuthMiddleware
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [switch]
        $EnableFlash,

        [switch]
        $Sessionless,

        [switch]
        $AutoLogin,

        [switch]
        $Logout
    )

    # ensure the auth method exists
    if (!$PodeContext.Server.Authentications.ContainsKey($Name)) {
        throw "Authentication method does not exist: $($Name)"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # create the options
    $options = @{
        Name = $Name
        Failure = @{
            Url = $FailureUrl
            Message = $FailureMessage
            FlashEnabled = $EnableFlash
        }
        Success = @{
            Url = $SuccessUrl
        }
        Sessionless = $Sessionless
        AutoLogin = $AutoLogin
        Logout = $Logout
    }

    # return the middleware
    return (Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options)
}
src\Public\Cookies.ps1
<#
.SYNOPSIS
Sets a cookie on the Response.

.DESCRIPTION
Sets a cookie on the Response using the "Set-Cookie" header. You can also set cookies to expire, or being signed.

.PARAMETER Name
The name of the cookie.

.PARAMETER Value
The value of the cookie.

.PARAMETER Secret
If supplied, the secret with which to sign the cookie.

.PARAMETER Duration
The duration, in seconds, before the cookie is expired.

.PARAMETER ExpiryDate
An explicit expiry date for the cookie.

.PARAMETER HttpOnly
Only allow the cookie to be used in browsers.

.PARAMETER Discard
Inform browsers to remove the cookie.

.PARAMETER Secure
Only allow the cookie on secure (HTTPS) connections.

.EXAMPLE
Set-PodeCookie -Name 'Views' -Value 2

.EXAMPLE
Set-PodeCookie -Name 'Views' -Value 2 -Secret 'hunter2'

.EXAMPLE
Set-PodeCookie -Name 'Views' -Value 2 -Duration 3600
#>
function Set-PodeCookie
{
    [CmdletBinding(DefaultParameterSetName='Duration')]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Value,

        [Parameter()]
        [string]
        $Secret,

        [Parameter(ParameterSetName='Duration')]
        [int]
        $Duration = 0,

        [Parameter(ParameterSetName='ExpiryDate')]
        [datetime]
        $ExpiryDate,

        [switch]
        $HttpOnly,

        [switch]
        $Discard,

        [switch]
        $Secure
    )

    # sign the value if we have a secret
    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $Value = (Invoke-PodeCookieSign -Value $Value -Secret $Secret)
    }

    # create a new cookie
    $cookie = [System.Net.Cookie]::new($Name, $Value)
    $cookie.Secure = $Secure
    $cookie.Discard = $Discard
    $cookie.HttpOnly = $HttpOnly

    if (!(Test-IsEmpty $ExpiryDate)) {
        $cookie.Expires = $ExpiryDate
    }
    elseif ($Duration -gt 0) {
        $cookie.Expires = [datetime]::UtcNow.AddSeconds($Duration)
    }

    # sets the cookie on the the response
    $WebEvent.PendingCookies[$cookie.Name] = $cookie
    Add-PodeHeader -Name 'Set-Cookie' -Value (ConvertTo-PodeCookieString -Cookie $cookie)
    return (ConvertTo-PodeCookie -Cookie $cookie)
}

<#
.SYNOPSIS
Retrieves a cookie from the Request.

.DESCRIPTION
Retrieves a cookie from the Request, with the option to supply a secret to unsign the cookie's value.

.PARAMETER Name
The name of the cookie to retrieve.

.PARAMETER Secret
The secret used to unsign the cookie's value.

.PARAMETER Raw
If supplied, the cookie returned will be the raw .NET Cookie object for manipulation.

.EXAMPLE
Get-PodeCookie -Name 'Views'

.EXAMPLE
Get-PodeCookie -Name 'Views' -Secret 'hunter2'
#>
function Get-PodeCookie
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $Raw
    )

    # get the cookie from the request
    $cookie = $WebEvent.Cookies[$Name]
    if (!$Raw) {
        $cookie = (ConvertTo-PodeCookie -Cookie $cookie)
    }

    if (($null -eq $cookie) -or [string]::IsNullOrWhiteSpace($cookie.Value)) {
        return $null
    }

    # if a secret was supplied, attempt to unsign the cookie
    if (![string]::IsNullOrWhiteSpace($Secret)) {
        $value = (Invoke-PodeCookieUnsign -Signature $cookie.Value -Secret $Secret)
        if (![string]::IsNullOrWhiteSpace($value)) {
            $cookie.Value = $value
        }
    }

    return $cookie
}

<#
.SYNOPSIS
Tests if a cookie exists on the Request.

.DESCRIPTION
Tests if a cookie exists on the Request.

.PARAMETER Name
The name of the cookie to test for on the Request.

.EXAMPLE
Test-PodeCookie -Name 'Views'
#>
function Test-PodeCookie
{
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    $cookie = $WebEvent.Cookies[$Name]
    return (($null -ne $cookie) -and ![string]::IsNullOrWhiteSpace($cookie.Value))
}

<#
.SYNOPSIS
Removes a cookie from the Response.

.DESCRIPTION
Removes a cookie from the Response, this is done by immediately expiring the cookie and flagging it for discard.

.PARAMETER Name
The name of the cookie to be removed.

.EXAMPLE
Remove-PodeCookie -Name 'Views'
#>
function Remove-PodeCookie
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    # get the cookie from the response - if it's not found, get it from the request
    $cookie = $WebEvent.PendingCookies[$Name]
    if ($null -eq $cookie) {
        $cookie = Get-PodeCookie -Name $Name -Raw
    }

    # remove the cookie from the response, and reset it to expire
    if ($null -ne $cookie) {
        $cookie.Discard = $true
        $cookie.Expires = [DateTime]::UtcNow.AddDays(-2)
        $WebEvent.PendingCookies[$cookie.Name] = $cookie
        Add-PodeHeader -Name 'Set-Cookie' -Value (ConvertTo-PodeCookieString -Cookie $cookie)
    }
}

<#
.SYNOPSIS
Tests if a cookie on the Request is validly signed.

.DESCRIPTION
Tests if a cookie on the Request is validly signed, by attempting to unsign it using some secret.

.PARAMETER Name
The name of the cookie to test.

.PARAMETER Secret
A secret to use for attempting to unsign the cookie's value.

.EXAMPLE
Test-PodeCookieSigned -Name 'Views' -Secret 'hunter2'
#>
function Test-PodeCookieSigned
{
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Secret
    )

    $cookie = $WebEvent.Cookies[$Name]
    if (($null -eq $cookie) -or [string]::IsNullOrWhiteSpace($cookie.Value)) {
        return $false
    }

    $value = (Invoke-PodeCookieUnsign -Signature $cookie.Value -Secret $Secret)
    return (![string]::IsNullOrWhiteSpace($value))
}

<#
.SYNOPSIS
Updates the exipry date of a cookie on the Response.

.DESCRIPTION
Updates the exipry date of a cookie on the Response. This can either be done by suppling a duration, or and explicit expiry date.

.PARAMETER Name
The name of the cookie to extend.

.PARAMETER Duration
The duration, in seconds, to extend the cookie's expiry.

.PARAMETER ExpiryDate
An explicit expiry date for the cookie.

.EXAMPLE
Update-PodeCookieExpiry -Name  'Views' -Duration 1800

.EXAMPLE
Update-PodeCookieExpiry -Name  'Views' -ExpiryDate ([datetime]::UtcNow.AddSeconds(1800))
#>
function Update-PodeCookieExpiry
{
    [CmdletBinding(DefaultParameterSetName='Duration')]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(ParameterSetName='Duration')]
        [int]
        $Duration = 0,

        [Parameter(ParameterSetName='ExpiryDate')]
        [datetime]
        $ExpiryDate
    )

    # get the cookie from the response - if it's not found, get it from the request
    $cookie = $WebEvent.PendingCookies[$Name]
    if ($null -eq $cookie) {
        $cookie = Get-PodeCookie -Name $Name -Raw
    }

    # extends the expiry on the cookie
    if (!(Test-IsEmpty $ExpiryDate)) {
        $cookie.Expires = $ExpiryDate
    }
    elseif ($Duration -gt 0) {
        $cookie.Expires = [datetime]::UtcNow.AddSeconds($Duration)
    }

    # sets the cookie on the the response
    $WebEvent.PendingCookies[$cookie.Name] = $cookie
    Add-PodeHeader -Name 'Set-Cookie' -Value (ConvertTo-PodeCookieString -Cookie $cookie)
    return (ConvertTo-PodeCookie -Cookie $cookie)
}

<#
.SYNOPSIS
Stores secrets that can be used to sign cookies.

.DESCRIPTION
Stores secrets that can be used to sign cookies. A global secret can be set for easier retrieval.

.PARAMETER Name
The name of the secret to store.

.PARAMETER Value
The value of the secret to store.

.PARAMETER Global
If flagged, the secret being stored will be set as the global secret.

.EXAMPLE
Set-PodeCookieSecret -Name 'my-secret' -Value 'shhhh!'

.EXAMPLE
Set-PodeCookieSecret -Value 'hunter2' -Global
#>
function Set-PodeCookieSecret
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='General')]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [string]
        $Value,

        [Parameter(ParameterSetName='Global')]
        [switch]
        $Global
    )

    if ($Global) {
        $Name = 'global'
    }

    $PodeContext.Server.Cookies.Secrets[$Name] = $Value
}

<#
.SYNOPSIS
Retrieves a stored secret value.

.DESCRIPTION
Retrieves a stored secret value.

.PARAMETER Name
The name of the secret to retrieve.

.PARAMETER Global
If flagged, will return the current global secret value.

.EXAMPLE
Get-PodeCookieSecret -Name 'my-secret'

.EXAMPLE
Get-PodeCookieSecret -Global
#>
function Get-PodeCookieSecret
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='General')]
        [string]
        $Name,

        [Parameter(ParameterSetName='Global')]
        [switch]
        $Global
    )

    if ($Global) {
        return ($PodeContext.Server.Cookies.Secrets['global'])
    }

    return ($PodeContext.Server.Cookies.Secrets[$Name])
}
src\Public\Core.ps1
<#
.SYNOPSIS
Starts a Pode Server with the supplied ScriptBlock.

.DESCRIPTION
Starts a Pode Server with the supplied ScriptBlock.

.PARAMETER ScriptBlock
The main logic for the Server.

.PARAMETER Interval
For 'Service' type Servers, will invoke the ScriptBlock every X seconds.

.PARAMETER Name
An optional name for the Server (intended for future ideas).

.PARAMETER Threads
The numbers of threads to use for Web and TCP servers.

.PARAMETER RootPath
An override for the Server's root path.

.PARAMETER Request
Intended for Serverless environments, this is Requests details that Pode can parse and use.

.PARAMETER Type
The server type, to define how Pode should run and deal with incoming Requests.

.PARAMETER DisableTermination
Disables the ability to terminate the Server.

.PARAMETER Browse
Open the web Server's default endpoint in your defualt browser.

.EXAMPLE
Start-PodeServer { /* logic */ }

.EXAMPLE
Start-PodeServer -Interval 10 { /* logic */ }

.EXAMPLE
Start-PodeServer -Request $LambdaInput -Type 'AwsLambda' { /* logic */ }
#>
function Start-PodeServer
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Interval = 0,

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Threads = 1,

        [Parameter()]
        [string]
        $RootPath,

        [Parameter()]
        $Request,

        [Parameter()]
        [ValidateSet('', 'AzureFunctions', 'AwsLambda', 'Pode')]
        [string]
        $Type = [string]::Empty,

        [switch]
        $DisableTermination,

        [switch]
        $Browse
    )

    # ensure the session is clean
    $PodeContext = $null
    $ShowDoneMessage = $true

    try {
        # configure the server's root path
        if (!(Test-IsEmpty $RootPath)) {
            $RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath
        }

        # create main context object
        $PodeContext = New-PodeContext -ScriptBlock $ScriptBlock `
            -Threads $Threads `
            -Interval $Interval `
            -ServerRoot (Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) `
            -ServerType $Type

        # set it so ctrl-c can terminate, unless serverless
        if (!$PodeContext.Server.IsServerless) {
            [Console]::TreatControlCAsInput = $true
        }

        # start the file monitor for interally restarting
        Start-PodeFileMonitor

        # start the server
        Start-PodeInternalServer -Request $Request -Browse:$Browse

        # at this point, if it's just a one-one off script, return
        if ([string]::IsNullOrWhiteSpace($PodeContext.Server.Type) -or $PodeContext.Server.IsServerless) {
            return
        }

        # sit here waiting for termination/cancellation, or to restart the server
        while (!(Test-PodeTerminationPressed -Key $key) -and !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) {
            Start-Sleep -Seconds 1

            # get the next key presses
            $key = Get-PodeConsoleKey

            # check for internal restart
            if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) {
                Restart-PodeInternalServer
            }
        }

        Write-Host 'Terminating...' -NoNewline -ForegroundColor Yellow
        $PodeContext.Tokens.Cancellation.Cancel()
    }
    catch {
        $ShowDoneMessage = $false
        throw
    }
    finally {
        # clean the runspaces and tokens
        Close-PodeServer -ShowDoneMessage:$ShowDoneMessage

        # clean the session
        $PodeContext = $null
    }
}

<#
.SYNOPSIS
The CLI for Pode, to initialise, build and start your Server.

.DESCRIPTION
The CLI for Pode, to initialise, build and start your Server.

.PARAMETER Action
The action to invoke on your Server.

.PARAMETER Dev
Supply when running "pode install", this will install any dev packages defined in your package.json.

.EXAMPLE
pode install -dev

.EXAMPLE
pode build

.EXAMPLE
pode start
#>
function Pode
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('init', 'test', 'start', 'install', 'build')]
        [Alias('a')]
        [string]
        $Action,

        [switch]
        [Alias('d')]
        $Dev
    )

    # default config file name and content
    $file = './package.json'
    $name = Split-Path -Leaf -Path $pwd
    $data = $null

    # default config data that's used to populate on init
    $map = @{
        'name' = $name;
        'version' = '1.0.0';
        'description' = '';
        'main' = './server.ps1';
        'scripts' = @{
            'start' = './server.ps1';
            'install' = 'yarn install --force --ignore-scripts --modules-folder pode_modules';
            "build" = 'psake';
            'test' = 'invoke-pester ./tests/*.ps1'
        };
        'author' = '';
        'license' = 'MIT';
    }

    # check and load config if already exists
    if (Test-Path $file) {
        $data = (Get-Content $file | ConvertFrom-Json)
    }

    # quick check to see if the data is required
    if ($Action -ine 'init') {
        if ($null -eq $data) {
            Write-Host 'package.json file not found' -ForegroundColor Red
            return
        }
        else {
            $actionScript = $data.scripts.$Action

            if ([string]::IsNullOrWhiteSpace($actionScript) -and $Action -ieq 'start') {
                $actionScript = $data.main
            }

            if ([string]::IsNullOrWhiteSpace($actionScript) -and $Action -ine 'install') {
                Write-Host "package.json does not contain a script for the $($Action) action" -ForegroundColor Yellow
                return
            }
        }
    }
    else {
        if ($null -ne $data) {
            Write-Host 'package.json already exists' -ForegroundColor Yellow
            return
        }
    }

    switch ($Action.ToLowerInvariant())
    {
        'init' {
            $v = Read-Host -Prompt "name ($($map.name))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.name = $v }

            $v = Read-Host -Prompt "version ($($map.version))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.version = $v }

            $map.description = Read-Host -Prompt "description"

            $v = Read-Host -Prompt "entry point ($($map.main))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.main = $v; $map.scripts.start = $v }

            $map.author = Read-Host -Prompt "author"

            $v = Read-Host -Prompt "license ($($map.license))"
            if (![string]::IsNullOrWhiteSpace($v)) { $map.license = $v }

            $map | ConvertTo-Json -Depth 10 | Out-File -FilePath $file -Encoding utf8 -Force
            Write-Host 'Success, saved package.json' -ForegroundColor Green
        }

        'test' {
            Invoke-PodePackageScript -ActionScript $actionScript
        }

        'start' {
            Invoke-PodePackageScript -ActionScript $actionScript
        }

        'install' {
            if ($Dev) {
                Install-PodeLocalModules -Modules $data.devModules
            }

            Install-PodeLocalModules -Modules $data.modules
            Invoke-PodePackageScript -ActionScript $actionScript
        }

        'build' {
            Invoke-PodePackageScript -ActionScript $actionScript
        }
    }
}

<#
.SYNOPSIS
Opens a Web Server up as a Desktop Application.

.DESCRIPTION
Opens a Web Server up as a Desktop Application.

.PARAMETER Title
The title of the Application's window.

.PARAMETER Icon
A path to an icon image for the Application.

.PARAMETER WindowState
The state the Application's window starts, such as Minimized.

.PARAMETER WindowStyle
The border style of the Application's window.

.PARAMETER ResizeMode
Specifies if the Application's window is resizable.

.PARAMETER Height
The height of the window.

.PARAMETER Width
The width of the window.

.PARAMETER EndpointName
The specific endpoint name to use, if you are listening on multiple endpoints.

.PARAMETER HideFromTaskbar
Stops the Application from appearing on the taskbar.

.EXAMPLE
Show-PodeGui -Title 'MyApplication' -WindowState 'Maximized'
#>
function Show-PodeGui
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Title,

        [Parameter()]
        [string]
        $Icon,

        [Parameter()]
        [ValidateSet('Normal', 'Maximized', 'Minimized')]
        [string]
        $WindowState = 'Normal',

        [Parameter()]
        [ValidateSet('None', 'SingleBorderWindow', 'ThreeDBorderWindow', 'ToolWindow')]
        [string]
        $WindowStyle = 'SingleBorderWindow',

        [Parameter()]
        [ValidateSet('CanResize', 'CanMinimize', 'NoResize')]
        [string]
        $ResizeMode = 'CanResize',

        [Parameter()]
        [int]
        $Height = 0,

        [Parameter()]
        [int]
        $Width = 0,

        [Parameter()]
        [string]
        $EndpointName,

        [switch]
        $HideFromTaskbar
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Show-PodeGui' -ThrowError

    # only valid for Windows PowerShell
    if ((Test-IsPSCore) -and ($PSVersionTable.PSVersion.Major -eq 6)) {
        throw 'Show-PodeGui is currently only available for Windows PowerShell, and PowerShell 7 on Windows'
    }

    # enable the gui and set general settings
    $PodeContext.Server.Gui.Enabled = $true
    $PodeContext.Server.Gui.Title = $Title
    $PodeContext.Server.Gui.ShowInTaskbar = !$HideFromTaskbar
    $PodeContext.Server.Gui.WindowState = $WindowState
    $PodeContext.Server.Gui.WindowStyle = $WindowStyle
    $PodeContext.Server.Gui.ResizeMode = $ResizeMode

    # set the window's icon path
    if (![string]::IsNullOrWhiteSpace($Icon)) {
        $PodeContext.Server.Gui.Icon = (Resolve-Path $Icon).Path
        if (!(Test-Path $PodeContext.Server.Gui.Icon)) {
            throw "Path to icon for GUI does not exist: $($PodeContext.Server.Gui.Icon)"
        }
    }

    # set the height of the window
    $PodeContext.Server.Gui.Height = $Height
    if ($PodeContext.Server.Gui.Height -le 0) {
        $PodeContext.Server.Gui.Height = 'auto'
    }

    # set the width of the window
    $PodeContext.Server.Gui.Width = $Width
    if ($PodeContext.Server.Gui.Width -le 0) {
        $PodeContext.Server.Gui.Width = 'auto'
    }

    # set the gui to use a specific listener
    $PodeContext.Server.Gui.EndpointName = $EndpointName

    if (![string]::IsNullOrWhiteSpace($PodeContext.Server.Gui.EndpointName)) {
        $found = ($PodeContext.Server.Endpoints | Where-Object {
            $_.Name -eq $PodeContext.Server.Gui.EndpointName
        } | Select-Object -First 1)

        if ($null -eq $found) {
            throw "Endpoint with name '$($EndpointName)' does not exist"
        }

        $PodeContext.Server.Gui.Endpoint = $found
    }
}

<#
.SYNOPSIS
Bind an endpoint to listen for incoming Requests.

.DESCRIPTION
Bind an endpoint to listen for incoming Requests. The endpoints can be HTTP, HTTPS, TCP or SMTP, with the option to bind certificates.

.PARAMETER Address
The IP/Hostname of the endpoint.

.PARAMETER Port
The Port number of the endpoint.

.PARAMETER Protocol
The protocol of the supplied endpoint.

.PARAMETER Certificate
A certificate name to find and bind onto HTTPS endpoints (Windows only).

.PARAMETER CertificateThumbprint
A certificate thumbprint to bind onto HTTPS endpoints (Windows only).

.PARAMETER CertificateFile
The path to a certificate that can be use to enable HTTPS (Cross-platform)

.PARAMETER CertificatePassword
The password for the certificate referenced in CertificateFile (Cross-platform)

.PARAMETER RawCertificate
The raw X509 certificate that can be use to enable HTTPS (Cross-platform)

.PARAMETER Name
An optional name for the endpoint, that can be used with other functions.

.PARAMETER RedirectTo
The Name of another Endpoint to automatically generate a redirect route for all traffic.

.PARAMETER Force
Ignore Adminstrator checks for non-localhost endpoints.

.PARAMETER SelfSigned
Create and bind a self-signed certifcate onto HTTPS endpoints (Windows only).

.EXAMPLE
Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http

.EXAMPLE
Add-PodeEndpoint -Address localhost -Protocol Smtp

.EXAMPLE
Add-PodeEndpoint -Address dev.pode.com -Port 8443 -Protocol Https -SelfSigned

.EXAMPLE
Add-PodeEndpoint -Address live.pode.com -Protocol Https -CertificateThumbprint '2A9467F7D3940243D6C07DE61E7FCCE292'
#>
function Add-PodeEndpoint
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Address = 'localhost',

        [Parameter()]
        [int]
        $Port = 0,

        [Parameter()]
        [ValidateSet('Http', 'Https', 'Smtp', 'Tcp', 'Ws', 'Wss')]
        [string]
        $Protocol,

        [Parameter(Mandatory=$true, ParameterSetName='CertName')]
        [string]
        $Certificate = $null,

        [Parameter(Mandatory=$true, ParameterSetName='CertThumb')]
        [string]
        $CertificateThumbprint = $null,

        [Parameter(Mandatory=$true, ParameterSetName='CertFile')]
        [string]
        $CertificateFile = $null,

        [Parameter(ParameterSetName='CertFile')]
        [string]
        $CertificatePassword = $null,

        [Parameter(Mandatory=$true, ParameterSetName='CertRaw')]
        [Parameter()]
        [X509Certificate]
        $RawCertificate = $null,

        [Parameter()]
        [string]
        $Name = $null,

        [Parameter()]
        [string]
        $RedirectTo = $null,

        [switch]
        $Force,

        [Parameter(ParameterSetName='CertSelf')]
        [switch]
        $SelfSigned
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeEndpoint' -ThrowError

    # parse the endpoint for host/port info
    $FullAddress = "$($Address):$($Port)"
    $_endpoint = Get-PodeEndpointInfo -Endpoint $FullAddress

    # if a name was supplied, check it is unique
    if (!(Test-IsEmpty $Name) -and
        (Get-PodeCount ($PodeContext.Server.Endpoints | Where-Object { $_.Name -eq $Name })) -ne 0)
    {
        throw "An endpoint with the name '$($Name)' has already been defined"
    }

    # new endpoint object
    $obj = @{
        Name = $Name
        Address = $null
        RawAddress = $FullAddress
        Port = $null
        IsIPAddress = $true
        HostName = 'localhost'
        Ssl = (@('https', 'wss') -icontains $Protocol)
        Protocol = $Protocol
        Certificate = @{
            Name = $Certificate
            Thumbprint = $CertificateThumbprint
            Raw = $RawCertificate
            SelfSigned = $SelfSigned
        }
    }

    # set the ip for the context
    $obj.Address = (Get-PodeIPAddress $_endpoint.Host)
    if (!(Test-PodeIPAddressLocalOrAny -IP $obj.Address)) {
        $obj.HostName = "$($obj.Address)"
    }

    $obj.IsIPAddress = (Test-PodeIPAddress -IP $obj.Address -IPOnly)

    # set the port for the context
    $obj.Port = $_endpoint.Port

    # if the address is non-local, then check admin privileges
    if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and !(Test-IsAdminUser)) {
        throw 'Must be running with administrator priviledges to listen on non-localhost addresses'
    }

    # has this endpoint been added before? (for http/https we can just not add it again)
    $exists = ($PodeContext.Server.Endpoints | Where-Object {
        ($_.Address -eq $obj.Address) -and ($_.Port -eq $obj.Port) -and ($_.Ssl -eq $obj.Ssl)
    } | Measure-Object).Count

    # if we're dealing with a certificate file, attempt to import it
    if ($PSCmdlet.ParameterSetName -ieq 'certfile') {
        # fail if protocol is not https
        if (@('https', 'wss') -inotcontains $Protocol) {
            throw "Certificate supplied for non-HTTPS/WSS endpoint"
        }

        $_path = Get-PodeRelativePath -Path $CertificateFile -JoinRoot -Resolve

        if ([string]::IsNullOrWhiteSpace($CertificatePassword)) {
            $obj.Certificate.Raw = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($_path)
        }
        else {
            $obj.Certificate.Raw = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($_path, $CertificatePassword)
        }

        # fail if the cert is expired
        if ($obj.Certificate.Raw.NotAfter -lt [datetime]::Now) {
            throw "The certificate '$($CertificateFile)' has expired: $($obj.Certificate.Raw.NotAfter)"
        }
    }

    if (!$exists) {
        # has an endpoint already been defined for smtp/tcp?
        if ((@('smtp', 'tcp') -icontains $Protocol) -and ($Protocol -ieq $PodeContext.Server.Type)) {
            throw "An endpoint for $($Protocol.ToUpperInvariant()) has already been defined"
        }

        # set server type, ensure we aren't trying to change the server's type
        if (@('ws', 'wss') -icontains $Protocol) {
            $PodeContext.Server.WebSockets.Enabled = $true
        }
        else {
            $_type = (Resolve-PodeValue -Check ($Protocol -ieq 'https') -TrueValue 'http' -FalseValue $Protocol)
            if (($_type -ieq 'http') -and ($PodeContext.Server.Type -ieq 'pode')) {
                $_type = 'pode'
            }

            if ([string]::IsNullOrWhiteSpace($PodeContext.Server.Type)) {
                $PodeContext.Server.Type = $_type
            }
            elseif ($PodeContext.Server.Type -ine $_type) {
                throw "Cannot add $($Protocol.ToUpperInvariant()) endpoint when already listening to $($PodeContext.Server.Type.ToUpperInvariant()) endpoints"
            }
        }

        # add the new endpoint
        $PodeContext.Server.Endpoints += $obj
    }

    # if RedirectTo is set, attempt to build a redirecting route
    if (![string]::IsNullOrWhiteSpace($RedirectTo)) {
        $redir_endpoint = ($PodeContext.Server.Endpoints | Where-Object { $_.Name -eq $RedirectTo } | Select-Object -First 1)

        # ensure the name exists
        if (Test-IsEmpty $redir_endpoint) {
            throw "An endpoint with the name '$($RedirectTo)' has not been defined for redirecting"
        }

        # build the redirect route
        Add-PodeRoute -Method * -Path * -Endpoint $obj.RawAddress -Protocol $obj.Protocol -ArgumentList $redir_endpoint -ScriptBlock {
            param($e, $endpoint)

            $addr = Resolve-PodeValue -Check (Test-PodeIPAddressAny -IP $endpoint.Address) -TrueValue 'localhost' -FalseValue $endpoint.Address
            Move-PodeResponseUrl -Address $addr -Port $endpoint.Port -Protocol $endpoint.Protocol
        }
    }
}

<#
.SYNOPSIS
Adds a new Timer with logic to periodically invoke.

.DESCRIPTION
Adds a new Timer with logic to periodically invoke, with options to only run a specific number of times.

.PARAMETER Name
The Name of the Timer.

.PARAMETER Interval
The number of seconds to periodically invoke the Timer's ScriptBlock.

.PARAMETER ScriptBlock
The script for the Timer.

.PARAMETER Limit
The number of times the Timer should be invoked before being removed. (If 0, it will run indefinitely)

.PARAMETER Skip
The number of "invokes" to skip before the Timer actually runs.

.PARAMETER ArgumentList
An array of arguments to supply to the Timer's ScriptBlock.

.PARAMETER OnStart
If supplied, the timer will trigger when the server starts.

.EXAMPLE
Add-PodeTimer -Name 'Hello' -Interval 10 -ScriptBlock { 'Hello, world!' | Out-Default }

.EXAMPLE
Add-PodeTimer -Name 'RunOnce' -Interval 1 -Limit 1 -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeTimer -Name 'RunAfter60secs' -Interval 10 -Skip 6 -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeTimer -Name 'Args' -Interval 2 -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeTimer
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [int]
        $Interval,

        [Parameter(Mandatory=$true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Limit = 0,

        [Parameter()]
        [int]
        $Skip = 0,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [switch]
        $OnStart
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeTimer' -ThrowError

    # ensure the timer doesn't already exist
    if ($PodeContext.Timers.ContainsKey($Name)) {
        throw "[Timer] $($Name): Timer already defined"
    }

    # is the interval valid?
    if ($Interval -le 0) {
        throw "[Timer] $($Name): Interval must be greater than 0"
    }

    # is the limit valid?
    if ($Limit -lt 0) {
        throw "[Timer] $($Name): Cannot have a negative limit"
    }

    # is the skip valid?
    if ($Skip -lt 0) {
        throw "[Timer] $($Name): Cannot have a negative skip value"
    }

    # calculate the next tick time (based on Skip)
    $NextTick = [DateTime]::Now.AddSeconds($Interval)
    if ($Skip -gt 1) {
        $NextTick = $NextTick.AddSeconds($Interval * $Skip)
    }

    # add the timer
    $PodeContext.Timers[$Name] = @{
        Name = $Name
        Interval = $Interval
        Limit = $Limit
        Count = 0
        Skip = $Skip
        Countable = ($Limit -gt 0)
        NextTick = $NextTick
        Script = $ScriptBlock
        Arguments = $ArgumentList
        OnStart = $OnStart
    }
}

<#
.SYNOPSIS
Removes a specific Timer.

.DESCRIPTION
Removes a specific Timer.

.PARAMETER Name
The Name of Timer to be removed.

.EXAMPLE
Remove-PodeTimer -Name 'SaveState'
#>
function Remove-PodeTimer
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name
    )

    $PodeContext.Timers.Remove($Name) | Out-Null
}

<#
.SYNOPSIS
Removes all Timers.

.DESCRIPTION
Removes all Timers.

.EXAMPLE
Clear-PodeTimers
#>
function Clear-PodeTimers
{
    [CmdletBinding()]
    param()

    $PodeContext.Timers.Clear()
}

<#
.SYNOPSIS
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.

.DESCRIPTION
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.

.PARAMETER Name
The Name of the Schedule.

.PARAMETER Cron
One, or an Array, of Cron Expressions to define when the Schedule should trigger.

.PARAMETER ScriptBlock
The script defining the Schedule's logic.

.PARAMETER Limit
The number of times the Schedule should trigger before being removed.

.PARAMETER StartTime
A DateTime for when the Schedule should start triggering.

.PARAMETER EndTime
A DateTime for when the Schedule should stop triggering, and be removed.

.PARAMETER ArgumentList
A hashtable of arguments to supply to the Schedule's ScriptBlock.

.PARAMETER OnStart
If supplied, the schedule will trigger when the server starts, regardless if the cron-expression matches the current time.

.EXAMPLE
Add-PodeSchedule -Name 'RunEveryMinute' -Cron '@minutely' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeSchedule -Name 'RunEveryTuesday' -Cron '0 0 * * TUE' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeSchedule -Name 'StartAfter2days' -Cron '@hourly' -StartTime [DateTime]::Now.AddDays(2) -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeSchedule -Name 'Args' -Cron '@minutely' -ScriptBlock { /* logic */ } -ArgumentList @{ Arg1 = 'value' }
#>
function Add-PodeSchedule
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [string[]]
        $Cron,

        [Parameter(Mandatory=$true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [int]
        $Limit = 0,

        [Parameter()]
        [DateTime]
        $StartTime,

        [Parameter()]
        [DateTime]
        $EndTime,

        [Parameter()]
        [hashtable]
        $ArgumentList,

        [switch]
        $OnStart
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeSchedule' -ThrowError

    # ensure the schedule doesn't already exist
    if ($PodeContext.Schedules.ContainsKey($Name)) {
        throw "[Schedule] $($Name): Schedule already defined"
    }

    # ensure the limit is valid
    if ($Limit -lt 0) {
        throw "[Schedule] $($Name): Cannot have a negative limit"
    }

    # ensure the start/end dates are valid
    if (($null -ne $EndTime) -and ($EndTime -lt [DateTime]::Now)) {
        throw "[Schedule] $($Name): The EndTime value must be in the future"
    }

    if (($null -ne $StartTime) -and ($null -ne $EndTime) -and ($EndTime -lt $StartTime)) {
        throw "[Schedule] $($Name): Cannot have a StartTime after the EndTime"
    }

    # add the schedule
    $PodeContext.Schedules[$Name] = @{
        Name = $Name
        StartTime = $StartTime
        EndTime = $EndTime
        Crons = (ConvertFrom-PodeCronExpressions -Expressions @($Cron))
        Limit = $Limit
        Count = 0
        Countable = ($Limit -gt 0)
        Script = $ScriptBlock
        Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{})
        OnStart = $OnStart
    }
}

<#
.SYNOPSIS
Removes a specific Schedule.

.DESCRIPTION
Removes a specific Schedule.

.PARAMETER Name
The Name of the Schedule to be removed.

.EXAMPLE
Remove-PodeSchedule -Name 'RenewToken'
#>
function Remove-PodeSchedule
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name
    )

    $PodeContext.Schedules.Remove($Name) | Out-Null
}

<#
.SYNOPSIS
Removes all Schedules.

.DESCRIPTION
Removes all Schedules.

.EXAMPLE
Clear-PodeSchedules
#>
function Clear-PodeSchedules
{
    [CmdletBinding()]
    param()

    $PodeContext.Schedules.Clear()
}

<#
.SYNOPSIS
Adds a new Middleware to be invoked before every Route, or certain Routes.

.DESCRIPTION
Adds a new Middleware to be invoked before every Route, or certain Routes.

.PARAMETER Name
The Name of the Middleware.

.PARAMETER ScriptBlock
The Script defining the logic of the Middleware.

.PARAMETER InputObject
A Middleware HashTable from New-PodeMiddleware, or from certain other functions that return Middleware as a HashTable.

.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.

.PARAMETER ArgumentList
An array of arguments to supply to the Middleware's ScriptBlock.

.EXAMPLE
Add-PodeMiddleware -Name 'BlockAgents' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeMiddleware -Name 'CheckEmailOnApi' -Route '/api/*' -ScriptBlock { /* logic */ }
#>
function Add-PodeMiddleware
{
    [CmdletBinding(DefaultParameterSetName='Script')]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ParameterSetName='Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory=$true, ParameterSetName='Input', ValueFromPipeline=$true)]
        [hashtable]
        $InputObject,

        [Parameter()]
        [string]
        $Route,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # ensure name doesn't already exist
    if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) {
        throw "[Middleware] $($Name): Middleware already defined"
    }

    # if it's a script - call New-PodeMiddleware
    if ($PSCmdlet.ParameterSetName -ieq 'script') {
        $InputObject = New-PodeMiddleware -ScriptBlock $ScriptBlock -Route $Route -ArgumentList $ArgumentList
    }
    else {
        if (![string]::IsNullOrWhiteSpace($Route)) {
            $Route = ConvertTo-PodeRouteRegex -Path $Route
        }

        $InputObject.Route = Protect-PodeValue -Value $Route -Default $InputObject.Route
        $InputObject.Options = Protect-PodeValue -Value $Options -Default $InputObject.Options
    }

    # ensure we have a script to run
    if (Test-IsEmpty $InputObject.Logic) {
        throw "[Middleware]: No logic supplied in ScriptBlock"
    }

    # set name, and override route/args
    $InputObject.Name = $Name

    # add the logic to array of middleware that needs to be run
    $PodeContext.Server.Middleware += $InputObject
}

<#
.SYNOPSIS
Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes.

.DESCRIPTION
Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes.

.PARAMETER ScriptBlock
The Script that defines the logic of the Middleware.

.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.

.PARAMETER ArgumentList
An array of arguments to supply to the Middleware's ScriptBlock.

.EXAMPLE
New-PodeMiddleware -ScriptBlock { /* logic */ } -ArgumentList 'Email' | Add-PodeMiddleware -Name 'CheckEmail'
#>
function New-PodeMiddleware
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string]
        $Route,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # if route is empty, set it to root
    $Route = ConvertTo-PodeRouteRegex -Path $Route

    # create the middleware hashtable from a scriptblock
    $HashTable = @{
        Route = $Route
        Logic = $ScriptBlock
        Arguments = $ArgumentList
    }

    if (Test-IsEmpty $HashTable.Logic) {
        throw "[Middleware]: No logic supplied in ScriptBlock"
    }

    # return the middleware, so it can be cached/added at a later date
    return $HashTable
}

<#
.SYNOPSIS
Removes a specific user defined Middleware.

.DESCRIPTION
Removes a specific user defined Middleware.

.PARAMETER Name
The Name of the Middleware to be removed.

.EXAMPLE
Remove-PodeMiddleware -Name 'Sessions'
#>
function Remove-PodeMiddleware
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    $PodeContext.Server.Middleware = @($PodeContext.Server.Middleware | Where-Object { $_.Name -ine $Name })
}

<#
.SYNOPSIS
Removes all user defined Middleware.

.DESCRIPTION
Removes all user defined Middleware.

.EXAMPLE
Clear-PodeMiddleware
#>
function Clear-PodeMiddleware
{
    [CmdletBinding()]
    param()

    $PodeContext.Server.Middleware = @()
}
src\Public\Flash.ps1
<#
.SYNOPSIS
Appends a message to the current flash messages stored in the session.

.DESCRIPTION
Appends a message to the current flash messages stored in the session for the supplied name.
The messages per name are stored as an array.

.PARAMETER Name
The name of the flash message to be appended.

.PARAMETER Message
The message to append.

.EXAMPLE
Add-PodeFlashMessage -Name 'error' -Message 'There was an error'
#>
function Add-PodeFlashMessage
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

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

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use Flash messages'
    }

    # append the message against the key
    if ($null -eq $WebEvent.Session.Data.Flash) {
        $WebEvent.Session.Data.Flash = @{}
    }

    if ($null -eq $WebEvent.Session.Data.Flash[$Name]) {
        $WebEvent.Session.Data.Flash[$Name] = @($Message)
    }
    else {
        $WebEvent.Session.Data.Flash[$Name] += @($Message)
    }
}

<#
.SYNOPSIS
Clears all flash messages.

.DESCRIPTION
Clears all of the flash messages currently stored in the session.

.EXAMPLE
Clear-PodeFlashMessages
#>
function Clear-PodeFlashMessages
{
    [CmdletBinding()]
    param()

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use Flash messages'
    }

    # clear all keys
    if ($null -ne $WebEvent.Session.Data.Flash) {
        $WebEvent.Session.Data.Flash = @{}
    }
}

<#
.SYNOPSIS
Returns all flash messages stored against a name, and the clears the messages.

.DESCRIPTION
Returns all of the flash messages, as an array, currently stored for the name within the session.
Once retrieved, the messages are removed from storage.

.PARAMETER Name
The name of the flash messages to return.

.EXAMPLE
Get-PodeFlashMessage -Name 'error'
#>
function Get-PodeFlashMessage
{
    [CmdletBinding()]
    [OutputType([string[]])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use Flash messages'
    }

    # retrieve messages from session, then delete it
    if ($null -eq $WebEvent.Session.Data.Flash) {
        return @()
    }

    $v = @($WebEvent.Session.Data.Flash[$Name])
    $WebEvent.Session.Data.Flash.Remove($Name)

    if (Test-IsEmpty $v) {
        return @()
    }

    return @($v)
}

<#
.SYNOPSIS
Returns all of the names for each of the messages currently being stored.

.DESCRIPTION
Returns all of the names for each of the messages currently being stored. This does not clear the messages.

.EXAMPLE
Get-PodeFlashMessageNames
#>
function Get-PodeFlashMessageNames
{
    [CmdletBinding()]
    [OutputType([string[]])]
    param()

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use Flash messages'
    }

    # return list of all current keys
    if ($null -eq $WebEvent.Session.Data.Flash) {
        return @()
    }

    return @($WebEvent.Session.Data.Flash.Keys)
}

<#
.SYNOPSIS
Removes flash messages for the supplied name currently being stored.

.DESCRIPTION
Removes flash messages for the supplied name currently being stored.

.PARAMETER Name
The name of the flash messages to remove.

.EXAMPLE
Remove-PodeFlashMessage -Name 'error'
#>
function Remove-PodeFlashMessage
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use Flash messages'
    }

    # remove key from flash messages
    if ($null -ne $WebEvent.Session.Data.Flash) {
        $WebEvent.Session.Data.Flash.Remove($Name)
    }
}

<#
.SYNOPSIS
Tests if there are any flash messages currently being stored for a supplied name.

.DESCRIPTION
Tests if there are any flash messages currently being stored for a supplied name.

.PARAMETER Name
The name of the flash message to check.

.EXAMPLE
Test-PodeFlashMessage -Name 'error'
#>
function Test-PodeFlashMessage
{
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use Flash messages'
    }

    # return if a key exists as a flash message
    if ($null -eq $WebEvent.Session.Data.Flash) {
        return $false
    }

    return $WebEvent.Session.Data.Flash.ContainsKey($Name)
}
src\Public\Handlers.ps1
<#
.SYNOPSIS
Adds a Handler of a specific Type.

.DESCRIPTION
Adds a Handler of a specific Type.

.PARAMETER Type
The Type of the Handler.

.PARAMETER Name
The Name of the Handler.

.PARAMETER ScriptBlock
The ScriptBlock for the Handler's main logic.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Handler's main logic.

.PARAMETER ArgumentList
An array of arguments to supply to the Handler's ScriptBlock.

.EXAMPLE
Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeHandler -Type Service -Name 'Looper' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeHandler
{
    [CmdletBinding(DefaultParameterSetName='Script')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Service', 'Smtp', 'Tcp')]
        [string]
        $Type,

        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ParameterSetName='Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeHandler' -ThrowError

    # ensure handler isn't already set
    if ($PodeContext.Server.Handlers[$Type].ContainsKey($Name)) {
        throw "[$($Type)] $($Name): Handler already defined"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        # if file doesn't exist, error
        if (!(Test-PodePath -Path $FilePath -NoStatus)) {
            throw "[$($Type)] $($Name): The FilePath does not exist: $($FilePath)"
        }

        # if the path is a wildcard or directory, error
        if (!(Test-PodePathIsFile -Path $FilePath -FailOnWildcard)) {
            throw "[$($Type)] $($Name): The FilePath cannot be a wildcard or directory: $($FilePath)"
        }

        $ScriptBlock = [scriptblock](Use-PodeScript -Path $FilePath)
    }

    # add the handler
    $PodeContext.Server.Handlers[$Type][$Name] += @(@{
        Logic = $ScriptBlock
        Arguments = $ArgumentList
    })
}

<#
.SYNOPSIS
Remove a specific Handler.

.DESCRIPTION
Remove a specific Handler.

.PARAMETER Type
The type of the Handler to be removed.

.PARAMETER Name
The name of the Handler to be removed.

.EXAMPLE
Remove-PodeHandler -Type Smtp -Name 'Main'
#>
function Remove-PodeHandler
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Service', 'Smtp', 'Tcp')]
        [string]
        $Type,

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

    # ensure handler does exist
    if (!$PodeContext.Server.Handlers[$Type].ContainsKey($Name)) {
        return
    }

    # remove the handler
    $PodeContext.Server.Handlers[$Type].Remove($Name) | Out-Null
}

<#
.SYNOPSIS
Removes all added Handlers, or Handlers of a specific Type.

.DESCRIPTION
Removes all added Handlers, or Handlers of a specific Type.

.PARAMETER Type
The Type of Handlers to remove.

.EXAMPLE
Clear-PodeHandlers -Type Smtp
#>
function Clear-PodeHandlers
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet('', 'Service', 'Smtp', 'Tcp')]
        [string]
        $Type
    )

    if (![string]::IsNullOrWhiteSpace($Type)) {
        $PodeContext.Server.Handlers[$Type].Clear()
    }
    else {
        $PodeContext.Server.Handlers.Keys.Clone() | ForEach-Object {
            $PodeContext.Server.Handlers[$_].Clear()
        }
    }
}
tools\ChocolateyInstall.ps1
$ErrorActionPreference = 'Stop'


# create the module directory, and copy files over
function Install-PodeModule($path, $version)
{
    # Create module
    $path = Join-Path $path 'Pode'
    if (![string]::IsNullOrWhiteSpace($version)) {
        $path = Join-Path $path $version
    }

    if (!(Test-Path $path))
    {
        Write-Host "Creating module directory: $($path)"
        New-Item -ItemType Directory -Path $path -Force | Out-Null
        if (!$?) {
            throw "Failed to create: $path"
        }
    }

    # Copy contents to module
    Write-Host 'Copying scripts to module path'

    try
    {
        Push-Location (Join-Path $env:ChocolateyPackageFolder 'src')

        New-Item -ItemType Directory -Path (Join-Path $path 'Private') -Force | Out-Null
        New-Item -ItemType Directory -Path (Join-Path $path 'Public') -Force | Out-Null
        New-Item -ItemType Directory -Path (Join-Path $path 'Misc') -Force | Out-Null

        Copy-Item -Path ./Private/* -Destination (Join-Path $path 'Private') -Force | Out-Null
        Copy-Item -Path ./Public/* -Destination (Join-Path $path 'Public') -Force | Out-Null
        Copy-Item -Path ./Misc/* -Destination (Join-Path $path 'Misc') -Force | Out-Null
        Copy-Item -Path ./Pode.psm1 -Destination $path -Force | Out-Null
        Copy-Item -Path ./Pode.psd1 -Destination $path -Force | Out-Null
        Copy-Item -Path ./LICENSE.txt -Destination $path -Force | Out-Null
    }
    finally {
        Pop-Location
    }
}



# Determine which Program Files path to use
$progFiles = [string]$env:ProgramFiles

# Install PS Module
# Set the module path
$modulePath = Join-Path $progFiles (Join-Path 'WindowsPowerShell' 'Modules')

# Check to see if Modules path is in PSModulePaths
$psModules = $env:PSModulePath
if (!$psModules.Contains($modulePath))
{
    Write-Host 'Adding module path to PSModulePaths'
    $psModules += ";$modulePath"
    Install-ChocolateyEnvironmentVariable -VariableName 'PSModulePath' -VariableValue $psModules -VariableType Machine
    $env:PSModulePath = $psModules
}

# create the module
if ($PSVersionTable.PSVersion.Major -ge 5) {
    Install-PodeModule $modulePath '1.2.0'
}
else {
    Install-PodeModule $modulePath
}


# Install PS-Core Module
$def = (Get-Command pwsh -ErrorAction SilentlyContinue).Definition

if (![string]::IsNullOrWhiteSpace($def))
{
    # Set the module path
    $modulePath = Join-Path $progFiles (Join-Path 'PowerShell' 'Modules')

    # create the module
    Install-PodeModule $modulePath '1.2.0'
}
tools\ChocolateyUninstall.ps1
function Remove-PodeModule($path)
{
    $path = Join-Path $path 'Pode'
    if (Test-Path $path)
    {
        Write-Host "Deleting module directory: $($path)"
        Remove-Item -Path $path -Recurse -Force | Out-Null
        if (!$?) {
            throw "Failed to delete: $path"
        }
    }
}



# Determine which Program Files path to use
$progFiles = [string]$env:ProgramFiles

# Remove PS Module
# Set the module path
$modulePath = Join-Path $progFiles (Join-Path 'WindowsPowerShell' 'Modules')

# Delete module
Remove-PodeModule $modulePath


# Remove PS-Core Module
$def = (Get-Command pwsh -ErrorAction SilentlyContinue).Definition

if (![string]::IsNullOrWhiteSpace($def))
{
    # Set the module path
    $modulePath = Join-Path $progFiles (Join-Path 'PowerShell' 'Modules')

    # Delete module
    Remove-PodeModule $modulePath
}
src\VERIFICATION.txt
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy.

This embedded PowerShell module is packaged and distributed by the author.

The contents of which can be found on the releases pages at <https://github.com/Badgerati/Pode/releases>.

To verify contents, either:
1. Compare the Checksum on the release notes against the Module's source.
2. Download the zip from the release, run 'checksum -t sha256' on it, and compare.
src\Public\Utilities.ps1
<#
.SYNOPSIS
Waits for a task to finish, and returns a result if there is one.

.DESCRIPTION
Waits for a task to finish, and returns a result if there is one.

.PARAMETER Task
The task to wait on.

.EXAMPLE
$context = Wait-PodeTask -Task $listener.GetContextAsync()
#>
function Wait-PodeTask
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true)]
        [System.Threading.Tasks.Task]
        $Task
    )

    # is there a cancel token to supply?
    if (($null -eq $PodeContext) -or ($null -eq $PodeContext.Tokens.Cancellation.Token)) {
        $Task.Wait()
    }
    else {
        $Task.Wait($PodeContext.Tokens.Cancellation.Token)
    }

    # only return a value if the result has one
    if ($null -ne $Task.Result) {
        return $Task.Result
    }
}

<#
.SYNOPSIS
Dispose and close streams, tokens, and other Disposables.

.DESCRIPTION
Dispose and close streams, tokens, and other Disposables.

.PARAMETER Disposable
The Disposable object to dispose and close.

.PARAMETER Close
Should the Disposable also be closed, as well as disposed?

.PARAMETER CheckNetwork
If an error is thrown, check the reason - if it's network related ignore the error.

.EXAMPLE
Close-PodeDisposable -Disposable $stream -Close
#>
function Close-PodeDisposable
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [System.IDisposable]
        $Disposable,

        [switch]
        $Close,

        [switch]
        $CheckNetwork
    )

    if ($null -eq $Disposable) {
        return
    }

    try {
        if ($Close) {
            $Disposable.Close()
        }
    }
    catch [exception] {
        if ($CheckNetwork -and (Test-PodeValidNetworkFailure $_.Exception)) {
            return
        }

        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        $Disposable.Dispose()
    }
}

<#
.SYNOPSIS
Places a temporary lock on a object while a ScriptBlock is invoked.

.DESCRIPTION
Places a temporary lock on a object while a ScriptBlock is invoked.

.PARAMETER Object
The object to lock.

.PARAMETER ScriptBlock
The ScriptBlock to invoke.

.PARAMETER Return
If supplied, any values from the ScriptBlock will be returned.

.EXAMPLE
Lock-PodeObject -Object $SomeArray -ScriptBlock { /* logic */ }

.EXAMPLE
$result = (Lock-PodeObject -Return -Object $SomeArray -ScriptBlock { /* logic */ })
#>
function Lock-PodeObject
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [object]
        $Object,

        [Parameter(Mandatory=$true)]
        [scriptblock]
        $ScriptBlock,

        [switch]
        $Return
    )

    if ($null -eq $Object) {
        return
    }

    if ($Object -is [valuetype]) {
        throw 'Cannot lock value types'
    }

    $locked = $false

    try {
        [System.Threading.Monitor]::Enter($Object.SyncRoot)
        $locked = $true

        if ($null -ne $ScriptBlock) {
            if ($Return) {
                return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return)
            }
            else {
                Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure
            }
        }
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        if ($locked) {
            [System.Threading.Monitor]::Pulse($Object.SyncRoot)
            [System.Threading.Monitor]::Exit($Object.SyncRoot)
        }
    }
}

<#
.SYNOPSIS
Returns the literal path of the server.

.DESCRIPTION
Returns the literal path of the server.

.EXAMPLE
$path = Get-PodeServerPath
#>
function Get-PodeServerPath
{
    [CmdletBinding()]
    [OutputType([string])]
    param()

    return $PodeContext.Server.Root
}

<#
.SYNOPSIS
Starts a Stopwatch on some ScriptBlock, and outputs the duration at the end.

.DESCRIPTION
Starts a Stopwatch on some ScriptBlock, and outputs the duration at the end.

.PARAMETER Name
The name of the Stopwatch.

.PARAMETER ScriptBlock
The ScriptBlock to time.

.EXAMPLE
Start-PodeStopwatch -Name 'ReadFile' -ScriptBlock { $content = Get-Content './file.txt' }
#>
function Start-PodeStopwatch
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [scriptblock]
        $ScriptBlock
    )

    try {
        $watch = [System.Diagnostics.Stopwatch]::StartNew()
        . $ScriptBlock
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        $watch.Stop()
        Out-Default -InputObject "[Stopwatch]: $($watch.Elapsed) [$($Name)]"
    }
}

<#
.SYNOPSIS
Like the "using" keyword in .NET. Allows you to use a Stream and then disposes of it.

.DESCRIPTION
Like the "using" keyword in .NET. Allows you to use a Stream and then disposes of it.

.PARAMETER Stream
The Stream to use and then dispose.

.PARAMETER ScriptBlock
The ScriptBlock to invoke. It will be supplied the Stream.

.EXAMPLE
$content = (Use-PodeStream -Stream $stream -ScriptBlock { return $args[0].ReadToEnd() })
#>
function Use-PodeStream
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true)]
        [System.IDisposable]
        $Stream,

        [Parameter(Mandatory=$true)]
        [scriptblock]
        $ScriptBlock
    )

    try {
        return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $Stream -Return -NoNewClosure)
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
    finally {
        $Stream.Dispose()
    }
}

<#
.SYNOPSIS
Loads a script, by dot-sourcing, at the supplied path.

.DESCRIPTION
Loads a script, by dot-sourcing, at the supplied path. If the path is relative, the server's path is prepended.

.PARAMETER Path
The path, literal or relative to the server, to some script.

.EXAMPLE
Use-PodeScript -Path './scripts/tools.ps1'
#>
function Use-PodeScript
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    # if path is '.', replace with server root
    $_path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve

    # we have a path, if it's a directory/wildcard then loop over all files
    if (![string]::IsNullOrWhiteSpace($_path)) {
        $_paths = Get-PodeWildcardFiles -Path $Path -Wildcard '*.ps1'
        if (!(Test-IsEmpty $_paths)) {
            foreach ($_path in $_paths) {
                Use-PodeScript -Path $_path
            }

            return
        }
    }

    # check if the path exists
    if (!(Test-PodePath $_path -NoStatus)) {
        throw "The script path does not exist: $(Protect-PodeValue -Value $_path -Default $Path)"
    }

    # dot-source the script
    . $_path
}

<#
.SYNOPSIS
Returns the loaded configuration of the server.

.DESCRIPTION
Returns the loaded configuration of the server.

.EXAMPLE
$s = Get-PodeConfig
#>
function Get-PodeConfig
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    return $PodeContext.Server.Configuration
}

<#
.SYNOPSIS
Adds a ScriptBlock as Endware to run at the end of each web Request.

.DESCRIPTION
Adds a ScriptBlock as Endware to run at the end of each web Request.

.PARAMETER ScriptBlock
The ScriptBlock to add. It will be supplied the current web event.

.PARAMETER ArgumentList
An array of arguments to supply to the Endware's ScriptBlock.

.EXAMPLE
Add-PodeEndware -ScriptBlock { /* logic */ }
#>
function Add-PodeEndware
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # add the scriptblock to array of endware that needs to be run
    $PodeContext.Server.Endware += @{
        Logic = $ScriptBlock
        Arguments = $ArgumentList
    }
}

<#
.SYNOPSIS
Imports a Module into the current, and all runspaces that Pode uses.

.DESCRIPTION
Imports a Module into the current, and all runspaces that Pode uses. Modules can also be imported from the ps_modules directory.

.PARAMETER Name
The name of a globally installed Module, or one within the ps_modules directory, to import.

.PARAMETER Path
The path, literal or relative, to a Module to import.

.PARAMETER Now
Import the Module now, into the current runspace.

.EXAMPLE
Import-PodeModule -Name IISManager

.EXAMPLE
Import-PodeModule -Path './modules/utilities.psm1'
#>
function Import-PodeModule
{
    [CmdletBinding(DefaultParameterSetName='Name')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Name')]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ParameterSetName='Path')]
        [string]
        $Path,

        [switch]
        $Now
    )

    # get the path of a module, or import modules on mass
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'name' {
            $modulePath = Join-PodeServerRoot -Folder (Join-PodePaths @('ps_modules', $Name))
            if ([string]::IsNullOrWhiteSpace($modulePath)) {
                $Path = (Get-ChildItem (Join-PodePaths @($modulePath, '*', "$($Name).ps*1")) -Recurse -Force | Select-Object -First 1).FullName
            }
            else {
                $Path = (Get-Module -Name $Name -ListAvailable | Select-Object -First 1).Path
            }
        }

        'path' {
            $Path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve
            $paths = Get-PodeWildcardFiles -Path $Path -Wildcard '*.ps*1'
            if (!(Test-IsEmpty $paths)) {
                foreach ($_path in $paths) {
                    Import-PodeModule -Path $_path -Now:$Now
                }

                return
            }
        }
    }

    # if it's still empty, error
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw "Failed to import module: $(Protect-PodeValue -Value $Path -Default $Name)"
    }

    # check if the path exists
    if (!(Test-PodePath $Path -NoStatus)) {
        throw "The module path does not exist: $(Protect-PodeValue -Value $Path -Default $Name)"
    }

    # import the module into the runspace state
    $PodeContext.RunspaceState.ImportPSModule($Path)

    # import the module now, if specified
    if ($Now) {
        Write-Verbose "Importing module now: $($Path)"
        Import-Module $Path -Force -DisableNameChecking -Scope Global -ErrorAction Stop | Out-Null
    }
}

<#
.SYNOPSIS
Imports a SnapIn into the current, and all runspaces that Pode uses.

.DESCRIPTION
Imports a SnapIn into the current, and all runspaces that Pode uses.

.PARAMETER Name
The name of a SnapIn to import.

.PARAMETER Now
Import the SnapIn now, into the current runspace.

.EXAMPLE
Import-PodeSnapIn -Name 'WDeploySnapin3.0'
#>
function Import-PodeSnapIn
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [switch]
        $Now
    )

    # if non-windows or core, fail
    if ((Test-IsPSCore) -or (Test-IsUnix)) {
        throw 'SnapIns are only supported on Windows PowerShell'
    }

    # import the snap-in into the runspace state
    $exp = $null
    $PodeContext.RunspaceState.ImportPSSnapIn($Name, ([ref]$exp))

    # import the snap-in now, if specified
    if ($Now) {
        Add-PSSnapin -Name $Name | Out-Null
    }
}

<#
.SYNOPSIS
Protects a value, by returning a default value is the main one is null/empty.

.DESCRIPTION
Protects a value, by returning a default value is the main one is null/empty.

.PARAMETER Value
The main value to use.

.PARAMETER Default
A default value to return should the main value be null/empty.

.EXAMPLE
$Name = Protect-PodeValue -Value $Name -Default 'Rick'
#>
function Protect-PodeValue
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter()]
        $Value,

        [Parameter()]
        $Default
    )

    return (Resolve-PodeValue -Check (Test-IsEmpty $Value) -TrueValue $Default -FalseValue $Value)
}

<#
.SYNOPSIS
Resolves a query, and returns a value based on the response.

.DESCRIPTION
Resolves a query, and returns a value based on the response.

.PARAMETER Check
The query, or variable, to evalulate.

.PARAMETER TrueValue
The value to use if evaluated to True.

.PARAMETER FalseValue
The value to use if evaluated to False.

.EXAMPLE
$Port = Resolve-PodeValue -Check $AllowSsl -TrueValue 443 -FalseValue -80
#>
function Resolve-PodeValue
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true)]
        [bool]
        $Check,

        [Parameter()]
        $TrueValue,

        [Parameter()]
        $FalseValue
    )

    if ($Check) {
        return $TrueValue
    }

    return $FalseValue
}

<#
.SYNOPSIS
Invokes a ScriptBlock.

.DESCRIPTION
Invokes a ScriptBlock, supplying optional arguments, splatting, and returning any optional values.

.PARAMETER ScriptBlock
The ScriptBlock to invoke.

.PARAMETER Arguments
Any arguments that should be supplied to the ScriptBlock.

.PARAMETER Scoped
Run the ScriptBlock in a scoped context.

.PARAMETER Return
Return any values that the ScriptBlock may return.

.PARAMETER Splat
Spat the argument onto the ScriptBlock.

.PARAMETER NoNewClosure
Don't create a new closure before invoking the ScriptBlock.

.EXAMPLE
Invoke-PodeScriptBlock -ScriptBlock { Write-Host 'Hello!' }

.EXAMPLE
Invoke-PodeScriptBlock -Arguments 'Morty' -ScriptBlock { /* logic */ }
#>
function Invoke-PodeScriptBlock
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        $Arguments = $null,

        [switch]
        $Scoped,

        [switch]
        $Return,

        [switch]
        $Splat,

        [switch]
        $NoNewClosure
    )

    if ($PodeContext.Server.IsServerless) {
        $NoNewClosure = $true
    }

    if (!$NoNewClosure) {
        $ScriptBlock = ($ScriptBlock).GetNewClosure()
    }

    if ($Scoped) {
        if ($Splat) {
            $result = (& $ScriptBlock @Arguments)
        }
        else {
            $result = (& $ScriptBlock $Arguments)
        }
    }
    else {
        if ($Splat) {
            $result = (. $ScriptBlock @Arguments)
        }
        else {
            $result = (. $ScriptBlock $Arguments)
        }
    }

    if ($Return) {
        return $result
    }
}

<#
.SYNOPSIS
Tests if a value is empty - the value can be of any type.

.DESCRIPTION
Tests if a value is empty - the value can be of any type.

.PARAMETER Value
The value to test.

.EXAMPLE
if (Test-IsEmpty @{}) { /* logic */ }
#>
function Test-IsEmpty
{
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter()]
        $Value
    )

    if ($null -eq $Value) {
        return $true
    }

    if ($Value -is [string]) {
        return [string]::IsNullOrWhiteSpace($Value)
    }

    if ($Value -is [array]) {
        return ($Value.Length -eq 0)
    }

    if ($Value -is [hashtable]) {
        return ($Value.Count -eq 0)
    }

    if ($Value -is [scriptblock]) {
        return ([string]::IsNullOrWhiteSpace($Value.ToString()))
    }

    if ($Value -is [valuetype]) {
        return $false
    }

    return ([string]::IsNullOrWhiteSpace($Value) -or ((Get-PodeCount $Value) -eq 0))
}

<#
.SYNOPSIS
Tests if the the current session is running in PowerShell Core.

.DESCRIPTION
Tests if the the current session is running in PowerShell Core.

.EXAMPLE
if (Test-IsPSCore) { /* logic */ }
#>
function Test-IsPSCore
{
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    return (Get-PodePSVersionTable).PSEdition -ieq 'core'
}

<#
.SYNOPSIS
Tests if the current OS is Unix.

.DESCRIPTION
Tests if the current OS is Unix.

.EXAMPLE
if (Test-IsUnix) { /* logic */ }
#>
function Test-IsUnix
{
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    return (Get-PodePSVersionTable).Platform -ieq 'unix'
}

<#
.SYNOPSIS
Tests if the current OS is Windows.

.DESCRIPTION
Tests if the current OS is Windows.

.EXAMPLE
if (Test-IsWindows) { /* logic */ }
#>
function Test-IsWindows
{
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    $v = Get-PodePSVersionTable
    return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop'))
}

<#
.SYNOPSIS
Outputs an object to the main Host.

.DESCRIPTION
Due to Pode's use of runspaces, this will output a given object back to the main Host.

.PARAMETER InputObject
The object to output.

.EXAMPLE
'Hello, world!' | Out-PodeHost

.EXAMPLE
@{ Name = 'Rick' } | Out-PodeHost
#>
function Out-PodeHost
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [object]
        $InputObject
    )

    $InputObject | Out-Default
}
src\Public\State.ps1
<#
.SYNOPSIS
Sets an object within the shared state.

.DESCRIPTION
Sets an object within the shared state.

.PARAMETER Name
The name of the state object.

.PARAMETER Value
The value to set in the state.

.EXAMPLE
Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' }
#>
function Set-PodeState
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(ValueFromPipeline=$true)]
        [object]
        $Value
    )

    if ($null -eq $PodeContext.Server.State) {
        throw "Pode has not been initialised"
    }

    $PodeContext.Server.State[$Name] = $Value
    return $Value
}

<#
.SYNOPSIS
Retrieves some state object from the shared state.

.DESCRIPTION
Retrieves some state object from the shared state.

.PARAMETER Name
The name of the state object.

.EXAMPLE
Get-PodeState -Name 'Data'
#>
function Get-PodeState
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    if ($null -eq $PodeContext.Server.State) {
        throw "Pode has not been initialised"
    }

    return $PodeContext.Server.State[$Name]
}

<#
.SYNOPSIS
Removes some state object from the shared state.

.DESCRIPTION
Removes some state object from the shared state. After removal, the original object being stored is returned.

.PARAMETER Name
The name of the state object.

.EXAMPLE
Remove-PodeState -Name 'Data'
#>
function Remove-PodeState
{
    [CmdletBinding()]
    [OutputType([object])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    if ($null -eq $PodeContext.Server.State) {
        throw "Pode has not been initialised"
    }

    $value = $PodeContext.Server.State[$Name]
    $PodeContext.Server.State.Remove($Name) | Out-Null
    return $value
}

<#
.SYNOPSIS
Saves the current shared state to a supplied JSON file.

.DESCRIPTION
Saves the current shared state to a supplied JSON file. When using this function, it's recommended to wrap it in a Lock-PodeObject block.

.PARAMETER Path
The path to a JSON file which the current state will be saved to.

.EXAMPLE
Save-PodeState -Path './state.json'
#>
function Save-PodeState
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    if ($null -eq $PodeContext.Server.State) {
        throw "Pode has not been initialised"
    }

    $Path = Get-PodeRelativePath -Path $Path -JoinRoot
    $PodeContext.Server.State |
        ConvertTo-Json -Depth 10 |
        Out-File -FilePath $Path -Force |
        Out-Null
}

<#
.SYNOPSIS
Restores the shared state from some JSON file.

.DESCRIPTION
Restores the shared state from some JSON file.

.PARAMETER Path
The path to a JSON file that contains the state information.

.EXAMPLE
Restore-PodeState -Path './state.json'
#>
function Restore-PodeState
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Path
    )

    if ($null -eq $PodeContext.Server.State) {
        throw "Pode has not been initialised"
    }

    $Path = Get-PodeRelativePath -Path $Path -JoinRoot
    if (!(Test-Path $Path)) {
        return
    }

    if (Test-IsPSCore) {
        $PodeContext.Server.State = (Get-Content $Path -Force | ConvertFrom-Json -AsHashtable -Depth 10)
    }
    else {
        (Get-Content $Path -Force | ConvertFrom-Json).psobject.properties | ForEach-Object {
            $PodeContext.Server.State[$_.Name] = $_.Value
        }
    }
}

<#
.SYNOPSIS
Tests if the shared state contains some state object.

.DESCRIPTION
Tests if the shared state contains some state object.

.PARAMETER Name
The name of the state object.

.EXAMPLE
Test-PodeState -Name 'Data'
#>
function Test-PodeState
{
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    if ($null -eq $PodeContext.Server.State) {
        throw "Pode has not been initialised"
    }

    return $PodeContext.Server.State.ContainsKey($Name)
}
src\Public\Routes.ps1
<#
.SYNOPSIS
Adds a Route for a specific HTTP Method.

.DESCRIPTION
Adds a Route for a specific HTTP Method, with path, that when called with invoke any logic and/or Middleware.

.PARAMETER Method
The HTTP Method of this Route.

.PARAMETER Path
The URI path for the Route.

.PARAMETER Middleware
An array of ScriptBlocks for optional Middleware.

.PARAMETER ScriptBlock
A ScriptBlock for the Route's main logic.

.PARAMETER Protocol
The protocol this Route should be bound against.

.PARAMETER Endpoint
The endpoint this Route should be bound against.

.PARAMETER EndpointName
The EndpointName of an Endpoint this Route should be bound against.

.PARAMETER ContentType
The content type the Route should use when parsing any payloads.

.PARAMETER ErrorContentType
The content type of any error pages that may get returned.

.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Route's main logic.

.PARAMETER ArgumentList
An array of arguments to supply to the Route's ScriptBlock.

.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Post -Path '/users/:userId/message' -Middleware (Get-PodeCsrfMiddleware) -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Get -Path '/api/cpu' -ErrorContentType 'application/json' -ScriptBlock { /* logic */ }

.EXAMPLE
Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2'
#>
function Add-PodeRoute
{
    [CmdletBinding(DefaultParameterSetName='Script')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter(ParameterSetName='Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [ValidateSet('', 'Http', 'Https')]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint,

        [Parameter()]
        [string]
        $EndpointName,

        [Parameter()]
        [string]
        $ContentType,

        [Parameter()]
        [string]
        $ErrorContentType,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $FilePath,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw "[$($Method)]: No Path supplied for Route"
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path
    $Path = Update-PodeRoutePlaceholders -Path $Path

    # if an EndpointName was supplied, find it and use it
    $_endpoint = Get-PodeEndpointByName -EndpointName $EndpointName -ThrowError
    if ($null -ne $_endpoint) {
        $Protocol = $_endpoint.Protocol
        $Endpoint = $_endpoint.RawAddress
    }

    # if we have an endpoint, set any appropriate wildcards
    if (!(Test-IsEmpty $Endpoint)) {
        $_endpoint = Get-PodeEndpointInfo -Endpoint $Endpoint -AnyPortOnZero
        $Endpoint = "$($_endpoint.Host):$($_endpoint.Port)"
    }

    # ensure route doesn't already exist
    Test-PodeRouteAndError -Method $Method -Path $Path -Protocol $Protocol -Endpoint $Endpoint

    # if middleware, scriptblock and file path are all null/empty, error
    if ((Test-IsEmpty $Middleware) -and (Test-IsEmpty $ScriptBlock) -and (Test-IsEmpty $FilePath)) {
        throw "[$($Method)] $($Path): No logic passed"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        # resolve for relative path
        $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot

        # if file doesn't exist, error
        if (!(Test-PodePath -Path $FilePath -NoStatus)) {
            throw "[$($Method)] $($Path): The FilePath does not exist: $($FilePath)"
        }

        # if the path is a wildcard or directory, error
        if (!(Test-PodePathIsFile -Path $FilePath -FailOnWildcard)) {
            throw "[$($Method)] $($Path): The FilePath cannot be a wildcard or directory: $($FilePath)"
        }

        $ScriptBlock = [scriptblock](Use-PodeScript -Path $FilePath)
    }

    # ensure supplied middlewares are either a scriptblock, or a valid hashtable
    if (!(Test-IsEmpty $Middleware)) {
        @($Middleware) | ForEach-Object {
            # check middleware is a type valid
            if (($_ -isnot [scriptblock]) -and ($_ -isnot [hashtable])) {
                throw "One of the Route Middlewares supplied for the '[$($Method)] $($Path)' Route is an invalid type. Expected either ScriptBlock or Hashtable, but got: $($_.GetType().Name)"
            }

            # if middleware is hashtable, ensure the keys are valid (logic is a scriptblock)
            if ($_ -is [hashtable]) {
                if ($null -eq $_.Logic) {
                    throw "A Hashtable Middleware supplied for the '[$($Method)] $($Path)' Route has no Logic defined"
                }

                if ($_.Logic -isnot [scriptblock]) {
                    throw "A Hashtable Middleware supplied for the '[$($Method)] $($Path)' Route has has an invalid Logic type. Expected ScriptBlock, but got: $($_.Logic.GetType().Name)"
                }
            }
        }
    }

    # if we have middleware, convert scriptblocks to hashtables
    if (!(Test-IsEmpty $Middleware))
    {
        $Middleware = @($Middleware)

        for ($i = 0; $i -lt $Middleware.Length; $i++) {
            if ($Middleware[$i] -is [scriptblock]) {
                $Middleware[$i] = @{
                    'Logic' = $Middleware[$i]
                }
            }
        }
    }

    # workout a default content type for the route
    if ([string]::IsNullOrWhiteSpace($ContentType)) {
        $ContentType = $PodeContext.Server.Web.ContentType.Default

        # find type by pattern from settings
        $matched = ($PodeContext.Server.Web.ContentType.Routes.Keys | Where-Object {
            $Path -imatch $_
        } | Select-Object -First 1)

        # if we get a match, set it
        if (!(Test-IsEmpty $matched)) {
            $ContentType = $PodeContext.Server.Web.ContentType.Routes[$matched]
        }
    }

    # add the route
    Write-Verbose "Adding Route: [$($Method)] $($Path)"
    $PodeContext.Server.Routes[$Method][$Path] += @(@{
        Logic = $ScriptBlock
        Middleware = $Middleware
        Protocol = $Protocol
        Endpoint = $Endpoint.Trim()
        ContentType = $ContentType
        ErrorType = $ErrorContentType
        Arguments = $ArgumentList
    })
}

<#
.SYNOPSIS
Add a static Route for rendering static content.

.DESCRIPTION
Add a static Route for rendering static content. You can also define default pages to display.

.PARAMETER Path
The URI path for the static Route.

.PARAMETER Source
The literal, or relative, path to the directory that contains the static content.

.PARAMETER Protocol
The protocol this static Route should be bound against.

.PARAMETER Endpoint
The endpoint this static Route should be bound against.

.PARAMETER EndpointName
The EndpointName of an Endpoint to bind the static Route against.

.PARAMETER Defaults
An array of default pages to display, such as 'index.html'.

.PARAMETER DownloadOnly
When supplied, all static content on this Route will be attached as downloads - rather than rendered.

.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets'

.EXAMPLE
Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html')

.EXAMPLE
Add-PodeStaticRoute -Path '/installers' -Source './exes' -DownloadOnly
#>
function Add-PodeStaticRoute
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter(Mandatory=$true)]
        [string]
        $Source,

        [Parameter()]
        [ValidateSet('', 'Http', 'Https')]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint,

        [Parameter()]
        [string]
        $EndpointName,

        [Parameter()]
        [string[]]
        $Defaults,

        [switch]
        $DownloadOnly
    )

    # store the route method
    $Method = 'Static'

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw "[$($Method)]: No Path path supplied for Static Route"
    }

    # ensure the route has appropriate slashes
    $Path = Update-PodeRouteSlashes -Path $Path -Static

    # if an EndpointName was supplied, find it and use it
    $_endpoint = Get-PodeEndpointByName -EndpointName $EndpointName -ThrowError
    if ($null -ne $_endpoint) {
        $Protocol = $_endpoint.Protocol
        $Endpoint = $_endpoint.RawAddress
    }

    # if we have an endpoint, set any appropriate wildcards
    if (!(Test-IsEmpty $Endpoint)) {
        $_endpoint = Get-PodeEndpointInfo -Endpoint $Endpoint -AnyPortOnZero
        $Endpoint = "$($_endpoint.Host):$($_endpoint.Port)"
    }

    # ensure route doesn't already exist
    Test-PodeRouteAndError -Method $Method -Path $Path -Protocol $Protocol -Endpoint $Endpoint

    # if static, ensure the path exists at server root
    $Source = Get-PodeRelativePath -Path $Source -JoinRoot
    if (!(Test-PodePath -Path $Source -NoStatus)) {
        throw "[$($Method))] $($Path): The Source path supplied for Static Route does not exist: $($Source)"
    }

    # setup a temp drive for the path
    $Source = New-PodePSDrive -Path $Source

    # setup default static files
    if ($null -eq $Defaults) {
        $Defaults = Get-PodeStaticRouteDefaults
    }

    # add the route path
    $PodeContext.Server.Routes[$Method][$Path] += @(@{
        Path = $Source
        Defaults = $Defaults
        Protocol = $Protocol
        Endpoint = $Endpoint.Trim()
        Download = $DownloadOnly
    })

}

<#
.SYNOPSIS
Remove a specific Route.

.DESCRIPTION
Remove a specific Route.

.PARAMETER Method
The method of the Route to remove.

.PARAMETER Path
The path of the Route to remove.

.PARAMETER Protocol
The protocol of the Route to remove.

.PARAMETER Endpoint
The endpoint of the Route to remove.

.EXAMPLE
Remove-PodeRoute -Method Get -Route '/about'

.EXAMPLE
Remove-PodeRoute -Method Post -Route '/users/:userId' -Endpoint 127.0.0.2:8001
#>
function Remove-PodeRoute
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method,

        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint
    )

    # split route on '?' for query
    $Path = Split-PodeRouteQuery -Path $Path
    if ([string]::IsNullOrWhiteSpace($Path)) {
        throw "[$($Method)]: No Route path supplied for removing a Route"
    }

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlashes -Path $Path
    $Path = Update-PodeRoutePlaceholders -Path $Path

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].ContainsKey($Path)) {
        return
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
        !(($_.Protocol -ieq $Protocol) -and ($_.Endpoint -ieq $Endpoint))
    })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $PodeContext.Server.Routes[$Method].Remove($Path) | Out-Null
    }
}

<#
.SYNOPSIS
Remove a specific static Route.

.DESCRIPTION
Remove a specific static Route.

.PARAMETER Path
The path of the static Route to remove.

.PARAMETER Protocol
The protocol of the static Route to remove.

.PARAMETER Endpoint
The endpoint of the static Route to remove.

.EXAMPLE
Remove-PodeStaticRoute -Path '/assets'

.EXAMPLE
Remove-PodeStaticRoute -Path '/assets' -Protocol
#>
function Remove-PodeStaticRoute
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Protocol,

        [Parameter()]
        [string]
        $Endpoint
    )

    $Method = 'Static'

    # ensure the route has appropriate slashes and replace parameters
    $Path = Update-PodeRouteSlashes -Path $Path -Static

    # ensure route does exist
    if (!$PodeContext.Server.Routes[$Method].ContainsKey($Path)) {
        return
    }

    # remove the route's logic
    $PodeContext.Server.Routes[$Method][$Path] = @($PodeContext.Server.Routes[$Method][$Path] | Where-Object {
        !(($_.Protocol -ieq $Protocol) -and ($_.Endpoint -ieq $Endpoint))
    })

    # if the route has no more logic, just remove it
    if ((Get-PodeCount $PodeContext.Server.Routes[$Method][$Path]) -eq 0) {
        $PodeContext.Server.Routes[$Method].Remove($Path) | Out-Null
    }
}

<#
.SYNOPSIS
Removes all added Routes, or Routes for a specific Method.

.DESCRIPTION
Removes all added Routes, or Routes for a specific Method.

.PARAMETER Method
The Method to from which to remove all Routes.

.EXAMPLE
Clear-PodeRoutes

.EXAMPLE
Clear-PodeRoutes -Method Get
#>
function Clear-PodeRoutes
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet('', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace', '*')]
        [string]
        $Method
    )

    if (![string]::IsNullOrWhiteSpace($Method)) {
        $PodeContext.Server.Routes[$Method].Clear()
    }
    else {
        $PodeContext.Server.Routes.Keys.Clone() | ForEach-Object {
            $PodeContext.Server.Routes[$_].Clear()
        }
    }
}

<#
.SYNOPSIS
Removes all added static Routes.

.DESCRIPTION
Removes all added static Routes.

.EXAMPLE
Clear-PodeStaticRoutes
#>
function Clear-PodeStaticRoutes
{
    [CmdletBinding()]
    param()

    $PodeContext.Server.Routes['Static'].Clear()
}

<#
.SYNOPSIS
Takes an array of Commands, or a Module, and converts them into Routes.

.DESCRIPTION
Takes an array of Commands (Functions/Aliases), or a Module, and generates appropriate Routes for the commands.

.PARAMETER Commands
An array of Commands to convert - if a Module is supplied, these Commands must be present within that Module.

.PARAMETER Module
A Module whose exported commands will be converted.

.PARAMETER Method
An override HTTP method to use when generating the Routes. If not supplied, Pode will make a best guess based on the Command's Verb.

.PARAMETER Path
An optional Path for the Route, to prepend before the Command Name and Module.

.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.

.PARAMETER NoVerb
If supplied, the Command's Verb will not be included in the Route's path.

.EXAMPLE
ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Middleware (Get-PodeAuthMiddleware -Name 'auth-name' -Sessionless)

.EXAMPLE
ConvertTo-PodeRoute -Module Pester -Path '/api'

.EXAMPLE
ConvertTo-PodeRoute -Commands @('Invoke-Pester') -Module Pester
#>
function ConvertTo-PodeRoute
{
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        [string[]]
        $Commands,

        [Parameter()]
        [string]
        $Module,

        [Parameter()]
        [ValidateSet('', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string]
        $Method,

        [Parameter()]
        [string]
        $Path = '/',

        [Parameter()]
        [object[]]
        $Middleware,

        [switch]
        $NoVerb
    )

    # if a module was supplied, import it - then validate the commands
    if (![string]::IsNullOrWhiteSpace($Module)) {
        Import-PodeModule -Name $Module -Now

        Write-Verbose "Getting exported commands from module"
        $ModuleCommands = (Get-Module -Name $Module).ExportedCommands.Keys

        # if commands were supplied validate them - otherwise use all exported ones
        if (Test-IsEmpty $Commands) {
            Write-Verbose "Using all commands in $($Module) for converting to routes"
            $Commands = $ModuleCommands
        }
        else {
            Write-Verbose "Validating supplied commands against module's exported commands"
            foreach ($cmd in $Commands) {
                if ($ModuleCommands -inotcontains $cmd) {
                    throw "Module $($Module) does not contain function $($cmd) to convert to a Route"
                }
            }
        }
    }

    # if there are no commands, fail
    if (Test-IsEmpty $Commands) {
        throw 'No commands supplied to convert to Routes'
    }

    # trim end trailing slashes from the path
    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = $Path.TrimEnd('/')

    # create the routes for each of the commands
    foreach ($cmd in $Commands) {
        # get module verb/noun and comvert verb to HTTP method
        $split = ($cmd -split '\-')

        if ($split.Length -ge 2) {
            $verb = $split[0]
            $noun = $split[1..($split.Length - 1)] -join ([string]::Empty)
        }
        else {
            $verb = [string]::Empty
            $noun = $split[0]
        }

        # determine the http method, or use the one passed
        $_method = $Method
        if ([string]::IsNullOrWhiteSpace($_method)) {
            $_method = Convert-PodeFunctionVerbToHttpMethod -Verb $verb
        }

        # use the full function name, or remove the verb
        $name = $cmd
        if ($NoVerb) {
            $name = $noun
        }

        # build the route's path
        $_path = ("$($Path)/$($Module)/$($name)" -replace '[/]+', '/')

        # create the route
        Add-PodeRoute -Method $_method -Path $_path -Middleware $Middleware -ArgumentList $cmd -ScriptBlock {
            param($e, $cmd)

            # either get params from the QueryString or Payload
            if ($e.Method -ieq 'get') {
                $parameters = $e.Query
            }
            else {
                $parameters = $e.Data
            }

            # invoke the function
            $result = (. $cmd @parameters)

            # if we have a result, convert it to json
            if (!(Test-IsEmpty $result)) {
                Write-PodeJsonResponse -Value $result -Depth 1
            }
        }
    }
}

<#
.SYNOPSIS
Helper function to generate simple GET routes.

.DESCRIPTION
Helper function to generate simple GET routes from ScritpBlocks, Files, and Views.
The output is always rendered as HTML.

.PARAMETER Name
A unique name for the page, that will be used in the Path for the route.

.PARAMETER ScriptBlock
A ScriptBlock to invoke, where any results will be converted to HTML.

.PARAMETER FilePath
A FilePath, literal or relative, to a valid HTML file.

.PARAMETER View
The name of a View to render, this can be HTML or Dynamic.

.PARAMETER Data
A hashtable of Data to supply to a Dynamic File/View, or to be splatted as arguments for the ScriptBlock.

.PARAMETER Path
An optional Path for the Route, to prepend before the Name.

.PARAMETER Middleware
Like normal Routes, an array of Middleware that will be applied to all generated Routes.

.PARAMETER FlashMessages
If supplied, Views will have any flash messages supplied to them for rendering.

.EXAMPLE
Add-PodePage -Name Services -ScriptBlock { Get-Service }

.EXAMPLE
Add-PodePage -Name Index -View 'index'

.EXAMPLE
Add-PodePage -Name About -FilePath '.\views\about.pode' -Data @{ Date = [DateTime]::UtcNow }
#>
function Add-PodePage
{
    [CmdletBinding(DefaultParameterSetName='ScriptBlock')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ParameterSetName='ScriptBlock')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $FilePath,

        [Parameter(Mandatory=$true, ParameterSetName='View')]
        [string]
        $View,

        [Parameter()]
        [hashtable]
        $Data,

        [Parameter()]
        [string]
        $Path = '/',

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter(ParameterSetName='View')]
        [switch]
        $FlashMessages
    )

    $logic = $null
    $arg = $null

    # ensure the name is a valid alphanumeric
    if ($Name -inotmatch '^[a-z0-9\-_]+$') {
        throw "The Page name should be a valid AlphaNumeric value: $($Name)"
    }

    # trim end trailing slashes from the path
    $Path = Protect-PodeValue -Value $Path -Default '/'
    $Path = $Path.TrimEnd('/')

    # define the appropriate logic
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant())
    {
        'scriptblock' {
            if (Test-IsEmpty $ScriptBlock){
                throw 'A non-empty ScriptBlock is required to created a Page Route'
            }

            $arg = @($ScriptBlock, $Data)
            $logic = {
                param($e, $script, $data)

                # invoke the function (optional splat data)
                if (Test-IsEmpty $data) {
                    $result = (. $script)
                }
                else {
                    $result = (. $script @data)
                }

                # if we have a result, convert it to html
                if (!(Test-IsEmpty $result)) {
                    Write-PodeHtmlResponse -Value $result
                }
            }
        }

        'file' {
            $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -TestPath
            $arg = @($FilePath, $Data)
            $logic = {
                param($e, $file, $data)
                Write-PodeFileResponse -Path $file -ContentType 'text/html' -Data $data
            }
        }

        'view' {
            $arg = @($View, $Data, $FlashMessages)
            $logic = {
                param($e, $view, $data, [bool]$flash)
                Write-PodeViewResponse -Path $view -Data $data -FlashMessages:$flash
            }
        }
    }

    # build the route's path
    $_path = ("$($Path)/$($Name)" -replace '[/]+', '/')

    # create the route
    Add-PodeRoute -Method Get -Path $_path -Middleware $Middleware -ArgumentList $arg -ScriptBlock $logic
}
src\Public\Responses.ps1
<#
.SYNOPSIS
Attaches a file onto the Response for downloading.

.DESCRIPTION
Attaches a file from the "/public", and static Routes, onto the Response for downloading.
If the supplied path is not in the Static Routes but is a literal/relative path, then this file is used instead.

.PARAMETER Path
The Path to a static file relative to the "/public" directory, or a static Route.
If the supplied Path doesn't match any custom static Route, then Pode will look in the "/public" directory.
Failing this, if the file path exists as a literal/relative file, then this file is used as a fall back.

.PARAMETER ContentType
Manually specify the content type of the response rather than infering it from the attachment's file extension.
The supplied value must match the valid ContentType format, e.g. application/json

.EXAMPLE
Set-PodeResponseAttachment -Path 'downloads/installer.exe'

.EXAMPLE
Set-PodeResponseAttachment -Path './image.png'

.EXAMPLE
Set-PodeResponseAttachment -Path 'c:/content/accounts.xlsx'

.EXAMPLE
Set-PodeResponseAttachment -Path './data.txt' -ContentType 'application/json'
#>
function Set-PodeResponseAttachment
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType
    )

    # only attach files from public/static-route directories when path is relative
    $_path = (Get-PodeStaticRoutePath -Route $Path).Path

    # if there's no path, check the original path (in case it's literal/relative)
    if (!(Test-PodePath $_path -NoStatus)) {
        $Path = Get-PodeRelativePath -Path $Path -JoinRoot

        if (Test-PodePath $Path -NoStatus) {
            $_path = $Path
        }
    }

    # test the file path, and set status accordingly
    if (!(Test-PodePath $_path)) {
        return
    }

    $filename = Get-PodeFileName -Path $_path
    $ext = Get-PodeFileExtension -Path $_path -TrimPeriod

    try {
        # setup the content type and disposition
        if (!$ContentType) {
            $WebEvent.Response.ContentType = (Get-PodeContentType -Extension $ext)
        }
        else {
            $WebEvent.Response.ContentType = $ContentType
        }
        Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($filename)"

        # if serverless, get the content raw and return
        if (!$WebEvent.Streamed) {
            if (Test-IsPSCore) {
                $content = (Get-Content -Path $_path -Raw -AsByteStream)
            }
            else {
                $content = (Get-Content -Path $_path -Raw -Encoding byte)
            }

            $WebEvent.Response.Body = $content
        }

        # else if normal, stream the content back
        else {
            # setup the response details and headers
            $WebEvent.Response.SendChunked = $false

            # set file as an attachment on the response
            $buffer = [byte[]]::new(64 * 1024)
            $read = 0

            # open up the file as a stream
            $fs = (Get-Item $_path).OpenRead()
            $WebEvent.Response.ContentLength64 = $fs.Length

            while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) {
                $WebEvent.Response.OutputStream.Write($buffer, 0, $read)
            }
        }
    }
    finally {
        Close-PodeDisposable -Disposable $fs
    }
}

<#
.SYNOPSIS
Writes a String or a Byte[] to the Response.

.DESCRIPTION
Writes a String or a Byte[] to the Response, as some specified content type. This value can also be cached.

.PARAMETER Value
A String value to write.

.PARAMETER Bytes
An array of Bytes to write.

.PARAMETER ContentType
The content type of the data being written.

.PARAMETER MaxAge
The maximum age to cache the value on the browser, in seconds.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER Cache
Should the value be cached by browsers, or not?

.EXAMPLE
Write-PodeTextResponse -Value 'Leeeeeerrrooooy Jeeeenkiiins!'

.EXAMPLE
Write-PodeTextResponse -Value '{"name": "Rick"}' -ContentType 'application/json'

.EXAMPLE
Write-PodeTextResponse -Bytes (Get-Content -Path ./some/image.png -Raw -AsByteStream) -Cache -MaxAge 1800

.EXAMPLE
Write-PodeTextResponse -Value 'Untitled Text Response' -StatusCode 418
#>
function Write-PodeTextResponse
{
    [CmdletBinding(DefaultParameterSetName='String')]
    param (
        [Parameter(ParameterSetName='String')]
        [string]
        $Value,

        [Parameter(ParameterSetName='Bytes')]
        [byte[]]
        $Bytes,

        [Parameter()]
        [string]
        $ContentType = 'text/plain',

        [Parameter()]
        [int]
        $MaxAge = 3600,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $Cache
    )

    $isStringValue = ($PSCmdlet.ParameterSetName -ieq 'string')
    $isByteValue = ($PSCmdlet.ParameterSetName -ieq 'bytes')

    # set the status code of the response, but only if it's not 200 (to prevent overriding)
    if ($StatusCode -ne 200) {
        Set-PodeResponseStatus -Code $StatusCode -NoErrorPage
    }

    # if there's nothing to write, return
    if ($isStringValue -and [string]::IsNullOrWhiteSpace($Value)) {
        return
    }

    if ($isByteValue -and (($null -eq $Bytes) -or ($Bytes.Length -eq 0))) {
        return
    }

    # if the response stream isn't writable, return
    $res = $WebEvent.Response
    if (($null -eq $res) -or ($WebEvent.Streamed -and (($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite))) {
        return
    }

    # set a cache value
    if ($Cache) {
        Set-PodeHeader -Name 'Cache-Control' -Value "max-age=$($MaxAge), must-revalidate"
        Set-PodeHeader -Name 'Expires' -Value ([datetime]::UtcNow.AddSeconds($MaxAge).ToString("r", [CultureInfo]::InvariantCulture))
    }

    # specify the content-type if supplied (adding utf-8 if missing)
    if (![string]::IsNullOrWhiteSpace($ContentType)) {
        $charset = 'charset=utf-8'
        if ($ContentType -inotcontains $charset) {
            $ContentType = "$($ContentType); $($charset)"
        }

        $res.ContentType = $ContentType
    }

    # if we're serverless, set the string as the body
    if (!$WebEvent.Streamed) {
        if ($isStringValue) {
            $res.Body = $Value
        }
        else {
            $res.Body = $Bytes
        }
    }

    else {
        # convert string to bytes
        if ($isStringValue) {
            $Bytes = ConvertFrom-PodeValueToBytes -Value $Value
        }

        # write the content to the response stream
        $res.ContentLength64 = $Bytes.Length

        try {
            $ms = New-Object -TypeName System.IO.MemoryStream
            $ms.Write($Bytes, 0, $Bytes.Length)
            $ms.WriteTo($res.OutputStream)
        }
        catch {
            if ((Test-PodeValidNetworkFailure $_.Exception)) {
                return
            }

            $_ | Write-PodeErrorLog
            throw
        }
        finally {
            if ($null -ne $ms) {
                $ms.Close()
            }
        }
    }
}

<#
.SYNOPSIS
Renders the content of a static, or dynamic, file on the Response.

.DESCRIPTION
Renders the content of a static, or dynamic, file on the Response.
You can set browser's to cache the content, and also override the file's content type.

.PARAMETER Path
The path to a file.

.PARAMETER Data
A HashTable of dynamic data to supply to a dynamic file.

.PARAMETER ContentType
The content type of the file's contents - this overrides the file's extension.

.PARAMETER MaxAge
The maximum age to cache the file's content on the browser, in seconds.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER Cache
Should the file's content be cached by browsers, or not?

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt'

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -Cache -MaxAge 1800

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -ContentType 'application/json'

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Views/Index.pode' -Data @{ Counter = 2 }

.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -StatusCode 201
#>
function Write-PodeFileResponse
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [string]
        $Path,

        [Parameter()]
        $Data = @{},

        [Parameter()]
        [string]
        $ContentType = $null,

        [Parameter()]
        [int]
        $MaxAge = 3600,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $Cache
    )

    # resolve for relative path
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path -FailOnDirectory)) {
        return
    }

    # are we dealing with a dynamic file for the view engine? (ignore html)
    $mainExt = Get-PodeFileExtension -Path $Path -TrimPeriod

    # generate dynamic content
    if (![string]::IsNullOrWhiteSpace($mainExt) -and (($mainExt -ieq 'pode') -or ($mainExt -ieq $PodeContext.Server.ViewEngine.Extension))) {
        $content = Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data

        # get the sub-file extension, if empty, use original
        $subExt = Get-PodeFileExtension -Path (Get-PodeFileName -Path $Path -WithoutExtension) -TrimPeriod
        $subExt = (Protect-PodeValue -Value $subExt -Default $mainExt)

        $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $subExt))
        Write-PodeTextResponse -Value $content -ContentType $ContentType -StatusCode $StatusCode
    }

    # this is a static file
    else {
        if (Test-IsPSCore) {
            $content = (Get-Content -Path $Path -Raw -AsByteStream)
        }
        else {
            $content = (Get-Content -Path $Path -Raw -Encoding byte)
        }

        $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $mainExt))
        Write-PodeTextResponse -Bytes $content -ContentType $ContentType -MaxAge $MaxAge -StatusCode $StatusCode -Cache:$Cache
    }
}

<#
.SYNOPSIS
Writes CSV data to the Response.

.DESCRIPTION
Writes CSV data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to a CSV file.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeCsvResponse -Value "Name`nRick"

.EXAMPLE
Write-PodeCsvResponse -Value @{ Name = 'Rick' }

.EXAMPLE
Write-PodeCsvResponse -Path 'E:/Files/Names.csv'
#>
function Write-PodeCsvResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value')]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = @(foreach ($v in $Value) {
                    New-Object psobject -Property $v
                })

                if (Test-IsPSCore) {
                    $Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false)
                }
                else {
                    $Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation)
                }

                $Value = ($Value -join ([environment]::NewLine))
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/csv' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes HTML data to the Response.

.DESCRIPTION
Writes HTML data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to a HTML file.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeHtmlResponse -Value '<html><body>Hello!</body></html>'

.EXAMPLE
Write-PodeHtmlResponse -Value @{ Message = 'Hello, all!' }

.EXAMPLE
Write-PodeHtmlResponse -Path 'E:/Site/About.html'
#>
function Write-PodeHtmlResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value')]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = ($Value | ConvertTo-Html)
                $Value = ($Value -join ([environment]::NewLine))
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/html' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes Markdown data to the Response.

.DESCRIPTION
Writes Markdown data to the Response, with the option to render it as HTML.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to a Markdown file.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER AsHtml
If supplied, the Markdown will be converted to HTML. (This is only supported in PS7+)

.EXAMPLE
Write-PodeMarkdownResponse -Value '# Hello, world!' -AsHtml

.EXAMPLE
Write-PodeMarkdownResponse -Path 'E:/Site/About.md'
#>
function Write-PodeMarkdownResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value')]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $AsHtml
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    $mimeType = 'text/markdown'

    if ($AsHtml) {
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $mimeType = 'text/html'
            $Value = ($Value | ConvertFrom-Markdown).Html
        }
    }

    Write-PodeTextResponse -Value $Value -ContentType $mimeType -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes JSON data to the Response.

.DESCRIPTION
Writes JSON data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.

.PARAMETER Path
The path to a JSON file.

.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeJsonResponse -Value '{"name": "Rick"}'

.EXAMPLE
Write-PodeJsonResponse -Value @{ Name = 'Rick' } -StatusCode 201

.EXAMPLE
Write-PodeJsonResponse -Path 'E:/Files/Names.json'
#>
function Write-PodeJsonResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value')]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $Depth = 10,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                if ($Depth -le 0) {
                    $Value = ($Value | ConvertTo-Json -Compress)
                }
                else {
                    $Value = ($Value | ConvertTo-Json -Depth $Depth -Compress)
                }
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = '{}'
    }

    Write-PodeTextResponse -Value $Value -ContentType 'application/json' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes XML data to the Response.

.DESCRIPTION
Writes XML data to the Response, setting the content type accordingly.

.PARAMETER Value
A String, PSObject, or HashTable value.

.PARAMETER Path
The path to an XML file.

.PARAMETER StatusCode
The status code to set against the response.

.EXAMPLE
Write-PodeXmlResponse -Value '<root><name>Rick</name></root>'

.EXAMPLE
Write-PodeXmlResponse -Value @{ Name = 'Rick' } -StatusCode 201

.EXAMPLE
Write-PodeXmlResponse -Path 'E:/Files/Names.xml'
#>
function Write-PodeXmlResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value')]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = @(foreach ($v in $Value) {
                    New-Object psobject -Property $v
                })

                $Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation)
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/xml' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Renders a dynamic, or static, View on the Response.

.DESCRIPTION
Renders a dynamic, or static, View on the Response; allowing for dynamic data to be supplied.

.PARAMETER Path
The path to a View, relative to the "/views" directory. (Extension is optional).

.PARAMETER Data
Any dynamic data to supply to a dynamic View.

.PARAMETER StatusCode
The status code to set against the response.

.PARAMETER FlashMessages
Automatically supply all Flash messages in the current session to the View.

.EXAMPLE
Write-PodeViewResponse -Path 'index'

.EXAMPLE
Write-PodeViewResponse -Path 'accounts/profile_page' -Data @{ Username = 'Morty' }

.EXAMPLE
Write-PodeViewResponse -Path 'login' -FlashMessages
#>
function Write-PodeViewResponse
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [hashtable]
        $Data = @{},

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $FlashMessages
    )

    # default data if null
    if ($null -eq $Data) {
        $Data = @{}
    }

    # add path to data as "pagename" - unless key already exists
    if (!$Data.ContainsKey('pagename')) {
        $Data['pagename'] = $Path
    }

    # load all flash messages if needed
    if ($FlashMessages -and ($null -ne $WebEvent.Session.Data.Flash)) {
        $Data['flash'] = @{}

        foreach ($name in (Get-PodeFlashMessageNames)) {
            $Data.flash[$name] = (Get-PodeFlashMessage -Name $name)
        }
    }
    elseif ($null -eq $Data['flash']) {
        $Data['flash'] = @{}
    }

    # add view engine extension
    $ext = Get-PodeFileExtension -Path $Path
    if ([string]::IsNullOrWhiteSpace($ext)) {
        $Path += ".$($PodeContext.Server.ViewEngine.Extension)"
    }

    # only look in the view directory
    $Path = (Join-Path $PodeContext.Server.InbuiltDrives['views'] $Path)

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path)) {
        return
    }

    # run any engine logic and render it
    $engine = (Get-PodeViewEngineType -Path $Path)
    $value = (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)

    switch ($engine.ToLowerInvariant()) {
        'md' {
            Write-PodeMarkdownResponse -Value $value -StatusCode $StatusCode -AsHtml
        }

        default {
            Write-PodeHtmlResponse -Value $value -StatusCode $StatusCode
        }
    }
}

<#
.SYNOPSIS
Sets the Status Code of the Response, and controls rendering error pages.

.DESCRIPTION
Sets the Status Code of the Response, and controls rendering error pages.

.PARAMETER Code
The Status Code to set on the Response.

.PARAMETER Description
An optional Status Description.

.PARAMETER Exception
An exception to use when detailing error information on error pages.

.PARAMETER ContentType
The content type of the error page to use.

.PARAMETER NoErrorPage
Don't render an error page when the Status Code is 400+.

.EXAMPLE
Set-PodeResponseStatus -Code 404

.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception

.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception -ContentType 'application/json'
#>
function Set-PodeResponseStatus
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]
        $Code,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        $Exception,

        [Parameter()]
        [string]
        $ContentType = $null,

        [switch]
        $NoErrorPage
    )

    # set the code
    $WebEvent.Response.StatusCode = $Code

    # set an appropriate description (mapping if supplied is blank)
    if ([string]::IsNullOrWhiteSpace($Description)) {
        $Description = (Get-PodeStatusDescription -StatusCode $Code)
    }

    if (!$PodeContext.Server.IsServerless -and ![string]::IsNullOrWhiteSpace($Description)) {
        $WebEvent.Response.StatusDescription = $Description
    }

    # if the status code is >=400 then attempt to load error page
    if (!$NoErrorPage -and ($Code -ge 400)) {
        Show-PodeErrorPage -Code $Code -Description $Description -Exception $Exception -ContentType $ContentType
    }
}

<#
.SYNOPSIS
Redirecting a user to a new URL.

.DESCRIPTION
Redirecting a user to a new URL, or the same URL as the Request but a different Protocol - or other components.

.PARAMETER Url
Redirect the user to a new URL, or a relative path.

.PARAMETER Port
Change the port of the current Request before redirecting.

.PARAMETER Protocol
Change the protocol of the current Request before redirecting.

.PARAMETER Address
Change the domain address of the current Request before redirecting.

.PARAMETER Moved
Set the Status Code as "301 Moved", rather than "302 Redirect".

.EXAMPLE
Move-PodeResponseUrl -Url 'https://google.com'

.EXAMPLE
Move-PodeResponseUrl -Url '/about'

.EXAMPLE
Move-PodeResponseUrl -Protocol HTTPS

.EXAMPLE
Move-PodeResponseUrl -Port 9000 -Moved
#>
function Move-PodeResponseUrl
{
    [CmdletBinding(DefaultParameterSetName='Url')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Url')]
        [string]
        $Url,

        [Parameter(ParameterSetName='Components')]
        [int]
        $Port = 0,

        [Parameter(ParameterSetName='Components')]
        [ValidateSet('', 'Http', 'Https')]
        [string]
        $Protocol,

        [Parameter(ParameterSetName='Components')]
        [string]
        $Address,

        [switch]
        $Moved
    )

    if ($PSCmdlet.ParameterSetName -ieq 'components') {
        $uri = $WebEvent.Request.Url

        # set the protocol
        $Protocol = $Protocol.ToLowerInvariant()
        if ([string]::IsNullOrWhiteSpace($Protocol)) {
            $Protocol = $uri.Scheme
        }

        # set the domain
        if ([string]::IsNullOrWhiteSpace($Address)) {
            $Address = $uri.Host
        }

        # set the port
        if ($Port -le 0) {
            $Port = $uri.Port
        }

        $PortStr = [string]::Empty
        if (@(80, 443) -notcontains $Port) {
            $PortStr = ":$($Port)"
        }

        # combine to form the url
        $Url = "$($Protocol)://$($Address)$($PortStr)$($uri.PathAndQuery)"
    }

    Set-PodeHeader -Name 'Location' -Value $Url

    if ($Moved) {
        Set-PodeResponseStatus -Code 301 -Description 'Moved'
    }
    else {
        Set-PodeResponseStatus -Code 302 -Description 'Redirect'
    }
}

<#
.SYNOPSIS
Writes data to a TCP Client stream.

.DESCRIPTION
Writes data to a TCP Client stream.

.PARAMETER Message
Parameter description

.PARAMETER Client
An optional TcpClient to write data.

.EXAMPLE
Write-PodeTcpClient -Message '250 OK'
#>
function Write-PodeTcpClient
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Message,

        [Parameter()]
        $Client
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Write-PodeTcpClient' -ThrowError

    # use the main client if one isn't supplied
    if ($null -eq $Client) {
        $Client = $TcpEvent.Client
    }

    $encoder = New-Object System.Text.ASCIIEncoding
    $buffer = $encoder.GetBytes("$($Message)`r`n")
    $stream = $Client.GetStream()
    Wait-PodeTask -Task $stream.WriteAsync($buffer, 0, $buffer.Length)
    $stream.Flush()
}

<#
.SYNOPSIS
Reads data from a TCP Client stream.

.DESCRIPTION
Reads data from a TCP Client stream.

.PARAMETER Client
An optional TcpClient from which to read data.

.EXAMPLE
$data = Read-PodeTcpClient
#>
function Read-PodeTcpClient
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter()]
        $Client
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Read-PodeTcpClient' -ThrowError

    # use the main client if one isn't supplied
    if ($null -eq $Client) {
        $Client = $TcpEvent.Client
    }

    $bytes = New-Object byte[] 8192
    $encoder = New-Object System.Text.ASCIIEncoding
    $stream = $Client.GetStream()
    $bytesRead = (Wait-PodeTask -Task $stream.ReadAsync($bytes, 0, 8192))
    return $encoder.GetString($bytes, 0, $bytesRead)
}

<#
.SYNOPSIS
Saves an uploaded file on the Request to the File System.

.DESCRIPTION
Saves an uploaded file on the Request to the File System.

.PARAMETER Key
The name of the key within the web event's Data HashTable that stores the file's name.

.PARAMETER Path
The path to save files.

.EXAMPLE
Save-PodeRequestFile -Key 'avatar'

.EXAMPLE
Save-PodeRequestFile -Key 'avatar' -Path 'F:/Images'
#>
function Save-PodeRequestFile
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $Path = '.'
    )

    # if path is '.', replace with server root
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot

    # ensure the parameter name exists in data
    $fileName = $WebEvent.Data[$Key]
    if ([string]::IsNullOrWhiteSpace($fileName)) {
        throw "A parameter called '$($Key)' was not supplied in the request"
    }

    # ensure the file data exists
    if (!$WebEvent.Files.ContainsKey($fileName)) {
        throw "No data for file '$($fileName)' was uploaded in the request"
    }

    # if the path is a directory, add the filename
    if (Test-PodePathIsDirectory -Path $Path) {
        $Path = Join-Path $Path $fileName
    }

    # save the file
    [System.IO.File]::WriteAllBytes($Path, $WebEvent.Files[$fileName].Bytes)
}

<#
.SYNOPSIS
Short description

.DESCRIPTION
Long description

.PARAMETER Type
The type name of the view engine (inbuilt types are: Pode and HTML).

.PARAMETER ScriptBlock
A ScriptBlock for specifying custom view engine rendering rules.

.PARAMETER Extension
A custom extension for the engine's files.

.EXAMPLE
Set-PodeViewEngine -Type HTML

.EXAMPLE
Set-PodeViewEngine -Type Markdown

.EXAMPLE
Set-PodeViewEngine -Type PSHTML -Extension PS1 -ScriptBlock { param($path, $data) /* logic */ }
#>
function Set-PodeViewEngine
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Type,

        [Parameter()]
        [scriptblock]
        $ScriptBlock = $null,

        [Parameter()]
        [string]
        $Extension
    )

    # truncate markdown
    if ($Type -ieq 'Markdown') {
        $Type = 'md'
    }

    # override extension with type
    if ([string]::IsNullOrWhiteSpace($Extension)) {
        $Extension = $Type.ToLowerInvariant()
    }

    # setup view engine config
    $PodeContext.Server.ViewEngine.Type = $Type.ToLowerInvariant()
    $PodeContext.Server.ViewEngine.Extension = $Extension
    $PodeContext.Server.ViewEngine.Script = $ScriptBlock
    $PodeContext.Server.ViewEngine.IsDynamic = (@('html', 'md') -inotcontains $Type)
}

<#
.SYNOPSIS
Includes the contents of a partial View into another dynamic View.

.DESCRIPTION
Includes the contents of a partial View into another dynamic View. The partial View can be static or dynamic.

.PARAMETER Path
The path to a partial View, relative to the "/views" directory. (Extension is optional).

.PARAMETER Data
Any dynamic data to supply to a dynamic partial View.

.EXAMPLE
Use-PodePartialView -Path 'shared/footer'
#>
function Use-PodePartialView
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        $Data = @{}
    )

    # default data if null
    if ($null -eq $Data) {
        $Data = @{}
    }

    # add view engine extension
    $ext = Get-PodeFileExtension -Path $Path
    if ([string]::IsNullOrWhiteSpace($ext)) {
        $Path += ".$($PodeContext.Server.ViewEngine.Extension)"
    }

    # only look in the view directory
    $Path = (Join-Path $PodeContext.Server.InbuiltDrives['views'] $Path)

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path -NoStatus)) {
        throw "File not found at path: $($Path)"
    }

    # run any engine logic
    return (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)
}

<#
.SYNOPSIS
Broadcasts a message to connected WebSocket clients.

.DESCRIPTION
Broadcasts a message to all, or some, connected WebSocket clients. You can specify a path to send messages to, or a specific ClientId.

.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.

.PARAMETER Path
The Path of connected clients to send the message.

.PARAMETER ClientId
A specific ClientId of a connected client to send a message. Not currently used.

.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.

.EXAMPLE
Send-PodeSignal -Value @{ Message = 'Hello, world!' }

.EXAMPLE
Send-PodeSignal -Value @{ Data = @(123, 100, 101) } -Path '/response-charts'
#>
function Send-PodeSignal
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $Value,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $ClientId,

        [Parameter()]
        [int]
        $Depth = 10
    )

    if ($Value -isnot [string]) {
        if ($Depth -le 0) {
            $Value = ($Value | ConvertTo-Json -Compress)
        }
        else {
            $Value = ($Value | ConvertTo-Json -Depth $Depth -Compress)
        }
    }

    $PodeContext.Server.WebSockets.Queues.Messages.Enqueue(@{
        Value = $Value
        ClientId = $ClientId
        Path = $Path
    })
}
src\Public\Middleware.ps1
<#
.SYNOPSIS
Adds an access rule to allow or deny IP addresses.

.DESCRIPTION
Adds an access rule to allow or deny IP addresses.

.PARAMETER Access
The type of access to enable.

.PARAMETER Type
What type of request are we configuring?

.PARAMETER Values
A single, or an array of values.

.EXAMPLE
Add-PodeAccessRule -Access Allow -Type IP -Values '127.0.0.1'

.EXAMPLE
Add-PodeAccessRule -Access Deny -Type IP -Values @('192.168.1.1', '10.10.1.0/24')
#>
function Add-PodeAccessRule
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Allow', 'Deny')]
        [string]
        $Access,

        [Parameter(Mandatory=$true)]
        [ValidateSet('IP')]
        [string]
        $Type,

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

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeAccessRule' -ThrowError

    # call the appropriate access method
    switch ($Type.ToLowerInvariant())
    {
        'ip' {
            foreach ($ip in $Values) {
                Add-PodeIPAccess -Access $Access -IP $ip
            }
        }
    }
}

<#
.SYNOPSIS
Adds rate limiting rules for an IP address.

.DESCRIPTION
Adds rate limiting rules for an IP address.

.PARAMETER Type
What type of request are we limiting?

.PARAMETER Values
A single, or an array of values.

.PARAMETER Limit
The maximum number of requests to allow.

.PARAMETER Seconds
The number of seconds to count requests before restarting the count.

.PARAMETER Group
If supplied, groups of IPs in a subnet will be concidered as one IP.

.EXAMPLE
Add-PodeLimitRule -Type IP -Values '127.0.0.1' -Limit 10 -Seconds 1

.EXAMPLE
Add-PodeLimitRule -Type IP -Values @('192.168.1.1', '10.10.1.0/24') -Limit 50 -Seconds 1 -Group
#>
function Add-PodeLimitRule
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('IP')]
        [string]
        $Type,

        [Parameter(Mandatory=$true)]
        [string[]]
        $Values,

        [Parameter(Mandatory=$true)]
        [int]
        $Limit,

        [Parameter(Mandatory=$true)]
        [int]
        $Seconds,

        [switch]
        $Group
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeLimitRule' -ThrowError

    # call the appropriate limit method
    switch ($Type.ToLowerInvariant())
    {
        'ip' {
            foreach ($ip in $Values) {
                Add-PodeIPLimit -IP $ip -Limit $Limit -Seconds $Seconds -Group:$Group
            }
        }
    }
}

<#
.SYNOPSIS
Enables Middleware for creating, retrieving and using Sessions within Pode.

.DESCRIPTION
Enables Middleware for creating, retrieving and using Sessions within Pode. With support for defining Session duration, and custom Storage.

.PARAMETER Secret
A secret to use when signing Session cookies.

.PARAMETER Name
The name of the cookie Sessions use.

.PARAMETER Duration
The duration a Session cookie should last for, before being expired.

.PARAMETER Generator
A custom ScriptBlock to generate a random unique SessionId. The value returned must be a String.

.PARAMETER Storage
A custom PSObject that defines methods for Delete, Get, and Set. This allow you to store Sessions in custom Storage such as Redis.

.PARAMETER Extend
If supplied, the Session's cookie will have its duration extended on each successful Request.

.PARAMETER HttpOnly
If supplied, the Session cookie will only be accessible to browsers.

.PARAMETER Secure
If supplied, the Session cookie will only be accessible over HTTPS Requests.

.EXAMPLE
Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120

.EXAMPLE
Enable-PodeSessionMiddleware -Secret 'schwifty' -Duration 120 -Extend -Generator { return [System.IO.Path]::GetRandomFileName() }
#>
function Enable-PodeSessionMiddleware
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Secret,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name = 'pode.sid',

        [Parameter()]
        [ValidateScript({
            if ($_ -lt 0) {
                throw "Duration must be 0 or greater, but got: $($_)s"
            }

            return $true
        })]
        [int]
        $Duration = 0,

        [Parameter()]
        [scriptblock]
        $Generator,

        [Parameter()]
        [psobject]
        $Storage,

        [switch]
        $Extend,

        [switch]
        $HttpOnly,

        [switch]
        $Secure
    )

    # check that session logic hasn't already been initialised
    if (Test-PodeSessionsConfigured) {
        throw 'Session Middleware has already been intialised'
    }

    # ensure the override store has the required methods
    if (!(Test-IsEmpty $Storage)) {
        $members = @($Storage | Get-Member | Select-Object -ExpandProperty Name)
        @('delete', 'get', 'set') | ForEach-Object {
            if ($members -inotcontains $_) {
                throw "Custom session storage does not implement the required '$($_)()' method"
            }
        }
    }

    # if no custom storage, use the inmem one
    if (Test-IsEmpty $Storage) {
        $Storage = (Get-PodeSessionCookieInMemStore)
        Set-PodeSessionCookieInMemClearDown
    }

    # set options against server context
    $PodeContext.Server.Cookies.Session = @{
        Name = $Name
        Secret = $Secret
        GenerateId = (Protect-PodeValue -Value $Generator -Default { return (New-PodeGuid) })
        Store = $Storage
        Info = @{
            Duration = $Duration
            Extend = $Extend
            Secure = $Secure
            HttpOnly = $HttpOnly
        }
    }

    # return scriptblock for the session middleware
    $script = {
        param($e)

        # if session already set, return
        if ($e.Session) {
            return $true
        }

        try
        {
            # get the session cookie
            $_sessionInfo = $PodeContext.Server.Cookies.Session
            $e.Session = Get-PodeSessionCookie -Name $_sessionInfo.Name -Secret $_sessionInfo.Secret

            # if no session on browser, create a new one
            if (!$e.Session) {
                $e.Session = (New-PodeSessionCookie)
                $new = $true
            }

            # get the session's data
            elseif ($null -ne ($data = $_sessionInfo.Store.Get($e.Session.Id))) {
                $e.Session.Data = $data
                Set-PodeSessionCookieDataHash -Session $e.Session
            }

            # session not in store, create a new one
            else {
                $e.Session = (New-PodeSessionCookie)
                $new = $true
            }

            # add helper methods to session
            Set-PodeSessionCookieHelpers -Session $e.Session

            # add cookie to response if it's new or extendible
            if ($new -or $e.Session.Cookie.Extend) {
                Set-PodeSessionCookie -Session $e.Session
            }

            # assign endware for session to set cookie/storage
            $e.OnEnd += @{
                Logic = {
                    Save-PodeSession -Force
                }
            }
        }
        catch {
            $_ | Write-PodeErrorLog
            return $false
        }

        # move along
        return $true
    }

    (New-PodeMiddleware -ScriptBlock $script) | Add-PodeMiddleware -Name '__pode_mw_sessions__'
}

<#
.SYNOPSIS
Remove the current Session, logging it out.

.DESCRIPTION
Remove the current Session, logging it out. This will remove the session from Storage, and Cookies.

.EXAMPLE
Remove-PodeSession
#>
function Remove-PodeSession
{
    [CmdletBinding()]
    param()

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions have not been configured'
    }

    # error if session is null
    if ($null -eq $WebEvent.Session) {
        return
    }

    # remove the session, and from auth and cookies
    Remove-PodeAuthSession -Event $WebEvent
}

<#
.SYNOPSIS
Saves the current Session's data.

.DESCRIPTION
Saves the current Session's data.

.PARAMETER Force
If supplied, the data will be saved even if nothing has changed.

.EXAMPLE
Save-PodeSession -Force
#>
function Save-PodeSession
{
    [CmdletBinding()]
    param(
        [switch]
        $Force
    )

    # if sessions haven't been setup, error
    if (!(Test-PodeSessionsConfigured)) {
        throw 'Sessions have not been configured'
    }

    # error if session is null
    if ($null -eq $WebEvent.Session) {
        throw 'There is no session available to save'
    }

    # if auth is in use, then assign to session store
    if (!(Test-IsEmpty $WebEvent.Auth) -and $WebEvent.Auth.Store) {
        $WebEvent.Session.Data.Auth = $WebEvent.Auth
    }

    # save the session
    Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Session.Save -Arguments @($WebEvent.Session, $Force) -Splat
}

<#
.SYNOPSIS
Creates and returns a new secure token for use with CSRF.

.DESCRIPTION
Creates and returns a new secure token for use with CSRF.

.EXAMPLE
$token = New-PodeCsrfToken
#>
function New-PodeCsrfToken
{
    [CmdletBinding()]
    [OutputType([string])]
    param()

    # fail if the csrf logic hasn't been initialised
    if (!(Test-PodeCsrfConfigured)) {
        throw 'CSRF Middleware has not been initialised'
    }

    # generate a new secret and salt
    $Secret = New-PodeCsrfSecret
    $Salt = (New-PodeSalt -Length 8)

    # return a new token
    return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
}

<#
.SYNOPSIS
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.

.DESCRIPTION
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.

.EXAMPLE
$csrf = Get-PodeCsrfMiddleware
Add-PodeRoute -Method Get -Path '/cpu' -Middleware $csrf -ScriptBlock { /* logic */ }
#>
function Get-PodeCsrfMiddleware
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    # fail if the csrf logic hasn't been initialised
    if (!(Test-PodeCsrfConfigured)) {
        throw 'CSRF Middleware has not been initialised'
    }

    # return scriptblock for the csrf route middleware to test tokens
    $script = {
        param($e)

        # if there's not a secret, generate and store it
        $secret = New-PodeCsrfSecret

        # verify the token on the request, if invalid, throw a 403
        $token = Get-PodeCsrfToken

        if (!(Test-PodeCsrfToken -Secret $secret -Token $token)){
            Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
            return $false
        }

        # token is valid, move along
        return $true
    }

    return (New-PodeMiddleware -ScriptBlock $script)
}

<#
.SYNOPSIS
Initialises CSRF within Pode for adhoc usage.

.DESCRIPTION
Initialises CSRF within Pode for adhoc usage, with configurable HTTP methods to ignore verification.

.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.

.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.

.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.

.EXAMPLE
Initialize-PodeCsrf -IgnoreMethods @('Get', 'Trace')

.EXAMPLE
Initialize-PodeCsrf -Secret 'some-secret' -UseCookies
#>
function Initialize-PodeCsrf
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet('Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string[]]
        $IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $UseCookies
    )

    # check that csrf logic hasn't already been intialised
    if (Test-PodeCsrfConfigured) {
        return
    }

    # if sessions haven't been setup and we're not using cookies, error
    if (!$UseCookies -and !(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use CSRF unless you want to use cookies'
    }

    # if we're using cookies, ensure a global secret exists
    if ($UseCookies) {
        $Secret = (Protect-PodeValue -Value $Secret -Default (Get-PodeCookieSecret -Global))

        if (Test-IsEmpty $Secret) {
            throw "When using cookies for CSRF, a Secret is required. You can either supply a Secret, or set the Cookie global secret - (Set-PodeCookieSecret '<value>' -Global)"
        }
    }

    # set the options against the server context
    $PodeContext.Server.Cookies.Csrf = @{
        Name = 'pode.csrf'
        UseCookies = $UseCookies
        Secret = $Secret
        IgnoredMethods = $IgnoreMethods
    }
}

<#
.SYNOPSIS
Enables Middleware for verifying CSRF tokens on Requests.

.DESCRIPTION
Enables Middleware for verifying CSRF tokens on Requests, with configurable HTTP methods to ignore verification.

.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.

.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.

.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.

.EXAMPLE
Enable-PodeCsrfMiddleware -IgnoreMethods @('Get', 'Trace')

.EXAMPLE
Enable-PodeCsrfMiddleware -Secret 'some-secret' -UseCookies
#>
function Enable-PodeCsrfMiddleware
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet('Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string[]]
        $IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),

        [Parameter(ParameterSetName='Cookies')]
        [string]
        $Secret,

        [Parameter(ParameterSetName='Cookies')]
        [switch]
        $UseCookies
    )

    Initialize-PodeCsrf -IgnoreMethods $IgnoreMethods -Secret $Secret -UseCookies:$UseCookies

    # return scriptblock for the csrf middleware
    $script = {
        param($e)

        # if the current route method is ignored, just return
        $ignored = @($PodeContext.Server.Cookies.Csrf.IgnoredMethods)
        if (!(Test-IsEmpty $ignored) -and ($ignored -icontains $e.Method)) {
            return $true
        }

        # if there's not a secret, generate and store it
        $secret = New-PodeCsrfSecret

        # verify the token on the request, if invalid, throw a 403
        $token = Get-PodeCsrfToken

        if (!(Test-PodeCsrfToken -Secret $secret -Token $token)){
            Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
            return $false
        }

        # token is valid, move along
        return $true
    }

    (New-PodeMiddleware -ScriptBlock $script) | Add-PodeMiddleware -Name '__pode_mw_csrf__'
}

<#
.SYNOPSIS
Adds a custom body parser middleware.

.DESCRIPTION
Adds a custom body parser middleware script for a content-type, which will be used if a payload is sent with a Request.

.PARAMETER ContentType
The ContentType of the custom body parser.

.PARAMETER ScriptBlock
The ScriptBlock that will parse the body content, and return the result.

.EXAMPLE
Add-PodeBodyParser -ContentType 'application/json' -ScriptBlock { param($body) /* parsing logic */ }
#>
function Add-PodeBodyParser
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [scriptblock]
        $ScriptBlock
    )

    # if a parser for the type already exists, fail
    if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
        throw "There is already a body parser defined for the $($ContentType) content-type"
    }

    $PodeContext.Server.BodyParsers[$ContentType] = $ScriptBlock
}

<#
.SYNOPSIS
Removes a custom body parser.

.DESCRIPTION
Removes a custom body parser middleware script for a content-type.

.PARAMETER ContentType
The ContentType of the custom body parser.

.EXAMPLE
Remove-PodeBodyParser -ContentType 'application/json'
#>
function Remove-PodeBodyParser
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType
    )

    # if there's no parser for the type, return
    if (!$PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
        return
    }

    $PodeContext.Server.BodyParsers.Remove($ContentType) | Out-Null
}
src\Public\Logging.ps1
<#
.SYNOPSIS
Create a new method of outputting logs.

.DESCRIPTION
Create a new method of outputting logs.

.PARAMETER Terminal
If supplied, will use the inbuilt Terminal logging output method.

.PARAMETER File
If supplied, will use the inbuilt File logging output method.

.PARAMETER Path
The File Path of where to store the logs.

.PARAMETER Name
The File Name to prepend new log files using.

.PARAMETER MaxDays
The maximum number of days to keep logs, before Pode automatically removes them.

.PARAMETER MaxSize
The maximum size of a log file, before Pode starts writing to a new log file.

.PARAMETER Custom
If supplied, will allow you to create a Custom Logging output method.

.PARAMETER ScriptBlock
The ScriptBlock that defines how to output a log item.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Logging output method's ScriptBlock.

.EXAMPLE
$term_logging = New-PodeLoggingMethod -Terminal

.EXAMPLE
$file_logging = New-PodeLoggingMethod -File -Path ./logs -Name 'requests'

.EXAMPLE
$custom_logging = New-PodeLoggingMethod -Custom -ScriptBlock { /* logic */ }
#>
function New-PodeLoggingMethod
{
    [CmdletBinding(DefaultParameterSetName='Terminal')]
    [OutputType([hashtable])]
    param (
        [Parameter(ParameterSetName='Terminal')]
        [switch]
        $Terminal,

        [Parameter(ParameterSetName='File')]
        [switch]
        $File,

        [Parameter(ParameterSetName='File')]
        [string]
        $Path = './logs',

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Name,

        [Parameter(ParameterSetName='File')]
        [ValidateScript({
            if ($_ -lt 0) {
                throw "MaxDays must be 0 or greater, but got: $($_)s"
            }

            return $true
        })]
        [int]
        $MaxDays = 0,

        [Parameter(ParameterSetName='File')]
        [ValidateScript({
            if ($_ -lt 0) {
                throw "MaxSize must be 0 or greater, but got: $($_)s"
            }

            return $true
        })]
        [int]
        $MaxSize = 0,

        [Parameter(ParameterSetName='Custom')]
        [switch]
        $Custom,

        [Parameter(Mandatory=$true, ParameterSetName='Custom')]
        [ValidateScript({
            if (Test-IsEmpty $_) {
                throw "A non-empty ScriptBlock is required for the Custom logging output method"
            }

            return $true
        })]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName='Custom')]
        [object[]]
        $ArgumentList
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'terminal' {
            return @{
                ScriptBlock = (Get-PodeLoggingTerminalMethod)
                Arguments = @{}
            }
        }

        'file' {
            $Path = (Protect-PodeValue -Value $Path -Default './logs')
            $Path = (Get-PodeRelativePath -Path $Path -JoinRoot)
            New-Item -Path $Path -ItemType Directory -Force | Out-Null

            return @{
                ScriptBlock = (Get-PodeLoggingFileMethod)
                Arguments = @{
                    Name = $Name
                    Path = $Path
                    MaxDays = $MaxDays
                    MaxSize = $MaxSize
                    FileId = 0
                    Date = $null
                    NextClearDown = [datetime]::Now.Date
                }
            }
        }

        'custom' {
            return @{
                ScriptBlock = $ScriptBlock
                Arguments = $ArgumentList
            }
        }
    }
}


<#
.SYNOPSIS
Enables Request Logging using a supplied output method.

.DESCRIPTION
Enables Request Logging using a supplied output method.

.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).

.PARAMETER Raw
If supplied, the log item returned will be the raw Request item as a hashtable and not a string (for Custom methods).

.EXAMPLE
New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
#>
function Enable-PodeRequestLogging
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $Method,

        [switch]
        $Raw
    )

    Test-PodeIsServerless -FunctionName 'Enable-PodeRequestLogging' -ThrowError

    $name = Get-PodeRequestLoggingName

    # error if it's already enabled
    if ($PodeContext.Server.Logging.Types.Contains($name)) {
        throw 'Request Logging has already been enabled'
    }

    # ensure the Method contains a scriptblock
    if (Test-IsEmpty $Method.ScriptBlock) {
        throw "The supplied output Method for Request Logging requires a valid ScriptBlock"
    }

    # add the request logger
    $PodeContext.Server.Logging.Types[$name] = @{
        Method = $Method
        ScriptBlock = (Get-PodeLoggingInbuiltType -Type Requests)
        Arguments = @{
            Raw = $Raw
        }
    }
}

<#
.SYNOPSIS
Disables Request Logging.

.DESCRIPTION
Disables Request Logging.

.EXAMPLE
Disable-PodeRequestLogging
#>
function Disable-PodeRequestLogging
{
    [CmdletBinding()]
    param()

    Remove-PodeLogger -Name (Get-PodeRequestLoggingName)
}

<#
.SYNOPSIS
Enables Error Logging using a supplied output method.

.DESCRIPTION
Enables Error Logging using a supplied output method.

.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).

.PARAMETER Levels
The Levels of errors that should be logged (default is Error).

.PARAMETER Raw
If supplied, the log item returned will be the raw Error item as a hashtable and not a string (for Custom methods).

.EXAMPLE
New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
#>
function Enable-PodeErrorLogging
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $Method,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug')]
        [string[]]
        $Levels = @('Error'),

        [switch]
        $Raw
    )

    $name = Get-PodeErrorLoggingName

    # error if it's already enabled
    if ($PodeContext.Server.Logging.Types.Contains($name)) {
        throw 'Error Logging has already been enabled'
    }

    # ensure the Method contains a scriptblock
    if (Test-IsEmpty $Method.ScriptBlock) {
        throw "The supplied output Method for Error Logging requires a valid ScriptBlock"
    }

    # add the error logger
    $PodeContext.Server.Logging.Types[$name] = @{
        Method = $Method
        ScriptBlock = (Get-PodeLoggingInbuiltType -Type Errors)
        Arguments = @{
            Raw = $Raw
            Levels = $Levels
        }
    }
}

<#
.SYNOPSIS
Disables Error Logging.

.DESCRIPTION
Disables Error Logging.

.EXAMPLE
Disable-PodeErrorLogging
#>
function Disable-PodeErrorLogging
{
    [CmdletBinding()]
    param()

    Remove-PodeLogger -Name (Get-PodeErrorLoggingName)
}

<#
.SYNOPSIS
Adds a custom Logging method for parsing custom log items.

.DESCRIPTION
Adds a custom Logging method for parsing custom log items.

.PARAMETER Name
A unique Name for the Logging method.

.PARAMETER Method
The Method to use for output the log entry (From New-PodeLoggingMethod).

.PARAMETER ScriptBlock
The ScriptBlock defining logic that transforms an item, and returns it for outputting.

.PARAMETER ArgumentList
An array of arguments to supply to the Custom Logger's ScriptBlock.

.EXAMPLE
New-PodeLoggingMethod -Terminal | Add-PodeLogger -Name 'Main' -ScriptBlock { /* logic */ }
#>
function Add-PodeLogger
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [hashtable]
        $Method,

        [Parameter(Mandatory=$true)]
        [ValidateScript({
            if (Test-IsEmpty $_) {
                throw "A non-empty ScriptBlock is required for the logging method"
            }

            return $true
        })]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList
    )

    # ensure the name doesn't already exist
    if ($PodeContext.Server.Logging.Types.ContainsKey($Name)) {
        throw "Logging method already defined: $($Name)"
    }

    # ensure the Method contains a scriptblock
    if (Test-IsEmpty $Method.ScriptBlock) {
        throw "The supplied output Method for the '$($Name)' Logging method requires a valid ScriptBlock"
    }

    # add logging method to server
    $PodeContext.Server.Logging.Types[$Name] = @{
        Method = $Method
        ScriptBlock = $ScriptBlock
        Arguments = $ArgumentList
    }
}

<#
.SYNOPSIS
Removes a configured Logging method.

.DESCRIPTION
Removes a configured Logging method.

.PARAMETER Name
The Name of the Logging method.

.EXAMPLE
Remove-PodeLogger -Name 'LogName'
#>
function Remove-PodeLogger
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name
    )

    $PodeContext.Server.Logging.Types.Remove($Name) | Out-Null
}

<#
.SYNOPSIS
Clears all Logging methods that have been configured.

.DESCRIPTION
Clears all Logging methods that have been configured.

.EXAMPLE
Clear-PodeLoggers
#>
function Clear-PodeLoggers
{
    [CmdletBinding()]
    param()

    $PodeContext.Server.Logging.Types.Clear()
}

<#
.SYNOPSIS
Writes and Exception or ErrorRecord using the inbuilt error logging.

.DESCRIPTION
Writes and Exception or ErrorRecord using the inbuilt error logging.

.PARAMETER Exception
An Exception to write.

.PARAMETER ErrorRecord
An ErrorRecord to write.

.PARAMETER Level
The Level of the error being logged.

.PARAMETER CheckInnerException
If supplied, any exceptions are check for inner exceptions. If one is present, this is also logged.

.EXAMPLE
try { /* logic */ } catch { $_ | Write-PodeErrorLog }

.EXAMPLE
[System.Exception]::new('error message') | Write-PodeErrorLog
#>
function Write-PodeErrorLog
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='Exception')]
        [System.Exception]
        $Exception,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName='Error')]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug')]
        [string]
        $Level = 'Error',

        [Parameter(ParameterSetName='Exception')]
        [switch]
        $CheckInnerException
    )

    # do nothing if logging is disabled, or error logging isn't setup
    $name = Get-PodeErrorLoggingName
    if (!(Test-PodeLoggerEnabled -Name $name)) {
        return
    }

    # do nothing if the error level isn't present
    $_args = (Get-PodeLogger -Name $name).Arguments
    if (@($_args.Levels) -inotcontains $Level) {
        return
    }

    # build error object for what we need
    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'exception' {
            $item = @{
                Category = $Exception.Source
                Message = $Exception.Message
                StackTrace = $Exception.StackTrace
            }
        }

        'error' {
            $item = @{
                Category = $ErrorRecord.CategoryInfo.ToString()
                Message = $ErrorRecord.Exception.Message
                StackTrace = $ErrorRecord.ScriptStackTrace
            }
        }
    }

    # add general info
    $item['Server'] = $env:COMPUTERNAME
    $item['Level'] = $Level
    $item['Date'] = [datetime]::Now
    $item['ThreadId'] = [int]$ThreadId

    # add the item to be processed
    $PodeContext.LogsToProcess.Add(@{
        Name = $name
        Item = $item
    }) | Out-Null

    # for exceptions, check the inner exception
    if ($CheckInnerException -and ($null -ne $Exception.InnerException) -and ![string]::IsNullOrWhiteSpace($Exception.InnerException.Message)) {
        $Exception.InnerException | Write-PodeErrorLog
    }
}

<#
.SYNOPSIS
Write an object to a configured custom Logging method.

.DESCRIPTION
Write an object to a configured custom Logging method.

.PARAMETER Name
The Name of the Logging method.

.PARAMETER InputObject
The Object to write.

.EXAMPLE
$object | Write-PodeLog -Name 'LogName'
#>
function Write-PodeLog
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [object]
        $InputObject
    )

    # do nothing if logging is disabled, or logger isn't setup
    if (!(Test-PodeLoggerEnabled -Name $Name)) {
        return
    }

    # add the item to be processed
    $PodeContext.LogsToProcess.Add(@{
        Name = $Name
        Item = $InputObject
    }) | Out-Null
}

<#
.SYNOPSIS
Masks values within a log item to protect sensitive information.

.DESCRIPTION
Masks values within a log item, or any string, to protect sensitive information.
Patterns, and the Mask, can be configured via the server.psd1 configuration file.

.PARAMETER Item
The string Item to mask values.

.EXAMPLE
$value = Protect-PodeLogItem -Item 'Username=Morty, Password=Hunter2'
#>
function Protect-PodeLogItem
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [string]
        $Item
    )

    # do nothing if there are no masks
    if (Test-IsEmpty $PodeContext.Server.Logging.Masking.Patterns) {
        return $item
    }

    # attempt to apply each mask
    foreach ($mask in $PodeContext.Server.Logging.Masking.Patterns) {
        if ($Item -imatch $mask) {
            # has both keep before/after
            if ($Matches.ContainsKey('keep_before') -and $Matches.ContainsKey('keep_after')) {
                $Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}")
            }

            # has just keep before
            elseif ($Matches.ContainsKey('keep_before')) {
                $Item = ($Item -ireplace $mask, "`${keep_before}$($PodeContext.Server.Logging.Masking.Mask)")
            }

            # has just keep after
            elseif ($Matches.ContainsKey('keep_after')) {
                $Item = ($Item -ireplace $mask, "$($PodeContext.Server.Logging.Masking.Mask)`${keep_after}")
            }

            # normal mask
            else {
                $Item = ($Item -ireplace $mask, $PodeContext.Server.Logging.Masking.Mask)
            }
        }
    }

    return $Item
}
src\Public\Headers.ps1
<#
.SYNOPSIS
Appends a header against the Response.

.DESCRIPTION
Appends a header against the Response. If the current context is serverless, then this function acts like Set-PodeHeader.

.PARAMETER Name
The name of the header.

.PARAMETER Value
The value to set against the header.

.EXAMPLE
Add-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33'
#>
function Add-PodeHeader
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

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

    switch ($PodeContext.Server.Type) {
        'http' {
            $WebEvent.Response.AppendHeader($Name, $Value) | Out-Null
        }

        'pode' {
            if (!$WebEvent.Response.Headers.ContainsKey($Name)) {
                $WebEvent.Response.Headers[$Name] = @()
            }

            $WebEvent.Response.Headers[$Name] += $Value
        }

        default {
            $WebEvent.Response.Headers[$Name] = $Value
        }
    }
}

<#
.SYNOPSIS
Tests if a header is present on the Request.

.DESCRIPTION
Tests if a header is present on the Request.

.PARAMETER Name
The name of the header to test.

.EXAMPLE
Test-PodeHeader -Name 'X-AuthToken'
#>
function Test-PodeHeader
{
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    $header = (Get-PodeHeader -Name $Name)
    return (![string]::IsNullOrWhiteSpace($header))
}

<#
.SYNOPSIS
Retrieves the value of a header from the Request.

.DESCRIPTION
Retrieves the value of a header from the Request.

.PARAMETER Name
The name of the header to retrieve.

.EXAMPLE
Get-PodeHeader -Name 'X-AuthToken'
#>
function Get-PodeHeader
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    if ($PodeContext.Server.Type -ine 'http') {
        $header = $WebEvent.Request.Headers.$Name
    }
    else {
        $header = $WebEvent.Request.Headers[$Name]
    }

    return $header
}

<#
.SYNOPSIS
Sets a header on the Response, clearing all current values for the header.

.DESCRIPTION
Sets a header on the Response, clearing all current values for the header.

.PARAMETER Name
The name of the header.

.PARAMETER Value
The value to set against the header.

.EXAMPLE
Set-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33'
#>
function Set-PodeHeader
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

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


    switch ($PodeContext.Server.Type) {
        'http' {
            $WebEvent.Response.AddHeader($Name, $Value) | Out-Null
        }

        'pode' {
            $WebEvent.Response.Headers[$Name] = @($Value)
        }

        default {
            $WebEvent.Response.Headers[$Name] = $Value
        }
    }
}

Log in or click on link to see number of positives.

In cases where actual malware is found, the packages are subject to removal. Software sometimes has false positives. Moderators do not necessarily validate the safety of the underlying software, only that a package retrieves software from the official distribution point and/or validate embedded software against official distribution point (where distribution rights allow redistribution).

Chocolatey Pro provides runtime protection from possible malware.

Add to Builder Version Downloads Last Updated Status
Pode 2.8.0 236 Friday, February 3, 2023 Approved
Pode 2.7.2 130 Tuesday, October 25, 2022 Approved
Pode 2.7.1 112 Thursday, July 21, 2022 Approved
Pode 2.7.0 122 Wednesday, June 22, 2022 Approved
Pode 2.6.2 165 Wednesday, March 2, 2022 Approved
Pode 2.6.1 87 Monday, February 21, 2022 Approved
Pode 2.6.0 87 Thursday, February 10, 2022 Approved
Pode 2.5.2 99 Tuesday, January 4, 2022 Approved
Pode 2.5.1 94 Tuesday, December 21, 2021 Approved
Pode 2.5.0 111 Saturday, November 13, 2021 Approved
Pode 2.4.2 127 Monday, September 13, 2021 Approved
Pode 2.4.1 112 Monday, August 9, 2021 Approved
Pode 2.4.0 94 Wednesday, July 21, 2021 Approved
Pode 2.3.0 120 Tuesday, June 1, 2021 Approved
Pode 2.2.3 136 Saturday, April 10, 2021 Approved
Pode 2.2.2 90 Friday, April 9, 2021 Approved
Pode 2.2.1 94 Saturday, March 27, 2021 Approved
Pode 2.2.0 111 Sunday, March 21, 2021 Approved
Pode 2.1.1 113 Friday, February 19, 2021 Approved
Pode 2.1.0 1164 Wednesday, February 3, 2021 Approved
Pode 2.0.3 148 Monday, December 21, 2020 Approved
Pode 2.0.2 121 Saturday, December 5, 2020 Approved
Pode 2.0.1 105 Sunday, November 29, 2020 Approved
Pode 2.0.0 175 Saturday, November 14, 2020 Approved
Pode 1.8.4 167 Friday, October 16, 2020 Approved
Pode 1.8.3 150 Sunday, September 20, 2020 Approved
Pode 1.8.2 190 Friday, July 31, 2020 Approved
Pode 1.8.1 169 Friday, June 26, 2020 Approved
Pode 1.8.0 180 Sunday, May 24, 2020 Approved
Pode 1.7.3 182 Sunday, May 10, 2020 Approved
Pode 1.7.2 162 Monday, April 27, 2020 Approved
Pode 1.7.1 154 Friday, April 17, 2020 Approved
Pode 1.7.0 171 Friday, April 10, 2020 Approved
Pode 1.6.1 223 Saturday, March 7, 2020 Approved
Pode 1.6.0 184 Tuesday, March 3, 2020 Approved
Pode 1.5.0 220 Sunday, February 2, 2020 Approved
Pode 1.4.0 193 Friday, January 10, 2020 Approved
Pode 1.3.0 179 Friday, December 27, 2019 Approved
Pode 1.2.1 200 Monday, December 2, 2019 Approved
Pode 1.2.0 185 Wednesday, November 13, 2019 Approved
Pode 1.1.0 201 Saturday, September 28, 2019 Approved
Pode 1.0.1 191 Wednesday, September 4, 2019 Approved
Pode 1.0.0 192 Monday, September 2, 2019 Approved
Pode 0.32.0 230 Friday, June 28, 2019 Approved
Pode 0.31.0 193 Tuesday, June 11, 2019 Approved
Pode 0.30.0 194 Sunday, May 26, 2019 Approved
Pode 0.29.0 192 Friday, May 10, 2019 Approved
Pode 0.28.1 227 Tuesday, April 16, 2019 Approved
Pode 0.28.0 180 Saturday, April 13, 2019 Approved
Pode 0.27.3 200 Thursday, April 4, 2019 Approved
Pode 0.27.2 218 Wednesday, March 27, 2019 Approved
Pode 0.27.1 210 Saturday, March 16, 2019 Approved
Pode 0.27.0 209 Thursday, March 14, 2019 Approved
Pode 0.26.0 233 Sunday, February 17, 2019 Approved
Pode 0.25.0 228 Tuesday, February 5, 2019 Approved
Pode 0.24.0 252 Friday, January 18, 2019 Approved
Pode 0.23.0 245 Monday, December 24, 2018 Approved
Pode 0.22.0 236 Friday, December 7, 2018 Approved
Pode 0.21.0 258 Friday, November 2, 2018 Approved
Pode 0.20.0 260 Saturday, October 20, 2018 Approved
Pode 0.19.1 228 Tuesday, October 9, 2018 Approved
Pode 0.19.0 244 Friday, September 14, 2018 Approved
Pode 0.18.0 234 Saturday, August 25, 2018 Approved
Pode 0.17.0 203 Sunday, August 19, 2018 Approved
Pode 0.16.0 250 Wednesday, August 8, 2018 Approved
Pode 0.15.0 274 Friday, July 13, 2018 Approved
Pode 0.14.0 246 Friday, July 6, 2018 Approved
Pode 0.13.0 247 Saturday, June 23, 2018 Approved
Pode 0.12.0 239 Friday, June 15, 2018 Approved
Pode 0.11.3 275 Sunday, June 10, 2018 Approved
Pode 0.11.2 271 Friday, June 8, 2018 Approved
Pode 0.11.1 291 Friday, June 1, 2018 Approved
Pode 0.11.0 258 Wednesday, May 30, 2018 Approved
Pode 0.10.1 311 Wednesday, May 16, 2018 Approved
Pode 0.9.0 332 Thursday, January 11, 2018 Approved

This package has no dependencies.

Discussion for the Pode Package

Ground Rules:

  • This discussion is only about Pode and the Pode package. If you have feedback for Chocolatey, please contact the Google Group.
  • This discussion will carry over multiple versions. If you have a comment about a particular version, please note that in your comments.
  • The maintainers of this Chocolatey Package will be notified about new comments that are posted to this Disqus thread, however, it is NOT a guarantee that you will get a response. If you do not hear back from the maintainers after posting a message below, please follow up by using the link on the left side of this page or follow this link to contact maintainers. If you still hear nothing back, please follow the package triage process.
  • Tell us what you love about the package or Pode, or tell us what needs improvement.
  • Share your experiences with the package, or extra configuration or gotchas that you've found.
  • If you use a url, the comment will be flagged for moderation until you've been whitelisted. Disqus moderated comments are approved on a weekly schedule if not sooner. It could take between 1-5 days for your comment to show up.
comments powered by Disqus