Public/Middleware.ps1

<#
.SYNOPSIS
Adds an access rule to allow or deny IP addresses.
 
.DESCRIPTION
Adds an access rule to allow or deny IP addresses.
 
.PARAMETER Access
The type of access to enable.
 
.PARAMETER Type
What type of request are we configuring?
 
.PARAMETER Values
A single, or an array of values.
 
.EXAMPLE
Add-PodeAccessRule -Access Allow -Type IP -Values '127.0.0.1'
 
.EXAMPLE
Add-PodeAccessRule -Access Deny -Type IP -Values @('192.168.1.1', '10.10.1.0/24')
#>

function Add-PodeAccessRule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Allow', 'Deny')]
        [string]
        $Access,

        [Parameter(Mandatory = $true)]
        [ValidateSet('IP')]
        [string]
        $Type,

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

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeAccessRule' -ThrowError

    # call the appropriate access method
    switch ($Type.ToLowerInvariant()) {
        'ip' {
            foreach ($ip in $Values) {
                Add-PodeIPAccess -Access $Access -IP $ip
            }
        }
    }
}

<#
.SYNOPSIS
Adds rate limiting rules for an IP addresses, Routes, or Endpoints.
 
.DESCRIPTION
Adds rate limiting rules for an IP addresses, Routes, or Endpoints.
 
.PARAMETER Type
What type of request is being rate limited: IP, Route, or Endpoint?
 
.PARAMETER Values
A single, or an array of values.
 
.PARAMETER Limit
The maximum number of requests to allow.
 
.PARAMETER Seconds
The number of seconds to count requests before restarting the count.
 
.PARAMETER Group
If supplied, groups of IPs in a subnet will be considered as one IP.
 
.EXAMPLE
Add-PodeLimitRule -Type IP -Values '127.0.0.1' -Limit 10 -Seconds 1
 
.EXAMPLE
Add-PodeLimitRule -Type IP -Values @('192.168.1.1', '10.10.1.0/24') -Limit 50 -Seconds 1 -Group
 
.EXAMPLE
Add-PodeLimitRule -Type Route -Values '/downloads' -Limit 5 -Seconds 1
#>

function Add-PodeLimitRule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('IP', 'Route', 'Endpoint')]
        [string]
        $Type,

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

        [Parameter(Mandatory = $true)]
        [int]
        $Limit,

        [Parameter(Mandatory = $true)]
        [int]
        $Seconds,

        [switch]
        $Group
    )

    # call the appropriate limit method
    foreach ($value in $Values) {
        switch ($Type.ToLowerInvariant()) {
            'ip' {
                Test-PodeIsServerless -FunctionName 'Add-PodeLimitRule' -ThrowError
                Add-PodeIPLimit -IP $value -Limit $Limit -Seconds $Seconds -Group:$Group
            }

            'route' {
                Add-PodeRouteLimit -Path $value -Limit $Limit -Seconds $Seconds -Group:$Group
            }

            'endpoint' {
                Add-PodeEndpointLimit -EndpointName $value -Limit $Limit -Seconds $Seconds -Group:$Group
            }
        }
    }
}

<#
.SYNOPSIS
Creates and returns a new secure token for use with CSRF.
 
.DESCRIPTION
Creates and returns a new secure token for use with CSRF.
 
.EXAMPLE
$token = New-PodeCsrfToken
#>

function New-PodeCsrfToken {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    # fail if the csrf logic hasn't been initialised
    if (!(Test-PodeCsrfConfigured)) {
        # CSRF Middleware has not been initialized
        throw ($PodeLocale.csrfMiddlewareNotInitializedExceptionMessage)
    }

    # generate a new secret and salt
    $Secret = New-PodeCsrfSecret
    $Salt = (New-PodeSalt -Length 8)

    # return a new token
    return "t:$($Salt).$(Invoke-PodeSHA256Hash -Value "$($Salt)-$($Secret)")"
}

<#
.SYNOPSIS
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.
 
.DESCRIPTION
Returns adhoc CSRF CSRF verification Middleware, for use on Routes.
 
