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.

744 lines 63.3 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; } /** * Update the routes array to keep it in sync with SmartProxy * This prevents stale route data when adding/removing challenge routes */ setRoutes(routes) { this.routes = routes; } /** * 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 const expiryDate = this.extractExpiryDate(cert); const issueDate = new Date(); // Current date as issue date const certData = { cert, key, expiryDate, issueDate, 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); // Keep local routes in sync after updating this.routes = 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); // Keep local routes in sync after updating this.routes = 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); // Unref the timer so it doesn't keep the process alive if (this.renewalTimer.unref) { this.renewalTimer.unref(); } // 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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