src/APRSMessenger.psm1

# This file is part of APRSMessenger.
#
# APRSMessenger is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# APRSMessenger is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with APRSMessenger. If not, see <https://www.gnu.org/licenses/>.

Function Send-APRSThing
{
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Low')]
    [OutputType([Void],   ParameterSetName='APRS-IS')]
    [OutputType([String], ParameterSetName='ToScreen')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $From,

        [Parameter(Mandatory, Position=1)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $To,

        [Parameter(Mandatory, Position=2)]
        [String] $Message,

        [Parameter(ParameterSetName="APRS-IS")]
        [ValidateSet(
            'asia.aprs2.net', 'aunz.aprs2.net', 'euro.aprs2.net',
            'noam.aprs2.net', 'rotate.aprs2.net', 'soam.aprs2.net'
        )]
        [String] $Server = 'rotate.aprs2.net',

        [Parameter(ParameterSetName="APRS-IS")]
        [ValidateNotNullOrEmpty()]
        [ValidateRange(1,65535)]
        [UInt16] $Port = 14580,

        [Parameter(ParameterSetName="APRS-IS")]
        [Switch] $Force
    )

    #region Handle group bulletins correctly
    # Group bulletins need to be sent to BLNn, with the
    # group name included just before the message itself.
    $MsgTo = $To
    If ($To -Match "^BLN[0-9]")
    {
        $To = "BLN" + $To[3]
    }
    #endregion

    $ToSend = "$From>$To,TCPIP*::$($MsgTo.PadRight(9)):$Message"

    If ($PSCmdlet.ParameterSetName -ne 'APRS-IS')
    {
        Return $ToSend
    }
    Else
    {
        #region Create user agent and prepare to sign onto the server
        $ThisModuleName    = $MyInvocation.MyCommand.Module.Name
        $ThisModuleVersion = $MyInvocation.MyCommand.Module.Version
        $Greeting = "user $From pass $(Get-APRSISPasscode $From) vers $ThisModuleName $ThisModuleVersion"
        #endregion

        If ($Force -or $PSCmdlet.ShouldProcess("Send a message to $To via APRS-IS", $To, 'Send a message'))
        {
            Try
            {
                $socket = [Net.Sockets.TcpClient]::new($Server, $Port)
                $stream = $socket.GetStream()

                Write-Debug "Sending a greeting to APRS: $Greeting"
                $writer = [IO.StreamWriter]::new($stream)
                $writer.WriteLine("$Greeting`n")
                $writer.Flush()

                Write-Debug 'Waiting for a response'
                $bufsize = 256
                $buffer = New-Object -TypeName Byte[] -ArgumentList $bufsize
                $null = $stream.Read($buffer, 0, $bufsize)

                Write-Debug "Sending packet: $ToSend"
                $writer.WriteLine("$ToSend`n")
                $writer.Flush()
            }
            Catch [IO.IOException] {
                Write-Error "Could not connect to ${Server}:$Port"
            }
            Finally {
                If ($null -ne $writer) {
                    $writer.Close()
                }
                If ($null -ne $stream) {
                    $stream.close()
                }
            }
        }
    }
}

Function Send-APRSMessage
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ToScreen', ConfirmImpact='Low')]
    [OutputType([Void],   ParameterSetName='APRS-IS')]
    [OutputType([String], ParameterSetName='ToScreen')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $From,

        [Parameter(Mandatory, Position=1)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $To,

        [Parameter(Mandatory, Position=2, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('InputObject', 'MessageText', 'Text')]
        [ValidateLength(0,67)]
        [ValidatePattern("[^~\|\{]*")]
        [String] $Message,

        [Alias('Acknowledgment', 'Acknowledgement')]
        [ValidateLength(1,5)]
        [String] $Acknowledge,

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateSet(
            'asia.aprs2.net', 'aunz.aprs2.net', 'euro.aprs2.net',
            'noam.aprs2.net', 'rotate.aprs2.net', 'soam.aprs2.net'
        )]
        [String] $Server = 'rotate.aprs2.net',

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateNotNullOrEmpty()]
        [ValidateRange(1,65535)]
        [UInt16] $Port = 14580,

        [Parameter(ParameterSetName='APRS-IS')]
        [Switch] $Force
    )

    $ToSend = $Message
    If ($Acknowledge)
    {
        $ToSend += "{$Acknowledge"
    }

    $Arguments = @{
        'From' = $From
        'To' = $To
        'Message' = $ToSend
        'WhatIf' = $WhatIfPreference
        'Verbose' = $VerbosePreference
        'Debug' = $DebugPreference
    }
    If ($PSCmdlet.ParameterSetName -eq 'APRS-IS') {
        $Arguments.Server = $Server
        $Arguments.Port = $Port
        $Arguments.Force = $Force
        Write-Debug "From:$From To:$To Msg:$ToSend - sending to ${Server}:$Port (Force:$Force)"
    }
    Else {
        Write-Debug "From:$From To:$To Msg:$ToSend - printing to screen"
    }
    Return (Send-APRSThing @Arguments)
}

