RestPS.psm1
Write-Verbose 'Importing from [C:\projects\restps\RestPS\private]' # .\RestPS\private\Get-ClientCertInfo.ps1 function Get-ClientCertInfo { <# .DESCRIPTION This function Collect Information on a Client Certificate. .EXAMPLE Get-ClientCertInfo .NOTES This will return null. #> $script:ClientCert = $script:Request.GetClientCertificate() $script:SubjectName = $script:ClientCert.Subject } # .\RestPS\private\Invoke-AvailableRouteSet.ps1 function Invoke-AvailableRouteSet { <# .DESCRIPTION This function defines the available Routes (Rest Methods and Commands/Scripts). .EXAMPLE Invoke-AvailableRouteSet .NOTES This will return null. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", '')] $script:Routes = @( @{ 'RequestType' = 'GET' 'RequestURL' = '/proc' 'RequestCommand' = 'Get-Process -ErrorAction SilentlyContinue | Select-Object -PropertyName ProcessName -ErrorAction SilentlyContinue' } @{ 'RequestType' = 'GET' 'RequestURL' = '/status' 'RequestCommand' = 'return 1' } @{ 'RequestType' = 'GET' 'RequestURL' = '/process' 'RequestCommand' = 'C:\RestPS\endpoints\GET\Invoke-GetProcess.ps1' } @{ 'RequestType' = 'PUT' 'RequestURL' = '/Service' 'RequestCommand' = 'C:\RestPS\endpoints\PUT\Invoke-GetProcess.ps1' } @{ 'RequestType' = 'POST' 'RequestURL' = '/data' 'RequestCommand' = 'C:\RestPS\endpoints\POST\Invoke-GetProcess.ps1' } @{ 'RequestType' = 'DELETE' 'RequestURL' = '/data' 'RequestCommand' = 'C:\RestPS\endpoints\DELETE\Invoke-GetProcess.ps1' } ) } Invoke-AvailableRouteSet # .\RestPS\private\Invoke-GetBody.ps1 function Invoke-GetBody { <# .DESCRIPTION This function retrieves the Data from the HTTP Listener Body property. .EXAMPLE Invoke-GetBody .NOTES This will return a Body object. #> if ($script:Request.HasEntityBody) { $script:RawBody = $script:Request.InputStream $Reader = New-Object System.IO.StreamReader @($script:RawBody, [System.Text.Encoding]::UTF8) $script:Body = $Reader.ReadToEnd() $Reader.close() $script:Body } else { $script:Body = "null" $script:Body } } # .\RestPS\private\Invoke-GetContext.ps1 function Invoke-GetContext { <# .DESCRIPTION This function retrieves the Data from the HTTP Listener. .EXAMPLE Invoke-GetContext .NOTES This will return a HTTPListenerContext object. #> $script:context = $listener.GetContext() $Request = $script:context.Request $Request } # .\RestPS\private\Invoke-RequestRouter.ps1 function Invoke-RequestRouter { <# .DESCRIPTION This function will attempt to run a Client specified command defined in the Endpoint Routes. .PARAMETER RequestType A RequestType is required. .PARAMETER RequestURL A RequestURL is is required. .PARAMETER RequestArgs A RequestArgs is Optional. .PARAMETER RoutesFilePath A RoutesFilePath is Optional. .EXAMPLE Invoke-RequestRouter -RequestType GET -RequestURL /process .EXAMPLE Invoke-RequestRouter -RequestType GET -RequestURL /process -RoutesFilePath c:\RestPS\Invoke-AvailableRouteSet.ps1 .EXAMPLE Invoke-RequestRouter -RequestType GET -RequestURL /process -RoutesFilePath c:\RestPS\Invoke-AvailableRouteSet.ps1 -RequestArgs foo=Bar&cash=Money .NOTES This will return output from the Endpoint Command/script. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", '')] [OutputType([boolean])] [OutputType([Hashtable])] param( [Parameter(Mandatory = $true)][String]$RequestType, [Parameter(Mandatory = $true)][String]$RequestURL, [Parameter(Mandatory = $false)][String]$RequestArgs, [Parameter()][String]$RoutesFilePath ) # Import Routes each pass, to include new routes. . $RoutesFilePath $Route = ($Routes | Where-Object {$_.RequestType -eq $RequestType -and $_.RequestURL -eq $RequestURL}) if ($null -ne $Route) { # Process Request $RequestCommand = $Route.RequestCommand set-location $PSScriptRoot if ($RequestCommand -like "*.ps1") { # Execute Endpoint Script $CommandReturn = . $RequestCommand -RequestArgs $RequestArgs -Body $script:Body } else { # Execute Endpoint Command (No body allowed.) $Command = $RequestCommand + " " + $RequestArgs $CommandReturn = Invoke-Expression -Command "$Command" -ErrorAction SilentlyContinue } if ($null -eq $CommandReturn) { # Not a valid response $script:result = "400 Invalid Command" } else { # Valid response $script:result = $CommandReturn } } else { # No matching Routes $script:result = "404 No Matching Routes" } $script:result } # .\RestPS\private\Invoke-StartListener.ps1 function Invoke-StartListener { <# .DESCRIPTION This function will start a defined HTTP listener. .PARAMETER Port A Port is required. .PARAMETER SSLThumbprint An SSLThumbprint is optional. .PARAMETER AppGuid A AppGuid is Optional. .EXAMPLE Invoke-StartListener -Port 8080 .EXAMPLE Invoke-StartListener -Port 8080 -SSLThumbPrint $SSLThumbPrint -AppGuid $AppGuid .NOTES This will return null. #> param( [Parameter(Mandatory = $true)][String]$Port, [Parameter()][String]$SSLThumbprint, [Parameter()][String]$AppGuid ) if ($SSLThumbprint) { # Verify the Certificate with the Specified Thumbprint is available. $CertificateListCount = ((Get-ChildItem -Path Cert:\LocalMachine -Recurse | Where-Object {$_.Thumbprint -eq "$SSLThumbprint"}) | Measure-Object).Count if ($CertificateListCount -ne 0) { # SSL Thumbprint present, enabling SSL netsh http delete sslcert ipport=0.0.0.0:$Port netsh http add sslcert ipport=0.0.0.0:$Port certhash=$SSLThumbprint "appid={$AppGuid}" $Prefix = "https://" } else { Throw "Invoke-StartListener: Could not find Matching Certificate in CertStore: Cert:\LocalMachine" } } else { # No SSL Thumbprint present $Prefix = "http://" } try { $listener.Prefixes.Add("$Prefix+:$Port/") $listener.Start() $Host.UI.RawUI.WindowTitle = "RestPS - $Prefix - Port: $Port" Write-Output "Starting: $Prefix Listener on Port: $Port" } catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Throw "Invoke-StartListener: $ErrorMessage $FailedItem" } } # .\RestPS\private\Invoke-StopListener.ps1 function Invoke-StopListener { <# .DESCRIPTION This function will stop the specified Http(s) Listener. .PARAMETER Port A Port is Optional. Defaults to 8080. .EXAMPLE Invoke-StopListener -Port 8080 .EXAMPLE Invoke-StopListener .NOTES This will return Null. #> param( [Parameter()][String]$Port = 8080 ) Write-Output "Stopping HTTP Listener on port: $Port ..." $listener.Stop() } # .\RestPS\private\Invoke-StreamOutput.ps1 function Invoke-StreamOutput { <# .DESCRIPTION This function will Stream output back to the Client. .EXAMPLE Invoke-StreamOutput .NOTES This will returns a stream of data. #> # Process the Return data to send Json message back. $message = $script:result | ConvertTo-Json # Convert the data to UTF8 bytes [byte[]]$buffer = [System.Text.Encoding]::UTF8.GetBytes("$message") # Set length of response $script:Response.ContentLength64 = $buffer.length # Write response out and close $script:Response.OutputStream.Write($buffer, 0, $buffer.length) $script:Response.Close() } # .\RestPS\private\Invoke-ValidateClient.ps1 function Invoke-ValidateClient { <# .DESCRIPTION This function provides several way to validate or authenticate a client. A client could be a user or a computer. .PARAMETER VerificationType A VerificationType is optional - Accepted values are: -"VerifyRootCA": Verifies the Root CA of the Server and Client Cert Match. -"VerifySubject": Verifies the Root CA, and the Client is on a User provide ACL. -"VerifyUserAuth": Provides an option for Advanced Authentication, plus the RootCA,Subject Checks. .PARAMETER RestPSLocalRoot The RestPSLocalRoot is also optional, and defaults to "C:\RestPS" .EXAMPLE Invoke-ValidateClient -VerificationType VerifyRootCA -RestPSLocalRoot c:\RestPS .NOTES This will return a boolean. #> [CmdletBinding()] [OutputType([boolean])] param( [ValidateSet("VerifyRootCA", "VerifySubject", "VerifyUserAuth")] [Parameter()][String]$VerificationType, [Parameter()][String]$RestPSLocalRoot = "c:\RestPS" ) switch ($VerificationType) { "VerifyRootCA" { # Source the File . $RestPSLocalRoot\bin\Invoke-VerifyRootCA.ps1 $script:VerifyStatus = Invoke-VerifyRootCA } "VerifySubject" { # Source the File . $RestPSLocalRoot\bin\Invoke-VerifySubject.ps1 $script:VerifyStatus = Invoke-VerifySubject } "VerifyUserAuth" { # Source the File . $RestPSLocalRoot\bin\Invoke-VerifyUserAuth.ps1 $script:VerifyStatus = Invoke-VerifyUserAuth } default { Write-Output "No Client Validation Selected." $script:VerifyStatus = $true } } $script:VerifyStatus } Write-Verbose 'Importing from [C:\projects\restps\RestPS\public]' # .\RestPS\public\Invoke-DeployRestPS.ps1 function Invoke-DeployRestPS { <# .DESCRIPTION This function will setup a local Endpoint directory structure. .PARAMETER LocalDir A LocalDir is Optional. .EXAMPLE Invoke-DeployRestPS -LocalDir c:\RestPS .NOTES This will return a boolean. #> [CmdletBinding()] [OutputType([boolean])] param( [string]$LocalDir = "c:\RestPS" ) try { # Setup the local File directories if (Test-Path -Path "$LocalDir") { Write-Output "Directory: $LocalDir, exists." } else { Write-Output "Creating RestPS Directories." New-Item -Path "$LocalDir" -ItemType Directory New-Item -Path "$LocalDir\bin" -ItemType Directory New-Item -Path "$LocalDir\endpoints" -ItemType Directory New-Item -Path "$LocalDir\endpoints\Logs" -ItemType Directory New-Item -Path "$LocalDir\endpoints\GET" -ItemType Directory New-Item -Path "$LocalDir\endpoints\POST" -ItemType Directory New-Item -Path "$LocalDir\endpoints\PUT" -ItemType Directory New-Item -Path "$LocalDir\endpoints\DELETE" -ItemType Directory } # Move Example files to the Local Directory $Source = (Split-Path -Path (Get-Module -ListAvailable RestPS | Sort-Object -Property Version -Descending | Select-Object -First 1).path) $RoutesFileSource = $Source + "\endpoints\Invoke-AvailableRouteSet.ps1" Copy-Item -Path "$RoutesFileSource" -Destination $LocalDir\Endpoints -Confirm:$false -Force $EndpointVerbs = @("GET", "POST", "PUT", "DELETE") foreach ($Verb in $EndpointVerbs) { $EndpointSource = $Source + "\endpoints\$Verb\Invoke-GetProcess.ps1" Write-Output "Copying $EndpointSource to Desination $LocalDir\Endpoints\$Verb" Copy-Item -Path "$EndpointSource" -Destination $LocalDir\Endpoints\$Verb -Confirm:$false -Force } $BinFiles = Get-ChildItem -Path ($Source + "\bin") -File foreach ($file in $BinFiles) { $filePath = $file.FullName $filename = $file.Name Write-Output "Copying File $fileName to $localDir\bin" Copy-Item -Path "$filePath" -Destination $LocalDir\bin -Confirm:$false -Force } } catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Throw "Invoke-DeployRestPS: $ErrorMessage $FailedItem" } } # .\RestPS\public\Invoke-SSLIgnore.ps1 function Invoke-SSLIgnore { <# .DESCRIPTION Ignore SSL validation. .EXAMPLE Invoke-SSLIgnore .NOTES No notes. #> begin { # No code. } process { if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) { $certCallback = @" using System; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public class ServerCertificateValidationCallback { public static void Ignore(){ if(ServicePointManager.ServerCertificateValidationCallback ==null){ ServicePointManager.ServerCertificateValidationCallback += delegate( Object obj, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors ){ return true; }; } } } "@ Add-Type $certCallback } [ServerCertificateValidationCallback]::Ignore(); return $true } } # .\RestPS\public\Start-RestPSListener.ps1 function Start-RestPSListener { <# .DESCRIPTION Start a HTTP listener on a specified port. .PARAMETER Port A Port can be specified, but is not required, Default is 8080. .PARAMETER SSLThumbprint A SSLThumbprint can be specified, but is not required. .PARAMETER RestPSLocalRoot A RestPSLocalRoot be specified, but is not required. Default is c:\RestPS .PARAMETER AppGuid A AppGuid can be specified, but is not required. .PARAMETER VerificationType A VerificationType is optional - Accepted values are: -"VerifyRootCA": Verifies the Root CA of the Server and Client Cert Match. -"VerifySubject": Verifies the Root CA, and the Client is on a User provide ACL. -"VerifyUserAuth": Provides an option for Advanced Authentication, plus the RootCA,Subject Checks. .PARAMETER RoutesFilePath A Custom Routes file can be specified, but is not required, default is included in the module. .EXAMPLE Start-RestPSListener .EXAMPLE Start-RestPSListener -Port 8081 .EXAMPLE Start-RestPSListener -Port 8081 -RoutesFilePath C:\temp\customRoutes.ps1 .EXAMPLE Start-RestPSListener -RoutesFilePath C:\temp\customRoutes.ps1 .EXAMPLE Start-RestPSListener -RoutesFilePath C:\temp\customRoutes.ps1 -VerificationType VerifyRootCA -SSLThumbprint $Thumb -AppGuid $Guid .NOTES No notes at this time. #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = "Low" )] [OutputType([boolean])] [OutputType([Hashtable])] [OutputType([String])] param( [Parameter()][String]$RoutesFilePath = "null", [Parameter()][String]$RestPSLocalRoot = "c:\RestPS", [Parameter()][String]$Port = 8080, [Parameter()][String]$SSLThumbprint, [Parameter()][String]$AppGuid = ((New-Guid).Guid), [ValidateSet("VerifyRootCA", "VerifySubject", "VerifyUserAuth")] [Parameter()][String]$VerificationType ) # Set a few Flags $script:Status = $true $script:ValidateClient = $true if ($pscmdlet.ShouldProcess("Starting HTTP Listener.")) { $script:listener = New-Object System.Net.HttpListener Invoke-StartListener -Port $Port -SSLThumbPrint $SSLThumbprint -AppGuid $AppGuid # Run until you send a GET request to /shutdown Do { # Capture requests as they come in (not Asyncronous) # Routes can be configured to be Asyncronous in Nature. $script:Request = Invoke-GetContext $script:ProcessRequest = $true $script:result = $null # Perform Client Verification if SSLThumbprint is present and a Verification Method is specified if ($VerificationType -ne "") { Get-ClientCertInfo Write-Output "Validating Client CN: $script:SubjectName" $script:ProcessRequest = (Invoke-ValidateClient -VerificationType $VerificationType -RestPSLocalRoot $RestPSLocalRoot) } else { Write-Output "Not Validating Client" $script:ProcessRequest = $true } # Determine if a Body was sent with the Client request $script:Body = Invoke-GetBody # Request Handler Data $RequestType = $script:Request.HttpMethod $RawRequestURL = $script:Request.RawUrl # Specific args will need to be parsed in the Route commands/scripts $RequestURL, $RequestArgs = $RawRequestURL.split("?") if ($script:ProcessRequest -eq $true) { # Break from loop if GET request sent to /shutdown if ($RequestURL -match '/EndPoint/Shutdown$') { Write-Output "Received Request to shutdown Endpoint." $script:result = "Shutting down ReST Endpoint." $script:Status = $false $script:HttpCode = 200 } else { # Attempt to process the Request. Write-Output "Processing RequestType: $RequestType URL: $RequestURL Args: $RequestArgs" if ($RoutesFilePath -eq "null") { $RoutesFilePath = "Invoke-AvailableRouteSet" } $script:result = Invoke-RequestRouter -RequestType "$RequestType" -RequestURL "$RequestURL" -RoutesFilePath "$RoutesFilePath" -RequestArgs "$RequestArgs" } } else { Write-Output "Not Processing RequestType: $RequestType URL: $RequestURL Args: $RequestArgs" $script:result = "401 Client failed Verification or Authentication" } # Setup a placeholder to deliver a response $script:Response = $script:context.Response # Convert the returned data to JSON and set the HTTP content type to JSON $script:Response.ContentType = 'application/json' $script:Response.StatusCode = 200 # Stream the output back to requestor. Invoke-StreamOutput } while ($script:Status -eq $true) #Terminate the listener Invoke-StopListener -Port $Port Write-Output "Listener Stopped" } else { # -WhatIf was used. return $false } } Write-Verbose 'Importing from [C:\projects\restps\RestPS\classes]' |