@push.rocks/smartproxy
Version:
A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.
754 lines • 77.7 kB
JavaScript
import * as plugins from '../plugins.js';
import { ConnectionManager } from './classes.pp.connectionmanager.js';
import { SecurityManager } from './classes.pp.securitymanager.js';
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
import { TlsManager } from './classes.pp.tlsmanager.js';
import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
import { PortRangeManager } from './classes.pp.portrangemanager.js';
/**
* Handles new connection processing and setup logic
*/
export class ConnectionHandler {
constructor(settings, connectionManager, securityManager, domainConfigManager, tlsManager, networkProxyBridge, timeoutManager, portRangeManager) {
this.settings = settings;
this.connectionManager = connectionManager;
this.securityManager = securityManager;
this.domainConfigManager = domainConfigManager;
this.tlsManager = tlsManager;
this.networkProxyBridge = networkProxyBridge;
this.timeoutManager = timeoutManager;
this.portRangeManager = portRangeManager;
}
/**
* Handle a new incoming connection
*/
handleConnection(socket) {
const remoteIP = socket.remoteAddress || '';
const localPort = socket.localPort || 0;
// Validate IP against rate limits and connection limits
const ipValidation = this.securityManager.validateIP(remoteIP);
if (!ipValidation.allowed) {
console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`);
socket.end();
socket.destroy();
return;
}
// Create a new connection record
const record = this.connectionManager.createConnection(socket);
const connectionId = record.id;
// Apply socket optimizations
socket.setNoDelay(this.settings.noDelay);
// Apply keep-alive settings if enabled
if (this.settings.keepAlive) {
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
record.hasKeepAlive = true;
// Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) {
try {
// These are platform-specific and may not be available
if ('setKeepAliveProbes' in socket) {
socket.setKeepAliveProbes(10);
}
if ('setKeepAliveInterval' in socket) {
socket.setKeepAliveInterval(1000);
}
}
catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
}
}
}
}
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
`Active connections: ${this.connectionManager.getConnectionCount()}`);
}
else {
console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`);
}
// Check if this connection should be forwarded directly to NetworkProxy
if (this.portRangeManager.shouldUseNetworkProxy(localPort)) {
this.handleNetworkProxyConnection(socket, record);
}
else {
// For non-NetworkProxy ports, proceed with normal processing
this.handleStandardConnection(socket, record);
}
}
/**
* Handle a connection that should be forwarded to NetworkProxy
*/
handleNetworkProxyConnection(socket, record) {
const connectionId = record.id;
let initialDataReceived = false;
// Set an initial timeout for handshake data
let initialTimeout = setTimeout(() => {
if (!initialDataReceived) {
console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`);
// Add a grace period instead of immediate termination
setTimeout(() => {
if (!initialDataReceived) {
console.log(`[${connectionId}] Final initial data timeout after grace period`);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'initial_timeout';
this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'initial_timeout');
}
}, 30000); // 30 second grace period
}
}, this.settings.initialDataTimeout);
// Make sure timeout doesn't keep the process alive
if (initialTimeout.unref) {
initialTimeout.unref();
}
// Set up error handler
socket.on('error', this.connectionManager.handleError('incoming', record));
// First data handler to capture initial TLS handshake for NetworkProxy
socket.once('data', (chunk) => {
// Clear the initial timeout since we've received data
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
initialDataReceived = true;
record.hasReceivedInitialData = true;
// Block non-TLS connections on port 443
const localPort = record.localPort;
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
console.log(`[${connectionId}] Non-TLS connection detected on port 443. ` +
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'non_tls_blocked';
this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
return;
}
// Check if this looks like a TLS handshake
if (this.tlsManager.isTlsHandshake(chunk)) {
record.isTLS = true;
// Check for ClientHello to extract SNI - but don't enforce it for NetworkProxy
if (this.tlsManager.isClientHello(chunk)) {
// Create connection info for SNI extraction
const connInfo = {
sourceIp: record.remoteIP,
sourcePort: socket.remotePort || 0,
destIp: socket.localAddress || '',
destPort: socket.localPort || 0,
};
// Extract SNI for domain-specific NetworkProxy handling if available
const serverName = this.tlsManager.extractSNI(chunk, connInfo);
// For NetworkProxy connections, we'll allow session tickets even without SNI
// We'll only use the serverName if available to determine the specific NetworkProxy port
if (serverName) {
// Save domain config and SNI in connection record
const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
record.domainConfig = domainConfig;
record.lockedDomain = serverName;
// Use domain-specific NetworkProxy port if configured
if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`);
}
// Forward to NetworkProxy with domain-specific port
this.networkProxyBridge.forwardToNetworkProxy(connectionId, socket, record, chunk, networkProxyPort, (reason) => this.connectionManager.initiateCleanupOnce(record, reason));
return;
}
}
else if (this.settings.allowSessionTicket === false &&
this.settings.enableDetailedLogging) {
// Log that we're allowing a session resumption without SNI for NetworkProxy
console.log(`[${connectionId}] Allowing session resumption without SNI for NetworkProxy forwarding`);
}
}
// Forward directly to NetworkProxy without domain-specific settings
this.networkProxyBridge.forwardToNetworkProxy(connectionId, socket, record, chunk, undefined, (reason) => this.connectionManager.initiateCleanupOnce(record, reason));
}
else {
// If not TLS, use normal direct connection
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`);
this.setupDirectConnection(socket, record, undefined, undefined, chunk);
}
});
}
/**
* Handle a standard (non-NetworkProxy) connection
*/
handleStandardConnection(socket, record) {
const connectionId = record.id;
const localPort = record.localPort;
// Define helpers for rejecting connections
const rejectIncomingConnection = (reason, logMessage) => {
console.log(`[${connectionId}] ${logMessage}`);
socket.end();
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = reason;
this.connectionManager.incrementTerminationStat('incoming', reason);
}
this.connectionManager.cleanupConnection(record, reason);
};
let initialDataReceived = false;
// Set an initial timeout for SNI data if needed
let initialTimeout = null;
if (this.settings.sniEnabled) {
initialTimeout = setTimeout(() => {
if (!initialDataReceived) {
console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`);
// Add a grace period instead of immediate termination
setTimeout(() => {
if (!initialDataReceived) {
console.log(`[${connectionId}] Final initial data timeout after grace period`);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'initial_timeout';
this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'initial_timeout');
}
}, 30000); // 30 second grace period
}
}, this.settings.initialDataTimeout);
// Make sure timeout doesn't keep the process alive
if (initialTimeout.unref) {
initialTimeout.unref();
}
}
else {
initialDataReceived = true;
record.hasReceivedInitialData = true;
}
socket.on('error', this.connectionManager.handleError('incoming', record));
// Track data for bytes counting
socket.on('data', (chunk) => {
record.bytesReceived += chunk.length;
this.timeoutManager.updateActivity(record);
// Check for TLS handshake if this is the first chunk
if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
record.isTLS = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake detected from ${record.remoteIP}, ${chunk.length} bytes`);
}
}
});
/**
* Sets up the connection to the target host.
*/
const setupConnection = (serverName, initialChunk, forcedDomain, overridePort) => {
// Clear the initial timeout since we've received data
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
// Mark that we've received initial data
initialDataReceived = true;
record.hasReceivedInitialData = true;
// Check if this looks like a TLS handshake
if (initialChunk && this.tlsManager.isTlsHandshake(initialChunk)) {
record.isTLS = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`);
}
}
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
const domainConfig = forcedDomain
? forcedDomain
: serverName
? this.domainConfigManager.findDomainConfig(serverName)
: undefined;
// Save domain config in connection record
record.domainConfig = domainConfig;
// Check if this domain should use NetworkProxy (domain-specific setting)
if (domainConfig &&
this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
this.networkProxyBridge.getNetworkProxy()) {
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`);
}
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
if (initialChunk && record.isTLS) {
// For TLS connections with initial chunk, forward to NetworkProxy
this.networkProxyBridge.forwardToNetworkProxy(connectionId, socket, record, initialChunk, networkProxyPort, (reason) => this.connectionManager.initiateCleanupOnce(record, reason));
return; // Skip normal connection setup
}
}
// IP validation
if (domainConfig) {
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
// Skip IP validation if allowedIPs is empty
if (domainConfig.allowedIPs.length > 0 &&
!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
}
}
else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
if (!this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${record.remoteIP} not allowed by default allowed list`);
}
}
// Save the initial SNI
if (serverName) {
record.lockedDomain = serverName;
}
// Set up the direct connection
this.setupDirectConnection(socket, record, domainConfig, serverName, initialChunk, overridePort);
};
// --- PORT RANGE-BASED HANDLING ---
// Only apply port-based rules if the incoming port is within one of the global port ranges.
if (this.portRangeManager.isPortInGlobalRanges(localPort)) {
if (this.portRangeManager.shouldUseGlobalForwarding(localPort)) {
if (this.settings.defaultAllowedIPs &&
this.settings.defaultAllowedIPs.length > 0 &&
!this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)) {
console.log(`[${connectionId}] Connection from ${record.remoteIP} rejected: IP ${record.remoteIP} not allowed in global default allowed list.`);
socket.end();
return;
}
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
}
setupConnection('', undefined, {
domains: ['global'],
allowedIPs: this.settings.defaultAllowedIPs || [],
blockedIPs: this.settings.defaultBlockedIPs || [],
targetIPs: [this.settings.targetIP],
portRanges: [],
}, localPort);
return;
}
else {
// Attempt to find a matching forced domain config based on the local port.
const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
if (forcedDomain) {
const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
console.log(`[${connectionId}] Connection from ${record.remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
socket.end();
return;
}
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
}
setupConnection('', undefined, forcedDomain, localPort);
return;
}
// Fall through to SNI/default handling if no forced domain config is found.
}
}
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
if (this.settings.sniEnabled) {
initialDataReceived = false;
socket.once('data', (chunk) => {
// Clear timeout immediately
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
initialDataReceived = true;
// Block non-TLS connections on port 443
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
console.log(`[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'non_tls_blocked';
this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
return;
}
// Try to extract SNI
let serverName = '';
if (this.tlsManager.isTlsHandshake(chunk)) {
record.isTLS = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`);
}
// Create connection info object for SNI extraction
const connInfo = {
sourceIp: record.remoteIP,
sourcePort: socket.remotePort || 0,
destIp: socket.localAddress || '',
destPort: socket.localPort || 0,
};
// Extract SNI
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
// If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection
if (this.settings.allowSessionTicket === false &&
this.tlsManager.isClientHello(chunk) &&
!serverName) {
// Block ClientHello without SNI when allowSessionTicket is false
console.log(`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`);
// Set the termination reason first
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
}
// Create a warning-level alert for unrecognized_name
// This encourages Chrome to retry immediately with SNI
const serverNameUnknownAlertData = Buffer.from([
0x15, // Alert record type
0x03,
0x03, // TLS 1.2 version
0x00,
0x02, // Length
0x01, // Warning alert level (not fatal)
0x70, // unrecognized_name alert (code 112)
]);
try {
// Use cork/uncork to ensure the alert is sent as a single packet
socket.cork();
const writeSuccessful = socket.write(serverNameUnknownAlertData);
socket.uncork();
socket.end();
// Function to handle the clean socket termination - but more gradually
const finishConnection = () => {
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
};
if (writeSuccessful) {
// Wait longer before ending connection to ensure alert is processed by client
setTimeout(finishConnection, 200); // Increased from 50ms to 200ms
}
else {
// If the kernel buffer was full, wait for the drain event
socket.once('drain', () => {
// Wait longer after drain as well
setTimeout(finishConnection, 200);
});
// Safety timeout is increased too
setTimeout(() => {
socket.removeAllListeners('drain');
finishConnection();
}, 400); // Increased from 250ms to 400ms
}
}
catch (err) {
// If we can't send the alert, fall back to immediate termination
console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`);
socket.end();
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
}
return;
}
}
// Lock the connection to the negotiated SNI.
record.lockedDomain = serverName;
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Received connection from ${record.remoteIP} with SNI: ${serverName || '(empty)'}`);
}
setupConnection(serverName, chunk);
});
}
else {
initialDataReceived = true;
record.hasReceivedInitialData = true;
if (this.settings.defaultAllowedIPs &&
this.settings.defaultAllowedIPs.length > 0 &&
!this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${record.remoteIP} not allowed for non-SNI connection`);
}
setupConnection('');
}
}
/**
* Sets up a direct connection to the target
*/
setupDirectConnection(socket, record, domainConfig, serverName, initialChunk, overridePort) {
const connectionId = record.id;
// Determine target host
const targetHost = domainConfig
? this.domainConfigManager.getTargetIP(domainConfig)
: this.settings.targetIP;
// Determine target port
const targetPort = overridePort !== undefined ? overridePort : this.settings.toPort;
// Setup connection options
const connectionOptions = {
host: targetHost,
port: targetPort,
};
// Preserve source IP if configured
if (this.settings.preserveSourceIP) {
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
}
// Create a safe queue for incoming data
const dataQueue = [];
let queueSize = 0;
let processingQueue = false;
let drainPending = false;
let pipingEstablished = false;
// Pause the incoming socket to prevent buffer overflows
socket.pause();
// Function to safely process the data queue without losing events
const processDataQueue = () => {
if (processingQueue || dataQueue.length === 0 || pipingEstablished)
return;
processingQueue = true;
try {
// Process all queued chunks with the current active handler
while (dataQueue.length > 0) {
const chunk = dataQueue.shift();
queueSize -= chunk.length;
// Once piping is established, we shouldn't get here,
// but just in case, pass to the outgoing socket directly
if (pipingEstablished && record.outgoing) {
record.outgoing.write(chunk);
continue;
}
// Track bytes received
record.bytesReceived += chunk.length;
// Check for TLS handshake
if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
record.isTLS = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`);
}
}
// Check if adding this chunk would exceed the buffer limit
const newSize = record.pendingDataSize + chunk.length;
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
console.log(`[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`);
socket.end(); // Gracefully close the socket
this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
return;
}
// Buffer the chunk and update the size counter
record.pendingData.push(Buffer.from(chunk));
record.pendingDataSize = newSize;
this.timeoutManager.updateActivity(record);
}
}
finally {
processingQueue = false;
// If there's a pending drain and we've processed everything,
// signal we're ready for more data if we haven't established piping yet
if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
drainPending = false;
socket.resume();
}
}
};
// Unified data handler that safely queues incoming data
const safeDataHandler = (chunk) => {
// If piping is already established, just let the pipe handle it
if (pipingEstablished)
return;
// Add to our queue for orderly processing
dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
queueSize += chunk.length;
// If queue is getting large, pause socket until we catch up
if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
socket.pause();
drainPending = true;
}
// Process the queue
processDataQueue();
};
// Add our safe data handler
socket.on('data', safeDataHandler);
// Add initial chunk to pending data if present
if (initialChunk) {
record.bytesReceived += initialChunk.length;
record.pendingData.push(Buffer.from(initialChunk));
record.pendingDataSize = initialChunk.length;
}
// Create the target socket but don't set up piping immediately
const targetSocket = plugins.net.connect(connectionOptions);
record.outgoing = targetSocket;
record.outgoingStartTime = Date.now();
// Apply socket optimizations
targetSocket.setNoDelay(this.settings.noDelay);
// Apply keep-alive settings to the outgoing connection as well
if (this.settings.keepAlive) {
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
// Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) {
try {
if ('setKeepAliveProbes' in targetSocket) {
targetSocket.setKeepAliveProbes(10);
}
if ('setKeepAliveInterval' in targetSocket) {
targetSocket.setKeepAliveInterval(1000);
}
}
catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
}
}
}
}
// Setup specific error handler for connection phase
targetSocket.once('error', (err) => {
// This handler runs only once during the initial connection phase
const code = err.code;
console.log(`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`);
// Resume the incoming socket to prevent it from hanging
socket.resume();
if (code === 'ECONNREFUSED') {
console.log(`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`);
}
else if (code === 'ETIMEDOUT') {
console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`);
}
else if (code === 'ECONNRESET') {
console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`);
}
else if (code === 'EHOSTUNREACH') {
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
}
// Clear any existing error handler after connection phase
targetSocket.removeAllListeners('error');
// Re-add the normal error handler for established connections
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
if (record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'connection_failed';
this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
}
// Clean up the connection
this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
});
// Setup close handler
targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
socket.on('close', this.connectionManager.handleClose('incoming', record));
// Handle timeouts with keep-alive awareness
socket.on('timeout', () => {
// For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) {
console.log(`[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`);
return;
}
// For non-keep-alive connections, proceed with normal cleanup
console.log(`[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('incoming', 'timeout');
}
this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
});
targetSocket.on('timeout', () => {
// For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) {
console.log(`[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`);
return;
}
// For non-keep-alive connections, proceed with normal cleanup
console.log(`[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
if (record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
}
this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
});
// Apply socket timeouts
this.timeoutManager.applySocketTimeouts(record);
// Track outgoing data for bytes counting
targetSocket.on('data', (chunk) => {
record.bytesSent += chunk.length;
this.timeoutManager.updateActivity(record);
});
// Wait for the outgoing connection to be ready before setting up piping
targetSocket.once('connect', () => {
// Clear the initial connection error handler
targetSocket.removeAllListeners('error');
// Add the normal error handler for established connections
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
// Process any remaining data in the queue before switching to piping
processDataQueue();
// Set up piping immediately
pipingEstablished = true;
// Flush all pending data to target
if (record.pendingData.length > 0) {
const combinedData = Buffer.concat(record.pendingData);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`);
}
// Write pending data immediately
targetSocket.write(combinedData, (err) => {
if (err) {
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
}
});
// Clear the buffer now that we've processed it
record.pendingData = [];
record.pendingDataSize = 0;
}
// Setup piping in both directions without any delays
socket.pipe(targetSocket);
targetSocket.pipe(socket);
// Resume the socket to ensure data flows
socket.resume();
// Process any data that might be queued in the interim
if (dataQueue.length > 0) {
// Write any remaining queued data directly to the target socket
for (const chunk of dataQueue) {
targetSocket.write(chunk);
}
// Clear the queue
dataQueue.length = 0;
queueSize = 0;
}
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName
? ` (SNI: ${serverName})`
: domainConfig
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
: ''}` +
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`);
}
else {
console.log(`Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName
? ` (SNI: ${serverName})`
: domainConfig
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
: ''}`);
}
// Add the renegotiation handler for SNI validation
if (serverName) {
// Create connection info object for the existing connection
const connInfo = {
sourceIp: record.remoteIP,
sourcePort: record.incoming.remotePort || 0,
destIp: record.incoming.localAddress || '',
destPort: record.incoming.localPort || 0,
};
// Create a renegotiation handler function
const renegotiationHandler = this.tlsManager.createRenegotiationHandler(connectionId, serverName, connInfo, (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason));
// Store the handler in the connection record so we can remove it during cleanup
record.renegotiationHandler = renegotiationHandler;
// Add the handler to the socket
socket.on('data', renegotiationHandler);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`);
if (this.settings.allowSessionTicket === false) {
console.log(`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`);
}
}
}
// Set connection timeout
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
console.log(`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`);
this.connectionManager.initiateCleanupOnce(record, reason);
});
// Mark TLS handshake as complete for TLS connections
if (record.isTLS) {
record.tlsHandshakeComplete = true;
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`);
}
}
});
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5wcC5jb25uZWN0aW9uaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3NtYXJ0cHJveHkvY2xhc3Nlcy5wcC5jb25uZWN0aW9uaGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQU16QyxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUN0RSxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDbEUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0scUNBQXFDLENBQUM7QUFDMUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ3hELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBQ3hFLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNoRSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUVwRTs7R0FFRztBQUNILE1BQU0sT0FBTyxpQkFBaUI7SUFDNUIsWUFDVSxRQUE0QixFQUM1QixpQkFBb0MsRUFDcEMsZUFBZ0MsRUFDaEMsbUJBQXdDLEVBQ3hDLFVBQXNCLEVBQ3RCLGtCQUFzQyxFQUN0QyxjQUE4QixFQUM5QixnQkFBa0M7UUFQbEMsYUFBUSxHQUFSLFFBQVEsQ0FBb0I7UUFDNUIsc0JBQWlCLEdBQWpCLGlCQUFpQixDQUFtQjtRQUNwQyxvQkFBZSxHQUFmLGVBQWUsQ0FBaUI7UUFDaEMsd0JBQW1CLEdBQW5CLG1CQUFtQixDQUFxQjtRQUN4QyxlQUFVLEdBQVYsVUFBVSxDQUFZO1FBQ3RCLHVCQUFrQixHQUFsQixrQkFBa0IsQ0FBb0I7UUFDdEMsbUJBQWMsR0FBZCxjQUFjLENBQWdCO1FBQzlCLHFCQUFnQixHQUFoQixnQkFBZ0IsQ0FBa0I7SUFDekMsQ0FBQztJQUVKOztPQUVHO0lBQ0ksZ0JBQWdCLENBQUMsTUFBMEI7UUFDaEQsTUFBTSxRQUFRLEdBQUcsTUFBTSxDQUFDLGFBQWEsSUFBSSxFQUFFLENBQUM7UUFDNUMsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLFNBQVMsSUFBSSxDQUFDLENBQUM7UUFFeEMsd0RBQXdEO1FBQ3hELE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxlQUFlLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQy9ELElBQUksQ0FBQyxZQUFZLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDMUIsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0QkFBNEIsUUFBUSxLQUFLLFlBQVksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDO1lBQzVFLE1BQU0sQ0FBQyxHQUFHLEVBQUUsQ0FBQztZQUNiLE1BQU0sQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNqQixPQUFPO1FBQ1QsQ0FBQztRQUVELGlDQUFpQztRQUNqQyxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsaUJBQWlCLENBQUMsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDL0QsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUUvQiw2QkFBNkI7UUFDN0IsTUFBTSxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBRXpDLHVDQUF1QztRQUN2QyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDNUIsTUFBTSxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO1lBQy9ELE1BQU0sQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDO1lBRTNCLG1EQUFtRDtZQUNuRCxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMscUJBQXFCLEVBQUUsQ0FBQztnQkFDeEMsSUFBSSxDQUFDO29CQUNILHVEQUF1RDtvQkFDdkQsSUFBSSxvQkFBb0IsSUFBSSxNQUFNLEVBQUUsQ0FBQzt3QkFDbEMsTUFBYyxDQUFDLGtCQUFrQixDQUFDLEVBQUUsQ0FBQyxDQUFDO29CQUN6QyxDQUFDO29CQUNELElBQUksc0JBQXNCLElBQUksTUFBTSxFQUFFLENBQUM7d0JBQ3BDLE1BQWMsQ0FBQyxvQkFBb0IsQ0FBQyxJQUFJLENBQUMsQ0FBQztvQkFDN0MsQ0FBQztnQkFDSCxDQUFDO2dCQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7b0JBQ2Isa0RBQWtEO29CQUNsRCxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMscUJBQXFCLEVBQUUsQ0FBQzt3QkFDeEMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLFlBQVkscURBQXFELEdBQUcsRUFBRSxDQUFDLENBQUM7b0JBQzFGLENBQUM7Z0JBQ0gsQ0FBQztZQUNILENBQUM7UUFDSCxDQUFDO1FBRUQsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUFFLENBQUM7WUFDeEMsT0FBTyxDQUFDLEdBQUcsQ0FDVCxJQUFJLFlBQVkseUJBQXlCLFFBQVEsWUFBWSxTQUFTLElBQUk7Z0JBQ3hFLGVBQWUsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxVQUFVLElBQUk7Z0JBQy9ELHVCQUF1QixJQUFJLENBQUMsaUJBQWlCLENBQUMsa0JBQWtCLEVBQUUsRUFBRSxDQUN2RSxDQUFDO1FBQ0osQ0FBQzthQUFNLENBQUM7WUFDTixPQUFPLENBQUMsR0FBRyxDQUNULHVCQUF1QixRQUFRLFlBQVksU0FBUyx5QkFBeUIsSUFBSSxDQUFDLGlCQUFpQixDQUFDLGtCQUFrQixFQUFFLEVBQUUsQ0FDM0gsQ0FBQztRQUNKLENBQUM7UUFFRCx3RUFBd0U7UUFDeEUsSUFBSSxJQUFJLENBQUMsZ0JBQWdCLENBQUMscUJBQXFCLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztZQUMzRCxJQUFJLENBQUMsNEJBQTRCLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1FBQ3BELENBQUM7YUFBTSxDQUFDO1lBQ04sNkRBQTZEO1lBQzdELElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUM7UUFDaEQsQ0FBQztJQUNILENBQUM7SUFFRDs7T0FFRztJQUNLLDRCQUE0QixDQUNsQyxNQUEwQixFQUMxQixNQUF5QjtRQUV6QixNQUFNLFlBQVksR0FBRyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQy9CLElBQUksbUJBQW1CLEdBQUcsS0FBSyxDQUFDO1FBRWhDLDRDQUE0QztRQUM1QyxJQUFJLGNBQWMsR0FBMEIsVUFBVSxDQUFDLEdBQUcsRUFBRTtZQUMxRCxJQUFJLENBQUMsbUJBQW1CLEVBQUUsQ0FBQztnQkFDekIsT0FBTyxDQUFDLEdBQUcsQ0FDVCxJQUFJLFlBQVksMkJBQTJCLElBQUksQ0FBQyxRQUFRLENBQUMsa0JBQWtCLDJCQUEyQixNQUFNLENBQUMsUUFBUSxFQUFFLENBQ3hILENBQUM7Z0JBRUYsc0RBQXNEO2dCQUN0RCxVQUFVLENBQUMsR0FBRyxFQUFFO29CQUNkLElBQUksQ0FBQyxtQkFBbUIsRUFBRSxDQUFDO3dCQUN6QixPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksWUFBWSxpREFBaUQsQ0FBQyxDQUFDO3dCQUMvRSxJQUFJLE1BQU0sQ0FBQyx5QkFBeUIsS0FBSyxJQUFJLEVBQUUsQ0FBQzs0QkFDOUMsTUFBTSxDQUFDLHlCQUF5QixHQUFHLGlCQUFpQixDQUFDOzRCQUNyRCxJQUFJLENBQUMsaUJBQWlCLENBQUMsd0JBQXdCLENBQUMsVUFBVSxFQUFFLGlCQUFpQixDQUFDLENBQUM7d0JBQ2pGLENBQUM7d0JBQ0QsTUFBTSxDQUFDLEdBQUcsRUFBRSxDQUFDO3dCQUNiLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLEVBQUUsaUJBQWlCLENBQUMsQ0FBQztvQkFDdEUsQ0FBQztnQkFDSCxDQUFDLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyx5QkFBeUI7WUFDdEMsQ0FBQztRQUNILENBQUMsRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDLGtCQUFtQixDQUFDLENBQUM7UUFFdEMsbURBQW1EO1FBQ25ELElBQUksY0FBYyxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ3pCLGNBQWMsQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUN6QixDQUFDO1FBRUQsdUJBQXVCO1FBQ3ZCLE1BQU0sQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxXQUFXLENBQUMsVUFBVSxFQUFFLE1BQU0sQ0FBQyxDQUFDLENBQUM7UUFFM0UsdUVBQXVFO1FBQ3ZFLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUMsS0FBYSxFQUFFLEVBQUU7WUFDcEMsc0RBQXNEO1lBQ3RELElBQUksY0FBYyxFQUFFLENBQUM7Z0JBQ25CLFlBQVksQ0FBQyxjQUFjLENBQUMsQ0FBQztnQkFDN0IsY0FBYyxHQUFHLElBQUksQ0FBQztZQUN4QixDQUFDO1lBRUQsbUJBQW1CLEdBQUcsSUFBSSxDQUFDO1lBQzNCLE1BQU0sQ0FBQyxzQkFBc0IsR0FBRyxJQUFJLENBQUM7WUFFckMsd0NBQXdDO1lBQ3hDLE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxTQUFTLENBQUM7WUFDbkMsSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsY0FBYyxDQUFDLEtBQUssQ0FBQyxJQUFJLFNBQVMsS0FBSyxHQUFHLEVBQUUsQ0FBQztnQkFDaEUsT0FBTyxDQUFDLEdBQUcsQ0FDVCxJQUFJLFlBQVksNkNBQTZDO29CQUMzRCw4RUFBOEUsQ0FDakYsQ0FBQztnQkFDRixJQUFJLE1BQU0sQ0FBQyx5QkFBeUIsS0FBSyxJQUFJLEVBQUUsQ0FBQztvQkFDOUMsTUFBTSxDQUFDLHlCQUF5QixHQUFHLGlCQUFpQixDQUFDO29CQUNyRCxJQUFJLENBQUMsaUJBQWlCLENBQUMsd0JBQXdCLENBQUMsVUFBVSxFQUFFLGlCQUFpQixDQUFDLENBQUM7Z0JBQ2pGLENBQUM7Z0JBQ0QsTUFBTSxDQUFDLEdBQUcsRUFBRSxDQUFDO2dCQUNiLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLEVBQUUsaUJBQWlCLENBQUMsQ0FBQztnQkFDcEUsT0FBTztZQUNULENBQUM7WUFFRCwyQ0FBMkM7WUFDM0MsSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO2dCQUMxQyxNQUFNLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQztnQkFFcEIsK0VBQStFO2dCQUMvRSxJQUFJLElBQUksQ0FBQyxVQUFVLENBQUMsYUFBYSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7b0JBQ3pDLDRDQUE0QztvQkFDNUMsTUFBTSxRQUFRLEdBQUc7d0JBQ2YsUUFBUSxFQUFFLE1BQU0sQ0FBQyxRQUFRO3dCQUN6QixVQUFVLEVBQUUsTUFBTSxDQUFDLFVBQVUsSUFBSSxDQUFDO3dCQUNsQyxNQUFNLEVBQUUsTUFBTSxDQUFDLFlBQVksSUFBSSxFQUFFO3dCQUNqQyxRQUFRLEVBQUUsTUFBTSxDQUFDLFNBQVMsSUFBSSxDQUFDO3FCQUNoQyxDQUFDO29CQUVGLHFFQUFxRTtvQkFDckUsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxVQUFVLENBQUMsS0FBSyxFQUFFLFFBQVEsQ0FBQyxDQUFDO29CQUUvRCw2RUFBNkU7b0JBQzdFLHlGQUF5RjtvQkFDekYsSUFBSSxVQUFVLEVBQUUsQ0FBQzt3QkFDZixrREFBa0Q7d0JBQ2xELE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxnQkFBZ0IsQ0FBQyxVQUFVLENBQUMsQ0FBQzt3QkFDM0UsTUFBTSxDQUFDLFlBQVksR0FBRyxZQUFZLENBQUM7d0JBQ25DLE1BQU0sQ0FBQyxZQUFZLEdBQUcsVUFBVSxDQUFDO3dCQUVqQyxzREFBc0Q7d0JBQ3RELElBQUksWUFBWSxJQUFJLElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxxQkFBcUIsQ0FBQyxZQUFZLENBQUMsRUFBRSxDQUFDOzRCQUNqRixNQUFNLGdCQUFnQixHQUFHLElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxtQkFBbUIsQ0FBQyxZQUFZLENBQUMsQ0FBQzs0QkFFcEYsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUFFLENBQUM7Z0NBQ3hDLE9BQU8sQ0FBQyxHQUFHLENBQ1QsSUFBSSxZQUFZLDRDQUE0QyxVQUFVLFlBQVksZ0JBQWdCLEVBQUUsQ0FDckcsQ0FBQzs0QkFDSixDQUFDOzRCQUVELG9EQUFvRDs0QkFDcEQsSUFBSSxDQUFDLGtCQUFrQixDQUFDLHFCQUFxQixDQUMzQyxZQUFZLEVBQ1osTUFBTSxFQUNOLE1BQU0sRUFDTixLQUFLLEVBQ0wsZ0JBQWdCLEVBQ2hCLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsaUJBQWlCLENBQUMsbUJBQW1CLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxDQUN2RSxDQUFDOzRCQUNGLE9BQU87d0JBQ1QsQ0FBQztvQkFDSCxDQUFDO3lCQUFNLElBQ0wsSUFBSSxDQUFDLFFBQVEsQ0FBQyxrQkFBa0IsS0FBSyxLQUFLO3dCQUMxQyxJQUFJLENBQUMsUUFBUSxDQUFDLHFCQUFxQixFQUNuQyxDQUFDO3dCQUNELDRFQUE0RTt3QkFDNUUsT0FBTyxDQUFDLEdBQUcsQ0FDVCxJQUFJLFlBQVksdUVBQXVFLENBQ3hGLENBQUM7b0JBQ0osQ0FBQztnQkFDSCxDQUFDO2dCQUVELG9FQUFvRTtnQkFDcEUsSUFBSSxDQUFDLGtCQUFrQixDQUFDLHFCQUFxQixDQUMzQyxZQUFZLEVBQ1osTUFBTSxFQUNOLE1BQU0sRUFDTixLQUFLLEVBQ0ws