2atMonitoring.psm1

#Requires -Version 4.0 -Modules 2atWeb,2atGeneral,2atSql

$PSDefaultParameterValues.Clear()
Set-StrictMode -Version 2.0

$ErrorActionPreference = 'Stop'

Add-Type -Assembly System.Web
Import-Module 2atGeneral
Import-Module 2atWeb
Import-Module 2atSql

Import-Module (Join-Path -Path $PSScriptRoot -ChildPath HtmlAgilityPack.dll)
[HtmlAgilityPack.HtmlNode]::ElementsFlags.Remove("form") | Out-Null

Function RelToAbs {
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({(New-Object System.Uri $_)})]
        [string]$BaseUrl,
        
        [Parameter(Mandatory=$true)]
        [string]$RelativeUrl,
        
        [switch]$NoHtmlDecode
    )
    if (! $NoHtmlDecode) { $RelativeUrl = [System.Web.HttpUtility]::HtmlDecode($RelativeUrl) }

    if ([System.Uri]::IsWellFormedUriString($RelativeUrl,[System.UriKind]::Absolute)) { return $RelativeUrl }
    
    if ([System.Uri]::IsWellFormedUriString($RelativeUrl,[System.UriKind]::Relative)) {
        $l = (New-Object System.Uri((New-Object System.Uri $BaseUrl), $RelativeUrl)).AbsoluteUri
        Write-Verbose "Absolute URL is $l"
        return $l
    }
    
    if ($NoHtmlDecode -and ([Uri]$RelativeUrl).IsAbsoluteUri) {
        Write-Warning "RelativeUrl is not correctly URL encoded '$RelativeUrl'. If this is a HTTP REDIRECT Location header, this is incorrect server behavior. Retrying with encoded URL."
        return ([Uri]$RelativeUrl).AbsoluteUri
    }    
    
    throw "RelativeUrl is not a valid (absolute or relative) url: '$RelativeUrl'"
}

Function Step {
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({(New-Object System.Uri $_)})]
        [string]$Url,

        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session,
        
        [string]$Method = 'GET',
        
        [ValidateScript({ $_ -is [HashTable] -or $_ -is [System.Collections.Generic.Dictionary[string,string]] })]
        [object]$FormData
    )
    
    if ($Session.UserAgent) { 
        $UserAgent = $Session.UserAgent 
    } else {
        $UserAgent = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; 2AT Monitoring; +http://2at.nl)'
    }
    $res = Get-WebResponse -Url $Url -Method $Method -FormData $FormData -HostIPs $Session.HostIPs -CookieContainer $Session.CookieContainer -Proxy $Session.Proxy -UserAgent $UserAgent -Credentials $Session.Credentials
    
    if ($Session.History | ?{ $Url -eq $_.Url -and $Method -eq $_.Method -and $res.ResponseBody -eq $_.ResponseBody }) {
        $res.WebRequestStatus='LoopDetected'
        $res.WebRequestStatusDescription="The same URL was already visited on the same Step and received the same ResponseBody. ($Url)"
    }
    
    $Session.History += $res

    if ($res.WebRequestStatus -eq [System.Net.WebExceptionStatus]::Success) {
        if ($res.HTTPStatus -In (301, 302, 303, 307)) {
            $l = RelToAbs -BaseUrl $url -RelativeUrl $res.ResponseHeaders['Location'] -NoHtmlDecode
            Write-Verbose "REDIRECT ($($res.HTTPStatus)) -> $l"
            Step -Url $l -Session $Session
            return
        }
        
        if ($res.HTTPStatus -eq 200) {
            if ($res.ResponseBody.EndsWith('<noscript><p>Script is disabled. Click Submit to continue.</p><input type="submit" value="Submit" /></noscript></form><script language="javascript">window.setTimeout(''document.forms[0].submit()'', 0);</script></body></html>')) {
                Write-Verbose "ADFS auto POST"
                ProcessForm -Previous $res -Session $Session
                return
            }
            
            $u = (new-object System.Uri $res.Url).GetLeftPart([System.UriPartial]::Path)
            if ($Session.LoginSteps[$u]) {
                RunStep -Step $Session.LoginSteps[$u] -Previous $res -Session $Session
                return
            }
        }
    }
}

Function ProcessForm {
    Param(
        [HashTable]$FormData,

        [string]$FormId,

        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Previous,
        
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session
    )
    
    $htmldoc = New-Object HtmlAgilityPack.HtmlDocument
    $htmldoc.LoadHtml($Previous.ResponseBody)
    if ($FormId) {
        $form = $htmldoc.DocumentNode.SelectNodes("//form[@id='$FormId']")[0]
        if (!$form) { throw "No form with id='$FormId' found" }
    } else {
        $forms = $htmldoc.DocumentNode.SelectNodes('//form')
        if (!$forms) { throw 'No forms found' }
        if ($forms.Count -gt 1) {
            Write-Warning "Found $($forms.Count) forms but no FormId was specified, selecting the first"
        }
        $form=$forms[0]
    }
    $l = RelToAbs $Previous.Url $form.Attributes['action'].Value

    $b = New-Object 'System.Collections.Generic.Dictionary[string,string]'
    $form.SelectNodes('//input') | ?{ $_.Attributes['name'] -And $_.Attributes['value'] } | %{ 
        $b[$_.Attributes['name'].Value] = [System.Web.HttpUtility]::HtmlDecode($_.Attributes['value'].Value)
    }
    if ($FormData) { $FormData.Keys | %{ $b[$_]=$FormData[$_] } }
    
    Write-Verbose "FORM: $(($b.Keys | %{ $_ + '=' + $b[$_] } ) -join [Environment]::NewLine)"
    Step -Url $l -Method $form.Attributes['method'].Value -FormData $b -Session $Session
}