.EXAMPLE
$csrf = Get-PodeCsrfMiddleware
Add-PodeRoute -Method Get -Path '/cpu' -Middleware $csrf -ScriptBlock { /* logic */ }
#>

function Get-PodeCsrfMiddleware {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    # fail if the csrf logic hasn't been initialised
    if (!(Test-PodeCsrfConfigured)) {
        # CSRF Middleware has not been initialized
        throw ($PodeLocale.csrfMiddlewareNotInitializedExceptionMessage)
    }

    # return scriptblock for the csrf route middleware to test tokens
    $script = {
        # if there's not a secret, generate and store it
        $secret = New-PodeCsrfSecret

        # verify the token on the request, if invalid, throw a 403
        $token = Get-PodeCsrfToken

        if (!(Test-PodeCsrfToken -Secret $secret -Token $token)) {
            Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
            return $false
        }

        # token is valid, move along
        return $true
    }

    return (New-PodeMiddleware -ScriptBlock $script)
}

<#
.SYNOPSIS
Initialises CSRF within Pode for adhoc usage.
 
.DESCRIPTION
Initialises CSRF within Pode for adhoc usage, with configurable HTTP methods to ignore verification.
 
.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.
 
.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.
 
.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.
 
.EXAMPLE
Initialize-PodeCsrf -IgnoreMethods @('Get', 'Trace')
 
.EXAMPLE
Initialize-PodeCsrf -Secret 'some-secret' -UseCookies
#>

function Initialize-PodeCsrf {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string[]]
        $IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),

        [Parameter()]
        [string]
        $Secret,

        [switch]
        $UseCookies
    )

    # check that csrf logic hasn't already been intialised
    if (Test-PodeCsrfConfigured) {
        return
    }

    # if sessions haven't been setup and we're not using cookies, error
    if (!$UseCookies -and !(Test-PodeSessionsEnabled)) {
        # Sessions are required to use CSRF unless you want to use cookies
        throw ($PodeLocale.sessionsRequiredForCsrfExceptionMessage)
    }

    # if we're using cookies, ensure a global secret exists
    if ($UseCookies) {
        $Secret = (Protect-PodeValue -Value $Secret -Default (Get-PodeCookieSecret -Global))

        if (Test-PodeIsEmpty $Secret) {
            # When using cookies for CSRF, a Secret is required
            throw ($PodeLocale.csrfCookieRequiresSecretExceptionMessage)
        }
    }

    # set the options against the server context
    $PodeContext.Server.Cookies.Csrf = @{
        Name           = 'pode.csrf'
        UseCookies     = $UseCookies
        Secret         = $Secret
        IgnoredMethods = $IgnoreMethods
    }
}

<#
.SYNOPSIS
Enables Middleware for verifying CSRF tokens on Requests.
 
.DESCRIPTION
Enables Middleware for verifying CSRF tokens on Requests, with configurable HTTP methods to ignore verification.
 
.PARAMETER IgnoreMethods
An array of HTTP methods to ignore CSRF verification.
 
.PARAMETER Secret
A secret to use when signing cookies - for when using CSRF with cookies.
 
.PARAMETER UseCookies
If supplied, CSRF will used cookies rather than sessions.
 
.EXAMPLE
Enable-PodeCsrfMiddleware -IgnoreMethods @('Get', 'Trace')
 
.EXAMPLE
Enable-PodeCsrfMiddleware -Secret 'some-secret' -UseCookies
#>

