- Renamed port proxy and SNI handler source files to classes.pp.portproxy.js and classes.pp.snihandler.js respectively - Updated import paths in index.ts and test files (e.g. in test.ts and test.router.ts) to reference the new file names - This refactor improves code organization but breaks direct imports from the old paths
147 lines
4.2 KiB
TypeScript
147 lines
4.2 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import type { IPortProxySettings } from './classes.pp.interfaces.js';
|
|
|
|
/**
|
|
* Handles security aspects like IP tracking, rate limiting, and authorization
|
|
*/
|
|
export class SecurityManager {
|
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
|
|
constructor(private settings: IPortProxySettings) {}
|
|
|
|
/**
|
|
* Get connections count by IP
|
|
*/
|
|
public getConnectionCountByIP(ip: string): number {
|
|
return this.connectionsByIP.get(ip)?.size || 0;
|
|
}
|
|
|
|
/**
|
|
* Check and update connection rate for an IP
|
|
* @returns true if within rate limit, false if exceeding limit
|
|
*/
|
|
public checkConnectionRate(ip: string): boolean {
|
|
const now = Date.now();
|
|
const minute = 60 * 1000;
|
|
|
|
if (!this.connectionRateByIP.has(ip)) {
|
|
this.connectionRateByIP.set(ip, [now]);
|
|
return true;
|
|
}
|
|
|
|
// Get timestamps and filter out entries older than 1 minute
|
|
const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
|
|
timestamps.push(now);
|
|
this.connectionRateByIP.set(ip, timestamps);
|
|
|
|
// Check if rate exceeds limit
|
|
return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
|
|
}
|
|
|
|
/**
|
|
* Track connection by IP
|
|
*/
|
|
public trackConnectionByIP(ip: string, connectionId: string): void {
|
|
if (!this.connectionsByIP.has(ip)) {
|
|
this.connectionsByIP.set(ip, new Set());
|
|
}
|
|
this.connectionsByIP.get(ip)!.add(connectionId);
|
|
}
|
|
|
|
/**
|
|
* Remove connection tracking for an IP
|
|
*/
|
|
public removeConnectionByIP(ip: string, connectionId: string): void {
|
|
if (this.connectionsByIP.has(ip)) {
|
|
const connections = this.connectionsByIP.get(ip)!;
|
|
connections.delete(connectionId);
|
|
if (connections.size === 0) {
|
|
this.connectionsByIP.delete(ip);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an IP is allowed using glob patterns
|
|
*/
|
|
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
|
|
// Skip IP validation if allowedIPs is empty
|
|
if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
|
|
return true;
|
|
}
|
|
|
|
// First check if IP is blocked
|
|
if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
|
|
return false;
|
|
}
|
|
|
|
// Then check if IP is allowed
|
|
return this.isGlobIPMatch(ip, allowedIPs);
|
|
}
|
|
|
|
/**
|
|
* Check if the IP matches any of the glob patterns
|
|
*/
|
|
private isGlobIPMatch(ip: string, patterns: string[]): boolean {
|
|
if (!ip || !patterns || patterns.length === 0) return false;
|
|
|
|
const normalizeIP = (ip: string): string[] => {
|
|
if (!ip) return [];
|
|
if (ip.startsWith('::ffff:')) {
|
|
const ipv4 = ip.slice(7);
|
|
return [ip, ipv4];
|
|
}
|
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
|
return [ip, `::ffff:${ip}`];
|
|
}
|
|
return [ip];
|
|
};
|
|
|
|
const normalizedIPVariants = normalizeIP(ip);
|
|
if (normalizedIPVariants.length === 0) return false;
|
|
|
|
const expandedPatterns = patterns.flatMap(normalizeIP);
|
|
return normalizedIPVariants.some((ipVariant) =>
|
|
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if IP should be allowed considering connection rate and max connections
|
|
* @returns Object with result and reason
|
|
*/
|
|
public validateIP(ip: string): { allowed: boolean; reason?: string } {
|
|
// Check connection count limit
|
|
if (
|
|
this.settings.maxConnectionsPerIP &&
|
|
this.getConnectionCountByIP(ip) >= this.settings.maxConnectionsPerIP
|
|
) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
|
|
};
|
|
}
|
|
|
|
// Check connection rate limit
|
|
if (
|
|
this.settings.connectionRateLimitPerMinute &&
|
|
!this.checkConnectionRate(ip)
|
|
) {
|
|
return {
|
|
allowed: false,
|
|
reason: `Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
|
|
};
|
|
}
|
|
|
|
return { allowed: true };
|
|
}
|
|
|
|
/**
|
|
* Clears all IP tracking data (for shutdown)
|
|
*/
|
|
public clearIPTracking(): void {
|
|
this.connectionsByIP.clear();
|
|
this.connectionRateByIP.clear();
|
|
}
|
|
} |