Function ProcessLink {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$LinkText,

        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Previous,
        
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session,

        [Parameter(Mandatory=$false)]
        [string]$LinkType
    )
    
    $htmldoc = New-Object HtmlAgilityPack.HtmlDocument
    $htmldoc.LoadHtml($Previous.ResponseBody)
    if ($LinkType -eq 'IMG') {
        $links = $htmldoc.DocumentNode.SelectNodes('//img[@src]') | ? { $_ -and $_.Attributes['src'].Value.Contains($LinkText) } | % { $_.Attributes['src'].Value } | select -Unique
    } else {
        $links = $htmldoc.DocumentNode.SelectNodes('//a[@href]') | ? { $_ -and $_.InnerText.Trim() -eq $LinkText } | % { $_.Attributes['href'].Value } | select -Unique
    }

    $c = ($links | measure).Count
    if ($c -ne 1) { 
        Write-Warning "Found $c links matching '$LinkText', expected exactly one."
        $Previous.WebRequestStatus='LinkFailed'
        $Previous.WebRequestStatusDescription="Found $c links matching '$LinkText', expected exactly one."
        return
    }

    $l = RelToAbs $Previous.Url $links
    Write-Verbose "LINK: $l"
    
    Step -url $l -Session $Session
}

Function ProcessScript {
    Param(
        [Parameter(Mandatory=$true)]
        [string]$ScriptId,

        [Parameter(Mandatory=$true)]
        [string]$ScriptRegEx,

        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Previous,
        
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session
    )
    
    $htmldoc = New-Object HtmlAgilityPack.HtmlDocument
    $htmldoc.LoadHtml($Previous.ResponseBody)
    $s = $htmldoc.DocumentNode.SelectNodes('//script') | ?{ $_ -and $_.InnerText.Trim() -match [Regex]::Escape($ScriptId) }
    
    if (!$s) { throw "No script block matching '$ScriptId' found (using literal match)" }
    
    if ($s -is [HtmlAgilityPack.HtmlNodeCollection]) {
        Write-Warning 'Multiple matching script blocks found, using the first'
        $s = $s[0]
    }
    
    if (!($s.InnerText.Trim() -match $ScriptRegEx)) {
        throw "No url could be found: Script block doesn't match '$ScriptRegEx'"
    }
    
    if ($Matches['link']) {
        $l = RelToAbs $Previous.Url $Matches['link']
    } else {
        if ($Matches.Count -eq 1) {
            Write-Warning 'No match groups found, using full match as link to follow'
            $l = RelToAbs $Previous.Url $Matches[0]
        } else {
            if ($Matches.Count -gt 2) {
                Write-Warning 'Found multiple match groups, using first (create a named group ''link'' to specify witch group to use)'
            }
            $l = RelToAbs $Previous.Url $Matches[1]
        }
    }
    
    Step -url $l -Session $Session
}

Function ProcessSOAP {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Url,

        [HashTable]$RequestData,

        [Parameter(Mandatory=$false)]
        [PSCustomObject]$Previous,
        
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session
    )

        $Cred = New-Object System.Management.Automation.PSCredential ("$($session.Credentials.Domain)\$($session.Credentials.username)", (ConvertTo-SecureString $session.Credentials.Password -AsPlainText -Force) )

        $FormData = @{ 'SOAP' = $RequestData['Request']; 'SOAPAction' = $RequestData['SOAPAction'] }

        $res = Get-WebResponse -Url $url -Method 'POST' -Credentials $cred -FormData $FormData -Proxy $Session.Proxy -UserAgent $Session.UserAgent
   
        $session.History += $res

}

Function ProcessAPI {
        param(
        [Parameter(Mandatory=$true)]
        [string]$Url,

        [HashTable]$RequestData,

        [Parameter(Mandatory=$false)]
        [PSCustomObject]$Previous,
        
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session
    )

    $Cred = New-Object System.Management.Automation.PSCredential ("$($session.Credentials.Domain)\$($session.Credentials.username)", (ConvertTo-SecureString $session.Credentials.Password -AsPlainText -Force) )

    $res = Get-WebResponse -Url $url -Method 'GET' -Credentials $cred -FormData @{'API'=$RequestData} -Proxy $Session.Proxy -UserAgent $Session.UserAgent
   
    $session.History += $res
    
}

Function UpdateSession {
    Param(
        [Parameter(Mandatory=$true)]    
        [HashTable]$Step,
        
        [Parameter(Mandatory=$true)]    
        [PSCustomObject]$Session,
        
        [PSCustomObject]$TMGInfo,
        
        [Switch]$IsNewSession
    )
    Write-Debug "Updating $(if($IsNewSession) {'NEW'})session"
    
    if (!$IsNewSession -and ($Step['SqlConnection'] -or $Step['Name'])) { throw 'Specifying a SqlConnection or Name is only supported for new sessions' }

    if ($Step['Proxy']) { $Session.Proxy = New-Object System.Net.WebProxy $Step.Proxy    }
    if ($Step['LoginSteps']) { $Session.LoginSteps = $Step.LoginSteps }
    if ($Step['Servers']) { 
        if($TMGInfo){ SetSessionCookiesTmg -Session $Session -TMGInfo $TMGInfo -Servers $Step.Servers }
        if($Step['CookieName'] -and $Step['TargetServer'] -and $Step['CookieUrls']) { SetSessionCookie -Session $Session -TargetServer $Step.TargetServer -CookieName $Step.CookieName -Urls $Step.CookieUrls}
    }
    if ($Step['TargetServer']) { $Session.TargetServer = $Step.TargetServer }
    if ($Step['NextLogMinutes']) { $Session.NextLogMinutes = $Step.NextLogMinutes }
    if ($Step['HostIPs']) { $Session.HostIPs = $Step.HostIPs }
    if ($Step['Credentials']) {
        if ($Step.Credentials -is [PSCredential]) {
            $Session.Credentials = $Step.Credentials.GetNetworkCredential()
        } elseif ($Step.Credentials -is [System.Net.ICredentials]) {
            $Session.Credentials = $Step.Credentials
        } else {
            throw "Session credentials specified are of an unsupported type: $($Step.Credentials.GetType().FullName), please use either a PSCredential or a NetworkCredential"
        }
    }
    if ($Step['UserAgent']) { 
        $Session.UserAgent = $Step.UserAgent 
    } else {
        $Session.UserAgent = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; 2AT Monitoring; +http://2at.nl)'
    }
    
    if ($IsNewSession) { $Session }
}

