From 3efd9c72ba387748dc2042e08e51212d899f8148 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 19 Aug 2025 13:58:22 +0000 Subject: [PATCH] fix(route-validator): Relax domain validation to accept localhost, prefix wildcards (e.g. *example.com) and IP literals; add comprehensive domain validation tests --- changelog.md | 8 + test/test.domain-validation.ts | 189 ++++++++++++++++++ ts/00_commitinfo_data.ts | 2 +- .../smart-proxy/utils/route-validator.ts | 18 +- 4 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 test/test.domain-validation.ts diff --git a/changelog.md b/changelog.md index e86ef1c..73ca7aa 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # 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 diff --git a/test/test.domain-validation.ts b/test/test.domain-validation.ts new file mode 100644 index 0000000..243d025 --- /dev/null +++ b/test/test.domain-validation.ts @@ -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(); \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 5386679..d968c2c 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { 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.' } diff --git a/ts/proxies/smart-proxy/utils/route-validator.ts b/ts/proxies/smart-proxy/utils/route-validator.ts index 55fd247..3b29026 100644 --- a/ts/proxies/smart-proxy/utils/route-validator.ts +++ b/ts/proxies/smart-proxy/utils/route-validator.ts @@ -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)); } /**