Private/Routes.ps1
function Test-PodeRouteFromRequest { param( [Parameter(Mandatory = $true)] [ValidateSet('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', 'SIGNAL', '*')] [string] $Method, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [string] $EndpointName, [switch] $CheckWildMethod ) $route = Find-PodeRoute -Method $Method -Path $Path -EndpointName $EndpointName -CheckWildMethod:$CheckWildMethod return ($null -ne $route) } function Find-PodeRoute { param( [Parameter(Mandatory = $true)] [ValidateSet('CONNECT', 'DELETE', 'GET', 'HEAD', 'MERGE', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE', 'STATIC', 'SIGNAL', '*')] [string] $Method, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [string] $EndpointName, [switch] $CheckWildMethod ) # first, if supplied, check the wildcard method if ($CheckWildMethod -and ($PodeContext.Server.Routes['*'].Count -ne 0)) { $found = Find-PodeRoute -Method '*' -Path $Path -EndpointName $EndpointName if ($null -ne $found) { return $found } } # first ensure we have the method $_method = $PodeContext.Server.Routes[$Method] if ($null -eq $_method) { return $null } # is this a static route? $isStatic = ($Method -ieq 'static') # if we have a perfect match for the route, return it if the protocol is right if (!$isStatic) { $found = Get-PodeRouteByUrl -Routes $_method[$Path] -EndpointName $EndpointName if ($null -ne $found) { return $found } } # otherwise, match the path to routes on regex (first match only) $paths = @($_method.Keys) if ($isStatic) { [array]::Sort($paths) [array]::Reverse($paths) } $valid = @(foreach ($key in $paths) { if ($Path -imatch "^$($key)$") { $key break } })[0] if ($null -eq $valid) { return $null } # is the route valid for any protocols/endpoints? $found = Get-PodeRouteByUrl -Routes $_method[$valid] -EndpointName $EndpointName if ($null -eq $found) { return $null } return $found } function Find-PodePublicRoute { param( [Parameter(Mandatory = $true)] [string] $Path ) $source = $null $publicPath = $PodeContext.Server.InbuiltDrives['public'] # reutrn null if there is no public directory if ([string]::IsNullOrWhiteSpace($publicPath)) { return $source } # use the public static directory (but only if path is a file, and a public dir is present) if (Test-PodePathIsFile $Path) { $source = [System.IO.Path]::Combine($publicPath, $Path.TrimStart('/', '\')) if (!(Test-PodePath -Path $source -NoStatus)) { $source = $null } } # return the route details return $source } <# .SYNOPSIS Finds a static route for a given path in a Pode web server application, with optional checks for public routes. .DESCRIPTION This function searches for a static route matching the specified path within a Pode web server application. It attempts to resolve the route to a physical file or directory and supports additional checks for public routes as a fallback option. The function returns a hashtable with route details, including whether the route is for a downloadable file, if it's cacheable, and whether it redirects to a default document. .PARAMETER Path The URL path for which to find a static route. This parameter is mandatory. .PARAMETER EndpointName Optional. Specifies the name of the endpoint to which the route may belong. If not provided, the function searches across all endpoints. .PARAMETER CheckPublic A switch parameter. If specified, the function also checks for the route in public routes as a fallback option. .EXAMPLE $staticRoute = Find-PodeStaticRoute -Path '/images/logo.png' -CheckPublic Searches for a static route for '/images/logo.png'. If not found, checks if a public route exists for the same path. .EXAMPLE $staticRoute = Find-PodeStaticRoute -Path '/css/style.css' -EndpointName 'WebUI' Searches for a static route for '/css/style.css' specifically within the 'WebUI' endpoint, without checking public routes. .OUTPUTS Hashtable. Returns a hashtable containing the route details, such as the source path, download flag, cacheability, and redirect status. .NOTES This is an internal function and may change in future releases of Pode. #> function Find-PodeStaticRoute { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $EndpointName, [switch] $CheckPublic ) # attempt to get a static route for the path $found = Find-PodeRoute -Method 'static' -Path $Path -EndpointName $EndpointName $download = ([bool]$found.Download) $source = $null $isDefault = $false $redirectToDefault = ([bool]$found.RedirectToDefault) # if we have a defined static route, use that if ($null -ne $found) { # see if we have a file $file = [string]::Empty if ($found.KleeneStar) { $matchingPath = "$($found.Path -ireplace '.\*', '.+?')$" } else { $matchingPath = "$($found.Path)$" } if ($Path -imatch $matchingPath) { $file = (Protect-PodeValue -Value $Matches['file'] -Default ([string]::Empty)) } $fileInfo = Get-Item -Path ([System.IO.Path]::Combine($found.Source, $file)) -Force -ErrorAction Ignore #if $file doesn't exist return $null if ($null -eq $fileInfo) { return $null } # if there's no file, we need to check defaults if (!$found.Download -and $fileInfo.PSIsContainer -and (Get-PodeCount @($found.Defaults)) -gt 0) { foreach ($def in $found.Defaults) { $fileInfoDefaultFile = Get-Item -Path ([System.IO.Path]::Combine($fileInfo.FullName, $def)) -Force -ErrorAction Ignore if ($fileInfoDefaultFile) { $file = $fileInfoDefaultFile.FullName $isDefault = $true break } } } $source = [System.IO.Path]::Combine($found.Source, $file) } # check public, if flagged if ($CheckPublic -and !(Test-PodePath -Path $source -NoStatus)) { $source = Find-PodePublicRoute -Path $Path $download = $false $found = $null $isDefault = $false $redirectToDefault = $false } # return nothing if no source if ([string]::IsNullOrWhiteSpace($source)) { return $null } # return the route details if ($redirectToDefault -and $isDefault) { $redirectToDefault = $true } else { $redirectToDefault = $false } return @{ Content = @{ Source = $source IsDownload = $download IsCachable = (Test-PodeRouteValidForCaching -Path $Path) RedirectToDefault = $redirectToDefault } Route = $found } } function Find-PodeSignalRoute { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $EndpointName ) # attempt to get a signal route for the path return (Find-PodeRoute -Method 'signal' -Path $Path -EndpointName $EndpointName) } function Test-PodeRouteValidForCaching { param( [Parameter(Mandatory = $true)] [string] $Path ) # 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 ($Path -imatch $config.Exclude)) { $caching = $false } if (($null -ne $config.Include) -and ($Path -inotmatch $config.Include)) { $caching = $false } } return $caching } <# .SYNOPSIS Finds and returns a route from an array of routes based on an endpoint name and/or path. .DESCRIPTION This function iterates over an array of route definitions to locate a specific route that matches the provided endpoint name and path. It supports scenarios where only one of the parameters is provided or both. If no matching route is found, or if the routes array is empty or null, the function returns $null. .PARAMETER Routes An array of hashtable objects, each representing a route with potentially defined properties like Root and Endpoint.Name. .PARAMETER EndpointName The name of the endpoint to search for within the route definitions. This parameter is optional. .EXAMPLE $routes = @( @{ Root = '/api'; Endpoint = @{ Name = 'GetData' } }, @{ Root = '/home'; Endpoint = @{ Name = 'Index' } } ) Get-PodeRouteByUrl -Routes $routes -EndpointName 'GetData' Returns the route for the '/api' endpoint named 'GetData'. .EXAMPLE $routes = @( @{ Root = '/api'; Endpoint = @{ Name = 'GetData' } }, @{ Root = '/home'; Endpoint = @{ Name = 'Index' } } ) Get-PodeRouteByUrl -Routes $routes -Path '/api' Returns the route for the '/api' path, regardless of the endpoint name. .NOTES The function prioritizes matching both the endpoint name and path but can return a route based on either criterion if the other is unspecified. #> function Get-PodeRouteByUrl { param( [Parameter()] [hashtable[]] $Routes, [Parameter()] [string] $EndpointName ) # Return null immediately if routes are not defined or empty if (($null -eq $Routes) -or ($Routes.Length -eq 0)) { return $null } # Handle case when no specific endpoint name is provided if ([string]::IsNullOrWhiteSpace($EndpointName)) { foreach ($route in $Routes) { # Return the first route as a default if no path is specified return $route } } else { # Handle case when an endpoint name is provided foreach ($route in $Routes) { if ( $route.Endpoint.Name -ieq $EndpointName) { # Return the first route that matches the endpoint name as a default return $route } } } # Last resort check only route with no endpoint name foreach ($route in $Routes) { if ([string]::IsNullOrWhiteSpace($route.Endpoint.Name)) { # Return the first route that matches the endpoint name as a default return $route } } # Return null if no matching route is found return $null } <# .SYNOPSIS Updates a Pode route path to ensure proper formatting. .DESCRIPTION This function takes a Pode route path and ensures that it starts with a leading slash ('/') and follows the correct format for static routes. It also replaces '*' with '.*' for proper regex matching. .PARAMETER Path The Pode route path to update. .PARAMETER Static Indicates whether the route is a static route (default is false). .PARAMETER NoLeadingSlash Indicates whether the route should not have a leading slash (default is false). .OUTPUTS The updated Pode route path. .NOTES This is an internal function and may change in future releases of Pode. #> function Update-PodeRouteSlash { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] $Path, [switch] $Static, [switch] $NoLeadingSlash ) # ensure route starts with a '/' if (!$NoLeadingSlash -and !$Path.StartsWith('/')) { $Path = "/$($Path)" } if ($Static) { # ensure the static route ends with '/{0,1}.*' $Path = $Path.TrimEnd('/*') $Path = "$($Path)[/]{0,1}(?<file>*)" } # replace * with .* $Path = ($Path -ireplace '\*', '.*') return $Path } function Split-PodeRouteQuery { param( [Parameter(Mandatory = $true)] [string] $Path ) return ($Path -isplit '\?')[0] } function ConvertTo-PodeRouteRegex { param( [Parameter()] [string] $Path ) if ([string]::IsNullOrWhiteSpace($Path)) { return [string]::Empty } $Path = Protect-PodeValue -Value $Path -Default '/' $Path = Split-PodeRouteQuery -Path $Path $Path = Protect-PodeValue -Value $Path -Default '/' $Path = Update-PodeRouteSlash -Path $Path $Path = Resolve-PodePlaceholder -Path $Path return $Path } function Get-PodeStaticRouteDefault { if (!(Test-PodeIsEmpty $PodeContext.Server.Web.Static.Defaults)) { return @($PodeContext.Server.Web.Static.Defaults) } return @( 'index.html', 'index.htm', 'default.html', 'default.htm' ) } function Test-PodeRouteInternal { param( [Parameter(Mandatory = $true)] [string] $Method, [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $Protocol, [Parameter()] [string] $Address, [switch] $ThrowError ) # check the routes $found = $false $routes = @($PodeContext.Server.Routes[$Method][$Path]) foreach ($route in $routes) { if (($route.Endpoint.Protocol -ieq $Protocol) -and ($route.Endpoint.Address -ieq $Address)) { $found = $true break } } # skip if not found if (!$found) { return $false } # do we want to throw an error if found, or skip? if (!$ThrowError) { return $true } # throw error $_url = $Protocol if (![string]::IsNullOrEmpty($_url) -and ![string]::IsNullOrWhiteSpace($Address)) { $_url = "$($_url)://$($Address)" } elseif (![string]::IsNullOrWhiteSpace($Address)) { $_url = $Address } if ([string]::IsNullOrEmpty($_url)) { throw ($PodeLocale.methodPathAlreadyDefinedExceptionMessage -f $Method, $Path) #"[$($Method)] $($Path): Already defined" } throw ($PodeLocale.methodPathAlreadyDefinedForUrlExceptionMessage -f $Method, $Path, $_url) #"[$($Method)] $($Path): Already defined for $($_url)" } function Convert-PodeFunctionVerbToHttpMethod { param( [Parameter()] [string] $Verb ) # if empty, just return default switch ($Verb) { { $_ -iin @('Find', 'Format', 'Get', 'Join', 'Search', 'Select', 'Split', 'Measure', 'Ping', 'Test', 'Trace') } { 'GET' } { $_ -iin @('Set') } { 'PUT' } { $_ -iin @('Rename', 'Edit', 'Update') } { 'PATCH' } { $_ -iin @('Clear', 'Close', 'Exit', 'Hide', 'Remove', 'Undo', 'Dismount', 'Unpublish', 'Disable', 'Uninstall', 'Unregister') } { 'DELETE' } Default { 'POST' } } } <# .SYNOPSIS Finds and returns the appropriate transfer encoding for a given route path in a Pode server context. .DESCRIPTION This function determines the correct transfer encoding for a specified route path within a Pode web server. It checks if a transfer encoding is already specified and returns it; otherwise, it defaults to the server's default transfer encoding. The function searches the server's transfer encoding route settings for a pattern that matches the given path. If a match is found, the corresponding transfer encoding is returned. This is useful for dynamically setting response encodings based on specific route patterns. .PARAMETER Path The route path for which the transfer encoding is being determined. This parameter is mandatory. .PARAMETER TransferEncoding The current transfer encoding, if already determined. This is an optional parameter. If specified and not null or whitespace, this function returns the given value without further processing. .EXAMPLE $encoding = Find-PodeRouteTransferEncoding -Path "/api/data" -TransferEncoding "chunked" This example determines the transfer encoding for the route "/api/data", with an initial encoding of "chunked". If "/api/data" matches a specific pattern in the server's transfer encoding settings, the corresponding encoding is returned; otherwise, "chunked" is returned. .OUTPUTS String. Returns the determined transfer encoding for the given route path. This will be either the input TransferEncoding (if provided and valid), a matched encoding from the server's settings, or the server's default transfer encoding. .NOTES - The function uses a case-insensitive match (`-imatch`) to find the first route key pattern that matches the specified path. - This is an internal function and may change in future releases of Pode. #> function Find-PodeRouteTransferEncoding { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $TransferEncoding ) # if we already have one, return it if (![string]::IsNullOrWhiteSpace($TransferEncoding)) { return $TransferEncoding } # set the default $TransferEncoding = $PodeContext.Server.Web.TransferEncoding.Default # find type by pattern from settings $matched = $null foreach ($key in $PodeContext.Server.Web.TransferEncoding.Routes.Keys) { if ($Path -imatch $key) { $matched = $key break } } # if we get a match, set it if (!(Test-PodeIsEmpty $matched)) { $TransferEncoding = $PodeContext.Server.Web.TransferEncoding.Routes[$matched] } return $TransferEncoding } function Find-PodeRouteContentType { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $ContentType ) # if we already have one, return it if (![string]::IsNullOrWhiteSpace($ContentType)) { return $ContentType } # set the default $ContentType = $PodeContext.Server.Web.ContentType.Default # find type by pattern from settings $matched = $null foreach ($key in $PodeContext.Server.Web.ContentType.Routes.Keys) { if ($Path -imatch $key) { $matched = $key break } } # if we get a match, set it if (!(Test-PodeIsEmpty $matched)) { $ContentType = $PodeContext.Server.Web.ContentType.Routes[$matched] } return $ContentType } function ConvertTo-PodeMiddleware { [OutputType([hashtable[]])] param( [Parameter()] [object[]] $Middleware, [Parameter(Mandatory = $true)] [System.Management.Automation.SessionState] $PSSession ) # return if no middleware if (Test-PodeIsEmpty $Middleware) { return $null } $Middleware = @($Middleware) # ensure supplied middlewares are either a scriptblock, or a valid hashtable foreach ($mid in $Middleware) { if ($null -eq $mid) { continue } # check middleware is a type valid if (($mid -isnot [scriptblock]) -and ($mid -isnot [hashtable])) { throw ($PodeLocale.invalidMiddlewareTypeExceptionMessage -f $mid.GetType().Name)#"One of the Middlewares supplied is an invalid type. Expected either a ScriptBlock or Hashtable, but got: $($mid.GetType().Name)" } # if middleware is hashtable, ensure the keys are valid (logic is a scriptblock) if ($mid -is [hashtable]) { if ($null -eq $mid.Logic) { # A Hashtable Middleware supplied has no Logic defined throw ($PodeLocale.hashtableMiddlewareNoLogicExceptionMessage) } if ($mid.Logic -isnot [scriptblock]) { # A Hashtable Middleware supplied has an invalid Logic type. Expected ScriptBlock, but got: {0} throw ($PodeLocale.invalidLogicTypeInHashtableMiddlewareExceptionMessage -f $mid.Logic.GetType().Name) } } } # if we have middleware, convert scriptblocks to hashtables $converted = @(for ($i = 0; $i -lt $Middleware.Length; $i++) { if ($null -eq $Middleware[$i]) { continue } if ($Middleware[$i] -is [scriptblock]) { $_script, $_usingVars = Convert-PodeScopedVariables -ScriptBlock $Middleware[$i] -PSSession $PSSession $Middleware[$i] = @{ Logic = $_script UsingVariables = $_usingVars } } $Middleware[$i] }) return $converted } function Get-PodeRouteIfExistsPreference { # from route groups $groupPref = $RouteGroup.IfExists if (![string]::IsNullOrWhiteSpace($groupPref) -and ($groupPref -ine 'default')) { return $groupPref } # from Use-PodeRoute if (![string]::IsNullOrWhiteSpace($script:RouteIfExists) -and ($script:RouteIfExists -ine 'default')) { return $script:RouteIfExists } # global preference $globalPref = $PodeContext.Server.Preferences.Routes.IfExists if (![string]::IsNullOrWhiteSpace($globalPref) -and ($globalPref -ine 'default')) { return $globalPref } # final global default return 'Error' } |