Function NewSession {
    Param(
             [Parameter(Mandatory=$true)]     
             [HashTable]$Step,
             
             [PSCustomObject]$TMGInfo
       )
       
       $Session = @{
            Id=-1
            Name=$Step['Name']
            HostIPs=@{}
            CookieContainer = New-Object System.Net.CookieContainer
            Proxy=$null
            SqlConnection=$Step['SqlConnection']
            WSConnection=$Step['WSConnection']
            ReportingConnection = $Step['ReportingConnection']
            LoginSteps=@{}
            Credentials=$null
            History=@()
            RequestNumber=1
            TargetServer=$null
            NextLogMinutes=10
            UserAgent=$null
       }      


    if ($Session.SqlConnection) {
        Write-Verbose "NEWSESSION: Connecting to SQL '$($Step.SqlConnection)'"
                
        Set-SqlData -ConnectionString $Session.SqlConnection -CommandText 'ops.LogJob' -Parameters @{ Job='HTTPMonitor'; Title='Job started' }
        $MonitorSession = Get-SqlData -ConnectionString $Session.SqlConnection -CommandText 'ops.SaveMonitorSession' -Parameters @{ Name=$Session.Name; Servers=($Step['Servers'] -Join ' / ') }
        $Session.Id = $MonitorSession.SessionId
    }

    if (!$Step['Servers']) { Write-Verbose 'NEWSESSION: New session created without cookies' }
    
    UpdateSession -Step $Step -Session ([PSCustomObject]$Session) -TMGInfo $TMGInfo -IsNewSession
}

Function SetSessionCookie{
    Param(
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session,
        [Parameter(Mandatory=$true)]
        [string]$TargetServer,
        [Parameter(Mandatory=$true)]
        [string]$CookieName,
        [Parameter(Mandatory=$true)]
        [string[]]$Urls
    )
        foreach($url in $Urls) {
            $cookie = New-Object System.Net.Cookie $CookieName, $TargetServer
            $cookie.HttpOnly = $true        
            $Session.CookieContainer.Add($url, $cookie)
            Write-Verbose "Added cookie to session: $cookieName=$TargetServer $url"
        }
}

Function SetSessionCookiesTmg {
    Param(
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session,

        [Parameter(Mandatory=$true)]
        [PSCustomObject]$TMGInfo,

        [Parameter(Mandatory=$true)]
        [string[]]$Servers
    )

    $TMGInfo.CookieIndex | %{
        $cookieName = $_.Cookie
        $Url = New-Object System.Uri $_.Url

        $rule = $TMGInfo.Rules[$cookieName.SubString(7)]

        if (! $rule) {
            Write-Warning "Unable to add cookie to session. No matching rule found for cookie '$cookieName' ($Url). Possible cause is an outdated TMGRuleGUIDs.xml file." 
            return
        }
        
        foreach($server in $Servers) {
            $entry = $rule.ServerFarm | ?{ $_.HostName -Match $server }
            if ($entry) { break }
        }
        if (! $entry) { 
            Write-Warning "Unable to add cookie to session. No matching server found for rule '$($rule.Name)' ($Url)"
            return
        }
        
        $cookie = New-Object System.Net.Cookie $cookieName, $entry.GUID, '/', $Url.Host
        $cookie.HttpOnly = $true
        
        $Session.CookieContainer.Add($Url, $cookie)

        Write-Verbose "Added cookie to session: $cookieName=$($entry.GUID) ($Url)"
    }
}

Function GetServer {
    Param
    (
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$WebResponse
    )

    if ($WebResponse.ResponseHeaders['X-WFE']) { $WebResponse.ResponseHeaders['X-WFE'] } else { $WebResponse.ResponseHeaders['X-Powered-by-server'] }
}

Function ValidateResponse {
    Param(
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Response,
        
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Validate
    )
    #TODO: Transform $Response.WebRequestStatusDescription to hashtable (or xml)

    if ($Response.WebRequestStatus -ne [System.Net.WebExceptionStatus]::Success) { 
        Write-Verbose "Skipped validation because request already marked as failed ($($Response.WebRequestStatus)"
        return
    }
    
    if ($Validate['Url']) {
        if ($Response.Url.TrimEnd('/') -ne $Validate.Url.TrimEnd('/')) {
            $Response.WebRequestStatus='UrlValidationFailed'
            $Response.WebRequestStatusDescription="Url validation failed, found '$($Response.Url)', expected '$($Validate.Url)'"
            Write-Warning $Response.WebRequestStatusDescription
            return
        } else {
            Write-Verbose 'Url validation succeeded'
        }
    }
    if ($Validate['ContentMatch']) {
        if (! ($Response.ResponseBody -match $Validate.ContentMatch)) {
            $Response.WebRequestStatus='ContentValidationFailed'
            $Response.WebRequestStatusDescription="Content validation failed, page '$($Response.Url)' does not match '$($Validate.ContentMatch)'"
            Write-Warning $Response.WebRequestStatusDescription
            return
        } else {
            Write-Verbose "Content validation succeeded '$($Validate.ContentMatch)'"
        }
    }
    if ($Validate['Time']) {
        if (! ([int]$Response.TimeToFirstByte.TotalMilliseconds -le [int]$Validate.Time)) {
            $Response.WebRequestStatus='TimeValidationFailed'
            $Response.WebRequestStatusDescription="Time exceeded, page took $([int]$Response.TimeToFirstByte.TotalMilliseconds)ms, maximum was set at $([int]$Validate.Time)ms ($($Response.Url))"
            Write-Warning $Response.WebRequestStatusDescription
            return
        } else {
            Write-Verbose "Time validation succeeded ($([int]$Response.TimeToFirstByte.TotalMilliseconds)ms < $([int]$Validate.Time)ms)"
        }
    }
    if ($Validate['TargetServer']) {
        $ResponseServer = GetServer -WebResponse $Response
        if ($Validate.TargetServer -ne $ResponseServer) {
            $Response.WebRequestStatus='TargetServerValidationFailed'
            $Response.WebRequestStatusDescription="Response received from other than TargetServer. TargetServer = '$($Validate.TargetServer)'. ResponseServer = '$ResponseServer'."
            Write-Warning $Response.WebRequestStatusDescription
            return
        } else {
            Write-Verbose "TargetServer validation succeeded. TargetServer = ResponseServer = '$ResponseServer'."
        }
    }
    if ($Validate['ResponseHeader']) {
        if ( $response.ResponseHeaders[$Validate.ResponseHeader] -ne $Validate.HeaderMatch) {
            $Response.WebRequestStatus='ResponseHeaderValidationFailed'
            $Response.WebRequestStatusDescription="Response header is different than expected value ($($response.ResponseHeaders[$Validate.ResponseHeader]), expected $($Validate.HeaderMatch))"
            Write-Warning $Response.WebRequestStatusDescription
            return
        } else {
            Write-Verbose "Response header validation succeeded. Header has expected value ($($Validate.HeaderMatch))"
        }
    }
}

