src/PSGopher.psm1
<# PSGopher -- a PowerShell client for Gopher and Gopher+ servers. Copyright (C) 2021-2023 Colin Cogle <colin@colincogle.name> This program 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. This program 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 this program. If not, see <https://www.gnu.org/licenses/>. #> #Requires -Version 7.1 Function Invoke-GopherRequest { [CmdletBinding(DefaultParameterSetName='ToScreen')] [OutputType([PSCustomObject], ParameterSetName='ToScreen')] [OutputType([Void], ParameterSetName='OutFile')] [Alias('igr')] Param( [Parameter(Mandatory, Position=0)] [Alias('Url')] [ValidateNotNullOrEmpty()] [ValidatePattern('^(gophers?|sgopher|gopher\+tls):\/\/')] [Uri] $Uri, [Alias('UseTLS', 'RequireTLS', 'RequireSSL')] [Switch] $UseSSL, [Alias('TryTLS', 'OpportunisticTLS', 'OpportunisticSSL')] [Switch] $TrySSL, [Alias('Abstract','Admin','Attributes','Information')] [Switch] $Info, [ValidatePattern("[a-z]+\/.+")] [AllowNull()] [String[]] $Views, [ValidateSet('ASCII','UTF7','UTF8','UTF16','Unicode','UTF32')] [String] $Encoding = 'UTF8', [Parameter(ParameterSetName='OutFile')] [ValidateNotNullOrEmpty()] [String] $OutFile, [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('Post','PostData','Query','QueryString')] [AllowNull()] [String] $InputObject ) #region Establish TCP connection. # If we have a secure URL scheme, set UseSSL to true. $UseSSL = $UseSSL -or ($Uri.Scheme -In @('gophers','sgopher','gopher+tls')) # Sometimes, the .NET runtime doesn't recognize which port we're supposed # to be using -- especially if we use a Gopher-TLS scheme for a secure # connection. If so, we need to make a new URL with the port defined. If ($Uri.Port -eq -1) { $Path, $Query = $Uri.PathAndQuery -Split '\?',2 $Uri = [Uri]::new("$($Uri.Scheme)://$($Uri.Host):70$Path$($Query ? "?$Query" : '')") # New URL = ___ Write-Debug (Get-MessageTranslation 1 $Uri) } If ($UseSSL -ne $true) { # "Connecting to ___." Write-Verbose (Get-MessageTranslation 2 $Uri.Host) } Else { # "Connecting to ___ securely." Write-Verbose (Get-MessageTranslation 3 $Uri.Host) } Try { $TcpSocket = [Net.Sockets.TcpClient]::new($Uri.Host, $Uri.Port ?? 70) $TcpStream = $TcpSocket.GetStream() $TcpStream.ReadTimeout = 2000 #milliseconds If ($UseSSL -or $TrySSL) { # "Upgrading connection to TLS." Write-Debug (Get-MessageTranslation 4) $secureStream = [Net.Security.SslStream]::new($TcpStream, $false) $secureStream.AuthenticateAsClient($Uri.Host) $TcpStream = $secureStream # Connected with <TLS version> using <cipher/ciphersuite>". Write-Debug (Get-MessageTranslation 5 @($TcpStream.SslProtocol, $TcpStream.NegotiatedCipherSuite)) } } Catch { # If we're using -TrySSL, then we'll retry without encryption. Else,s # If we're using -UseSSL, then fail the connection. If ($UseSSL) { # Throw a non-terminating error so that $? is set properly and the # pipeline can continue. This will allow chaining operators to work as # intended. Should a future version of this module support pipeline # input, that will let this cmdlet keep running with other input URIs. $er = [Management.Automation.ErrorRecord]::new( # "Could not connect to {host}:{port} with SSL/TLS. Aborting." [Net.WebException]::new((Get-MessageTranslation 7 @($Uri.Host, $Uri.Port ?? 70))), 'TlsConnectionFailed', [Management.Automation.ErrorCategory]::ConnectionError, $Uri ) $er.CategoryInfo.Activity = 'NegotiateTlsConnection' $PSCmdlet.WriteError($er) Return $null } ElseIf ($TrySSL) { # "Could not connect to {host}:{port} with SSL/TLS. Retrying with a non-secured connection." Write-Verbose (Get-MessageTranslation 8 @($Uri.Host, $Uri.Port ?? 70)) $NewParameters = @{ 'Uri' = $Uri 'Info' = $Info 'Views' = $Views ?? @() # not sure why this is needed 'Encoding' = $Encoding 'InputObject' = $InputObject 'TrySSL' = $null 'UseSSL' = $null } If ($PSCmdlet.ParameterSetName -eq 'OutFile') { $NewParameters.'OutFile' = $OutFile } Remove-Variable -Name 'SecureStream' -Force Return (Invoke-GopherRequest @NewParameters) Exit } Else { # "Could not connect to {host}:{port}. Aborting." Write-Error (Get-MessageTranslation 6 @($Uri.Host, $Uri.Port ?? 70)) Return $null } } #endregion (Establish TCP connection) #region Content type negotiation $ContentTypeExpected = $null # If the user provided one, we'll use that. # But it needs to be removed from the URI. If ($Uri.PathAndQuery -CMatch "^\/[0123456789+gIT:;<dhis]") { $ContentTypeExpected = $Uri.PathAndQuery[1] # The code may have removed the leading slash. Put that back. # If there was already one, remove it. $Path = $Uri.PathAndQuery.Substring(2) $Path = "/$Path" -Replace '//','/' Write-Debug (Get-MessageTranslation 9 @($Uri.PathAndQuery, $Path)) $Uri = [Uri]::new("$($Uri.Scheme)://$($Uri.Host):$($Uri.Port)$Path") } # Otherwise, let's try and guess -- if we have a file extension. ElseIf ($Uri.AbsolutePath -Match '\.') { $ContentTypeExpected = (Get-GopherType ($Uri.AbsolutePath -Split '\.')[-1] -Verbose:$VerbosePreference -Debug:$DebugPreference) } # If we still can't figure it out after all this, assume it's a Gopher menu. $ContentTypeExpected ??= '1' # Determine if we're reading a binary file or text. $BINARY_TRANSFER = (-Not $Info) -and ($ContentTypeExpected -In @('4','5','9','g','I',':',';','<','d','s') ) #endregion (Content type negotiation) #region Parse input parameters If ($null -eq $InputObject -or $InputObject.Length -eq 0) { # "No additional query string detected" Write-Debug (Get-MessageTranslation 10) } Else { # "Found additional query string=___" Write-Debug (Get-MessageTranslation 11 $InputObject) $Encoder = [Web.HttpUtility]::ParseQueryString('') $Encoder.Add($null, $InputObject) $EncodedInput = $Encoder.ToString() -Replace '\+','%20' # Gopher requires URL (percent) encoding for spaces. # "Encoded additional query string=___" Write-Debug (Get-MessageTranslation 12 $EncodedInput) # If there was already a query string specified in the URL, we will send # both of them, with the URL taking precedence. If ($Uri.Query) { # "Found existing query string=___" Write-Debug (Get-MessageTranslation 13 $Uri.Query) } $Uri = [Uri]::new($Uri.ToString() + ($Uri.Query ? '&' : '?') + $EncodedInput) } #endregion #region Send request $ToSend = $Uri.PathAndQuery If ($Info) { If ($ContentTypeExpected -eq '1') { $ToSend += "`t$" } Else { $ToSend += "`t!" } } If ($Views) { $ToSend += "`t+$Views" } $ToSend += "`r`n" # "Sending ___ bytes to server: ___" Write-Debug (Get-MessageTranslation 14 @($ToSend.Length, ($ToSend -Replace "`r",'\r' -Replace "`n",'\n' -Replace "`t",'\t'))) $writer = [IO.StreamWriter]::new($TcpStream) $writer.WriteLine($ToSend) $writer.Flush() #endregion (Send request) #region Receive data # Set text encoding for reading and writing textual output. If (-Not $BINARY_TRANSFER) { Switch ($Encoding) { 'ASCII' {$Encoder = [Text.AsciiEncoding]::new()} 'UTF7' {$Encoder = [Text.UTF7Encoding]::new()} 'UTF8' {$Encoder = [Text.UTF8Encoding]::new()} 'UTF16' {$Encoder = [Text.UnicodeEncoding]::new()} 'Unicode' {$Encoder = [Text.UnicodeEncoding]::new()} 'UTF32' {$Encoder = [Text.UTF32Encoding]::new()} default {Throw [NotImplementedException]::new((Get-MessageTranslation 15))} } } # Read the full response. $response = ($BINARY_TRANSFER ? [IO.MemoryStream]::new() : '') $BufferSize = 102400 # 100 KB, more than enough for text, but a sizable # buffer to make binary transfers fast. $buffer = New-Object Byte[] $BufferSize If (-Not $BINARY_TRANSFER) { If ($Info) { # "Beginning to read (attributes)" Write-Debug (Get-MessageTranslation 16) } Else { # "Beginning to read (textual type ___)" Write-Debug (Get-MessageTranslation 17 $ContentTypeExpected) } While (0 -ne ($bytesRead = $TcpStream.Read($buffer, 0, $BufferSize))) { # <TAB> "Reading ≤___ bytes from the server." Write-Debug "`t$(Get-MessageTranslation 18 $BufferSize)" $response += $Encoder.GetString($buffer, 0, $bytesRead) } # "Received ___ bytes from server." Write-Verbose (Get-MessageTranslation 19 $Encoder.GetByteCount($response)) } Else # it is a binary transfer # { # "Beginning to read (binary type ___)." Write-Debug (Get-MessageTranslation 20 $ContentTypeExpected) While (0 -ne ($bytesRead = $TcpStream.Read($buffer, 0, $BufferSize))) { # <TAB> "Reading ≤___ bytes from the server." Write-Debug "`t$(Get-MessageTranslation 18 $BufferSize)" Write-Debug "`tGot $bytesRead bytes" $response.Write($buffer, 0, $bytesRead) } $response.Flush() # "Received ___ bytes from server." Write-Verbose (Get-MessageTranslation 19 $response.Length) } #endregion (Receive data) # Close connections. Write-Debug (Get-MessageTranslation 21) $writer.Close() $TcpSocket.Close() #region Parse response $Content = '' $Links = @() # Check for errors. All errors begin with '3'. If ( ` ($BINARY_TRANSFER -and $response.ToArray()[0] -eq 51) -or ` (-Not $BINARY_TRANSFER -and $response[0] -eq '3' -and $response -CLike '*error.host*') ` ) { If ($BINARY_TRANSFER) { $response = [Text.Encoding]::ASCII.GetString($response.ToArray()) } Write-Error -Message ($response.Substring(1, $response.IndexOf("`t"))) -TargetObject $Uri -ErrorId 3 -Category 'ResourceUnavailable' Return $null } # If this is not a Gopher menu, then simply return the raw output. ElseIf ($BINARY_TRANSFER) { If (-Not $Views) { $Content = $response.ToArray() } Else { # A Views query will include a plus sign, the file size, and then a # \r\n. Remove that header from the Content. $arr = $response.ToArray() $dataStarts = 0 If ($arr[0] -eq 43) <# a plus sign #> { For ($i = 0; $i -lt $arr.Length; $i++) { If ($arr[$i] -eq 13 -and $arr[$i + 1] -eq 10) { $dataStarts = $i + 2 Break } } } $Content = $arr[$dataStarts..($arr.Length)] } } # If this is anything non-binary and not a menu, simply return it. ElseIf ($ContentTypeExpected -ne '1') { If (-Not $Views) { $Content = $response } Else { $Content = ($response -Split "`r`n",2)[1] } } Else { $response -Split "(`r`n)" | ForEach-Object { # Show the output Write-Debug (Get-MessageTranslation 22 ($_ -Replace "`r",'' -Replace "`n",'')) # Build Content variable If ($_.Length -gt 0) { $Content += ($_.Substring(1) -Split "`t")[0] } Else { $Content += "`r`n" } # Look for links or errors. However, we can skip this if we're using # the -OutFile or -Info parameters, because no link objects are returned. If (-Not $OutFile -and -Not $Info -and $_ -Match "`t") { $line = $_ Switch -CaseSensitive -RegEx ($_[0]) { 'i' { Break } default { $result = Convert-GopherLink $line -Server $Uri.Host -Port $Uri.Port -Verbose:$VerbosePreference -Debug:$DebugPreference $Links += $result } } } } } #endregion (Parse response) #region Generate output # If we are saving the output to a file, then we do not send anything to the # output buffer. We save the Content to a file instead. If ($OutFile) { # Don't write output if an error occurred. If ($response[0] -eq '3') { Write-Error $Content Return $null } Else { If (-Not $BINARY_TRANSFER) { # "Writing # bytes to <filename>" Write-Verbose (Get-MessageTranslation 23 @($Encoder.GetByteCount($response), $OutFile)) Set-Content -Path $OutFile -Value $Content -Encoding $Encoding -NoNewline } Else { # "Writing # bytes to <filename>" Write-Verbose (Get-MessageTranslation 23 @($response.Length, $OutFile)) Set-Content -Path $OutFile -Value $Content -AsByteStream } } Return } # TODO: figure out how to parse Gophermaps in Gopher+ mode. # For now, let's skip all this and return it as plain text. ElseIf ($Info -and $ContentTypeExpected -ne '1') { $Result = [PSCustomObject]@{} # For each line of Gopher+ output, we're going to see if it begins with # a plus sign. If so, we have an attribute name. Then, we're going to # go through each line of output and save that. Once we find another # attribute name, add the two items to $Result. $AttributeName = '' $AttributeValue = '' $response -Split "(\+[A-Z]+):" | ForEach-Object { If ($_.Length -gt 0) { # "Gopher+ output line: ___" Write-Debug (Get-MessageTranslation 24 $_) # If we've found an attribute, then add the current name/value # into $Result (if there is one). If ($_[0] -eq '+') { If ($AttributeValue) { If ($AttributeName -In @('ADMIN', 'VIEWS')) { $splits = $AttributeValue.Split("`r`n", [StringSplitOptions]::RemoveEmptyEntries).Trim() $Result | Add-Member -NotePropertyName $AttributeName -NotePropertyValue $splits # ($AttributeValue -Split "\s*\r\n\s*") } Else { $Result | Add-Member -NotePropertyName $AttributeName -NotePropertyValue $AttributeValue.Trim() } } # Now, get ready for the next attribute. $AttributeName = $_.Substring(1).Trim() $AttributeValue = '' } # This is not an attribute name, so add it to our # currently-saved value. Else { $AttributeValue += $_ } } } # What's left in $AttributeValue must be an attribute. # This is a repeat of the above few lines of code. If anyone can # refactor this into something better, please do! If ($AttributeName -In @('ADMIN', 'VIEWS')) { $Result | Add-Member -NotePropertyName $AttributeName -NotePropertyValue $AttributeValue.Split("\s*(\r\n)+\s*", [StringSplitOptions]::RemoveEmptyEntries) } Else { $Result | Add-Member -NotePropertyName $AttributeName -NotePropertyValue $AttributeValue.Trim() } Return $Result } Else { # Let's tell the user how we fetched this resource/attributes. # This will be more useful when I implement opportunistic TLS. $Protocol = 'Gopher' If ($Info -or $Views) { $Protocol = 'Gopher+' } If ((Test-Path 'variable:\SecureStream') -and ($null -ne $SecureStream)) { $Protocol = "Secure$Protocol" } Return [PSCustomObject]@{ 'Protocol' = $Protocol 'ContentType' = $ContentTypeExpected ?? '1' 'Content' = $Content 'Encoding' = ($BINARY_TRANSFER ? $Content.GetType() : $Encoder.GetType()) 'Images' = $Links | Where-Object Type -In @('g','I') 'Links' = $Links 'RawContent' = ($BINARY_TRANSFER ? $response.ToArray() : $response) 'RawContentLength' = $response.Length } } #endregion (Generate output) } Function Convert-GopherLink { [CmdletBinding()] [OutputType([PSCustomObject])] Param( [Parameter(Mandatory, Position=0)] [ValidateNotNullOrEmpty()] [ValidatePattern('^(?:.)(?:[^\t]*)\t')] [String] $InputObject, [String] $Server, [UInt16] $Port ) # "*** Found a Gopher link." Write-Debug "*** $(Get-MessageTranslation 25)" $fields = $InputObject -Split "`t" $Uri = $null # Are we dealing with a /URL: link? If so, we can easily create # the href [Uri]. Otherwise, we'll need to build it ourselves. If ($fields[1] -CLike 'URL:*' -or $fields[1] -CLike '/URL:*') { $Uri = [Uri]::new($fields[1] -Replace [RegEx]"\/?URL:",'') } Else { $Server = ${fields}?[2] ?? $Server $Port = ${fields}?[3] ?? $Port # Pick the appropriate URL schema for the link type. # For the first two (CCSO and Telnet), there should be nothing after the # optional port, but let's include it anyway. Switch -CaseSensitive -RegEx ($fields[0][0]) { '2' {$Port ??= 105; $Uri = [Uri]::new("cso://${Server}:$Port/$($fields[1])")} '[8T]' {$Port ??= 23; $Uri = [Uri]::new("telnet://${Server}:$Port/$($fields[1])")} default {$Port ??= 70; $Uri = [Uri]::new("gopher://${Server}:$Port/$($fields[1])")} } } # "*** Type=_: <URL>" and "LINK: Type=_: <URL>", respectively. Write-Debug "*** $(Get-MessageTranslation 26 @($fields[0][0], $Uri))" Write-Verbose "*** $(Get-MessageTranslation 27 @($fields[0][0], $Uri))" Return [PSCustomObject]@{ 'href' = $uri 'Type' = $fields[0][0] 'Description' = $fields[0].Substring(1) 'Resource' = $Uri.AbsolutePath 'Server' = $Uri.Host 'Port' = $Uri.Port 'UrlLink' = ($InputObject -Match '\t\/?URL:') } } # This helper function guessed at types when the user forgets to enter one. # This ensures that data will be returned in either text or binary format. # Feel free to add extensions and types as you see fit. Function Get-GopherType { [CmdletBinding()] [OutputType([Char])] Param( [ValidateNotNullOrEmpty()] [String] $Extension ) # This list will be searched case-insensitively. $Extensions = @{ 'ace' = '9' # ACE archive 'ai' = 'I' # Adobe Illustrator image 'aif[cf]?' = '<' # AIFF sound 'applescript|scpt' = '0'# AppleScript code 'arj' = '9' # ARJ archive 'art' = 'I' # AOL ART image 'asc' = '0' # GPG data (text) 'asf' = ';' # ASF sound 'asm|s' = '0' # Assembly code 'ass|ssa|srt' = '0' # Subtitles 'au' = '<' # Sound 'av1' = ';' # AV1 movie 'avi' = ';' # AVI movie 'avif' = 'I' # AVIF image 'bat|cmd' = '0' # Batch file 'bin' = '9' # Generic binary 'bmp|dib|pcx' = ':' # Bitmap image 'br' = '9' # Brotli-compressed data 'bz2' = '9' # BZIP2 archive 'c|h' = '0' # C source code 'cab' = '9' # Windows cabinet 'cer' = '0' # Certificate (probably text) 'cgm' = 'I' # CGM image 'coffee' = '0' # CoffeeScript 'conf|cfg?|ini' = '0' # Config file 'cpio' = '9' # CPIO archive '[ch](?:pp|xx)' = '0' # C++ code 'crl' = '9' # Certificate revocation list 'crt' = '9' # Certificate (probably binary) '[ch]s' = '0' # C# code 'css' = '0' # CSS stylesheet 'csv' = '0' # CSV data 'cur|ani' = '5' # Windows cursor 'deb|rpm|apk' = '9' # Linux packages 'der' = '0' # Certificate (as text) 'diff' = '0' # diff 'dll' = '5' # DOS/Windows library 'dmg|sparseimage' = '9' # macOS disk image 'dng' = 'I' # Digital negative 'dns' = '0' # DNS zone 'do[ct][mx]?' = 'd' # Microsoft Word document 'dsk' = '9' # Disk image 'dvi' = 'd' # DVI document 'dvr-ms' = ';' # Windows Media Center movie 'dwg' = 'I' # AutoCAD image 'ebuild' = '0' # Gentoo ebuild 'emf|wmf' = 'I' # Windows metafile image 'eml|msg' = '0' # Email message 'eps' = 'I' # Vector image 'epub|mobi' = '9' # Book 'exe|com|pif' = '5' # DOS/Windows app 'f?odg|otg' = 'I' # OpenDocument drawing 'f?odp|otp' = 'd' # OpenDocument presentation 'f?ods|ots' = 'd' # OpenDocument spreadsheet 'f?odt|ott' = 'd' # OpenDocument document 'fon|fot' = '5' # DOS/Windows font 'flac' = '<' # FLAC audio 'flv' = ';' # Flash video 'gif' = 'g' # GIF image 'gifv' = ';' # GIFV video 'gmi' = '0' # Gemtext 'gnumeric' = 'd' # Gnumeric spreadsheet 'go' = '0' # Go source code 'gpg' = '9' # GPG data (binary) 'gz' = '9' # Compressed data 'hei[cf]' = 'I' # HEIC image 'hqx' = '4' # BinHex archive 'html?' = 'h' # HTML document 'icns' = 'I' # macOS icon 'ico' = 'I' # Windows icon 'img' = '9' # Disk image 'inf' = '0' # Windows INF file 'ini' = '0' # Configuration file 'ipsw' = '9' # iOS/iPod software update 'iso' = '9' # CD image 'jar' = '9' # Java app 'java' = '0' # Java source code 'jp2' = 'I' # JPEG 2000 image 'jpe?g' = 'I' # JPEG image 'js' = '0' # JavaScript code 'json' = '0' # JSON data 'jsonld' = '0' # JSON-LD data 'jxl' = 'I' # JPEG XL image 'lnk' = '5' # Windows shortcut 'log' = '0' # Log 'lua' = '0' # Lua source code 'lz' = '9' # Compressed data 'lzh' = '9' # Compressed data 'lzma' = '9' # Compressed data 'lzo' = '9' # Compressed data 'm3u8?' = '<' # Playlist 'm4' = '0' # M4 source code 'm4[abpr]' = '<' # MPEG-4 audio formats (mostly iTunes) 'm4v|mp4' = ';' # MPEG-4 container (usually video) 'md|markdown' = '0' # Markdown text 'midi?' = '<' # MIDI music 'mkv' = ';' # Matroska video 'mov' = ';' # QuickTime movie 'mp3' = '<' # MP3 audio 'mpe?g' = ';' # MPEG movie 'mpp' = 'd' # Microsoft Project document 'msp' = 'I' # Microsoft Paintbrush image 'numbers' = 'd' # Numbers spreadsheet 'o' = '9' # Object file 'ocx' = '5' # ActiveX control 'odb' = 'd' # OpenDoucment database 'odf' = 'd' # OpenDocument formula 'og[gm]' = '<' # Ogg audio 'ogv' = ';' # Ogg video 'ovl' = '5' # DOS overlay 'o?xps' = 'd' # (Open)XPS document 'pages' = 'd' # Pages document 'par2?' = '9' # PAR archive 'pas' = '0' # Pascal code 'pbm' = ':' # PBM image 'pi?ct' = 'I' # Apple PICT image 'pdf' = 'd' # PDF document 'pdn' = 'I' # Paint.NET image 'pem' = '0' # PEM-encoded data 'pfx|p12|p7[bc]' = '9' # Certificates 'php[345]?' = '0' # PHP code 'pl' = '0' # Perl code 'png' = 'I' # PNG image 'pptx?|pps|pot' = 'd' # PowerPoint presentation 'ps' = 'd' # PostScript document 'psd' = 'I' # Photoshop image 'psp' = 'I' # Paint Shop Pro image 'ps[cdm]?1' = '0' # PowerShell code 'ps1xml' = '0' # PowerShell types or formats 'pub' = 'd' # Publisher document 'py' = '0' # Python code 'py[co]' = '9' # Python bytecode 'r' = '0' # R code 'rar' = '9' # WinRAR archive 'rb' = '0' # Ruby source code 'rdp' = '0' # Microsoft Remote Desktop connection 'rss|atom' = '0' # News feed 'rtfd?' = 'd' # Rich Text Format document 'scr' = '5' # Windows screen saver 'scss' = '0' # Sass code 'sh|bash|command' = '0' # Shell script 'sht(?:ml)?' = 'h' # HTML with includes 'sitx?' = '9' # Stuffit archive 'snd' = '<' # Sound 'sql' = '0' # SQL code 'svgz?' = 'I' # SVG image 'sys|drv' = '5' # DOS/Windows driver 'tab|tsv' = 'd' # Tab-encoded values 'tar' = '9' # TAR archive 'targa|tga' = 'I' # Targa image 'tcl' = '0' # TCL code 'tex' = '0' # Tex code 'tiff?' = 'I' # TIFF image 'tt[cf]|otf|woff2?' = '9' # Font 'txt' = '0' # Text 'uue' = '6' # UUEncoded data 'vbs' = '0' # VBScript 'vhdx?' = '9' # Windows disk image 'vsdx?' = 'I' # Visio drawing 'wav' = '<' # Wave audio 'webm' = ';' # WebM video 'webp|wp2' = 'I' # WebP (2) image 'wim|esd' = '9' # Windows Image (archive) 'wma|asf' = ';' # Windows Media audio 'wmf|emf' = 'I' # Windows Metafile image 'wmv' = ';' # Windows Media video 'wpt?' = 'd' # WordPerfect doucment 'wri' = 'd' # Windows Write document 'xcf' = 'I' # GIMP image 'xht(?:ml)?' = 'h' # XHTML code 'xl(s[bmx]?|w)' = 'd' # Excel document 'xltm?' = 'd' # Excel template 'xml|rdf' = '0' # XML code 'xpm' = 'I' # XPM image 'xsd' = '0' # XSD code 'xslt?' = '0' # XML stylesheet 'xz' = '9' # Compressed data 'ya?ml' = '0' # YAML code 'Z' = '9' # Compressed data 'zip' = '9' # Zip archive 'zoo' = '9' # ZOO archive '123' = 'd' # Lotus 1-2-3 document '7z' = '9' # 7-zip archive } $Result = $null ForEach ($regex in $Extensions.GetEnumerator()) { # "Testing extension $Extension against $($regex.Name)." Write-Debug (Get-MessageTranslation 28 @($Extension, $regex.Name)) If ($Extension -Match $regex.Name) { $Result = $regex.Value Break } } # "Guessing that the extension ___ is of type $(___ ?? 'unknown')." Write-Verbose (Get-MessageTranslation 29 @($Extension, ($Result ?? (Get-MessageTranslation 30)))) Return $Result } Function Get-MessageTranslation { [OutputType([String])] Param( [Parameter(Position=0, Mandatory)] [UInt16] $MessageID, [Parameter(Position=1)] [AllowNull()] [String[]] $Substitutions = @() ) If ($null -eq $script:Translations) { Import-Translation } Return ($script:Translations[$MessageID - 1] -f $Substitutions) } Function Import-Translation { # Error messages in this function indicate that a localization does # not exist, could not be loaded, or has not yet been loaded and any # lookup would cause an infinite loop. Do not translate these # errors; leave them in English. [CmdletBinding()] [OutputType([Void])] Param( [Switch] $ForceEnUs ) $Language = (Get-Culture) Try { # "Attempting to load translations for <your language here>." Write-Debug "Attempting to load translations for $($Language.Name)" $File = Join-Path -Path (Get-Module 'PSGopher').ModuleBase -ChildPath $Language.Name -AdditionalChildPath 'translations.json' $script:Translations = Get-Content -Path $File -Encoding 'UTF8' | ConvertFrom-Json } Catch { If (-Not $ForceEnUs) { Write-Debug "Falling back to English (United States)" Import-Translation -ForceEnUs:$true } Else { Throw 'Failed to load en-US translation!' } } Return } # SIG # Begin signature block # MIIo5AYJKoZIhvcNAQcCoIIo1TCCKNECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDB0KkG5Jks/aEU # 04WHAPI/dlaNF1R6168hwEiQqCIIraCCI6swggR+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 # CSqGSIb3DQEJBDEiBCCVgmrv04882Tf1lYGGVfoMLuSiDo6hyRKy8H5qbzVjZTAL # BgcqhkjOPQIBBQAEZzBlAjEAt84Bf0HqRZA1t0PIit9tSXG2v02jJbikWfN4AmjP # bnwT2gnZBXhD/bC4YiBBUuFCAjAxQeKEQuscalwrVy9vBdllUW6kCir4gqWkvkFZ # qCm3uPxbwKsV7dxo0LJk4NxvSfKhggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkC # AQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5 # BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0 # YW1waW5nIENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJ # KoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMwMzI5MTQz # NDIxWjAvBgkqhkiG9w0BCQQxIgQgfFtYeXKJ2Gnj5zQZnc+8NHgM/RE30uwwGo5U # i0abca0wDQYJKoZIhvcNAQEBBQAEggIAmPlKG959Yd66VzIhFWIKlt/18ZCnsyAR # gDmGbtvP3VeejpaEGFWM0nPMNnxkQ1W7Z0rqyKPkHso6HMZbJW3aDAwjKPBy6NP3 # XqIDUJx5gqwOcptRGhr2NEwuX/tauSboTcyDNesgP9gNk3LJ9HDlF3WTom26+zKa # LSOPeSkQCizqcmbIekJsXIMiFQ+3/St87AoLiWaLq1FPsShX7S+JXVx13qgYTMlt # +rFlC9TBTG2Lrnbpt9reSlkUlm5NCV5teD+KTdkZ/LSG9uePZ7kjsorTZVZ24cdy # rGO4O/dFqSZ4MKHO1iR6umgIDfNKht86re82TiaAyh3f8Zm7yB29qN+68O8kDk34 # pHlxs0L9wsSghATZeaXO2fc1sN4X2SV+FASVTRIozhJe+/T1IG2pSsz5Eo2S4Ljo # raioH42qZFf42Y1XkKv+3Q5tgdb+TlR35mL4WKJIH9DFFr2ZkbzjqUNzOSqvIhI/ # C47NvtE098CVhzfuDDIG/I+BSsUc+/u5sRdnQXs+yn+sBcQ9dz1nkd4bGFbm4nx8 # wIoPPzD0gk1jTaBpecTDI7pZv6IVQ1xvjXII3GVp3ctqWS42Ckrx40oxsyPfS5vN # vYL0B/36Ufd4bUJ0LjTJHBqC++SUHnhNWLpi8dGg2rGWzGWDCTrCASysiJfl1+4F # 1iZBvAor7bo= # SIG # End signature block |