function Enable-PodeCsrfMiddleware {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string[]]
        $IgnoreMethods = @('Get', 'Head', 'Options', 'Trace'),

        [Parameter(ParameterSetName = 'Cookies')]
        [string]
        $Secret,

        [Parameter(ParameterSetName = 'Cookies')]
        [switch]
        $UseCookies
    )

    Initialize-PodeCsrf -IgnoreMethods $IgnoreMethods -Secret $Secret -UseCookies:$UseCookies

    # return scriptblock for the csrf middleware
    $script = {
        # if the current route method is ignored, just return
        $ignored = @($PodeContext.Server.Cookies.Csrf.IgnoredMethods)
        if (!(Test-PodeIsEmpty $ignored) -and ($ignored -icontains $WebEvent.Method)) {
            return $true
        }

        # if there's not a secret, generate and store it
        $secret = New-PodeCsrfSecret

        # verify the token on the request, if invalid, throw a 403
        $token = Get-PodeCsrfToken

        if (!(Test-PodeCsrfToken -Secret $secret -Token $token)) {
            Set-PodeResponseStatus -Code 403 -Description 'Invalid CSRF Token'
            return $false
        }

        # token is valid, move along
        return $true
    }

    (New-PodeMiddleware -ScriptBlock $script) | Add-PodeMiddleware -Name '__pode_mw_csrf__'
}

<#
.SYNOPSIS
Adds a custom body parser middleware.
 
.DESCRIPTION
Adds a custom body parser middleware script for a content-type, which will be used if a payload is sent with a Request.
 
.PARAMETER ContentType
The ContentType of the custom body parser.
 
.PARAMETER ScriptBlock
The ScriptBlock that will parse the body content, and return the result.
 
.EXAMPLE
Add-PodeBodyParser -ContentType 'application/json' -ScriptBlock { param($body) /* parsing logic */ }
#>

function Add-PodeBodyParser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock
    )
    begin {
        $pipelineItemCount = 0
    }

    process {
        $pipelineItemCount++
    }

    end {
        if ($pipelineItemCount -gt 1) {
            throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name))
        }
        # if a parser for the type already exists, fail
        if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
            # A body-parser is already defined for the content-type
            throw ($PodeLocale.bodyParserAlreadyDefinedForContentTypeExceptionMessage -f $ContentType)
        }

        # check for scoped vars
        $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

        $PodeContext.Server.BodyParsers[$ContentType] = @{
            ScriptBlock    = $ScriptBlock
            UsingVariables = $usingVars
        }
    }
}

<#
.SYNOPSIS
Removes a custom body parser.
 
.DESCRIPTION
Removes a custom body parser middleware script for a content-type.
 
.PARAMETER ContentType
The ContentType of the custom body parser.
 
.EXAMPLE
Remove-PodeBodyParser -ContentType 'application/json'
#>

function Remove-PodeBodyParser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType
    )

    process {
        # if there's no parser for the type, return
        if (!$PodeContext.Server.BodyParsers.ContainsKey($ContentType)) {
            return
        }

        $null = $PodeContext.Server.BodyParsers.Remove($ContentType)
    }
}

<#
.SYNOPSIS
Adds a new Middleware to be invoked before every Route, or certain Routes.
 
.DESCRIPTION
Adds a new Middleware to be invoked before every Route, or certain Routes. ScriptBlock should return $true to continue execution, or $false to stop.
 
.PARAMETER Name
The Name of the Middleware.
 
.PARAMETER ScriptBlock
The Script defining the logic of the Middleware. Should return $true to continue execution, or $false to stop.
 
.PARAMETER InputObject
A Middleware HashTable from New-PodeMiddleware, or from certain other functions that return Middleware as a HashTable.
 
.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.
 
.PARAMETER ArgumentList
An array of arguments to supply to the Middleware's ScriptBlock.
 
.OUTPUTS
Boolean. ScriptBlock should return $true to continue to the next middleware/route, or return $false to stop execution.
 
.EXAMPLE
Add-PodeMiddleware -Name 'BlockAgents' -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeMiddleware -Name 'CheckEmailOnApi' -Route '/api/*' -ScriptBlock { /* logic */ }
#>

