diff --git a/PServeREST.ps1 b/PServeREST.ps1 index 6f51196..d24a533 100644 --- a/PServeREST.ps1 +++ b/PServeREST.ps1 @@ -4,82 +4,16 @@ Param( Add-Type -AssemblyName System.Web # For [System.Web.HttpUtility]::ParseQueryString (possibly used in resource handlers) -<# - .SYNOPSIS - Extracts the resource name from the URL - - .DESCRIPTION - Takes "/time/month?SomeQuery=1" and returns "time" i.e the first node in the URL path -#> -function Get-ResourceFromURL { - Param( - [Parameter(Mandatory=$true)] - [string]$RawURL - ) - Write-Verbose "Get-ResourceFromURL| RawURL: $RawURL" - - $Resource = (($RawURL -split "\?")[0] -split "/")[1] # element 0 is empty because of leading "/" - Write-Verbose "Get-ResourceFromURL| Extracted resource: $Resource" - - $VerificationRegex = '^(?:[a-zA-Z0-9]+|favicon.ico)$' - if (-not ($Resource -match $VerificationRegex)){ - Write-Verbose "Get-ResourceFromURL| Resource name does not match the verification RegEx: $VerificationRegex" - throw "Resource identifier contains invalid characters" - } - $Resource -} - -<# - .SYNOPSIS - Receives HTTP request body -#> -function Receive-Request { - Param( - [Parameter(Mandatory,ValueFromPipelineByPropertyName)] - $Request - ) - $Output = "" - - $Size = $Request.ContentLength64 + 1 - - $buffer = New-Object byte[] $Size - do { - $count = $Request.InputStream.Read($buffer, 0, $Size) - Write-Verbose "Receive-Request | Received $count bytes" - $Output += $Request.ContentEncoding.GetString($buffer, 0, $count) - } until($count -lt $Size) - - $Request.InputStream.Close() - $Output -} +$Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue ) +$Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue ) -<# - .SYNOPSIS - Writes the response and closes Response object -#> -function Send-Response { - Param( - [Parameter(Position=0, Mandatory=$true)] - $ResponseObject, - - [Parameter(Position=1, Mandatory=$true)] - [int]$StatusCode = 200, - - [Parameter(Position=2, ValueFromPipeline=$true)] - $Content = "" - ) - # Seems like we need to set the status code first before we write data. Otherwise 200 is set by default - $ResponseObject.StatusCode = $StatusCode - - Write-Verbose "Send-ResponseNew| Content type is $($Content.GetType().ToString())" - switch($Content.GetType().ToString()){ - "System.Object[]" { $buffer = $Content } - default { $buffer = [System.Text.Encoding]::UTF8.GetBytes($Content) } +foreach($Import in @($Public + $Private)){ + Write-Verbose "Loading $($Import.FullName)" + try { + . $Import.FullName + } catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" } - - $ResponseObject.ContentLength64 = $buffer.Length - $ResponseObject.OutputStream.Write($buffer, 0, $buffer.Length) - $ResponseObject.Close() } @@ -91,60 +25,56 @@ $listener.Start() Write-Host "Listening at $ListenUrl..." try { -while ($listener.IsListening) -{ - $Context = $listener.GetContext() # Blocking while waiting for request - try{ - Write-Host '' - Write-Host "> $($Context.Request.RemoteEndPoint.ToString()) -> $($Context.Request.HttpMethod) $($Context.Request.RawUrl)" - - $Resource = $(Get-ResourceFromURL $Context.Request.RawUrl) # Extract the resource user wants to access from the url - - # Generate resource handler file name and make sure it exists - $ResourceHandlerFile = Join-Path $PSScriptRoot "Resources\$Resource.ps1" - Write-Verbose "MainLoop| ResourceHandlerFile: $ResourceHandlerFile" - - if (Test-Path -LiteralPath $ResourceHandlerFile -Type Leaf){ - # Run the Resource.ps1 to get the handler functions into our scope + while ($listener.IsListening){ + $Context = $listener.GetContext() # Blocking while waiting for request + try { + Write-Host '' + Write-Host "> $($Context.Request.RemoteEndPoint.ToString()) -> $($Context.Request.HttpMethod) $($Context.Request.RawUrl)" + + $Resource = $(Get-ResourceFromURL $Context.Request.RawUrl) # Extract the resource user wants to access from the url + + # Generate resource handler file name and make sure it exists + $ResourceHandlerFile = Join-Path $PSScriptRoot "Resources\$Resource.ps1" + Write-Verbose "MainLoop| ResourceHandlerFile: $ResourceHandlerFile" + if ( -not (Test-Path -LiteralPath $ResourceHandlerFile -Type Leaf) ){ + Write-Verbose "MainLoop| Resource handler script not found" + Send-Response $Context.Response -StatusCode 404 "Resource handler script not found" + Continue + } + + # Run the .ps1 to get the handler functions into our scope . $ResourceHandlerFile - + # Generate resource handler function name and make sure we have it our scope $ResourceHandlerProc = "$($Context.Request.HttpMethod)-$Resource" Write-Verbose "MainLoop| ResourceHandlerProc: $ResourceHandlerProc" - - if (Get-Command -Name $ResourceHandlerProc -ErrorAction SilentlyContinue) { - # Receive content if POST or PUT - if(($Context.Request.HttpMethod -eq 'POST') -or ($Context.Request.HttpMethod -eq 'PUT')){ - Write-Verbose "MainLoop| Receiving POST content" - $Context.Request | Add-Member -MemberType NoteProperty -Name RawContent -Value $(Receive-Request $Context.Request) - } - - # Run the handler function - Write-Verbose "MainLoop| >Calling handler" - $ResponseBody = & $ResourceHandlerProc $Context - Write-Verbose "MainLoop| Calling handler" + $ResponseBody = & $ResourceHandlerProc $Context + Write-Verbose "MainLoop| +function Get-ResourceFromURL { + Param( + [Parameter(Mandatory=$true)] + [string]$RawURL + ) + Write-Verbose "Get-ResourceFromURL| RawURL: $RawURL" + + $Resource = (($RawURL -split "\?")[0] -split "/")[1] # element 0 is empty because of leading "/" + Write-Verbose "Get-ResourceFromURL| Extracted resource: $Resource" + + $VerificationRegex = '^(?:[a-zA-Z0-9]+|favicon.ico)$' + if (-not ($Resource -match $VerificationRegex)){ + Write-Verbose "Get-ResourceFromURL| Resource name does not match the verification RegEx: $VerificationRegex" + throw "Resource identifier contains invalid characters" + } + $Resource +} \ No newline at end of file diff --git a/Private/Receive-Request.ps1 b/Private/Receive-Request.ps1 new file mode 100644 index 0000000..cc20cd2 --- /dev/null +++ b/Private/Receive-Request.ps1 @@ -0,0 +1,23 @@ +<# + .SYNOPSIS + Receives HTTP request body +#> +function Receive-Request { + Param( + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [System.Net.HttpListenerRequest]$Request + ) + $Output = "" + + $Size = $Request.ContentLength64 + 1 + + $buffer = New-Object byte[] $Size + do { + $count = $Request.InputStream.Read($buffer, 0, $Size) + Write-Verbose "Receive-Request | Received $count bytes" + $Output += $Request.ContentEncoding.GetString($buffer, 0, $count) + } until($count -lt $Size) + + $Request.InputStream.Close() + $Output +} \ No newline at end of file diff --git a/Private/Send-Response.ps1 b/Private/Send-Response.ps1 new file mode 100644 index 0000000..6f5c869 --- /dev/null +++ b/Private/Send-Response.ps1 @@ -0,0 +1,28 @@ +<# + .SYNOPSIS + Writes the response and closes Response object +#> +function Send-Response { + Param( + [Parameter(Position=0, Mandatory=$true)] + [System.Net.HttpListenerResponse]$Response, + + [Parameter(Position=1, Mandatory=$true)] + [int]$StatusCode = 200, + + [Parameter(Position=2, ValueFromPipeline=$true)] + $Content = "" + ) + # Seems like we need to set the status code first before we write data. Otherwise 200 is set by default + $Response.StatusCode = $StatusCode + + Write-Verbose "Send-ResponseNew| Content type is $($Content.GetType().ToString())" + switch($Content.GetType().ToString()){ + "System.Object[]" { $buffer = $Content } + default { $buffer = [System.Text.Encoding]::UTF8.GetBytes($Content) } + } + + $Response.ContentLength64 = $buffer.Length + $Response.OutputStream.Write($buffer, 0, $buffer.Length) + $Response.Close() +} \ No newline at end of file