fix(route-validator): Relax domain validation to accept localhost, prefix wildcards (e.g. *example.com) and IP literals; add comprehensive domain validation tests
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-08-19 - 21.1.6 - fix(ip-utils)
|
||||||
Fix IP wildcard/shorthand handling and add validation test
|
Fix IP wildcard/shorthand handling and add validation test
|
||||||
|
|
||||||
|
|||||||
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();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '21.1.6',
|
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.'
|
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,10 +335,22 @@ export class RouteValidator {
|
|||||||
private static isValidDomain(domain: string): boolean {
|
private static isValidDomain(domain: string): boolean {
|
||||||
if (!domain || typeof domain !== 'string') return false;
|
if (!domain || typeof domain !== 'string') return false;
|
||||||
if (domain === '*') return true;
|
if (domain === '*') return true;
|
||||||
|
if (domain === 'localhost') return true;
|
||||||
|
|
||||||
// Basic domain pattern validation
|
// Allow both *.domain and *domain patterns
|
||||||
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])?$/;
|
// Also allow regular domains and subdomains
|
||||||
return domainPattern.test(domain) || domain === 'localhost';
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user