Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad44274075 | |||
| 3efd9c72ba | |||
| b96e0cd48e | |||
| c909d3db3e | |||
| c09e2cef9e | |||
| 8544ad8322 |
26
changelog.md
26
changelog.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-19 - 21.1.7 - fix(route-validator)
|
||||
Relax domain validation to accept 'localhost', prefix wildcards (e.g. *example.com) and IP literals; add comprehensive domain validation tests
|
||||
|
||||
- Allow 'localhost' as a valid domain pattern in route validation
|
||||
- Support prefix wildcard patterns like '*example.com' in addition to '*.example.com'
|
||||
- Accept IPv4 and IPv6 literal addresses in domain validation
|
||||
- Add test coverage: new test/test.domain-validation.ts with many real-world and edge-case patterns
|
||||
|
||||
## 2025-08-19 - 21.1.6 - fix(ip-utils)
|
||||
Fix IP wildcard/shorthand handling and add validation test
|
||||
|
||||
- Support shorthand IPv4 wildcard patterns (e.g. '10.*', '192.168.*') by expanding them to full 4-octet patterns before matching
|
||||
- Normalize and expand patterns in IpUtils.isGlobIPMatch and SharedSecurityManager IP checks to ensure consistent minimatch comparisons
|
||||
- Relax route validator wildcard checks to accept 1-4 octet wildcard specifications for IPv4 patterns
|
||||
- Add test harness test-ip-validation.ts to exercise common wildcard/shorthand IP patterns
|
||||
|
||||
## 2025-08-19 - 21.1.5 - fix(core)
|
||||
Prepare patch release: documentation, tests and stability fixes (metrics, ACME, connection cleanup)
|
||||
|
||||
- Byte counting and throughput: per-route and per-IP throughput trackers with per-second sampling; removed double-counting and improved sampling buffers for accurate rates
|
||||
- HttpProxy and forwarding: Ensure metricsCollector.recordBytes() is called in forwarding paths so throughput is recorded reliably
|
||||
- ACME / Certificate Manager: support for custom certProvisionFunction with configurable fallback to ACME (http01) and improved challenge route lifecycle
|
||||
- Connection lifecycle and cleanup: improved lifecycle component timer/listener cleanup, better cleanup queue batching and zombie/half-zombie detection
|
||||
- Various utilities and stability improvements: enhanced IP utils, path/domain matching improvements, safer socket handling and more robust fragment/ClientHello handling
|
||||
- Tests and docs: many test files and readme.hints.md updated with byte-counting audit, connection cleanup and ACME guidance
|
||||
|
||||
## 2025-08-14 - 21.1.4 - fix(security)
|
||||
Critical security and stability fixes
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "21.1.4",
|
||||
"version": "21.1.7",
|
||||
"private": false,
|
||||
"description": "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.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
||||
189
test/test.domain-validation.ts
Normal file
189
test/test.domain-validation.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { RouteValidator } from '../ts/proxies/smart-proxy/utils/route-validator.js';
|
||||
|
||||
tap.test('Domain Validation - Standard wildcard patterns', async () => {
|
||||
const testPatterns = [
|
||||
{ pattern: '*.example.com', shouldPass: true, description: 'Standard wildcard subdomain' },
|
||||
{ pattern: '*.sub.example.com', shouldPass: true, description: 'Nested wildcard subdomain' },
|
||||
{ pattern: 'example.com', shouldPass: true, description: 'Plain domain' },
|
||||
{ pattern: 'sub.example.com', shouldPass: true, description: 'Subdomain' },
|
||||
{ pattern: '*', shouldPass: true, description: 'Catch-all wildcard' },
|
||||
{ pattern: 'localhost', shouldPass: true, description: 'Localhost' },
|
||||
{ pattern: '192.168.1.1', shouldPass: true, description: 'IPv4 address' },
|
||||
];
|
||||
|
||||
for (const { pattern, shouldPass, description } of testPatterns) {
|
||||
const route = {
|
||||
name: 'test',
|
||||
match: {
|
||||
ports: 443,
|
||||
domains: pattern
|
||||
},
|
||||
action: {
|
||||
type: 'forward' as const,
|
||||
targets: [{ host: 'localhost', port: 8080 }]
|
||||
}
|
||||
};
|
||||
|
||||
const result = RouteValidator.validateRoute(route);
|
||||
|
||||
if (shouldPass) {
|
||||
expect(result.valid).toEqual(true);
|
||||
console.log(`✅ Domain '${pattern}' correctly accepted (${description})`);
|
||||
} else {
|
||||
expect(result.valid).toEqual(false);
|
||||
console.log(`✅ Domain '${pattern}' correctly rejected (${description})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Domain Validation - Prefix wildcard patterns (*domain)', async () => {
|
||||
const testPatterns = [
|
||||
{ pattern: '*nevermind.cloud', shouldPass: true, description: 'Prefix wildcard without dot' },
|
||||
{ pattern: '*example.com', shouldPass: true, description: 'Prefix wildcard for TLD' },
|
||||
{ pattern: '*sub.example.com', shouldPass: true, description: 'Prefix wildcard for subdomain' },
|
||||
{ pattern: '*api.service.io', shouldPass: true, description: 'Prefix wildcard for nested domain' },
|
||||
];
|
||||
|
||||
for (const { pattern, shouldPass, description } of testPatterns) {
|
||||
const route = {
|
||||
name: 'test',
|
||||
match: {
|
||||
ports: 443,
|
||||
domains: pattern
|
||||
},
|
||||
action: {
|
||||
type: 'forward' as const,
|
||||
targets: [{ host: 'localhost', port: 8080 }]
|
||||
}
|
||||
};
|
||||
|
||||
const result = RouteValidator.validateRoute(route);
|
||||
|
||||
if (shouldPass) {
|
||||
expect(result.valid).toEqual(true);
|
||||
console.log(`✅ Domain '${pattern}' correctly accepted (${description})`);
|
||||
} else {
|
||||
expect(result.valid).toEqual(false);
|
||||
console.log(`✅ Domain '${pattern}' correctly rejected (${description})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Domain Validation - Invalid patterns', async () => {
|
||||
const invalidPatterns = [
|
||||
// Note: Empty string validation is handled differently in the validator
|
||||
// { pattern: '', description: 'Empty string' },
|
||||
{ pattern: '*.', description: 'Wildcard with trailing dot' },
|
||||
{ pattern: '.example.com', description: 'Leading dot' },
|
||||
{ pattern: 'example..com', description: 'Double dots' },
|
||||
{ pattern: 'exam ple.com', description: 'Space in domain' },
|
||||
{ pattern: 'example-.com', description: 'Hyphen at end of label' },
|
||||
{ pattern: '-example.com', description: 'Hyphen at start of label' },
|
||||
];
|
||||
|
||||
for (const { pattern, description } of invalidPatterns) {
|
||||
const route = {
|
||||
name: 'test',
|
||||
match: {
|
||||
ports: 443,
|
||||
domains: pattern
|
||||
},
|
||||
action: {
|
||||
type: 'forward' as const,
|
||||
targets: [{ host: 'localhost', port: 8080 }]
|
||||
}
|
||||
};
|
||||
|
||||
const result = RouteValidator.validateRoute(route);
|
||||
if (result.valid === false) {
|
||||
console.log(`✅ Domain '${pattern}' correctly rejected (${description})`);
|
||||
} else {
|
||||
console.log(`❌ Domain '${pattern}' was unexpectedly accepted! (${description})`);
|
||||
console.log(` Errors: ${result.errors.join(', ')}`);
|
||||
}
|
||||
expect(result.valid).toEqual(false);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Domain Validation - Multiple domains in array', async () => {
|
||||
const route = {
|
||||
name: 'test',
|
||||
match: {
|
||||
ports: 443,
|
||||
domains: [
|
||||
'*.example.com',
|
||||
'*nevermind.cloud',
|
||||
'api.service.io',
|
||||
'localhost'
|
||||
]
|
||||
},
|
||||
action: {
|
||||
type: 'forward' as const,
|
||||
targets: [{ host: 'localhost', port: 8080 }]
|
||||
}
|
||||
};
|
||||
|
||||
const result = RouteValidator.validateRoute(route);
|
||||
expect(result.valid).toEqual(true);
|
||||
console.log('✅ Multiple valid domains in array correctly accepted');
|
||||
});
|
||||
|
||||
tap.test('Domain Validation - Mixed valid and invalid domains', async () => {
|
||||
const route = {
|
||||
name: 'test',
|
||||
match: {
|
||||
ports: 443,
|
||||
domains: [
|
||||
'*.example.com', // valid
|
||||
'', // invalid - empty
|
||||
'localhost' // valid
|
||||
]
|
||||
},
|
||||
action: {
|
||||
type: 'forward' as const,
|
||||
targets: [{ host: 'localhost', port: 8080 }]
|
||||
}
|
||||
};
|
||||
|
||||
const result = RouteValidator.validateRoute(route);
|
||||
expect(result.valid).toEqual(false);
|
||||
expect(result.errors.some(e => e.includes('Invalid domain pattern'))).toEqual(true);
|
||||
console.log('✅ Mixed valid/invalid domains correctly rejected');
|
||||
});
|
||||
|
||||
tap.test('Domain Validation - Real-world patterns from email routes', async () => {
|
||||
// These are the patterns that were failing from the email conversion
|
||||
const realWorldPatterns = [
|
||||
{ pattern: '*nevermind.cloud', shouldPass: true, description: 'nevermind.cloud wildcard' },
|
||||
{ pattern: '*push.email', shouldPass: true, description: 'push.email wildcard' },
|
||||
{ pattern: '*.bleu.de', shouldPass: true, description: 'bleu.de subdomain wildcard' },
|
||||
{ pattern: '*bleu.de', shouldPass: true, description: 'bleu.de prefix wildcard' },
|
||||
];
|
||||
|
||||
for (const { pattern, shouldPass, description } of realWorldPatterns) {
|
||||
const route = {
|
||||
name: 'email-route',
|
||||
match: {
|
||||
ports: 443,
|
||||
domains: pattern
|
||||
},
|
||||
action: {
|
||||
type: 'forward' as const,
|
||||
targets: [{ host: 'mail.server.com', port: 8080 }]
|
||||
}
|
||||
};
|
||||
|
||||
const result = RouteValidator.validateRoute(route);
|
||||
|
||||
if (shouldPass) {
|
||||
expect(result.valid).toEqual(true);
|
||||
console.log(`✅ Real-world domain '${pattern}' correctly accepted (${description})`);
|
||||
} else {
|
||||
expect(result.valid).toEqual(false);
|
||||
console.log(`✅ Real-world domain '${pattern}' correctly rejected (${description})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
128
test/test.ip-validation.ts
Normal file
128
test/test.ip-validation.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartproxy from '../ts/index.js';
|
||||
import { RouteValidator } from '../ts/proxies/smart-proxy/utils/route-validator.js';
|
||||
import { IpUtils } from '../ts/core/utils/ip-utils.js';
|
||||
|
||||
tap.test('IP Validation - Shorthand patterns', async () => {
|
||||
|
||||
// Test shorthand patterns are now accepted
|
||||
const testPatterns = [
|
||||
{ pattern: '192.168.*', shouldPass: true },
|
||||
{ pattern: '192.168.*.*', shouldPass: true },
|
||||
{ pattern: '10.*', shouldPass: true },
|
||||
{ pattern: '10.*.*.*', shouldPass: true },
|
||||
{ pattern: '172.16.*', shouldPass: true },
|
||||
{ pattern: '10.0.0.0/8', shouldPass: true },
|
||||
{ pattern: '192.168.0.0/16', shouldPass: true },
|
||||
{ pattern: '192.168.1.100', shouldPass: true },
|
||||
{ pattern: '*', shouldPass: true },
|
||||
{ pattern: '192.168.1.1-192.168.1.100', shouldPass: true },
|
||||
];
|
||||
|
||||
for (const { pattern, shouldPass } of testPatterns) {
|
||||
const route = {
|
||||
name: 'test',
|
||||
match: { ports: 80 },
|
||||
action: { type: 'forward' as const, targets: [{ host: 'localhost', port: 8080 }] },
|
||||
security: { ipAllowList: [pattern] }
|
||||
};
|
||||
|
||||
const result = RouteValidator.validateRoute(route);
|
||||
|
||||
if (shouldPass) {
|
||||
expect(result.valid).toEqual(true);
|
||||
console.log(`✅ Pattern '${pattern}' correctly accepted`);
|
||||
} else {
|
||||
expect(result.valid).toEqual(false);
|
||||
console.log(`✅ Pattern '${pattern}' correctly rejected`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('IP Matching - Runtime shorthand pattern matching', async () => {
|
||||
|
||||
// Test runtime matching with shorthand patterns
|
||||
const testCases = [
|
||||
{ ip: '192.168.1.100', patterns: ['192.168.*'], expected: true },
|
||||
{ ip: '192.168.1.100', patterns: ['192.168.1.*'], expected: true },
|
||||
{ ip: '192.168.1.100', patterns: ['192.168.2.*'], expected: false },
|
||||
{ ip: '10.0.0.1', patterns: ['10.*'], expected: true },
|
||||
{ ip: '10.1.2.3', patterns: ['10.*'], expected: true },
|
||||
{ ip: '172.16.0.1', patterns: ['10.*'], expected: false },
|
||||
{ ip: '192.168.1.1', patterns: ['192.168.*.*'], expected: true },
|
||||
];
|
||||
|
||||
for (const { ip, patterns, expected } of testCases) {
|
||||
const result = IpUtils.isGlobIPMatch(ip, patterns);
|
||||
expect(result).toEqual(expected);
|
||||
console.log(`✅ IP ${ip} with pattern ${patterns[0]} = ${result} (expected ${expected})`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('IP Matching - CIDR notation', async () => {
|
||||
|
||||
// Test CIDR notation matching
|
||||
const cidrTests = [
|
||||
{ ip: '10.0.0.1', cidr: '10.0.0.0/8', expected: true },
|
||||
{ ip: '10.255.255.255', cidr: '10.0.0.0/8', expected: true },
|
||||
{ ip: '11.0.0.1', cidr: '10.0.0.0/8', expected: false },
|
||||
{ ip: '192.168.1.1', cidr: '192.168.0.0/16', expected: true },
|
||||
{ ip: '192.168.255.255', cidr: '192.168.0.0/16', expected: true },
|
||||
{ ip: '192.169.0.1', cidr: '192.168.0.0/16', expected: false },
|
||||
{ ip: '192.168.1.100', cidr: '192.168.1.0/24', expected: true },
|
||||
{ ip: '192.168.2.100', cidr: '192.168.1.0/24', expected: false },
|
||||
];
|
||||
|
||||
for (const { ip, cidr, expected } of cidrTests) {
|
||||
const result = IpUtils.isGlobIPMatch(ip, [cidr]);
|
||||
expect(result).toEqual(expected);
|
||||
console.log(`✅ IP ${ip} in CIDR ${cidr} = ${result} (expected ${expected})`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('IP Matching - Range notation', async () => {
|
||||
|
||||
// Test range notation matching
|
||||
const rangeTests = [
|
||||
{ ip: '192.168.1.1', range: '192.168.1.1-192.168.1.100', expected: true },
|
||||
{ ip: '192.168.1.50', range: '192.168.1.1-192.168.1.100', expected: true },
|
||||
{ ip: '192.168.1.100', range: '192.168.1.1-192.168.1.100', expected: true },
|
||||
{ ip: '192.168.1.101', range: '192.168.1.1-192.168.1.100', expected: false },
|
||||
{ ip: '192.168.2.50', range: '192.168.1.1-192.168.1.100', expected: false },
|
||||
];
|
||||
|
||||
for (const { ip, range, expected } of rangeTests) {
|
||||
const result = IpUtils.isGlobIPMatch(ip, [range]);
|
||||
expect(result).toEqual(expected);
|
||||
console.log(`✅ IP ${ip} in range ${range} = ${result} (expected ${expected})`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('IP Matching - Mixed patterns', async () => {
|
||||
|
||||
// Test with mixed pattern types
|
||||
const allowList = [
|
||||
'10.0.0.0/8', // CIDR
|
||||
'192.168.*', // Shorthand glob
|
||||
'172.16.1.*', // Specific subnet glob
|
||||
'8.8.8.8', // Single IP
|
||||
'1.1.1.1-1.1.1.10' // Range
|
||||
];
|
||||
|
||||
const tests = [
|
||||
{ ip: '10.1.2.3', expected: true }, // Matches CIDR
|
||||
{ ip: '192.168.100.1', expected: true }, // Matches shorthand glob
|
||||
{ ip: '172.16.1.5', expected: true }, // Matches specific glob
|
||||
{ ip: '8.8.8.8', expected: true }, // Matches single IP
|
||||
{ ip: '1.1.1.5', expected: true }, // Matches range
|
||||
{ ip: '9.9.9.9', expected: false }, // Doesn't match any
|
||||
];
|
||||
|
||||
for (const { ip, expected } of tests) {
|
||||
const result = IpUtils.isGlobIPMatch(ip, allowList);
|
||||
expect(result).toEqual(expected);
|
||||
console.log(`✅ IP ${ip} in mixed patterns = ${result} (expected ${expected})`);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '19.5.19',
|
||||
version: '21.1.7',
|
||||
description: '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.'
|
||||
}
|
||||
|
||||
@@ -21,13 +21,47 @@ export class IpUtils {
|
||||
const normalizedIPVariants = this.normalizeIP(ip);
|
||||
if (normalizedIPVariants.length === 0) return false;
|
||||
|
||||
// Normalize the pattern IPs for consistent comparison
|
||||
const expandedPatterns = patterns.flatMap(pattern => this.normalizeIP(pattern));
|
||||
// Check each pattern
|
||||
for (const pattern of patterns) {
|
||||
// Handle CIDR notation
|
||||
if (pattern.includes('/')) {
|
||||
if (this.matchCIDR(ip, pattern)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for any match between normalized IP variants and patterns
|
||||
return normalizedIPVariants.some((ipVariant) =>
|
||||
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
|
||||
);
|
||||
// Handle range notation
|
||||
if (pattern.includes('-') && !pattern.includes('*')) {
|
||||
if (this.matchIPRange(ip, pattern)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Expand shorthand patterns for glob matching
|
||||
let expandedPattern = pattern;
|
||||
if (pattern.includes('*') && !pattern.includes(':')) {
|
||||
const parts = pattern.split('.');
|
||||
while (parts.length < 4) {
|
||||
parts.push('*');
|
||||
}
|
||||
expandedPattern = parts.join('.');
|
||||
}
|
||||
|
||||
// Normalize and check with minimatch
|
||||
const normalizedPatterns = this.normalizeIP(expandedPattern);
|
||||
|
||||
for (const ipVariant of normalizedIPVariants) {
|
||||
for (const normalizedPattern of normalizedPatterns) {
|
||||
if (plugins.minimatch(ipVariant, normalizedPattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,6 +158,100 @@ export class IpUtils {
|
||||
return !this.isPrivateIP(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP matches a CIDR notation
|
||||
*
|
||||
* @param ip The IP address to check
|
||||
* @param cidr The CIDR notation (e.g., "192.168.1.0/24")
|
||||
* @returns true if IP is within the CIDR range
|
||||
*/
|
||||
private static matchCIDR(ip: string, cidr: string): boolean {
|
||||
if (!cidr.includes('/')) return false;
|
||||
|
||||
const [networkAddr, prefixStr] = cidr.split('/');
|
||||
const prefix = parseInt(prefixStr, 10);
|
||||
|
||||
// Handle IPv4-mapped IPv6 in the IP being checked
|
||||
let checkIP = ip;
|
||||
if (checkIP.startsWith('::ffff:')) {
|
||||
checkIP = checkIP.slice(7);
|
||||
}
|
||||
|
||||
// Handle IPv6 CIDR
|
||||
if (networkAddr.includes(':')) {
|
||||
// TODO: Implement IPv6 CIDR matching
|
||||
return false;
|
||||
}
|
||||
|
||||
// IPv4 CIDR matching
|
||||
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false;
|
||||
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(networkAddr)) return false;
|
||||
if (isNaN(prefix) || prefix < 0 || prefix > 32) return false;
|
||||
|
||||
const ipParts = checkIP.split('.').map(Number);
|
||||
const netParts = networkAddr.split('.').map(Number);
|
||||
|
||||
// Validate IP parts
|
||||
for (const part of [...ipParts, ...netParts]) {
|
||||
if (part < 0 || part > 255) return false;
|
||||
}
|
||||
|
||||
// Convert to 32-bit integers
|
||||
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
|
||||
const netNum = (netParts[0] << 24) | (netParts[1] << 16) | (netParts[2] << 8) | netParts[3];
|
||||
|
||||
// Create mask
|
||||
const mask = (-1 << (32 - prefix)) >>> 0;
|
||||
|
||||
// Check if IP is in network range
|
||||
return (ipNum & mask) === (netNum & mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP matches a range notation
|
||||
*
|
||||
* @param ip The IP address to check
|
||||
* @param range The range notation (e.g., "192.168.1.1-192.168.1.100")
|
||||
* @returns true if IP is within the range
|
||||
*/
|
||||
private static matchIPRange(ip: string, range: string): boolean {
|
||||
if (!range.includes('-')) return false;
|
||||
|
||||
const [startIP, endIP] = range.split('-').map(s => s.trim());
|
||||
|
||||
// Handle IPv4-mapped IPv6 in the IP being checked
|
||||
let checkIP = ip;
|
||||
if (checkIP.startsWith('::ffff:')) {
|
||||
checkIP = checkIP.slice(7);
|
||||
}
|
||||
|
||||
// Only handle IPv4 for now
|
||||
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false;
|
||||
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(startIP)) return false;
|
||||
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(endIP)) return false;
|
||||
|
||||
const ipParts = checkIP.split('.').map(Number);
|
||||
const startParts = startIP.split('.').map(Number);
|
||||
const endParts = endIP.split('.').map(Number);
|
||||
|
||||
// Validate parts
|
||||
for (const part of [...ipParts, ...startParts, ...endParts]) {
|
||||
if (part < 0 || part > 255) return false;
|
||||
}
|
||||
|
||||
// Convert to 32-bit integers for comparison
|
||||
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
|
||||
const startNum = (startParts[0] << 24) | (startParts[1] << 16) | (startParts[2] << 8) | startParts[3];
|
||||
const endNum = (endParts[0] << 24) | (endParts[1] << 16) | (endParts[2] << 8) | endParts[3];
|
||||
|
||||
// Convert to unsigned for proper comparison
|
||||
const ipUnsigned = ipNum >>> 0;
|
||||
const startUnsigned = startNum >>> 0;
|
||||
const endUnsigned = endNum >>> 0;
|
||||
|
||||
return ipUnsigned >= startUnsigned && ipUnsigned <= endUnsigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a subnet CIDR to an IP range for filtering
|
||||
*
|
||||
|
||||
@@ -127,8 +127,20 @@ export class SecurityManager {
|
||||
const normalizedIPVariants = normalizeIP(ip);
|
||||
if (normalizedIPVariants.length === 0) return false;
|
||||
|
||||
// Normalize the pattern IPs for consistent comparison
|
||||
const expandedPatterns = patterns.flatMap(normalizeIP);
|
||||
// Expand shorthand patterns and normalize IPs for consistent comparison
|
||||
const expandShorthand = (pattern: string): string => {
|
||||
// Expand shorthand IP patterns like '192.168.*' to '192.168.*.*'
|
||||
if (pattern.includes('*') && !pattern.includes(':')) {
|
||||
const parts = pattern.split('.');
|
||||
while (parts.length < 4) {
|
||||
parts.push('*');
|
||||
}
|
||||
return parts.join('.');
|
||||
}
|
||||
return pattern;
|
||||
};
|
||||
|
||||
const expandedPatterns = patterns.map(expandShorthand).flatMap(normalizeIP);
|
||||
|
||||
// Check for any match between normalized IP variants and patterns
|
||||
return normalizedIPVariants.some((ipVariant) =>
|
||||
|
||||
@@ -335,10 +335,22 @@ export class RouteValidator {
|
||||
private static isValidDomain(domain: string): boolean {
|
||||
if (!domain || typeof domain !== 'string') return false;
|
||||
if (domain === '*') return true;
|
||||
if (domain === 'localhost') return true;
|
||||
|
||||
// Basic domain pattern validation
|
||||
const domainPattern = /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
||||
return domainPattern.test(domain) || domain === 'localhost';
|
||||
// Allow both *.domain and *domain patterns
|
||||
// Also allow regular domains and subdomains
|
||||
const domainPatterns = [
|
||||
// Standard domain with optional wildcard subdomain (*.example.com)
|
||||
/^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/,
|
||||
// Wildcard prefix without dot (*example.com)
|
||||
/^\*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?))*$/,
|
||||
// IP address
|
||||
/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
// IPv6 address
|
||||
/^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/
|
||||
];
|
||||
|
||||
return domainPatterns.some(pattern => pattern.test(domain));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,7 +405,8 @@ export class RouteValidator {
|
||||
// Check for wildcards in IPv4
|
||||
if (ip.includes('*') && !ip.includes(':')) {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length !== 4) return false;
|
||||
// Allow 1-4 parts for wildcard patterns (e.g., '10.*', '192.168.*', '192.168.1.*')
|
||||
if (parts.length < 1 || parts.length > 4) return false;
|
||||
|
||||
for (const part of parts) {
|
||||
if (part !== '*' && !/^\d{1,3}$/.test(part)) return false;
|
||||
|
||||
Reference in New Issue
Block a user