UNPKG

@push.rocks/smartproxy

Version:

A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.

730 lines 62.7 kB
import * as plugins from '../../plugins.js'; import { HttpProxy } from '../http-proxy/index.js'; import { CertStore } from './cert-store.js'; import { logger } from '../../core/utils/logger.js'; import { SocketHandlers } from './utils/route-helpers.js'; export class SmartCertManager { constructor(routes, certDir = './certs', acmeOptions, initialState) { this.routes = routes; this.certDir = certDir; this.acmeOptions = acmeOptions; this.initialState = initialState; this.smartAcme = null; this.httpProxy = null; this.renewalTimer = null; this.pendingChallenges = new Map(); this.challengeRoute = null; // Track certificate status by route name this.certStatus = new Map(); // Global ACME defaults from top-level configuration this.globalAcmeDefaults = null; // Flag to track if challenge route is currently active this.challengeRouteActive = false; // Flag to track if provisioning is in progress this.isProvisioning = false; // ACME state manager reference this.acmeStateManager = null; // Whether to fallback to ACME if custom provision fails this.certProvisionFallbackToAcme = true; this.certStore = new CertStore(certDir); // Apply initial state if provided if (initialState) { this.challengeRouteActive = initialState.challengeRouteActive || false; } } setHttpProxy(httpProxy) { this.httpProxy = httpProxy; } /** * Set the ACME state manager */ setAcmeStateManager(stateManager) { this.acmeStateManager = stateManager; } /** * Set global ACME defaults from top-level configuration */ setGlobalAcmeDefaults(defaults) { this.globalAcmeDefaults = defaults; } /** * Set custom certificate provision function */ setCertProvisionFunction(fn) { this.certProvisionFunction = fn; } /** * Set whether to fallback to ACME if custom provision fails */ setCertProvisionFallbackToAcme(fallback) { this.certProvisionFallbackToAcme = fallback; } /** * Set callback for updating routes (used for challenge routes) */ setUpdateRoutesCallback(callback) { this.updateRoutesCallback = callback; try { logger.log('debug', 'Route update callback set successfully', { component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log('[DEBUG] Route update callback set successfully'); } } /** * Initialize certificate manager and provision certificates for all routes */ async initialize() { // Create certificate directory if it doesn't exist await this.certStore.initialize(); // Initialize SmartAcme if we have any ACME routes const hasAcmeRoutes = this.routes.some(r => r.action.tls?.certificate === 'auto'); if (hasAcmeRoutes && this.acmeOptions?.email) { // Create HTTP-01 challenge handler const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); // Set up challenge handler integration with our routing this.setupChallengeHandler(http01Handler); // Create SmartAcme instance with built-in MemoryCertManager and HTTP-01 handler this.smartAcme = new plugins.smartacme.SmartAcme({ accountEmail: this.acmeOptions.email, environment: this.acmeOptions.useProduction ? 'production' : 'integration', certManager: new plugins.smartacme.certmanagers.MemoryCertManager(), challengeHandlers: [http01Handler] }); await this.smartAcme.start(); // Add challenge route once at initialization if not already active if (!this.challengeRouteActive) { logger.log('info', 'Adding ACME challenge route during initialization', { component: 'certificate-manager' }); await this.addChallengeRoute(); } else { logger.log('info', 'Challenge route already active from previous instance', { component: 'certificate-manager' }); } } // Skip automatic certificate provisioning during initialization // This will be called later after ports are listening logger.log('info', 'Certificate manager initialized. Deferring certificate provisioning until after ports are listening.', { component: 'certificate-manager' }); // Start renewal timer this.startRenewalTimer(); } /** * Provision certificates for all routes that need them */ async provisionAllCertificates() { const certRoutes = this.routes.filter(r => r.action.tls?.mode === 'terminate' || r.action.tls?.mode === 'terminate-and-reencrypt'); // Set provisioning flag to prevent concurrent operations this.isProvisioning = true; try { for (const route of certRoutes) { try { await this.provisionCertificate(route, true); // Allow concurrent since we're managing it here } catch (error) { logger.log('error', `Failed to provision certificate for route ${route.name}`, { routeName: route.name, error, component: 'certificate-manager' }); } } } finally { this.isProvisioning = false; } } /** * Provision certificate for a single route */ async provisionCertificate(route, allowConcurrent = false) { const tls = route.action.tls; if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) { return; } // Check if provisioning is already in progress (prevent concurrent provisioning) if (!allowConcurrent && this.isProvisioning) { logger.log('info', `Certificate provisioning already in progress, skipping ${route.name}`, { routeName: route.name, component: 'certificate-manager' }); return; } const domains = this.extractDomainsFromRoute(route); if (domains.length === 0) { logger.log('warn', `Route ${route.name} has TLS termination but no domains`, { routeName: route.name, component: 'certificate-manager' }); return; } const primaryDomain = domains[0]; if (tls.certificate === 'auto') { // ACME certificate await this.provisionAcmeCertificate(route, domains); } else if (typeof tls.certificate === 'object') { // Static certificate await this.provisionStaticCertificate(route, primaryDomain, tls.certificate); } } /** * Provision ACME certificate */ async provisionAcmeCertificate(route, domains) { const primaryDomain = domains[0]; const routeName = route.name || primaryDomain; // Check if we already have a valid certificate const existingCert = await this.certStore.getCertificate(routeName); if (existingCert && this.isCertificateValid(existingCert)) { logger.log('info', `Using existing valid certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' }); await this.applyCertificate(primaryDomain, existingCert); this.updateCertStatus(routeName, 'valid', existingCert.source || 'acme', existingCert); return; } // Check for custom provision function first if (this.certProvisionFunction) { try { logger.log('info', `Attempting custom certificate provision for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' }); const result = await this.certProvisionFunction(primaryDomain); if (result === 'http01') { logger.log('info', `Custom function returned 'http01', falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' }); // Continue with existing ACME logic below } else { // Use custom certificate const customCert = result; // Convert to internal certificate format const certData = { cert: customCert.publicKey, key: customCert.privateKey, ca: '', issueDate: new Date(), expiryDate: this.extractExpiryDate(customCert.publicKey), source: 'custom' }; // Store and apply certificate await this.certStore.saveCertificate(routeName, certData); await this.applyCertificate(primaryDomain, certData); this.updateCertStatus(routeName, 'valid', 'custom', certData); logger.log('info', `Custom certificate applied for ${primaryDomain}`, { domain: primaryDomain, expiryDate: certData.expiryDate, component: 'certificate-manager' }); return; } } catch (error) { logger.log('error', `Custom cert provision failed for ${primaryDomain}: ${error.message}`, { domain: primaryDomain, error: error.message, component: 'certificate-manager' }); // Check if we should fallback to ACME if (!this.certProvisionFallbackToAcme) { throw error; } logger.log('info', `Falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' }); } } if (!this.smartAcme) { throw new Error('SmartAcme not initialized. This usually means no ACME email was provided. ' + 'Please ensure you have configured ACME with an email address either:\n' + '1. In the top-level "acme" configuration\n' + '2. In the route\'s "tls.acme" configuration'); } // Apply renewal threshold from global defaults or route config const renewThreshold = route.action.tls?.acme?.renewBeforeDays || this.globalAcmeDefaults?.renewThresholdDays || 30; logger.log('info', `Requesting ACME certificate for ${domains.join(', ')} (renew ${renewThreshold} days before expiry)`, { domains: domains.join(', '), renewThreshold, component: 'certificate-manager' }); this.updateCertStatus(routeName, 'pending', 'acme'); try { // Challenge route should already be active from initialization // No need to add it for each certificate // Determine if we should request a wildcard certificate // Only request wildcards if: // 1. The primary domain is not already a wildcard // 2. The domain has multiple parts (can have subdomains) // 3. We have DNS-01 challenge support (required for wildcards) const hasDnsChallenge = this.smartAcme.challengeHandlers?.some((handler) => handler.getSupportedTypes && handler.getSupportedTypes().includes('dns-01')); const shouldIncludeWildcard = !primaryDomain.startsWith('*.') && primaryDomain.includes('.') && primaryDomain.split('.').length >= 2 && hasDnsChallenge; if (shouldIncludeWildcard) { logger.log('info', `Requesting wildcard certificate for ${primaryDomain} (DNS-01 available)`, { domain: primaryDomain, challengeType: 'DNS-01', component: 'certificate-manager' }); } // Use smartacme to get certificate with optional wildcard const cert = await this.smartAcme.getCertificateForDomain(primaryDomain, shouldIncludeWildcard ? { includeWildcard: true } : undefined); // SmartAcme's Cert object has these properties: // - publicKey: The certificate PEM string // - privateKey: The private key PEM string // - csr: Certificate signing request // - validUntil: Timestamp in milliseconds // - domainName: The domain name const certData = { cert: cert.publicKey, key: cert.privateKey, ca: cert.publicKey, // Use same as cert for now expiryDate: new Date(cert.validUntil), issueDate: new Date(cert.created), source: 'acme' }; await this.certStore.saveCertificate(routeName, certData); await this.applyCertificate(primaryDomain, certData); this.updateCertStatus(routeName, 'valid', 'acme', certData); logger.log('info', `Successfully provisioned ACME certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' }); } catch (error) { logger.log('error', `Failed to provision ACME certificate for ${primaryDomain}: ${error.message}`, { domain: primaryDomain, error: error.message, component: 'certificate-manager' }); this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message); throw error; } } /** * Provision static certificate */ async provisionStaticCertificate(route, domain, certConfig) { const routeName = route.name || domain; try { let key = certConfig.key; let cert = certConfig.cert; // Load from files if paths are provided if (certConfig.keyFile) { const keyFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.keyFile); key = keyFile.contents.toString(); } if (certConfig.certFile) { const certFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.certFile); cert = certFile.contents.toString(); } // Parse certificate to get dates // Parse certificate to get dates - for now just use defaults // TODO: Implement actual certificate parsing if needed const certInfo = { validTo: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), validFrom: new Date() }; const certData = { cert, key, expiryDate: certInfo.validTo, issueDate: certInfo.validFrom, source: 'static' }; // Save to store for consistency await this.certStore.saveCertificate(routeName, certData); await this.applyCertificate(domain, certData); this.updateCertStatus(routeName, 'valid', 'static', certData); logger.log('info', `Successfully loaded static certificate for ${domain}`, { domain, component: 'certificate-manager' }); } catch (error) { logger.log('error', `Failed to provision static certificate for ${domain}: ${error.message}`, { domain, error: error.message, component: 'certificate-manager' }); this.updateCertStatus(routeName, 'error', 'static', undefined, error.message); throw error; } } /** * Apply certificate to HttpProxy */ async applyCertificate(domain, certData) { if (!this.httpProxy) { logger.log('warn', `HttpProxy not set, cannot apply certificate for domain ${domain}`, { domain, component: 'certificate-manager' }); return; } // Apply certificate to HttpProxy this.httpProxy.updateCertificate(domain, certData.cert, certData.key); // Also apply for wildcard if it's a subdomain if (domain.includes('.') && !domain.startsWith('*.')) { const parts = domain.split('.'); if (parts.length >= 2) { const wildcardDomain = `*.${parts.slice(-2).join('.')}`; this.httpProxy.updateCertificate(wildcardDomain, certData.cert, certData.key); } } } /** * Extract domains from route configuration */ extractDomainsFromRoute(route) { if (!route.match.domains) { return []; } const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; // Filter out wildcards and patterns return domains.filter(d => !d.includes('*') && !d.includes('{') && d.includes('.')); } /** * Check if certificate is valid */ isCertificateValid(cert) { const now = new Date(); // Use renewal threshold from global defaults or fallback to 30 days const renewThresholdDays = this.globalAcmeDefaults?.renewThresholdDays || 30; const expiryThreshold = new Date(now.getTime() + renewThresholdDays * 24 * 60 * 60 * 1000); return cert.expiryDate > expiryThreshold; } /** * Extract expiry date from a PEM certificate */ extractExpiryDate(_certPem) { // For now, we'll default to 90 days for custom certificates // In production, you might want to use a proper X.509 parser // or require the custom cert provider to include expiry info logger.log('info', 'Using default 90-day expiry for custom certificate', { component: 'certificate-manager' }); return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); } /** * Add challenge route to SmartProxy * * This method adds a special route for ACME HTTP-01 challenges, which typically uses port 80. * Since we may already be listening on port 80 for regular routes, we need to be * careful about how we add this route to avoid binding conflicts. */ async addChallengeRoute() { // Check with state manager first - avoid duplication if (this.acmeStateManager && this.acmeStateManager.isChallengeRouteActive()) { try { logger.log('info', 'Challenge route already active in global state, skipping', { component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log('[INFO] Challenge route already active in global state, skipping'); } this.challengeRouteActive = true; return; } if (this.challengeRouteActive) { try { logger.log('info', 'Challenge route already active locally, skipping', { component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log('[INFO] Challenge route already active locally, skipping'); } return; } if (!this.updateRoutesCallback) { throw new Error('No route update callback set'); } if (!this.challengeRoute) { throw new Error('Challenge route not initialized'); } // Get the challenge port const challengePort = this.globalAcmeDefaults?.port || 80; // Check if any existing routes are already using this port // This helps us determine if we need to create a new binding or can reuse existing one const portInUseByRoutes = this.routes.some(route => { const routePorts = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports]; return routePorts.some(p => { // Handle both number and port range objects if (typeof p === 'number') { return p === challengePort; } else if (typeof p === 'object' && 'from' in p && 'to' in p) { // Port range case - check if challengePort is in range return challengePort >= p.from && challengePort <= p.to; } return false; }); }); try { // Log whether port is already in use by other routes if (portInUseByRoutes) { try { logger.log('info', `Port ${challengePort} is already used by another route, merging ACME challenge route`, { port: challengePort, component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log(`[INFO] Port ${challengePort} is already used by another route, merging ACME challenge route`); } } else { try { logger.log('info', `Adding new ACME challenge route on port ${challengePort}`, { port: challengePort, component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log(`[INFO] Adding new ACME challenge route on port ${challengePort}`); } } // Add the challenge route to the existing routes const challengeRoute = this.challengeRoute; const updatedRoutes = [...this.routes, challengeRoute]; // With the re-ordering of start(), port binding should already be done // This updateRoutes call should just add the route without binding again await this.updateRoutesCallback(updatedRoutes); this.challengeRouteActive = true; // Register with state manager if (this.acmeStateManager) { this.acmeStateManager.addChallengeRoute(challengeRoute); } try { logger.log('info', 'ACME challenge route successfully added', { component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log('[INFO] ACME challenge route successfully added'); } } catch (error) { // Enhanced error handling based on error type if (error.code === 'EADDRINUSE') { try { logger.log('warn', `Challenge port ${challengePort} is unavailable - it's already in use by another process. Consider configuring a different ACME port.`, { port: challengePort, error: error.message, component: 'certificate-manager' }); } catch (logError) { // Silently handle logging errors console.log(`[WARN] Challenge port ${challengePort} is unavailable - it's already in use by another process. Consider configuring a different ACME port.`); } // Provide a more informative and actionable error message throw new Error(`ACME HTTP-01 challenge port ${challengePort} is already in use by another process. ` + `Please configure a different port using the acme.port setting (e.g., 8080).`); } else if (error.message && error.message.includes('EADDRINUSE')) { // Some Node.js versions embed the error code in the message rather than the code property try { logger.log('warn', `Port ${challengePort} conflict detected: ${error.message}`, { port: challengePort, component: 'certificate-manager' }); } catch (logError) { // Silently handle logging errors console.log(`[WARN] Port ${challengePort} conflict detected: ${error.message}`); } // More detailed error message with suggestions throw new Error(`ACME HTTP challenge port ${challengePort} conflict detected. ` + `To resolve this issue, try one of these approaches:\n` + `1. Configure a different port in ACME settings (acme.port)\n` + `2. Add a regular route that uses port ${challengePort} before initializing the certificate manager\n` + `3. Stop any other services that might be using port ${challengePort}`); } // Log and rethrow other types of errors try { logger.log('error', `Failed to add challenge route: ${error.message}`, { error: error.message, component: 'certificate-manager' }); } catch (logError) { // Silently handle logging errors console.log(`[ERROR] Failed to add challenge route: ${error.message}`); } throw error; } } /** * Remove challenge route from SmartProxy */ async removeChallengeRoute() { if (!this.challengeRouteActive) { try { logger.log('info', 'Challenge route not active, skipping removal', { component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log('[INFO] Challenge route not active, skipping removal'); } return; } if (!this.updateRoutesCallback) { return; } try { const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge'); await this.updateRoutesCallback(filteredRoutes); this.challengeRouteActive = false; // Remove from state manager if (this.acmeStateManager) { this.acmeStateManager.removeChallengeRoute('acme-challenge'); } try { logger.log('info', 'ACME challenge route successfully removed', { component: 'certificate-manager' }); } catch (error) { // Silently handle logging errors console.log('[INFO] ACME challenge route successfully removed'); } } catch (error) { try { logger.log('error', `Failed to remove challenge route: ${error.message}`, { error: error.message, component: 'certificate-manager' }); } catch (logError) { // Silently handle logging errors console.log(`[ERROR] Failed to remove challenge route: ${error.message}`); } // Reset the flag even on error to avoid getting stuck this.challengeRouteActive = false; throw error; } } /** * Start renewal timer */ startRenewalTimer() { // Check for renewals every 12 hours this.renewalTimer = setInterval(() => { this.checkAndRenewCertificates(); }, 12 * 60 * 60 * 1000); // Also do an immediate check this.checkAndRenewCertificates(); } /** * Check and renew certificates that are expiring */ async checkAndRenewCertificates() { for (const route of this.routes) { if (route.action.tls?.certificate === 'auto') { const routeName = route.name || this.extractDomainsFromRoute(route)[0]; const cert = await this.certStore.getCertificate(routeName); if (cert && !this.isCertificateValid(cert)) { logger.log('info', `Certificate for ${routeName} needs renewal`, { routeName, component: 'certificate-manager' }); try { await this.provisionCertificate(route); } catch (error) { logger.log('error', `Failed to renew certificate for ${routeName}: ${error.message}`, { routeName, error: error.message, component: 'certificate-manager' }); } } } } } /** * Update certificate status */ updateCertStatus(routeName, status, source, certData, error) { this.certStatus.set(routeName, { domain: routeName, status, source, expiryDate: certData?.expiryDate, issueDate: certData?.issueDate, error }); } /** * Get certificate status for a route */ getCertificateStatus(routeName) { return this.certStatus.get(routeName); } /** * Force renewal of a certificate */ async renewCertificate(routeName) { const route = this.routes.find(r => r.name === routeName); if (!route) { throw new Error(`Route ${routeName} not found`); } // Remove existing certificate to force renewal await this.certStore.deleteCertificate(routeName); await this.provisionCertificate(route); } /** * Setup challenge handler integration with SmartProxy routing */ setupChallengeHandler(http01Handler) { // Use challenge port from global config or default to 80 const challengePort = this.globalAcmeDefaults?.port || 80; // Create a challenge route that delegates to SmartAcme's HTTP-01 handler const challengeRoute = { name: 'acme-challenge', priority: 1000, // High priority match: { ports: challengePort, path: '/.well-known/acme-challenge/*' }, action: { type: 'socket-handler', socketHandler: SocketHandlers.httpServer((req, res) => { // Extract the token from the path const token = req.url?.split('/').pop(); if (!token) { res.status(404); res.send('Not found'); return; } // Create mock request/response objects for SmartAcme let responseData = null; const mockReq = { url: req.url, method: req.method, headers: req.headers }; const mockRes = { statusCode: 200, setHeader: (name, value) => { }, end: (data) => { responseData = data; } }; // Use SmartAcme's handler const handleAcme = () => { http01Handler.handleRequest(mockReq, mockRes, () => { // Not handled by ACME res.status(404); res.send('Not found'); }); // Give it a moment to process, then send response setTimeout(() => { if (responseData) { res.header('Content-Type', 'text/plain'); res.send(String(responseData)); } else { res.status(404); res.send('Not found'); } }, 100); }; handleAcme(); }) } }; // Store the challenge route to add it when needed this.challengeRoute = challengeRoute; } /** * Stop certificate manager */ async stop() { if (this.renewalTimer) { clearInterval(this.renewalTimer); this.renewalTimer = null; } // Always remove challenge route on shutdown if (this.challengeRoute) { logger.log('info', 'Removing ACME challenge route during shutdown', { component: 'certificate-manager' }); await this.removeChallengeRoute(); } if (this.smartAcme) { await this.smartAcme.stop(); } // Clear any pending challenges if (this.pendingChallenges.size > 0) { this.pendingChallenges.clear(); } } /** * Get ACME options (for recreating after route updates) */ getAcmeOptions() { return this.acmeOptions; } /** * Get certificate manager state */ getState() { return { challengeRouteActive: this.challengeRouteActive }; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2VydGlmaWNhdGUtbWFuYWdlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL3Byb3hpZXMvc21hcnQtcHJveHkvY2VydGlmaWNhdGUtbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUduRCxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFNUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ3BELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQW9CMUQsTUFBTSxPQUFPLGdCQUFnQjtJQWdDM0IsWUFDVSxNQUFzQixFQUN0QixVQUFrQixTQUFTLEVBQzNCLFdBSVAsRUFDTyxZQUVQO1FBVE8sV0FBTSxHQUFOLE1BQU0sQ0FBZ0I7UUFDdEIsWUFBTyxHQUFQLE9BQU8sQ0FBb0I7UUFDM0IsZ0JBQVcsR0FBWCxXQUFXLENBSWxCO1FBQ08saUJBQVksR0FBWixZQUFZLENBRW5CO1FBeENLLGNBQVMsR0FBdUMsSUFBSSxDQUFDO1FBQ3JELGNBQVMsR0FBcUIsSUFBSSxDQUFDO1FBQ25DLGlCQUFZLEdBQTBCLElBQUksQ0FBQztRQUMzQyxzQkFBaUIsR0FBd0IsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUNuRCxtQkFBYyxHQUF3QixJQUFJLENBQUM7UUFFbkQseUNBQXlDO1FBQ2pDLGVBQVUsR0FBNkIsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUV6RCxvREFBb0Q7UUFDNUMsdUJBQWtCLEdBQXdCLElBQUksQ0FBQztRQUt2RCx1REFBdUQ7UUFDL0MseUJBQW9CLEdBQVksS0FBSyxDQUFDO1FBRTlDLCtDQUErQztRQUN2QyxtQkFBYyxHQUFZLEtBQUssQ0FBQztRQUV4QywrQkFBK0I7UUFDdkIscUJBQWdCLEdB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