function Add-PodeMiddleware {
    [CmdletBinding(DefaultParameterSetName = 'Script')]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Script')]
        [scriptblock]
        $ScriptBlock,

        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Input')]
        [hashtable]
        $InputObject,

        [Parameter()]
        [string]
        $Route,

        [Parameter()]
        [object[]]
        $ArgumentList
    )
    begin {
        $pipelineItemCount = 0
    }

    process {
        $pipelineItemCount++
    }

    end {
        if ($pipelineItemCount -gt 1) {
            throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name))
        }
        # ensure name doesn't already exist
        if (($PodeContext.Server.Middleware | Where-Object { $_.Name -ieq $Name } | Measure-Object).Count -gt 0) {
            # [Middleware] Name: Middleware already defined
            throw ($PodeLocale.middlewareAlreadyDefinedExceptionMessage -f $Name)

        }

        # if it's a script - call New-PodeMiddleware
        if ($PSCmdlet.ParameterSetName -ieq 'script') {
            $InputObject = (New-PodeMiddlewareInternal `
                    -ScriptBlock $ScriptBlock `
                    -Route $Route `
                    -ArgumentList $ArgumentList `
                    -PSSession $PSCmdlet.SessionState)
        }
        else {
            $Route = ConvertTo-PodeRouteRegex -Path $Route
            $InputObject.Route = Protect-PodeValue -Value $Route -Default $InputObject.Route
            $InputObject.Options = Protect-PodeValue -Value $Options -Default $InputObject.Options
        }

        # ensure we have a script to run
        if (Test-PodeIsEmpty $InputObject.Logic) {
            # [Middleware]: No logic supplied in ScriptBlock
            throw ($PodeLocale.middlewareNoLogicSuppliedExceptionMessage)
        }

        # set name, and override route/args
        $InputObject.Name = $Name

        # add the logic to array of middleware that needs to be run
        $PodeContext.Server.Middleware += $InputObject
    }
}

<#
.SYNOPSIS
Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes.
 
.DESCRIPTION
Creates a new Middleware HashTable object, that can be piped/used in Add-PodeMiddleware or in Routes. ScriptBlock should return $true to continue execution, or $false to stop.
 
.PARAMETER ScriptBlock
The Script that defines the logic of the Middleware. Should return $true to continue execution, or $false to stop.
 
.PARAMETER Route
A Route path for which Routes this Middleware should only be invoked against.
 
.PARAMETER ArgumentList
An array of arguments to supply to the Middleware's ScriptBlock.
 
.OUTPUTS
Boolean. ScriptBlock should return $true to continue to the next middleware/route, or return $false to stop execution.
 
.EXAMPLE
New-PodeMiddleware -ScriptBlock { /* logic */ } -ArgumentList 'Email' | Add-PodeMiddleware -Name 'CheckEmail'
#>

function New-PodeMiddleware {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [string]
        $Route,

        [Parameter()]
        [object[]]
        $ArgumentList
    )
    begin {
        $pipelineItemCount = 0
    }

    process {
        $pipelineItemCount++
    }

    end {
        if ($pipelineItemCount -gt 1) {
            throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name))
        }
        return New-PodeMiddlewareInternal `
            -ScriptBlock $ScriptBlock `
            -Route $Route `
            -ArgumentList $ArgumentList `
            -PSSession $PSCmdlet.SessionState
    }
}

<#
.SYNOPSIS
Removes a specific user defined Middleware.
 
.DESCRIPTION
Removes a specific user defined Middleware.
 
.PARAMETER Name
The Name of the Middleware to be removed.
 
.EXAMPLE
Remove-PodeMiddleware -Name 'Sessions'
#>

function Remove-PodeMiddleware {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    $PodeContext.Server.Middleware = @($PodeContext.Server.Middleware | Where-Object { $_.Name -ine $Name })
}

<#
.SYNOPSIS
Removes all user defined Middleware.
 
.DESCRIPTION
Removes all user defined Middleware.
 
.EXAMPLE
Clear-PodeMiddleware
#>

function Clear-PodeMiddleware {
    [CmdletBinding()]
    param()

    $PodeContext.Server.Middleware = @()
}

<#
.SYNOPSIS
Automatically loads middleware ps1 files
 
.DESCRIPTION
Automatically loads middleware ps1 files from either a /middleware folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
 
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
 
.EXAMPLE
Use-PodeMiddleware
 
.EXAMPLE
Use-PodeMiddleware -Path './my-middleware'
#>

function Use-PodeMiddleware {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'middleware'
}