Function Send-APRSBulletin
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ToScreen', ConfirmImpact='Low')]
    [Alias('Send-APRSGeneralBulletin')]
    [OutputType([Void],   ParameterSetName='APRS-IS')]
    [OutputType([String], ParameterSetName='ToScreen')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $From,

        [Parameter(Mandatory, Position=1)]
        [ValidateRange(0,9)]
        [UInt] $BulletinID,

        [Parameter(Mandatory, Position=2, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('InputObject', 'MessageText', 'Text')]
        [ValidateLength(0,67)]
        [ValidatePattern("[^~\|]*")]
        [String] $Message,

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateSet(
            'asia.aprs2.net', 'aunz.aprs2.net', 'euro.aprs2.net',
            'noam.aprs2.net', 'rotate.aprs2.net', 'soam.aprs2.net'
        )]
        [String] $Server = 'rotate.aprs2.net',

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateNotNullOrEmpty()]
        [ValidateRange(1,65535)]
        [UInt16] $Port = 14580,

        [Parameter(ParameterSetName='APRS-IS')]
        [Switch] $Force
    )

    $To = "BLN$BulletinID"

    $Arguments = @{
        'From' = $From
        'To' = $To
        'Message' = $Message
        'WhatIf' = $WhatIfPreference
        'Verbose' = $VerbosePreference
        'Debug' = $DebugPreference
    }
    If ($PSCmdlet.ParameterSetName -eq 'APRS-IS') {
        $Arguments.Server = $Server
        $Arguments.Port = $Port
        $Arguments.Force = $Force
        Write-Debug "From:$From To:$To Msg:$Message - sending to ${Server}:$Port (Force:$Force)"
    }
    Else {
        Write-Debug "From:$From To:$To Msg:$Message - printing to screen"
    }
    Return (Send-APRSThing @Arguments)
}

Function Send-APRSGroupBulletin
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ToScreen', ConfirmImpact='Low')]
    [Alias('Send-APRSGeneralBulletin')]
    [OutputType([Void],   ParameterSetName='APRS-IS')]
    [OutputType([String], ParameterSetName='ToScreen')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $From,

        [Parameter(Mandatory, Position=1)]
        [ValidateRange(0,9)]
        [UInt] $BulletinID,

        [Parameter(Mandatory, Position=2)]
        [ValidateLength(0,5)]
        [String] $GroupName,

        [Parameter(Mandatory, Position=3, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('InputObject', 'MessageText', 'Text')]
        [ValidateLength(0,67)]
        [ValidatePattern("[^~\|]*")]
        [String] $Message,

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateSet(
            'asia.aprs2.net', 'aunz.aprs2.net', 'euro.aprs2.net',
            'noam.aprs2.net', 'rotate.aprs2.net', 'soam.aprs2.net'
        )]
        [String] $Server = 'rotate.aprs2.net',

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateNotNullOrEmpty()]
        [ValidateRange(1,65535)]
        [UInt16] $Port = 14580,

        [Parameter(ParameterSetName='APRS-IS')]
        [Switch] $Force
    )

    $To = "BLN$BulletinID$GroupName"

    $Arguments = @{
        'From' = $From
        'To' = $To
        'Message' = $Message
        'WhatIf' = $WhatIfPreference
        'Verbose' = $VerbosePreference
        'Debug' = $DebugPreference
    }
    If ($PSCmdlet.ParameterSetName -eq 'APRS-IS') {
        $Arguments.Server = $Server
        $Arguments.Port = $Port
        $Arguments.Force = $Force
        Write-Debug "From:$From To:$To Msg:$Message - sending to ${Server}:$Port (Force:$Force)"
    }
    Else {
        Write-Debug "From:$From To:$To Msg:$Message - printing to screen"
    }
    Return (Send-APRSThing @Arguments)
}