Function PostEntityUpdate {
    Param
    (
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session,

        [string]$Monitor
    )
    
    $lr = $Session.History[$Session.History.Length-1]

    $FormData = @{
        name = "$Monitor-$($Session.TargetServer)";
        webRequestStatus = $lr.WebRequestStatus;
        nextLogTime = $lr.DateTime.AddMinutes($Session.NextLogMinutes).ToUniversalTime().ToString('u');
        logTime = $lr.DateTime.ToUniversalTime().ToString('u');
    }

    if ($lr.WebRequestStatus -ne 'Success') {
        $information =  @{
            WebRequestStatusDescription = $lr.WebRequestStatusDescription
        }

        $FormData.information = New-Object PSObject -Property $information | ConvertTo-Xml -NoTypeInformation -as String
    }

    try {
        Write-Verbose "Posting to $($Session.WSConnection)"
        Invoke-RestMethod -Uri $Session.WSConnection -Method 'POST' -Body $FormData -TimeoutSec 5                         
    } catch {
        #TODO This should become Write-Error once the new monitoring becomes permanent
        Write-Warning "Entity endpoint unavailable $($_.Exception.Message)"                      
    }
}

Function PostEntityReportData {
    Param
    (
        [Parameter(Mandatory=$true)]
        [ValidateScript({(New-Object System.Uri $_)})]
        [string]$entityReportingUrl,
        
        [Parameter(Mandatory=$true)]     
        [string]$entityName,

        [Parameter(Mandatory=$false)]         
        [datetime]$logTime,

        [Parameter(Mandatory=$true)]
        [string]$propertyName,

        [Parameter(Mandatory=$true)]
        [int]$propertyValue
    )
        

    $FormData = @{ 
        Name = $entityName;
        LogTime = $logTime.ToUniversalTime().ToString('u');
        PropertyName = $propertyName;
        PropertyValue = $propertyValue;
    }

    try {
        Write-Verbose "Posting reportingData to $entityReportingUrl"
        Invoke-RestMethod -Uri $entityReportingUrl -Method 'POST' -Body $FormData -TimeoutSec 5
    } catch {
        #TODO This may need to become Write-Error once the new monitoring becomes permanent
        Write-Warning "Entity reporting endpoint unavailable $($_.Exception.Message)"                      
    }
}

Function LogSteps {
       Param(
             [Parameter(Mandatory=$true)]
             [PSCustomObject]$Session,
             
             [int]
             $StepNumber,
             
             [string]$Monitor,
             
             [switch]
             $PassThru
       )
       
       $i=1
       foreach($WebResponse in $Session.History) {
             $u = New-Object System.Uri $WebResponse.Url
             $hostname = "$($u.Host)"
             if ($u.Port -ne 80) { $hostname+=":$($u.Port)"}
             if ($u.Query.Length -ne 0) { $query=$u.Query.Substring(1) } else { $query='' } 

             $p = @{
                    LogDateTime=$WebResponse.DateTime
                    Host=$hostname
                    UriStem=$u.AbsolutePath
                    UriQuery=$query
                    Method=$WebResponse.Method
                    HTTPStatus=[int]$WebResponse.HTTPStatus
                     TimeTaken=[int]$WebResponse.TimeToFirstByte.TotalMilliseconds
                    RequestStatus=[string]$WebResponse.WebRequestStatus
                     RequestStatusDescription=$WebResponse.WebRequestStatusDescription
                    Url=$WebResponse.Url
                    SessionId=$Session.Id
                    Monitor=$Monitor
                    TargetServer=$Session.TargetServer
                    NextLogMinutes=$Session.NextLogMinutes
                    StepNumber=$StepNumber
                    RequestNumber=$Session.RequestNumber++
                    IsStepResult=($i -eq $Session.History.Count)
             }

             if ($WebResponse.FormData) {
                    $p.FormData=(($WebResponse.FormData.Keys | %{ $_ + '=' + $WebResponse.FormData[$_] } ) -join [Environment]::NewLine)
             }
             
             if ($WebResponse.ResponseHeaders) {
                    $p.Server=GetServer -WebResponse $WebResponse
                    
                    $p.XSharePointHealthScore=$WebResponse.ResponseHeaders['X-SharePointHealthScore']
                  $p.SPIisLatency=$WebResponse.ResponseHeaders['SPIisLatency']
             $p.SPRequestDuration=$WebResponse.ResponseHeaders['SPRequestDuration']
                    $p.RequestGuid=$WebResponse.ResponseHeaders['request-id']
                    $p.RawResponse=Get-WebResponseString -WebResponse $WebResponse
             }
             
             Write-Debug "Writing data"
             if ($Session.SqlConnection) {
            Write-Verbose "Writing data to the database"
                    Set-SqlData -ConnectionString $Session.SqlConnection -CommandText 'ops.SaveMonitorResult' -Parameters $p
             }
            

            if (!$Session.SqlConnection -And !$Session.WSConnection) {
                        if ($p.HTTPStatus) {
                            Write-Host "$(Get-Date) $($p.Server) ($($p.XSharePointHealthScore)) $($p.HTTPStatus) $($p.Method) $($u.GetLeftPart([System.UriPartial]::Path))"
                        } else {
                            Write-Host "$(Get-Date) $($p.RequestStatus) $($u.GetLeftPart([System.UriPartial]::Path))"
                        }
            }

            if ($PassThru) { $WebResponse }
            $i++
       }
       
       if ($Session.WSConnection) {
            Write-Verbose "Posting data to the entity endpoint"

            PostEntityUpdate -Session $Session -Monitor $Monitor
        }
    
    if ($Session.ReportingConnection) {
        Write-Verbose "Posting data to the reporting endpoint"
        $lastIndex = $Session.History.Length-1
        if($Session.History[$lastIndex].WebRequestStatus -eq "Success"){
            $logTime = $Session.History[$lastIndex].DateTime
            $propertyName="Response time"
            $propertyValue=[int]$Session.History[$lastIndex].TimeToFirstByte.TotalMilliseconds

            PostEntityReportData -entityReportingUrl $Session.ReportingConnection -entityName "$Monitor-$($Session.TargetServer)" -logTime $LogTime -propertyName $propertyName -propertyValue $propertyValue
        }
    }
}

