Public/Limit.ps1

<#
.SYNOPSIS
Adds an access rule to allow or deny IP addresses. This is a legacy function, use Add-PodeLimitAccessRule instead.
 
.DESCRIPTION
Adds an access rule to allow or deny IP addresses. This is a legacy function, use Add-PodeLimitAccessRule instead.
 
.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
    )

    Add-PodeLimitAccessRule `
        -Name (New-PodeGuid) `
        -Action $Access `
        -Component (New-PodeLimitIPComponent -IP $Values)
}

<#
.SYNOPSIS
Adds rate limiting rules for an IP addresses, Routes, or Endpoints. This is a legacy function, use Add-PodeLimitRateRule instead.
 
.DESCRIPTION
Adds rate limiting rules for an IP addresses, Routes, or Endpoints. This is a legacy function, use Add-PodeLimitRateRule instead.
 
.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
    )

    $component = $null

    switch ($Type.ToLowerInvariant()) {
        'ip' {
            $component = New-PodeLimitIPComponent -IP $Values -Group:$Group
        }

        'route' {
            $component = New-PodeLimitRouteComponent -Path $Values
        }

        'endpoint' {
            $component = New-PodeLimitEndpointComponent -Name $Values
        }
    }

    Add-PodeLimitRateRule `
        -Name (New-PodeGuid) `
        -Limit $Limit `
        -Duration ($Seconds * 1000) `
        -Component $component
}

<#
.SYNOPSIS
Adds a rate limit rule.
 
.DESCRIPTION
Adds a rate limit rule.
 
.PARAMETER Name
The name of the rate limit rule.
 
.PARAMETER Component
The component(s) to check. This can be a single, or an array of components.
 
.PARAMETER Limit
The limit for the rule - the maximum number of requests to allow within the duration.
 
.PARAMETER Duration
The duration for the rule, in milliseconds. (Default: 60000)
 
.PARAMETER StatusCode
The status code to return when the limit is reached. (Default: 429)
 
.PARAMETER Priority
The priority of the rule. The higher the number, the higher the priority. (Default: [int]::MinValue)
 
.EXAMPLE
# limit to 10 requests per minute for all IPs
Add-PodeLimitRateRule -Name 'rule1' -Limit 10 -Component @(
    New-PodeLimitIPComponent
)
 
.EXAMPLE
# limit to 5 requests per minute for all IPs and the /downloads route
Add-PodeLimitRateRule -Name 'rule1' -Limit 5 -Component @(
    New-PodeLimitIPComponent
    New-PodeLimitRouteComponent -Path '/downloads'
)
 
.EXAMPLE
# limit to 1 request, per 30 seconds, for all IPs in a subnet grouped, to the /downloads route
Add-PodeLimitRateRule -Name 'rule1' -Limit 1 -Duration 30000 -Component @(
    New-PodeLimitIPComponent -IP '10.0.0.0/24' -Group
    New-PodeLimitRouteComponent -Path '/downloads'
)
 
.EXAMPLE
# limit to 10 requests per second, for specific IPs, with a custom status code and priority
Add-PodeLimitRateRule -Name 'rule1' -Limit 10 -Duration 1000 -StatusCode 401 -Priority 100 -Component @(
    New-PodeLimitIPComponent -IP '127.0.0.1', '192.0.0.1', '10.0.0.1'
)
#>

function Add-PodeLimitRateRule {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [hashtable[]]
        $Component,

        [Parameter()]
        [ValidateRange(0, [int]::MaxValue)]
        [int]
        $Limit,

        [Parameter()]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Duration = 60000,

        [Parameter()]
        [int]
        $StatusCode = 429,

        [Parameter()]
        [int]
        $Priority = [int]::MinValue
    )

    if (Test-PodeLimitRateRule -Name $Name) {
        # A rate limit rule with the name '$($Name)' already exists
        throw ($PodeLocale.rateLimitRuleAlreadyExistsExceptionMessage -f $Name)
    }

    $PodeContext.Server.Limits.Rate.Rules[$Name] = @{
        Name       = $Name
        Components = $Component
        Limit      = $Limit
        Duration   = $Duration
        StatusCode = $StatusCode
        Priority   = $Priority
        Active     = [System.Collections.Concurrent.ConcurrentDictionary[string, hashtable]]::new()
    }

    $PodeContext.Server.Limits.Rate.RulesAltered = $true
    Add-PodeLimitRateTimer
}

<#
.SYNOPSIS
Updates a rate limit rule.
 
.DESCRIPTION
Updates a rate limit rule.
 
.PARAMETER Name
The name of the rate limit rule.
 
.PARAMETER Limit
The new limit for the rule. If not supplied, the limit will not be updated.
 
.PARAMETER Duration
The new duration for the rule, in milliseconds. If not supplied, the duration will not be updated.
 
.PARAMETER StatusCode
The new status code for the rule. If not supplied, the status code will not be updated.
 
.EXAMPLE
Update-PodeLimitRateRule -Name 'rule1' -Limit 10
 
.EXAMPLE
Update-PodeLimitRateRule -Name 'rule1' -Duration 10000
 
.EXAMPLE
Update-PodeLimitRateRule -Name 'rule1' -StatusCode 429
 
.EXAMPLE
Update-PodeLimitRateRule -Name 'rule1' -Limit 10 -Duration 10000 -StatusCode 429
#>

function Update-PodeLimitRateRule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Limit = -1,

        [Parameter()]
        [int]
        $Duration = -1,

        [Parameter()]
        [int]
        $StatusCode = -1
    )

    $rule = $PodeContext.Server.Limits.Rate.Rules[$Name]
    if (!$rule) {
        # A rate limit rule with the name '$($Name)' does not exist
        throw ($PodeLocale.rateLimitRuleDoesNotExistExceptionMessage -f $Name)
    }

    if ($Limit -ge 0) {
        $rule.Limit = $Limit
    }

    if ($Duration -gt 0) {
        $rule.Duration = $Duration
    }

    if ($StatusCode -gt 0) {
        $rule.StatusCode = $StatusCode
    }
}

<#
.SYNOPSIS
Removes a rate limit rule.
 
.DESCRIPTION
Removes a rate limit rule.
 
.PARAMETER Name
The name of the rate limit rule.
 
.EXAMPLE
Remove-PodeLimitRateRule -Name 'rule1'
#>

function Remove-PodeLimitRateRule {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name
    )

    $null = $PodeContext.Server.Limits.Rate.Rules.Remove($Name)
    $PodeContext.Server.Limits.Rate.RulesAltered = $true
    Remove-PodeLimitRateTimer
}

<#
.SYNOPSIS
Tests if a rate limit rule exists.
 
.DESCRIPTION
Tests if a rate limit rule exists.
 
.PARAMETER Name
The name of the rate limit rule.
 
.EXAMPLE
Test-PodeLimitRateRule -Name 'rule1'
 
.NOTES
This function is used to test if a rate limit rule exists.
#>

function Test-PodeLimitRateRule {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter()]
        [string]
        $Name
    )

    return $PodeContext.Server.Limits.Rate.Rules.Contains($Name)
}

<#
.SYNOPSIS
Gets a rate limit rule by name.
 
.DESCRIPTION
Gets a rate limit rule by name.
 
.PARAMETER Name
The name(s) of the rate limit rule.
 
.EXAMPLE
$rules = Get-PodeLimitRateRule -Name 'rule1'
 
.EXAMPLE
$rules = Get-PodeLimitRateRule -Name 'rule1', 'rule2'
 
.EXAMPLE
$rules = Get-PodeLimitRateRule
 
.OUTPUTS
A hashtable array containing the rate limit rule(s).
#>

function Get-PodeLimitRateRule {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    if ($Name) {
        return $Name | ForEach-Object { $PodeContext.Server.Limits.Rate.Rules[$_] }
    }

    return $PodeContext.Server.Limits.Rate.Rules.Values
}

<#
.SYNOPSIS
Adds an access limit rule.
 
.DESCRIPTION
Adds an access limit rule.
 
.PARAMETER Name
The name of the access rule.
 
.PARAMETER Component
The component(s) to check. This can be a single, or an array of components.
 
.PARAMETER Action
The action to take. Either 'Allow' or 'Deny'.
 
.PARAMETER StatusCode
The status code to return. (Default: 403)
 
.PARAMETER Priority
The priority of the rule. The higher the number, the higher the priority. (Default: [int]::MinValue)
 
.EXAMPLE
# only allow localhost
Add-PodeLimitAccessRule -Name 'rule1' -Action Allow -Component @(
    New-PodeLimitIPComponent -IP '127.0.0.1'
)
 
.EXAMPLE
# only allow localhost and the /downloads route
Add-PodeLimitAccessRule -Name 'rule1' -Action Allow -Component @(
    New-PodeLimitIPComponent -IP '127.0.0.1'
    New-PodeLimitRouteComponent -Path '/downloads'
)
 
.EXAMPLE
# deny all requests
Add-PodeLimitAccessRule -Name 'rule1' -Action Deny -Component @(
    New-PodeLimitIPComponent
)
 
.EXAMPLE
# deny all requests from a subnet, with a custom status code
Add-PodeLimitAccessRule -Name 'rule1' -Action Deny -StatusCode 401 -Component @(
    New-PodeLimitIPComponent -IP '10.0.0.0/24'
)
 
.EXAMPLE
# deny all requests from a subnet, with a custom status code and priority
Add-PodeLimitAccessRule -Name 'rule1' -Action Deny -StatusCode 401 -Priority 100 -Component @(
    New-PodeLimitIPComponent -IP '192.0.1.0/16'
)
#>

function Add-PodeLimitAccessRule {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [hashtable[]]
        $Component,

        [Parameter()]
        [ValidateSet('Allow', 'Deny')]
        [string]
        $Action,

        [Parameter()]
        [int]
        $StatusCode = 403,

        [Parameter()]
        [int]
        $Priority = [int]::MinValue
    )

    if (Test-PodeLimitAccessRule -Name $Name) {
        # An access limit rule with the name '$($Name)' already exists
        throw ($PodeLocale.accessLimitRuleAlreadyExistsExceptionMessage -f $Name)
    }

    $PodeContext.Server.Limits.Access.Rules[$Name] = @{
        Name       = $Name
        Components = $Component
        Action     = $Action
        StatusCode = $StatusCode
        Priority   = $Priority
    }

    $PodeContext.Server.Limits.Access.RulesAltered = $true

    # set the flag if we have any allow rules
    if ($Action -eq 'Allow') {
        $PodeContext.Server.Limits.Access.HaveAllowRules = $true
    }
}

<#
.SYNOPSIS
Updates an access rule.
 
.DESCRIPTION
Updates an access rule.
 
.PARAMETER Name
The name of the access rule.
 
.PARAMETER Action
The action to take. Either 'Allow' or 'Deny'. If not supplied, the action will not be updated.
 
.PARAMETER StatusCode
The status code to return. If not supplied, the status code will not be updated.
 
.EXAMPLE
Update-PodeLimitAccessRule -Name 'rule1' -Action 'Deny'
 
.EXAMPLE
Update-PodeLimitAccessRule -Name 'rule1' -StatusCode 404
 
.EXAMPLE
Update-PodeLimitAccessRule -Name 'rule1' -Action 'Allow' -StatusCode 200
#>

function Update-PodeLimitAccessRule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [ValidateSet('Allow', 'Deny')]
        [string]
        $Action = $null,

        [Parameter()]
        [int]
        $StatusCode = -1
    )

    $rule = $PodeContext.Server.Limits.Access.Rules[$Name]
    if (!$rule) {
        # An access limit rule with the name '$($Name)' does not exist
        throw ($PodeLocale.accessLimitRuleDoesNotExistExceptionMessage -f $Name)
    }

    if (![string]::IsNullOrWhiteSpace($Action)) {
        $rule.Action = $Action
    }

    if ($StatusCode -gt 0) {
        $rule.StatusCode = $StatusCode
    }

    # reset the flag if we have any allow rules
    $PodeContext.Server.Limits.Access.HaveAllowRules = ($PodeContext.Server.Limits.Access.Rules.Value |
            Where-Object { $_.Action -eq 'Allow' } |
            Measure-Object).Count -gt 0
}

<#
.SYNOPSIS
Removes an access rule.
 
.DESCRIPTION
Removes an access rule.
 
.PARAMETER Name
The name of the access rule.
 
.EXAMPLE
Remove-PodeLimitAccessRule -Name 'rule1'
#>

function Remove-PodeLimitAccessRule {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Name
    )

    # remove the rule
    $null = $PodeContext.Server.Limits.Access.Rules.Remove($Name)
    $PodeContext.Server.Limits.Access.RulesAltered = $true

    # reset the flag if we have any allow rules
    $PodeContext.Server.Limits.Access.HaveAllowRules = ($PodeContext.Server.Limits.Access.Rules.Value |
            Where-Object { $_.Action -eq 'Allow' } |
            Measure-Object).Count -gt 0
}

<#
.SYNOPSIS
Tests if an access rule exists.
 
.DESCRIPTION
Tests if an access rule exists.
 
.PARAMETER Name
The name of the access rule.
 
.EXAMPLE
Test-PodeLimitAccessRule -Name 'rule1'
 
.OUTPUTS
A boolean indicating if the access rule exists.
#>

function Test-PodeLimitAccessRule {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter()]
        [string]
        $Name
    )

    return $PodeContext.Server.Limits.Access.Rules.Contains($Name)
}

<#
.SYNOPSIS
Gets an access rule by name.
 
.DESCRIPTION
Gets an access rule by name.
 
.PARAMETER Name
The name(s) of the access rule.
 
.EXAMPLE
$rules = Get-PodeLimitAccessRule -Name 'rule1'
 
.EXAMPLE
$rules = Get-PodeLimitAccessRule -Name 'rule1', 'rule2'
 
.EXAMPLE
$rules = Get-PodeLimitAccessRule
 
.OUTPUTS
A hashtable array containing the access rule(s).
#>

function Get-PodeLimitAccessRule {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    if ($Name) {
        return $Name | ForEach-Object { $PodeContext.Server.Limits.Access.Rules[$_] }
    }

    return $PodeContext.Server.Limits.Access.Rules.Values
}

<#
.SYNOPSIS
Creates a new Limit IP component.
 
.DESCRIPTION
Creates a new Limit IP component. This supports the WebEvent, SmtpEvent, and TcpEvent IPs.
 
.PARAMETER IP
The IP address(es) to check. Supports raw IPs, subnets, local, and any.
 
.PARAMETER Location
Where to get the IP from: RemoteAddress or XForwardedFor. (Default: RemoteAddress)
 
.PARAMETER XForwardedForType
If the Location is XForwardedFor, which IP in the X-Forwarded-For header to use: Leftmost, Rightmost, or All. (Default: Leftmost)
If Leftmost, the first IP in the X-Forwarded-For header will be used.
If Rightmost, the last IP in the X-Forwarded-For header will be used.
If All, all IPs in the X-Forwarded-For header will be used - at least one must match.
 
.PARAMETER Group
If supplied, IPs in a subnet will be treated as a single entity.
 
.EXAMPLE
New-PodeLimitIPComponent
 
.EXAMPLE
New-PodeLimitIPComponent -IP '127.0.0.1'
 
.EXAMPLE
New-PodeLimitIPComponent -IP '10.0.0.0/24'
 
.EXAMPLE
New-PodeLimitIPComponent -IP 'localhost'
 
.EXAMPLE
New-PodeLimitIPComponent -IP 'all'
 
.EXAMPLE
New-PodeLimitIPComponent -IP '192.0.1.0/16' -Group
 
.EXAMPLE
New-PodeLimitIPComponent -IP '10.0.0.1' -Location XForwardedFor
 
.EXAMPLE
New-PodeLimitIPComponent -IP '192.0.1.0/16' -Group -Location XForwardedFor -XForwardedForType Rightmost
 
.OUTPUTS
A hashtable containing the options and scriptblock for the IP component.
The scriptblock will return the IP - or subnet for grouped - if found, or null if not.
#>

function New-PodeLimitIPComponent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [string[]]
        $IP,

        [Parameter()]
        [ValidateSet('RemoteAddress', 'XForwardedFor')]
        [string]
        $Location = 'RemoteAddress',

        [Parameter()]
        [ValidateSet('Leftmost', 'Rightmost', 'All')]
        [string]
        $XForwardedForType = 'Leftmost',

        [switch]
        $Group
    )

    # map of ip/subnet details
    $ipDetails = [ordered]@{
        Raw     = @{}
        Subnets = [ordered]@{}
        Any     = (Test-PodeIsEmpty -Value $IP)
        Local   = $false
    }

    # loop through each IP to parse details
    foreach ($_ip in $IP) {
        # is the ip valid?
        if (!(Test-PodeIPAddressLocal -IP $_ip) -and !(Test-PodeIPAddress -IP $_ip -IPOnly)) {
            # The IP address supplied is invalid: {0}
            throw ($PodeLocale.invalidIpAddressExceptionMessage -f $_ip)
        }

        # for any, just flag as any and continue
        if ([string]::IsNullOrWhiteSpace($_ip) -or (Test-PodeIPAddressAny -IP $_ip)) {
            $ipDetails.Any = $true
            continue
        }

        # for local, just flag as local and continue
        if (Test-PodeIPAddressLocal -IP $_ip) {
            $ipDetails.Local = $true
            continue
        }

        # for subnet, parse the subnet details
        if (Test-PodeIPAddressIsSubnetMask -IP $_ip) {
            $subnetRange = Get-PodeSubnetRange -SubnetMask $_ip
            $lowerDetails = Get-PodeIPAddress -IP $subnetRange.Lower
            $upperDetails = Get-PodeIPAddress -IP $subnetRange.Upper

            $ipDetails.Subnets[$_ip] = @{
                Family = $lowerDetails.Family
                Lower  = $lowerDetails.GetAddressBytes()
                Upper  = $upperDetails.GetAddressBytes()
            }
            continue
        }

        # for raw IP, just parse the IP details
        $details = Get-PodeIPAddress -IP $_ip
        $ipDetails.Raw[$_ip] = @{
            Family = $details.Family
        }
    }

    # pass back the IP component
    return @{
        Options     = @{
            IP                = $ipDetails
            Location          = $Location.ToLowerInvariant()
            XForwardedForType = $XForwardedForType.ToLowerInvariant()
            Group             = $Group.IsPresent
        }
        ScriptBlock = {
            param($options)

            # current request ip - for webevent, smtpevent, or tcpevent
            # for webevent, we can get the ip from the remote address or x-forwarded-for
            $ipAddresses = $null

            if ($WebEvent) {
                switch ($options.Location) {
                    'remoteaddress' {
                        $ipAddresses = @($WebEvent.Request.RemoteEndPoint.Address)
                    }
                    'xforwardedfor' {
                        $xForwardedFor = $WebEvent.Request.Headers['X-Forwarded-For']
                        if ([string]::IsNullOrEmpty($xForwardedFor)) {
                            return $null
                        }

                        $xffIps = $xForwardedFor.Split(',')
                        switch ($options.XForwardedForType) {
                            'leftmost' {
                                $ipAddresses = @(Get-PodeIPAddress -IP $xffIps[0].Trim() -ContainsPort)
                            }
                            'rightmost' {
                                $ipAddresses = @(Get-PodeIPAddress -IP $xffIps[-1].Trim() -ContainsPort)
                            }
                            'all' {
                                $ipAddresses = @(foreach ($ip in $xffIps) { Get-PodeIPAddress -IP $ip.Trim() -ContainsPort })
                            }
                        }
                    }
                }
            }
            elseif ($SmtpEvent) {
                $ipAddresses = @($SmtpEvent.Request.RemoteEndPoint.Address)
            }
            elseif ($TcpEvent) {
                $ipAddresses = @($TcpEvent.Request.RemoteEndPoint.Address)
            }

            # if we have no ip addresses, then return null
            if (($null -eq $ipAddresses) -or ($ipAddresses.Length -eq 0)) {
                return $null
            }

            # loop through each ip address
            for ($i = $ipAddresses.Length - 1; $i -ge 0; $i--) {
                $ip = $ipAddresses[$i]

                $ipDetails = @{
                    Value  = $ip.IPAddressToString
                    Family = $ip.AddressFamily
                    Bytes  = $ip.GetAddressBytes()
                }

                # is the ip in the Raw list?
                if ($options.IP.Raw.ContainsKey($ipDetails.Value)) {
                    return $ipDetails.Value
                }

                # is the ip in the Subnets list?
                foreach ($subnet in $options.IP.Subnets.Keys) {
                    $subnetDetails = $options.IP.Subnets[$subnet]
                    if ($subnetDetails.Family -ne $ipDetails.Family) {
                        continue
                    }

                    # if the ip is in the subnet range, then return the subnet
                    if (Test-PodeIPAddressInSubnet -IP $ipDetails.Bytes -Lower $subnetDetails.Lower -Upper $subnetDetails.Upper) {
                        if ($options.Group) {
                            return $subnet
                        }

                        return $ipDetails.Value
                    }
                }

                # is the ip local?
                if ($options.IP.Local) {
                    if ([System.Net.IPAddress]::IsLoopback($ip)) {
                        if ($options.Group) {
                            return 'local'
                        }

                        return $ipDetails.Value
                    }
                }

                # is any allowed?
                if ($options.IP.Any -and ($i -eq 0)) {
                    if ($options.Group) {
                        return '*'
                    }

                    return $ipDetails.Value
                }
            }

            # ip didn't match any rules
            return $null
        }
    }
}

<#
.SYNOPSIS
Creates a new Limit Route component.
 
.DESCRIPTION
Creates a new Limit Route component. This supports the WebEvent routes.
 
.PARAMETER Path
The route path(s) to check. This can be a full path, or a wildcard path.
 
.PARAMETER Group
If supplied, the routes will be grouped by any wildcard, ignoring the full path.
For example, any routes matching "/api/*" will be grouped as "/api/*", and not "/api/test" or "/api/test/hello".
 
.EXAMPLE
New-PodeLimitRouteComponent -Path '/downloads'
 
.EXAMPLE
New-PodeLimitRouteComponent -Path '/downloads', '/api/*'
 
.EXAMPLE
New-PodeLimitRouteComponent -Path '/api/*' -Group
 
.OUTPUTS
A hashtable containing the options and scriptblock for the route component.
The scriptblock will return the route path if found, or null if not.
#>

function New-PodeLimitRouteComponent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [string[]]
        $Path,

        [switch]
        $Group
    )

    # convert paths into a hashtable for easier lookup
    $htPath = @{}
    foreach ($p in $Path) {
        $htPath[(ConvertTo-PodeRouteRegex -Path $p)] = $true
    }

    # pass back the route component
    return @{
        Options     = @{
            Path  = $htPath
            Group = $Group.IsPresent
            All   = (Test-PodeIsEmpty -Value $Path)
        }
        ScriptBlock = {
            param($options)

            # current request path
            $path = $WebEvent.Path
            if ([string]::IsNullOrEmpty($path)) {
                return $null
            }

            # if the list is empty, or the list contains the path, then return the path
            if ($options.All -or $options.Path.ContainsKey($path)) {
                return $path
            }

            # check if the path is a wildcard
            foreach ($key in $options.Path.Keys) {
                if ($path -imatch "^$($key)$") {
                    if ($options.Group) {
                        return $key
                    }

                    return $path
                }
            }

            # return null
            return $null
        }
    }
}

<#
.SYNOPSIS
Creates a new Limit Endpoint component.
 
.DESCRIPTION
Creates a new Limit Endpoint component. This supports the WebEvent, SmtpEvent, and TcpEvent endpoints.
 
.PARAMETER Name
The endpoint name(s) to check.
 
.EXAMPLE
New-PodeLimitEndpointComponent
 
.EXAMPLE
New-PodeLimitEndpointComponent -Name 'api'
 
.OUTPUTS
A hashtable containing the options and scriptblock for the endpoint component.
The scriptblock will return the endpoint name if found, or null if not.
#>

function New-PodeLimitEndpointComponent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [string[]]
        $Name
    )

    # convert endpoint names into a hashtable for easier lookup
    $htName = @{}
    foreach ($e in $Name) {
        $htName[$e] = $true
    }

    # pass back the endpoint component
    return @{
        Options     = @{
            EndpointName = $htName
            All          = (Test-PodeIsEmpty -Value $Name)
        }
        ScriptBlock = {
            param($options)

            # current request endpoint name - from webevent, smtpevent, or tcpevent
            $endpointName = $null
            if ($WebEvent) {
                $endpointName = $WebEvent.Endpoint.Name
            }
            elseif ($SmtpEvent) {
                $endpointName = $SmtpEvent.Endpoint.Name
            }
            elseif ($TcpEvent) {
                $endpointName = $TcpEvent.Endpoint.Name
            }

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

            # if the list is empty, or the list contains the endpoint name, then return the endpoint name
            if ($options.All -or $options.EndpointName.ContainsKey($endpointName)) {
                return $endpointName
            }

            # return null
            return $null
        }
    }
}

<#
.SYNOPSIS
Creates a new Limit HTTP Method component.
 
.DESCRIPTION
Creates a new Limit HTTP Method component. This supports the WebEvent methods.
 
.PARAMETER Method
The HTTP method(s) to check.
 
.EXAMPLE
New-PodeLimitMethodComponent
 
.EXAMPLE
New-PodeLimitMethodComponent -Method 'Get'
 
.EXAMPLE
New-PodeLimitMethodComponent -Method 'Get', 'Post'
 
.OUTPUTS
A hashtable containing the options and scriptblock for the method component.
The scriptblock will return the method if found, or null if not.
#>

function New-PodeLimitMethodComponent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter()]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [string[]]
        $Method
    )

    # convert methods into a hashtable for easier lookup
    $htMethod = @{}
    foreach ($m in $Method) {
        $htMethod[$m] = $true
    }

    # pass back the method component
    return @{
        Options     = @{
            Method = $htMethod
            All    = (Test-PodeIsEmpty -Value $Method)
        }
        ScriptBlock = {
            param($options)

            # current request method
            $method = $WebEvent.Method
            if ([string]::IsNullOrEmpty($method)) {
                return $null
            }

            # if the list is empty, or the list contains the method, then return the method
            if ($options.All -or $options.Method.ContainsKey($method)) {
                return $method
            }

            # return null
            return $null
        }
    }
}

<#
.SYNOPSIS
Creates a new Limit Header component.
 
.DESCRIPTION
Creates a new Limit Header component. This support WebEvent and SmtpEvent headers.
 
.PARAMETER Name
The name of the header(s) to check.
 
.PARAMETER Value
The value of the header(s) to check.
 
.PARAMETER Group
If supplied, the headers will be grouped by name, ignoring the value.
For example, any headers matching "X-AuthToken" will be grouped as "X-AuthToken", and not "X-AuthToken=123".
 
.EXAMPLE
New-PodeLimitHeaderComponent -Name 'X-AuthToken'
 
.EXAMPLE
New-PodeLimitHeaderComponent -Name 'X-AuthToken', 'X-AuthKey'
 
.EXAMPLE
New-PodeLimitHeaderComponent -Name 'X-AuthToken' -Value '12345'
 
.EXAMPLE
New-PodeLimitHeaderComponent -Name 'X-AuthToken' -Group
 
.OUTPUTS
A hashtable containing the options and scriptblock for the header component.
The scriptblock will return the header name and value if found, or just the name if Group is supplied.
#>

function New-PodeLimitHeaderComponent {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name,

        [Parameter()]
        [string[]]
        $Value,

        [switch]
        $Group
    )

    # convert header names into a hashtable for easier lookup
    $htHeaderName = @{}
    foreach ($h in $Name) {
        $htHeaderName[$h] = $true
    }

    # convert header values into a hashtable for easier lookup
    $htHeaderValue = @{}
    foreach ($h in $Value) {
        $htHeaderValue[$h] = $true
    }

    # pass back the header component
    return @{
        Options     = @{
            HeaderNames  = $htHeaderName
            HeaderValues = $htHeaderValue
            Group        = $Group.IsPresent
            AllValues    = (Test-PodeIsEmpty -Value $Value)
        }
        ScriptBlock = {
            param($options)

            # current request headers - from webevent or smtpevent
            $reqHeaders = @{}
            if ($WebEvent) {
                $reqHeaders = $WebEvent.Request.Headers
            }
            elseif ($SmtpEvent) {
                $reqHeaders = $SmtpEvent.Request.Headers
            }

            if ($reqHeaders.Count -eq 0) {
                return $null
            }

            # loop through each specified header
            foreach ($header in $options.HeaderNames.Keys) {
                # skip if the header is not in the request
                if (!$reqHeaders.ContainsKey($header)) {
                    continue
                }

                # are we checking any specific values - if not, return name/value or just name
                if ($options.AllValues) {
                    if ($options.Group) {
                        return $header
                    }
                    return "$($header)=$($reqHeaders[$header])"
                }

                # otherwise, check if the header value is in the list
                if ($options.HeaderValues.ContainsKey($reqHeaders[$header])) {
                    return "$($header)=$($reqHeaders[$header])"
                }
            }

            # return null
            return $null
        }
    }
}