diff --git a/changelog.md b/changelog.md index 3016813..8adce93 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-02-21 - 3.3.0 - feat(PortProxy) +Enhanced PortProxy with domain and IP filtering, SNI support, and minimatch integration + +- Added new ProxySettings interface to configure domain patterns, SNI, and default allowed IPs. +- Integrated minimatch to filter allowed IPs and domains. +- Enabled SNI support for PortProxy connections. +- Updated port proxy test to accommodate new settings. + ## 2025-02-04 - 3.2.0 - feat(testing) Added a comprehensive test suite for the PortProxy class diff --git a/package.json b/package.json index f37bb27..f11b268 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "@push.rocks/smartstring": "^4.0.15", "@tsclass/tsclass": "^4.4.0", "@types/ws": "^8.5.14", - "ws": "^8.18.0" + "ws": "^8.18.0", + "minimatch": "^9.0.3", + "@types/minimatch": "^5.1.2" }, "files": [ "ts/**/*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf892ba..5180534 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,15 @@ importers: '@tsclass/tsclass': specifier: ^4.4.0 version: 4.4.0 + '@types/minimatch': + specifier: ^5.1.2 + version: 5.1.2 '@types/ws': specifier: ^8.5.14 version: 8.5.14 + minimatch: + specifier: ^9.0.3 + version: 9.0.5 ws: specifier: ^8.18.0 version: 8.18.0 diff --git a/test/test.portproxy.ts b/test/test.portproxy.ts index 40674bd..d603ad1 100644 --- a/test/test.portproxy.ts +++ b/test/test.portproxy.ts @@ -58,7 +58,11 @@ function createTestClient(port: number, data: string): Promise { // Setup test environment tap.test('setup port proxy test environment', async () => { testServer = await createTestServer(TEST_SERVER_PORT); - portProxy = new PortProxy(PROXY_PORT, TEST_SERVER_PORT); + portProxy = new PortProxy(PROXY_PORT, TEST_SERVER_PORT, { + domains: [], + sniEnabled: false, + defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'] + }); }); tap.test('should start port proxy', async () => { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index cee2290..1cf7e3c 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: '3.2.0', + version: '3.3.0', description: 'a proxy for handling high workloads of proxying' } diff --git a/ts/smartproxy.plugins.ts b/ts/smartproxy.plugins.ts index dd18836..47a8e6a 100644 --- a/ts/smartproxy.plugins.ts +++ b/ts/smartproxy.plugins.ts @@ -23,5 +23,6 @@ export { lik, smartdelay, smartrequest, smartpromise, smartstring }; // third party scope import * as ws from 'ws'; import wsDefault from 'ws'; +import { minimatch } from 'minimatch'; -export { wsDefault, ws }; +export { wsDefault, ws, minimatch }; diff --git a/ts/smartproxy.portproxy.ts b/ts/smartproxy.portproxy.ts index 3552377..8cc4c46 100644 --- a/ts/smartproxy.portproxy.ts +++ b/ts/smartproxy.portproxy.ts @@ -1,14 +1,30 @@ import * as plugins from './smartproxy.plugins.js'; import * as net from 'net'; +import * as tls from 'tls'; + + +export interface DomainConfig { + domain: string; // glob pattern for domain + allowedIPs: string[]; // glob patterns for IPs allowed to access this domain +} + +export interface ProxySettings { + domains: DomainConfig[]; + sniEnabled?: boolean; + tlsOptions?: tls.TlsOptions; + defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found +} export class PortProxy { netServer: plugins.net.Server; fromPort: number; toPort: number; + settings: ProxySettings; - constructor(fromPortArg: number, toPortArg: number) { + constructor(fromPortArg: number, toPortArg: number, settings: ProxySettings) { this.fromPort = fromPortArg; this.toPort = toPortArg; + this.settings = settings; } public async start() { @@ -22,8 +38,43 @@ export class PortProxy { from.destroy(); to.destroy(); }; - this.netServer = net - .createServer((from) => { + const isAllowed = (value: string, patterns: string[]): boolean => { + return patterns.some(pattern => plugins.minimatch(value, pattern)); + }; + + const findMatchingDomain = (serverName: string): DomainConfig | undefined => { + return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain)); + }; + + const server = this.settings.sniEnabled ? tls.createServer(this.settings.tlsOptions || {}) : net.createServer(); + + this.netServer = server.on('connection', (from: net.Socket) => { + const remoteIP = from.remoteAddress || ''; + if (this.settings.sniEnabled && from instanceof tls.TLSSocket) { + const serverName = (from as any).servername || ''; + const domainConfig = findMatchingDomain(serverName); + + if (!domainConfig) { + // If no matching domain config found, check default IPs if available + if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { + console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`); + from.end(); + return; + } + } else { + // Check if IP is allowed for this domain + if (!isAllowed(remoteIP, domainConfig.allowedIPs)) { + console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`); + from.end(); + return; + } + } + } else if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { + console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`); + from.end(); + return; + } + const to = net.createConnection({ host: 'localhost', port: this.toPort,