Function RunStep {
    Param(
        [Parameter(Mandatory=$true)]
        [HashTable]$Step,
        
        [PSCustomObject]$Previous,
        
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Session
    )
        
    switch ($Step.Action)
    {
        'Url' {
            Write-Debug "URL: $($Step.Url)"
            Step -Url $Step.Url -Session $Session
        }
        'Link' {
            Write-Debug "LINK: $($Step.LinkText)"
            ProcessLink -LinkText $Step.LinkText -Previous $Previous -Session $Session -LinkType $Step['LinkType']
        }
        'Form' {
            Write-Debug "FORM"
            ProcessForm -FormId $Step['FormId'] -FormData $Step['FormData'] -Previous $Previous -Session $Session
        }
        'Script' {
            Write-Debug 'SCRIPT'
            ProcessScript -ScriptId $Step['ScriptId'] -ScriptRegEx $Step['ScriptRegEx'] -Previous $Previous -Session $Session
        }
        'SOAP' {
            Write-Debug 'SOAP'
            ProcessSOAP -RequestData $Step['RequestData'] -Url $Step['Url'] -Previous $Previous -Session $Session
        }
        'API' {
            Write-Debug 'API'
            ProcessAPI -RequestData $Step['RequestData'] -Url $Step['Url'] -Previous $Previous -Session $Session
        }
        default {
            throw "Unrecognized step $($CurrentStep.Action)"
        }
    }
    
    if ($Step['Validate']) {
        ValidateResponse -Response $Session.History[$Session.History.Length-1] -Validate $Step.Validate
    }
}

Function Invoke-Monitoring {
    Param(
        [Parameter(Mandatory=$true)]
        [System.Array]$Steps,

        [PSCustomObject]$TMGInfo
    )
    Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $i=0
    foreach($CurrentStep in $Steps) {
        switch ($CurrentStep.Action) {
            'NewSession' {
                Write-Progress -Activity 'Initializing session' -PercentComplete (100*$i/$Steps.Count)
                Write-Debug 'NEWSESSION'
                
                $Session = NewSession -Step $CurrentStep -TMGInfo $TMGInfo                                
                $Previous = $null
            }
            'UpdateSession' {
                Write-Progress -Activity 'Updating session' -PercentComplete (100*$i/$Steps.Count)
                Write-Debug 'UPDATESESSION'

                UpdateSession -Step $CurrentStep -TMGInfo $TMGInfo -Session $Session
            }
            default {
                Write-Progress -Activity 'Retrieving webpage' -CurrentOperation $CurrentStep.Url -PercentComplete (100*$i/$Steps.Count)

                RunStep -Step $CurrentStep -Previous $Previous -Session $Session
                LogSteps -Session $Session -StepNumber ($i+1) -Monitor $CurrentStep['Monitor'] -PassThru
                $Previous = $Session.History | Select-Object -Last 1
                $Session.History=@()
            }
        }
        
        $i++
    }
}

Export-ModuleMember -Function Invoke-*

