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.

920 lines 62.3 kB
/** * Route Helper Functions * * This file provides utility functions for creating route configurations for common scenarios. * These functions aim to simplify the creation of route configurations for typical use cases. * * This module includes helper functions for creating: * - HTTP routes (createHttpRoute) * - HTTPS routes with TLS termination (createHttpsTerminateRoute) * - HTTP to HTTPS redirects (createHttpToHttpsRedirect) * - HTTPS passthrough routes (createHttpsPassthroughRoute) * - Complete HTTPS servers with redirects (createCompleteHttpsServer) * - Load balancer routes (createLoadBalancerRoute) * - API routes (createApiRoute) * - WebSocket routes (createWebSocketRoute) * - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute) * - Dynamic routing (createDynamicRoute, createSmartLoadBalancer) * - NFTables routes (createNfTablesRoute, createNfTablesTerminateRoute) */ import * as plugins from '../../../plugins.js'; import { mergeRouteConfigs } from './route-utils.js'; import { ProtocolDetector, HttpDetector } from '../../../detection/index.js'; /** * Create an HTTP-only route configuration * @param domains Domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createHttpRoute(domains, target, options = {}) { // Create route match const match = { ports: options.match?.ports || 80, domains }; // Create route action const action = { type: 'forward', targets: [target] }; // Create the route config return { match, action, name: options.name || `HTTP Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`, ...options }; } /** * Create an HTTPS route with TLS termination (including HTTP redirect to HTTPS) * @param domains Domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createHttpsTerminateRoute(domains, target, options = {}) { // Create route match const match = { ports: options.httpsPort || 443, domains }; // Create route action const action = { type: 'forward', targets: [target], tls: { mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate', certificate: options.certificate || 'auto' } }; // Create the route config return { match, action, name: options.name || `HTTPS Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`, ...options }; } /** * Create an HTTP to HTTPS redirect route * @param domains Domain(s) to match * @param httpsPort HTTPS port to redirect to (default: 443) * @param options Additional route options * @returns Route configuration object */ export function createHttpToHttpsRedirect(domains, httpsPort = 443, options = {}) { // Create route match const match = { ports: options.match?.ports || 80, domains }; // Create route action const action = { type: 'socket-handler', socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301) }; // Create the route config return { match, action, name: options.name || `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}`, ...options }; } /** * Create an HTTPS passthrough route (SNI-based forwarding without TLS termination) * @param domains Domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createHttpsPassthroughRoute(domains, target, options = {}) { // Create route match const match = { ports: options.match?.ports || 443, domains }; // Create route action const action = { type: 'forward', targets: [target], tls: { mode: 'passthrough' } }; // Create the route config return { match, action, name: options.name || `HTTPS Passthrough for ${Array.isArray(domains) ? domains.join(', ') : domains}`, ...options }; } /** * Create a complete HTTPS server with HTTP to HTTPS redirects * @param domains Domain(s) to match * @param target Target host and port * @param options Additional configuration options * @returns Array of two route configurations (HTTPS and HTTP redirect) */ export function createCompleteHttpsServer(domains, target, options = {}) { // Create the HTTPS route const httpsRoute = createHttpsTerminateRoute(domains, target, options); // Create the HTTP redirect route const httpRedirectRoute = createHttpToHttpsRedirect(domains, // Extract the HTTPS port from the HTTPS route - ensure it's a number typeof options.httpsPort === 'number' ? options.httpsPort : Array.isArray(options.httpsPort) ? options.httpsPort[0] : 443, { // Set the HTTP port match: { ports: options.httpPort || 80, domains }, name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}` }); return [httpsRoute, httpRedirectRoute]; } /** * Create a load balancer route (round-robin between multiple backend hosts) * @param domains Domain(s) to match * @param backendsOrHosts Array of backend servers OR array of host strings (legacy) * @param portOrOptions Port number (legacy) OR options object * @param options Additional route options (legacy) * @returns Route configuration object */ export function createLoadBalancerRoute(domains, backendsOrHosts, portOrOptions, options) { // Handle legacy signature: (domains, hosts[], port, options) let backends; let finalOptions; if (Array.isArray(backendsOrHosts) && backendsOrHosts.length > 0 && typeof backendsOrHosts[0] === 'string') { // Legacy signature const hosts = backendsOrHosts; const port = portOrOptions; backends = hosts.map(host => ({ host, port })); finalOptions = options || {}; } else { // New signature backends = backendsOrHosts; finalOptions = portOrOptions || {}; } // Extract hosts and ensure all backends use the same port const port = backends[0].port; const hosts = backends.map(backend => backend.host); // Create route match const match = { ports: finalOptions.match?.ports || (finalOptions.tls || finalOptions.useTls ? 443 : 80), domains }; // Create route target const target = { host: hosts, port }; // Create route action const action = { type: 'forward', targets: [target] }; // Add TLS configuration if provided if (finalOptions.tls || finalOptions.useTls) { action.tls = { mode: finalOptions.tls?.mode || 'terminate', certificate: finalOptions.tls?.certificate || finalOptions.certificate || 'auto' }; } // Add load balancing options if (finalOptions.algorithm || finalOptions.healthCheck) { action.loadBalancing = { algorithm: finalOptions.algorithm || 'round-robin', healthCheck: finalOptions.healthCheck }; } // Create the route config return { match, action, name: finalOptions.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`, ...finalOptions }; } /** * Create an API route configuration * @param domains Domain(s) to match * @param apiPath API base path (e.g., "/api") * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createApiRoute(domains, apiPath, target, options = {}) { // Normalize API path const normalizedPath = apiPath.startsWith('/') ? apiPath : `/${apiPath}`; const pathWithWildcard = normalizedPath.endsWith('/') ? `${normalizedPath}*` : `${normalizedPath}/*`; // Create route match const match = { ports: options.useTls ? (options.httpsPort || 443) : (options.httpPort || 80), domains, path: pathWithWildcard }; // Create route action const action = { type: 'forward', targets: [target] }; // Add TLS configuration if using HTTPS if (options.useTls) { action.tls = { mode: 'terminate', certificate: options.certificate || 'auto' }; } // Add CORS headers if requested const headers = {}; if (options.addCorsHeaders) { headers.response = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400' }; } // Create the route config return { match, action, headers: Object.keys(headers).length > 0 ? headers : undefined, name: options.name || `API Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, priority: options.priority || 100, // Higher priority for specific path matches ...options }; } /** * Create a WebSocket route configuration * @param domains Domain(s) to match * @param targetOrPath Target server OR WebSocket path (legacy) * @param targetOrOptions Target server (legacy) OR options * @param options Additional route options (legacy) * @returns Route configuration object */ export function createWebSocketRoute(domains, targetOrPath, targetOrOptions, options) { // Handle different signatures let target; let wsPath; let finalOptions; if (typeof targetOrPath === 'string') { // Legacy signature: (domains, path, target, options) wsPath = targetOrPath; target = targetOrOptions; finalOptions = options || {}; } else { // New signature: (domains, target, options) target = targetOrPath; finalOptions = targetOrOptions || {}; wsPath = finalOptions.path || '/ws'; } // Normalize WebSocket path const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`; // Create route match const match = { ports: finalOptions.useTls ? (finalOptions.httpsPort || 443) : (finalOptions.httpPort || 80), domains, path: normalizedPath }; // Create route action const action = { type: 'forward', targets: [target], websocket: { enabled: true, pingInterval: finalOptions.pingInterval || 30000, // 30 seconds pingTimeout: finalOptions.pingTimeout || 5000 // 5 seconds } }; // Add TLS configuration if using HTTPS if (finalOptions.useTls) { action.tls = { mode: 'terminate', certificate: finalOptions.certificate || 'auto' }; } // Create the route config return { match, action, name: finalOptions.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, priority: finalOptions.priority || 100, // Higher priority for WebSocket routes ...finalOptions }; } /** * Create a helper function that applies a port offset * @param offset The offset to apply to the matched port * @returns A function that adds the offset to the matched port */ export function createPortOffset(offset) { return (context) => context.port + offset; } /** * Create a port mapping route with context-based port function * @param options Port mapping route options * @returns Route configuration object */ export function createPortMappingRoute(options) { // Create route match const match = { ports: options.sourcePortRange, domains: options.domains }; // Create route action const action = { type: 'forward', targets: [{ host: options.targetHost, port: options.portMapper }] }; // Create the route config return { match, action, name: options.name || `Port Mapping Route for ${options.domains || 'all domains'}`, priority: options.priority, ...options }; } /** * Create a simple offset port mapping route * @param options Offset port mapping route options * @returns Route configuration object */ export function createOffsetPortMappingRoute(options) { return createPortMappingRoute({ sourcePortRange: options.ports, targetHost: options.targetHost, portMapper: (context) => context.port + options.offset, name: options.name || `Offset Mapping (${options.offset > 0 ? '+' : ''}${options.offset}) for ${options.domains || 'all domains'}`, domains: options.domains, priority: options.priority, ...options }); } /** * Create a dynamic route with context-based host and port mapping * @param options Dynamic route options * @returns Route configuration object */ export function createDynamicRoute(options) { // Create route match const match = { ports: options.ports, domains: options.domains, path: options.path, clientIp: options.clientIp }; // Create route action const action = { type: 'forward', targets: [{ host: options.targetHost, port: options.portMapper }] }; // Create the route config return { match, action, name: options.name || `Dynamic Route for ${options.domains || 'all domains'}`, priority: options.priority, ...options }; } /** * Create a smart load balancer with dynamic domain-based backend selection * @param options Smart load balancer options * @returns Route configuration object */ export function createSmartLoadBalancer(options) { // Extract all domain keys to create the match criteria const domains = Object.keys(options.domainTargets); // Create the smart host selector function const hostSelector = (context) => { const domain = context.domain || ''; return options.domainTargets[domain] || options.defaultTarget || 'localhost'; }; // Create route match const match = { ports: options.ports, domains }; // Create route action const action = { type: 'forward', targets: [{ host: hostSelector, port: options.portMapper }] }; // Create the route config return { match, action, name: options.name || `Smart Load Balancer for ${domains.join(', ')}`, priority: options.priority, ...options }; } /** * Create an NFTables-based route for high-performance packet forwarding * @param nameOrDomains Name or domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createNfTablesRoute(nameOrDomains, target, options = {}) { // Determine if this is a name or domain let name; let domains; if (Array.isArray(nameOrDomains) || (typeof nameOrDomains === 'string' && nameOrDomains.includes('.'))) { domains = nameOrDomains; name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains; } else { name = nameOrDomains; domains = undefined; // No domains } // Create route match const match = { domains, ports: options.ports || 80 }; // Create route action const action = { type: 'forward', targets: [{ host: target.host, port: target.port }], forwardingEngine: 'nftables', nftables: { protocol: options.protocol || 'tcp', preserveSourceIP: options.preserveSourceIP, maxRate: options.maxRate, priority: options.priority, tableName: options.tableName, useIPSets: options.useIPSets, useAdvancedNAT: options.useAdvancedNAT } }; // Add TLS options if needed if (options.useTls) { action.tls = { mode: 'passthrough' }; } // Create the route config const routeConfig = { name, match, action }; // Add security if allowed or blocked IPs are specified if (options.ipAllowList?.length || options.ipBlockList?.length) { routeConfig.security = { ipAllowList: options.ipAllowList, ipBlockList: options.ipBlockList }; } return routeConfig; } /** * Create an NFTables-based TLS termination route * @param nameOrDomains Name or domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createNfTablesTerminateRoute(nameOrDomains, target, options = {}) { // Create basic NFTables route const route = createNfTablesRoute(nameOrDomains, target, { ...options, ports: options.ports || 443, useTls: false }); // Set TLS termination route.action.tls = { mode: 'terminate', certificate: options.certificate || 'auto' }; return route; } /** * Create a complete NFTables-based HTTPS setup with HTTP redirect * @param nameOrDomains Name or domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Array of two route configurations (HTTPS and HTTP redirect) */ export function createCompleteNfTablesHttpsServer(nameOrDomains, target, options = {}) { // Create the HTTPS route using NFTables const httpsRoute = createNfTablesTerminateRoute(nameOrDomains, target, { ...options, ports: options.httpsPort || 443 }); // Determine the domain(s) for HTTP redirect const domains = typeof nameOrDomains === 'string' && !nameOrDomains.includes('.') ? undefined : nameOrDomains; // Extract the HTTPS port for the redirect destination const httpsPort = typeof options.httpsPort === 'number' ? options.httpsPort : Array.isArray(options.httpsPort) && typeof options.httpsPort[0] === 'number' ? options.httpsPort[0] : 443; // Create the HTTP redirect route (this uses standard forwarding, not NFTables) const httpRedirectRoute = createHttpToHttpsRedirect(domains, // Type cast needed since domains can be undefined now httpsPort, { match: { ports: options.httpPort || 80, domains: domains // Type cast needed since domains can be undefined now }, name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains || 'all domains'}` }); return [httpsRoute, httpRedirectRoute]; } /** * Create a socket handler route configuration * @param domains Domain(s) to match * @param ports Port(s) to listen on * @param handler Socket handler function * @param options Additional route options * @returns Route configuration object */ export function createSocketHandlerRoute(domains, ports, handler, options = {}) { return { name: options.name || 'socket-handler-route', priority: options.priority !== undefined ? options.priority : 50, match: { domains, ports, ...(options.path && { path: options.path }) }, action: { type: 'socket-handler', socketHandler: handler } }; } /** * Pre-built socket handlers for common use cases */ export const SocketHandlers = { /** * Simple echo server handler */ echo: (socket, context) => { socket.write('ECHO SERVER READY\n'); socket.on('data', data => socket.write(data)); }, /** * TCP proxy handler */ proxy: (targetHost, targetPort) => (socket, context) => { const target = plugins.net.connect(targetPort, targetHost); socket.pipe(target); target.pipe(socket); socket.on('close', () => target.destroy()); target.on('close', () => socket.destroy()); target.on('error', (err) => { console.error('Proxy target error:', err); socket.destroy(); }); }, /** * Line-based protocol handler */ lineProtocol: (handler) => (socket, context) => { let buffer = ''; socket.on('data', (data) => { buffer += data.toString(); const lines = buffer.split('\n'); buffer = lines.pop() || ''; lines.forEach(line => { if (line.trim()) { handler(line.trim(), socket); } }); }); }, /** * Simple HTTP response handler (for testing) */ httpResponse: (statusCode, body) => (socket, context) => { const response = [ `HTTP/1.1 ${statusCode} ${statusCode === 200 ? 'OK' : 'Error'}`, 'Content-Type: text/plain', `Content-Length: ${body.length}`, 'Connection: close', '', body ].join('\r\n'); socket.write(response); socket.end(); }, /** * Block connection immediately */ block: (message) => (socket, context) => { const finalMessage = message || `Connection blocked from ${context.clientIp}`; if (finalMessage) { socket.write(finalMessage); } socket.end(); }, /** * HTTP block response */ httpBlock: (statusCode = 403, message) => (socket, context) => { const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`; const finalMessage = message || defaultMessage; const response = [ `HTTP/1.1 ${statusCode} ${finalMessage}`, 'Content-Type: text/plain', `Content-Length: ${finalMessage.length}`, 'Connection: close', '', finalMessage ].join('\r\n'); socket.write(response); socket.end(); }, /** * HTTP redirect handler * Now uses the centralized detection module for HTTP parsing */ httpRedirect: (locationTemplate, statusCode = 301) => (socket, context) => { const connectionId = ProtocolDetector.createConnectionId({ socketId: context.connectionId || `${Date.now()}-${Math.random()}` }); socket.once('data', async (data) => { // Use detection module for parsing const detectionResult = await ProtocolDetector.detectWithConnectionTracking(data, connectionId, { extractFullHeaders: false } // We only need method and path ); if (detectionResult.protocol === 'http' && detectionResult.connectionInfo.path) { const method = detectionResult.connectionInfo.method || 'GET'; const path = detectionResult.connectionInfo.path || '/'; const domain = context.domain || 'localhost'; const port = context.port; let finalLocation = locationTemplate .replace('{domain}', domain) .replace('{port}', String(port)) .replace('{path}', path) .replace('{clientIp}', context.clientIp); const message = `Redirecting to ${finalLocation}`; const response = [ `HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`, `Location: ${finalLocation}`, 'Content-Type: text/plain', `Content-Length: ${message.length}`, 'Connection: close', '', message ].join('\r\n'); socket.write(response); } else { // Not a valid HTTP request, close connection socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'); } socket.end(); // Clean up detection state ProtocolDetector.cleanupConnections(); }); }, /** * HTTP server handler for ACME challenges and other HTTP needs * Now uses the centralized detection module for HTTP parsing */ httpServer: (handler) => (socket, context) => { let requestParsed = false; const connectionId = ProtocolDetector.createConnectionId({ socketId: context.connectionId || `${Date.now()}-${Math.random()}` }); const processData = async (data) => { if (requestParsed) return; // Only handle the first request // Use HttpDetector for parsing const detectionResult = await ProtocolDetector.detectWithConnectionTracking(data, connectionId, { extractFullHeaders: true }); if (detectionResult.protocol !== 'http' || !detectionResult.isComplete) { // Not a complete HTTP request yet return; } requestParsed = true; const connInfo = detectionResult.connectionInfo; // Create request object from detection result const req = { method: connInfo.method || 'GET', url: connInfo.path || '/', headers: connInfo.headers || {}, body: detectionResult.remainingBuffer?.toString() || '' }; // Create response object let statusCode = 200; const responseHeaders = {}; let ended = false; const res = { status: (code) => { statusCode = code; }, header: (name, value) => { responseHeaders[name] = value; }, send: (data) => { if (ended) return; ended = true; if (!responseHeaders['content-type']) { responseHeaders['content-type'] = 'text/plain'; } responseHeaders['content-length'] = String(data.length); responseHeaders['connection'] = 'close'; const statusText = statusCode === 200 ? 'OK' : statusCode === 404 ? 'Not Found' : statusCode === 500 ? 'Internal Server Error' : 'Response'; let response = `HTTP/1.1 ${statusCode} ${statusText}\r\n`; for (const [name, value] of Object.entries(responseHeaders)) { response += `${name}: ${value}\r\n`; } response += '\r\n'; response += data; socket.write(response); socket.end(); }, end: () => { if (ended) return; ended = true; socket.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'); socket.end(); } }; try { handler(req, res); // Ensure response is sent even if handler doesn't call send() setTimeout(() => { if (!ended) { res.send(''); } }, 1000); } catch (error) { if (!ended) { res.status(500); res.send('Internal Server Error'); } } }; socket.on('data', processData); socket.on('error', () => { if (!requestParsed) { socket.end(); } }); socket.on('close', () => { // Clean up detection state ProtocolDetector.cleanupConnections(); }); } }; /** * Create an API Gateway route pattern * @param domains Domain(s) to match * @param apiBasePath Base path for API endpoints (e.g., '/api') * @param target Target host and port * @param options Additional route options * @returns API route configuration */ export function createApiGatewayRoute(domains, apiBasePath, target, options = {}) { // Normalize apiBasePath to ensure it starts with / and doesn't end with / const normalizedPath = apiBasePath.startsWith('/') ? apiBasePath : `/${apiBasePath}`; // Add wildcard to path to match all API endpoints const apiPath = normalizedPath.endsWith('/') ? `${normalizedPath}*` : `${normalizedPath}/*`; // Create base route const baseRoute = options.useTls ? createHttpsTerminateRoute(domains, target, { certificate: options.certificate || 'auto' }) : createHttpRoute(domains, target); // Add API-specific configurations const apiRoute = { match: { ...baseRoute.match, path: apiPath }, name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`, priority: options.priority || 100 // Higher priority for specific path matching }; // Add CORS headers if requested if (options.addCorsHeaders) { apiRoute.headers = { response: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400' } }; } return mergeRouteConfigs(baseRoute, apiRoute); } /** * Create a rate limiting route pattern * @param baseRoute Base route to add rate limiting to * @param rateLimit Rate limiting configuration * @returns Route with rate limiting */ export function addRateLimiting(baseRoute, rateLimit) { return mergeRouteConfigs(baseRoute, { security: { rateLimit: { enabled: true, maxRequests: rateLimit.maxRequests, window: rateLimit.window, keyBy: rateLimit.keyBy || 'ip', headerName: rateLimit.headerName, errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.' } } }); } /** * Create a basic authentication route pattern * @param baseRoute Base route to add authentication to * @param auth Authentication configuration * @returns Route with basic authentication */ export function addBasicAuth(baseRoute, auth) { return mergeRouteConfigs(baseRoute, { security: { basicAuth: { enabled: true, users: auth.users, realm: auth.realm || 'Restricted Area', excludePaths: auth.excludePaths || [] } } }); } /** * Create a JWT authentication route pattern * @param baseRoute Base route to add JWT authentication to * @param jwt JWT authentication configuration * @returns Route with JWT authentication */ export function addJwtAuth(baseRoute, jwt) { return mergeRouteConfigs(baseRoute, { security: { jwtAuth: { enabled: true, secret: jwt.secret, algorithm: jwt.algorithm || 'HS256', issuer: jwt.issuer, audience: jwt.audience, expiresIn: jwt.expiresIn, excludePaths: jwt.excludePaths || [] } } }); } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUtaGVscGVycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL3Byb3hpZXMvc21hcnQtcHJveHkvdXRpbHMvcm91dGUtaGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBa0JHO0FBRUgsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUUvQyxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNyRCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsWUFBWSxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFFN0U7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLGVBQWUsQ0FDN0IsT0FBMEIsRUFDMUIsTUFBaUQsRUFDakQsVUFBaUMsRUFBRTtJQUVuQyxxQkFBcUI7SUFDckIsTUFBTSxLQUFLLEdBQWdCO1FBQ3pCLEtBQUssRUFBRSxPQUFPLENBQUMsS0FBSyxFQUFFLEtBQUssSUFBSSxFQUFFO1FBQ2pDLE9BQU87S0FDUixDQUFDO0lBRUYsc0JBQXNCO0lBQ3RCLE1BQU0sTUFBTSxHQUFpQjtRQUMzQixJQUFJLEVBQUUsU0FBUztRQUNmLE9BQU8sRUFBRSxDQUFDLE1BQU0sQ0FBQztLQUNsQixDQUFDO0lBRUYsMEJBQTBCO0lBQzFCLE9BQU87UUFDTCxLQUFLO1FBQ0wsTUFBTTtRQUNOLElBQUksRUFBRSxPQUFPLENBQUMsSUFBSSxJQUFJLGtCQUFrQixLQUFLLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLEVBQUU7UUFDL0YsR0FBRyxPQUFPO0tBQ1gsQ0FBQztBQUNKLENBQUM7QUFFRDs7Ozs7O0dBTUc7QUFDSCxNQUFNLFVBQVUseUJBQXlCLENBQ3ZDLE9BQTBCLEVBQzFCLE1BQWlELEVBQ2pELFVBT0ksRUFBRTtJQUVOLHFC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