Public/Middleware/Add-KrForwardHeader.ps1

<#
.SYNOPSIS
    Adds Forwarded Headers middleware to a Kestrun server.
.DESCRIPTION
    This cmdlet adds and configures the ASP.NET Core Forwarded Headers middleware
    for a Kestrun server. This middleware processes proxy-related headers such as
    X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Prefix to
    update the request's Scheme, Host, and Remote IP address accordingly.
    This is essential when hosting behind reverse proxies or load balancers that
    modify these headers.
.PARAMETER Server
    The Kestrun server instance to which the Forwarded Headers middleware will be added.
    If not specified, the cmdlet will attempt to resolve the current server context.
.PARAMETER Options
    An instance of Microsoft.AspNetCore.Builder.ForwardedHeadersOptions to configure
    the middleware. This allows for full customization of the middleware behavior.
.PARAMETER XForwardedFor
    Switch to enable processing of the X-Forwarded-For header.
.PARAMETER XForwardedProto
    Switch to enable processing of the X-Forwarded-Proto header.
.PARAMETER XForwardedHost
    Switch to enable processing of the X-Forwarded-Host header.
.PARAMETER XForwardedPrefix
    Switch to enable processing of the X-Forwarded-Prefix header.
.PARAMETER All
    Switch to enable processing of all supported forwarded headers.
.PARAMETER ForwardLimit
    Specifies the maximum number of entries to read from the forwarded headers.
    Default is 1.
.PARAMETER KnownNetworks
    An array of IPNetwork objects representing known networks from which forwarded
    headers will be accepted.
.PARAMETER KnownProxies
    An array of IPAddress objects representing known proxy servers from which
    forwarded headers will be accepted.
.PARAMETER ForwardedForHeaderName
    Custom header name for X-Forwarded-For.
.PARAMETER ForwardedProtoHeaderName
    Custom header name for X-Forwarded-Proto.
.PARAMETER ForwardedHostHeaderName
    Custom header name for X-Forwarded-Host.
.PARAMETER ForwardedPrefixHeaderName
    Custom header name for X-Forwarded-Prefix.
.PARAMETER OriginalForHeaderName
    Custom header name for Original-For.
.PARAMETER OriginalProtoHeaderName
    Custom header name for Original-Proto.
.PARAMETER OriginalHostHeaderName
    Custom header name for Original-Host.
.PARAMETER OriginalPrefixHeaderName
    Custom header name for Original-Prefix.
.PARAMETER RequireHeaderSymmetry
    Switch to require that all enabled forwarded headers are present in the request.
.PARAMETER PassThru
    If specified, the cmdlet returns the Kestrun server instance after adding
    the middleware.
.EXAMPLE
    Add-KrForwardedHeader -XForwardedFor -XForwardedProto -KnownProxies $proxyIps
    Adds Forwarded Headers middleware to the current Kestrun server, enabling
    processing of X-Forwarded-For and X-Forwarded-Proto headers, and
    trusting the specified proxy IP addresses.
.NOTES
    This cmdlet is part of the Kestrun PowerShell module.
#>

