@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.
297 lines • 21.2 kB
JavaScript
import * as plugins from '../../plugins.js';
import { isIPAuthorized, checkMaxConnections, checkConnectionRate, trackConnection, removeConnection, cleanupExpiredRateLimits, parseBasicAuthHeader, normalizeIP } from './security-utils.js';
/**
* Shared SecurityManager for use across proxy components
* Handles IP tracking, rate limiting, and authentication
*/
export class SharedSecurityManager {
/**
* Create a new SharedSecurityManager
*
* @param options - Configuration options
* @param logger - Logger instance
*/
constructor(options, logger) {
this.logger = logger;
// IP connection tracking
this.connectionsByIP = new Map();
// Route-specific rate limiting
this.rateLimits = new Map();
// Cache IP filtering results to avoid constant regex matching
this.ipFilterCache = new Map();
// Cache cleanup interval
this.cleanupInterval = null;
this.maxConnectionsPerIP = options.maxConnectionsPerIP || 100;
this.connectionRateLimitPerMinute = options.connectionRateLimitPerMinute || 300;
// Set up logger with defaults if not provided
this.logger = logger || {
info: console.log,
warn: console.warn,
error: console.error
};
// Set up cache cleanup interval
const cleanupInterval = options.cleanupIntervalMs || 60000; // Default: 1 minute
this.cleanupInterval = setInterval(() => {
this.cleanupCaches();
}, cleanupInterval);
// Don't keep the process alive just for cleanup
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
}
/**
* Get connections count by IP
*
* @param ip - The IP address to check
* @returns Number of connections from this IP
*/
getConnectionCountByIP(ip) {
// Check all normalized variants of the IP
const variants = normalizeIP(ip);
for (const variant of variants) {
const info = this.connectionsByIP.get(variant);
if (info) {
return info.connections.size;
}
}
return 0;
}
/**
* Track connection by IP
*
* @param ip - The IP address to track
* @param connectionId - The connection ID to associate
*/
trackConnectionByIP(ip, connectionId) {
// Check if any variant already exists
const variants = normalizeIP(ip);
let existingKey = null;
for (const variant of variants) {
if (this.connectionsByIP.has(variant)) {
existingKey = variant;
break;
}
}
// Use existing key or the original IP
trackConnection(existingKey || ip, connectionId, this.connectionsByIP);
}
/**
* Remove connection tracking for an IP
*
* @param ip - The IP address to update
* @param connectionId - The connection ID to remove
*/
removeConnectionByIP(ip, connectionId) {
// Check all variants to find where the connection is tracked
const variants = normalizeIP(ip);
for (const variant of variants) {
if (this.connectionsByIP.has(variant)) {
removeConnection(variant, connectionId, this.connectionsByIP);
break;
}
}
}
/**
* Check if IP is authorized based on route security settings
*
* @param ip - The IP address to check
* @param allowedIPs - List of allowed IP patterns
* @param blockedIPs - List of blocked IP patterns
* @returns Whether the IP is authorized
*/
isIPAuthorized(ip, allowedIPs = ['*'], blockedIPs = []) {
return isIPAuthorized(ip, allowedIPs, blockedIPs);
}
/**
* Validate IP against rate limits and connection limits
*
* @param ip - The IP address to validate
* @returns Result with allowed status and reason if blocked
*/
validateIP(ip) {
// Check connection count limit
const connectionResult = checkMaxConnections(ip, this.connectionsByIP, this.maxConnectionsPerIP);
if (!connectionResult.allowed) {
return connectionResult;
}
// Check connection rate limit
const rateResult = checkConnectionRate(ip, this.connectionsByIP, this.connectionRateLimitPerMinute);
if (!rateResult.allowed) {
return rateResult;
}
return { allowed: true };
}
/**
* Check if a client is allowed to access a specific route
*
* @param route - The route to check
* @param context - The request context
* @param routeConnectionCount - Current connection count for this route (optional)
* @returns Whether access is allowed
*/
isAllowed(route, context, routeConnectionCount) {
if (!route.security) {
return true; // No security restrictions
}
// --- IP filtering ---
if (!this.isClientIpAllowed(route, context.clientIp)) {
this.logger?.debug?.(`IP ${context.clientIp} is blocked for route ${route.name || 'unnamed'}`);
return false;
}
// --- Route-level connection limit ---
if (route.security.maxConnections !== undefined && routeConnectionCount !== undefined) {
if (routeConnectionCount >= route.security.maxConnections) {
this.logger?.debug?.(`Route connection limit (${route.security.maxConnections}) exceeded for route ${route.name || 'unnamed'}`);
return false;
}
}
// --- Rate limiting ---
if (route.security.rateLimit?.enabled && !this.isWithinRateLimit(route, context)) {
this.logger?.debug?.(`Rate limit exceeded for route ${route.name || 'unnamed'}`);
return false;
}
return true;
}
/**
* Check if a client IP is allowed for a route
*
* @param route - The route to check
* @param clientIp - The client IP
* @returns Whether the IP is allowed
*/
isClientIpAllowed(route, clientIp) {
if (!route.security) {
return true; // No security restrictions
}
const routeId = route.id || route.name || 'unnamed';
// Check cache first
if (!this.ipFilterCache.has(routeId)) {
this.ipFilterCache.set(routeId, new Map());
}
const routeCache = this.ipFilterCache.get(routeId);
if (routeCache.has(clientIp)) {
return routeCache.get(clientIp);
}
// Check IP against route security settings
const ipAllowList = route.security.ipAllowList;
const ipBlockList = route.security.ipBlockList;
const allowed = this.isIPAuthorized(clientIp, ipAllowList, ipBlockList);
// Cache the result
routeCache.set(clientIp, allowed);
return allowed;
}
/**
* Check if request is within rate limit
*
* @param route - The route to check
* @param context - The request context
* @returns Whether the request is within rate limit
*/
isWithinRateLimit(route, context) {
if (!route.security?.rateLimit?.enabled) {
return true;
}
const rateLimit = route.security.rateLimit;
const routeId = route.id || route.name || 'unnamed';
// Determine rate limit key (by IP, path, or header)
let key = context.clientIp; // Default to IP
if (rateLimit.keyBy === 'path' && context.path) {
key = `${context.clientIp}:${context.path}`;
}
else if (rateLimit.keyBy === 'header' && rateLimit.headerName && context.headers) {
const headerValue = context.headers[rateLimit.headerName.toLowerCase()];
if (headerValue) {
key = `${context.clientIp}:${headerValue}`;
}
}
// Get or create rate limit tracking for this route
if (!this.rateLimits.has(routeId)) {
this.rateLimits.set(routeId, new Map());
}
const routeLimits = this.rateLimits.get(routeId);
const now = Date.now();
// Get or create rate limit tracking for this key
let limit = routeLimits.get(key);
if (!limit || limit.expiry < now) {
// Create new rate limit or reset expired one
limit = {
count: 1,
expiry: now + (rateLimit.window * 1000)
};
routeLimits.set(key, limit);
return true;
}
// Increment the counter
limit.count++;
// Check if rate limit is exceeded
return limit.count <= rateLimit.maxRequests;
}
/**
* Validate HTTP Basic Authentication
*
* @param route - The route to check
* @param authHeader - The Authorization header
* @returns Whether authentication is valid
*/
validateBasicAuth(route, authHeader) {
// Skip if basic auth not enabled for route
if (!route.security?.basicAuth?.enabled) {
return true;
}
// No auth header means auth failed
if (!authHeader) {
return false;
}
// Parse auth header
const credentials = parseBasicAuthHeader(authHeader);
if (!credentials) {
return false;
}
// Check credentials against configured users
const { username, password } = credentials;
const users = route.security.basicAuth.users;
return users.some(user => user.username === username && user.password === password);
}
/**
* Clean up caches to prevent memory leaks
*/
cleanupCaches() {
// Clean up rate limits
cleanupExpiredRateLimits(this.rateLimits, this.logger);
// Clean up IP connection tracking
let cleanedIPs = 0;
for (const [ip, info] of this.connectionsByIP.entries()) {
// Remove IPs with no active connections and no recent timestamps
if (info.connections.size === 0 && info.timestamps.length === 0) {
this.connectionsByIP.delete(ip);
cleanedIPs++;
}
}
if (cleanedIPs > 0 && this.logger?.debug) {
this.logger.debug(`Cleaned up ${cleanedIPs} IPs with no active connections`);
}
// IP filter cache doesn't need cleanup (tied to routes)
}
/**
* Clear all IP tracking data (for shutdown)
*/
clearIPTracking() {
this.connectionsByIP.clear();
this.rateLimits.clear();
this.ipFilterCache.clear();
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
/**
* Update routes for security checking
*
* @param routes - New routes to use
*/
setRoutes(routes) {
// Only clear the IP filter cache - route-specific
this.ipFilterCache.clear();
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2hhcmVkLXNlY3VyaXR5LW1hbmFnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9jb3JlL3V0aWxzL3NoYXJlZC1zZWN1cml0eS1tYW5hZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFRNUMsT0FBTyxFQUNMLGNBQWMsRUFDZCxtQkFBbUIsRUFDbkIsbUJBQW1CLEVBQ25CLGVBQWUsRUFDZixnQkFBZ0IsRUFDaEIsd0JBQXdCLEVBQ3hCLG9CQUFvQixFQUNwQixXQUFXLEVBQ1osTUFBTSxxQkFBcUIsQ0FBQztBQUU3Qjs7O0dBR0c7QUFDSCxNQUFNLE9BQU8scUJBQXFCO0lBaUJoQzs7Ozs7T0FLRztJQUNILFlBQVksT0FLWCxFQUFVLE1BQXdCO1FBQXhCLFdBQU0sR0FBTixNQUFNLENBQWtCO1FBM0JuQyx5QkFBeUI7UUFDakIsb0JBQWUsR0FBbUMsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUVwRSwrQkFBK0I7UUFDdkIsZUFBVSxHQUE2QyxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBRXpFLDhEQUE4RDtRQUN0RCxrQkFBYSxHQUFzQyxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBTXJFLHlCQUF5QjtRQUNqQixvQkFBZSxHQUEwQixJQUFJLENBQUM7UUFjcEQsSUFBSSxDQUFDLG1CQUFtQixHQUFHLE9BQU8sQ0FBQyxtQkFBbUIsSUFBSSxHQUFHLENBQUM7UUFDOUQsSUFBSSxDQUFDLDRCQUE0QixHQUFHLE9BQU8sQ0FBQyw0QkFBNEIsSUFBSSxHQUFHLENBQUM7UUFFaEYsOENBQThDO1FBQzlDLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxJQUFJO1lBQ3RCLElBQUksRUFBRSxPQUFPLENBQUMsR0FBRztZQUNqQixJQUFJLEVBQUUsT0FBTyxDQUFDLElBQUk7WUFDbEIsS0FBSyxFQUFFLE9BQU8sQ0FBQyxLQUFLO1NBQ3JCLENBQUM7UUFFRixnQ0FBZ0M7UUFDaEMsTUFBTSxlQUFlLEdBQUcsT0FBTyxDQUFDLGlCQUFpQixJQUFJLEtBQUssQ0FBQyxDQUFDLG9CQUFvQjtRQUNoRixJQUFJLENBQUMsZUFBZSxHQUFHLFdBQVcsQ0FBQyxHQUFHLEVBQUU7WUFDdEMsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDO1FBQ3ZCLENBQUMsRUFBRSxlQUFlLENBQUMsQ0FBQztRQUVwQixnREFBZ0Q7UUFDaEQsSUFBSSxJQUFJLENBQUMsZUFBZSxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQy9CLElBQUksQ0FBQyxlQUFlLENBQUMsS0FBSyxFQUFFLENBQUM7UUFDL0IsQ0FBQztJQUNILENBQUM7SUFFRDs7Ozs7T0FLRztJQUNJLHNCQUFzQixDQUFDLEVBQVU7UUFDdEMsMENBQTBDO1FBQzFDLE1BQU0sUUFBUSxHQUFHLFdBQVcsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNqQyxLQUFLLE1BQU0sT0FBTyxJQUFJLFFBQVEsRUFBRSxDQUFDO1lBQy9CLE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxlQUFlLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBQy9DLElBQUksSUFBSSxFQUFFLENBQUM7Z0JBQ1QsT0FBTyxJQUFJLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQztZQUMvQixDQUFDO1FBQ0gsQ0FBQztRQUNELE9BQU8sQ0FBQyxDQUFDO0lBQ1gsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ0ksbUJBQW1CLENBQUMsRUFBVSxFQUFFLFlBQW9CO1FBQ3pELHNDQUFzQztRQUN0QyxNQUFNLFFBQVEsR0FBRyxXQUFXLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDakMsSUFBSSxXQUFXLEdBQWtCLElBQUksQ0FBQztRQUV0QyxLQUFLLE1BQU0sT0FBTyxJQUFJLFFBQVEsRUFBRSxDQUFDO1lBQy9CLElBQUksSUFBSSxDQUFDLGVBQWUsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztnQkFDdEMsV0FBVyxHQUFHLE9BQU8sQ0FBQztnQkFDdEIsTUFBTTtZQUNSLENBQUM7UUFDSCxDQUFDO1FBRUQsc0NBQXNDO1FBQ3RDLGVBQWUsQ0FBQyxXQUFXLElBQUksRUFBRSxFQUFFLFlBQVksRUFBRSxJQUFJLENBQUMsZUFBZSxDQUFDLENBQUM7SUFDekUsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ0ksb0JBQW9CLENBQUMsRUFBVSxFQUFFLFlBQW9CO1FBQzFELDZEQUE2RDtRQUM3RCxNQUFNLFFBQVEsR0FBRyxXQUFXLENBQUMsRUFBRSxDQUFDLENBQUM7UUFFakMsS0FBSyxNQUFNLE9BQU8sSUFBSSxRQUFRLEVBQUUsQ0FBQztZQUMvQixJQUFJLElBQUksQ0FBQyxlQUFlLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7Z0JBQ3RDLGdCQUFnQixDQUFDLE9BQU8sRUFBRSxZQUFZLEVBQUUsSUFBSSxDQUFDLGVBQWUsQ0FBQyxDQUFDO2dCQUM5RCxNQUFNO1lBQ1IsQ0FBQztRQUNILENBQUM7SUFDSCxDQUFDO0lBRUQ7Ozs7Ozs7T0FPRztJQUNJLGNBQWMsQ0FDbkIsRUFBVSxFQUNWLGFBQXVCLENBQUMsR0FBRyxDQUFDLEVBQzVCLGFBQXVCLEVBQUU7UUFFekIsT0FBTyxjQUFjLENBQUMsRUFBRSxFQUFFLFVBQVUsRUFBRSxVQUFVLENBQUMsQ0FBQztJQUNwRCxDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSSxVQUFVLENBQUMsRUFBVTtRQUMxQiwrQkFBK0I7UUFDL0IsTUFBTSxnQkFBZ0IsR0FBRyxtQkFBbUIsQ0FDMUMsRUFBRSxFQUNGLElBQUksQ0FBQyxlQUFlLEVBQ3BCLElBQUksQ0FBQyxtQkFBbUIsQ0FDekIsQ0FBQztRQUNGLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUM5QixPQUFPLGdCQUFnQixDQUFDO1FBQzFCLENBQUM7UUFFRCw4QkFBOEI7UUFDOUIsTUFBTSxVQUFVLEdBQUcsbUJBQW1CLENBQ3BDLEVBQUUsRUFDRixJQUFJLENBQUMsZUFBZSxFQUNwQixJQUFJLENBQUMsNEJBQTRCLENBQ2xDLENBQUM7UUFDRixJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ3hCLE9BQU8sVUFBVSxDQUFDO1FBQ3BCLENBQUM7UUFFRCxPQUFPLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxDQUFDO0lBQzNCLENBQUM7SUFFRDs7Ozs7OztPQU9HO0lBQ0ksU0FBUyxDQUFDLEtBQW1CLEVBQUUsT0FBc0IsRUFBRSxvQkFBNkI7UUFDekYsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNwQixPQUFPLElBQUksQ0FBQyxDQUFDLDJCQUEyQjtRQUMxQyxDQUFDO1FBRUQsdUJBQXVCO1FBQ3ZCLElBQUksQ0FBQyxJQUFJLENBQUMsaUJBQWlCLENBQUMsS0FBSyxFQUFFLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1lBQ3JELElBQUksQ0FBQyxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUMsTUFBTSxPQUFPLENBQUMsUUFBUSx5QkFBeUIsS0FBSyxDQUFDLElBQUksSUFBSSxTQUFTLEVBQUUsQ0FBQyxDQUFDO1lBQy9GLE9BQU8sS0FBSyxDQUFDO1FBQ2YsQ0FBQztRQUVELHVDQUF1QztRQUN2QyxJQUFJLEtBQUssQ0FBQyxRQUFRLENBQUMsY0FBYyxLQUFLLFNBQVMsSUFBSSxvQkFBb0IsS0FBSyxTQUFTLEVBQUUsQ0FBQztZQUN0RixJQUFJLG9CQUFvQixJQUFJLEtBQUssQ0FBQyxRQUFRLENBQUMsY0FBYyxFQUFFLENBQUM7Z0JBQzFELElBQUksQ0FBQyxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUMsMkJBQTJCLEtBQUssQ0FBQyxRQUFRLENBQUMsY0FBYyx3QkFBd0IsS0FBSyxDQUFDLElBQUksSUFBSSxTQUFTLEVBQUUsQ0FBQyxDQUFDO2dCQUNoSSxPQUFPLEtBQUssQ0FBQztZQUNmLENBQUM7UUFDSCxDQUFDO1FBRUQsd0JBQXdCO1FBQ3hCLElBQUksS0FBSyxDQUFDLFFBQVEsQ0FBQyxTQUFTLEVBQUUsT0FBTyxJQUFJLENBQUMsSUFBSSxDQUFDLGlCQUFpQixDQUFDLEtBQUssRUFBRSxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ2pGLElBQUksQ0FBQyxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUMsaUNBQWlDLEtBQUssQ0FBQyxJQUFJLElBQUksU0FBUyxFQUFFLENBQUMsQ0FBQztZQUNqRixPQUFPLEtBQUssQ0FBQztRQUNmLENBQUM7UUFFRCxPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7SUFFRDs7Ozs7O09BTUc7SUFDSyxpQkFBaUIsQ0FBQyxLQUFtQixFQUFFLFFBQWdCO1FBQzdELElBQUksQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDcEIsT0FBTyxJQUFJLENBQUMsQ0FBQywyQkFBMkI7UUFDMUMsQ0FBQztRQUVELE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxFQUFFLElBQUksS0FBSyxDQUFDLElBQUksSUFBSSxTQUFTLENBQUM7UUFFcEQsb0JBQW9CO1FBQ3BCLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ3JDLElBQUksQ0FBQyxhQUFhLENBQUMsR0FBRyxDQUFDLE9BQU8sRUFBRSxJQUFJLEdBQUcsRUFBRSxDQUFDLENBQUM7UUFDN0MsQ0FBQztRQUVELE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxhQUFhLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBRSxDQUFDO1FBQ3BELElBQUksVUFBVSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1lBQzdCLE9BQU8sVUFBVSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUUsQ0FBQztRQUNuQyxDQUFDO1FBRUQsMkNBQTJDO1FBQzNDLE1BQU0sV0FBVyxHQUFHLEtBQUssQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDO1FBQy9DLE1BQU0sV0FBVyxHQUFHLEtBQUssQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDO1FBRS9DLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxjQUFjLENBQUMsUUFBUSxFQUFFLFdBQVcsRUFBRSxXQUFXLENBQUMsQ0FBQztRQUV4RSxtQkFBbUI7UUFDbkIsVUFBVSxDQUFDLEdBQUcsQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFFbEMsT0FBTyxPQUFPLENBQUM7SUFDakIsQ0FBQztJQUVEOzs7Ozs7T0FNRztJQUNLLGlCQUFpQixDQUFDLEtBQW1CLEVBQUUsT0FBc0I7UUFDbkUsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxDQUFDO1lBQ3hDLE9BQU8sSUFBSSxDQUFDO1FBQ2QsQ0FBQztRQUVELE1BQU0sU0FBUyxHQUFHLEtBQUssQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDO1FBQzNDLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxFQUFFLElBQUksS0FBSyxDQUFDLElBQUksSUFBSSxTQUFTLENBQUM7UUFFcEQsb0RBQW9EO1FBQ3BELElBQUksR0FBRyxHQUFHLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQyxnQkFBZ0I7UUFFNUMsSUFBSSxTQUFTLENBQUMsS0FBSyxLQUFLLE1BQU0sSUFBSSxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUM7WUFDL0MsR0FBRyxHQUFHLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxPQUFPLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDOUMsQ0FBQzthQUFNLElBQUksU0FBUyxDQUFDLEtBQUssS0FBSyxRQUFRLElBQUksU0FBUyxDQUFDLFVBQVUsSUFBSSxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDbkYsTUFBTSxXQUFXLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUM7WUFDeEUsSUFBSSxXQUFXLEVBQUUsQ0FBQztnQkFDaEIsR0FBRyxHQUFHLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxXQUFXLEVBQUUsQ0FBQztZQUM3QyxDQUFDO1FBQ0gsQ0FBQztRQUVELG1EQUFtRDtRQUNuRCxJQUFJLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNsQyxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsSUFBSSxHQUFHLEVBQUUsQ0FBQyxDQUFDO1FBQzFDLENBQUM7UUFFRCxNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUUsQ0FBQztRQUNsRCxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7UUFFdkIsaURBQWlEO1FBQ2pELElBQUksS0FBSyxHQUFHLFdBQVcsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDakMsSUFBSSxDQUFDLEtBQUssSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLEdBQUcsRUFBRSxDQUFDO1lBQ2pDLDZDQUE2QztZQUM3QyxLQUFLLEdBQUc7Z0JBQ04sS0FBSyxFQUFFLENBQUM7Z0JBQ1IsTUFBTSxFQUFFLEdBQUcsR0FBRyxDQUFDLFNBQVMsQ0FBQyxNQUFNLEdBQUcsSUFBSSxDQUFDO2FBQ3hDLENBQUM7WUFDRixXQUFXLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxLQUFLLENBQUMsQ0FBQztZQUM1QixPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFFRCx3QkFBd0I7UUFDeEIsS0FBSyxDQUFDLEtBQUssRUFBRSxDQUFDO1FBRWQsa0NBQWtDO1FBQ2xDLE9BQU8sS0FBSyxDQUFDLEtBQUssSUFBSSxTQUFTLENBQUMsV0FBVyxDQUFDO0lBQzlDLENBQUM7SUFFRDs7Ozs7O09BTUc7SUFDSSxpQkFBaUIsQ0FBQyxLQUFtQixFQUFFLFVBQW1CO1FBQy9ELDJDQUEyQztRQUMzQyxJQUFJLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLENBQUM7WUFDeEMsT0FBTyxJQUFJLENBQUM7UUFDZCxDQUFDO1FBRUQsbUNBQW1DO1FBQ25DLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztZQUNoQixPQUFPLEtBQUssQ0FBQztRQUNmLENBQUM7UUFFRCxvQkFBb0I7UUFDcEIsTUFBTSxXQUFXLEdBQUcsb0JBQW9CLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDckQsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1lBQ2pCLE9BQU8sS0FBSyxDQUFDO1FBQ2YsQ0FBQztRQUVELDZDQUE2QztRQUM3QyxNQUFNLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxHQUFHLFdBQVcsQ0FBQztRQUMzQyxNQUFNLEtBQUssR0FBRyxLQUFLLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUM7UUFFN0MsT0FBTyxLQUFLLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQ3ZCLElBQUksQ0FBQyxRQUFRLEtBQUssUUFBUSxJQUFJLElBQUksQ0FBQyxRQUFRLEtBQUssUUFBUSxDQUN6RCxDQUFDO0lBQ0osQ0FBQztJQUVEOztPQUVHO0lBQ0ssYUFBYTtRQUNuQix1QkFBdUI7UUFDdkIsd0JBQXdCLENBQUMsSUFBSSxDQUFDLFVBQVUsRUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7UUFFdkQsa0NBQWtDO1FBQ2xDLElBQUksVUFBVSxHQUFHLENBQUMsQ0FBQztRQUNuQixLQUFLLE1BQU0sQ0FBQyxFQUFFLEVBQUUsSUFBSSxDQUFDLElBQUksSUFBSSxDQUFDLGVBQWUsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDO1lBQ3hELGlFQUFpRTtZQUNqRSxJQUFJLElBQUksQ0FBQyxXQUFXLENBQUMsSUFBSSxLQUFLLENBQUMsSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztnQkFDaEUsSUFBSSxDQUFDLGVBQWUsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUM7Z0JBQ2hDLFVBQVUsRUFBRSxDQUFDO1lBQ2YsQ0FBQztRQUNILENBQUM7UUFFRCxJQUFJLFVBQVUsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsQ0FBQztZQUN6QyxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxjQUFjLFVBQVUsaUNBQWlDLENBQUMsQ0FBQztRQUMvRSxDQUFDO1FBRUQsd0RBQXdEO0lBQzFELENBQUM7SUFFRDs7T0FFRztJQUNJLGVBQWU7UUFDcEIsSUFBSSxDQUFDLGVBQWUsQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUM3QixJQUFJLENBQUMsVUFBVSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ3hCLElBQUksQ0FBQyxhQUFhLENBQUMsS0FBSyxFQUFFLENBQUM7UUFFM0IsSUFBSSxJQUFJLENBQUMsZUFBZSxFQUFFLENBQUM7WUFDekIsYUFBYSxDQUFDLElBQUksQ0FBQyxlQUFlLENBQUMsQ0FBQztZQUNwQyxJQUFJLENBQUMsZUFBZSxHQUFHLElBQUksQ0FBQztRQUM5QixDQUFDO0lBQ0gsQ0FBQztJQUVEOzs7O09BSUc7SUFDSSxTQUFTLENBQUMsTUFBc0I7UUFDckMsa0RBQWtEO1FBQ2xELElBQUksQ0FBQyxhQUFhLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDN0IsQ0FBQztDQUNGIn0=