Function Send-APRSAnnouncement
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ToScreen', ConfirmImpact='Low')]
    [OutputType([Void],   ParameterSetName='APRS-IS')]
    [OutputType([String], ParameterSetName='ToScreen')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $From,

        [Parameter(Mandatory, Position=1)]
        [ValidatePattern('[A-Z]')]
        [Char] $AnnouncementID,

        [Parameter(Mandatory, Position=2, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('InputObject', 'MessageText', 'Text')]
        [ValidateLength(0,67)]
        [ValidatePattern("[^~\|]*")]
        [String] $Message,

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateSet(
            'asia.aprs2.net', 'aunz.aprs2.net', 'euro.aprs2.net',
            'noam.aprs2.net', 'rotate.aprs2.net', 'soam.aprs2.net'
        )]
        [String] $Server = 'rotate.aprs2.net',

        [Parameter(ParameterSetName='APRS-IS')]
        [ValidateNotNullOrEmpty()]
        [ValidateRange(1,65535)]
        [UInt16] $Port = 14580,

        [Parameter(ParameterSetName='APRS-IS')]
        [Switch] $Force
    )

    $To = "BLN$AnnouncementID"

    $Arguments = @{
        'From' = $From
        'To' = $To
        'Message' = $Message
        'WhatIf' = $WhatIfPreference
        'Verbose' = $VerbosePreference
        'Debug' = $DebugPreference
    }
    If ($PSCmdlet.ParameterSetName -eq 'APRS-IS') {
        $Arguments.Server = $Server
        $Arguments.Port = $Port
        $Arguments.Force = $Force
        Write-Debug "From:$From To:$To Msg:$Message - sending to ${Server}:$Port (Force:$Force)"
    }
    Else {
        Write-Debug "From:$From To:$To Msg:$Message - printing to screen"
    }
    Return (Send-APRSThing @Arguments)
}