function Add-KrForwardedHeader {
    [KestrunRuntimeApi('Definition')]
    [CmdletBinding(DefaultParameterSetName = 'Items')]
    [OutputType([Kestrun.Hosting.KestrunHost])]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [Kestrun.Hosting.KestrunHost]$Server,

        # --- ParameterSet: Options (verbatim pass-through) ---
        [Parameter(Mandatory = $true, ParameterSetName = 'Options')]
        [Microsoft.AspNetCore.Builder.ForwardedHeadersOptions]$Options,

        # --- ParameterSet: Items (switches compose the enum) ---
        [Parameter(ParameterSetName = 'Items')]
        [switch]$XForwardedFor,
        [Parameter(ParameterSetName = 'Items')]
        [switch]$XForwardedProto,
        [Parameter(ParameterSetName = 'Items')]
        [switch]$XForwardedHost,
        [Parameter(ParameterSetName = 'Items')]
        [switch]$XForwardedPrefix,
        [Parameter(ParameterSetName = 'Items')]
        [switch]$All,  # convenience

        [Parameter(ParameterSetName = 'Items')]
        [int]$ForwardLimit = 1,

        [Parameter(ParameterSetName = 'Items')]
        [string[]]$KnownNetworks,
        [Parameter(ParameterSetName = 'Items')]
        [string[]]$KnownProxies,

        # Optional header name overrides (default to ForwardedHeadersDefaults)
        [Parameter(ParameterSetName = 'Items')]
        [string]$ForwardedForHeaderName,
        [Parameter(ParameterSetName = 'Items')]
        [string]$ForwardedProtoHeaderName,
        [Parameter(ParameterSetName = 'Items')]
        [string]$ForwardedHostHeaderName,
        [Parameter(ParameterSetName = 'Items')]
        [string]$ForwardedPrefixHeaderName,
        [Parameter(ParameterSetName = 'Items')]
        [string]$OriginalForHeaderName,
        [Parameter(ParameterSetName = 'Items')]
        [string]$OriginalProtoHeaderName,
        [Parameter(ParameterSetName = 'Items')]
        [string]$OriginalHostHeaderName,
        [Parameter(ParameterSetName = 'Items')]
        [string]$OriginalPrefixHeaderName,

        [Parameter(ParameterSetName = 'Items')]
        [switch]$RequireHeaderSymmetry,

        [switch]$PassThru
    )
    begin {
        $Server = Resolve-KestrunServer -Server $Server
    }
    process {
        if ($PSCmdlet.ParameterSetName -eq 'Items') {
            # Compose ForwardedHeadersOptions from switches
            $fhEnum = [Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders]

            # Create options
            $Options = [Microsoft.AspNetCore.Builder.ForwardedHeadersOptions]::new()
            # Compose flags from switches
            if ($All) {
                $Options.ForwardedHeaders = [Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders]::All
            } else {
                $flags = $fhEnum::None
                if ($XForwardedFor) { $flags = $flags -bor $fhEnum::XForwardedFor }
                if ($XForwardedProto) { $flags = $flags -bor $fhEnum::XForwardedProto }
                if ($XForwardedHost) { $flags = $flags -bor $fhEnum::XForwardedHost }
                if ($XForwardedPrefix) { $flags = $flags -bor $fhEnum::XForwardedPrefix }
                if ($flags -eq $fhEnum::None) {
                    # Sensible default if user didn’t specify: For + Proto
                    $flags = $fhEnum::XForwardedFor -bor $fhEnum::XForwardedProto
                }
                $Options.ForwardedHeaders = $flags
            }

            # Forward limit
            $Options.ForwardLimit = $ForwardLimit

            # Known proxies
            if ($PSBoundParameters.ContainsKey('KnownProxies')) {
                $Options.KnownProxies.Clear()
                foreach ($p in $KnownProxies) {
                    $ip = if ($p -is [System.Net.IPAddress]) { $p } else { [System.Net.IPAddress]::Parse($p) }
                    [void]$Options.KnownProxies.Add($ip)
                }
            }
            # Known networks
            if ($PSBoundParameters.ContainsKey('KnownNetworks')) {
                $Options.KnownNetworks.Clear()
                foreach ($n in $KnownNetworks) {
                    # Allow "10.0.0.0/24" or IPNetwork objects directly
                    $net = if ($n -is [System.Net.IPNetwork]) { $n } else { [System.Net.IPNetwork]::Parse($n) }
                    [void]$Options.KnownNetworks.Add($net)
                }
            }
            # Custom header names
            if ($PSBoundParameters.ContainsKey('ForwardedForHeaderName')) {
                $Options.ForwardedForHeaderName = $ForwardedForHeaderName
            }
            if ($PSBoundParameters.ContainsKey('ForwardedProtoHeaderName')) {
                $Options.ForwardedProtoHeaderName = $ForwardedProtoHeaderName
            }
            if ($PSBoundParameters.ContainsKey('ForwardedHostHeaderName')) {
                $Options.ForwardedHostHeaderName = $ForwardedHostHeaderName
            }
            if ($PSBoundParameters.ContainsKey('ForwardedPrefixHeaderName')) {
                $Options.ForwardedPrefixHeaderName = $ForwardedPrefixHeaderName
            }
            if ($PSBoundParameters.ContainsKey('OriginalForHeaderName')) {
                $Options.OriginalForHeaderName = $OriginalForHeaderName
            }
            if ($PSBoundParameters.ContainsKey('OriginalProtoHeaderName')) {
                $Options.OriginalProtoHeaderName = $OriginalProtoHeaderName
            }
            if ($PSBoundParameters.ContainsKey('OriginalHostHeaderName')) {
                $Options.OriginalHostHeaderName = $OriginalHostHeaderName
            }
            if ($PSBoundParameters.ContainsKey('OriginalPrefixHeaderName')) {
                $Options.OriginalPrefixHeaderName = $OriginalPrefixHeaderName
            }

            # Symmetry
            $Options.RequireHeaderSymmetry = $RequireHeaderSymmetry.IsPresent
        }

        # Attach to server so host.Apply() can call app.UseForwardedHeaders($Options)
        $Server.ForwardedHeaderOptions = $Options

        if ($PassThru) { return $Server }
    }
}