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.

737 lines 68.7 kB
import * as plugins from '../../plugins.js'; import '../../core/models/socket-augmentation.js'; import { createLogger, } from './models/types.js'; import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js'; import { ConnectionPool } from './connection-pool.js'; import { ContextCreator } from './context-creator.js'; import { HttpRequestHandler } from './http-request-handler.js'; import { Http2RequestHandler } from './http2-request-handler.js'; import { toBaseContext } from '../../core/models/route-context.js'; import { TemplateUtils } from '../../core/utils/template-utils.js'; import { SecurityManager } from './security-manager.js'; /** * Handles HTTP request processing and proxying */ export class RequestHandler { constructor(options, connectionPool, routeManager, functionCache, // FunctionCache - using any to avoid circular dependency router // HttpRouter - using any to avoid circular dependency ) { this.options = options; this.connectionPool = connectionPool; this.routeManager = routeManager; this.functionCache = functionCache; this.router = router; this.defaultHeaders = {}; this.metricsTracker = null; // HTTP/2 client sessions for backend proxying this.h2Sessions = new Map(); // Context creator for route contexts this.contextCreator = new ContextCreator(); // Rate limit cleanup interval this.rateLimitCleanupInterval = null; this.logger = createLogger(options.logLevel || 'info'); this.securityManager = new SecurityManager(this.logger); // Schedule rate limit cleanup every minute this.rateLimitCleanupInterval = setInterval(() => { this.securityManager.cleanupExpiredRateLimits(); }, 60000); // Make sure the interval doesn't keep the process alive if (this.rateLimitCleanupInterval.unref) { this.rateLimitCleanupInterval.unref(); } } /** * Set the route manager instance */ setRouteManager(routeManager) { this.routeManager = routeManager; } /** * Set the metrics tracker instance */ setMetricsTracker(tracker) { this.metricsTracker = tracker; } /** * Set default headers to be included in all responses */ setDefaultHeaders(headers) { this.defaultHeaders = { ...this.defaultHeaders, ...headers }; this.logger.info('Updated default response headers'); } /** * Get all default headers */ getDefaultHeaders() { return { ...this.defaultHeaders }; } /** * Select the appropriate target from the targets array based on sub-matching criteria */ selectTarget(targets, context) { // Sort targets by priority (higher first) const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0)); // Find the first matching target for (const target of sortedTargets) { if (!target.match) { // No match criteria means this is a default/fallback target return target; } // Check port match if (target.match.ports && !target.match.ports.includes(context.port)) { continue; } // Check path match (supports wildcards) if (target.match.path && context.path) { const pathPattern = target.match.path.replace(/\*/g, '.*'); const pathRegex = new RegExp(`^${pathPattern}$`); if (!pathRegex.test(context.path)) { continue; } } // Check method match if (target.match.method && context.method && !target.match.method.includes(context.method)) { continue; } // Check headers match if (target.match.headers && context.headers) { let headersMatch = true; for (const [key, pattern] of Object.entries(target.match.headers)) { const headerValue = context.headers[key.toLowerCase()]; if (!headerValue) { headersMatch = false; break; } if (pattern instanceof RegExp) { if (!pattern.test(headerValue)) { headersMatch = false; break; } } else if (headerValue !== pattern) { headersMatch = false; break; } } if (!headersMatch) { continue; } } // All criteria matched return target; } // No matching target found, return the first target without match criteria (default) return sortedTargets.find(t => !t.match) || null; } /** * Apply CORS headers to response if configured * Implements Phase 5.5: Context-aware CORS handling * * @param res The server response to apply headers to * @param req The incoming request * @param route Optional route config with CORS settings */ applyCorsHeaders(res, req, route) { // Use route-specific CORS config if available, otherwise use global config let corsConfig = null; // Route CORS config takes precedence if enabled if (route?.headers?.cors?.enabled) { corsConfig = route.headers.cors; this.logger.debug(`Using route-specific CORS config for ${route.name || 'unnamed route'}`); } // Fall back to global CORS config if available else if (this.options.cors) { corsConfig = this.options.cors; this.logger.debug('Using global CORS config'); } // If no CORS config available, skip if (!corsConfig) { return; } // Get origin from request const origin = req.headers.origin; // Apply Allow-Origin (with dynamic validation if needed) if (corsConfig.allowOrigin) { // Handle multiple origins in array format if (Array.isArray(corsConfig.allowOrigin)) { if (origin && corsConfig.allowOrigin.includes(origin)) { // Match found, set specific origin res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); // Important for caching } else if (corsConfig.allowOrigin.includes('*')) { // Wildcard match res.setHeader('Access-Control-Allow-Origin', '*'); } } // Handle single origin or wildcard else if (corsConfig.allowOrigin === '*') { res.setHeader('Access-Control-Allow-Origin', '*'); } // Match single origin against request else if (origin && corsConfig.allowOrigin === origin) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); } // Use template variables if present else if (origin && corsConfig.allowOrigin.includes('{')) { const resolvedOrigin = TemplateUtils.resolveTemplateVariables(corsConfig.allowOrigin, { domain: req.headers.host }); if (resolvedOrigin === origin || resolvedOrigin === '*') { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); } } } // Apply other CORS headers if (corsConfig.allowMethods) { res.setHeader('Access-Control-Allow-Methods', corsConfig.allowMethods); } if (corsConfig.allowHeaders) { res.setHeader('Access-Control-Allow-Headers', corsConfig.allowHeaders); } if (corsConfig.allowCredentials) { res.setHeader('Access-Control-Allow-Credentials', 'true'); } if (corsConfig.exposeHeaders) { res.setHeader('Access-Control-Expose-Headers', corsConfig.exposeHeaders); } if (corsConfig.maxAge) { res.setHeader('Access-Control-Max-Age', corsConfig.maxAge.toString()); } // Handle CORS preflight requests if enabled (default: true) if (req.method === 'OPTIONS' && corsConfig.preflight !== false) { res.statusCode = 204; // No content res.end(); return; } } // First implementation of applyRouteHeaderModifications moved to the second implementation below /** * Apply default headers to response */ applyDefaultHeaders(res) { // Apply default headers for (const [key, value] of Object.entries(this.defaultHeaders)) { if (!res.hasHeader(key)) { res.setHeader(key, value); } } // Add server identifier if not already set if (!res.hasHeader('Server')) { res.setHeader('Server', 'NetworkProxy'); } } /** * Apply URL rewriting based on route configuration * Implements Phase 5.2: URL rewriting using route context * * @param req The request with the URL to rewrite * @param route The route configuration containing rewrite rules * @param routeContext Context for template variable resolution * @returns True if URL was rewritten, false otherwise */ applyUrlRewriting(req, route, routeContext) { // Check if route has URL rewriting configuration if (!route.action.advanced?.urlRewrite) { return false; } const rewriteConfig = route.action.advanced.urlRewrite; // Store original URL for logging const originalUrl = req.url; if (rewriteConfig.pattern && rewriteConfig.target) { try { // Create a RegExp from the pattern const regex = new RegExp(rewriteConfig.pattern, rewriteConfig.flags || ''); // Apply rewriting with template variable resolution let target = rewriteConfig.target; // Replace template variables in target with values from context target = TemplateUtils.resolveTemplateVariables(target, routeContext); // If onlyRewritePath is set, split URL into path and query parts if (rewriteConfig.onlyRewritePath && req.url) { const [path, query] = req.url.split('?'); const rewrittenPath = path.replace(regex, target); req.url = query ? `${rewrittenPath}?${query}` : rewrittenPath; } else { // Perform the replacement on the entire URL req.url = req.url?.replace(regex, target); } this.logger.debug(`URL rewritten: ${originalUrl} -> ${req.url}`); return true; } catch (err) { this.logger.error(`Error in URL rewriting: ${err}`); return false; } } return false; } /** * Apply header modifications from route configuration * Implements Phase 5.1: Route-based header manipulation */ applyRouteHeaderModifications(route, req, res) { // Check if route has header modifications if (!route.headers) { return; } // Apply request header modifications (these will be sent to the backend) if (route.headers.request && req.headers) { for (const [key, value] of Object.entries(route.headers.request)) { // Skip if header already exists and we're not overriding if (req.headers[key.toLowerCase()] && !value.startsWith('!')) { continue; } // Handle special delete directive (!delete) if (value === '!delete') { delete req.headers[key.toLowerCase()]; this.logger.debug(`Deleted request header: ${key}`); continue; } // Handle forced override (!value) let finalValue; if (value.startsWith('!') && value !== '!delete') { // Keep the ! but resolve any templates in the rest const templateValue = value.substring(1); finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {}); } else { // Resolve templates in the entire value finalValue = TemplateUtils.resolveTemplateVariables(value, {}); } // Set the header req.headers[key.toLowerCase()] = finalValue; this.logger.debug(`Modified request header: ${key}=${finalValue}`); } } // Apply response header modifications (these will be stored for later use) if (route.headers.response) { for (const [key, value] of Object.entries(route.headers.response)) { // Skip if header already exists and we're not overriding if (res.hasHeader(key) && !value.startsWith('!')) { continue; } // Handle special delete directive (!delete) if (value === '!delete') { res.removeHeader(key); this.logger.debug(`Deleted response header: ${key}`); continue; } // Handle forced override (!value) let finalValue; if (value.startsWith('!') && value !== '!delete') { // Keep the ! but resolve any templates in the rest const templateValue = value.substring(1); finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {}); } else { // Resolve templates in the entire value finalValue = TemplateUtils.resolveTemplateVariables(value, {}); } // Set the header res.setHeader(key, finalValue); this.logger.debug(`Modified response header: ${key}=${finalValue}`); } } } /** * Handle an HTTP request */ async handleRequest(req, res) { // Record start time for logging const startTime = Date.now(); // Get route before applying CORS (we might need its settings) // Try to find a matching route using RouteManager let matchingRoute = null; if (this.routeManager) { try { // Create a connection ID for this request const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`; // Create route context for function-based targets const routeContext = this.contextCreator.createHttpRouteContext(req, { connectionId, clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0', serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0', tlsVersion: req.socket.getTLSVersion?.() || undefined }); const matchResult = this.routeManager.findMatchingRoute(toBaseContext(routeContext)); matchingRoute = matchResult?.route || null; } catch (err) { this.logger.error('Error finding matching route', err); } } // Apply CORS headers with route-specific settings if available this.applyCorsHeaders(res, req, matchingRoute); // If this is an OPTIONS request, the response has already been ended in applyCorsHeaders // so we should return early to avoid trying to set more headers if (req.method === 'OPTIONS') { // Increment metrics for OPTIONS requests too if (this.metricsTracker) { this.metricsTracker.incrementRequestsServed(); } return; } // Apply default headers this.applyDefaultHeaders(res); // We already have the connection ID and routeContext from CORS handling const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`; // Create route context for function-based targets (if we don't already have one) const routeContext = this.contextCreator.createHttpRouteContext(req, { connectionId, clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0', serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0', tlsVersion: req.socket.getTLSVersion?.() || undefined }); // Check security restrictions if we have a matching route if (matchingRoute) { // Check IP filtering and rate limiting if (!this.securityManager.isAllowed(matchingRoute, routeContext)) { this.logger.warn(`Access denied for ${routeContext.clientIp} to ${matchingRoute.name || 'unnamed'}`); res.statusCode = 403; res.end('Forbidden: Access denied by security policy'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Check basic auth if (matchingRoute.security?.basicAuth?.enabled) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Basic ')) { // No auth header provided - send 401 with WWW-Authenticate header res.statusCode = 401; const realm = matchingRoute.security.basicAuth.realm || 'Protected Area'; res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`); res.end('Authentication Required'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Verify credentials try { const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8'); const [username, password] = credentials.split(':'); if (!this.securityManager.checkBasicAuth(matchingRoute, username, password)) { res.statusCode = 401; const realm = matchingRoute.security.basicAuth.realm || 'Protected Area'; res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`); res.end('Invalid Credentials'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } catch (err) { this.logger.error(`Error verifying basic auth: ${err}`); res.statusCode = 401; res.end('Authentication Error'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } // Check JWT auth if (matchingRoute.security?.jwtAuth?.enabled) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { // No auth header provided - send 401 res.statusCode = 401; res.end('Authentication Required: JWT token missing'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Verify token const token = authHeader.substring(7); if (!this.securityManager.verifyJwtToken(matchingRoute, token)) { res.statusCode = 401; res.end('Invalid or Expired JWT'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } } // If we found a matching route with forward action, select appropriate target if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) { this.logger.debug(`Found matching route: ${matchingRoute.name || 'unnamed'}`); // Select the appropriate target from the targets array const selectedTarget = this.selectTarget(matchingRoute.action.targets, { port: routeContext.port, path: routeContext.path, headers: routeContext.headers, method: routeContext.method }); if (!selectedTarget) { this.logger.error(`No matching target found for route ${matchingRoute.name}`); req.socket.end(); return; } // Extract target information, resolving functions if needed let targetHost; let targetPort; try { // Check function cache for host and resolve or use cached value if (typeof selectedTarget.host === 'function') { // Generate a function ID for caching (use route name or ID if available) const functionId = `host-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedHost = this.functionCache.getCachedHost(routeContext, functionId); if (cachedHost !== undefined) { targetHost = cachedHost; this.logger.debug(`Using cached host value for ${functionId}`); } else { // Resolve the function and cache the result const resolvedHost = selectedTarget.host(toBaseContext(routeContext)); targetHost = resolvedHost; // Cache the result this.functionCache.cacheHost(routeContext, functionId, resolvedHost); this.logger.debug(`Resolved and cached function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { // No cache available, just resolve const resolvedHost = selectedTarget.host(routeContext); targetHost = resolvedHost; this.logger.debug(`Resolved function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { targetHost = selectedTarget.host; } // Check function cache for port and resolve or use cached value if (typeof selectedTarget.port === 'function') { // Generate a function ID for caching const functionId = `port-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedPort = this.functionCache.getCachedPort(routeContext, functionId); if (cachedPort !== undefined) { targetPort = cachedPort; this.logger.debug(`Using cached port value for ${functionId}`); } else { // Resolve the function and cache the result const resolvedPort = selectedTarget.port(toBaseContext(routeContext)); targetPort = resolvedPort; // Cache the result this.functionCache.cachePort(routeContext, functionId, resolvedPort); this.logger.debug(`Resolved and cached function-based port to: ${resolvedPort}`); } } else { // No cache available, just resolve const resolvedPort = selectedTarget.port(routeContext); targetPort = resolvedPort; this.logger.debug(`Resolved function-based port to: ${resolvedPort}`); } } else { targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port; } // Select a single host if an array was provided const selectedHost = Array.isArray(targetHost) ? targetHost[Math.floor(Math.random() * targetHost.length)] : targetHost; // Create a destination for the connection pool const destination = { host: selectedHost, port: targetPort }; // Apply URL rewriting if configured this.applyUrlRewriting(req, matchingRoute, routeContext); // Apply header modifications if configured this.applyRouteHeaderModifications(matchingRoute, req, res); // Continue with handling using the resolved destination HttpRequestHandler.handleHttpRequestWithDestination(req, res, destination, routeContext, startTime, this.logger, this.metricsTracker, matchingRoute // Pass the route config for additional processing ); return; } catch (err) { this.logger.error(`Error evaluating function-based target: ${err}`); res.statusCode = 500; res.end('Internal Server Error: Failed to evaluate target functions'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } // If no route was found, return 404 this.logger.warn(`No route configuration for host: ${req.headers.host}`); res.statusCode = 404; res.end('Not Found: No route configuration for this host'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); } /** * Handle HTTP/2 stream requests with function-based target support */ async handleHttp2(stream, headers) { const startTime = Date.now(); // Create a connection ID for this HTTP/2 stream const connectionId = `http2-${Date.now()}-${Math.floor(Math.random() * 10000)}`; // Get client IP and server IP from the socket const socket = stream.session?.socket; const clientIp = socket?.remoteAddress?.replace('::ffff:', '') || '0.0.0.0'; const serverIp = socket?.localAddress?.replace('::ffff:', '') || '0.0.0.0'; // Create route context for function-based targets const routeContext = this.contextCreator.createHttp2RouteContext(stream, headers, { connectionId, clientIp, serverIp }); // Try to find a matching route using RouteManager let matchingRoute = null; if (this.routeManager) { try { const matchResult = this.routeManager.findMatchingRoute(toBaseContext(routeContext)); matchingRoute = matchResult?.route || null; } catch (err) { this.logger.error('Error finding matching route for HTTP/2 request', err); } } // If we found a matching route with forward action, select appropriate target if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) { this.logger.debug(`Found matching route for HTTP/2 request: ${matchingRoute.name || 'unnamed'}`); // Select the appropriate target from the targets array const selectedTarget = this.selectTarget(matchingRoute.action.targets, { port: routeContext.port, path: routeContext.path, headers: routeContext.headers, method: routeContext.method }); if (!selectedTarget) { this.logger.error(`No matching target found for route ${matchingRoute.name}`); stream.respond({ ':status': 502 }); stream.end(); return; } // Extract target information, resolving functions if needed let targetHost; let targetPort; try { // Check function cache for host and resolve or use cached value if (typeof selectedTarget.host === 'function') { // Generate a function ID for caching (use route name or ID if available) const functionId = `host-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedHost = this.functionCache.getCachedHost(routeContext, functionId); if (cachedHost !== undefined) { targetHost = cachedHost; this.logger.debug(`Using cached host value for HTTP/2: ${functionId}`); } else { // Resolve the function and cache the result const resolvedHost = selectedTarget.host(toBaseContext(routeContext)); targetHost = resolvedHost; // Cache the result this.functionCache.cacheHost(routeContext, functionId, resolvedHost); this.logger.debug(`Resolved and cached HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { // No cache available, just resolve const resolvedHost = selectedTarget.host(routeContext); targetHost = resolvedHost; this.logger.debug(`Resolved HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { targetHost = selectedTarget.host; } // Check function cache for port and resolve or use cached value if (typeof selectedTarget.port === 'function') { // Generate a function ID for caching const functionId = `port-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedPort = this.functionCache.getCachedPort(routeContext, functionId); if (cachedPort !== undefined) { targetPort = cachedPort; this.logger.debug(`Using cached port value for HTTP/2: ${functionId}`); } else { // Resolve the function and cache the result const resolvedPort = selectedTarget.port(toBaseContext(routeContext)); targetPort = resolvedPort; // Cache the result this.functionCache.cachePort(routeContext, functionId, resolvedPort); this.logger.debug(`Resolved and cached HTTP/2 function-based port to: ${resolvedPort}`); } } else { // No cache available, just resolve const resolvedPort = selectedTarget.port(routeContext); targetPort = resolvedPort; this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`); } } else { targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port; } // Select a single host if an array was provided const selectedHost = Array.isArray(targetHost) ? targetHost[Math.floor(Math.random() * targetHost.length)] : targetHost; // Create a destination for forwarding const destination = { host: selectedHost, port: targetPort }; // Handle HTTP/2 stream based on backend protocol const backendProtocol = matchingRoute.action.options?.backendProtocol || this.options.backendProtocol; if (backendProtocol === 'http2') { // Forward to HTTP/2 backend return Http2RequestHandler.handleHttp2WithHttp2Destination(stream, headers, destination, routeContext, this.h2Sessions, this.logger, this.metricsTracker); } else { // Forward to HTTP/1.1 backend return Http2RequestHandler.handleHttp2WithHttp1Destination(stream, headers, destination, routeContext, this.logger, this.metricsTracker); } } catch (err) { this.logger.error(`Error evaluating function-based target for HTTP/2: ${err}`); stream.respond({ ':status': 500 }); stream.end('Internal Server Error: Failed to evaluate target functions'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } // Fall back to legacy routing if no matching route found const method = headers[':method'] || 'GET'; const path = headers[':path'] || '/'; // No route was found stream.respond({ ':status': 404 }); stream.end('Not Found: No route configuration for this request'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); } /** * Cleanup resources and stop intervals */ destroy() { if (this.rateLimitCleanupInterval) { clearInterval(this.rateLimitCleanupInterval); this.rateLimitCleanupInterval = null; } // Close all HTTP/2 sessions for (const [key, session] of this.h2Sessions) { session.close(); } this.h2Sessions.clear(); // Clear function cache if it has a destroy method if (this.functionCache && typeof this.functionCache.destroy === 'function') { this.functionCache.destroy(); } this.logger.debug('RequestHandler destroyed'); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVxdWVzdC1oYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvcHJveGllcy9odHRwLXByb3h5L3JlcXVlc3QtaGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sMENBQTBDLENBQUM7QUFDbEQsT0FBTyxFQUdMLFlBQVksR0FDYixNQUFNLG1CQUFtQixDQUFDO0FBQzNCLE9BQU8sRUFBRSxrQkFBa0IsSUFBSSxZQUFZLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQztBQUN6RixPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDdEQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQy9ELE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBR2pFLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxvQ0FBb0MsQ0FBQztBQUNuRSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDbkUsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBYXhEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGNBQWM7SUFnQnpCLFlBQ1UsT0FBMEIsRUFDMUIsY0FBOEIsRUFDOUIsWUFBMkIsRUFDM0IsYUFBbUIsRUFBRSx5REFBeUQ7SUFDOUUsTUFBWSxDQUFDLHNEQUFzRDs7UUFKbkUsWUFBTyxHQUFQLE9BQU8sQ0FBbUI7UUFDMUIsbUJBQWMsR0FBZCxjQUFjLENBQWdCO1FBQzlCLGlCQUFZLEdBQVosWUFBWSxDQUFlO1FBQzNCLGtCQUFhLEdBQWIsYUFBYSxDQUFNO1FBQ25CLFdBQU0sR0FBTixNQUFNLENBQU07UUFwQmQsbUJBQWMsR0FBOEIsRUFBRSxDQUFDO1FBRS9DLG1C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