Function Get-APRSISPasscode
{
    [CmdletBinding()]
    [Alias('Get-APRSISPassword')]
    [OutputType([UInt16])]
    Param(
        [Parameter(Mandatory, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Call', 'InputObject')]
        [ValidatePattern('^[A-Z0-9]{3,}(?:\-(?:[0-9]|1[0-5]))?$')]
        [String] $Callsign
    )

    #region Remove SSID from callsign
    $pos = $Callsign.IndexOf('-')
    If ($pos -ne -1) {
        $Callsign = $Callsign.Substring(0, $pos)
    }
    #endregion

    #region Calculate APRS-IS passcode
    # This function was adapted from PHP code by Peter Goodhall, 2M0SQL.
    # His project is not licensed, so I am using this code in good faith,
    # under the assumption that it was released into the public domain.
    # You can find it at https://github.com/magicbug/PHP-APRS-Passcode
    $Hash = [UInt16]0x73E2
    For ($i = 0; $i -lt $Callsign.Length; $i += 2)
    {
        If ($i -lt $Callsign.Length - 1)
        {
            $Upper,$Lower = [Text.Encoding]::ASCII.GetBytes($Callsign.Substring($i,2))
        }
        Else
        {
            $Upper = [Text.Encoding]::ASCII.GetBytes($Callsign[$i])[0]
            $Lower = 0
        }

        [UInt16]$ToHash = $Upper
        $ToHash = $ToHash -shl 8
        $ToHash = $ToHash -bor $Lower

        Write-Debug "Hash = $(Format-AsHex $Hash) -bxor $(Format-AsHex $ToHash)"
        $Hash = $Hash -bxor $ToHash
    }
    Write-Debug "Hash = $(Format-AsHex $Hash) -band 0x7FFF"
    $Hash = $Hash -band 0x7FFF
    Write-Debug "Hash is $(Format-AsHex $Hash) = $Hash"
    #endregion

    Write-Verbose "Your APRS-IS username is $Callsign, and your password is $Hash."
    Return $Hash
}

# This is a helper function used to generate debugging output inside the
# previous function.
Function Format-AsHex
{
    [OutputType([String])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [UInt16] $Number
    )

    Return "0x$('{0:X}' -f $Number)"
}

# SIG # Begin signature block
# MIIo5AYJKoZIhvcNAQcCoIIo1TCCKNECAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDkaSgtwPk7QWA/
# hW15n2s/mTRIe0BHK3wVcm8zyDNpVaCCI6swggR+MIIC5qADAgECAhEApna5vdQ8
# txEq0UQhUxLsMzANBgkqhkiG9w0BAQwFADBBMQswCQYDVQQGEwJVUzEQMA4GA1UE
# ChMHQ2VydGVyYTEgMB4GA1UEAxMXQ2VydGVyYSBDb2RlIFNpZ25pbmcgQ0EwHhcN
# MjIxMTI1MDAwMDAwWhcNMjUxMTI0MjM1OTU5WjBPMQswCQYDVQQGEwJVUzEUMBIG
# A1UECAwLQ29ubmVjdGljdXQxFDASBgNVBAoMC0NvbGluIENvZ2xlMRQwEgYDVQQD
# DAtDb2xpbiBDb2dsZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIS0nGDy1zQpFKyt
# Jcg1PiDfvpNR79NCbfgewfNj/SLANVb3XbggjeibCl1fcefKLnXFv0DXHIKjYg0e
# hcFMbUQ1hqpwnnWQji1DcLeshAMdvWmTguYmtL6P4ik/BQDUuaOCAY8wggGLMB8G
# A1UdIwQYMBaAFP7HyA+eaTU9w8t0+WyaszQGqVwJMB0GA1UdDgQWBBSO8z1ie4Xj
# RAjUjX9ctrNH9aglYzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADATBgNV
# HSUEDDAKBggrBgEFBQcDAzBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgJlMCUwIwYI
# KwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAEEATBIBgNV
# HR8EQTA/MD2gO6A5hjdodHRwOi8vQ2VydGVyYS5jcmwuc2VjdGlnby5jb20vQ2Vy
# dGVyYUNvZGVTaWduaW5nQ0EuY3JsMIGABggrBgEFBQcBAQR0MHIwQwYIKwYBBQUH
# MAKGN2h0dHA6Ly9DZXJ0ZXJhLmNydC5zZWN0aWdvLmNvbS9DZXJ0ZXJhQ29kZVNp
# Z25pbmdDQS5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9DZXJ0ZXJhLm9jc3Auc2Vj
# dGlnby5jb20wDQYJKoZIhvcNAQEMBQADggGBAAslTgxzcZ0FYetE3IOghFsEtGV+
# yEM03ZrGFRGt7/DmHe4MK15XUsORJzN60eyNzxchQhV1S90jqQflkl6ImuvdaRve
# 586ZhYtW4tl2+2YbM26jwVqB9tT06W1SHb03+Vb29jjRbp5r+w3lEXxzGC660MFk
# 1L8kRQcqKjt0izVeVm6qKfNVQyak5xWpeX8n8NVaCqVWfijWlLDr8Ydeg9XeJy4H
# c9OweQ7+seRJzr/MgHQ0SFuXaRrbk0v5UmyoH83LZt/qo+XnrU+XeX870UVxucTl
# AitkDB6t/dvmetmXQGE5stJMyIK5jgtMqQ/q/GIrTFYMmcAsXxNQh8uv+jFa0HhF
# PZVhhdRbximJQUPyKb7IMuAzwdw1jrTcAF1FbkLlHXdu7dohbSfsN8ZA5Cr397wN
# n7UBs939mMBb4ZR+nBPFhibj5RISssbICi8z3LNb6CNuayOn3PtG/NRcf5T8iFyW
# /XbipYDJcxuQKwP8HWmlVIfQooRP6HR+Doee+DCCBY0wggR1oAMCAQICEA6bGI75
# 0C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG
# A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAw
# MFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD
# ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGln
# aUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuE
# DcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNw
# wrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs0
# 6wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e
# 5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtV
# gkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85
# tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+S
# kjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1Yxw
# LEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzl
# DlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFr
# b7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATow
# ggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiu
# HA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQE
# AwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
# Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2
# hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290
# Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/
# Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNK
# ei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHr
# lnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4
# oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5A
# Y8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNN
# n3O3AamfV6peKOK5lDCCBd4wggPGoAMCAQICEAH9bTD8o8pRqBu8ZA41Ay0wDQYJ
# KoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
# MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
# ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0
# aG9yaXR5MB4XDTEwMDIwMTAwMDAwMFoXDTM4MDExODIzNTk1OVowgYgxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJzZXkgQ2l0
# eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQDEyVVU0VS
# VHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sIs9CsVw127c0n00ytUINh4qogTQkt
# ZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnGvDoZtF+mvX2do2NCtnbyqTsrkfji
# b9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQIjy8/hPwhxR79uQfjtTkUcYRZ0YI
# UcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfbIWax1Jt4A8BQOujM8Ny8nkz+rwWW
# NR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0tyA9yn8iNK5+O2hmAUTnAU5GU5sz
# YPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97Exwzf4TKuzJM7UXiVZ4vuPVb+DNBp
# DxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNVicQNwZNUMBkTrNN9N6frXTpsNVzb
# QdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5D9kCnusSTJV882sFqV4Wg8y4Z+Lo
# E53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJWBp/kjbmUZIO8yZ9HE0XvMnsQybQ
# v0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ5lhCLkMaTLTwJUdZ+gQek9QmRkpQ
# gbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzGKAgEJTm4Diup8kyXHAc/DVL17e8v
# gg8CAwEAAaNCMEAwHQYDVR0OBBYEFFN5v1qqK0rPVIDh2JvAnfKyA2bLMA4GA1Ud
# DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQBc
# 1HwNz/cBfUGZZQxzxVKfy/jPmQZ/G9pDFZ+eAlVXlhTxUjwnh5Qo7R86ATeidvxT
# UMCEm8ZrTrqMIU+ijlVikfNpFdi8iOPEqgv976jpS1UqBiBtVXgpGe5fMFxLJBFV
# /ySabl4qK+4LTZ9/9wE4lBSVQwcJ+2Cp7hyrEoygml6nmGpZbYs/CPvI0UWvGBVk
# kBIPcyguxeIkTvxY7PD0Rf4is+svjtLZRWEFwZdvqHZyj4uMNq+/DQXOcY3mpm8f
# bKZxYsXY0INyDPFnEYkMnBNMcjTfvNVx36px3eG5bIw8El1l2r1XErZDa//l3k1m
# EVHPma7sF7bocZGM3kn+3TVxohUnlBzPYeMmu2+jZyUhXebdHQsuaBs7gq/sg2eF
# 1JhRdLG5mYCJ/394GVx5SmAukkCuTDcqLMnHYsgOXfc2W8rgJSUBtN0aB5x3AD/Q
# 3NXsPdT6uz/MhdZvf6kt37kC9/WXmrU12sNnsIdKqSieI47/XCdr4bBP8wfuAC7U
# WYfLUkGV6vRH1+5kQVV8jVkCld1incK57loodISlm7eQxwwH3/WJNnQy1ijBsLAL
# 4JxMwxzW/ONptUdGgS+igqvTY0RwxI3/LTO6rY97tXCIrj4Zz0Ao2PzIkLtdmSL1
# UuZYxR+IMUPuiB3Xxo48Q2odpxjefT0W8WL5ypCo/TCCBjwwggQkoAMCAQICECFm
# 8IpR6/yrzI9EMJGpSw4wDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UE
# ChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNB
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIyMDkwNzAwMDAwMFoXDTMyMDkw
# NjIzNTk1OVowQTELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0NlcnRlcmExIDAeBgNV
# BAMTF0NlcnRlcmEgQ29kZSBTaWduaW5nIENBMIIBojANBgkqhkiG9w0BAQEFAAOC
# AY8AMIIBigKCAYEAvp9xPhzayPelQMu7ycbIP8Kls73mzciRa7hO+f06rZl7Xw4F
# DKuA1Cu7nen1GFCPuqRvCqEizDiO4/WnM4nQcfVFkfpXfZf24qUztHzq5qsxlwpK
# W/Dkksj+I9A15W1dFbmToYswFElXzmKHSnZXoYMz+R4ZSwmnVB/XsvUPaAFi2dCr
# KN54pMcsBweUOKFunKWkji/MMnnPJGebOF1fLeDgyEHQvYuzlVfOWU3xjMiZYfqY
# gi8jo28qa0IYR17SdFZIgUWRlKhJnNKwyXfY8kElpfpeSbjM20jLch1+UhPXwTU/
# 5yHwXvUCSW4idXEihxbcleNXbeO8wfwfNHn2of4Y1w4mShxHFhDu/kPmzDIkpPct
# AmDyJfJfcL1E+aRFqGYhJwCOiMNQE9dfDkYL11Rtue3zmcpkqKbH6P6EI3UQSG1t
# H0OqY65xpSadXS/yGoXqOOEQpDf/U3trlyqroxhUhm0dN82CBqSXqMa23scYns1O
# 3u2kSPPHIEULOVq5AgMBAAGjggFmMIIBYjAfBgNVHSMEGDAWgBRTeb9aqitKz1SA
# 4dibwJ3ysgNmyzAdBgNVHQ4EFgQU/sfID55pNT3Dy3T5bJqzNAapXAkwDgYDVR0P
# AQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUH
# AwMwIgYDVR0gBBswGTANBgsrBgEEAbIxAQICZTAIBgZngQwBBAEwUAYDVR0fBEkw
# RzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUNl
# cnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHEGCCsGAQUFBwEBBGUwYzA6BggrBgEF
# BQcwAoYuaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUFBQUNB
# LmNydDAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkq
# hkiG9w0BAQwFAAOCAgEAe11w9/hMUEgtubZdffaBE4vbRYL0hunnc2Yaup6rzig/
# GjVOaTA7gdoChGhuxDE0AoYMF1znfLBSuNrU6B8tO/ikxFprLayPz9IUmbhEd/Ry
# VbMimZiC7z74OfjIVx86Y279nJ0VmX6lgHvwc8QcAVMN00Qse97OD9EeWMuY+hB7
# 1mKUp6pTipoqKJD4+hs2fOxjXew9OBYu6wjlgK6kbuBo+R2T7EuYyyfWubg9Cpwg
# dzRSpWmRO5DMG+u0FojEtP8MITbtJ1bLOWZ0JVvGKDWqNLVBvxHE8DwaAx3IrlZ8
# 1lxLO3zEL/mpUnC6cdQlVkq3G7qdWfIdkaNhNAv3hu0tH3t8bLoXYDB6Kyp5hdGZ
# 1XAO7H4b7MVW1amciuBXys6/VvfWmR/9Wh1rjWuYtP+y94oLg1gEisa7+Qid2qy/
# WSKC7cjpzwmg+6BGb2oEAO56pZToRc5a8vE9XcMPMO6hxI+MGbpqioQ/Nwa+94Ep
# D2aGUkmqX3gP6kUBbvS4Pys0jLgKxlyZDfwJb+4CWQOoZaiZoLAr/Y9+9j2YkeQD
# rt1A2zEDgOHRLlXYQDPuVNSu014pt8yAMY1OnHQSrTKwBZ2Y5H8AOw1yyIsMQISq
# OcPiepvzMAwSMJtTedvFq51+kuBHgltH2AdDlPfT13i3CAqn3LcFhehUZU4VIPsw
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGwDCCBKigAwIBAgIQ
# DE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAw
# MDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp
# Z2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSF
# dDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWv
# M+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyI
# xvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3U
# TZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyV
# R4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQ
# ln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq
# 5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk
# +lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl
# 5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7
# ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076
# XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn
# gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP
# nshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+g
# TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS
# U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB
# gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF
# BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk
# RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA
# A4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4
# iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIr
# UPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk
# /9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+Y
# siaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YB
# ZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD
# +5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQ
# RWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+b
# vdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTo
# ntRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/
# Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDGCBI8wggSLAgEB
# MFYwQTELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0NlcnRlcmExIDAeBgNVBAMTF0Nl
# cnRlcmEgQ29kZSBTaWduaW5nIENBAhEApna5vdQ8txEq0UQhUxLsMzANBglghkgB
# ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ
# AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G
# CSqGSIb3DQEJBDEiBCDZZqkWOJCeQlzEpNqVtUyf361nUwGRf3TUuI4vgEPh/jAL
# BgcqhkjOPQIBBQAEZzBlAjB7w4LFHTNYFgyA7b30ypnkeRmtRVGhgSq7ajLSUzKb
# W8KJxaqka05Sfx0zjv82swgCMQD4rt1QOGzA9IVGSTvFueFdhT4g05+fIMc4KGOJ
# yKAyMessFhaXHOMR59tXrOyNqBahggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkC
# AQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5
# BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0
# YW1waW5nIENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJ
# KoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMwMzIxMTA0
# MzAzWjAvBgkqhkiG9w0BCQQxIgQg+OVrbAZpIIZvI+fp5Bwz50CROJkONM0wq98f
# VRQi58EwDQYJKoZIhvcNAQEBBQAEggIAEg8y8pIIW7jplL2JMXrUIFVHnQ8Rwmla
# 5EokJeUQVg2VP+LZp62W3dGIunmDbAXDW2zFNOSNEkWd+vwtRf/Hg/ceJK+Ck0Ik
# d8Q9uj+IwwEYdLlf5ijsJLXhKWK6ixl2XwC9Qh7M+FhHWb6Bsj4srrTsMesz8MeB
# r5fTOMloBQGDMw2BJa2bT1uv4zUfoLaTBYA/IJXy2ExJkwVSEHwBVRu/+31NjSg3
# tC3Iz3kHwHwNdqTjv56MdNn2PeDuwIsMToc1aVo0VKH6fnfRPTcFPTgPw2TE0spG
# 1ILTv76t7qpQ+4wD28lxvBr7KGe3wIFbrgQm43JBlIN6SX64uDRvDgxQqEYNj50q
# CDzEiSQyxTUCXuvEP+slXQcsaHweietq0bY8L18C+bMZ+VI5D8hZaBkuks1kMLoU
# MBcdXsBlLKi0VzneH9c2Lk/DlNObZz6GVmN4XCJMDaVjlxRalDBx8GVCA7AbUquE
# KeCxAoy3sfP5SoIq4kSIedsny8qZ49PJ6sMxqSXUNUTCvQ6FeoZPS3PGLR7guKt6
# +9DXKpmuaU+vtfktGTnU28bcdKvGOXkOE88LxAQ1Rsi8eVOHQpGKhvzfJEd2wuYk
# zwAXFJTJKCsxsDrB8VygbEtcUl/XRgANWpvUg7FCqF7bEFx8kVHVtsbZLUPnKGWP
# zH6qUXT8JHY=
# SIG # End signature block