Private/Limit.ps1

function Get-PodeLimitRateTimerName {
    return '__pode_rate_limit_housekeeper__'
}

function Test-PodeLimitRateTimer {
    return Test-PodeTimer -Name (Get-PodeLimitRateTimerName)
}

function Add-PodeLimitRateTimer {
    if (Test-PodeLimitRateTimer) {
        return
    }

    Add-PodeTimer -Name (Get-PodeLimitRateTimerName) -Interval 30 -ScriptBlock {
        try {
            $now = [DateTime]::UtcNow
            $value = $null

            foreach ($rule in $PodeContext.Server.Limits.Rate.Rules.Values) {
                if ($rule.Active.Count -eq 0) {
                    continue
                }

                foreach ($key in $rule.Active.Keys.Clone()) {
                    try {
                        $item = $rule.Active[$key]

                        if ($item.Timeout.AddSeconds(5) -lt $now) {
                            $rule.Active.TryRemove($key, [ref]$value)
                        }
                    }
                    catch {
                        $_ | Write-PodeErrorLog
                    }
                }
            }
        }
        catch {
            $_ | Write-PodeErrorLog
        }
    }
}

function Remove-PodeLimitRateTimer {
    if (($PodeContext.Server.Limits.Rate.Rules.Count -gt 0) -or !(Test-PodeLimitRateTimer)) {
        return
    }

    Remove-PodeTimer -Name (Get-PodeLimitRateTimerName)
}

function Invoke-PodeLimitAccessRuleRequest {
    # are there any rules?
    if ($PodeContext.Server.Limits.Access.Rules.Count -eq 0) {
        return $null
    }

    # generate the rule order, if rules have been altered
    if ($PodeContext.Server.Limits.Access.RulesAltered) {
        $PodeContext.Server.Limits.Access.RulesOrder = $PodeContext.Server.Limits.Access.Rules.Values |
            Sort-Object -Property { $_.Priority } -Descending |
            Select-Object -ExpandProperty Name
        $PodeContext.Server.Limits.Access.RulesAltered = $false
    }

    # loop through each access rule
    foreach ($ruleName in $PodeContext.Server.Limits.Access.RulesOrder) {
        $rule = $PodeContext.Server.Limits.Access.Rules[$ruleName]

        # loop through each component of the rule, checking if the request matches
        $skip = $false
        foreach ($component in $rule.Components) {
            $result = Invoke-PodeScriptBlock -ScriptBlock $component.ScriptBlock -Arguments $component.Options -Return

            # if result is null/empty then move to the next rule
            if ([string]::IsNullOrEmpty($result)) {
                $skip = $true
                break
            }
        }

        # if we skipped the rule, then move to the next one
        if ($skip) {
            continue
        }

        # if we get here, then the request matches all the components - so allow or deny the request
        if ($rule.Action -ieq 'Deny') {
            return @{
                StatusCode = $rule.StatusCode
            }
        }

        return $null
    }

    # if we get here, then the request didn't match any rules
    # if we have any allow rules, then deny the request
    if ($PodeContext.Server.Limits.Access.HaveAllowRules) {
        return @{
            StatusCode = 403
        }
    }

    return $null
}

function Test-PodeLimitAccessRuleRequest {
    $result = Invoke-PodeLimitAccessRuleRequest
    return ($null -eq $result)
}

function Invoke-PodeLimitRateRuleRequest {
    # are there any rate rules?
    if ($PodeContext.Server.Limits.Rate.Rules.Count -eq 0) {
        return $null
    }

    # generate the rule order, if rules have been altered
    if ($PodeContext.Server.Limits.Rate.RulesAltered) {
        $PodeContext.Server.Limits.Rate.RulesOrder = $PodeContext.Server.Limits.Rate.Rules.Values |
            Sort-Object -Property { $_.Priority } -Descending |
            Select-Object -ExpandProperty Name
        $PodeContext.Server.Limits.Rate.RulesAltered = $false
    }

    # loop through each rate rule
    foreach ($ruleName in $PodeContext.Server.Limits.Rate.RulesOrder) {
        $rule = $PodeContext.Server.Limits.Rate.Rules[$ruleName]
        $ruleKey = @()
        $now = [DateTime]::UtcNow

        # loop through each component of the rule
        $skip = $false
        foreach ($component in $rule.Components) {
            $result = Invoke-PodeScriptBlock -ScriptBlock $component.ScriptBlock -Arguments $component.Options -Return

            # if result is null/empty then move to the next rule
            if ([string]::IsNullOrEmpty($result)) {
                $skip = $true
                break
            }

            # add the result to the rule key
            $ruleKey += $result
        }

        # if we skipped the rule, then move to the next one
        if ($skip) {
            continue
        }

        # concatenate the rule key
        $ruleKey = $ruleKey -join '|'

        # if it's not in the active dictionary, or the timeout has passed, then add/reset it
        if (!$rule.Active.ContainsKey($ruleKey) -or ($rule.Active[$ruleKey].Timeout -le $now)) {
            $rule.Active[$ruleKey] = @{
                Timeout = $now.AddMilliseconds($rule.Duration)
                Counter = 0
            }
        }

        # increment the counter
        $rule.Active[$ruleKey].Counter++

        # if the key is in the active dictionary, then check the timeout/counter and set the status code if needed
        if ($rule.Active.ContainsKey($ruleKey) -and
            ($rule.Active[$ruleKey].Timeout -gt $now) -and
            ($rule.Active[$ruleKey].Counter -gt $rule.Limit)) {
            return @{
                RetryAfter = [int][System.Math]::Ceiling(($rule.Active[$ruleKey].Timeout - $now).TotalSeconds)
                StatusCode = $rule.StatusCode
            }
        }
    }

    # request is allowed
    return $null
}

function Test-PodeLimitRateRuleRequest {
    $result = Invoke-PodeLimitRateRuleRequest
    return ($null -eq $result)
}