# SIG # Begin signature block
# MIIhcgYJKoZIhvcNAQcCoIIhYzCCIV8CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC/2jOAPLr/9SYt
# gvL6kefHE5Xnqy3pYo/hTFSIY1fwFaCCCxswggUzMIIEG6ADAgECAhEAgNHe/U3D
# BzyckFGAgIDcJDANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJHQjEbMBkGA1UE
# CBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQK
# ExFDT01PRE8gQ0EgTGltaXRlZDEjMCEGA1UEAxMaQ09NT0RPIFJTQSBDb2RlIFNp
# Z25pbmcgQ0EwHhcNMTcwMTEzMDAwMDAwWhcNMjAwMTEzMjM1OTU5WjCBgDELMAkG
# A1UEBhMCTkwxEDAOBgNVBBEMBzM1NDIgRFoxEDAOBgNVBAgMB1V0cmVjaHQxEDAO
# BgNVBAcMB1V0cmVjaHQxFTATBgNVBAkMDEVuZXJnaWV3ZWcgMTERMA8GA1UECgwI
# MkFUIEIuVi4xETAPBgNVBAMMCDJBVCBCLlYuMIIBIjANBgkqhkiG9w0BAQEFAAOC
# AQ8AMIIBCgKCAQEAzB3KZ2CBenaD2WDwOsy0cHE6mLIeIYqWP718FuWeUZ5eejvw
# 8BozajbtBWgISZ2IMsTYZ1I7KFBzHgXXkNglmyboa6++x7j2Ws+T0hmHCUZ64AFb
# OkXjqYsOBCPhi3yuKIRLwc4snA3F3DCH24mBpDYymrU22+0vMIlDqpzRXBNEeIhG
# ss3jehu86l85fWVS54F5KGeDYQ2BT0Tc0UO6hMlcpCEVKIbthLm36q1/oSchRYjH
# B4JCT1KqACRhD0hJcQmTcJZvhpgOrglUVlj1ClS5xfWgHq3ySShOOZMecl0VNMtY
# xNi5TF1Ae+sie4044ioyGB6dGItGXwhObIk/9wIDAQABo4IBqDCCAaQwHwYDVR0j
# BBgwFoAUKZFg/4pN+uv5pmq4z/nmS71JzhIwHQYDVR0OBBYEFDHc2o80OMg8zNfF
# WMH8QB57E7rnMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQM
# MAoGCCsGAQUFBwMDMBEGCWCGSAGG+EIBAQQEAwIEEDBGBgNVHSAEPzA9MDsGDCsG
# AQQBsjEBAgEDAjArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8u
# bmV0L0NQUzBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNv
# bS9DT01PRE9SU0FDb2RlU2lnbmluZ0NBLmNybDB0BggrBgEFBQcBAQRoMGYwPgYI
# KwYBBQUHMAKGMmh0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9ET1JTQUNvZGVT
# aWduaW5nQ0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5j
# b20wGQYDVR0RBBIwEIEOc3VwcG9ydEAyYXQubmwwDQYJKoZIhvcNAQELBQADggEB
# AHGDJyOKLJwzdt4Y8ow7H4ZKZXs9Hopf0GhizzhcPWyWL7GI6QHhKHzFWYGsFhh2
# vesuY7p89jthK5YqSn1u2KUQuLWzQZQj3cZCK2BwSz6FpgmmjqIo49qCfKIB5IrE
# DcZAQPC9wxaXPI+R3B32JmTllBpkFQNTIJVcB7jR/Ft991iV17tMMq0GssMAHnVd
# /yvTWlUaE7XNtgtNYQ5v/8HxxNtdBXsIbdjiv/A8GjUmyPN8Dum9CW82hUqOE7U9
# AXHZIBWy9yrooSieo26GA1OzrBvnDc+L42JZnjvwdhBqSnbQrSS7L6VjVHU+Ct84
# Fnb5u23Jypdmj9123Hw9qJwwggXgMIIDyKADAgECAhAufIfMDpNKUv6U/Ry3zTSv
# MA0GCSqGSIb3DQEBDAUAMIGFMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRl
# ciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8g
# Q0EgTGltaXRlZDErMCkGA1UEAxMiQ09NT0RPIFJTQSBDZXJ0aWZpY2F0aW9uIEF1
# dGhvcml0eTAeFw0xMzA1MDkwMDAwMDBaFw0yODA1MDgyMzU5NTlaMH0xCzAJBgNV
# BAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1Nh
# bGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSMwIQYDVQQDExpDT01P
# RE8gUlNBIENvZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
# AQoCggEBAKaYkGN3kTR/itHd6WcxEevMHv0xHbO5Ylc/k7xb458eJDIRJ2u8UZGn
# z56eJbNfgagYDx0eIDAO+2F7hgmz4/2iaJ0cLJ2/cuPkdaDlNSOOyYruGgxkx9hC
# oXu1UgNLOrCOI0tLY+AilDd71XmQChQYUSzm/sES8Bw/YWEKjKLc9sMwqs0oGHVI
# wXlaCM27jFWM99R2kDozRlBzmFz0hUprD4DdXta9/akvwCX1+XjXjV8QwkRVPJA8
# MUbLcK4HqQrjr8EBb5AaI+JfONvGCF1Hs4NB8C4ANxS5Eqp5klLNhw972GIppH4w
# vRu1jHK0SPLj6CH5XkxieYsCBp9/1QsCAwEAAaOCAVEwggFNMB8GA1UdIwQYMBaA
# FLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBQpkWD/ik366/mmarjP+eZL
# vUnOEjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUE
# DDAKBggrBgEFBQcDAzARBgNVHSAECjAIMAYGBFUdIAAwTAYDVR0fBEUwQzBBoD+g
# PYY7aHR0cDovL2NybC5jb21vZG9jYS5jb20vQ09NT0RPUlNBQ2VydGlmaWNhdGlv
# bkF1dGhvcml0eS5jcmwwcQYIKwYBBQUHAQEEZTBjMDsGCCsGAQUFBzAChi9odHRw
# Oi8vY3J0LmNvbW9kb2NhLmNvbS9DT01PRE9SU0FBZGRUcnVzdENBLmNydDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA
# A4ICAQACPwI5w+74yjuJ3gxtTbHxTpJPr8I4LATMxWMRqwljr6ui1wI/zG8Zwz3W
# GgiU/yXYqYinKxAa4JuxByIaURw61OHpCb/mJHSvHnsWMW4j71RRLVIC4nUIBUzx
# t1HhUQDGh/Zs7hBEdldq8d9YayGqSdR8N069/7Z1VEAYNldnEc1PAuT+89r8dRfb
# 7Lf3ZQkjSR9DV4PqfiB3YchN8rtlTaj3hUUHr3ppJ2WQKUCL33s6UTmMqB9wea1t
# QiCizwxsA4xMzXMHlOdajjoEuqKhfB/LYzoVp9QVG6dSRzKp9L9kR9GqH1NOMjBz
# wm+3eIKdXP9Gu2siHYgL+BuqNKb8jPXdf2WMjDFXMdA27Eehz8uLqO8cGFjFBnfK
# S5tRr0wISnqP4qNS4o6OzCbkstjlOMKo7caBnDVrqVhhSgqXtEtCtlWdvpnncG1Z
# +G0qDH8ZYF8MmohsMKxSCZAWG/8rndvQIMqJ6ih+Mo4Z33tIMx7XZfiuyfiDFJN2
# fWTQjs6+NX3/cjFNn569HmwvqI8MBlD7jCezdsn05tfDNOKMhyGGYf6/VXThIXcD
# Cmhsu+TJqebPWSXrfOxFDnlmaOgizbjvmIVNlhE8CYrQf7woKBP7aspUjZJczcJl
# mAaezkhb1LU3k0ZBfAfdz/pD77pnYf99SeC7MH1cgOPmFjlLpzGCFa0wghWpAgEB
# MIGSMH0xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIx
# EDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSMw
# IQYDVQQDExpDT01PRE8gUlNBIENvZGUgU2lnbmluZyBDQQIRAIDR3v1Nwwc8nJBR
# gICA3CQwDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEMMQIwADAZBgkqhkiG
# 9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIB
# FTAvBgkqhkiG9w0BCQQxIgQg4oaN64KBgD5upZO1Arof5KUSnnQ6yt/O9sA4hD7L
# oPowDQYJKoZIhvcNAQEBBQAEggEAZCZcglkyfG28xO0WKCaP1fZYjoIsWU7uRFRo
# bgYaDRwUlzlhwHtO1KVyghJUHb6epL2CHA6askTXWiRQI06l+4j+WSs5YHByfBjL
# 35501WXrwT33h1MB6vLUH7fZqbVagPJ/jkxoMh3sDKRkHf5V4HDXtHsiMLrFK74s
# MYJljQKoOX0FlrAavolLfRY64NwuBv5FFiQDpkss0SJYAHCXzM5VyfAiCEjkl1cX
# Mkd9kCMDga6nRLKu4hu5r0+hPzup2uS8dQN1HvqYl1RXRw/btQY+l3Tx9flF2hHX
# UPxtpwnlZl+T7tvxKEiqNXxlxpND+MyznBzyiJsd0ywUTOg+RKGCE20wghNpBgor
# BgEEAYI3AwMBMYITWTCCE1UGCSqGSIb3DQEHAqCCE0YwghNCAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggEMBgsqhkiG9w0BCRABBKCB/ASB+TCB9gIBAQYKKwYBBAGyMQIB
# ATAxMA0GCWCGSAFlAwQCAQUABCA2q6Ug20PByU5oDwP3Q0nEe36StczEB/pOBthQ
# Y1DJFwIUCEbU2nx4sOLoWpVt2iPu+3A+6+cYDzIwMTkwNzExMDkyOTMwWqCBiqSB
# hzCBhDELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQ
# MA4GA1UEBwwHU2FsZm9yZDEYMBYGA1UECgwPU2VjdGlnbyBMaW1pdGVkMSwwKgYD
# VQQDDCNTZWN0aWdvIFJTQSBUaW1lIFN0YW1waW5nIFNpZ25lciAjMaCCDfowggcG
# MIIE7qADAgECAhA9GjVyMBWCYzDQE3F+gkEIMA0GCSqGSIb3DQEBDAUAMH0xCzAJ
# BgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcT
# B1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDElMCMGA1UEAxMcU2Vj
# dGlnbyBSU0EgVGltZSBTdGFtcGluZyBDQTAeFw0xOTA1MDIwMDAwMDBaFw0zMDA4
# MDEyMzU5NTlaMIGEMQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5j
# aGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRgwFgYDVQQKDA9TZWN0aWdvIExpbWl0
# ZWQxLDAqBgNVBAMMI1NlY3RpZ28gUlNBIFRpbWUgU3RhbXBpbmcgU2lnbmVyICMx
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy1FQ/1b+/HhjcAGTWp4Y
# 9DtT9gevIWz1og99HXAthHRIi5yKlQU9WYT5kYB5USzZirfBC5q6CorNZk8DiwG7
# MMqrvdvATxJe/ArM4kWwATiKu03n1BxUmO05WM9bwi9FmDEK+TU4uDEubbQeOXLh
# uCq+n4yMGqVGrgsrTJn+LEv8KLkiOmYX0KpWiiHA85YktNCFJmu68G9kmHmmrb1c
# 2FNrKwrWcoqFRuMNGAbaxntBVjabFT7xahGg92b1GNCAVWOHaGbrDnlVglyj7Um4
# cYaekzewa6PqYmyjrpbouf2Lq8b2WVsAPFcgGC1wA6ec75LreaHHXex8tI9L3+td
# /KMg3ZI45WpROmuFnEygmAhpWwbnKhnQlZOLO2uKBQkp2Nba2+Ny+lxKL3sVVoYy
# v38FCZ0tKs9Q4eZhINvHBoBcThRGvq5XcaKqbDCTHH53ywbpV82R9dUzchzh2spu
# 6/MP7Hlbuyee6B7+L/K7f+nl0GfruA18pCtZA4uV7SIozfosO8cWEa/j1rFQZ2nF
# jvV50K3/h8z4f6r5ou1h+MiNadqx9FGR62dX0WQR62TLA71JVTpFQxgsJWzRLwwt
# b/VBNSSg8mNZFl/ZpOksTtu7MRLGbfhbbgPcyxWPG41y7NsPFZDWEk7u4gAxJZM1
# b2pbpRJjQAGKuWmIOoi4DxkCAwEAAaOCAXgwggF0MB8GA1UdIwQYMBaAFBqh+GEZ
# IA/DQXdFKI7RNV8GEgRVMB0GA1UdDgQWBBRvTYYH2DInniwp0tATA4CB3QWDKTAO
# BgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEF
# BQcDCDBABgNVHSAEOTA3MDUGDCsGAQQBsjEBAgEDCDAlMCMGCCsGAQUFBwIBFhdo
# dHRwczovL3NlY3RpZ28uY29tL0NQUzBEBgNVHR8EPTA7MDmgN6A1hjNodHRwOi8v
# Y3JsLnNlY3RpZ28uY29tL1NlY3RpZ29SU0FUaW1lU3RhbXBpbmdDQS5jcmwwdAYI
# KwYBBQUHAQEEaDBmMD8GCCsGAQUFBzAChjNodHRwOi8vY3J0LnNlY3RpZ28uY29t
# L1NlY3RpZ29SU0FUaW1lU3RhbXBpbmdDQS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6
# Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4ICAQDAaO2z2NRQm+/T
# dcsPO/ck03o3RY0s7xb7UaksH7UltYqfXQvCGyB0jWYPNsuq9jYND36PS0p0Q2Ws
# DSr2Cu1rbcUJOO0AG/jl3KYKQAVH74TKCbxDZoO/n+3bjj3RQWSxcAItA1dbGG8c
# LMsesgDougkvW4EENbmpY22OCMUY0eEhrPkSChTAEtt+JZ2sHRDAWqWD0h8aZlX8
# myri7DdXjuXfljD4wJMLQxj5Am+pUa+4VwrzHAdpOY83nG3Xka6lLknpSt6z0Iy/
# OZANwIHO8CoHOgymLVHScvNTxvm97+8MaUl3nyxWxOmhCD0HrsUe1oQix7x9QxtY
# OGJO0QUlhMVC+B8v9tv6q4xU7EWKbBJNMFpS5aQXCSLm72/1X4ZD36EtvUpGkqCB
# lixhl39Ab9g/jDVaq9HGoDuFZlSA7x8a9fGbsKEnfbLnC8/2LZxYE5SphvxFUqIo
# bX90D1KRSXrpEvipO7CS/X2RFOlbbUiU8siW7gU4s8XsMD/hByAEsdiLvP2zPm/y
# AlMG9KDtyZpyo5dfAPvLY9DozXT9dcnUNkW6exJZcu3n8npQAHj4Q5pG2N+/VNRe
# scfRvBuD9CvnC+hHyFOezBqs9vqKdVNsIIWp1bhquiSOiisIkZ83BBz2b6LdNKqR
# /8YVLh5CGgkpT/TGzeKRotNADI544zCCBuwwggTUoAMCAQICEDAPb6zdZph0fKlG
# Nqd4LbkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpO
# ZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVT
# RVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmlj
# YXRpb24gQXV0aG9yaXR5MB4XDTE5MDUwMjAwMDAwMFoXDTM4MDExODIzNTk1OVow
# fTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
# A1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSUwIwYDVQQD
# ExxTZWN0aWdvIFJTQSBUaW1lIFN0YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAyBsBr9ksfoiZfQGYPyCQvZyAIVSTuc+gPlPvs1rAdtYa
# BKXOR4O168TMSTTL80VlufmnZBYmCfvVMlJ5LsljwhObtoY/AQWSZm8hq9VxEHmH
# 9EYqzcRaydvXXUlNclYP3MnjU5g6Kh78zlhJ07/zObu5pCNCrNAVw3+eolzXOPEW
# snDTo8Tfs8VyrC4Kd/wNlFK3/B+VcyQ9ASi8Dw1Ps5EBjm6dJ3VV0Rc7NCF7lwGU
# r3+Az9ERCleEyX9W4L1GnIK+lJ2/tCCwYH64TfUNP9vQ6oWMilZx0S2UTMiMPNMU
# opy9Jv/TUyDHYGmbWApU9AXn/TGs+ciFF8e4KRmkKS9G493bkV+fPzY+DjBnK0a3
# Na+WvtpMYMyou58NFNQYxDCYdIIhz2JWtSFzEh79qsoIWId3pBXrGVX/0DlULSbu
# RRo6b83XhPDX8CjFT2SDAtT74t7xvAIo9G3aJ4oG0paH3uhrDvBbfel2aZMgHEqX
# LHcZK5OVmJyXnuuOwXhWxkQl3wYSmgYtnwNe/YOiU2fKsfqNoWTJiJJZy6hGwMny
# pv99V9sSdvqKQSTUG/xypRSi1K1DHKRJi0E5FAMeKfobpSKupcNNgtCN2mu32/cY
# QFdz8HGj+0p9RTbB942C+rnJDVOAffq2OVgy728YUInXT50zvRq1naHelUF6p4MC
# AwEAAaOCAVowggFWMB8GA1UdIwQYMBaAFFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0G
# A1UdDgQWBBQaofhhGSAPw0F3RSiO0TVfBhIEVTAOBgNVHQ8BAf8EBAMCAYYwEgYD
# VR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcDCDARBgNVHSAECjAI
# MAYGBFUdIAAwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3Qu
# Y29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsG
# AQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29t
# L1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v
# b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQBtVIGlM10W4bVT
# gZF13wN6MgstJYQRsrDbKn0qBfW8Oyf0WqC5SVmQKWxhy7VQ2+J9+Z8A70DDrdPi
# 5Fb5WEHP8ULlEH3/sHQfj8ZcCfkzXuqgHCZYXPO0EQ/V1cPivNVYeL9IduFEZ22P
# sEMQD43k+ThivxMBxYWjTMXMslMwlaTW9JZWCLjNXH8Blr5yUmo7Qjd8Fng5k5OU
# m7Hcsm1BbWfNyW+QPX9FcsEbI9bCVYRm5LPFZgb289ZLXq2jK0KKIZL+qG9aJXBi
# gXNjXqC72NzXStM9r4MGOBIdJIct5PwC1j53BLwENrXnd8ucLo0jGLmjwkcd8F3W
# oXNXBWiap8k3ZR2+6rzYQoNDBaWLpgn/0aGUpk6qPQn1BWy30mRa2Coiwkud8Tle
# TN5IPZs0lpoJX47997FSkc4/ifYcobWpdR9xv1tDXWU9UIFuq/DQ0/yysx+2mZYm
# 9Dx5i1xkzM3uJ5rloMAMcofBbk1a0x7q8ETmMm8c6xdOlMN4ZSA7D0GqH+mhQZ3+
# sbigZSo04N6o+TzmwTC7wKBjLPxcFgCo0MR/6hGdHgbGpm0yXbQ4CStJB6r97DDa
# 8acvz7f9+tCjhNknnvsBZne5VhDhIG7GrrH5trrINV0zdo7xfCAMKneutaIChrop
# 7rRaALGMq+P5CslUXdS5anSevUiumDGCBBwwggQYAgEBMIGRMH0xCzAJBgNVBAYT
# AkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZv
# cmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDElMCMGA1UEAxMcU2VjdGlnbyBS
# U0EgVGltZSBTdGFtcGluZyBDQQIQPRo1cjAVgmMw0BNxfoJBCDANBglghkgBZQME
# AgEFAKCCAVswGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJ
# BTEPFw0xOTA3MTEwOTI5MzBaMC8GCSqGSIb3DQEJBDEiBCCZIBGIGZV6MacwsBSJ
# erR1ed5q8JwAeeYYzwC21fOydTCB7QYLKoZIhvcNAQkQAgwxgd0wgdowgdcwFgQU
# Jcisc05IULf42RORqBuSSTZln2EwgbwEFALWW5Xig3DBVwCV+oj5I92Tf62PMIGj
# MIGOpIGLMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNleTEUMBIG
# A1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29y
# azEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
# eQIQMA9vrN1mmHR8qUY2p3gtuTANBgkqhkiG9w0BAQEFAASCAgCfn45PfzGdHTrx
# sv+q0UIU0ug6XLI8qsbW+cCMols5bKP7r3T4KqxK8XMjYeGN2LmvprwcOQWtsGuj
# aXTk31WDncm1XTg3Mro9JRrvOYIfG7Qv0KIarqiq3VRXS7UyN+VgjVzUg04daO6W
# lG/yG+/sI5pIY5sKuq2kUyOx1pDV3Ce2XdYWdkIgoyVyBpoI4OE6eUPFC5cy+pTJ
# G1913nh9ut29uy1tOqGQOHMPqIuw7VWURajIvVr87u+bQ6JdEs7tB+v9qgyAB3Xv
# A7G2txXU5OxivCs7DonMbvotB0R7XzsVNJJ+ke3HY5roH7rdHtCJ7baz5TRkzvoH
# Z/y0pAwpj8L7trp/yJY9mm2+bGVABrgGlz+LTUzZloG/UNVhijs+y/rWcW5ezs50
# Md/YNLuEfuV4CF28NoGBto2B6oZkG2fJiWbwIVvKVdFraWCWxDPPKuFyGmRfMAiA
# V3ukFTYru8bz3vEjlSCds686wyeBwFd+Hx6sMtdbO4mnBXmJC+RbfldSZ5zgO4VZ
# HKMIr6Qd7zyxpCQ5OY8Y2HhTKDx+wkS5hxwoYCeVfCEzlM11+fn0Z/veGm1APUlw
# hHp9WQztzRakN2wiX2EyuBpHO59bP7M8vmih0L1V0lzm/ybfO/TGCqkQ9+eBY/Nl
# 0assPpfct1lgCYQYEIY55YZHeFtWdQ==
# SIG # End signature block