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'     = '/endpoint/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]'