Notices: This section not yet converted to new layout. Download stats are rolling back out.

Pode

0.32.0

Package test results are passing.

This package was approved as a trusted package on 8/7/2019.

Pode is 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.

Features

  • Cross-platform using PowerShell Core (with support for PS4.0+)
  • Docker support, including images for ARM/Raspberry Pi
  • Azure Functions and AWS Lambda support
  • Listen on a single or multiple IP address/hostnames
  • Support for HTTP, HTTPS, TCP and SMTP
  • 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 request logging to CLI, Files, and custom loggers to 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
  • (Windows) Generate/bind self-signed certificates, and signed certificates
  • (Windows) Open the hosted server as a desktop application

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

C:\> choco install pode

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

C:\> choco upgrade pode

Files

Hide
  • src\LICENSE.txt Show
    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 Show
    #
    # 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 = '0.32.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 = '3.0'
    
        # Functions to export from this Module
        FunctionsToExport = @(
            'Route',
            'Handler',
            'Tcp',
            'Server',
            'Engine',
            'Html',
            'Json',
            'View',
            'Xml',
            'Pode',
            'Timer',
            'Logger',
            'Csv',
            'Test-IsUnix',
            'Test-IsWindows',
            'Test-IsPSCore',
            'Test-Empty',
            'Status',
            'Redirect',
            'Include',
            'Lock',
            'State',
            'Listen',
            'Access',
            'Limit',
            'Stopwatch',
            'Dispose',
            'Stream',
            'Schedule',
            'Middleware',
            'Endware',
            'Session',
            'Invoke-ScriptBlock',
            'Auth',
            'Attach',
            'Script',
            'Import',
            'Coalesce',
            'Save',
            'Get-PodeConfiguration',
            'Root',
            'Flash',
            'Await',
            'Load',
            'Config',
            'Cookie',
            'Csrf',
            'Gui',
            'Text',
            'File',
            'Header'
        )
    
        # 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')
    
                # 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/v0.32.0'
    
            }
        }
    }
    
  • src\Pode.psm1 Show
    # get existing functions from memory for later comparison
    $sysfuncs = Get-ChildItem Function:
    
    # load pode functions
    $root = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
    Get-ChildItem "$($root)\Tools\*.ps1" | Resolve-Path | ForEach-Object { . $_ }
    
    # check if there are any extensions and load them
    $ext = 'C:/Pode/Extensions'
    if ($PSVersionTable.Platform -ieq 'unix') {
        $ext = '/usr/src/pode/extensions'
    }
    
    if (Test-Path $ext) {
        Get-ChildItem "$($ext)/*.ps1" | Resolve-Path | ForEach-Object { . $_ }
    }
    
    # get functions from memory and compare to existing to find new functions added
    $podefuncs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ }
    
    # export the module
    Export-ModuleMember -Function ($podefuncs.Name)
  • src\Tools\Authentication.ps1 Show
    function Auth
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('use', 'check')]
            [Alias('a')]
            [string]
            $Action,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('v')]
            [object]
            $Validator,
    
            [Parameter()]
            [Alias('p')]
            [scriptblock]
            $Parser,
    
            [Parameter()]
            [Alias('o')]
            [hashtable]
            $Options,
    
            [Parameter()]
            [Alias('t')]
            [string]
            $Type,
    
            [switch]
            [Alias('c')]
            $Custom
        )
    
        # for the 'use' action, ensure we have a validator. and a parser for custom types
        if ($Action -ieq 'use') {
            # was a validator passed
            if (Test-Empty $Validator) {
                throw "Authentication method '$($Name)' is missing required Validator script"
            }
    
            # is the validator a string/scriptblock?
            $vTypes = @('string', 'scriptblock')
            if ($vTypes -inotcontains (Get-PodeType $Validator).Name) {
                throw "Authentication method '$($Name)' has an invalid validator supplied, should be one of: $($vTypes -join ', ')"
            }
    
            # don't fail if custom and type supplied, and it's already defined
            if ($Custom)
            {
                $typeDefined = (![string]::IsNullOrWhiteSpace($Type) -and $PodeContext.Server.Authentications.ContainsKey($Type))
                if (!$typeDefined -and (Test-Empty $Parser)) {
                    throw "Custom authentication method '$($Name)' is missing required Parser script"
                }
            }
        }
    
        # invoke the appropriate auth logic for the action
        switch ($Action.ToLowerInvariant())
        {
            'use' {
                Invoke-PodeAuthUse -Name $Name -Type $Type -Validator $Validator -Parser $Parser -Options $Options -Custom:$Custom
            }
    
            'check' {
                return (Invoke-PodeAuthCheck -Name $Name -Options $Options)
            }
        }
    }
    
    function Invoke-PodeAuthUse
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter(Mandatory=$true)]
            [object]
            $Validator,
    
            [Parameter()]
            [string]
            $Type,
    
            [Parameter()]
            [scriptblock]
            $Parser,
    
            [Parameter()]
            [hashtable]
            $Options,
    
            [switch]
            $Custom
        )
    
        # get the auth data
        $AuthData = (Get-PodeAuthMethod -Name $Name -Type $Type -Validator $Validator -Parser $Parser -Custom:$Custom)
    
        # ensure the name doesn't already exist
        if ($PodeContext.Server.Authentications.ContainsKey($AuthData.Name)) {
            throw "Authentication method '$($AuthData.Name)' already defined"
        }
    
        # ensure the parser/validators aren't just empty scriptblocks
        if (Test-Empty $AuthData.Parser) {
            throw "Authentication method '$($AuthData.Name)' is has no Parser ScriptBlock logic defined"
        }
    
        if (Test-Empty $AuthData.Validator) {
            throw "Authentication method '$($AuthData.Name)' is has no Validator ScriptBlock logic defined"
        }
    
        # setup object for auth method
        $obj = @{
            'Type' = $AuthData.Type;
            'Options' = $Options;
            'Parser' = $AuthData.Parser;
            'Validator' = $AuthData.Validator;
            'Custom' = $AuthData.Custom;
        }
    
        # apply auth method to session
        $PodeContext.Server.Authentications[$AuthData.Name] = $obj
    }
    
    function Invoke-PodeAuthCheck
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter()]
            [hashtable]
            $Options
        )
    
        # ensure the auth type exists
        if (!$PodeContext.Server.Authentications.ContainsKey($Name)) {
            throw "Authentication method '$($Name)' is not defined"
        }
    
        # coalesce the options, and set auth type for middleware
        $Options = (coalesce $Options @{})
        $Options.AuthType = $Name
    
        # setup the middleware logic
        $logic = {
            param($e)
    
            # Route options for using sessions
            $storeInSession = ($e.Middleware.Options.Session -ne $false)
            $usingSessions = (!(Test-Empty $e.Session))
    
            # check for logout command
            if ($e.Middleware.Options.Logout -eq $true) {
                Remove-PodeAuth -Event $e
                return (Set-PodeAuthStatus -StatusCode 302 -Options $e.Middleware.Options)
            }
    
            # if the session already has a user/isAuth'd, then setup method and return
            if ($usingSessions -and !(Test-Empty $e.Session.Data.Auth.User) -and $e.Session.Data.Auth.IsAuthenticated) {
                $e.Auth = $e.Session.Data.Auth
                return (Set-PodeAuthStatus -Options $e.Middleware.Options)
            }
    
            # check if the login flag is set, in which case just return
            if ($e.Middleware.Options.Login -eq $true) {
                if (!(Test-Empty $e.Session.Data.Auth)) {
                    Remove-PodeSessionCookie -Session $e.Session
                }
    
                return $true
            }
    
            # get the auth type
            $auth = $PodeContext.Server.Authentications[$e.Middleware.Options.AuthType]
    
            # validate the request and get a user
            try {
                # if it's a custom type the parser will return the data for use to pass to the validator
                if ($auth.Custom) {
                    $data = (Invoke-ScriptBlock -ScriptBlock $auth.Parser -Arguments @($e, $auth.Options) -Return -Splat)
                    $data += $auth.Options
    
                    $result = (Invoke-ScriptBlock -ScriptBlock $auth.Validator -Arguments $data -Return -Splat)
                }
                else {
                    $result = (Invoke-ScriptBlock -ScriptBlock $auth.Parser -Arguments @($e, $auth) -Return -Splat)
                }
            }
            catch {
                $_.Exception | Out-Default
                return (Set-PodeAuthStatus -StatusCode 500 -Description $_.Exception.Message -Options $e.Middleware.Options)
            }
    
            # if there is no result return false (failed auth)
            if ((Test-Empty $result) -or (Test-Empty $result.User)) {
                return (Set-PodeAuthStatus -StatusCode (coalesce $result.Code 401) `
                    -Description $result.Message -Options $e.Middleware.Options)
            }
    
            # 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 $e.Middleware.Options)
        }
    
        # return the middleware
        return @{
            'Logic' = $logic;
            'Options' = $Options;
        }
    }
    
    function Get-PodeAuthMethod
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter(Mandatory=$true)]
            [object]
            $Validator,
    
            [Parameter()]
            [string]
            $Type,
    
            [Parameter()]
            [scriptblock]
            $Parser,
    
            [switch]
            $Custom
        )
    
        # set type as name, if no type passed
        if ([string]::IsNullOrWhiteSpace($Type)) {
            $Type = $Name
        }
    
        # if the validator is a string - check and get an inbuilt validator
        if ((Get-PodeType $Validator).Name -ieq 'string') {
            $Validator = (Get-PodeAuthValidator -Validator $Validator)
        }
    
        # first, is it just a custom type?
        if ($Custom) {
            # if type supplied, re-use an already defined custom type's parser
            if ($PodeContext.Server.Authentications.ContainsKey($Type))
            {
                $Parser = $PodeContext.Server.Authentications[$Type].Parser
            }
    
            return @{
                'Name' = $Name;
                'Type' = $Type;
                'Custom' = $true;
                'Parser' = $Parser;
                'Validator' = $Validator;
            }
        }
    
        # otherwise, check the inbuilt ones
        switch ($Type.ToLowerInvariant())
        {
            'basic' {
                return (Get-PodeAuthBasic -Name $Name -ScriptBlock $Validator)
            }
    
            'form' {
                return (Get-PodeAuthForm -Name $Name -ScriptBlock $Validator)
            }
        }
    
        # if we get here, check if a parser was passed for custom type
        if (Test-Empty $Parser) {
            throw "Authentication method '$($Type)' does not exist as an inbuilt type, nor has a Parser been passed for a custom type"
        }
    
        # a parser was passed, it is a custom type
        return @{
            'Name' = $Name;
            'Type' = $Type;
            'Custom' = $true;
            'Parser' = $Parser;
            'Validator' = $Validator;
        }
    }
    
    function Remove-PodeAuth
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Event
        )
    
        # blank out the auth
        $Event.Auth = @{}
    
        # if a session auth is found, blank it
        if (!(Test-Empty $Event.Session.Data.Auth)) {
            $Event.Session.Data.Remove('Auth')
        }
    
        # redirect to a failure url, or onto the current path?
        if (Test-Empty $Event.Middleware.Options.FailureUrl) {
            $Event.Middleware.Options.FailureUrl = $Event.Request.Url.AbsolutePath
        }
    
        # 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 = (coalesce $Options.FailureMessage $Description)
    
            # add error to flash if flagged
            if ([bool]$Options.FailureFlash) {
                flash add 'auth-error' $Description
            }
    
            # check if we have a failure url redirect
            if (!(Test-Empty $Options.FailureUrl)) {
                redirect $Options.FailureUrl
            }
            else {
                status $StatusCode $Description
            }
    
            return $false
        }
    
        # if no statuscode, success
        else
        {
            # check if we have a success url redirect
            if (!(Test-Empty $Options.SuccessUrl)) {
                redirect $Options.SuccessUrl
                return $false
            }
    
            return $true
        }
    }
    
    function Get-PodeAuthValidator
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Validator
        )
    
        # source the script for the validator
        switch ($Validator.ToLowerInvariant()) {
            'windows-ad' {
                # 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+'
                }
    
                # setup the AD vaidator
                return {
                    param($username, $password, $options)
    
                    # validate and retrieve the AD user
                    $result = Get-PodeAuthADUser -FQDN $options.Fqdn -Username $username -Password $password
    
                    # if there's a message, fail and return the message
                    if (!(Test-Empty $result.Message)) {
                        return $result
                    }
    
                    # if there's no user, then, err, oops
                    if (Test-Empty $result.User) {
                        return @{ 'Message' = 'An unexpected error occured' }
                    }
    
                    # if there are no groups/users supplied, return the user
                    if ((Test-Empty $options.Users) -and (Test-Empty $options.Groups)){
                        return $result
                    }
    
                    # before checking supplied groups, is the user in the supplied list of authorised users?
                    if (!(Test-Empty $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-Empty $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' }
                }
            }
    
            default {
                throw "An inbuilt validator does not exist for '$($Validator)'"
            }
        }
    }
    
    function Get-PodeAuthADUser
    {
        param (
            [Parameter()]
            [string]
            $FQDN,
    
            [Parameter()]
            [string]
            $Username,
    
            [Parameter()]
            [string]
            $Password
        )
    
        try
        {
            # setup the dns domain
            if (Test-Empty $FQDN) {
                $FQDN = $env:USERDNSDOMAIN
            }
    
            # validate the user's AD creds
            $ad = (New-Object System.DirectoryServices.DirectoryEntry "LDAP://$($FQDN)", "$($Username)", "$($Password)")
            if (Test-Empty $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-Empty $user) {
                return @{ 'Message' = 'User not found in Active Directory' }
            }
    
            # get the users groups
            $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-Empty $ad.distinguishedName)) {
                dispose $query
                dispose $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
        }
    }
    
    function Get-PodeAuthBasic
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock
        )
    
        $parser = {
            param($e, $auth)
    
            # get the auth header
            $header = (Get-PodeHeader -Name 'Authorization')
            if ($null -eq $header) {
                return @{
                    'User' = $null;
                    'Message' = 'No Authorization header found';
                    'Code' = 401;
                }
            }
    
            # ensure the first atom is basic (or opt override)
            $atoms = $header -isplit '\s+'
            $authType = (coalesce $auth.Options.Name 'Basic')
    
            if ($atoms[0] -ine $authType) {
                return @{
                    'User' = $null;
                    'Message' = "Header is not $($authType) Authorization";
                }
            }
    
            # decode the aut header
            $encType = (coalesce $auth.Options.Encoding 'ISO-8859-1')
    
            try {
                $enc = [System.Text.Encoding]::GetEncoding($encType)
            }
            catch {
                return @{
                    'User' = $null;
                    'Message' = 'Invalid encoding specified for Authorization';
                    'Code' = 400;
                }
            }
    
            try {
                $decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1]))
            }
            catch {
                return @{
                    'User' = $null;
                    'Message' = 'Invalid Base64 string found in Authorization header';
                    'Code' = 400;
                }
            }
    
            # validate and return user/result
            $index = $decoded.IndexOf(':')
            $u = $decoded.Substring(0, $index)
            $p = $decoded.Substring($index + 1)
    
            return (Invoke-ScriptBlock -ScriptBlock $auth.Validator -Arguments @($u, $p, $auth.Options) -Return -Splat)
        }
    
        return @{
            'Name' = $Name;
            'Type' = 'Basic';
            'Custom' = $false;
            'Parser' = $parser;
            'Validator' = $ScriptBlock;
        }
    }
    
    function Get-PodeAuthForm
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock
        )
    
        $parser = {
            param($e, $auth)
    
            # get user/pass keys to get from payload
            $userField = (coalesce $auth.Options.UsernameField 'username')
            $passField = (coalesce $auth.Options.PasswordField 'password')
    
            # get the user/pass
            $username = $e.Data.$userField
            $password = $e.Data.$passField
    
            # if either are empty, deny
            if ((Test-Empty $username) -or (Test-Empty $password)) {
                return @{
                    'User' = $null;
                    'Message' = 'Username or Password not supplied';
                    'Code' = 401;
                }
            }
    
            # validate and return
            return (Invoke-ScriptBlock -ScriptBlock $auth.Validator -Arguments @($username, $password, $auth.Options) -Return -Splat)
        }
    
        return @{
            'Name' = $Name;
            'Type' = 'Form';
            'Custom' = $false;
            'Parser' = $parser;
            'Validator' = $ScriptBlock;
        }
    }
  • src\Tools\Context.ps1 Show
    function New-PodeContext
    {
        param (
            [scriptblock]
            $ScriptBlock,
    
            [int]
            $Threads = 1,
    
            [int]
            $Interval = 0,
    
            [string]
            $ServerRoot,
    
            [string]
            $Name = $null,
    
            [string[]]
            $FileMonitorExclude = $null,
    
            [string[]]
            $FileMonitorInclude = $null,
    
            [string]
            $ServerType,
    
            [switch]
            $DisableLogging,
    
            [switch]
            $FileMonitor
        )
    
        # set a random server name if one not supplied
        if (Test-Empty $Name) {
            $Name = Get-PodeRandomName
        }
    
        # are we running in a serverless context
        $isServerless = (@('azure-functions', 'aws-lambda') -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 RequestsToLog -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)
    
        # 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-Empty $ctx.Server.Configuration.server.root)) {
            $ctx.Server.Root = Get-PodeRelativePath -Path $ctx.Server.Configuration.server.root -RootPath $ctx.Server.Root -JoinRoot -Resolve -TestPath
        }
    
        # setup file monitoring details (code has priority over config)
        if (!(Test-Empty $ctx.Server.Configuration)) {
            if (!$FileMonitor) {
                $FileMonitor = [bool]$ctx.Server.Configuration.server.fileMonitor.enable
            }
    
            if (Test-Empty $FileMonitorExclude) {
                $FileMonitorExclude = @($ctx.Server.Configuration.server.fileMonitor.exclude)
            }
    
            if (Test-Empty $FileMonitorInclude) {
                $FileMonitorInclude = @($ctx.Server.Configuration.server.fileMonitor.include)
            }
        }
    
        $ctx.Server.FileMonitor = @{
            Enabled = $FileMonitor
            Exclude = (Convert-PodePathPatternsToRegex -Paths $FileMonitorExclude)
            Include = (Convert-PodePathPatternsToRegex -Paths $FileMonitorInclude)
            ShowFiles = [bool]$ctx.Server.Configuration.server.fileMonitor.showFiles
            Files = @()
        }
    
        # set the server default type
        $ctx.Server.Type = ([string]::Empty)
        if ($Interval -gt 0) {
            $ctx.Server.Type = 'SERVICE'
        }
    
        if ($isServerless) {
            $ctx.Server.Type = $ServerType.ToUpperInvariant()
            $ctx.Server.IsServerless = $isServerless
        }
    
        # set the IP address details
        $ctx.Server.Endpoints = @()
    
        # 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 = @{
            'Engine' = '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' = $null;
            'smtp' = $null;
            'service' = $null;
        }
    
        # 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 = @{}
    
        # logging methods
        $ctx.Server.Logging = @{
            'Methods' = @{};
            'Disabled' = $DisableLogging;
        }
    
        # create new cancellation tokens
        $ctx.Tokens = @{
            'Cancellation' = New-Object System.Threading.CancellationTokenSource;
            'Restart' = New-Object System.Threading.CancellationTokenSource;
        }
    
        # requests that should be logged
        $ctx.RequestsToLog = New-Object System.Collections.ArrayList
    
        # middleware that needs to run
        $ctx.Server.Middleware = @()
    
        # endware that needs to run
        $ctx.Server.Endware = @()
    
        # runspace pools
        $ctx.RunspacePools = @{
            'Main' = $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)
        )
    
        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' = 1;
            '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 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)) {
            $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 RequestsToLog -Value $Context.RequestsToLog -PassThru |
            Add-Member -MemberType NoteProperty -Name Lockable -Value $Context.Lockable -PassThru |
            Add-Member -MemberType NoteProperty -Name Server -Value $Context.Server -PassThru)
    }
    
    function Get-PodeConfiguration
    {
        return (config)
    }
    
    function Config
    {
        return $PodeContext.Server.Configuration
    }
    
    function Open-PodeConfiguration
    {
        param (
            [Parameter()]
            [string]
            $ServerRoot = $null,
    
            [Parameter()]
            $Context
        )
    
        $config = @{}
    
        # set the path to the root config file
        $configPath = (Join-PodeServerRoot -Folder '.' -FilePath 'pode.json' -Root $ServerRoot)
    
        # check to see if an environmental config exists (if the env var is set)
        if (!(Test-Empty $env:PODE_ENVIRONMENT)) {
            $_path = (Join-PodeServerRoot -Folder '.' -FilePath "pode.$($env:PODE_ENVIRONMENT).json" -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 = (Get-Content $configPath -Raw | ConvertFrom-Json)
            Set-PodeWebConfiguration -Configuration $config -Context $Context
        }
    
        return $config
    }
    
    function Set-PodeWebConfiguration
    {
        param (
            [Parameter()]
            $Configuration,
    
            [Parameter()]
            $Context
        )
    
        # setup the main web config
        $Context.Server.Web = @{
            'Static' = @{
                'Defaults' = $Configuration.web.static.defaults;
                'Cache' = @{
                    'Enabled' = [bool]$Configuration.web.static.cache.enable;
                    'MaxAge' = [int](coalesce $Configuration.web.static.cache.maxAge 3600);
                    'Include' = (Convert-PodePathPatternsToRegex -Paths @($Configuration.web.static.cache.include) -NotSlashes);
                    'Exclude' = (Convert-PodePathPatternsToRegex -Paths @($Configuration.web.static.cache.exclude) -NotSlashes);
                }
            };
            'ErrorPages' = @{
                'ShowExceptions' = [bool]$Configuration.web.errorPages.showExceptions;
                'StrictContentTyping' = [bool]$Configuration.web.errorPages.strictContentTyping;
                'Default' = $Configuration.web.errorPages.default;
                'Routes' = @{};
            };
            'ContentType' = @{
                'Default' = $Configuration.web.contentType.default;
                'Routes' = @{};
            };
        }
    
        # setup content type route patterns for forced content types
        if ($null -ne $Configuration.web.contentType.routes) {
            $Configuration.web.contentType.routes.psobject.properties.name | ForEach-Object {
                $_pattern = $_
                $_type = $Configuration.web.contentType.routes.$_pattern
                $_pattern = (Convert-PodePathPatternToRegex -Path $_pattern -NotSlashes)
                $Context.Server.Web.ContentType.Routes[$_pattern] = $_type
            }
        }
    
        # setup content type route patterns for error pages
        if ($null -ne $Configuration.web.errorPages.routes) {
            $Configuration.web.errorPages.routes.psobject.properties.name | ForEach-Object {
                $_pattern = $_
                $_type = $Configuration.web.errorPages.routes.$_pattern
                $_pattern = (Convert-PodePathPatternToRegex -Path $_pattern -NotSlashes)
                $Context.Server.Web.ErrorPages.Routes[$_pattern] = $_type
            }
        }
    }
    
    function State
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('set', 'get', 'remove', 'save', 'restore')]
            [Alias('a')]
            [string]
            $Action,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('o')]
            [object]
            $Object
        )
    
        try {
            if ($null -eq $PodeContext -or $null -eq $PodeContext.Server.State) {
                return $null
            }
    
            switch ($Action.ToLowerInvariant())
            {
                'set' {
                    $PodeContext.Server.State[$Name] = $Object
                }
    
                'get' {
                    $Object = $PodeContext.Server.State[$Name]
                }
    
                'remove' {
                    $Object = $PodeContext.Server.State[$Name]
                    $PodeContext.Server.State.Remove($Name) | Out-Null
                }
    
                'save' {
                    $Path = Get-PodeRelativePath -Path $Name -JoinRoot
                    $PodeContext.Server.State |
                        ConvertTo-Json -Depth 10 |
                        Out-File -FilePath $Path -Force |
                        Out-Null
                    return
                }
    
                'restore' {
                    $Path = Get-PodeRelativePath -Path $Name -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
                        }
                    }
    
                    return
                }
            }
    
            return $Object
        }
        catch {
            $Error[0] | Out-Default
            throw $_.Exception
        }
    }
    
    function Listen
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('ipp', 'e', 'endpoint')]
            [string]
            $IPPort,
    
            [Parameter()]
            [ValidateSet('HTTP', 'HTTPS', 'SMTP', 'TCP')]
            [Alias('t')]
            [string]
            $Type,
    
            [Parameter()]
            [Alias('cert', 'cname')]
            [string]
            $Certificate = $null,
    
            [Parameter()]
            [Alias('thumb', 'cthumb')]
            [string]
            $Thumbprint = $null,
    
            [Parameter()]
            [Alias('n', 'id')]
            [string]
            $Name = $null,
    
            [switch]
            [Alias('f')]
            $Force
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'listen' -ThrowError
    
        # parse the endpoint for host/port info
        $_endpoint = Get-PodeEndpointInfo -Endpoint $IPPort
    
        # if a name was supplied, check it is unique
        if (!(Test-Empty $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' = $IPPort;
            'Port' = $null;
            'IsIPAddress' = $true;
            'HostName' = 'localhost';
            'Ssl' = $false;
            'Protocol' = $Type;
            'Certificate' = @{
                'Name' = $null;
                'Thumbprint' = $null;
            };
        }
    
        # 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 server type is https, set cert details
        if ($Type -ieq 'https') {
            $obj.Ssl = $true
            $obj.Certificate.Name = $Certificate
            $obj.Certificate.Thumbprint = $Thumbprint
        }
    
        # 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 (!$exists) {
            # has an endpoint already been defined for smtp/tcp?
            if (@('smtp', 'tcp') -icontains $Type -and $Type -ieq $PodeContext.Server.Type) {
                throw "An endpoint for $($Type.ToUpperInvariant()) has already been defined"
            }
    
            # set server type, ensure we aren't trying to change the server's type
            $_type = (iftet ($Type -ieq 'https') 'http' $Type)
            if ([string]::IsNullOrWhiteSpace($PodeContext.Server.Type)) {
                $PodeContext.Server.Type = $_type
            }
            elseif ($PodeContext.Server.Type -ine $_type) {
                throw "Cannot add $($Type.ToUpperInvariant()) endpoint when already listening to $($PodeContext.Server.Type.ToUpperInvariant()) endpoints"
            }
    
            # add the new endpoint
            $PodeContext.Server.Endpoints += $obj
        }
    }
    
    function Script
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Path
        )
    
        Import -Path $Path
    }
    
    function Import
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('p')]
            [string]
            $Path,
    
            [switch]
            [Alias('n')]
            $Now,
    
            [switch]
            [Alias('si')]
            $SnapIn
        )
    
        # for a snapin, just import it; for a module we need to check paths
        if ($SnapIn)
        {
            # 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($Path, ([ref]$exp))
    
            # import the snap-in now, if specified
            if ($Now) {
                Add-PSSnapin -Name $Path | Out-Null
            }
        }
        else
        {
            # if path is '.', replace with server root
            $_path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve
    
            # if the resolved path is empty, then it's a module name that was supplied
            if (Test-Empty $_path) {
                # check to see if module is in ps_modules
                $_psModulePath = Join-PodeServerRoot -Folder (Join-PodePaths @('ps_modules', $Path))
                if (Test-Path $_psModulePath) {
                    $_path = (Get-ChildItem (Join-PodePaths @($_psModulePath, '*', "$($Path).ps*1")) -Recurse -Force | Select-Object -First 1).FullName
                }
    
                # otherwise, use a global module
                else {
                    $_path = (Get-Module -Name $Path -ListAvailable | Select-Object -First 1).Path
                }
            }
    
            # else, we have a path, if it's a directory/wildcard then loop over all files
            else {
                $_paths = Get-PodeWildcardFiles -Path $Path -Wildcard '*.ps*1'
                if (!(Test-Empty $_paths)) {
                    foreach ($_path in $_paths) {
                        import -Path $_path -Now:$Now
                    }
    
                    return
                }
            }
    
            # if it's still empty, error
            if (Test-Empty $_path) {
                throw "Failed to import module: $($Path)"
            }
    
            # check if the path exists
            if (!(Test-PodePath $_path -NoStatus)) {
                throw "The module path does not exist: $(coalesce $_path $Path)"
            }
    
            # import the module into the runspace state
            $PodeContext.RunspaceState.ImportPSModule($_path)
    
            # import the module now, if specified
            if ($Now) {
                Import-Module $_path -Force -DisableNameChecking -Scope Global -ErrorAction Stop | Out-Null
            }
        }
    }
    
    function Load
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('p')]
            [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 (!(Test-Empty $_path)) {
            $_paths = Get-PodeWildcardFiles -Path $Path -Wildcard '*.ps1'
            if (!(Test-Empty $_paths)) {
                foreach ($_path in $_paths) {
                    load -Path $_path
                }
    
                return
            }
        }
    
        # check if the path exists
        if (!(Test-PodePath $_path -NoStatus)) {
            throw "The script path does not exist: $(coalesce $_path $Path)"
        }
    
        # dot-source the script
        . $_path
    }
    
    function New-PodeAutoRestartServer
    {
        # don't configure if not supplied, or running as serverless
        $config = (config)
        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) {
            Timer -Name '__pode_restart_period__' -Interval ($period * 60) -ScriptBlock {
                $PodeContext.Tokens.Restart.Cancel()
            } -Skip 1
        }
    
        # 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]) * * *"
            }
    
            Schedule -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) {
            Schedule -Name '__pode_restart_crons__' -Cron @($crons) -ScriptBlock {
                $PodeContext.Tokens.Restart.Cancel()
            }
        }
    }
  • src\Tools\Cookies.ps1 Show
    function Cookie
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('Check', 'Exists', 'Extend', 'Get', 'Remove', 'Secrets', 'Set')]
            [Alias('a')]
            [string]
            $Action,
    
            [Parameter(Mandatory=$true)]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('v')]
            [string]
            $Value,
    
            [Parameter()]
            [Alias('s')]
            [string]
            $Secret,
    
            [Parameter()]
            [Alias('ttl')]
            [int]
            $Duration = 0,
    
            [switch]
            [Alias('http')]
            $HttpOnly,
    
            [switch]
            [Alias('d')]
            $Discard,
    
            [switch]
            [Alias('ssl')]
            $Secure,
    
            [switch]
            [Alias('gs')]
            $GlobalSecret
        )
    
        # run logic for the action
        switch ($Action.ToLowerInvariant())
        {
            # add/set a cookie against the response
            'set' {
                return (Set-PodeCookie -Name $Name -Value $Value -Secret $Secret -Duration $Duration `
                    -HttpOnly:$HttpOnly -Discard:$Discard -Secure:$Secure -GlobalSecret:$GlobalSecret)
            }
    
            # get a cookie from the request
            'get' {
                return (Get-PodeCookie -Name $Name -Secret $Secret -GlobalSecret:$GlobalSecret)
            }
    
            # checks whether a given cookie exists on the request
            'exists' {
                return (Test-PodeCookieExists -Name $Name)
            }
    
            # removes a given cookie from the request/response
            'remove' {
                Remove-PodeCookie -Name $Name
            }
    
            # verifies whether a given cookie is signed
            'check' {
                return (Test-PodeCookieIsSigned -Name $Name -Secret $Secret -GlobalSecret:$GlobalSecret)
            }
    
            # extends a given cookies expiry (adding the cookie to the response)
            'extend' {
                return (Update-PodeCookieExpiry -Name $Name -Duration $Duration)
            }
    
            # set or get cookie secrets
            'secrets' {
                if ([string]::IsNullOrWhiteSpace($Value)) {
                    return ($PodeContext.Server.Cookies.Secrets[$Name])
                }
                else {
                    $PodeContext.Server.Cookies.Secrets[$Name] = $Value
                }
            }
        }
    }
    
    function Test-PodeCookieExists
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name
        )
    
        $cookie = $WebEvent.Cookies[$Name]
        return (($null -ne $cookie) -and ![string]::IsNullOrWhiteSpace($cookie.Value))
    }
    
    function Test-PodeCookieIsSigned
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name,
    
            [Parameter()]
            [string]
            $Secret,
    
            [switch]
            $GlobalSecret
        )
    
        # if the global secret flag is set, overwrite the passed secret
        if ($GlobalSecret) {
            $Secret = (Get-PodeCookieGlobalSecret)
        }
    
        $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))
    }
    
    function Get-PodeCookie
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name,
    
            [Parameter()]
            [string]
            $Secret,
    
            [switch]
            $Raw,
    
            [switch]
            $GlobalSecret
        )
    
        # if the global secret flag is set, overwrite the passed secret
        if ($GlobalSecret) {
            $Secret = (Get-PodeCookieGlobalSecret)
        }
    
        # 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
    }
    
    function Set-PodeCookie
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name,
    
            [Parameter()]
            [string]
            $Value,
    
            [Parameter()]
            [string]
            $Secret,
    
            [Parameter()]
            [int]
            $Duration = 0,
    
            [Parameter()]
            [datetime]
            $Expiry,
    
            [switch]
            $HttpOnly,
    
            [switch]
            $Discard,
    
            [switch]
            $Secure,
    
            [switch]
            $GlobalSecret
        )
    
        # if the global secret flag is set, overwrite the passed secret
        if ($GlobalSecret) {
            $Secret = (Get-PodeCookieGlobalSecret)
        }
    
        # 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-Empty $Expiry)) {
            $cookie.Expires = $Expiry
        }
        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)
    }
    
    function Get-PodeCookieGlobalSecret
    {
        return $PodeContext.Server.Cookies.Secrets['global']
    }
    
    function Update-PodeCookieExpiry
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name,
    
            [Parameter()]
            [int]
            $Duration = 0,
    
            [Parameter()]
            [datetime]
            $Expiry
        )
    
        # 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-Empty $Expiry)) {
            $cookie.Expires = $Expiry
        }
        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)
    }
    
    function Remove-PodeCookie
    {
        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)
        }
    }
    
    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\Tools\CronParser.ps1 Show
    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 * *';
            '@quaterly' = '0 0 1 1,4,8,7,10';
            '@yearly' = '0 0 1 1 *';
            '@annually' = '0 0 1 1 *';
    
            # twice
            '@twice-hourly' = '0,30 * * * *';
            '@twice-daily' = '0,12 0 * * *';
            '@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-Empty $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 week and day of month (both must fail)
        if (!(Test-RangeAndValue -AtomContraint $Expression.DayOfWeek -NowValue ([int]$DateTime.DayOfWeek)) -and
            !(Test-RangeAndValue -AtomContraint $Expression.DayOfMonth -NowValue $DateTime.Day)) {
            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\Tools\Cryptography.ps1 Show
    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 Get-PodeRandomBytes
    {
        param (
            [Parameter()]
            [int]
            $Length = 16
        )
    
        return (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\Tools\Endware.ps1 Show
    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-ScriptBlock -ScriptBlock $eware -Arguments $WebEvent -Scoped | Out-Null
            }
            catch {
                $Error[0] | Out-Default
            }
        }
    }
    
    function Endware
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock
        )
    
        # add the scriptblock to array of endware that needs to be run
        $PodeContext.Server.Endware += $ScriptBlock
    }
  • src\Tools\FileMonitor.ps1 Show
    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 (
            [ValidateSet('Create', 'Delete', 'Update')]
            [string]
            $Type
        )
    
        return "PodeFileMonitor$($Type)"
    }
    
    function Get-PodeFileMonitorTimerName
    {
        return 'PodeFileMonitorTimer'
    }
  • src\Tools\Flash.ps1 Show
    function Flash
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('Add', 'Clear', 'Get', 'Keys', 'Remove')]
            [Alias('a')]
            [string]
            $Action,
    
            [Parameter()]
            [Alias('k')]
            [string]
            $Key,
    
            [Parameter()]
            [Alias('m')]
            [string]
            $Message
        )
    
        # if sessions haven't been setup, error
        if (Test-Empty $PodeContext.Server.Cookies.Session) {
            throw 'Sessions are required to use Flash messages'
        }
    
        # run logic for the action
        switch ($Action.ToLowerInvariant())
        {
            'add' {
                Add-PodeFlashMessage -Key $Key -Message $Message
            }
    
            'get' {
                return @(Get-PodeFlashMessage -Key $Key)
            }
    
            'keys' {
                return @(Get-PodeFlashMessageKeys)
            }
    
            'clear' {
                Clear-PodeFlashMessages
            }
    
            'remove' {
                Remove-PodeFlashMessage -Key $Key
            }
        }
    }
    
    function Add-PodeFlashMessage
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Key,
    
            [Parameter()]
            [string]
            $Message
        )
    
        # append the message against the key
        if ($null -eq $WebEvent.Session.Data.Flash) {
            $WebEvent.Session.Data.Flash = @{}
        }
    
        if ($null -eq $WebEvent.Session.Data.Flash[$Key]) {
            $WebEvent.Session.Data.Flash[$Key] = @($Message)
        }
        else {
            $WebEvent.Session.Data.Flash[$Key] += @($Message)
        }
    }
    
    function Get-PodeFlashMessage
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Key
        )
    
        # retrieve messages from session, then delete it
        if ($null -eq $WebEvent.Session.Data.Flash) {
            return @()
        }
    
        $v = @($WebEvent.Session.Data.Flash[$Key])
        $WebEvent.Session.Data.Flash.Remove($Key)
    
        if (Test-Empty $v) {
            return @()
        }
    
        return @($v)
    }
    
    function Get-PodeFlashMessageKeys
    {
        # return list of all current keys
        if ($null -eq $WebEvent.Session.Data.Flash) {
            return @()
        }
    
        return @($WebEvent.Session.Data.Flash.Keys)
    }
    
    function Clear-PodeFlashMessages
    {
        # clear all keys
        if ($null -ne $WebEvent.Session.Data.Flash) {
            $WebEvent.Session.Data.Flash = @{}
        }
    }
    
    function Remove-PodeFlashMessage
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Key
        )
    
        # remove key from flash messages
        if ($null -ne $WebEvent.Session.Data.Flash) {
            $WebEvent.Session.Data.Flash.Remove($Key)
        }
    }
  • src\Tools\Gui.ps1 Show
    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.Name)`"
                        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-Empty $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-Empty $PodeContext.Server.Gui.State)) {
                    $form.WindowState = $PodeContext.Server.Gui.State
                }
    
                # 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 {
                $Error[0] | Out-Default
                throw $_.Exception
            }
            finally {
                # invoke the cancellation token to close the server
                $PodeContext.Tokens.Cancellation.Cancel()
            }
        }
    
        Add-PodeRunspace -Type 'Gui' -ScriptBlock $script
    }
    
    function Gui
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('o')]
            [hashtable]
            $Options
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'gui' -ThrowError
    
        # only valid for Windows PowerShell
        if (Test-IsPSCore) {
            throw 'The gui function is currently unavailable for PS Core, and only works for Windows PowerShell'
        }
    
        # enable the gui and set it's title/name
        $PodeContext.Server.Gui.Enabled = $true
        $PodeContext.Server.Gui.Name = $Name
    
        # coalesce the options
        $Options = (coalesce $Options @{})
    
        # set the window's icon path
        if (![string]::IsNullOrWhiteSpace($Options.Icon)) {
            $PodeContext.Server.Gui.Icon = (Resolve-Path $Options.Icon).Path
            if (!(Test-Path $PodeContext.Server.Gui.Icon)) {
                throw "Path to icon for GUI does not exist: $($PodeContext.Server.Gui.Icon)"
            }
        }
    
        # display the app in the taskbar?
        $PodeContext.Server.Gui.ShowInTaskbar = (coalesce $Options.ShowInTaskbar $true)
    
        # set the window's state
        $states = @('Normal', 'Maximized', 'Minimized')
        $PodeContext.Server.Gui.State = (coalesce $Options.State 'Normal')
        if ($states -inotcontains $PodeContext.Server.Gui.State) {
            throw "Invalid GUI window state supplied, should be blank or one of $($states -join ' / ')"
        }
    
        # set the window's style
        $styles = @('None', 'SingleBorderWindow', 'ThreeDBorderWindow', 'ToolWindow')
        $PodeContext.Server.Gui.WindowStyle = (coalesce $Options.WindowStyle 'SingleBorderWindow')
        if ($styles -inotcontains $PodeContext.Server.Gui.WindowStyle) {
            throw "Invalid GUI window style supplied, should be blank or one of $($styles -join ' / ')"
        }
    
        # set the height of the window
        $PodeContext.Server.Gui.Height = (coalesce ([int]$Options.Height) 0)
        if ($PodeContext.Server.Gui.Height -le 0) {
            $PodeContext.Server.Gui.Height = 'auto'
        }
    
        # set the width of the window
        $PodeContext.Server.Gui.Width = (coalesce ([int]$Options.Width) 0)
        if ($PodeContext.Server.Gui.Width -le 0) {
            $PodeContext.Server.Gui.Width = 'auto'
        }
    
        # set the resize mode of the window
        $modes = @('CanResize', 'CanMinimize', 'NoResize')
        $PodeContext.Server.Gui.ResizeMode = (coalesce $Options.ResizeMode 'CanResize')
        if ($modes -inotcontains $PodeContext.Server.Gui.ResizeMode) {
            throw "Invalid GUI window resize mode supplied, should be blank or one of $($modes -join ' / ')"
        }
    
        # set the gui to use a specific listener
        $PodeContext.Server.Gui.ListenName = $Options.ListenName
    
        if (!(Test-Empty $PodeContext.Server.Gui.ListenName)) {
            $found = ($PodeContext.Server.Endpoints | Where-Object {
                $_.Name -eq $PodeContext.Server.Gui.ListenName
            } | Select-Object -First 1)
    
            if ($null -eq $found) {
                throw "Listen endpoint with name '$($Name)' does not exist"
            }
    
            $PodeContext.Server.Gui.Endpoint = $found
        }
    }
  • src\Tools\Handlers.ps1 Show
    function Get-PodeTcpHandler
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('SMTP', 'TCP', 'Service')]
            [string]
            $Type
        )
    
        return $PodeContext.Server.Handlers[$Type]
    }
    
    function Handler
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('SMTP', 'TCP', 'Service')]
            [string]
            $Type,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'handler' -ThrowError
    
        # lower the type
        $Type = $Type.ToLowerInvariant()
    
        # ensure handler isn't already set
        if ($null -ne $PodeContext.Server.Handlers[$Type]) {
            throw "Handler for $($Type) already defined"
        }
    
        # add the handler
        $PodeContext.Server.Handlers[$Type] = $ScriptBlock
    }
  • src\Tools\Headers.ps1 Show
    function Header
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('Add', 'Exists', 'Get', 'Set')]
            [Alias('a')]
            [string]
            $Action,
    
            [Parameter(Mandatory=$true)]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('v')]
            [string]
            $Value
        )
    
        # run logic for the action
        switch ($Action.ToLowerInvariant())
        {
            # set a headers against the response (overwriting all with same name)
            'set' {
                return (Set-PodeHeader -Name $Name -Value $Value)
            }
    
            # appends a header against the response
            'add' {
                return (Add-PodeHeader -Name $Name -Value $Value)
            }
    
            # get a header from the request
            'get' {
                return (Get-PodeHeader -Name $Name)
            }
    
            # checks whether a given header exists on the request
            'exists' {
                return (Test-PodeHeaderExists -Name $Name)
            }
        }
    }
    
    function Test-PodeHeaderExists
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name
        )
    
        $header = (Get-PodeHeader -Name $Name)
        return (![string]::IsNullOrWhiteSpace($header))
    }
    
    function Get-PodeHeader
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name
        )
    
        # get the header from the request
        if ($PodeContext.Server.IsServerless) {
            $header = $WebEvent.Request.Headers.$Name
        }
        else {
            $header = $WebEvent.Request.Headers[$Name]
        }
    
        return $header
    }
    
    function Set-PodeHeader
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name,
    
            [Parameter()]
            [string]
            $Value
        )
    
        if ($PodeContext.Server.IsServerless) {
            $WebEvent.Response.Headers[$Name] = $Value
        }
        else {
            $WebEvent.Response.AddHeader($Name, $Value) | Out-Null
        }
    }
    
    function Set-PodeServerHeader
    {
        param (
            [Parameter()]
            [string]
            $Type
        )
    
        Set-PodeHeader -Name 'Server' -Value "Pode - $($Type)"
    }
    
    function Add-PodeHeader
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Name,
    
            [Parameter()]
            [string]
            $Value
        )
    
        if ($PodeContext.Server.IsServerless) {
            $WebEvent.Response.Headers[$Name] = $Value
        }
        else {
            $WebEvent.Response.AppendHeader($Name, $Value) | Out-Null
        }
    }
  • src\Tools\Helpers.ps1 Show
    # 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-ScriptBlock -ScriptBlock ([scriptblock]::Create($Content)) -Arguments $Data -Return)
    }
    
    function Get-PodeFileContentUsingViewEngine
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Path,
    
            [Parameter()]
            [hashtable]
            $Data
        )
    
        # work out the engine to use when parsing the file
        $engine = $PodeContext.Server.ViewEngine.Engine
    
        $ext = Get-PodeFileExtension -Path $Path -TrimPeriod
        if (![string]::IsNullOrWhiteSpace($ext) -and ($ext -ine $PodeContext.Server.ViewEngine.Extension)) {
            $engine = $ext
        }
    
        # setup the content
        $content = [string]::Empty
    
        # run the relevant engine logic
        switch ($engine.ToLowerInvariant())
        {
            'html' {
                $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-ScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.Script -Arguments $Path -Return)
                    }
                    else {
                        $content = (Invoke-ScriptBlock -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 Test-Empty
    {
        param (
            [Parameter()]
            $Value
        )
    
        if ($null -eq $Value) {
            return $true
        }
    
        switch ($Value) {
            { $_ -is 'string' } {
                return [string]::IsNullOrWhiteSpace($Value)
            }
    
            { $_ -is 'array' } {
                return ($Value.Length -eq 0)
            }
    
            { $_ -is 'hashtable' } {
                return ($Value.Count -eq 0)
            }
    
            { $_ -is 'scriptblock' } {
                return ($null -eq $Value -or [string]::IsNullOrWhiteSpace($Value.ToString()))
            }
    
            { $_ -is 'valuetype' } {
                return $false
            }
        }
    
        return ([string]::IsNullOrWhiteSpace($Value) -or ((Get-PodeCount $Value) -eq 0))
    }
    
    function Get-PodePSVersionTable
    {
        return $PSVersionTable
    }
    
    function Test-IsUnix
    {
        return (Get-PodePSVersionTable).Platform -ieq 'unix'
    }
    
    function Test-IsWindows
    {
        $v = Get-PodePSVersionTable
        return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop'))
    }
    
    function Test-IsPSCore
    {
        return (Get-PodePSVersionTable).PSEdition -ieq 'core'
    }
    
    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-Empty $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
        )
    
        $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 ((Test-Empty $Certificate) -and (Test-Empty $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-Empty $Thumbprint)) {
            $cert = $Thumbprint
        }
    
        # otherwise, generate/find a certificate
        else
        {
            # generate a self-signed cert
            if (@('self', 'self-signed') -icontains $Certificate) {
                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' = (iftet ($AnyPortOnZero -and $_port -eq 0) '*' $_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
        )
    
        # 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', '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-Empty $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 {
            $Error[0] | Out-Default
            throw $_.Exception
        }
    }
    
    function Close-PodeRunspaces
    {
        param (
            [switch]
            $ClosePool
        )
    
        if ($PodeContext.Server.IsServerless) {
            return
        }
    
        try {
            if (!(Test-Empty $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 {
                    dispose $_.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 {
                    dispose $_ -Close
                }
            }
        }
        catch {
            $Error[0] | Out-Default
            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)
    }
    
    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)
    }
    
    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-Pode
    {
        param (
            [switch]
            $Exit
        )
    
        # stpo all current runspaces
        Close-PodeRunspaces -ClosePool
    
        # stop the file monitor if it's running
        Stop-PodeFileMonitor
    
        try {
            # remove all the cancellation tokens
            dispose $PodeContext.Tokens.Cancellation
            dispose $PodeContext.Tokens.Restart
        }
        catch {
            $Error[0] | Out-Default
        }
    
        # remove all of the pode temp drives
        Remove-PodePSDrives
    
        if ($Exit -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 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
    }
    
    <#
    # Sourced and editted from https://davewyatt.wordpress.com/2014/04/06/thread-synchronization-in-powershell/
    #>
    function Lock
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [object]
            $InputObject,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock
        )
    
        if ($null -eq $InputObject) {
            return
        }
    
        if ($InputObject.GetType().IsValueType) {
            throw 'Cannot lock value types'
        }
    
        $locked = $false
    
        try {
            [System.Threading.Monitor]::Enter($InputObject.SyncRoot)
            $locked = $true
    
            if ($ScriptBlock -ne $null) {
                Invoke-ScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure
            }
        }
        catch {
            $Error[0] | Out-Default
            throw $_.Exception
        }
        finally {
            if ($locked) {
                [System.Threading.Monitor]::Pulse($InputObject.SyncRoot)
                [System.Threading.Monitor]::Exit($InputObject.SyncRoot)
            }
        }
    }
    
    function Await
    {
        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
        }
    }
    
    function Root
    {
        return $PodeContext.Server.Root
    }
    
    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 Invoke-ScriptBlock
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [Alias('s')]
            [scriptblock]
            $ScriptBlock,
    
            [Parameter()]
            [Alias('a')]
            $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
        }
    }
    
    <#
        If-This-Else-That. If Check is true return Value1, else return Value2
    #>
    function Iftet
    {
        param (
            [Parameter()]
            [bool]
            $Check,
    
            [Parameter()]
            $Value1,
    
            [Parameter()]
            $Value2
        )
    
        if ($Check) {
            return $Value1
        }
    
        return $Value2
    }
    
    function Coalesce
    {
        param (
            [Parameter()]
            $Value1,
    
            [Parameter()]
            $Value2
        )
    
        return (iftet (Test-Empty $Value1) $Value2 $Value1)
    }
    
    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 Stopwatch
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock
        )
    
        try {
            $watch = [System.Diagnostics.Stopwatch]::StartNew()
            . $ScriptBlock
        }
        catch {
            $Error[0] | Out-Default
            throw $_.Exception
        }
        finally {
            $watch.Stop()
            Out-Default -InputObject "[Stopwatch]: $($watch.Elapsed) [$($Name)]"
        }
    }
    
    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 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()) {
                'aws-lambda' {
                    $Content = $Request.body
                }
    
                'azure-functions' {
                    $Content = $Request.RawBody
                }
    
                default {
                    $Content = Read-PodeStreamToEnd -Stream $Request.InputStream -Encoding $Encoding
                }
            }
    
            # if there is no content then do nothing
            if ([string]::IsNullOrWhiteSpace($Content)) {
                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
                $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.Length -ge $Object.Count) {
            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) {
                status 404
            }
    
            return $false
        }
    
        # if the file isn't accessible then fail 401
        if (!(Test-PodePathAccess $Path)) {
            if (!$NoStatus) {
                status 401
            }
    
            return $false
        }
    
        # if we're failing on a directory then fail on 404
        if ($FailOnDirectory -and (Test-PodePathIsDirectory $Path)) {
            if (!$NoStatus) {
                status 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-Empty $_)
        })
    
        # if no paths, return null
        if (Test-Empty $Paths) {
            return $null
        }
    
        # replace certain chars
        $Paths = @($Paths | ForEach-Object {
            if (!(Test-Empty $_)) {
                Convert-PodePathPatternToRegex -Path $_ -NotStrict -NotSlashes:$NotSlashes
            }
        })
    
        # join them all together
        $joined = "($($Paths -join '|'))"
    
        if ($NotStrict) {
            return "$($joined)"
        }
    
        return "^$($joined)$"
    }
    
    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 'Engine').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-Empty $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-Empty $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-Empty $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 = (coalesce $Engine '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: $(coalesce $Path $_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 = (iftet $Endpoint.Ssl 'https' 'http')
    
        # grab the port number
        $port = $Endpoint.Port
        if ($port -eq 0) {
            $port = (iftet $Endpoint.Ssl 8443 8080)
        }
    
        return "$($protocol)://$($Endpoint.HostName):$($port)"
    }
  • src\Tools\Logging.ps1 Show
    function Get-PodeLogger
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name
        )
    
        return $PodeContext.Server.Logging.Methods[$Name]
    }
    
    function Add-PodeLogEndware
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $WebEvent
        )
    
        # don't setup logging if not configured
        if ($PodeContext.Server.Logging.Disabled -or (Get-PodeCount $PodeContext.Server.Logging.Methods) -eq 0) {
            return
        }
    
        # add the logging endware
        $WebEvent.OnEnd += {
            param($s)
            $obj = New-PodeLogObject -Request $s.Request -Path $s.Path
            Add-PodeLogObject -LogObject $obj -Response $s.Response
        }
    }
    
    function New-PodeLogObject
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Request,
    
            [Parameter()]
            [string]
            $Path
        )
    
        return @{
            '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' = '-';
                'StatusDescription' = '-';
                'Size' = '-';
            };
        }
    }
    
    function Add-PodeLogObject
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $LogObject,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Response
        )
    
        if ($PodeContext.Server.Logging.Disabled -or (Get-PodeCount $PodeContext.Server.Logging.Methods) -eq 0) {
            return
        }
    
        $LogObject.Response.StatusCode = $Response.StatusCode
        $LogObject.Response.StatusDescription = $Response.StatusDescription
    
        if ($Response.ContentLength64 -gt 0) {
            $LogObject.Response.Size = $Response.ContentLength64
        }
    
        $PodeContext.RequestsToLog.Add($LogObject) | Out-Null
    }
    
    function Start-PodeLoggerRunspace
    {
        if ((Get-PodeCount $PodeContext.Server.Logging.Methods) -eq 0) {
            return
        }
    
        $script = {
            # simple safegaurd function to set blank field to a dash(-)
            function sg($value) {
                if (Test-Empty $value) {
                    return '-'
                }
    
                return $value
            }
    
            # convert a log request into a Combined Log Format string
            function Get-RequestString($req) {
                $url = "$(sg $req.Request.Method) $(sg $req.Request.Resource) $(sg $req.Request.Protocol)"
                return "$(sg $req.Host) $(sg $req.RfcUserIdentity) $(sg $req.User) [$(sg $req.Date)] `"$($url)`" $(sg $req.Response.StatusCode) $(sg $req.Response.Size) `"$(sg $req.Request.Referrer)`" `"$(sg $req.Request.Agent)`""
            }
    
            # helper variables for files
            $_files_next_run = [DateTime]::Now.Date
    
            # main logic loop
            while ($true)
            {
                # if there are no requests to log, just sleep
                if ((Get-PodeCount $PodeContext.RequestsToLog) -eq 0) {
                    Start-Sleep -Seconds 1
                    continue
                }
    
                # safetly pop off the first log request from the array
                $r = $null
    
                lock $PodeContext.RequestsToLog {
                    $r = $PodeContext.RequestsToLog[0]
                    $PodeContext.RequestsToLog.RemoveAt(0) | Out-Null
                }
    
                # convert the request into a log string
                $str = (Get-RequestString $r)
    
                # apply log request to supplied loggers
                $PodeContext.Server.Logging.Methods.Keys | ForEach-Object {
                    switch ($_.ToLowerInvariant())
                    {
                        'terminal' {
                            $str | Out-Default
                        }
    
                        'file' {
                            $details = $PodeContext.Server.Logging.Methods[$_]
                            $date = [DateTime]::Now.ToString('yyyy-MM-dd')
    
                            # generate path to log path and date file
                            if ($null -eq $details -or (Test-Empty $details.Path)) {
                                $path = (Join-PodeServerRoot 'logs' "$($date).log" )
                            }
                            else {
                                $path = (Join-Path $details.Path "$($date).log")
                            }
    
                            # append log to file
                            $str | 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 ($null -ne $details -and [int]$details.MaxDays -gt 0 -and $_files_next_run -lt [DateTime]::Now) {
                                $date = [DateTime]::Now.AddDays(-$details.MaxDays)
    
                                Get-ChildItem -Path $path -Filter '*.log' -Force |
                                    Where-Object { $_.CreationTime -lt $date } |
                                    Remove-Item $_ -Force | Out-Null
    
                                $_files_next_run = [DateTime]::Now.Date.AddDays(1)
                            }
                        }
    
                        { $_ -ilike 'custom_*' } {
                            Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.Logging.Methods[$_] -Arguments @{
                                'Log' = $r;
                                'Lockable' = $PodeContext.Lockable;
                            }
                        }
                    }
                }
    
                # small sleep to lower cpu usage
                Start-Sleep -Milliseconds 100
            }
        }
    
        Add-PodeRunspace -Type 'Main' -ScriptBlock $script
    }
    
    function Logger
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('d')]
            [object]
            $Details = $null,
    
            [switch]
            [Alias('c')]
            $Custom
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'logger' -ThrowError
    
        # is logging disabled?
        if ($PodeContext.Server.Logging.Disabled) {
            Write-Host "Logging has been disabled for $($Name)" -ForegroundColor DarkCyan
            return
        }
    
        # set the logger as custom if flag is passed
        if ($Name -inotlike 'custom_*' -and $Custom) {
            $Name = "custom_$($Name)"
        }
    
        # lowercase the name
        $Name = $Name.ToLowerInvariant()
    
        # ensure the logger doesn't already exist
        if ($PodeContext.Server.Logging.Methods.ContainsKey($Name)) {
            throw "Logger called $($Name) already exists"
        }
    
        # ensure the details are of a correct type (inbuilt=hashtable, custom=scriptblock)
        $type = (Get-PodeType $Details)
    
        if ($Name -ilike 'custom_*') {
            if ($null -eq $Details) {
                throw 'For custom loggers, a ScriptBlock is required'
            }
    
            if ($type.Name -ine 'scriptblock') {
                throw "Custom logger details should be a ScriptBlock, but got: $($type.Name)"
            }
        }
        else {
            if ($null -ne $Details -and $type.Name -ine 'hashtable') {
                throw "Inbuilt logger details should be a HashTable, but got: $($type.Name)"
            }
        }
    
        # add the logger, along with any given details (hashtable/scriptblock)
        $PodeContext.Server.Logging.Methods[$Name] = $Details
    
        # if a file logger, create base directory (file is a dummy file, and won't be created)
        if ($Name -ieq 'file') {
            # has a specific logging path been supplied?
            if ($null -eq $Details -or (Test-Empty $Details.Path)) {
                $path = (Split-Path -Parent -Path (Join-PodeServerRoot 'logs' 'tmp.txt'))
            }
            else {
                $path = $Details.Path
            }
    
            Write-Host "Log Path: $($path)" -ForegroundColor DarkCyan
            New-Item -Path $path -ItemType Directory -Force | Out-Null
        }
    }
  • src\Tools\Mappers.ps1 Show
    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' }
            '.master' { return 'application/xml' }
            '.mbox' { return 'application/mbox' }
            '.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 (iftet $DefaultIsNull $null '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\Tools\Middleware.ps1 Show
    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 {
                # set any custom middleware options
                $WebEvent.Middleware = @{ 'Options' = $midware.Options }
    
                # invoke the middleware logic
                $continue = Invoke-ScriptBlock -ScriptBlock $midware.Logic -Arguments $WebEvent -Return -Scoped
    
                # remove any custom middleware options
                $WebEvent.Middleware.Clear()
            }
            catch {
                status 500 -e $_
                $continue = $false
                $_.Exception | Out-Default
            }
    
            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 '@access' -ScriptBlock {
            param($s)
    
            # ensure the request IP address is allowed
            if (!(Test-PodeIPAccess -IP $s.Request.RemoteEndPoint.Address)) {
                status 403
                return $false
            }
    
            # IP address is allowed
            return $true
        })
    }
    
    function Get-PodeLimitMiddleware
    {
        return (Get-PodeInbuiltMiddleware -Name '@limit' -ScriptBlock {
            param($s)
    
            # ensure the request IP address has not hit a rate limit
            if (!(Test-PodeIPLimit -IP $s.Request.RemoteEndPoint.Address)) {
                status 429
                return $false
            }
    
            # IP address is allowed
            return $true
        })
    }
    
    function Get-PodePublicMiddleware
    {
        return (Get-PodeInbuiltMiddleware -Name '@public' -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) {
                Attach -Path $e.Path
            }
            else {
                File -Path $info.Path -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$caching
            }
    
            # static content found, stop
            return $false
        })
    }
    
    function Get-PodeRouteValidateMiddleware
    {
        return @{
            'Name' = '@route-valid';
            'Logic' = {
                param($s)
    
                # ensure the path has a route
                $route = Get-PodeRoute -HttpMethod $s.Method -Route $s.Path -Protocol $s.Protocol -Endpoint $s.Endpoint -CheckWildMethod
    
                # if there's no route defined, it's a 404
                if ($null -eq $route -or $null -eq $route.Logic) {
                    status 404
                    return $false
                }
    
                # set the route parameters
                $WebEvent.Parameters = $route.Parameters
    
                # override the content type from the route if it's not empty
                if (![string]::IsNullOrWhiteSpace($route.ContentType)) {
                    $WebEvent.ContentType = $route.ContentType
                }
    
                # set the content type for any pages for the route if it's not empty
                $WebEvent.ErrorType = $route.ErrorType
    
                # route exists
                return $true
            }
        }
    }
    
    function Get-PodeBodyMiddleware
    {
        return (Get-PodeInbuiltMiddleware -Name '@body' -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 {
                status 400 -e $_
                return $false
            }
        })
    }
    
    function Get-PodeQueryMiddleware
    {
        return (Get-PodeInbuiltMiddleware -Name '@query' -ScriptBlock {
            param($s)
    
            try {
                # set the query string from the request
                $s.Query = (ConvertFrom-PodeNameValueToHashTable -Collection $s.Request.QueryString)
                return $true
            }
            catch {
                status 400 -e $_
                return $false
            }
        })
    }
    
    function Get-PodeCookieMiddleware
    {
        return (Get-PodeInbuiltMiddleware -Name '@cookie' -ScriptBlock {
            param($e)
    
            # if it's not serverless, return
            if (!$PodeContext.Server.IsServerless) {
                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 '=')
    
                $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
        })
    }
    
    function Middleware
    {
        param (
            [Parameter(Mandatory=$true, Position=0, ParameterSetName='Script')]
            [Parameter(Mandatory=$true, Position=1, ParameterSetName='ScriptRoute')]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock,
    
            [Parameter(Mandatory=$true, Position=0, ParameterSetName='ScriptRoute')]
            [Parameter(Mandatory=$true, Position=0, ParameterSetName='HashRoute')]
            [Alias('r')]
            [string]
            $Route,
    
            [Parameter(Mandatory=$true, Position=0, ParameterSetName='Hash')]
            [Parameter(Mandatory=$true, Position=1, ParameterSetName='HashRoute')]
            [Alias('h')]
            [hashtable]
            $HashTable,
    
            [Parameter()]
            [Alias('n')]
            [string]
            $Name,
    
            [switch]
            $Return
        )
    
        # if a name was supplied, ensure it doesn't already exist
        if (!(Test-Empty $Name)) {
            if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) {
                throw "Middleware with defined name of $($Name) already exists"
            }
        }
    
        # if route is empty, set it to root
        $Route = Coalesce $Route '/'
        $Route = Split-PodeRouteQuery -Route $Route
        $Route = Coalesce $Route '/'
        $Route = Update-PodeRouteSlashes -Route $Route
        $Route = Update-PodeRoutePlaceholders -Route $Route
    
        # create the middleware hash, or re-use a passed one
        if (Test-Empty $HashTable)
        {
            $HashTable = @{
                'Name' = $Name;
                'Route' = $Route;
                'Logic' = $ScriptBlock;
            }
        }
        else
        {
            if (Test-Empty $HashTable.Logic) {
                throw 'Middleware supplied has no Logic'
            }
    
            if (Test-Empty $HashTable.Route) {
                $HashTable.Route = $Route
            }
    
            if (Test-Empty $HashTable.Name) {
                $HashTable.Name = $Name
            }
        }
    
        # add the scriptblock to array of middleware that needs to be run
        if ($Return) {
            return $HashTable
        }
        else {
            $PodeContext.Server.Middleware += $HashTable
        }
    }
  • src\Tools\NameGenerator.ps1 Show
    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\Tools\Responses.ps1 Show
    # write data to http response stream, using a specific content-type
    function Text
    {
        param (
            [Parameter()]
            [Alias('v')]
            $Value,
    
            [Parameter()]
            [Alias('ctype', 'ct')]
            [string]
            $ContentType = 'text/plain',
    
            [Parameter()]
            [Alias('a')]
            [int]
            $MaxAge = 3600,
    
            [switch]
            [Alias('c')]
            $Cache
        )
    
        # if there's nothing to write, return
        if (Test-Empty $Value) {
            return
        }
    
        # if the value isn't a string/byte[] then error
        $valueType = $Value.GetType().Name
        if (@('string', 'byte[]') -inotcontains $valueType) {
            throw "Value to write to stream must be a String or Byte[], but got: $($valueType)"
        }
    
        # if the response stream isn't writable, return
        $res = $WebEvent.Response
        if (($null -eq $res) -or (!$PodeContext.Server.IsServerless -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("ddd, dd MMM yyyy HH:mm:ss 'GMT'"))
        }
    
        # 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 ($PodeContext.Server.IsServerless) {
            $res.Body = $Value
        }
    
        else {
            # convert string to bytes
            if ($valueType -ieq 'string') {
                $Value = ConvertFrom-PodeValueToBytes -Value $Value
            }
    
            # write the content to the response stream
            $res.ContentLength64 = $Value.Length
    
            try {
                $ms = New-Object -TypeName System.IO.MemoryStream
                $ms.Write($Value, 0, $Value.Length)
                $ms.WriteTo($res.OutputStream)
                $ms.Close()
            }
            catch {
                if ((Test-PodeValidNetworkFailure $_.Exception)) {
                    return
                }
    
                $_.Exception | Out-Default
                throw
            }
        }
    }
    
    function File
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [Alias('p')]
            [string]
            $Path,
    
            [Parameter()]
            [Alias('d')]
            $Data = @{},
    
            [Parameter()]
            [Alias('ctype', 'ct')]
            [string]
            $ContentType = $null,
    
            [Parameter()]
            [Alias('a')]
            [int]
            $MaxAge = 3600,
    
            [switch]
            [Alias('c')]
            $Cache
        )
    
        # 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 = (coalesce $subExt $mainExt)
    
            $ContentType = (coalesce $ContentType (Get-PodeContentType -Extension $subExt))
            Text -Value $content -ContentType $ContentType
        }
    
        # 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 = (coalesce $ContentType (Get-PodeContentType -Extension $mainExt))
            Text -Value $content -ContentType $ContentType -MaxAge $MaxAge -Cache:$Cache
        }
    }
    
    function Attach
    {
        param (
            [Parameter(Mandatory=$true)]
            [Alias('p')]
            [string]
            $Path
        )
    
        # only attach files from public/static-route directories when path is relative
        $Path = (Get-PodeStaticRoutePath -Route $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
            $WebEvent.Response.ContentType = (Get-PodeContentType -Extension $ext)
            Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($filename)"
    
            # if serverless, get the content raw and return
            if ($PodeContext.Server.IsServerless) {
                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.ContentLength64 = $fs.Length
                $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()
    
                while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) {
                    $WebEvent.Response.OutputStream.Write($buffer, 0, $read)
                }
            }
        }
        finally {
            dispose $fs
        }
    }
    
    function Save
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('p')]
            [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[$Name]
        if ([string]::IsNullOrWhiteSpace($fileName)) {
            throw "A parameter called '$($Name)' 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)
    }
    
    function Status
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [Alias('c')]
            [int]
            $Code,
    
            [Parameter()]
            [Alias('d')]
            [string]
            $Description,
    
            [Parameter()]
            [Alias('e')]
            $Exception,
    
            [Parameter()]
            [Alias('ctype', 'ct')]
            [string]
            $ContentType = $null,
    
            [switch]
            [Alias('nopage')]
            $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
        }
    }
    
    function Redirect
    {
        param (
            [Parameter()]
            [Alias('u')]
            [string]
            $Url,
    
            [Parameter()]
            [Alias('p')]
            [int]
            $Port = 0,
    
            [Parameter()]
            [ValidateSet('', 'HTTP', 'HTTPS')]
            [Alias('pr')]
            [string]
            $Protocol,
    
            [Parameter()]
            [Alias('e')]
            [string]
            $Endpoint,
    
            [switch]
            [Alias('m')]
            $Moved
        )
    
        if (Test-Empty $Url) {
            $uri = $WebEvent.Request.Url
    
            # set the protocol
            $Protocol = $Protocol.ToLowerInvariant()
            if (Test-Empty $Protocol) {
                $Protocol = $uri.Scheme
            }
    
            # set the endpoint
            if (Test-Empty $Endpoint) {
                $Endpoint = $uri.Host
            }
    
            # set the port
            if ($Port -le 0) {
                $Port = $uri.Port
            }
    
            $PortStr = [string]::Empty
            if ($Port -ne 80 -and $Port -ne 443) {
                $PortStr = ":$($Port)"
            }
    
            # combine to form the url
            $Url = "$($Protocol)://$($Endpoint)$($PortStr)$($uri.PathAndQuery)"
        }
    
        Set-PodeHeader -Name 'Location' -Value $Url
    
        if ($Moved) {
            status 301 'Moved'
        }
        else {
            status 302 'Redirect'
        }
    }
    
    function Json
    {
        param (
            [Parameter()]
            [Alias('v')]
            $Value,
    
            [switch]
            $File
        )
    
        if ($File) {
            # test the file path, and set status accordingly
            if (!(Test-PodePath $Value)) {
                return
            }
            else {
                $Value = Get-PodeFileContent -Path $Value
            }
        }
        elseif (Test-Empty $Value) {
            $Value = '{}'
        }
        elseif ($Value -isnot 'string') {
            $Value = ($Value | ConvertTo-Json -Depth 10 -Compress)
        }
    
        Text -Value $Value -ContentType 'application/json'
    }
    
    function Csv
    {
        param (
            [Parameter(Mandatory=$true)]
            [Alias('v')]
            $Value,
    
            [switch]
            $File
        )
    
        if ($File) {
            # test the file path, and set status accordingly
            if (!(Test-PodePath $Value)) {
                return
            }
            else {
                $Value = Get-PodeFileContent -Path $Value
            }
        }
        elseif (Test-Empty $Value) {
            $Value = [string]::Empty
        }
        elseif ($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)
            }
        }
    
        Text -Value $Value -ContentType 'text/csv'
    }
    
    function Xml
    {
        param (
            [Parameter(Mandatory=$true)]
            [Alias('v')]
            $Value,
    
            [switch]
            $File
        )
    
        if ($File) {
            # test the file path, and set status accordingly
            if (!(Test-PodePath $Value)) {
                return
            }
            else {
                $Value = Get-PodeFileContent -Path $Value
            }
        }
        elseif (Test-Empty $value) {
            $Value = [string]::Empty
        }
        elseif ($Value -isnot 'string') {
            $Value = @(foreach ($v in $Value) {
                New-Object psobject -Property $v
            })
    
            $Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation)
        }
    
        Text -Value $Value -ContentType 'text/xml'
    }
    
    function Html
    {
        param (
            [Parameter(Mandatory=$true)]
            [Alias('v')]
            $Value,
    
            [switch]
            $File
        )
    
        if ($File) {
            # test the file path, and set status accordingly
            if (!(Test-PodePath $Value)) {
                return
            }
            else {
                $Value = Get-PodeFileContent -Path $Value
            }
        }
        elseif (Test-Empty $value) {
            $Value = [string]::Empty
        }
        elseif ($Value -isnot 'string') {
            $Value = ($Value | ConvertTo-Html)
        }
    
        Text -Value $Value -ContentType 'text/html'
    }
    
    # include helper to import the content of a view into another view
    function Include
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('p')]
            [string]
            $Path,
    
            [Parameter()]
            [Alias('d')]
            $Data = @{}
        )
    
        # default data if null
        if ($null -eq $Data) {
            $Data = @{}
        }
    
        # add view engine extension
        $ext = Get-PodeFileExtension -Path $Path
        if (Test-Empty $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)
    }
    
    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-Empty $errorPage) {
            return
        }
    
        # if exception trace showing enabled then build the exception details object
        $ex = $null
        if (!(Test-Empty $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
        File -Path $errorPage.Path -Data $data -ContentType $errorPage.ContentType
    }
    
    function View
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [Alias('p')]
            $Path,
    
            [Parameter()]
            [Alias('d')]
            $Data = @{},
    
            [switch]
            [Alias('fm')]
            $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 !(Test-Empty $WebEvent.Session.Data.Flash)) {
            $Data['flash'] = @{}
    
            foreach ($key in (flash keys)) {
                $Data.flash[$key] = (flash get $key)
            }
        }
        elseif (Test-Empty $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
        html -Value (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)
    }
    
    function Close-PodeTcpConnection
    {
        param (
            [Parameter()]
            $Client,
    
            [switch]
            $Quit
        )
    
        if ($null -eq $Client) {
            $Client = $TcpEvent.Client
        }
    
        if ($null -ne $Client) {
            if ($Quit -and $Client.Connected) {
                tcp write '221 Bye'
            }
    
            dispose $Client -Close
        }
    }
    
    function Tcp
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('write', 'read')]
            [Alias('a')]
            [string]
            $Action,
    
            [Parameter()]
            [Alias('m')]
            [string]
            $Message,
    
            [Parameter()]
            [Alias('c')]
            $Client
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'tcp' -ThrowError
    
        # use the main client if one isn't supplied
        if ($null -eq $Client) {
            $Client = $TcpEvent.Client
        }
    
        switch ($Action.ToLowerInvariant())
        {
            'write' {
                $encoder = New-Object System.Text.ASCIIEncoding
                $buffer = $encoder.GetBytes("$($Message)`r`n")
                $stream = $Client.GetStream()
                await $stream.WriteAsync($buffer, 0, $buffer.Length)
                $stream.Flush()
            }
    
            'read' {
                $bytes = New-Object byte[] 8192
                $encoder = New-Object System.Text.ASCIIEncoding
                $stream = $Client.GetStream()
                $bytesRead = (await $stream.ReadAsync($bytes, 0, 8192))
                return $encoder.GetString($bytes, 0, $bytesRead)
            }
        }
    }
  • src\Tools\Routes.ps1 Show
    function Get-PodeRoute
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', '*')]
            [string]
            $HttpMethod,
    
            [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 -HttpMethod '*' -Route $Route -Protocol $Protocol -Endpoint $Endpoint
            if ($null -ne $found) {
                return $found
            }
        }
    
        # is this a static route?
        $isStatic = ($HttpMethod -ieq 'static')
    
        # first ensure we have the method
        $method = $PodeContext.Server.Routes[$HttpMethod]
        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;
            }
        }
    
        # 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;
                }
            }
        }
    }
    
    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 -HttpMethod '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 (coalesce $found.File ([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 = (coalesce $found.File ([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 Route
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', '*')]
            [Alias('hm')]
            [string]
            $HttpMethod,
    
            [Parameter(Mandatory=$true)]
            [Alias('r')]
            [string]
            $Route,
    
            [Parameter()]
            [Alias('m')]
            [object[]]
            $Middleware,
    
            [Parameter()]
            [Alias('s')]
            [scriptblock]
            $ScriptBlock,
    
            [Parameter()]
            [Alias('d')]
            [string[]]
            $Defaults,
    
            [Parameter()]
            [ValidateSet('', 'HTTP', 'HTTPS')]
            [Alias('p')]
            [string]
            $Protocol,
    
            [Parameter()]
            [Alias('e')]
            [string]
            $Endpoint,
    
            [Parameter()]
            [Alias('ln', 'lid')]
            [string]
            $ListenName,
    
            [Parameter()]
            [Alias('ctype', 'ct')]
            [string]
            $ContentType,
    
            [Parameter()]
            [Alias('etype', 'et')]
            [string]
            $ErrorType,
    
            [Parameter()]
            [Alias('fp')]
            [string]
            $FilePath,
    
            [switch]
            [Alias('rm')]
            $Remove,
    
            [switch]
            [Alias('do')]
            $DownloadOnly
        )
    
        # uppercase the method
        $HttpMethod = $HttpMethod.ToUpperInvariant()
    
        # if a ListenName was supplied, find it and use it
        if (!(Test-Empty $ListenName)) {
            # ensure it exists
            $found = ($PodeContext.Server.Endpoints | Where-Object { $_.Name -eq $ListenName } | Select-Object -First 1)
            if ($null -eq $found) {
                throw "Listen endpoint with name '$($ListenName)' does not exist"
            }
    
            # override and set the protocol and endpoint
            $Protocol = $found.Protocol
            $Endpoint = $found.RawAddress
        }
    
        # if an endpoint was supplied (or used from a listen name), set any appropriate wildcards
        if (!(Test-Empty $Endpoint)) {
            $_endpoint = Get-PodeEndpointInfo -Endpoint $Endpoint -AnyPortOnZero
            $Endpoint = "$($_endpoint.Host):$($_endpoint.Port)"
        }
    
        # are we removing the route's logic?
        if ($Remove) {
            Remove-PodeRoute -HttpMethod $HttpMethod -Route $Route -Protocol $Protocol -Endpoint $Endpoint
            return
        }
    
        # add a new dynamic or static route
        if ($HttpMethod -ieq 'static') {
            Add-PodeStaticRoute -Route $Route -Source ([string](@($Middleware))[0]) -Protocol $Protocol `
                -Endpoint $Endpoint -Defaults $Defaults -DownloadOnly:$DownloadOnly
        }
        else {
            # error if defaults are defined
            if ((Get-PodeCount $Defaults) -gt 0) {
                throw "[$($HttpMethod)] $($Route) has default static files defined, which is only for [STATIC] routes"
            }
    
            # error if download only passed
            if ($DownloadOnly) {
                throw "[$($HttpMethod)] $($Route) is flagged as DownloadOnly, which is only for [STATIC] routes"
            }
    
            # add the route
            Add-PodeRoute -HttpMethod $HttpMethod -Route $Route -Middleware $Middleware -ScriptBlock $ScriptBlock `
                -Protocol $Protocol -Endpoint $Endpoint -ContentType $ContentType -ErrorType $ErrorType -FilePath $FilePath
        }
    }
    
    function Remove-PodeRoute
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', '*')]
            [string]
            $HttpMethod,
    
            [Parameter(Mandatory=$true)]
            [string]
            $Route,
    
            [Parameter()]
            [string]
            $Protocol,
    
            [Parameter()]
            [string]
            $Endpoint
        )
    
        # split route on '?' for query
        $Route = Split-PodeRouteQuery -Route $Route
    
        # ensure route isn't empty
        if (Test-Empty $Route) {
            throw "No route supplied for removing the $($HttpMethod) definition"
        }
    
        # ensure the route has appropriate slashes and replace parameters
        $Route = Update-PodeRouteSlashes -Route $Route
        $Route = Update-PodeRoutePlaceholders -Route $Route
    
        # ensure route does exist
        if (!$PodeContext.Server.Routes[$HttpMethod].ContainsKey($Route)) {
            return
        }
    
        # remove the route's logic
        $PodeContext.Server.Routes[$HttpMethod][$Route] = @($PodeContext.Server.Routes[$HttpMethod][$Route] | 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[$HttpMethod][$Route]) -eq 0) {
            $PodeContext.Server.Routes[$HttpMethod].Remove($Route) | Out-Null
        }
    }
    
    function Add-PodeRoute
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', '*')]
            [string]
            $HttpMethod,
    
            [Parameter(Mandatory=$true)]
            [string]
            $Route,
    
            [Parameter()]
            [object[]]
            $Middleware,
    
            [Parameter()]
            [scriptblock]
            $ScriptBlock,
    
            [Parameter()]
            [string]
            $Protocol,
    
            [Parameter()]
            [string]
            $Endpoint,
    
            [Parameter()]
            [string]
            $ContentType,
    
            [Parameter()]
            [string]
            $ErrorType,
    
            [Parameter()]
            [string]
            $FilePath
        )
    
        # if middleware, scriptblock and file path are all null/empty, error
        if ((Test-Empty $Middleware) -and (Test-Empty $ScriptBlock) -and (Test-Empty $FilePath)) {
            throw "[$($HttpMethod)] $($Route) has no scriptblock defined"
        }
    
        # if both a scriptblock and a file path have been supplied, error
        if (!(Test-Empty $ScriptBlock) -and !(Test-Empty $FilePath)) {
            throw "[$($HttpMethod)] $($Route) has both a ScriptBlock and a FilePath defined"
        }
    
        # if we have a file path supplied, load that path as a scriptblock
        if (Test-PodePath -Path $FilePath -NoStatus) {
            # if the path is a wildcard or directory, error
            if (!(Test-PodePathIsFile -Path $FilePath -FailOnWildcard)) {
                throw "[$($HttpMethod)] $($Route) cannot have a wildcard or directory FilePath: $($FilePath)"
            }
    
            $ScriptBlock = [scriptblock](load $FilePath)
        }
    
        # ensure supplied middlewares are either a scriptblock, or a valid hashtable
        if (!(Test-Empty $Middleware)) {
            @($Middleware) | ForEach-Object {
                $_type = (Get-PodeType $_).Name
    
                # check middleware is a type valid
                if ($_type -ine 'scriptblock' -and $_type -ine 'hashtable') {
                    throw "A middleware supplied for the '[$($HttpMethod)] $($Route)' route is of an invalid type. Expected either ScriptBlock or Hashtable, but got: $($_type)"
                }
    
                # if middleware is hashtable, ensure the keys are valid (logic is a scriptblock)
                if ($_type -ieq 'hashtable') {
                    if ($null -eq $_.Logic) {
                        throw "A Hashtable middleware supplied for the '[$($HttpMethod)] $($Route)' route has no Logic defined"
                    }
    
                    $_ltype = (Get-PodeType $_.Logic).Name
                    if ($_ltype -ine 'scriptblock') {
                        throw "A Hashtable middleware supplied for the '[$($HttpMethod)] $($Route)' route has has an invalid Logic type. Expected ScriptBlock, but got: $($_ltype)"
                    }
                }
            }
        }
    
        # if middleware is set, but there is no scriptblock, set the middleware as the scriptblock
        if (!(Test-Empty $Middleware) -and ($null -eq $ScriptBlock)) {
            # if multiple middleware, error
            if ((Get-PodeType $Middleware).BaseName -ieq 'array' -and (Get-PodeCount $Middleware) -ne 1) {
                throw "[$($HttpMethod)] $($Route) has no logic defined"
            }
    
            $ScriptBlock = {}
            if ((Get-PodeType $Middleware[0]).Name -ieq 'scriptblock') {
                $ScriptBlock = $Middleware[0]
                $Middleware = $null
            }
        }
    
        # split route on '?' for query
        $Route = Split-PodeRouteQuery -Route $Route
    
        # ensure route isn't empty
        if (Test-Empty $Route) {
            throw "No route path supplied for $($HttpMethod) definition"
        }
    
        # ensure the route has appropriate slashes
        $Route = Update-PodeRouteSlashes -Route $Route
        $Route = Update-PodeRoutePlaceholders -Route $Route
    
        # ensure route doesn't already exist
        Test-PodeRouteAndError -HttpMethod $HttpMethod -Route $Route -Protocol $Protocol -Endpoint $Endpoint
    
        # if we have middleware, convert scriptblocks to hashtables
        if (!(Test-Empty $Middleware))
        {
            $Middleware = @($Middleware)
    
            for ($i = 0; $i -lt $Middleware.Length; $i++) {
                if ((Get-PodeType $Middleware[$i]).Name -ieq 'scriptblock')
                {
                    $Middleware[$i] = @{
                        'Logic' = $Middleware[$i]
                    }
                }
            }
        }
    
        # workout a default content type for the route
        if ((Test-Empty $ContentType) -and !(Test-Empty $PodeContext.Server.Web)) {
            $ContentType = $PodeContext.Server.Web.ContentType.Default
    
            # find type by pattern
            $matched = ($PodeContext.Server.Web.ContentType.Routes.Keys | Where-Object {
                $Route -imatch $_
            } | Select-Object -First 1)
    
            if (!(Test-Empty $matched)) {
                $ContentType = $PodeContext.Server.Web.ContentType.Routes[$matched]
            }
        }
    
        # add the route logic
        $PodeContext.Server.Routes[$HttpMethod][$Route] += @(@{
            'Logic' = $ScriptBlock;
            'Middleware' = $Middleware;
            'Protocol' = $Protocol;
            'Endpoint' = $Endpoint.Trim();
            'ContentType' = $ContentType;
            'ErrorType' = $ErrorType;
        })
    }
    
    function Add-PodeStaticRoute
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Route,
    
            [Parameter(Mandatory=$true)]
            [string]
            $Source,
    
            [Parameter()]
            [string[]]
            $Defaults,
    
            [Parameter()]
            [string]
            $Protocol,
    
            [Parameter()]
            [string]
            $Endpoint,
    
            [switch]
            $DownloadOnly
        )
    
        # store the route method
        $HttpMethod = 'static'
    
        # split route on '?' for query
        $Route = Split-PodeRouteQuery -Route $Route
    
        # ensure route isn't empty
        if (Test-Empty $Route) {
            throw "No route supplied for $($HttpMethod) definition"
        }
    
        # if static, ensure the path exists at server root
        if (Test-Empty $Source) {
            throw "No path supplied for $($HttpMethod) definition"
        }
    
        $Source = (Join-PodeServerRoot $Source)
        if (!(Test-Path $Source)) {
            throw "Source folder supplied for $($HttpMethod) route does not exist: $($Source)"
        }
    
        # setup a temp drive for the path
        $Source = New-PodePSDrive -Path $Source
    
        # ensure the route has appropriate slashes
        $Route = Update-PodeRouteSlashes -Route $Route -Static
    
        # ensure route doesn't already exist
        Test-PodeRouteAndError -HttpMethod $HttpMethod -Route $Route -Protocol $Protocol -Endpoint $Endpoint
    
        # setup default static files
        if ($null -eq $Defaults) {
            $Defaults = Get-PodeStaticRouteDefaults
        }
    
        # add the route path
        $PodeContext.Server.Routes[$HttpMethod][$Route] += @(@{
            'Path' = $Source;
            'Defaults' = $Defaults;
            'Protocol' = $Protocol;
            'Endpoint' = $Endpoint.Trim();
            'Download' = $DownloadOnly;
        })
    }
    
    function Update-PodeRoutePlaceholders
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Route
        )
    
        # replace placeholder parameters with regex
        $placeholder = '\:(?<tag>[\w]+)'
        if ($Route -imatch $placeholder) {
            $Route = [regex]::Escape($Route)
        }
    
        while ($Route -imatch $placeholder) {
            $Route = ($Route -ireplace $Matches[0], "(?<$($Matches['tag'])>[\w-_]+?)")
        }
    
        return $Route
    }
    
    function Update-PodeRouteSlashes
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Route,
    
            [switch]
            $Static
        )
    
        # ensure route starts with a '/'
        if (!$Route.StartsWith('/')) {
            $Route = "/$($Route)"
        }
    
        if ($Static)
        {
            # ensure the static route ends with '/{0,1}.*'
            $Route = $Route.TrimEnd('/*')
            $Route = "$($Route)[/]{0,1}(?<file>*)"
        }
    
        # replace * with .*
        $Route = ($Route -ireplace '\*', '.*')
        return $Route
    }
    
    function Split-PodeRouteQuery
    {
        param (
            [Parameter(Mandatory=$true)]
            [string]
            $Route
        )
    
        return ($Route -isplit "\?")[0]
    }
    
    function Get-PodeStaticRouteDefaults
    {
        if (!(Test-Empty $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]
            $HttpMethod,
    
            [Parameter(Mandatory=$true)]
            [string]
            $Route,
    
            [Parameter()]
            [string]
            $Protocol,
    
            [Parameter()]
            [string]
            $Endpoint
        )
    
        $found = @($PodeContext.Server.Routes[$HttpMethod][$Route])
    
        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 "[$($HttpMethod)] $($Route) is already defined"
        }
        else {
            throw "[$($HttpMethod)] $($Route) is already defined for $($_url)"
        }
    }
  • src\Tools\Schedules.ps1 Show
    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 = {
            # 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 {
    
                    # increment total number of triggers for the schedule
                    if ($_.Countable) {
                        $_.Count++
                        $_.Countable = ($_.Count -lt $_.Limit)
                    }
    
                    # check if we have hit the limit, and remove
                    if ($_.Limit -ne 0 -and $_.Count -ge $_.Limit) {
                        $_remove += $_.Name
                    }
    
                    try {
                        # trigger the schedules logic
                        Add-PodeRunspace -Type 'Schedules' -ScriptBlock (($_.Script).GetNewClosure()) `
                            -Parameters @{ 'Lockable' = $PodeContext.Lockable } -Forget
                    }
                    catch {
                        $Error[0]
                    }
    
                    # reset the cron if it's random
                    $_.Crons = Reset-PodeRandomCronExpressions -Expressions $_.Crons
                }
    
                # add any schedules to remove that have exceeded their end time
                $_remove += @($PodeContext.Schedules.Values |
                    Where-Object { ($null -ne $_.EndTime -and $_.EndTime -lt $_now) }).Name
    
                # remove any schedules
                $_remove | ForEach-Object {
                    if ($PodeContext.Schedules.ContainsKey($_)) {
                        $PodeContext.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
    }
    
    function Schedule
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string[]]
            $Cron,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock,
    
            [Parameter()]
            [Alias('l')]
            [int]
            $Limit = 0,
    
            [Parameter()]
            [Alias('start', 's')]
            $StartTime = $null,
    
            [Parameter()]
            [Alias('end', 'e')]
            $EndTime = $null
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'schedule' -ThrowError
    
        # lower the name
        $Name = $Name.ToLowerInvariant()
    
        # ensure the schedule doesn't already exist
        if ($PodeContext.Schedules.ContainsKey($Name)) {
            throw "Schedule called $($Name) already exists"
        }
    
        # 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) must have an EndTime 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;
        }
    }
  • src\Tools\Security.ps1 Show
    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-Empty $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 = (iftet $_rule_ip.Grouped $_rule_ip.IP $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-Empty $allow)
        $dnEmpty = (Test-Empty $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 Limit
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('IP')]
            [Alias('t')]
            [string]
            $Type,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [Alias('v')]
            [object]
            $Value,
    
            [Parameter(Mandatory=$true)]
            [Alias('l')]
            [int]
            $Limit,
    
            [Parameter(Mandatory=$true)]
            [Alias('s')]
            [int]
            $Seconds,
    
            [switch]
            $Group
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'limit' -ThrowError
    
        # if it's array add them all
        if ((Get-PodeType $Value).BaseName -ieq 'array') {
            $Value | ForEach-Object {
                limit -Type $Type -Value $_ -Limit $Limit -Seconds $Seconds -Group:$Group
            }
    
            return
        }
    
        # call the appropriate limit method
        switch ($Type.ToLowerInvariant())
        {
            'ip' {
                Add-PodeIPLimit -IP $Value -Limit $Limit -Seconds $Seconds -Group:$Group
            }
        }
    }
    
    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 Access
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('Allow', 'Deny')]
            [Alias('p')]
            [string]
            $Permission,
    
            [Parameter(Mandatory=$true)]
            [ValidateSet('IP')]
            [Alias('t')]
            [string]
            $Type,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [Alias('v')]
            [object]
            $Value
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'access' -ThrowError
    
        # if it's array add them all
        if ((Get-PodeType $Value).BaseName -ieq 'array') {
            $Value | ForEach-Object {
                access -Permission $Permission -Type $Type -Value $_
            }
    
            return
        }
    
        # call the appropriate access method
        switch ($Type.ToLowerInvariant())
        {
            'ip' {
                Add-PodeIPAccess -Permission $Permission -IP $Value
            }
        }
    }
    
    function Add-PodeIPAccess
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('Allow', 'Deny')]
            [string]
            $Permission,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [string]
            $IP
        )
    
        # current access type
        $type = 'IP'
    
        # get opposite permission
        $opp = "$(if ($Permission -ieq 'allow') { 'Deny' } else { 'Allow' })"
    
        # get permission lists for type
        $permType = $PodeContext.Server.Access[$Permission][$type]
        $oppType = $PodeContext.Server.Access[$opp][$type]
    
        # setup up perm type
        if ($null -eq $permType) {
            $PodeContext.Server.Access[$Permission][$type] = @{}
            $permType = $PodeContext.Server.Access[$Permission][$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 Csrf
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateSet('Check', 'Middleware', 'Setup', 'Token')]
            [Alias('a')]
            [string]
            $Action,
    
            [Parameter()]
            [ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC')]
            [Alias('i')]
            [string[]]
            $IgnoreMethods = @('GET', 'HEAD', 'OPTIONS', 'TRACE', 'STATIC'),
    
            [Parameter()]
            [Alias('s')]
            [string]
            $Secret,
    
            [switch]
            [Alias('c')]
            $Cookie
        )
    
        switch ($Action.ToLowerInvariant())
        {
            'check' {
                return (Get-PodeCsrfCheck)
            }
    
            'middleware' {
                Set-PodeCsrfSetup -IgnoreMethods $IgnoreMethods -Secret $Secret -Cookie:$Cookie
                return (Get-PodeCsrfMiddleware)
            }
    
            'setup' {
                Set-PodeCsrfSetup -IgnoreMethods $IgnoreMethods -Secret $Secret -Cookie:$Cookie
            }
    
            'token' {
                return (New-PodeCsrfToken)
            }
        }
    }
    
    function Set-PodeCsrfSetup
    {
        param (
            [Parameter()]
            [ValidateSet('DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC')]
            [string[]]
            $IgnoreMethods = @('GET', 'HEAD', 'OPTIONS', 'TRACE', 'STATIC'),
    
            [Parameter()]
            [string]
            $Secret,
    
            [switch]
            $Cookie
        )
    
        # check that csrf logic hasn't already been defined
        if (!(Test-Empty $PodeContext.Server.Cookies.Csrf)) {
            return
        }
    
        # if sessions haven't been setup and we're not using cookies, error
        if (!$Cookie -and (Test-Empty $PodeContext.Server.Cookies.Session)) {
            throw 'Sessions are required to use CSRF unless you pass the -Cookie flag'
        }
    
        # if we're using cookies, ensure a global secret exists
        if ($Cookie) {
            $Secret = (coalesce $Secret (Get-PodeCookieGlobalSecret))
    
            if (Test-Empty $Secret) {
                throw "When using cookies for CSRF, a secret is required. You can either supply a secret, or set the cookie global secret - (cookie secrets global <value>)"
            }
        }
    
        # set the options against the server context
        $PodeContext.Server.Cookies.Csrf = @{
            'Name' = 'pode.csrf';
            'Cookie' = $Cookie;
            'Secret' = $Secret;
            'IgnoredMethods' = $IgnoreMethods;
        }
    }
    
    function Get-PodeCsrfMiddleware
    {
        # check that csrf logic has been defined
        if (Test-Empty $PodeContext.Server.Cookies.Csrf) {
            throw 'CSRF middleware has not been defined'
        }
    
        # return scriptblock for the csrf middleware
        return {
            param($e)
    
            # if the current route method is ignored, just return
            $ignored = @($PodeContext.Server.Cookies.Csrf.IgnoredMethods)
            if (!(Test-Empty $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)){
                status 403 'Invalid CSRF Token'
                return $false
            }
    
            # token is valid, move along
            return $true
        }
    }
    
    function Get-PodeCsrfCheck
    {
        # check that csrf logic has been defined
        if (Test-Empty $PodeContext.Server.Cookies.Csrf) {
            throw 'CSRF middleware has not been defined'
        }
    
        # return scriptblock for the csrf check middleware
        return {
            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)){
                status 403 'Invalid CSRF Token'
                return $false
            }
    
            # token is valid, move along
            return $true
        }
    }
    
    function Get-PodeCsrfToken
    {
        # key name to search
        $key = $PodeContext.Server.Cookies.Csrf.Name
    
        # check the payload
        if (!(Test-Empty $WebEvent.Data[$key])) {
            return $WebEvent.Data[$key]
        }
    
        # check the query string
        if (!(Test-Empty $WebEvent.Query[$key])) {
            return $WebEvent.Query[$key]
        }
    
        # check the headers
        $value = (Get-PodeHeader -Name $key)
        if (!(Test-Empty $value)) {
            return $value
        }
    
        return $null
    }
    
    function Test-PodeCsrfToken
    {
        param (
            [Parameter()]
            [string]
            $Secret,
    
            [Parameter()]
            [string]
            $Token
        )
    
        # if there's no token/secret, fail
        if ((Test-Empty $Secret) -or (Test-Empty $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 ((New-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-Empty $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.Cookie) {
            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.Cookie) {
            (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 New-PodeCsrfToken
    {
        param (
            [Parameter()]
            [string]
            $Secret,
    
            [Parameter()]
            [string]
            $Salt
        )
    
        # fail if the csrf logic hasn't been defined
        if (Test-Empty $PodeContext.Server.Cookies.Csrf) {
            throw 'CSRF middleware has not been defined'
        }
    
        # generate a new secret if none supplied
        if (Test-Empty $Secret) {
            $Secret = New-PodeCsrfSecret
        }
    
        # generate a new salt if none supplied
        if (Test-Empty $Salt) {
            $Salt = (New-PodeSalt -Length 8)
        }
    
        # return a new token
        return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
    }
  • src\Tools\Server.ps1 Show
    function Server
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock,
    
            [Parameter()]
            [ValidateNotNull()]
            [Alias('p')]
            [int]
            $Port = 0,
    
            [Parameter()]
            [ValidateNotNull()]
            [Alias('i')]
            [int]
            $Interval = 0,
    
            [Parameter()]
            [string]
            $IP,
    
            [Parameter()]
            [Alias('n')]
            [string]
            $Name,
    
            [Parameter()]
            [Alias('t')]
            [int]
            $Threads = 1,
    
            [Parameter()]
            [Alias('fme')]
            [string[]]
            $FileMonitorExclude,
    
            [Parameter()]
            [Alias('fmi')]
            [string[]]
            $FileMonitorInclude,
    
            [Parameter()]
            [Alias('rp', 'root')]
            [string]
            $RootPath,
    
            [Parameter()]
            [Alias('r', 'req')]
            $Request,
    
            [Parameter()]
            [ValidateSet('', 'Azure-Functions', 'Aws-Lambda')]
            [string]
            $Type,
    
            [switch]
            $Smtp,
    
            [switch]
            $Tcp,
    
            [switch]
            $Http,
    
            [switch]
            $Https,
    
            [switch]
            [Alias('dt')]
            $DisableTermination,
    
            [switch]
            [Alias('dl')]
            $DisableLogging,
    
            [switch]
            [Alias('fm')]
            $FileMonitor,
    
            [switch]
            [Alias('b')]
            $Browse
        )
    
        # ensure the session is clean
        $PodeContext = $null
    
        # validate port passed
        if ($Port -lt 0) {
            throw "Port cannot be negative: $($Port)"
        }
    
        # if an ip address was passed, ensure it's valid
        if (!(Test-Empty $IP) -and !(Test-PodeIPAddress $IP)) {
            throw "Invalid IP address has been supplied: $($IP)"
        }
    
        try {
            # get the current server type for legacy purposes
            $serverType = Get-PodeServerType -Port $Port -Interval $Interval -Smtp:$Smtp -Tcp:$Tcp -Https:$Https
    
            # configure the server's root path
            if (!(Test-Empty $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 (coalesce $RootPath $MyInvocation.PSScriptRoot) `
                -FileMonitorExclude $FileMonitorExclude `
                -FileMonitorInclude $FileMonitorInclude `
                -ServerType $Type `
                -DisableLogging:$DisableLogging `
                -FileMonitor:$FileMonitor
    
            # for legacy support, create initial listener from Server parameters
            if (@('http', 'https', 'smtp', 'tcp') -icontains $serverType) {
                listen "$($IP):$($Port)" $serverType
            }
    
            # 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-PodeServer -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-PodeServer
                }
            }
    
            Write-Host 'Terminating...' -NoNewline -ForegroundColor Yellow
            $PodeContext.Tokens.Cancellation.Cancel()
        }
        finally {
            # clean the runspaces and tokens
            Close-Pode -Exit
    
            # clean the session
            $PodeContext = $null
        }
    }
    
    function Start-PodeServer
    {
        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-ScriptBlock -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-PodeLoggerRunspace
    
                # start runspace for timers
                Start-PodeTimerRunspace
    
                # start runspace for schedules
                Start-PodeScheduleRunspace
    
                # start runspace for gui
                Start-PodeGuiRunspace
            }
    
            # start the appropriate server
            switch ($_type)
            {
                'SMTP' {
                    Start-PodeSmtpServer
                }
    
                'TCP' {
                    Start-PodeTcpServer
                }
    
                { $_ -ieq 'HTTP' -or $_ -ieq 'HTTPS' } {
                    Start-PodeWebServer -Browse:$Browse
                }
    
                'SERVICE' {
                    Start-PodeServiceServer
                }
    
                'AZURE-FUNCTIONS' {
                    Start-PodeAzFuncServer -Data $Request
                }
    
                'AWS-LAMBDA' {
                    Start-PodeAwsLambdaServer -Data $Request
                }
            }
        }
        catch {
            throw $_.Exception
        }
    }
    
    function Restart-PodeServer
    {
        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[$_] = $null
            }
    
            $PodeContext.Timers.Clear()
            $PodeContext.Schedules.Clear()
            $PodeContext.Server.Logging.Methods.Clear()
    
            # clear middle/endware
            $PodeContext.Server.Middleware = @()
            $PodeContext.Server.Endware = @()
    
            # set view engine back to default
            $PodeContext.Server.ViewEngine = @{
                'Engine' = '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
            dispose $PodeContext.Tokens.Cancellation
            $PodeContext.Tokens.Cancellation = New-Object System.Threading.CancellationTokenSource
    
            dispose $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-PodeServer
        }
        catch {
            $Error[0] | Out-Default
            throw $_.Exception
        }
    }
    
    function Get-PodeServerType
    {
        param (
            [Parameter()]
            [int]
            $Port = 0,
    
            [Parameter()]
            [int]
            $Interval = 0,
    
            [switch]
            $Smtp,
    
            [switch]
            $Tcp,
    
            [switch]
            $Https
        )
    
        if ($Smtp) {
            return 'SMTP'
        }
    
        if ($Tcp) {
            return 'TCP'
        }
    
        if ($Https) {
            return 'HTTPS'
        }
    
        if ($Port -gt 0) {
            return 'HTTP'
        }
    
        if ($Interval -gt 0) {
            return 'SERVICE'
        }
    
        return ([string]::Empty)
    }
  • src\Tools\Serverless.ps1 Show
    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
                }
    
                # 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 -HttpMethod $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)) {
                        Invoke-ScriptBlock -ScriptBlock $route.Logic -Arguments $WebEvent -Scoped
                    }
                }
            }
            catch {
                status 500 -e $_
                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 = @{}
                }
    
                # 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 -HttpMethod $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)) {
                        Invoke-ScriptBlock -ScriptBlock $route.Logic -Arguments $WebEvent -Scoped
                    }
                }
            }
            catch {
                status 500 -e $_
                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\Tools\ServiceServer.ps1 Show
    function Start-PodeServiceServer
    {
        # ensure we have svc handler
        if ($null -eq (Get-PodeTcpHandler -Type 'Service')) {
            throw 'No Service handler has been passed'
        }
    
        # 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)
                {
                    # invoke the service logic
                    Invoke-ScriptBlock -ScriptBlock (Get-PodeTcpHandler -Type 'Service') -Scoped
                    #Invoke-ScriptBlock -ScriptBlock $PodeContext.Server.Logic -NoNewClosure
    
                    # sleep before next run
                    Start-Sleep -Seconds $PodeContext.Server.Interval
                }
            }
            catch [System.OperationCanceledException] {}
            catch {
                $Error[0] | Out-Default
                throw $_.Exception
            }
        }
    
        # start the runspace for the server
        Add-PodeRunspace -Type 'Main' -ScriptBlock $serverScript
    }
  • src\Tools\Sessions.ps1 Show
    function Session
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [hashtable]
            $Options
        )
    
        # check that session logic hasn't already been defined
        if (!(Test-Empty $PodeContext.Server.Cookies.Session)) {
            throw 'Session middleware has already been defined'
        }
    
        # ensure a secret was actually passed
        if (Test-Empty $Options.Secret) {
            throw 'A secret key is required for session cookies'
        }
    
        # ensure the override generator is a scriptblock
        if (!(Test-Empty $Options.GenerateId) -and ($Options.GenerateId -isnot 'scriptblock')) {
            throw "Session GenerateId should be a ScriptBlock, but got: $((Get-PodeType $Options.GenerateId).Name)"
        }
    
        # ensure the override store has the required methods
        if (!(Test-Empty $Options.Store)) {
            $members = @($Options.Store | Get-Member | Select-Object -ExpandProperty Name)
            @('delete', 'get', 'set') | ForEach-Object {
                if ($members -inotcontains $_) {
                    throw "Custom session store does not implement the required '$($_)' method"
                }
            }
        }
    
        # ensure the duration is not <0
        $Options.Duration = [int]($Options.Duration)
        if ($Options.Duration -lt 0) {
            throw "Session duration must be 0 or greater, but got: $($Options.Duration)s"
        }
    
        # get the appropriate store
        $store = $Options.Store
    
        # if no custom store, use the inmem one
        if (Test-Empty $store) {
            $store = (Get-PodeSessionCookieInMemStore)
            Set-PodeSessionCookieInMemClearDown
        }
    
        # set options against server context
        $PodeContext.Server.Cookies.Session = @{
            'Name' = (coalesce $Options.Name 'pode.sid');
            'SecretKey' = $Options.Secret;
            'GenerateId' = (coalesce $Options.GenerateId { return (New-PodeGuid) });
            'Store' = $store;
            'Info' = @{
                'Duration' = [int]($Options.Duration);
                'Extend' = [bool]($Options.Extend);
                'Secure' = [bool]($Options.Secure);
                'Discard' = [bool]($Options.Discard);
                'HttpOnly' = [bool]($Options.HttpOnly);
            };
        }
    
        # return scriptblock for the session middleware
        return {
            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.SecretKey
    
                # 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 += {
                    param($e)
    
                    # if auth is in use, then assign to session store
                    if (!(Test-Empty $e.Auth) -and $e.Auth.Store) {
                        $e.Session.Data.Auth = $e.Auth
                    }
    
                    Invoke-ScriptBlock -ScriptBlock $e.Session.Save -Arguments @($e.Session, $true) -Splat
                }
            }
            catch {
                $Error[0] | Out-Default
                return $false
            }
    
            # move along
            return $true
        }
    }
    
    function New-PodeSessionCookie
    {
        $sid = @{
            'Name' = $PodeContext.Server.Cookies.Session.Name;
            'Id' = (Invoke-ScriptBlock -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.SecretKey `
            -Expiry (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-PodeCookieIsSigned -Name $Name -Secret $Secret)) {
            return $null
        }
    
        # get the cookie from the request
        $cookie = Get-PodeCookie -Name $Name -Secret $Secret
        if (Test-Empty $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-ScriptBlock -ScriptBlock $Session.Delete -Arguments @($Session) -Splat
    
        # blank the session
        $Session.Clear()
    }
    
    function Set-PodeSessionCookieDataHash
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Session
        )
    
        $Session.Data = (coalesce $Session.Data @{})
        $Session.DataHash = (Invoke-PodeSHA256Hash -Value ($Session.Data | ConvertTo-Json -Depth 10 -Compress))
    }
    
    function Test-PodeSessionCookieDataHash
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            $Session
        )
    
        if (Test-Empty $Session.DataHash) {
            return $false
        }
    
        $Session.Data = (coalesce $Session.Data @{})
        $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
        )
    
        $expiry = (iftet $Session.Cookie.Extend ([DateTime]::UtcNow) $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)
    
            # 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
        Schedule -Name '__pode_session_inmem_cleanup__' -Cron '0/10 * * * *' -ScriptBlock {
            $store = $PodeContext.Server.Cookies.Session.Store
            if (Test-Empty $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)
                }
            }
        }
    }
  • src\Tools\Setup.ps1 Show
    function Pode
    {
        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
            }
        }
    }
    
    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\Tools\SmtpServer.ps1 Show
    function Start-PodeSmtpServer
    {
        # ensure we have smtp handlers
        if ($null -eq (Get-PodeTcpHandler -Type 'SMTP')) {
            throw 'No SMTP handler has been passed'
        }
    
        # 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 $TcpEvent.Client) {
                    return
                }
    
                # variables to store data for later processing
                $mail_from = [string]::Empty
                $rcpt_tos = @()
                $data = [string]::Empty
    
                # open response to smtp request
                tcp write "220 $($PodeContext.Server.Endpoints[0].HostName) -- Pode Proxy Server"
                $msg = [string]::Empty
    
                # respond to smtp request
                while ($true)
                {
                    try { $msg = (tcp read) }
                    catch {
                        $Error[0] | Out-Default
                        break
                    }
    
                    try {
                        if (!(Test-Empty $msg)) {
                            if ($msg.StartsWith('QUIT')) {
                                tcp write '221 Bye'
                                Close-PodeTcpConnection
                                break
                            }
    
                            if ($msg.StartsWith('EHLO') -or $msg.StartsWith('HELO')) {
                                tcp write '250 OK'
                            }
    
                            if ($msg.StartsWith('RCPT TO')) {
                                tcp write '250 OK'
                                $rcpt_tos += (Get-PodeSmtpEmail $msg)
                            }
    
                            if ($msg.StartsWith('MAIL FROM')) {
                                tcp write '250 OK'
                                $mail_from = (Get-PodeSmtpEmail $msg)
                            }
    
                            if ($msg.StartsWith('DATA'))
                            {
                                tcp write '354 Start mail input; end with <CR><LF>.<CR><LF>'
                                $data = (tcp read)
                                tcp write '250 OK'
    
                                # set event data/headers
                                $SmtpEvent.From = $mail_from
                                $SmtpEvent.To = $rcpt_tos
                                $SmtpEvent.Data = $data
                                $SmtpEvent.Headers = (Get-PodeSmtpHeadersFromData $data)
                                $SmtpEvent.Lockable = $PodeContext.Lockable
    
                                # set the subject/priority/content-types
                                $SmtpEvent.Subject = $SmtpEvent.Headers['Subject']
                                $SmtpEvent.IsUrgent = (($SmtpEvent.Headers['Priority'] -ieq 'urgent') -or ($SmtpEvent.Headers['Importance'] -ieq 'high'))
                                $SmtpEvent.ContentType = $SmtpEvent.Headers['Content-Type']
                                $SmtpEvent.ContentEncoding = $SmtpEvent.Headers['Content-Transfer-Encoding']
    
                                # set the email body
                                $SmtpEvent.Body = (Get-PodeSmtpBody -Data $data -ContentType $SmtpEvent.ContentType -ContentEncoding $SmtpEvent.ContentEncoding)
    
                                # call user handlers for processing smtp data
                                Invoke-ScriptBlock -ScriptBlock (Get-PodeTcpHandler -Type 'SMTP') -Arguments $SmtpEvent -Scoped
    
                                # reset the to list
                                $rcpt_tos = @()
                            }
                        }
                    }
                    catch [exception] {
                        $Error[0] | Out-Default
                        throw $_.exception
                    }
                }
            }
    
            try
            {
                while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested)
                {
                    # get an incoming request
                    $client = (await $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 = @{}
                        $TcpEvent = @{
                            'Client' = $client;
                            'Lockable' = $PodeContext.Lockable
                        }
    
                        Invoke-ScriptBlock -ScriptBlock $process
    
                        Close-PodeTcpConnection -Quit
                    }
                }
            }
            catch [System.OperationCanceledException] {
                Close-PodeTcpConnection -Quit
            }
            catch {
                $Error[0] | Out-Default
                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 {
                $Error[0] | Out-Default
                throw $_.Exception
            }
            finally {
                if ($null -ne $Listener) {
                    $Listener.Stop()
                }
            }
        }
    
        Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }
    
        # state where we're running
        Write-Host "Listening on smtp://$($PodeContext.Server.Endpoints[0].HostName):$($port) [$($PodeContext.Threads) thread(s)]" -ForegroundColor Yellow
    }
    
    
    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-Empty $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\Tools\Streams.ps1 Show
    function Read-PodeStreamToEnd
    {
        param (
            [Parameter()]
            $Stream,
    
            [Parameter()]
            $Encoding = [System.Text.Encoding]::UTF8
        )
    
        if ($null -eq $Stream) {
            return [string]::Empty
        }
    
        return (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-Empty $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]
    }
    
    <#
        This is basically like "using" in .Net
    #>
    function Stream
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [System.IDisposable]
            $InputObject,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock
        )
    
        try {
            return (Invoke-ScriptBlock -ScriptBlock $ScriptBlock -Arguments $InputObject -Return -NoNewClosure)
        }
        catch {
            $Error[0] | Out-Default
            throw $_.Exception
        }
        finally {
            $InputObject.Dispose()
        }
    }
    
    function Dispose
    {
        param (
            [Parameter()]
            [System.IDisposable]
            $InputObject,
    
            [switch]
            $Close,
    
            [switch]
            $CheckNetwork
        )
    
        if ($null -eq $InputObject) {
            return
        }
    
        try {
            if ($Close) {
                $InputObject.Close()
            }
        }
        catch [exception] {
            if ($CheckNetwork -and (Test-PodeValidNetworkFailure $_.Exception)) {
                return
            }
    
            $Error[0] | Out-Default
            throw $_.Exception
        }
        finally {
            $InputObject.Dispose()
        }
    }
  • src\Tools\TcpServer.ps1 Show
    function Start-PodeTcpServer
    {
        # ensure we have tcp handler
        if ($null -eq (Get-PodeTcpHandler -Type 'TCP')) {
            throw 'No TCP handler has been passed'
        }
    
        # 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 = (await $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;
                            'Lockalble' = $PodeContext.Lockable
                        }
    
                        Invoke-ScriptBlock -ScriptBlock (Get-PodeTcpHandler -Type 'TCP') -Arguments $TcpEvent -Scoped
                    }
    
                    # close the connection
                    Close-PodeTcpConnection
                }
            }
            catch [System.OperationCanceledException] {}
            catch {
                $Error[0] | Out-Default
                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 {
                $Error[0] | Out-Default
                throw $_.Exception
            }
            finally {
                if ($null -ne $Listener) {
                    $Listener.Stop()
                }
            }
        }
    
        Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }
    
        # state where we're running
        Write-Host "Listening on tcp://$($PodeContext.Server.Endpoints[0].HostName):$($port) [$($PodeContext.Threads) thread(s)]" -ForegroundColor Yellow
    }
    
  • src\Tools\Timers.ps1 Show
    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 { $_.NextTick -le $_now } | ForEach-Object {
                    $run = $true
    
                    # increment total number of runs for timer (do we still need to count?)
                    if ($_.Countable) {
                        $_.Count++
                        $_.Countable = ($_.Count -lt $_.Skip -or $_.Count -lt $_.Limit)
                    }
    
                    # check if this run should be skipped
                    if ($_.Count -lt $_.Skip) {
                        $run = $false
                    }
    
                    # check if we have hit the limit, and remove
                    if ($run -and $_.Limit -ne 0 -and $_.Count -ge $_.Limit) {
                        $run = $false
                        $_remove += $_.Name
                    }
    
                    if ($run) {
                        try {
                            Invoke-ScriptBlock -ScriptBlock $_.Script -Arguments @{ 'Lockable' = $PodeContext.Lockable } -Scoped
                        }
                        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
    }
    
    function Timer
    {
        param (
            [Parameter(Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [string]
            $Name,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [Alias('i')]
            [int]
            $Interval,
    
            [Parameter(Mandatory=$true)]
            [ValidateNotNull()]
            [scriptblock]
            $ScriptBlock,
    
            [Parameter()]
            [Alias('l')]
            [int]
            $Limit = 0,
    
            [Parameter()]
            [Alias('s')]
            [int]
            $Skip = 0
        )
    
        # error if serverless
        Test-PodeIsServerless -FunctionName 'timer' -ThrowError
    
        # lower the name
        $Name = $Name.ToLowerInvariant()
    
        # ensure the timer doesn't already exist
        if ($PodeContext.Timers.ContainsKey($Name)) {
            throw "Timer called $($Name) already exists"
        }
    
        # is the interval valid?
        if ($Interval -le 0) {
            throw "Timer $($Name) cannot have an interval less than or equal to 0"
        }
    
        # is the limit valid?
        if ($Limit -lt 0) {
            throw "Timer $($Name) cannot have a negative limit"
        }
    
        if ($Limit -ne 0) {
            $Limit += $Skip
        }
    
        # is the skip valid?
        if ($Skip -lt 0) {
            throw "Timer $($Name) cannot have a negative skip value"
        }
    
        # run script if it's not being skipped
        if ($Skip -eq 0) {
            Invoke-ScriptBlock -ScriptBlock $ScriptBlock -Arguments @{ 'Lockable' = $PodeContext.Lockable } -Scoped
        }
    
        # add the timer
        $PodeContext.Timers[$Name] = @{
            'Name' = $Name;
            'Interval' = $Interval;
            'Limit' = $Limit;
            'Count' = 0;
            'Skip' = $Skip;
            'Countable' = ($Skip -gt 0 -or $Limit -gt 0);
            'NextTick' = [DateTime]::Now.AddSeconds($Interval);
            'Script' = $ScriptBlock;
        }
    }
  • src\Tools\WebServer.ps1 Show
    function Engine
    {
        param (
            [Parameter()]
            [ValidateNotNullOrEmpty()]
            [Alias('t')]
            [string]
            $Engine,
    
            [Parameter()]
            [Alias('s')]
            [scriptblock]
            $ScriptBlock = $null,
    
            [Parameter()]
            [Alias('ext')]
            [string]
            $Extension
        )
    
        if ([string]::IsNullOrWhiteSpace($Extension)) {
            $Extension = $Engine.ToLowerInvariant()
        }
    
        $PodeContext.Server.ViewEngine.Engine = $Engine.ToLowerInvariant()
        $PodeContext.Server.ViewEngine.Extension = $Extension
        $PodeContext.Server.ViewEngine.Script = $ScriptBlock
        $PodeContext.Server.ViewEngine.IsDynamic = ($Engine -ine 'html')
    }
    
    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 = @()
        $PodeContext.Server.Endpoints | ForEach-Object {
            # get the protocol
            $_protocol = (iftet $_.Ssl 'https' '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 = (iftet $_.Ssl 8443 8080)
            }
    
            # if this endpoint is https, generate a self-signed cert or bind an existing one
            if ($_.Ssl) {
                $addr = (iftet $_.IsIPAddress $_.Address $_.HostName)
                Set-PodeCertificate -Address $addr -Port $_port -Certificate $_.Certificate.Name -Thumbprint $_.Certificate.Thumbprint
            }
    
            # 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 {
            $Error[0] | Out-Default
    
            if ($null -ne $Listener) {
                if ($Listener.IsListening) {
                    $Listener.Stop()
                }
    
                dispose $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 = (await $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 = @{}
                        }
    
                        # set pode in server response header
                        Set-PodeServerHeader
    
                        # add logging endware for post-request
                        Add-PodeLogEndware -WebEvent $WebEvent
    
                        # invoke middleware
                        if ((Invoke-PodeMiddleware -WebEvent $WebEvent -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) {
                            # get the route logic
                            $route = Get-PodeRoute -HttpMethod $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)) {
                                Invoke-ScriptBlock -ScriptBlock $route.Logic -Arguments $WebEvent -Scoped
                            }
                        }
                    }
                    catch {
                        status 500 -e $_
                        $Error[0] | Out-Default
                    }
    
                    # 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) {
                        dispose $response.OutputStream -Close -CheckNetwork
                    }
                }
            }
            catch [System.OperationCanceledException] {}
            catch {
                $Error[0] | Out-Default
                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 {
                $Error[0] | Out-Default
                throw $_.Exception
            }
            finally {
                if ($null -ne $Listener) {
                    if ($Listener.IsListening) {
                        $Listener.Stop()
                    }
    
                    dispose $Listener -Close
                }
            }
        }
    
        Add-PodeRunspace -Type 'Main' -ScriptBlock $waitScript -Parameters @{ 'Listener' = $listener }
    
        # state where we're running
        Write-Host "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads) thread(s)]:" -ForegroundColor Yellow
    
        $endpoints | ForEach-Object {
            Write-Host "`t- $($_.HostName)" -ForegroundColor Yellow
        }
    
        # browse to the first endpoint, if flagged
        if ($Browse) {
            Start-Process $endpoints[0].HostName
        }
    }
  • src\VERIFICATION.txt Show
    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.
  • tools\ChocolateyInstall.ps1 Show
    $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 'Tools') -Force | Out-Null
            New-Item -ItemType Directory -Path (Join-Path $path 'Misc') -Force | Out-Null
    
            Copy-Item -Path ./Tools/* -Destination (Join-Path $path 'Tools') -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 '0.32.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 '0.32.0'
    }
    
  • tools\ChocolateyUninstall.ps1 Show
    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
    }
    

Virus Scan Results

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.

Dependencies

This package has no dependencies.

Package Maintainer(s)

Software Author(s)

  • Badgerati

Copyright

Copyright 2017-2019

Tags

Release Notes

https://github.com/Badgerati/Pode/releases

Version History

Version Downloads Last updated Status
Pode 0.31.0 20 Tuesday, June 11, 2019 approved
Pode 0.30.0 30 Sunday, May 26, 2019 approved
Pode 0.29.0 36 Friday, May 10, 2019 approved
Pode 0.28.1 42 Tuesday, April 16, 2019 approved
Pode 0.28.0 35 Saturday, April 13, 2019 approved
Pode 0.27.3 44 Thursday, April 4, 2019 approved
Pode 0.27.2 42 Wednesday, March 27, 2019 approved
Pode 0.27.1 43 Saturday, March 16, 2019 approved
Pode 0.27.0 30 Thursday, March 14, 2019 approved
Show More

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
Chocolatey.org uses cookies to enhance the user experience of the site.
Ok