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\Import-RouteSet.ps1 function Import-RouteSet { <# .DESCRIPTION This function imports the specified routes file. .PARAMETER RoutesFilePath Provide a valid path to a .json file .EXAMPLE Invoke-AvailableRouteSet -RoutesFilePath $env:systemdrive/RestPS/endpoints/routes.json .NOTES This will return null. #> [CmdletBinding()] [OutputType([Hashtable])] param( [Parameter(Mandatory = $true)][String]$RoutesFilePath ) if (Test-Path -Path $RoutesFilePath) { $script:Routes = Get-Content -Raw $RoutesFilePath | ConvertFrom-Json } else { Throw "Import-RouteSet - Could not validate Path $RoutesFilePath" } } # .\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 $env:systemdrive/RestPS/endpoints/routes.json .EXAMPLE Invoke-RequestRouter -RequestType GET -RequestURL /process -RoutesFilePath $env:systemdrive/RestPS/endpoints/routes.json -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. Import-RouteSet -RoutesFilePath $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\Disable-SSLValidation.ps1 function Disable-SSLValidation { <# .SYNOPSIS Disables SSL certificate validation .DESCRIPTION Disable-SSLValidation disables SSL certificate validation by using reflection to implement the System.Net.ICertificatePolicy class. Author: Matthew Graeber (@mattifestation) License: BSD 3-Clause .NOTES Reflection is ideal in situations when a script executes in an environment in which you cannot call csc.ese to compile source code. If compiling code is an option, then implementing System.Net.ICertificatePolicy in C# and Add-Type is trivial. .EXAMPLE Disable-SSLValidation .LINK http://www.exploit-monday.com #> Set-StrictMode -Version 2 # You have already run this function if ([System.Net.ServicePointManager]::CertificatePolicy.ToString() -eq 'IgnoreCerts') { Return } $Domain = [AppDomain]::CurrentDomain $DynAssembly = New-Object System.Reflection.AssemblyName('IgnoreCerts') $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('IgnoreCerts', $false) $TypeBuilder = $ModuleBuilder.DefineType('IgnoreCerts', 'AutoLayout, AnsiClass, Class, Public, BeforeFieldInit', [System.Object], [System.Net.ICertificatePolicy]) $TypeBuilder.DefineDefaultConstructor('PrivateScope, Public, HideBySig, SpecialName, RTSpecialName') | Out-Null $MethodInfo = [System.Net.ICertificatePolicy].GetMethod('CheckValidationResult') $MethodBuilder = $TypeBuilder.DefineMethod($MethodInfo.Name, 'PrivateScope, Public, Virtual, HideBySig, VtableLayoutMask', $MethodInfo.CallingConvention, $MethodInfo.ReturnType, ([Type[]] ($MethodInfo.GetParameters() | ForEach-Object {$_.ParameterType}))) $ILGen = $MethodBuilder.GetILGenerator() $ILGen.Emit([Reflection.Emit.Opcodes]::Ldc_I4_1) $ILGen.Emit([Reflection.Emit.Opcodes]::Ret) $TypeBuilder.CreateType() | Out-Null # Disable SSL certificate validation [System.Net.ServicePointManager]::CertificatePolicy = New-Object IgnoreCerts return $true } # .\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 $env:SystemDrive/RestPS .NOTES This will return a boolean. #> [CmdletBinding()] [OutputType([boolean])] param( [string]$LocalDir = "$env:SystemDrive/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/RestPSRoutes.json" Copy-Item -Path "$RoutesFileSource" -Destination "$LocalDir/endpoints" -Confirm:$false -Force $GetRoutesFileSource = $Source + "/endpoints/GET/Invoke-GetRoutes.ps1" Copy-Item -Path $GetRoutesFileSource -Destination "$LocalDir/endpoints/GET" -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\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 $env:SystemDrive/RestPS/temp/customRoutes.ps1 .EXAMPLE Start-RestPSListener -RoutesFilePath $env:SystemDrive/RestPS/customRoutes.ps1 .EXAMPLE Start-RestPSListener -RoutesFilePath $env:SystemDrive/RestPS/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 = "$env:SystemDrive/RestPS/endpoints/RestPSRoutes.json", [Parameter()][String]$RestPSLocalRoot = "$env:SystemDrive/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" $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]' |