# Neptune Agent Health Check Script # This script checks if the neptune-agent service and process are running # If not, it attempts a service restart and falls back to org-less reinstall # # Features: # - Robust path resolution from service configuration (not hardcoded) # - Checksum verification using releases.json manifest # - Atomic file replacement with rollback on failure # - Optional strict signature verification (NEPTUNE_STRICT_SIGNATURE=1) # Default paths (will be overridden by service configuration if available) $script:DataDir = "" $script:BinDir = "" $script:LauncherPath = "" $script:AgentPath = "" $script:UpdaterPath = "" $Channel = "stable" $UpdatesRepository = "https://updates.neptune.easyteam.fr" # Signature verification mode: "compat" (default, warn-only) or "strict" (fail on invalid/unsigned) $script:SignatureMode = if ($env:NEPTUNE_STRICT_SIGNATURE -eq "1") { "strict" } else { "compat" } # Maximum number of backups to keep per binary $script:MaxBackups = 5 # Event Viewer logging configuration $EventSource = "NeptuneAgent" $EventLog = "Application" # Script-level temp files for cleanup tracking $script:TempFilesToCleanup = @() # Mutex for concurrent execution prevention $script:HealthCheckMutex = $null $MutexName = "Global\NeptuneAgentHealthCheck" # Cached releases data $script:ReleasesData = $null $script:ReleasesFile = "" # Ensure event source exists (requires admin, but SYSTEM has this) if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) { try { New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop } catch { # Source may already exist or be registered to another log } } # Function to log messages to Windows Event Viewer function Write-Log { param( [string]$Message, [string]$Level = "" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logMessage = "[HealthCheck] [$timestamp] $Message" Write-Output $logMessage # Auto-detect level from message prefix if not explicitly set if (-not $Level) { if ($Message -match "^ERROR:") { $Level = "Error" } elseif ($Message -match "^WARNING:") { $Level = "Warning" } else { $Level = "Information" } } $entryType = switch ($Level) { "Error" { "Error" } "Warning" { "Warning" } default { "Information" } } try { Write-EventLog -LogName $EventLog -Source $EventSource -EventId 1000 -EntryType $entryType -Message $logMessage -ErrorAction SilentlyContinue } catch { # Silently continue if event log write fails } } # Function to check if a process is running by name function Test-ProcessRunning { param( [string]$ProcessName ) try { $proc = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue return $null -ne $proc } catch { return $false } } # Function to check if the agent is responsive by verifying log freshness # Returns $true if the agent appears responsive, $false if it may be hung/zombie function Test-AgentResponsive { param( [int]$MaxStaleMinutes = 30 ) # Determine the logs directory (sibling of bin directory) $logsDir = $null if ($script:BinDir) { $logsDir = Join-Path (Split-Path $script:BinDir -Parent) "logs" } if (-not $logsDir -or -not (Test-Path $logsDir)) { # Try standard location $logsDir = "$env:ProgramFiles\Neptune\logs" } if (-not (Test-Path $logsDir)) { Write-Log "WARNING: Logs directory not found - skipping responsiveness check" return $true # Assume responsive if we can't check } # Find the most recently modified agent log file try { $latestLog = Get-ChildItem -Path $logsDir -Filter "neptune-agent-*.log" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $latestLog) { # Also check launcher logs as a proxy (launcher monitors updater which monitors agent) $latestLog = Get-ChildItem -Path $logsDir -Filter "neptune-launcher-*.log" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } if (-not $latestLog) { Write-Log "WARNING: No agent or launcher log files found in $logsDir - skipping responsiveness check" return $true # Assume responsive if no logs exist yet } $staleThreshold = (Get-Date).AddMinutes(-$MaxStaleMinutes) if ($latestLog.LastWriteTime -lt $staleThreshold) { $staleDuration = [math]::Round(((Get-Date) - $latestLog.LastWriteTime).TotalMinutes, 1) Write-Log "WARNING: Agent log file has not been updated for $staleDuration minutes (threshold: $MaxStaleMinutes min)" Write-Log " Last log: $($latestLog.FullName)" Write-Log " Last write: $($latestLog.LastWriteTime)" return $false } Write-Log "Agent log freshness OK (last write: $($latestLog.LastWriteTime), file: $($latestLog.Name))" return $true } catch { Write-Log "WARNING: Error checking log freshness: $_" return $true # Assume responsive on error } } # Function to get config.json path (if present) function Get-ConfigPath { if ($script:BinDir) { $basePath = Split-Path $script:BinDir -Parent return Join-Path $basePath "etc\config.json" } if ($script:DataDir) { $basePath = Split-Path $script:DataDir -Parent return Join-Path $basePath "etc\config.json" } return $null } # Function to get API endpoint from config.json function Get-ApiEndpoint { $configPath = Get-ConfigPath if (-not $configPath -or -not (Test-Path $configPath)) { return $null } try { $config = Get-Content $configPath -Raw | ConvertFrom-Json if ($config -and $config.api) { return $config.api } } catch { Write-Log "WARNING: Failed to read API endpoint from config.json: $_" } return $null } # Function to get proxy endpoint from config.json function Get-ProxyEndpoint { $configPath = Get-ConfigPath if (-not $configPath -or -not (Test-Path $configPath)) { return $null } try { $config = Get-Content $configPath -Raw | ConvertFrom-Json if ($config -and $config.proxy) { return $config.proxy } } catch { Write-Log "WARNING: Failed to read proxy endpoint from config.json: $_" } return $null } # Function to normalize URL by trimming trailing slash function ConvertTo-NormalizedUrl { param([string]$Url) if (-not $Url) { return $null } return $Url.TrimEnd("/") } # Function to normalize proxy URL (ensure scheme) function ConvertTo-ProxyUrl { param([string]$ProxyUrl) if (-not $ProxyUrl) { return $null } if ($ProxyUrl -match "^[a-zA-Z][a-zA-Z0-9+\-.]*://") { return $ProxyUrl } return "http://$ProxyUrl" } # Function to invoke org-less installer for reinstall function Invoke-OrglessReinstall { $api = Get-ApiEndpoint if (-not $api) { Write-Log "ERROR: Could not determine API endpoint from config.json" return $false } $api = ConvertTo-NormalizedUrl -Url $api $installUrl = "$api/install/windows" Write-Log "Invoking org-less installer: $installUrl" Write-Log "Stopping neptune-agent service before reinstall..." Stop-ServiceAndProcesses -ServiceName "neptune-agent" -TimeoutSeconds 30 | Out-Null $proxyUrl = ConvertTo-ProxyUrl -ProxyUrl (Get-ProxyEndpoint) if ($proxyUrl) { Write-Log "Using proxy from config.json for download" } $installerPath = Get-UniqueTempFile -Prefix "neptune-install" -Extension ".ps1" if (-not (Invoke-WebRequestWithRetry -Uri $installUrl -OutFile $installerPath -TimeoutSec 300 -ProxyUrl $proxyUrl)) { Write-Log "ERROR: Failed to download org-less installer" return $false } try { & powershell.exe -ExecutionPolicy Bypass -NoProfile -File $installerPath if ($LASTEXITCODE -ne 0) { Write-Log "ERROR: Org-less installer returned exit code $LASTEXITCODE" return $false } } catch { Write-Log "ERROR: Org-less installer execution failed: $_" return $false } Write-Log "Org-less installer completed successfully" return $true } # Function to preflight launcher using --version function Test-LauncherPreflight { if (-not $script:LauncherPath -or -not (Test-Path $script:LauncherPath)) { Write-Log "ERROR: neptune-launcher not found at $script:LauncherPath" return $false } try { & $script:LauncherPath --version | Out-Null if ($LASTEXITCODE -ne 0) { Write-Log "ERROR: neptune-launcher --version failed with exit code $LASTEXITCODE" return $false } } catch { Write-Log "ERROR: neptune-launcher --version failed: $_" return $false } Write-Log "Launcher preflight passed" return $true } # Function to cleanup temp files function Invoke-Cleanup { foreach ($file in $script:TempFilesToCleanup) { if (Test-Path $file) { Remove-Item $file -Force -ErrorAction SilentlyContinue } } $script:TempFilesToCleanup = @() # Release mutex if held if ($script:HealthCheckMutex) { try { $script:HealthCheckMutex.ReleaseMutex() } catch { # Ignore - mutex may not be owned } $script:HealthCheckMutex.Dispose() $script:HealthCheckMutex = $null } } # Function to get a unique temp file path function Get-UniqueTempFile { param([string]$Prefix = "neptune", [string]$Extension = ".tmp") $randomName = [System.IO.Path]::GetRandomFileName() $tempFile = Join-Path $env:TEMP "$Prefix-$randomName$Extension" $script:TempFilesToCleanup += $tempFile return $tempFile } # Function to configure TLS protocols function Set-TlsProtocols { # Enable TLS 1.2 and TLS 1.3 (if available) try { # TLS 1.3 is 12288 (0x3000) - may not be available on older systems $tls13 = [Net.SecurityProtocolType]::Tls13 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor $tls13 } catch { # Fall back to TLS 1.2 only if TLS 1.3 is not available [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } } function Set-NeptuneCATrust { $neptuneEtc = Join-Path (Split-Path $script:BinDir -Parent) "etc" $caBundle = Join-Path $neptuneEtc "cacert.pem" if (-not (Test-Path $caBundle)) { return } try { if (-not ([System.Management.Automation.PSTypeName]'NeptuneCertValidator').Type) { Add-Type -TypeDefinition @" using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; public static class NeptuneCertValidator { private static List _cas = new List(); public static void LoadPEM(string path) { string pem = File.ReadAllText(path); foreach (Match m in Regex.Matches(pem, @"-----BEGIN CERTIFICATE-----(.+?)-----END CERTIFICATE-----", RegexOptions.Singleline)) { byte[] der = Convert.FromBase64String( m.Groups[1].Value.Replace("\r","").Replace("\n","")); _cas.Add(new X509Certificate2(der)); } } public static bool Validate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors errors) { if (errors == SslPolicyErrors.None) return true; if (_cas.Count == 0) return false; X509Chain custom = new X509Chain(); try { foreach (X509Certificate2 ca in _cas) { custom.ChainPolicy.ExtraStore.Add(ca); } custom.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; custom.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; if (!custom.Build(new X509Certificate2(cert.GetRawCertData()))) return false; X509Certificate2 root = custom.ChainElements[ custom.ChainElements.Count - 1].Certificate; foreach (X509Certificate2 ca in _cas) { if (root.Thumbprint == ca.Thumbprint) return true; } return false; } finally { custom.Reset(); } } public static void Enable(string bundlePath) { LoadPEM(bundlePath); ServicePointManager.ServerCertificateValidationCallback = Validate; } } "@ } [NeptuneCertValidator]::Enable($caBundle) } catch { Write-Log "WARNING: Failed to initialize CA trust validator: $_" } } # Function to download with retry logic function Invoke-WebRequestWithRetry { param( [string]$Uri, [string]$OutFile, [int]$TimeoutSec = 60, [int]$MaxRetries = 3, [int]$RetryDelaySeconds = 5, [string]$ProxyUrl = "" ) $ProgressPreference = 'SilentlyContinue' Set-TlsProtocols for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) { try { $invokeParams = @{ Uri = $Uri OutFile = $OutFile UseBasicParsing = $true TimeoutSec = $TimeoutSec ErrorAction = "Stop" } if ($ProxyUrl) { $invokeParams["Proxy"] = $ProxyUrl } Invoke-WebRequest @invokeParams return $true } catch { if ($attempt -eq $MaxRetries) { Write-Log "ERROR: Failed to download after $MaxRetries attempts: $_" return $false } $delay = $RetryDelaySeconds * $attempt # Exponential backoff Write-Log "WARNING: Download attempt $attempt failed, retrying in ${delay}s..." Start-Sleep -Seconds $delay } } return $false } # Function to verify binary signature with configurable mode function Test-BinarySignature { param( [string]$FilePath, [switch]$Force # Force strict mode regardless of global setting ) $useStrict = $Force -or ($script:SignatureMode -eq "strict") if (-not (Test-Path $FilePath)) { Write-Log "WARNING: Cannot verify signature - file not found: $FilePath" return (-not $useStrict) # Fail in strict mode, pass in compat } try { $signature = Get-AuthenticodeSignature -FilePath $FilePath -ErrorAction Stop switch ($signature.Status) { "Valid" { Write-Log "Binary signature is valid: $FilePath" # Optionally check publisher in strict mode if ($useStrict -and $signature.SignerCertificate) { $subject = $signature.SignerCertificate.Subject Write-Log " Signer: $subject" } return $true } "NotSigned" { Write-Log "WARNING: Binary is not signed: $FilePath" if ($useStrict) { Write-Log "ERROR: Strict signature mode enabled - rejecting unsigned binary" return $false } return $true } default { Write-Log "WARNING: Binary signature status: $($signature.Status) for $FilePath" if ($useStrict) { Write-Log "ERROR: Strict signature mode enabled - rejecting binary with status: $($signature.Status)" return $false } return $true } } } catch { Write-Log "WARNING: Failed to verify binary signature: $_" if ($useStrict) { Write-Log "ERROR: Strict signature mode enabled - signature verification error is fatal" return $false } return $true } } # Function to compute SHA256 hash of a file function Get-FileChecksum { param([string]$FilePath) if (-not (Test-Path $FilePath)) { return $null } try { $hash = Get-FileHash -Path $FilePath -Algorithm SHA256 -ErrorAction Stop return $hash.Hash.ToLower() } catch { Write-Log "WARNING: Failed to compute hash for $FilePath`: $_" return $null } } # Function to parse service binary path and extract component paths function Get-ServiceBinaryPaths { param([string]$PathName) $result = @{ LauncherPath = $null AgentPath = $null UpdaterPath = $null BinDir = $null } if (-not $PathName) { return $result } # Parse the path - may be quoted or unquoted # Format: "C:\path\neptune-launcher.exe" --agent "C:\path\neptune-agent.exe" --updater "C:\path\neptune-updater.exe" # Or: C:\path\neptune-launcher.exe --agent C:\path\neptune-agent.exe ... try { # Extract launcher path (first component, possibly quoted) if ($PathName -match '^"([^"]+)"') { $result.LauncherPath = $matches[1] } elseif ($PathName -match '^([^\s]+)') { $result.LauncherPath = $matches[1] } # Extract agent path (--agent argument) if ($PathName -match '--agent\s+"([^"]+)"') { $result.AgentPath = $matches[1] } elseif ($PathName -match '--agent\s+([^\s]+)') { $result.AgentPath = $matches[1] } # Extract updater path (--updater argument) if ($PathName -match '--updater\s+"([^"]+)"') { $result.UpdaterPath = $matches[1] } elseif ($PathName -match '--updater\s+([^\s]+)') { $result.UpdaterPath = $matches[1] } # Derive bin directory from launcher path if ($result.LauncherPath -and (Test-Path $result.LauncherPath)) { $result.BinDir = Split-Path $result.LauncherPath -Parent } } catch { Write-Log "WARNING: Failed to parse service path: $_" } return $result } # Function to resolve Neptune install paths from service configuration function Resolve-NeptunePaths { Write-Log "Resolving Neptune installation paths..." # Method 1: Query service configuration via WMI/CIM try { $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='neptune-agent'" -ErrorAction Stop if ($service -and $service.PathName) { Write-Log "Found service path via CIM: $($service.PathName)" $paths = Get-ServiceBinaryPaths -PathName $service.PathName if ($paths.LauncherPath -and (Test-Path $paths.LauncherPath)) { $script:LauncherPath = $paths.LauncherPath $script:BinDir = $paths.BinDir if ($paths.AgentPath -and (Test-Path $paths.AgentPath)) { $script:AgentPath = $paths.AgentPath } else { $script:AgentPath = Join-Path $script:BinDir "neptune-agent.exe" } if ($paths.UpdaterPath -and (Test-Path $paths.UpdaterPath)) { $script:UpdaterPath = $paths.UpdaterPath } else { $script:UpdaterPath = Join-Path $script:BinDir "neptune-updater.exe" } $script:DataDir = Join-Path (Split-Path $script:BinDir -Parent) "var" Write-Log "Resolved paths from service configuration:" Write-Log " BinDir: $script:BinDir" Write-Log " LauncherPath: $script:LauncherPath" Write-Log " AgentPath: $script:AgentPath" Write-Log " UpdaterPath: $script:UpdaterPath" Write-Log " DataDir: $script:DataDir" return $true } } } catch { Write-Log "WARNING: CIM query failed: $_" } # Method 2: Query via sc.exe (fallback) try { $output = & sc.exe qc neptune-agent 2>$null if ($LASTEXITCODE -eq 0) { foreach ($line in $output) { if ($line -match '^\s*BINARY_PATH_NAME\s*:\s*(.+)$') { $pathName = $matches[1].Trim() Write-Log "Found service path via sc.exe: $pathName" $paths = Get-ServiceBinaryPaths -PathName $pathName if ($paths.LauncherPath -and (Test-Path $paths.LauncherPath)) { $script:LauncherPath = $paths.LauncherPath $script:BinDir = $paths.BinDir $script:AgentPath = if ($paths.AgentPath) { $paths.AgentPath } else { Join-Path $script:BinDir "neptune-agent.exe" } $script:UpdaterPath = if ($paths.UpdaterPath) { $paths.UpdaterPath } else { Join-Path $script:BinDir "neptune-updater.exe" } $script:DataDir = Join-Path (Split-Path $script:BinDir -Parent) "var" Write-Log "Resolved paths from sc.exe:" Write-Log " BinDir: $script:BinDir" Write-Log " LauncherPath: $script:LauncherPath" return $true } } } } } catch { Write-Log "WARNING: sc.exe query failed: $_" } # Method 3: Check script directory (health-check.ps1 is typically in bin/) if ($PSScriptRoot -and (Test-Path $PSScriptRoot)) { $launcherInScriptDir = Join-Path $PSScriptRoot "neptune-launcher.exe" if (Test-Path $launcherInScriptDir) { Write-Log "Found launcher in script directory: $launcherInScriptDir" $script:BinDir = $PSScriptRoot $script:LauncherPath = $launcherInScriptDir $script:AgentPath = Join-Path $script:BinDir "neptune-agent.exe" $script:UpdaterPath = Join-Path $script:BinDir "neptune-updater.exe" $script:DataDir = Join-Path (Split-Path $script:BinDir -Parent) "var" return $true } } # Method 4: Try ProgramW6432 (always 64-bit Program Files on 64-bit Windows) $programW6432 = $env:ProgramW6432 if ($programW6432) { $binDir = "$programW6432\Neptune\bin" $launcherPath = "$binDir\neptune-launcher.exe" if (Test-Path $launcherPath) { Write-Log "Found launcher in ProgramW6432: $launcherPath" $script:BinDir = $binDir $script:LauncherPath = $launcherPath $script:AgentPath = "$binDir\neptune-agent.exe" $script:UpdaterPath = "$binDir\neptune-updater.exe" $script:DataDir = "$programW6432\Neptune\var" return $true } } # Method 5: Standard Program Files (last resort) $standardBinDir = "$env:ProgramFiles\Neptune\bin" $standardLauncherPath = "$standardBinDir\neptune-launcher.exe" if (Test-Path $standardLauncherPath) { Write-Log "Found launcher in standard location: $standardLauncherPath" $script:BinDir = $standardBinDir $script:LauncherPath = $standardLauncherPath $script:AgentPath = "$standardBinDir\neptune-agent.exe" $script:UpdaterPath = "$standardBinDir\neptune-updater.exe" $script:DataDir = "$env:ProgramFiles\Neptune\var" return $true } Write-Log "ERROR: Could not resolve Neptune installation paths" return $false } # Function to stop service and ensure processes are terminated function Stop-ServiceAndProcesses { param( [string]$ServiceName = "neptune-agent", [int]$TimeoutSeconds = 30 ) try { $service = Get-Service -Name $ServiceName -ErrorAction Stop if ($service.Status -eq "Stopped") { Write-Log "Service is already stopped" } else { Write-Log "Stopping $ServiceName service..." Stop-Service -Name $ServiceName -Force -ErrorAction Stop # Wait for service to stop with timeout $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) { $service = Get-Service -Name $ServiceName -ErrorAction Stop if ($service.Status -eq "Stopped") { Write-Log "Service stopped successfully" break } Start-Sleep -Milliseconds 500 } } } catch { Write-Log "WARNING: Failed to stop service gracefully: $_" } # Ensure Neptune processes are terminated (launcher/updater/agent + subprocess components) # Note: process names do not include ".exe" in Get-Process. $processNames = @( "neptune-launcher", "neptune-agent", "neptune-updater", "neptune-reporter", "neptune-observability", "neptune-monitoring", "neptune-actions", "neptune-tunnels" ) foreach ($procName in $processNames) { try { $processes = Get-Process -Name $procName -ErrorAction SilentlyContinue foreach ($proc in $processes) { Write-Log "Terminating process: $procName (PID: $($proc.Id))" $proc | Stop-Process -Force -ErrorAction SilentlyContinue } } catch { # Ignore errors } } # Wait a moment for processes to fully terminate Start-Sleep -Seconds 2 # Verify processes are gone $remaining = @() foreach ($procName in $processNames) { $procs = Get-Process -Name $procName -ErrorAction SilentlyContinue if ($procs) { $remaining += $procs } } if ($remaining.Count -gt 0) { Write-Log "WARNING: Some Neptune processes still running after termination attempt" return $false } Write-Log "All Neptune processes terminated" return $true } # Function to start service with timeout function Start-ServiceWithTimeout { param( [string]$ServiceName, [int]$TimeoutSeconds = 30 ) try { Write-Log "Starting $ServiceName service..." Start-Service -Name $ServiceName -ErrorAction Stop # Wait for service to start with timeout $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) { $service = Get-Service -Name $ServiceName -ErrorAction Stop if ($service.Status -eq "Running") { Write-Log "Service started successfully" return $true } Start-Sleep -Milliseconds 500 } Write-Log "WARNING: Service did not start within $TimeoutSeconds seconds" return $false } catch { Write-Log "ERROR: Failed to start service: $_" return $false } } # Function to acquire mutex lock function Enter-HealthCheckLock { param([int]$TimeoutSeconds = 600) # 10 minute timeout try { $createdNew = $false $script:HealthCheckMutex = New-Object System.Threading.Mutex($false, $MutexName, [ref]$createdNew) # Try to acquire the mutex with timeout $acquired = $script:HealthCheckMutex.WaitOne($TimeoutSeconds * 1000) if (-not $acquired) { Write-Log "Another health check is already running - exiting" $script:HealthCheckMutex.Dispose() $script:HealthCheckMutex = $null return $false } return $true } catch [System.Threading.AbandonedMutexException] { # Previous owner died without releasing - we now own it Write-Log "WARNING: Acquired abandoned mutex - previous health check may have crashed" return $true } catch { Write-Log "WARNING: Failed to acquire health check lock: $_" return $false } } # Function to download and cache releases.json function Get-ReleasesData { if ($script:ReleasesData) { return $script:ReleasesData } $script:ReleasesFile = Get-UniqueTempFile -Prefix "neptune-releases" -Extension ".json" $releasesUrl = "$UpdatesRepository/$Channel/releases.json" Write-Log "Downloading releases metadata from: $releasesUrl" if (-not (Invoke-WebRequestWithRetry -Uri $releasesUrl -OutFile $script:ReleasesFile -TimeoutSec 60)) { Write-Log "ERROR: Failed to download releases metadata" return $null } try { $script:ReleasesData = Get-Content $script:ReleasesFile -Raw -ErrorAction Stop | ConvertFrom-Json # Validate structure if (-not $script:ReleasesData.latest -or -not $script:ReleasesData.latest.build_id) { Write-Log "ERROR: Invalid releases.json structure - missing latest.build_id" $script:ReleasesData = $null return $null } # Validate build ID format $buildId = $script:ReleasesData.latest.build_id if ($buildId -notmatch '^[a-fA-F0-9]{7,40}$') { Write-Log "ERROR: Invalid build ID format: $buildId" $script:ReleasesData = $null return $null } Write-Log "Latest build ID: $buildId" return $script:ReleasesData } catch { Write-Log "ERROR: Failed to parse releases.json: $_" $script:ReleasesData = $null return $null } } # Function to get expected checksum for a binary from releases.json function Get-ExpectedChecksum { param( [string]$Component, # e.g., "neptune-launcher" [string]$BuildId, [string]$Arch ) $releases = Get-ReleasesData if (-not $releases) { return $null } # Construct artifact name (e.g., "neptune-launcher.x86_64.f574fb1.exe") $artifactName = "$Component.$Arch.$BuildId.exe" Write-Log "Looking up checksum for artifact: $artifactName" # Look up checksum in builds[build_id].checksums[artifact_name] # Note: Must use PSObject.Properties for dynamic property access with dots in names try { if (-not $releases.builds) { Write-Log "WARNING: No builds in releases data" return $null } # Access build by ID (property name is the build ID like "f574fb1") $build = $releases.builds.PSObject.Properties[$BuildId] if (-not $build) { Write-Log "WARNING: Build $BuildId not found in releases" return $null } $buildData = $build.Value if (-not $buildData.checksums) { Write-Log "WARNING: No checksums in build $BuildId" return $null } # Access checksum by artifact name (property name contains dots, e.g., "neptune-launcher.x86_64.f574fb1.exe") $checksumProp = $buildData.checksums.PSObject.Properties[$artifactName] if ($checksumProp) { $checksum = $checksumProp.Value.ToLower() Write-Log "Found checksum for $artifactName`: $checksum" return $checksum } else { Write-Log "WARNING: No checksum found for artifact: $artifactName" } } catch { Write-Log "WARNING: Error looking up checksum: $_" } return $null } # Function to get the backup directory path function Get-BackupDirectory { # Backup directory is inside bin directory (e.g., C:\Program Files\Neptune\bin\backup) if ($script:BinDir) { $backupDir = Join-Path $script:BinDir "backup" } else { # Fallback to ProgramData if BinDir not resolved $backupDir = "$env:ProgramData\Neptune\bin\backup" } # Ensure backup directory exists if (-not (Test-Path $backupDir)) { try { New-Item -Path $backupDir -ItemType Directory -Force -ErrorAction Stop | Out-Null Write-Log "Created backup directory: $backupDir" } catch { Write-Log "WARNING: Failed to create backup directory: $_" return $null } } return $backupDir } # Function to create a timestamped backup of a binary function New-BinaryBackup { param([string]$BinaryPath) if (-not (Test-Path $BinaryPath)) { return $null } try { $backupDir = Get-BackupDirectory if (-not $backupDir) { Write-Log "WARNING: Could not get backup directory - skipping backup" return $null } $name = Split-Path $BinaryPath -Leaf $baseName = [System.IO.Path]::GetFileNameWithoutExtension($name) $ext = [System.IO.Path]::GetExtension($name) $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $backupName = "$baseName.$timestamp.backup$ext" $backupPath = Join-Path $backupDir $backupName Copy-Item -Path $BinaryPath -Destination $backupPath -Force -ErrorAction Stop Write-Log "Created backup: $backupPath" return $backupPath } catch { Write-Log "WARNING: Failed to create backup of $BinaryPath`: $_" return $null } } # Function to cleanup old backups, keeping only the most recent N per component function Remove-OldBackups { param( [string]$BinaryPath, [int]$KeepCount = 5 ) $backupDir = Get-BackupDirectory if (-not $backupDir -or -not (Test-Path $backupDir)) { return } $name = Split-Path $BinaryPath -Leaf $baseName = [System.IO.Path]::GetFileNameWithoutExtension($name) try { # Get all backup files matching either PowerShell or Go format # PowerShell format: baseName.YYYYMMDD-HHMMSS.backup.exe (e.g., neptune-launcher.20260106-155949.backup.exe) # Go format: name.hash.YYYYMMDD-HHMMSS.backup (e.g., neptune-launcher.exe.3cef503.20260106-155949.backup) $backupPatternPS = "$baseName.*.backup*" # PowerShell format $backupPatternGo = "$name.*.backup" # Go format (includes extension) $backupsPS = Get-ChildItem -Path $backupDir -Filter $backupPatternPS -ErrorAction SilentlyContinue $backupsGo = Get-ChildItem -Path $backupDir -Filter $backupPatternGo -ErrorAction SilentlyContinue # Combine and deduplicate backups, then sort by LastWriteTime descending $allBackups = @() if ($backupsPS) { $allBackups += $backupsPS } if ($backupsGo) { $allBackups += $backupsGo } # Remove duplicates (in case a file matches both patterns) and sort $backups = $allBackups | Sort-Object FullName -Unique | Sort-Object LastWriteTime -Descending if ($backups.Count -gt $KeepCount) { $toDelete = $backups | Select-Object -Skip $KeepCount foreach ($backup in $toDelete) { # Also remove associated .json metadata files $metadataPath = $backup.FullName + ".json" Remove-Item $backup.FullName -Force -ErrorAction SilentlyContinue Write-Log "Removed old backup: $($backup.Name)" if (Test-Path $metadataPath) { Remove-Item $metadataPath -Force -ErrorAction SilentlyContinue Write-Log "Removed backup metadata: $($backup.Name).json" } } } } catch { Write-Log "WARNING: Error cleaning up old backups: $_" } } # Function to atomically replace a binary with rollback support function Install-BinaryAtomic { param( [string]$Component, # e.g., "neptune-launcher" [string]$TargetPath, # Full path to target binary [string]$BuildId, [string]$Arch, [string]$ExpectedChecksum # Optional - will be looked up if not provided ) Write-Log "Installing $Component to $TargetPath" # Get expected checksum if not provided if (-not $ExpectedChecksum) { $ExpectedChecksum = Get-ExpectedChecksum -Component $Component -BuildId $BuildId -Arch $Arch } # Build download URL $binaryName = "$Component.$Arch.$BuildId.exe" $url = "$UpdatesRepository/$Channel/$binaryName" $tempFile = Get-UniqueTempFile -Prefix $Component -Extension ".exe" Write-Log "Downloading $Component from: $url" if (-not (Invoke-WebRequestWithRetry -Uri $url -OutFile $tempFile -TimeoutSec 300)) { Write-Log "ERROR: Failed to download $Component" return $false } # Verify checksum before install if ($ExpectedChecksum) { $actualChecksum = Get-FileChecksum -FilePath $tempFile if ($actualChecksum -ne $ExpectedChecksum) { Write-Log "ERROR: Checksum mismatch for $Component" Write-Log " Expected: $ExpectedChecksum" Write-Log " Actual: $actualChecksum" Remove-Item $tempFile -Force -ErrorAction SilentlyContinue return $false } Write-Log "Checksum verified for $Component" } else { Write-Log "WARNING: No expected checksum available - skipping verification" } # Verify binary signature if (-not (Test-BinarySignature -FilePath $tempFile)) { Write-Log "ERROR: Binary signature verification failed for $Component" Remove-Item $tempFile -Force -ErrorAction SilentlyContinue return $false } # Create backup of existing binary $backupPath = $null if (Test-Path $TargetPath) { $backupPath = New-BinaryBackup -BinaryPath $TargetPath } # Attempt atomic replacement try { if (Test-Path $TargetPath) { # Use File.Replace for atomic operation (creates backup automatically) $backupForReplace = "$TargetPath.replacing" [System.IO.File]::Replace($tempFile, $TargetPath, $backupForReplace) Remove-Item $backupForReplace -Force -ErrorAction SilentlyContinue } else { # Target doesn't exist - just move Move-Item -Path $tempFile -Destination $TargetPath -Force -ErrorAction Stop } # Verify installed binary checksum if ($ExpectedChecksum) { $installedChecksum = Get-FileChecksum -FilePath $TargetPath if ($installedChecksum -ne $ExpectedChecksum) { Write-Log "ERROR: Post-install checksum mismatch for $Component" Write-Log " Expected: $ExpectedChecksum" Write-Log " Installed: $installedChecksum" # Rollback from backup if ($backupPath -and (Test-Path $backupPath)) { Write-Log "Rolling back to backup..." Copy-Item -Path $backupPath -Destination $TargetPath -Force -ErrorAction Stop Write-Log "Rollback completed" } return $false } Write-Log "Post-install verification passed for $Component (checksum: $installedChecksum)" } # Cleanup old backups Remove-OldBackups -BinaryPath $TargetPath -KeepCount $script:MaxBackups Write-Log "Successfully installed $Component" return $true } catch { Write-Log "ERROR: Failed to install $Component - $_" # Attempt rollback if ($backupPath -and (Test-Path $backupPath)) { Write-Log "Attempting rollback from backup..." try { Copy-Item -Path $backupPath -Destination $TargetPath -Force -ErrorAction Stop Write-Log "Rollback completed" } catch { Write-Log "ERROR: Rollback failed: $_" } } Remove-Item $tempFile -Force -ErrorAction SilentlyContinue return $false } } # Function to check if a component needs update based on checksum function Test-ComponentNeedsUpdate { param( [string]$Component, [string]$CurrentPath, [string]$BuildId, [string]$Arch, [switch]$ForceIfMissing # Force update if binary is missing ) Write-Log "Checking $Component at $CurrentPath for build $BuildId..." # Check if binary exists if (-not (Test-Path $CurrentPath)) { Write-Log " DECISION: $Component not found at path - NEEDS INSTALL" return $true } # Get expected checksum from manifest $expectedChecksum = Get-ExpectedChecksum -Component $Component -BuildId $BuildId -Arch $Arch if (-not $expectedChecksum) { # No checksum in manifest - cannot verify, assume up to date Write-Log " DECISION: No checksum in manifest for $Component - ASSUMING UP TO DATE (cannot verify)" return $false } # Compute current checksum $currentChecksum = Get-FileChecksum -FilePath $CurrentPath if (-not $currentChecksum) { Write-Log " DECISION: Could not compute checksum for $Component - ASSUMING UP TO DATE (read error)" return $false } # Compare checksums if ($currentChecksum -eq $expectedChecksum) { Write-Log " DECISION: $Component is UP TO DATE (checksums match)" return $false } # Checksums differ - update needed Write-Log " DECISION: $Component NEEDS UPDATE (checksum mismatch)" Write-Log " Current: $currentChecksum" Write-Log " Expected: $expectedChecksum" return $true } # Main script execution wrapped in try/finally for cleanup try { Write-Log "=== Neptune Agent Health Check Started ===" Write-Log "Signature verification mode: $script:SignatureMode" # Acquire mutex lock to prevent concurrent execution if (-not (Enter-HealthCheckLock)) { exit 0 } # Resolve Neptune installation paths Write-Log "Performing prerequisite checks..." if (-not (Resolve-NeptunePaths)) { Write-Log "ERROR: Neptune installation not found" Write-Log "Please reinstall the Neptune agent" exit 1 } Set-NeptuneCATrust # Verify service exists try { $null = Get-Service -Name "neptune-agent" -ErrorAction Stop } catch { Write-Log "ERROR: Neptune agent service is not installed" Write-Log "Please reinstall the Neptune agent" exit 1 } # Ensure data directory exists if ($script:DataDir -and -not (Test-Path $script:DataDir)) { try { New-Item -Path $script:DataDir -ItemType Directory -Force -ErrorAction Stop | Out-Null Write-Log "Created data directory: $script:DataDir" } catch { Write-Log "WARNING: Failed to create data directory: $_" } } # Cleanup old backups at startup to prevent accumulation from previous failed updates Write-Log "=== STARTUP BACKUP CLEANUP ===" if ($script:LauncherPath) { Remove-OldBackups -BinaryPath $script:LauncherPath -KeepCount $script:MaxBackups } if ($script:UpdaterPath) { Remove-OldBackups -BinaryPath $script:UpdaterPath -KeepCount $script:MaxBackups } if ($script:AgentPath) { Remove-OldBackups -BinaryPath $script:AgentPath -KeepCount $script:MaxBackups } # Add random delay (0-60 seconds) to prevent all agents from running simultaneously $RandomDelay = Get-Random -Minimum 0 -Maximum 61 Write-Log "Random delay: ${RandomDelay}s" Start-Sleep -Seconds $RandomDelay # Check if service is running FIRST and verify process health $service = Get-Service -Name "neptune-agent" -ErrorAction SilentlyContinue $serviceRunning = $service -and $service.Status -eq "Running" $agentProcessRunning = Test-ProcessRunning -ProcessName "neptune-agent" Write-Log "Service status: $(if ($serviceRunning) { 'RUNNING' } else { $service.Status })" Write-Log "Process status: $(if ($agentProcessRunning) { 'neptune-agent RUNNING' } else { 'neptune-agent NOT FOUND' })" if ($serviceRunning -and $agentProcessRunning) { # Service and process are running - also check if the agent is actually responsive # A hung or zombie agent will have stale log files (no writes for 30+ minutes) if (Test-AgentResponsive -MaxStaleMinutes 30) { Write-Log "Neptune agent service and process are running - health check passed" exit 0 } # Agent appears hung/zombie - force restart Write-Log "WARNING: Neptune agent appears unresponsive (stale logs) - forcing service restart" try { Restart-Service -Name "neptune-agent" -Force -ErrorAction Stop # Wait for the service chain to start: launcher -> updater -> agent # This takes longer than a simple service start due to the multi-process architecture Start-Sleep -Seconds 15 # Only check if process is running after restart - log freshness check is unreliable # here because the new agent process may not have written logs yet if (Test-ProcessRunning -ProcessName "neptune-agent") { Write-Log "Service restarted successfully after hung process detection - process is running" exit 0 } } catch { Write-Log "ERROR: Failed to restart hung service: $_" } # Fall through to the recovery logic below Write-Log "Service restart after hung detection did not fully recover - continuing with recovery" } if (-not (Test-LauncherPreflight)) { Write-Log "WARNING: Launcher preflight failed, attempting org-less reinstall" if (Invoke-OrglessReinstall) { if (Start-ServiceWithTimeout -ServiceName "neptune-agent" -TimeoutSeconds 30) { if (Test-ProcessRunning -ProcessName "neptune-agent") { Write-Log "Service and process are running after org-less reinstall" exit 0 } } } Write-Log "ERROR: Launcher preflight failed and reinstall did not recover service" exit 1 } Write-Log "Service not healthy, attempting restart..." if (Start-ServiceWithTimeout -ServiceName "neptune-agent" -TimeoutSeconds 30) { if (Test-ProcessRunning -ProcessName "neptune-agent") { Write-Log "Service and process are now running - health check passed" exit 0 } Write-Log "WARNING: Service started but process not found" } else { Write-Log "ERROR: Failed to start neptune-agent service" } Write-Log "Attempting org-less reinstall after failed restart..." if (Invoke-OrglessReinstall) { if (Start-ServiceWithTimeout -ServiceName "neptune-agent" -TimeoutSeconds 30) { if (Test-ProcessRunning -ProcessName "neptune-agent") { Write-Log "Service and process are running after org-less reinstall" exit 0 } } } Write-Log "ERROR: Org-less reinstall did not recover service" exit 1 # Should not reach here Write-Log "Health check completed" exit 0 } finally { # Cleanup temp files and release mutex Invoke-Cleanup } # SIG # Begin signature block # MIIzLAYJKoZIhvcNAQcCoIIzHTCCMxkCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC2zZrFC68DC5wi # +7BHzhhpeB25j9oeLS3N9I32F3lkRaCCGykwggXMMIIDtKADAgECAhBUmNLR1FsZ # lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu # dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy # MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv # c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo # b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD # Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH # or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD # PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV # qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70 # ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1 # vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3 # W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK # xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg # trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj # O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7 # swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE # AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ # T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn # LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c # Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF # xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx # AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8 # FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY # lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2 # 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY # uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al # GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk # ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T # 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggaHMIIEb6ADAgECAhMzAAEx4xXG # XHpnv/j0AAAAATHjMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ # RCBWZXJpZmllZCBDUyBBT0MgQ0EgMDQwHhcNMjYwNTIwMTMzOTMxWhcNMjYwNTIz # MTMzOTMxWjBJMQswCQYDVQQGEwJGUjEUMBIGA1UEBxMLU0FJTlQtQ0xPVUQxETAP # BgNVBAoTCEVBU1lURUFNMREwDwYDVQQDEwhFQVNZVEVBTTCCAaIwDQYJKoZIhvcN # AQEBBQADggGPADCCAYoCggGBAJK0Cnv13nEPgU9RzlamJEkZHsQbQbmSh8hJvoqM # k9Ss+vIkW/6EMUOgKfYtATLuHxUrXNv1EKHoa4XDg3rjwC5xe1I7lhjhhIbAM1y+ # BY8gZ6qp8N0UMKrWWgOpYQZJKv4y8ddXXCym+PB82ln8bFMofWKMG/G2/cEt3D8c # ebaPqPu12lLICjE6zJzIPxpsNSN68/zHflAtqnsJ5/tPgy5tN6qmHg+7MQqavKhB # YUZu7pfMoIWWOjFNVLXHhYqkoJSNAAzbohvoHagJv3PKlUoyNYESQ0PXfnk6xOzB # G5du7AGgWDuxDhINxrhd1qXdf2cviOTb1rFCghfxvqOGZNF47O+LHwEVvl0ZFqRe # b5hnUmunSOMD0NJVaa/1Lh+609P2YdZuHVTZF1K5C7P8CHE+ZkCscw3oeUwZHBGC # 9JnVHa9UCXQzar5EQ0L9W/SY5FgGL/KkDqYdoghSlNvwpBIgZWSvjP0yMNv1Lr/W # 8sLR95h/MbTDkQ32+P/i+R7i6QIDAQABo4IB1TCCAdEwDAYDVR0TAQH/BAIwADAO # BgNVHQ8BAf8EBAMCB4AwPAYDVR0lBDUwMwYKKwYBBAGCN2EBAAYIKwYBBQUHAwMG # GysGAQQBgjdhg6qkjSqcqfcxgd+UlSiBs/ygQDAdBgNVHQ4EFgQUO+yqnoOeUunw # V3OBXdr9Bpz1x78wHwYDVR0jBBgwFoAUayVB3vtrfP0YgAotf492XapzPbgwZwYD # VR0fBGAwXjBcoFqgWIZWaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j # cmwvTWljcm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENTJTIwQU9DJTIwQ0ElMjAw # NC5jcmwwdAYIKwYBBQUHAQEEaDBmMGQGCCsGAQUFBzAChlhodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIwVmVyaWZp # ZWQlMjBDUyUyMEFPQyUyMENBJTIwMDQuY3J0MFQGA1UdIARNMEswSQYEVR0gADBB # MD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0Rv # Y3MvUmVwb3NpdG9yeS5odG0wDQYJKoZIhvcNAQEMBQADggIBAEHNZQOxO4Vqbs0R # lYjHHdAJeJybddsb7Yn6ghde56HbEcZAZ0BuvRJxpf3g16e0MdbJiP4dKQmV8K1w # YmXE2Fcxk6Rryg/bqEuk2R0ZRcoc/RxKmkC45iHdAX1733xBL4FFjPPoebAC45fg # i/jQO0WSVuInW8UANcVb2Sos49eCmzvNdCMEmSNeouHz7/yg4Mx0G5zoFpOiUwTs # h789FJUctOqg52ldNWcl9iqsBVzmE7Z70AS/R3HNfy6WdeN5yhUPLUNhWtSzPkbe # 7wVe4qg2IHtsmHyqNsNq8mRXcJsKIzL0tVa4Ft3b5HZtyHuVkAYI9kBPy6BMrRRs # 9Os54MBS+k0PafNEN/BolvKBdzYJCWXMvxz121ivpJYPEwEa+xicl0Hz1Nbpntz+ # R6f/gtfAr5eI5pUNIL7VSw0hGqyS3Fe0EpUn14+3jJGoSOL+vm5SbpNYFenK36Cm # f7yaUE8aOA0V3EQJVQ7kiHInO/ehlv9mfqmJhm8jP6UEesL0YzsZ55XTbB3tgRRr # weMgu/H1yUPs1w52S0l/OSJ0Ta4yVh1r34fF/AhZlaOnfXLVnNcB+fevmbBAfvL+ # jUphjxE2EMORPYMXRbHlNxLGxc7GsaAzSaL0PXuy4DVesa3eolIFaQDQ58ybM4Hf # LSH/gEDyOGHLUiMEP3CyVTto/7H8MIIHKDCCBRCgAwIBAgITMwAAABYxko2SAmV7 # mgAAAAAAFjANBgkqhkiG9w0BAQwFADBjMQswCQYDVQQGEwJVUzEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMTQwMgYDVQQDEytNaWNyb3NvZnQgSUQgVmVy # aWZpZWQgQ29kZSBTaWduaW5nIFBDQSAyMDIxMB4XDTI2MDMyNjE4MTEyOVoXDTMx # MDMyNjE4MTEyOVowWjELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjErMCkGA1UEAxMiTWljcm9zb2Z0IElEIFZlcmlmaWVkIENTIEFP # QyBDQSAwNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMpV+sjb6Akw # z/RtDk5Uo1284BLMttRQy9e5/5WXtga6h89pkdWjeAwcXmKdP4YxKkhx8hn2q1dT # VQbryvLy81tC04vg2bfSJ9emojX4HKIBYs7VhPRafMbtc866hN55aw1m/kWaPEKx # F4Fm/LPLMLJdlu7URB8nFZMfh5tTC0CJb2uox/14OP/BiGIR8214lXdkV6JsPbO0 # Iev0mEV133tducIeBChMipzTZfnGVEq1QYFr7460cEGOIn+7AGfNOSq7gWOlmNB4 # m2uZ1r66vUJPaN+VYgH/Kmfu7tX229b3Alsli38fYS0nQY41bElntMS7yNY+Kd04 # 7eXM8/tS3NL+ZUNX3ge5xZqW2aytrrNIbwGgQnzsgzxBJvu9+b87jUWFiRS/z1Yi # PkRLY7iTHKIxJ973kIyK8K5itE/aEq/Ht6A8ytaAMMGTEwuCspk72FE1Qyby+TfD # lv1KiAc7IlWHHIWbxoVd8jGCoMXhLSDhuFuGfOWOZKUIuW6YxlxcUYOOkXa02gC7 # dUYUZi0e1NI1Uq9mmAHdvjKdqgORs9/5aDjnbeO3hWd5qwGBmELWytkjY0KcIEF+ # CMurTimoQoBYSiHJbYrb0pKSQe+3lVoTVpzdi36jKI7MxLnu1fBAMksa8uD85cU7 # RU29zMOd18h9dgTEH9pvY+4xLB3lUgE7AgMBAAGjggHcMIIB2DAOBgNVHQ8BAf8E # BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGslQd77a3z9GIAKLX+P # dl2qcz24MFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIBFjNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wGQYJ # KwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNV # HSMEGDAWgBTZQSmwDw9jbO9p1/XNKZ6kSGow5jBwBgNVHR8EaTBnMGWgY6Bhhl9o # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBJ # RCUyMFZlcmlmaWVkJTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDIxLmNybDB9 # BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENv # ZGUlMjBTaWduaW5nJTIwUENBJTIwMjAyMS5jcnQwDQYJKoZIhvcNAQEMBQADggIB # AAbVUF5UdNVEGWOVxkPciIzHA/IyNDIs1oSdW7mY4ObaFEEl8fwawEsBOascBzwX # juOaTkKelQ+IiC4JvDpn8pWPgOyiKrbbw1Iswt7AV8YyuLu7A0YshILlxyny9R0A # MMQixLZ0T5Aj07CQOm/de5QPt36ECBwtVww6XUCx8Rm2xl6eEa9JhSub8W4urQGo # QYv0Zczlz4ej2ryj5Wf9L4ZZA3bL6CRE7XmzSQjTmdSmr904PiuL2uBWzq2KkR3R # hmoaAP4Jk2JplasNM5Bs0+dx3YX2o6xRrbSaJiu6hPk/AkBoj5BCJMTZkl4wk6Q6 # nOFNSCpUxnBmJ0RRkoKq51p5ADTxbCeRAx8rIfNpTyPjxQtxTiFTC8yy/t9K6s57 # 0YB1FAI+8XxZxIrmgMd7xVUkzi2/oooKb3UeovH0lqYGBEjpHZJ9jee7boQbOe7S # sKD/vr3PzFabg9VwLZlovpWUpWoWnU2w3xiozJ/m35tsZVvT2egUpwkb9TDIaXFG # ZAGV94FRDHn/K2XNnOwSeGskX19MB+N7yPc51fmUSd49MVAtGW/NkUGpRech5Aq/ # d8dCML2bZ+j9nTUMgj+qnd3wuEZVRbQEIbTh9HAck0fyKqoIb+qh8y/6TKbYDEEO # cfM2BGMvrQk4WIGCK6u8uFhA2EH3kpaiMUDqyFCeCCQxMIIHnjCCBYagAwIBAgIT # MwAAAAeHozSje6WOHAAAAAAABzANBgkqhkiG9w0BAQwFADB3MQswCQYDVQQGEwJV # UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMUgwRgYDVQQDEz9NaWNy # b3NvZnQgSWRlbnRpdHkgVmVyaWZpY2F0aW9uIFJvb3QgQ2VydGlmaWNhdGUgQXV0 # aG9yaXR5IDIwMjAwHhcNMjEwNDAxMjAwNTIwWhcNMzYwNDAxMjAxNTIwWjBjMQsw # CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTQwMgYD # VQQDEytNaWNyb3NvZnQgSUQgVmVyaWZpZWQgQ29kZSBTaWduaW5nIFBDQSAyMDIx # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsvDArxmIKOLdVHpMSWxp # CFUJtFL/ekr4weslKPdnF3cpTeuV8veqtmKVgok2rO0D05BpyvUDCg1wdsoEtuxA # CEGcgHfjPF/nZsOkg7c0mV8hpMT/GvB4uhDvWXMIeQPsDgCzUGzTvoi76YDpxDOx # hgf8JuXWJzBDoLrmtThX01CE1TCCvH2sZD/+Hz3RDwl2MsvDSdX5rJDYVuR3bjaj # 2QfzZFmwfccTKqMAHlrz4B7ac8g9zyxlTpkTuJGtFnLBGasoOnn5NyYlf0xF9/bj # VRo4Gzg2Yc7KR7yhTVNiuTGH5h4eB9ajm1OCShIyhrKqgOkc4smz6obxO+HxKeJ9 # bYmPf6KLXVNLz8UaeARo0BatvJ82sLr2gqlFBdj1sYfqOf00Qm/3B4XGFPDK/H04 # kteZEZsBRc3VT2d/iVd7OTLpSH9yCORV3oIZQB/Qr4nD4YT/lWkhVtw2v2s0TnRJ # ubL/hFMIQa86rcaGMhNsJrhysLNNMeBhiMezU1s5zpusf54qlYu2v5sZ5zL0KvBD # LHtL8F9gn6jOy3v7Jm0bbBHjrW5yQW7S36ALAt03QDpwW1JG1Hxu/FUXJbBO2Aww # VG4Fre+ZQ5Od8ouwt59FpBxVOBGfN4vN2m3fZx1gqn52GvaiBz6ozorgIEjn+PhU # XILhAV5Q/ZgCJ0u2+ldFGjcCAwEAAaOCAjUwggIxMA4GA1UdDwEB/wQEAwIBhjAQ # BgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU2UEpsA8PY2zvadf1zSmepEhqMOYw # VAYDVR0gBE0wSzBJBgRVHSAAMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTAZBgkrBgEEAYI3 # FAIEDB4KAFMAdQBiAEMAQTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFMh+ # 0mqFKhvKGZgEByfPUBBPaKiiMIGEBgNVHR8EfTB7MHmgd6B1hnNodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBJZGVudGl0eSUy # MFZlcmlmaWNhdGlvbiUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUy # MDIwMjAuY3JsMIHDBggrBgEFBQcBAQSBtjCBszCBgQYIKwYBBQUHMAKGdWh0dHA6 # Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwSWRl # bnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRo # b3JpdHklMjAyMDIwLmNydDAtBggrBgEFBQcwAYYhaHR0cDovL29uZW9jc3AubWlj # cm9zb2Z0LmNvbS9vY3NwMA0GCSqGSIb3DQEBDAUAA4ICAQB/JSqe/tSr6t1mCttX # I0y6XmyQ41uGWzl9xw+WYhvOL47BV09Dgfnm/tU4ieeZ7NAR5bguorTCNr58HOcA # 1tcsHQqt0wJsdClsu8bpQD9e/al+lUgTUJEV80Xhco7xdgRrehbyhUf4pkeAhBEj # ABvIUpD2LKPho5Z4DPCT5/0TlK02nlPwUbv9URREhVYCtsDM+31OFU3fDV8BmQXv # 5hT2RurVsJHZgP4y26dJDVF+3pcbtvh7R6NEDuYHYihfmE2HdQRq5jRvLE1Eb59P # YwISFCX2DaLZ+zpU4bX0I16ntKq4poGOFaaKtjIA1vRElItaOKcwtc04CBrXSfyL # 2Op6mvNIxTk4OaswIkTXbFL81ZKGD+24uMCwo/pLNhn7VHLfnxlMVzHQVL+bHa9K # hTyzwdG/L6uderJQn0cGpLQMStUuNDArxW2wF16QGZ1NtBWgKA8Kqv48M8HfFqNi # fN6+zt6J0GwzvU8g0rYGgTZR8zDEIJfeZxwWDHpSxB5FJ1VVU1LIAtB7o9PXbjXz # GifaIMYTzU4YKt4vMNwwBmetQDHhdAtTPplOXrnI9SI6HeTtjDD3iUN/7ygbahmY # OHk7VB7fwT4ze+ErCbMh6gHV1UuXPiLciloNxH6K4aMfZN1oLVk6YFeIJEokuPgN # Pa6EnTiOL60cPqfny+Fq8UiuZzGCF1kwghdVAgEBMHEwWjELMAkGA1UEBhMCVVMx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjErMCkGA1UEAxMiTWljcm9z # b2Z0IElEIFZlcmlmaWVkIENTIEFPQyBDQSAwNAITMwABMeMVxlx6Z7/49AAAAAEx # 4zANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkG # CSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEE # AYI3AgEVMC8GCSqGSIb3DQEJBDEiBCDMd8BWtAmW6rJbHsj5HdvPa8YExRY0XgVH # DIs/894QqzANBgkqhkiG9w0BAQEFAASCAYBUcdua5FmKaEvpquGYbrNqgWPMUosZ # sJVvxhRdWcE8eprdhQ+Q4U0/fiuJFD42YmxILKftnO4iVQVr80ZCBvYln8liV4eo # IqagGvRYzcs14inCMPTSydpM4tMD74qT/FXjqCBcZ4HvoNRJsRUb/DQ75UgcPxBv # DSPDaxQ5QxxxTXP6NC0jjulu9WtUlEW0jtSt4/508InFVC1aReUM6ocPaN5Q4ad0 # QEup7GvgGKqSzKHvjmp+prp3c5iTdlYYoRpfopw5ULm6f5QeTXvAGPk6FGDQ15q9 # Lp5RZhicsPJfZTegKx+UlakngmnV4B9T48/nRYc0XrLNYZ61fT7e/o7/Rh+ltdp7 # +C2zu+zHPypUFL7bix154S6UK4PlzNGQwL3HJd2x23VKmf4NLzZFVnjPEYuwa1UW # Tc/N5kOhK4L/lq3jfN4diWcHWGUmJoxFeLCxRKrO292H1VNwX1q0hrmGEvs1Eyhe # 8Ye1A7NljTdrmf0tTAusOfNbPiQsd5l9sq+hghSyMIIUrgYKKwYBBAGCNwMDATGC # FJ4wghSaBgkqhkiG9w0BBwKgghSLMIIUhwIBAzEPMA0GCWCGSAFlAwQCAQUAMIIB # agYLKoZIhvcNAQkQAQSgggFZBIIBVTCCAVECAQEGCisGAQQBhFkKAwEwMTANBglg # hkgBZQMEAgEFAAQgm/Lzpoq7zYUFT5rGOrCFbOh7VW05YNMs7pe5/2MRdd8CBmn0 # k0O9bxgTMjAyNjA1MjExMjQ3NDYuMjY3WjAEgAIB9KCB6aSB5jCB4zELMAkGA1UE # BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc # BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0 # IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMScwJQYDVQQLEx5uU2hpZWxkIFRT # UyBFU046N0ExQS0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMg # UlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5oIIPKTCCB4IwggVqoAMCAQICEzMA # AAAF5c8P/2YuyYcAAAAAAAUwDQYJKoZIhvcNAQEMBQAwdzELMAkGA1UEBhMCVVMx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjFIMEYGA1UEAxM/TWljcm9z # b2Z0IElkZW50aXR5IFZlcmlmaWNhdGlvbiBSb290IENlcnRpZmljYXRlIEF1dGhv # cml0eSAyMDIwMB4XDTIwMTExOTIwMzIzMVoXDTM1MTExOTIwNDIzMVowYTELMAkG # A1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UE # AxMpTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENBIDIwMjAwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCefOdSY/3gxZ8FfWO1BiKjHB7X # 55cz0RMFvWVGR3eRwV1wb3+yq0OXDEqhUhxqoNv6iYWKjkMcLhEFxvJAeNcLAyT+ # XdM5i2CgGPGcb95WJLiw7HzLiBKrxmDj1EQB/mG5eEiRBEp7dDGzxKCnTYocDOcR # r9KxqHydajmEkzXHOeRGwU+7qt8Md5l4bVZrXAhK+WSk5CihNQsWbzT1nRliVDwu # nuLkX1hyIWXIArCfrKM3+RHh+Sq5RZ8aYyik2r8HxT+l2hmRllBvE2Wok6IEaAJa # nHr24qoqFM9WLeBUSudz+qL51HwDYyIDPSQ3SeHtKog0ZubDk4hELQSxnfVYXdTG # ncaBnB60QrEuazvcob9n4yR65pUNBCF5qeA4QwYnilBkfnmeAjRN3LVuLr0g0FXk # qfYdUmj1fFFhH8k8YBozrEaXnsSL3kdTD01X+4LfIWOuFzTzuoslBrBILfHNj8Rf # OxPgjuwNvE6YzauXi4orp4Sm6tF245DaFOSYbWFK5ZgG6cUY2/bUq3g3bQAqZt65 # KcaewEJ3ZyNEobv35Nf6xN6FrA6jF9447+NHvCjeWLCQZ3M8lgeCcnnhTFtyQX3X # gCoc6IRXvFOcPVrr3D9RPHCMS6Ckg8wggTrtIVnY8yjbvGOUsAdZbeXUIQAWMs0d # 3cRDv09SvwVRd61evQIDAQABo4ICGzCCAhcwDgYDVR0PAQH/BAQDAgGGMBAGCSsG # AQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRraSg6NS9IY0DPe9ivSek+2T3bITBUBgNV # HSAETTBLMEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3Nv # ZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsG # AQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB # Af8wHwYDVR0jBBgwFoAUyH7SaoUqG8oZmAQHJ89QEE9oqKIwgYQGA1UdHwR9MHsw # eaB3oHWGc2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jv # c29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRpZmlj # YXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcmwwgZQGCCsGAQUFBwEBBIGHMIGEMIGB # BggrBgEFBQcwAoZ1aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0 # cy9NaWNyb3NvZnQlMjBJZGVudGl0eSUyMFZlcmlmaWNhdGlvbiUyMFJvb3QlMjBD # ZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIwMjAuY3J0MA0GCSqGSIb3DQEBDAUA # A4ICAQBfiHbHfm21WhV150x4aPpO4dhEmSUVpbixNDmv6TvuIHv1xIs174bNGO/i # lWMm+Jx5boAXrJxagRhHQtiFprSjMktTliL4sKZyt2i+SXncM23gRezzsoOiBhv1 # 4YSd1Klnlkzvgs29XNjT+c8hIfPRe9rvVCMPiH7zPZcw5nNjthDQ+zD563I1nUJ6 # y59TbXWsuyUsqw7wXZoGzZwijWT5oc6GvD3HDokJY401uhnj3ubBhbkR83RbfMvm # zdp3he2bvIUztSOuFzRqrLfEvsPkVHYnvH1wtYyrt5vShiKheGpXa2AWpsod4OJy # T4/y0dggWi8g/tgbhmQlZqDUf3UqUQsZaLdIu/XSjgoZqDjamzCPJtOLi2hBwL+K # sCh0Nbwc21f5xvPSwym0Ukr4o5sCcMUcSy6TEP7uMV8RX0eH/4JLEpGyae6Ki8JY # g5v4fsNGif1OXHJ2IWG+7zyjTDfkmQ1snFOTgyEX8qBpefQbF0fx6URrYiarjmBp # rwP6ZObwtZXJ23jK3Fg/9uqM3j0P01nzVygTppBabzxPAh/hHhhls6kwo3QLJ6No # 803jUsZcd4JQxiYHHc+Q/wAMcPUnYKv/q2O444LO1+n6j01z5mggCSlRwD9faBIy # SAcA9S8h22hIAcRQqIGEjolCK9F6nK9ZyX4lhthsGHumaABdWzCCB58wggWHoAMC # AQICEzMAAABbSrWNQTJt3HQAAAAAAFswDQYJKoZIhvcNAQEMBQAwYTELMAkGA1UE # BhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp # TWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZXN0YW1waW5nIENBIDIwMjAwHhcNMjYw # MTA4MTg1OTA1WhcNMjcwMTA3MTg1OTA1WjCB4zELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl # cmF0aW9ucyBMaW1pdGVkMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0ExQS0w # NUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUgU3Rh # bXBpbmcgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA # kFTMFtueUNd57QHQoPkbj/jvm2EXJ9y0LK4RJNZBe+UuLhbH+13apR16riJ156Dp # VaGI4d+7fAlXhNQZJG2qH0JyvUGaIEq/2K4WmAfIgG7lDHfxmzHCUV5dVL5mokkq # ddFsM1B1xhKgL/pzSFAn88fnQMFENCQ9dXDIWLMutEf0CWsl5SDsEp5PbfN+1Lz8 # o4ku8QRsc4XqlI5jdlWmtlRZtaNbBFOagdpD8Ty+ta0s3IQn5vTz1VbUiStre3gZ # MHlZvLcIvUrbNicDEEi9p+wowXKP065cdxM8owOgVIx5qYb0wo4xvq6gbU+N2cOC # ws/oQ4xFLOssvuMQPWZsH1FJ31+G3L4dCvq9mCwGfqhTL5hOk1UuyTB21QzzZZgC # Q/O2U63cCIvSrJXv9TeP+6re8cyM8zTDTfjQzns16LSDgEJwy3R1uqhz3VWAJvf/ # fqwdAA2ie2fUc4XaguTzX3RBFLjeKwdWtrwfyx/n4aWohixiIIpfTgdmI7Nlbzbq # dUjp377yXJN5aamP3RRr249smFWPATeiHq07nXTJKqZIxIsQ3Tuncht7cToEBvbD # 3etbNvbr52lK2FsoXiQCmh+oGxY9fgwS0cpI5+0+ZVMJDju2CGtW4eJr2Nj4eyPT # Wbgpbha2SZWbcvqExkQIxriyMzEBfP5tf8AmFZN7pNkCAwEAAaOCAcswggHHMB0G # A1UdDgQWBBTv8upSVZZiFcl1fCBgrHhvwa/StjAfBgNVHSMEGDAWgBRraSg6NS9I # Y0DPe9ivSek+2T3bITBsBgNVHR8EZTBjMGGgX6BdhltodHRwOi8vd3d3Lm1pY3Jv # c29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBU # aW1lc3RhbXBpbmclMjBDQSUyMDIwMjAuY3JsMHkGCCsGAQUFBwEBBG0wazBpBggr # BgEFBQcwAoZdaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9N # aWNyb3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3RhbXBpbmclMjBDQSUyMDIw # MjAuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYD # VR0PAQH/BAQDAgeAMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYB # BQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBv # c2l0b3J5Lmh0bTAIBgZngQwBBAIwDQYJKoZIhvcNAQEMBQADggIBAAAf7N35cqHg # 7FdgxYWa2CKVcBAZy06MJQHXD+4GIL85dwfchrj9dt1SErMVtqJNsgTq9hkp3Wni # 7uco4uRrDKYAxXK47stKXqssq21kjIuFaNMrTNc7PS7jEur35tG0EQom8DqwPmcn # AfUg7rPViLPK4hGhqUwKdutSLF9bFCfhMCY3u326T5fYVROERrd7DNHCG0b7HBoB # ssyTFGZHbgmd9d3VXEqj3T6btbO6i/3pS6DHnBl17CIgibVlZOPiUIke6nrv0tw5 # ru0DEkyKlVpKW1Af1+b1M4pzOV/G1a4FwtTh25l+rCCwguwfs8yRxfXPBDNAPTIC # 0+GdjP0o0bXbltf6KKU57VLxEeq/ZtsGkylqjiRxS9Ajp0yApG8WabV4tuFI05Cm # UMxMYPW01V00aQj3qNS762uhSNYwyLjpNB8EAfG0NOlGEi7/zu8BVDxnpEeEXF6z # PgR3klOFohBEDLoZw78mT5DMPOhnRqtEiQiwYnutmA5UCPH1y1/DyUf1F+NzAHfB # 0YFg0w1UmpClRqLZNp11/mlfNNkQciosQXndKsGMh4iehCs/tTlWVeIxCzF7At0g # 2sATaXZNHcoGKRv5FBHKBtOnyOPbKILQ0JTAb4r6d2CU3lExteMVbpoprn1er5vx # fMr8Mr4Am2A6keAm/xCuTrYD63A5Us6mMYID1DCCA9ACAQEweDBhMQswCQYDVQQG # EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylN # aWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMAITMwAAAFtK # tY1BMm3cdAAAAAAAWzANBglghkgBZQMEAgEFAKCCAS0wGgYJKoZIhvcNAQkDMQ0G # CyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCAS8qR0+uyRcdrsEK/KHr9V3Yqz # kabMa4SIxsqgeP1j3zCB3QYLKoZIhvcNAQkQAi8xgc0wgcowgccwgaAEIC8xA1Vd # nRvTHGUbDxf/cgTJs5u5PprlbV3rUJb5wYPvMHwwZaRjMGExCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jv # c29mdCBQdWJsaWMgUlNBIFRpbWVzdGFtcGluZyBDQSAyMDIwAhMzAAAAW0q1jUEy # bdx0AAAAAABbMCIEIBRb5uXDSGmfjRHNtoAAKCAqAwXztHr3ul4gQ4rbOXXRMA0G # CSqGSIb3DQEBCwUABIICAEt7nxz2QgkCCsyHMq+vPghf5dgAr4xBRWAOjRpJgkyz # msppT+MBQCvN+5UikuA2Hm5RCtitp/mcpnHCKNBjSmgAg11KfPvsnmaXI4tELN4v # KTwb7dzJD4A/ScpyTUtr/AXY15c6/lEO6epEXIZNgC3Te33su2vPLNsplj0Cxobb # WFIC/wLFOr2bMUPGMX8y/C8sHYld27ISaOFX3IaGB0zWseA+47Du7MLSI7gDWJFZ # tttZQG2vO2DMPj0sC8VSAlu6BQIbXS2LOeXQGg8cKOmqNPMhDbebP7U4ilsqIXb2 # PQYl9Tjkz+cTvjI4Hkdw1SbPRkNZVFEHALJXnvJKDX0sC8zrJyxSHA//n8P7yOEx # YQKu+dp59Ia0Gg4n0B7MocOIvgDAiYZhGui5FWPnkg5BpSggrr6ziHO7LE31BWKw # od5hRgxNvZUrN8guruNpIxc2zH19R1+xoW76+26MhedSNoftWdCtqlGzkRRa411I # wKInZjAAOOSy4VExsHGNSYUpnKsmzU1y9+CPwLXb4UC9WNhDd7y7dMjFPi4tZTWK # ZeZHTcoYYLPT/4jivwFqzHr96GdmbqNUulNZgoDXAY0iCAiRXYLSIxS03RaD9B+T # weZGQ9++8ref28zorF9itgjYpp42MBF4zcE2XJ8NmrIfVAE1y21tTQ+OMY4njSdq # SIG # End signature block