Compare commits

..

9 Commits

Author SHA1 Message Date
Juergen Kunz
8936f4ad46 fix(detection): fix SNI detection in TLS detector
Some checks failed
Default (tags) / security (push) Successful in 53s
Default (tags) / test (push) Failing after 43m34s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-22 00:19:59 +00:00
Juergen Kunz
36068a6d92 feat(protocols): refactor protocol utilities into centralized protocols module
Some checks failed
Default (tags) / security (push) Successful in 55s
Default (tags) / test (push) Failing after 30m45s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-21 22:37:45 +00:00
Juergen Kunz
d47b048517 feat(detection): add centralized protocol detection module
- Created ts/detection module for unified protocol detection
- Implemented TLS and HTTP detectors with fragmentation support
- Moved TLS detection logic from existing code to centralized module
- Updated RouteConnectionHandler to use ProtocolDetector for both TLS and HTTP
- Refactored ACME HTTP parsing to use detection module
- Added comprehensive tests for detection functionality
- Eliminated duplicate protocol detection code across codebase

This centralizes all non-destructive protocol detection into a single module,
improving code organization and reducing duplication between ACME and routing.
2025-07-21 19:40:01 +00:00
Juergen Kunz
c84947068c BREAKING_CHANGE(core): remove legacy forwarding module in favor of route-based system
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 30m40s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
- Removed the forwarding namespace export from main index
- Removed TForwardingType and all forwarding handlers
- Consolidated route helper functions into route-helpers.ts
- All functionality is now available through the route-based system
- Users must migrate from forwarding.* imports to direct route helper imports
2025-07-21 18:44:59 +00:00
Juergen Kunz
26f7431111 fix(docs): update documentation to improve clarity
Some checks failed
Default (tags) / security (push) Successful in 51s
Default (tags) / test (push) Failing after 31m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-21 12:23:22 +00:00
Juergen Kunz
aa6ddbc4a6 BREAKING_CHANGE(routing): refactor route configuration to support multiple targets
Some checks failed
Default (tags) / security (push) Successful in 53s
Default (tags) / test (push) Failing after 31m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-07-21 08:52:07 +00:00
Juergen Kunz
6aa5f415c1 update 2025-07-17 20:51:50 +00:00
Juergen Kunz
b26abbfd87 update 2025-07-17 15:34:58 +00:00
Juergen Kunz
82df9a6f52 update 2025-07-17 15:13:09 +00:00
124 changed files with 4667 additions and 5155 deletions

View File

@@ -1,5 +1,5 @@
{ {
"expiryDate": "2025-10-01T02:31:27.435Z", "expiryDate": "2025-10-19T23:55:27.838Z",
"issueDate": "2025-07-03T02:31:27.435Z", "issueDate": "2025-07-21T23:55:27.838Z",
"savedAt": "2025-07-03T02:31:27.435Z" "savedAt": "2025-07-21T23:55:27.838Z"
} }

View File

@@ -1,5 +1,39 @@
# Changelog # Changelog
## 2025-07-21 - 21.1.0 - feat(protocols)
Refactor protocol utilities into centralized protocols module
- Moved TLS utilities from `ts/tls/` to `ts/protocols/tls/`
- Created centralized protocol modules for HTTP, WebSocket, Proxy, and TLS
- Core utilities now delegate to protocol modules for parsing and utilities
- Maintains backward compatibility through re-exports in original locations
- Improves code organization and separation of concerns
## 2025-07-22 - 21.0.0 - BREAKING_CHANGE(forwarding)
Remove legacy forwarding module
- Removed the `forwarding` namespace export from main index
- Removed TForwardingType and all forwarding handlers
- Consolidated route helper functions into route-helpers.ts
- All functionality is now available through the route-based system
- MIGRATION: Replace `import { forwarding } from '@push.rocks/smartproxy'` with direct imports of route helpers
## 2025-07-21 - 20.0.2 - fix(docs)
Update documentation to improve clarity
- Enhanced readme with clearer breaking change warning for v20.0.0
- Fixed example email address from ssl@bleu.de to ssl@example.com
- Added load balancing and failover features to feature list
- Improved documentation structure and examples
## 2025-07-20 - 20.0.1 - BREAKING_CHANGE(routing)
Refactor route configuration to support multiple targets
- Changed route action configuration from single `target` to `targets` array
- Enables load balancing and failover capabilities with multiple upstream targets
- Updated all test files to use new `targets` array syntax
- Automatic certificate metadata refresh
## 2025-06-01 - 19.5.19 - fix(smartproxy) ## 2025-06-01 - 19.5.19 - fix(smartproxy)
Fix connection handling and improve route matching edge cases Fix connection handling and improve route matching edge cases

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "19.6.17", "version": "21.1.0",
"private": false, "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.", "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", "main": "dist_ts/index.js",
@@ -51,7 +51,8 @@
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
"npmextra.json", "npmextra.json",
"readme.md" "readme.md",
"changelog.md"
], ],
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"

3026
readme.md

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -32,14 +32,14 @@ tap.test('PathMatcher - wildcard matching', async () => {
const result = PathMatcher.match('/api/*', '/api/users/123/profile'); const result = PathMatcher.match('/api/*', '/api/users/123/profile');
expect(result.matches).toEqual(true); expect(result.matches).toEqual(true);
expect(result.pathMatch).toEqual('/api'); // Normalized without trailing slash expect(result.pathMatch).toEqual('/api'); // Normalized without trailing slash
expect(result.pathRemainder).toEqual('users/123/profile'); expect(result.pathRemainder).toEqual('/users/123/profile');
}); });
tap.test('PathMatcher - mixed parameters and wildcards', async () => { tap.test('PathMatcher - mixed parameters and wildcards', async () => {
const result = PathMatcher.match('/api/:version/*', '/api/v1/users/123'); const result = PathMatcher.match('/api/:version/*', '/api/v1/users/123');
expect(result.matches).toEqual(true); expect(result.matches).toEqual(true);
expect(result.params).toEqual({ version: 'v1' }); expect(result.params).toEqual({ version: 'v1' });
expect(result.pathRemainder).toEqual('users/123'); expect(result.pathRemainder).toEqual('/users/123');
}); });
tap.test('PathMatcher - trailing slash normalization', async () => { tap.test('PathMatcher - trailing slash normalization', async () => {

View File

@@ -58,7 +58,7 @@ tap.test('Shared Security Manager', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'target.com', port: 443 } targets: [{ host: 'target.com', port: 443 }]
}, },
security: { security: {
ipAllowList: ['10.0.0.*', '192.168.1.*'], ipAllowList: ['10.0.0.*', '192.168.1.*'],
@@ -113,7 +113,7 @@ tap.test('Shared Security Manager', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'target.com', port: 443 } targets: [{ host: 'target.com', port: 443 }]
}, },
security: { security: {
rateLimit: { rateLimit: {

View File

@@ -59,7 +59,7 @@ tap.test('should create ACME challenge route', async (tools) => {
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { host: 'localhost', port: 8080 } targets: [{ host: 'localhost', port: 8080 }]
} }
}, },
challengeRoute challengeRoute

View File

@@ -18,7 +18,7 @@ tap.test('should defer certificate provisioning until ports are ready', async (t
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8181 }, targets: [{ host: 'localhost', port: 8181 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',

View File

@@ -30,7 +30,7 @@ tap.test('should defer certificate provisioning until after ports are listening'
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8181 }, targets: [{ host: 'localhost', port: 8181 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',
@@ -126,7 +126,7 @@ tap.test('should have ACME challenge route ready before certificate provisioning
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8181 }, targets: [{ host: 'localhost', port: 8181 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'

View File

@@ -16,10 +16,10 @@ tap.test('SmartCertManager should call getCertificateForDomain with wildcard opt
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8080 port: 8080
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',

View File

@@ -59,10 +59,10 @@ tap.test('SmartProxy should support custom certificate provision function', asyn
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8080 port: 8080
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'
@@ -109,10 +109,10 @@ tap.test('Custom certificate provision function should be called', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8080 port: 8080
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'
@@ -172,10 +172,10 @@ tap.test('Should fallback to ACME when custom provision fails', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8080 port: 8080
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'
@@ -231,10 +231,10 @@ tap.test('Should not fallback when certProvisionFallbackToAcme is false', async
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8080 port: 8080
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'
@@ -310,10 +310,10 @@ tap.test('Should return http01 for unknown domains', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8080 port: 8080
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'

View File

@@ -7,7 +7,7 @@ const testProxy = new SmartProxy({
match: { ports: 9443, domains: 'test.local' }, match: { ports: 9443, domains: 'test.local' },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8080 }, targets: [{ host: 'localhost', port: 8080 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',
@@ -67,7 +67,7 @@ tap.test('should handle static certificates', async () => {
match: { ports: 9444, domains: 'static.example.com' }, match: { ports: 9444, domains: 'static.example.com' },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8080 }, targets: [{ host: 'localhost', port: 8080 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: { certificate: {
@@ -96,7 +96,7 @@ tap.test('should handle ACME challenge routes', async () => {
match: { ports: 9445, domains: 'acme.local' }, match: { ports: 9445, domains: 'acme.local' },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8080 }, targets: [{ host: 'localhost', port: 8080 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',
@@ -112,7 +112,7 @@ tap.test('should handle ACME challenge routes', async () => {
match: { ports: 9081, domains: 'acme.local' }, match: { ports: 9081, domains: 'acme.local' },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8080 } targets: [{ host: 'localhost', port: 8080 }]
} }
}], }],
acme: { acme: {
@@ -167,7 +167,7 @@ tap.test('should renew certificates', async () => {
match: { ports: 9446, domains: 'renew.local' }, match: { ports: 9446, domains: 'renew.local' },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8080 }, targets: [{ host: 'localhost', port: 8080 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',

View File

@@ -8,7 +8,7 @@ tap.test('should create SmartProxy with certificate routes', async () => {
match: { ports: 8443, domains: 'test.example.com' }, match: { ports: 8443, domains: 'test.example.com' },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8080 }, targets: [{ host: 'localhost', port: 8080 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',

View File

@@ -13,7 +13,7 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
match: { ports: 8588 }, match: { ports: 8588 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9996 } targets: [{ host: 'localhost', port: 9996 }]
} }
}], }],
enableDetailedLogging: false, enableDetailedLogging: false,

View File

@@ -18,10 +18,10 @@ tap.test('should handle clients that connect and immediately disconnect without
match: { ports: 8560 }, match: { ports: 8560 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 // Non-existent port port: 9999 // Non-existent port
} }]
} }
}] }]
}); });
@@ -173,10 +173,10 @@ tap.test('should handle clients that error during connection', async () => {
match: { ports: 8561 }, match: { ports: 8561 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 port: 9999
} }]
} }
}] }]
}); });

View File

@@ -20,10 +20,10 @@ tap.test('comprehensive connection cleanup test - all scenarios', async () => {
match: { ports: 8570 }, match: { ports: 8570 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 // Non-existent port port: 9999 // Non-existent port
} }]
} }
}, },
{ {
@@ -31,10 +31,10 @@ tap.test('comprehensive connection cleanup test - all scenarios', async () => {
match: { ports: 8571 }, match: { ports: 8571 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 // Non-existent port port: 9999 // Non-existent port
}, }],
tls: { tls: {
mode: 'passthrough' mode: 'passthrough'
} }
@@ -215,10 +215,10 @@ tap.test('comprehensive connection cleanup test - all scenarios', async () => {
action: { action: {
type: 'forward', type: 'forward',
forwardingEngine: 'nftables', forwardingEngine: 'nftables',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 port: 9999
} }]
} }
}] }]
}); });

View File

@@ -65,10 +65,10 @@ tap.test('should forward TCP connections correctly', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 7001, port: 7001,
}, }],
}, },
}, },
], ],
@@ -118,10 +118,10 @@ tap.test('should handle TLS passthrough correctly', async () => {
tls: { tls: {
mode: 'passthrough', mode: 'passthrough',
}, },
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 7002, port: 7002,
}, }],
}, },
}, },
], ],
@@ -179,10 +179,10 @@ tap.test('should handle SNI-based forwarding', async () => {
tls: { tls: {
mode: 'passthrough', mode: 'passthrough',
}, },
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 7002, port: 7002,
}, }],
}, },
}, },
{ {
@@ -197,10 +197,10 @@ tap.test('should handle SNI-based forwarding', async () => {
tls: { tls: {
mode: 'passthrough', mode: 'passthrough',
}, },
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 7002, port: 7002,
}, }],
}, },
}, },
], ],

View File

@@ -90,10 +90,10 @@ tap.test('Setup test environment', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: TEST_SERVER_PORT port: TEST_SERVER_PORT
} }]
}, },
security: { security: {
maxConnections: 5 // Low limit for testing maxConnections: 5 // Low limit for testing
@@ -198,10 +198,10 @@ tap.test('HttpProxy per-IP validation', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: TEST_SERVER_PORT port: TEST_SERVER_PORT
}, }],
tls: { tls: {
mode: 'terminate' mode: 'terminate'
} }

131
test/test.detection.ts Normal file
View File

@@ -0,0 +1,131 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartproxy from '../ts/index.js';
tap.test('Protocol Detection - TLS Detection', async () => {
// Test TLS handshake detection
const tlsHandshake = Buffer.from([
0x16, // Handshake record type
0x03, 0x01, // TLS 1.0
0x00, 0x05, // Length: 5 bytes
0x01, // ClientHello
0x00, 0x00, 0x01, 0x00 // Handshake length and data
]);
const detector = new smartproxy.detection.TlsDetector();
expect(detector.canHandle(tlsHandshake)).toEqual(true);
const result = detector.detect(tlsHandshake);
expect(result).toBeDefined();
expect(result?.protocol).toEqual('tls');
expect(result?.connectionInfo.tlsVersion).toEqual('TLSv1.0');
});
tap.test('Protocol Detection - HTTP Detection', async () => {
// Test HTTP request detection
const httpRequest = Buffer.from(
'GET /test HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'User-Agent: TestClient/1.0\r\n' +
'\r\n'
);
const detector = new smartproxy.detection.HttpDetector();
expect(detector.canHandle(httpRequest)).toEqual(true);
const result = detector.detect(httpRequest);
expect(result).toBeDefined();
expect(result?.protocol).toEqual('http');
expect(result?.connectionInfo.method).toEqual('GET');
expect(result?.connectionInfo.path).toEqual('/test');
expect(result?.connectionInfo.domain).toEqual('example.com');
});
tap.test('Protocol Detection - Main Detector TLS', async () => {
const tlsHandshake = Buffer.from([
0x16, // Handshake record type
0x03, 0x03, // TLS 1.2
0x00, 0x05, // Length: 5 bytes
0x01, // ClientHello
0x00, 0x00, 0x01, 0x00 // Handshake length and data
]);
const result = await smartproxy.detection.ProtocolDetector.detect(tlsHandshake);
expect(result.protocol).toEqual('tls');
expect(result.connectionInfo.tlsVersion).toEqual('TLSv1.2');
});
tap.test('Protocol Detection - Main Detector HTTP', async () => {
const httpRequest = Buffer.from(
'POST /api/test HTTP/1.1\r\n' +
'Host: api.example.com\r\n' +
'Content-Type: application/json\r\n' +
'Content-Length: 2\r\n' +
'\r\n' +
'{}'
);
const result = await smartproxy.detection.ProtocolDetector.detect(httpRequest);
expect(result.protocol).toEqual('http');
expect(result.connectionInfo.method).toEqual('POST');
expect(result.connectionInfo.path).toEqual('/api/test');
expect(result.connectionInfo.domain).toEqual('api.example.com');
});
tap.test('Protocol Detection - Unknown Protocol', async () => {
const unknownData = Buffer.from('UNKNOWN PROTOCOL DATA\r\n');
const result = await smartproxy.detection.ProtocolDetector.detect(unknownData);
expect(result.protocol).toEqual('unknown');
expect(result.isComplete).toEqual(true);
});
tap.test('Protocol Detection - Fragmented HTTP', async () => {
const connectionId = 'test-connection-1';
// First fragment
const fragment1 = Buffer.from('GET /test HT');
let result = await smartproxy.detection.ProtocolDetector.detectWithConnectionTracking(
fragment1,
connectionId
);
expect(result.protocol).toEqual('http');
expect(result.isComplete).toEqual(false);
// Second fragment
const fragment2 = Buffer.from('TP/1.1\r\nHost: example.com\r\n\r\n');
result = await smartproxy.detection.ProtocolDetector.detectWithConnectionTracking(
fragment2,
connectionId
);
expect(result.protocol).toEqual('http');
expect(result.isComplete).toEqual(true);
expect(result.connectionInfo.method).toEqual('GET');
expect(result.connectionInfo.path).toEqual('/test');
expect(result.connectionInfo.domain).toEqual('example.com');
});
tap.test('Protocol Detection - HTTP Methods', async () => {
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
for (const method of methods) {
const request = Buffer.from(
`${method} /test HTTP/1.1\r\n` +
'Host: example.com\r\n' +
'\r\n'
);
const detector = new smartproxy.detection.HttpDetector();
const result = detector.detect(request);
expect(result?.connectionInfo.method).toEqual(method);
}
});
tap.test('Protocol Detection - Invalid Data', async () => {
// Binary data that's not a valid protocol
const binaryData = Buffer.from([0xFF, 0xFE, 0xFD, 0xFC, 0xFB]);
const result = await smartproxy.detection.ProtocolDetector.detect(binaryData);
expect(result.protocol).toEqual('unknown');
});
tap.start();

View File

@@ -9,7 +9,7 @@ tap.test('should verify certificate manager callback is preserved on updateRoute
match: { ports: [18443], domains: ['test.local'] }, match: { ports: [18443], domains: ['test.local'] },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 3000 }, targets: [{ host: 'localhost', port: 3000 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',
@@ -63,7 +63,7 @@ tap.test('should verify certificate manager callback is preserved on updateRoute
match: { ports: [18444], domains: ['test2.local'] }, match: { ports: [18444], domains: ['test2.local'] },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 3001 }, targets: [{ host: 'localhost', port: 3001 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',

View File

@@ -37,7 +37,7 @@ tap.test('regular forward route should work correctly', async () => {
match: { ports: 7890 }, match: { ports: 7890 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 6789 } targets: [{ host: 'localhost', port: 6789 }]
} }
}] }]
}); });
@@ -106,7 +106,7 @@ tap.skip.test('NFTables forward route should not terminate connections (requires
action: { action: {
type: 'forward', type: 'forward',
forwardingEngine: 'nftables', forwardingEngine: 'nftables',
target: { host: 'localhost', port: 6789 } targets: [{ host: 'localhost', port: 6789 }]
} }
}] }]
}); });

View File

@@ -39,10 +39,10 @@ tap.test('forward connections should not be immediately closed', async (t) => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 9090, port: 9090,
}, }],
}, },
}, },
], ],

View File

@@ -1,9 +1,6 @@
import { tap, expect } from '@git.zone/tstest/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js'; import * as plugins from '../ts/plugins.js';
import type { IForwardConfig, TForwardingType } from '../ts/forwarding/config/forwarding-types.js';
// First, import the components directly to avoid issues with compiled modules
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
// Import route-based helpers // Import route-based helpers
import { import {
createHttpRoute, createHttpRoute,
@@ -39,7 +36,7 @@ tap.test('Route Helpers - Create HTTP routes', async () => {
const route = helpers.httpOnly('example.com', { host: 'localhost', port: 3000 }); const route = helpers.httpOnly('example.com', { host: 'localhost', port: 3000 });
expect(route.action.type).toEqual('forward'); expect(route.action.type).toEqual('forward');
expect(route.match.domains).toEqual('example.com'); expect(route.match.domains).toEqual('example.com');
expect(route.action.target).toEqual({ host: 'localhost', port: 3000 }); expect(route.action.targets?.[0]).toEqual({ host: 'localhost', port: 3000 });
}); });
tap.test('Route Helpers - Create HTTPS terminate to HTTP routes', async () => { tap.test('Route Helpers - Create HTTPS terminate to HTTP routes', async () => {

View File

@@ -1,53 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js';
// First, import the components directly to avoid issues with compiled modules
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
// Import route-based helpers from the correct location
import {
createHttpRoute,
createHttpsTerminateRoute,
createHttpsPassthroughRoute,
createHttpToHttpsRedirect,
createCompleteHttpsServer,
createLoadBalancerRoute
} from '../ts/proxies/smart-proxy/utils/route-patterns.js';
// Create helper functions for building forwarding configs
const helpers = {
httpOnly: () => ({ type: 'http-only' as const }),
tlsTerminateToHttp: () => ({ type: 'https-terminate-to-http' as const }),
tlsTerminateToHttps: () => ({ type: 'https-terminate-to-https' as const }),
httpsPassthrough: () => ({ type: 'https-passthrough' as const })
};
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
// HTTP-only defaults
const httpConfig = {
type: 'http-only' as const,
target: { host: 'localhost', port: 3000 }
};
const httpWithDefaults = ForwardingHandlerFactory['applyDefaults'](httpConfig);
expect(httpWithDefaults.port).toEqual(80);
expect(httpWithDefaults.socket).toEqual('/tmp/forwarding-http-only-80.sock');
// HTTPS passthrough defaults
const httpsPassthroughConfig = {
type: 'https-passthrough' as const,
target: { host: 'localhost', port: 443 }
};
const httpsPassthroughWithDefaults = ForwardingHandlerFactory['applyDefaults'](httpsPassthroughConfig);
expect(httpsPassthroughWithDefaults.port).toEqual(443);
expect(httpsPassthroughWithDefaults.socket).toEqual('/tmp/forwarding-https-passthrough-443.sock');
});
tap.test('ForwardingHandlerFactory - factory function for handlers', async () => {
// @todo Implement unit tests for ForwardingHandlerFactory
// These tests would need proper mocking of the handlers
});
export default tap.start();

View File

@@ -20,7 +20,7 @@ tap.test('should forward non-TLS connections on HttpProxy ports', async (tapTest
match: { ports: testPort }, match: { ports: testPort },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8181 } targets: [{ host: 'localhost', port: 8181 }]
} }
}] }]
}; };
@@ -81,7 +81,7 @@ tap.test('should use direct connection for non-HttpProxy ports', async (tapTest)
match: { ports: 8080 }, // Not in useHttpProxy match: { ports: 8080 }, // Not in useHttpProxy
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8181 } targets: [{ host: 'localhost', port: 8181 }]
} }
}] }]
}; };
@@ -142,7 +142,7 @@ tap.test('should handle ACME HTTP-01 challenges on port 80 with HttpProxy', asyn
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8080 } targets: [{ host: 'localhost', port: 8080 }]
} }
}] }]
}; };

View File

@@ -14,7 +14,7 @@ tap.test('should detect and forward non-TLS connections on useHttpProxy ports',
match: { ports: 8080 }, match: { ports: 8080 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8181 } targets: [{ host: 'localhost', port: 8181 }]
} }
}] }]
}; };
@@ -140,7 +140,7 @@ tap.test('should handle TLS connections normally', async (tapTest) => {
match: { ports: 443 }, match: { ports: 443 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8443 }, targets: [{ host: 'localhost', port: 8443 }],
tls: { mode: 'terminate' } tls: { mode: 'terminate' }
} }
}] }]

View File

@@ -17,7 +17,7 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy
match: { ports: 8081 }, match: { ports: 8081 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8181 } targets: [{ host: 'localhost', port: 8181 }]
} }
}] }]
}); });
@@ -120,7 +120,7 @@ tap.test('should properly detect non-TLS connections on HttpProxy ports', async
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: targetPort } targets: [{ host: 'localhost', port: targetPort }]
} }
}] }]
}); });

View File

@@ -42,7 +42,7 @@ tap.test('should forward HTTP connections on port 8080', async (tapTest) => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: targetPort } targets: [{ host: 'localhost', port: targetPort }]
} }
}] }]
}); });
@@ -131,7 +131,7 @@ tap.test('should handle basic HTTP request forwarding', async (tapTest) => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: targetPort } targets: [{ host: 'localhost', port: targetPort }]
} }
}] }]
}); });

View File

@@ -67,7 +67,7 @@ tap.test('should handle ACME challenges on port 8080 with improved port binding
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: targetPort }, targets: [{ host: 'localhost', port: targetPort }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' // Use ACME for certificate certificate: 'auto' // Use ACME for certificate
@@ -83,7 +83,7 @@ tap.test('should handle ACME challenges on port 8080 with improved port binding
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: targetPort } targets: [{ host: 'localhost', port: targetPort }]
} }
} }
], ],
@@ -191,7 +191,7 @@ tap.test('should handle ACME challenges on port 8080 with improved port binding
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { host: 'localhost', port: targetPort } targets: [{ host: 'localhost', port: targetPort }]
} }
} }
]; ];

View File

@@ -95,10 +95,10 @@ tap.test('should support static host/port routes', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: serverPort port: serverPort
} }]
} }
} }
]; ];
@@ -135,13 +135,13 @@ tap.test('should support function-based host', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: (context: IRouteContext) => { host: (context: IRouteContext) => {
// Return localhost always in this test // Return localhost always in this test
return 'localhost'; return 'localhost';
}, },
port: serverPort port: serverPort
} }]
} }
} }
]; ];
@@ -178,13 +178,13 @@ tap.test('should support function-based port', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: (context: IRouteContext) => { port: (context: IRouteContext) => {
// Return test server port // Return test server port
return serverPort; return serverPort;
} }
} }]
} }
} }
]; ];
@@ -221,14 +221,14 @@ tap.test('should support function-based host AND port', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: (context: IRouteContext) => { host: (context: IRouteContext) => {
return 'localhost'; return 'localhost';
}, },
port: (context: IRouteContext) => { port: (context: IRouteContext) => {
return serverPort; return serverPort;
} }
} }]
} }
} }
]; ];
@@ -265,7 +265,7 @@ tap.test('should support context-based routing with path', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: (context: IRouteContext) => { host: (context: IRouteContext) => {
// Use path to determine host // Use path to determine host
if (context.path?.startsWith('/api')) { if (context.path?.startsWith('/api')) {
@@ -275,7 +275,7 @@ tap.test('should support context-based routing with path', async () => {
} }
}, },
port: serverPort port: serverPort
} }]
} }
} }
]; ];

View File

@@ -232,10 +232,10 @@ tap.test('should start the proxy server', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3100 port: 3100
}, }],
tls: { tls: {
mode: 'terminate' mode: 'terminate'
}, },

View File

@@ -40,7 +40,7 @@ tap.test('keepalive support - verify keepalive connections are properly handled'
match: { ports: 8590 }, match: { ports: 8590 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9998 } targets: [{ host: 'localhost', port: 9998 }]
} }
}], }],
keepAlive: true, keepAlive: true,
@@ -117,7 +117,7 @@ tap.test('keepalive support - verify keepalive connections are properly handled'
match: { ports: 8591 }, match: { ports: 8591 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9998 } targets: [{ host: 'localhost', port: 9998 }]
} }
}], }],
keepAlive: true, keepAlive: true,
@@ -178,7 +178,7 @@ tap.test('keepalive support - verify keepalive connections are properly handled'
match: { ports: 8592 }, match: { ports: 8592 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9998 } targets: [{ host: 'localhost', port: 9998 }]
} }
}], }],
keepAlive: true, keepAlive: true,

View File

@@ -39,10 +39,10 @@ tap.test('setup test environment', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9876 port: 9876
} }]
// No TLS configuration - just plain TCP forwarding // No TLS configuration - just plain TCP forwarding
} }
}], }],

View File

@@ -29,7 +29,7 @@ tap.test('MetricsCollector provides accurate metrics', async (tools) => {
match: { ports: 8700 }, match: { ports: 8700 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9995 } targets: [{ host: 'localhost', port: 9995 }]
} }
}, },
{ {
@@ -37,7 +37,7 @@ tap.test('MetricsCollector provides accurate metrics', async (tools) => {
match: { ports: 8701 }, match: { ports: 8701 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9995 } targets: [{ host: 'localhost', port: 9995 }]
} }
} }
], ],

View File

@@ -36,10 +36,10 @@ tap.test('should create SmartProxy instance with new metrics', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: echoServerPort port: echoServerPort
}, }],
tls: { tls: {
mode: 'passthrough' mode: 'passthrough'
} }

View File

@@ -34,10 +34,10 @@ tap.skip.test('NFTables forwarding should not terminate connections (requires ro
action: { action: {
type: 'forward', type: 'forward',
forwardingEngine: 'nftables', forwardingEngine: 'nftables',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 8001, port: 8001,
}, }],
}, },
}, },
// Also add regular forwarding route for comparison // Also add regular forwarding route for comparison
@@ -49,10 +49,10 @@ tap.skip.test('NFTables forwarding should not terminate connections (requires ro
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 8001, port: 8001,
}, }],
}, },
}, },
], ],

View File

@@ -42,10 +42,10 @@ const sampleRoute: IRouteConfig = {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8000 port: 8000
}, }],
forwardingEngine: 'nftables', forwardingEngine: 'nftables',
nftables: { nftables: {
protocol: 'tcp', protocol: 'tcp',
@@ -115,10 +115,10 @@ tap.skip.test('NFTablesManager route updating test', async () => {
...sampleRoute, ...sampleRoute,
action: { action: {
...sampleRoute.action, ...sampleRoute.action,
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9000 // Different port port: 9000 // Different port
}, }],
nftables: { nftables: {
...sampleRoute.action.nftables, ...sampleRoute.action.nftables,
protocol: 'all' // Different protocol protocol: 'all' // Different protocol
@@ -147,10 +147,10 @@ tap.skip.test('NFTablesManager route deprovisioning test', async () => {
...sampleRoute, ...sampleRoute,
action: { action: {
...sampleRoute.action, ...sampleRoute.action,
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9000 // Different port from original test port: 9000 // Different port from original test
}, }],
nftables: { nftables: {
...sampleRoute.action.nftables, ...sampleRoute.action.nftables,
protocol: 'all' // Different protocol from original test protocol: 'all' // Different protocol from original test

View File

@@ -91,7 +91,7 @@ testFn('SmartProxy getNfTablesStatus functionality', async () => {
match: { ports: 3004 }, match: { ports: 3004 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 3005 } targets: [{ host: 'localhost', port: 3005 }]
} }
} }
] ]

View File

@@ -29,7 +29,7 @@ tap.test('port forwarding should not immediately close connections', async (tool
match: { ports: 9999 }, match: { ports: 9999 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 8888 } targets: [{ host: 'localhost', port: 8888 }]
} }
}] }]
}); });
@@ -63,7 +63,7 @@ tap.test('TLS passthrough should work correctly', async () => {
action: { action: {
type: 'forward', type: 'forward',
tls: { mode: 'passthrough' }, tls: { mode: 'passthrough' },
target: { host: 'localhost', port: 443 } targets: [{ host: 'localhost', port: 443 }]
} }
}] }]
}); });

View File

@@ -214,12 +214,12 @@ tap.test('should handle errors in port mapping functions', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: () => { port: () => {
throw new Error('Test error in port mapping function'); throw new Error('Test error in port mapping function');
} }
} }]
}, },
name: 'Error Route' name: 'Error Route'
}; };

View File

@@ -21,7 +21,7 @@ tap.test('should not double-register port 80 when user route and ACME use same p
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { host: 'localhost', port: 3000 } targets: [{ host: 'localhost', port: 3000 }]
} }
}, },
{ {
@@ -31,7 +31,7 @@ tap.test('should not double-register port 80 when user route and ACME use same p
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { host: 'localhost', port: 3001 }, targets: [{ host: 'localhost', port: 3001 }],
tls: { tls: {
mode: 'terminate' as const, mode: 'terminate' as const,
certificate: 'auto' as const certificate: 'auto' as const
@@ -153,7 +153,7 @@ tap.test('should handle ACME on different port than user routes', async (tools)
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { host: 'localhost', port: 3000 } targets: [{ host: 'localhost', port: 3000 }]
} }
}, },
{ {
@@ -163,7 +163,7 @@ tap.test('should handle ACME on different port than user routes', async (tools)
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { host: 'localhost', port: 3001 }, targets: [{ host: 'localhost', port: 3001 }],
tls: { tls: {
mode: 'terminate' as const, mode: 'terminate' as const,
certificate: 'auto' as const certificate: 'auto' as const

View File

@@ -15,10 +15,10 @@ tap.test('setup two smartproxies in a chain configuration', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'httpbin.org', host: 'httpbin.org',
port: 443 port: 443
} }]
} }
} }
], ],
@@ -45,10 +45,10 @@ tap.test('setup two smartproxies in a chain configuration', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8002 port: 8002
}, }],
sendProxyProtocol: true sendProxyProtocol: true
} }
} }

View File

@@ -32,10 +32,10 @@ tap.test('simple proxy chain test - identify connection accumulation', async ()
match: { ports: 8591 }, match: { ports: 8591 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9998 // Backend that closes immediately port: 9998 // Backend that closes immediately
} }]
} }
}] }]
}); });
@@ -50,10 +50,10 @@ tap.test('simple proxy chain test - identify connection accumulation', async ()
match: { ports: 8590 }, match: { ports: 8590 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8591 // Forward to proxy2 port: 8591 // Forward to proxy2
} }]
} }
}] }]
}); });

View File

@@ -19,10 +19,10 @@ tap.test('should handle proxy chaining without connection accumulation', async (
match: { ports: 8581 }, match: { ports: 8581 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 // Non-existent backend port: 9999 // Non-existent backend
} }]
} }
}] }]
}); });
@@ -37,10 +37,10 @@ tap.test('should handle proxy chaining without connection accumulation', async (
match: { ports: 8580 }, match: { ports: 8580 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8581 // Forward to proxy2 port: 8581 // Forward to proxy2
} }]
} }
}] }]
}); });
@@ -270,10 +270,10 @@ tap.test('should handle proxy chain with HTTP traffic', async () => {
match: { ports: 8583 }, match: { ports: 8583 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 // Non-existent backend port: 9999 // Non-existent backend
} }]
} }
}] }]
}); });
@@ -289,10 +289,10 @@ tap.test('should handle proxy chain with HTTP traffic', async () => {
match: { ports: 8582 }, match: { ports: 8582 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8583 // Forward to proxy2 port: 8583 // Forward to proxy2
} }]
} }
}] }]
}); });

View File

@@ -19,10 +19,10 @@ tap.test('should handle rapid connection retries without leaking connections', a
match: { ports: 8550 }, match: { ports: 8550 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9999 // Non-existent port to force connection failures port: 9999 // Non-existent port to force connection failures
} }]
} }
}] }]
}); });

View File

@@ -17,7 +17,7 @@ tap.test('should set update routes callback on certificate manager', async () =>
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 3000 }, targets: [{ host: 'localhost', port: 3000 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',
@@ -95,7 +95,7 @@ tap.test('should set update routes callback on certificate manager', async () =>
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 3001 }, targets: [{ host: 'localhost', port: 3001 }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',

View File

@@ -56,8 +56,8 @@ tap.test('Routes: Should create basic HTTP route', async () => {
expect(httpRoute.match.ports).toEqual(80); expect(httpRoute.match.ports).toEqual(80);
expect(httpRoute.match.domains).toEqual('example.com'); expect(httpRoute.match.domains).toEqual('example.com');
expect(httpRoute.action.type).toEqual('forward'); expect(httpRoute.action.type).toEqual('forward');
expect(httpRoute.action.target?.host).toEqual('localhost'); expect(httpRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(httpRoute.action.target?.port).toEqual(3000); expect(httpRoute.action.targets?.[0]?.port).toEqual(3000);
expect(httpRoute.name).toEqual('Basic HTTP Route'); expect(httpRoute.name).toEqual('Basic HTTP Route');
}); });
@@ -74,8 +74,8 @@ tap.test('Routes: Should create HTTPS route with TLS termination', async () => {
expect(httpsRoute.action.type).toEqual('forward'); expect(httpsRoute.action.type).toEqual('forward');
expect(httpsRoute.action.tls?.mode).toEqual('terminate'); expect(httpsRoute.action.tls?.mode).toEqual('terminate');
expect(httpsRoute.action.tls?.certificate).toEqual('auto'); expect(httpsRoute.action.tls?.certificate).toEqual('auto');
expect(httpsRoute.action.target?.host).toEqual('localhost'); expect(httpsRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(httpsRoute.action.target?.port).toEqual(8080); expect(httpsRoute.action.targets?.[0]?.port).toEqual(8080);
expect(httpsRoute.name).toEqual('HTTPS Route'); expect(httpsRoute.name).toEqual('HTTPS Route');
}); });
@@ -131,10 +131,10 @@ tap.test('Routes: Should create load balancer route', async () => {
// Validate the route configuration // Validate the route configuration
expect(lbRoute.match.domains).toEqual('app.example.com'); expect(lbRoute.match.domains).toEqual('app.example.com');
expect(lbRoute.action.type).toEqual('forward'); expect(lbRoute.action.type).toEqual('forward');
expect(Array.isArray(lbRoute.action.target?.host)).toBeTrue(); expect(Array.isArray(lbRoute.action.targets?.[0]?.host)).toBeTrue();
expect((lbRoute.action.target?.host as string[]).length).toEqual(3); expect((lbRoute.action.targets?.[0]?.host as string[]).length).toEqual(3);
expect((lbRoute.action.target?.host as string[])[0]).toEqual('10.0.0.1'); expect((lbRoute.action.targets?.[0]?.host as string[])[0]).toEqual('10.0.0.1');
expect(lbRoute.action.target?.port).toEqual(8080); expect(lbRoute.action.targets?.[0]?.port).toEqual(8080);
expect(lbRoute.action.tls?.mode).toEqual('terminate'); expect(lbRoute.action.tls?.mode).toEqual('terminate');
}); });
@@ -152,8 +152,8 @@ tap.test('Routes: Should create API route with CORS', async () => {
expect(apiRoute.match.path).toEqual('/v1/*'); expect(apiRoute.match.path).toEqual('/v1/*');
expect(apiRoute.action.type).toEqual('forward'); expect(apiRoute.action.type).toEqual('forward');
expect(apiRoute.action.tls?.mode).toEqual('terminate'); expect(apiRoute.action.tls?.mode).toEqual('terminate');
expect(apiRoute.action.target?.host).toEqual('localhost'); expect(apiRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(apiRoute.action.target?.port).toEqual(3000); expect(apiRoute.action.targets?.[0]?.port).toEqual(3000);
// Check CORS headers // Check CORS headers
expect(apiRoute.headers).toBeDefined(); expect(apiRoute.headers).toBeDefined();
@@ -177,8 +177,8 @@ tap.test('Routes: Should create WebSocket route', async () => {
expect(wsRoute.match.path).toEqual('/socket'); expect(wsRoute.match.path).toEqual('/socket');
expect(wsRoute.action.type).toEqual('forward'); expect(wsRoute.action.type).toEqual('forward');
expect(wsRoute.action.tls?.mode).toEqual('terminate'); expect(wsRoute.action.tls?.mode).toEqual('terminate');
expect(wsRoute.action.target?.host).toEqual('localhost'); expect(wsRoute.action.targets?.[0]?.host).toEqual('localhost');
expect(wsRoute.action.target?.port).toEqual(5000); expect(wsRoute.action.targets?.[0]?.port).toEqual(5000);
// Check WebSocket configuration // Check WebSocket configuration
expect(wsRoute.action.websocket).toBeDefined(); expect(wsRoute.action.websocket).toBeDefined();
@@ -209,10 +209,10 @@ tap.test('SmartProxy: Should create instance with route-based config', async ()
}) })
], ],
defaults: { defaults: {
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8080 port: 8080
}, }],
security: { security: {
ipAllowList: ['127.0.0.1', '192.168.0.*'], ipAllowList: ['127.0.0.1', '192.168.0.*'],
maxConnections: 100 maxConnections: 100
@@ -294,13 +294,13 @@ tap.test('Edge Case - Wildcard Domains and Path Matching', async () => {
const bestMatch = findBestMatchingRoute(routes, { domain: 'api.example.com', path: '/api/users', port: 443 }); const bestMatch = findBestMatchingRoute(routes, { domain: 'api.example.com', path: '/api/users', port: 443 });
expect(bestMatch).not.toBeUndefined(); expect(bestMatch).not.toBeUndefined();
if (bestMatch) { if (bestMatch) {
expect(bestMatch.action.target.port).toEqual(3001); // Should match the exact domain route expect(bestMatch.action.targets[0].port).toEqual(3001); // Should match the exact domain route
} }
// Test with a different subdomain - should only match the wildcard route // Test with a different subdomain - should only match the wildcard route
const otherMatches = findMatchingRoutes(routes, { domain: 'other.example.com', path: '/api/products', port: 443 }); const otherMatches = findMatchingRoutes(routes, { domain: 'other.example.com', path: '/api/products', port: 443 });
expect(otherMatches.length).toEqual(1); expect(otherMatches.length).toEqual(1);
expect(otherMatches[0].action.target.port).toEqual(3000); // Should match the wildcard domain route expect(otherMatches[0].action.targets[0].port).toEqual(3000); // Should match the wildcard domain route
}); });
tap.test('Edge Case - Disabled Routes', async () => { tap.test('Edge Case - Disabled Routes', async () => {
@@ -316,7 +316,7 @@ tap.test('Edge Case - Disabled Routes', async () => {
// Should only find the enabled route // Should only find the enabled route
expect(matches.length).toEqual(1); expect(matches.length).toEqual(1);
expect(matches[0].action.target.port).toEqual(3000); expect(matches[0].action.targets[0].port).toEqual(3000);
}); });
tap.test('Edge Case - Complex Path and Headers Matching', async () => { tap.test('Edge Case - Complex Path and Headers Matching', async () => {
@@ -333,10 +333,10 @@ tap.test('Edge Case - Complex Path and Headers Matching', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'internal-api', host: 'internal-api',
port: 8080 port: 8080
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto' certificate: 'auto'
@@ -376,10 +376,10 @@ tap.test('Edge Case - Port Range Matching', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'backend', host: 'backend',
port: 3000 port: 3000
} }]
}, },
name: 'Port Range Route' name: 'Port Range Route'
}; };
@@ -404,10 +404,10 @@ tap.test('Edge Case - Port Range Matching', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'backend', host: 'backend',
port: 3000 port: 3000
} }]
}, },
name: 'Multi Range Route' name: 'Multi Range Route'
}; };
@@ -452,7 +452,7 @@ tap.test('Wildcard Domain Handling', async () => {
expect(bestSpecificMatch).not.toBeUndefined(); expect(bestSpecificMatch).not.toBeUndefined();
if (bestSpecificMatch) { if (bestSpecificMatch) {
// Find which route was matched // Find which route was matched
const matchedPort = bestSpecificMatch.action.target.port; const matchedPort = bestSpecificMatch.action.targets[0].port;
console.log(`Matched route with port: ${matchedPort}`); console.log(`Matched route with port: ${matchedPort}`);
// Verify it's the specific subdomain route (with highest priority) // Verify it's the specific subdomain route (with highest priority)
@@ -465,7 +465,7 @@ tap.test('Wildcard Domain Handling', async () => {
expect(bestWildcardMatch).not.toBeUndefined(); expect(bestWildcardMatch).not.toBeUndefined();
if (bestWildcardMatch) { if (bestWildcardMatch) {
// Find which route was matched // Find which route was matched
const matchedPort = bestWildcardMatch.action.target.port; const matchedPort = bestWildcardMatch.action.targets[0].port;
console.log(`Matched route with port: ${matchedPort}`); console.log(`Matched route with port: ${matchedPort}`);
// Verify it's the wildcard subdomain route (with medium priority) // Verify it's the wildcard subdomain route (with medium priority)
@@ -513,7 +513,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
expect(webServerMatch).not.toBeUndefined(); expect(webServerMatch).not.toBeUndefined();
if (webServerMatch) { if (webServerMatch) {
expect(webServerMatch.action.type).toEqual('forward'); expect(webServerMatch.action.type).toEqual('forward');
expect(webServerMatch.action.target.host).toEqual('web-server'); expect(webServerMatch.action.targets[0].host).toEqual('web-server');
} }
// Web server (HTTP redirect via socket handler) // Web server (HTTP redirect via socket handler)
@@ -532,7 +532,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
expect(apiMatch).not.toBeUndefined(); expect(apiMatch).not.toBeUndefined();
if (apiMatch) { if (apiMatch) {
expect(apiMatch.action.type).toEqual('forward'); expect(apiMatch.action.type).toEqual('forward');
expect(apiMatch.action.target.host).toEqual('api-server'); expect(apiMatch.action.targets[0].host).toEqual('api-server');
} }
// WebSocket server // WebSocket server
@@ -544,7 +544,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
expect(wsMatch).not.toBeUndefined(); expect(wsMatch).not.toBeUndefined();
if (wsMatch) { if (wsMatch) {
expect(wsMatch.action.type).toEqual('forward'); expect(wsMatch.action.type).toEqual('forward');
expect(wsMatch.action.target.host).toEqual('websocket-server'); expect(wsMatch.action.targets[0].host).toEqual('websocket-server');
expect(wsMatch.action.websocket?.enabled).toBeTrue(); expect(wsMatch.action.websocket?.enabled).toBeTrue();
} }

View File

@@ -28,10 +28,10 @@ tap.test('route security should block connections from unauthorized IPs', async
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 9990 port: 9990
} }]
}, },
security: { security: {
// Only allow a non-existent IP // Only allow a non-existent IP
@@ -142,10 +142,10 @@ tap.test('route security with block list should work', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 9992 port: 9992
} }]
}, },
security: { // Security at route level, not action level security: { // Security at route level, not action level
ipBlockList: ['127.0.0.1', '::1', '::ffff:127.0.0.1'] ipBlockList: ['127.0.0.1', '::1', '::ffff:127.0.0.1']
@@ -234,10 +234,10 @@ tap.test('route without security should allow all connections', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 9994 port: 9994
} }]
} }
// No security defined // No security defined
}]; }];

View File

@@ -10,10 +10,10 @@ tap.test('route security should be correctly configured', async () => {
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 8991 port: 8991
}, }],
security: { security: {
ipAllowList: ['192.168.1.1'], ipAllowList: ['192.168.1.1'],
ipBlockList: ['10.0.0.1'] ipBlockList: ['10.0.0.1']

View File

@@ -26,10 +26,10 @@ tap.test('route-specific security should be enforced', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 8877 port: 8877
} }]
}, },
security: { security: {
ipAllowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1'] ipAllowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1']
@@ -108,10 +108,10 @@ tap.test('route-specific IP block list should be enforced', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 8879 port: 8879
} }]
}, },
security: { security: {
ipAllowList: ['0.0.0.0/0', '::/0'], // Allow all IPs ipAllowList: ['0.0.0.0/0', '::/0'], // Allow all IPs
@@ -215,10 +215,10 @@ tap.test('routes without security should allow all connections', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: 8881 port: 8881
} }]
// No security section - should allow all // No security section - should allow all
} }
}]; }];

View File

@@ -13,10 +13,10 @@ const createRoute = (id: number, domain: string, port: number = 8443) => ({
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 + id port: 3000 + id
}, }],
tls: { tls: {
mode: 'terminate' as const, mode: 'terminate' as const,
certificate: 'auto' as const, certificate: 'auto' as const,
@@ -209,10 +209,10 @@ tap.test('should handle route updates when cert manager is not initialized', asy
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
} }
}] }]
}); });

View File

@@ -47,7 +47,7 @@ import {
addRateLimiting, addRateLimiting,
addBasicAuth, addBasicAuth,
addJwtAuth addJwtAuth
} from '../ts/proxies/smart-proxy/utils/route-patterns.js'; } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
import type { import type {
IRouteConfig, IRouteConfig,
@@ -134,10 +134,10 @@ tap.test('Route Validation - validateRouteAction', async () => {
// Valid forward action // Valid forward action
const validForwardAction: IRouteAction = { const validForwardAction: IRouteAction = {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
}; };
const validForwardResult = validateRouteAction(validForwardAction); const validForwardResult = validateRouteAction(validForwardAction);
expect(validForwardResult.valid).toBeTrue(); expect(validForwardResult.valid).toBeTrue();
@@ -154,14 +154,14 @@ tap.test('Route Validation - validateRouteAction', async () => {
expect(validSocketResult.valid).toBeTrue(); expect(validSocketResult.valid).toBeTrue();
expect(validSocketResult.errors.length).toEqual(0); expect(validSocketResult.errors.length).toEqual(0);
// Invalid action (missing target) // Invalid action (missing targets)
const invalidAction: IRouteAction = { const invalidAction: IRouteAction = {
type: 'forward' type: 'forward'
}; };
const invalidResult = validateRouteAction(invalidAction); const invalidResult = validateRouteAction(invalidAction);
expect(invalidResult.valid).toBeFalse(); expect(invalidResult.valid).toBeFalse();
expect(invalidResult.errors.length).toBeGreaterThan(0); expect(invalidResult.errors.length).toBeGreaterThan(0);
expect(invalidResult.errors[0]).toInclude('Target is required'); expect(invalidResult.errors[0]).toInclude('Targets array is required');
// Invalid action (missing socket handler) // Invalid action (missing socket handler)
const invalidSocketAction: IRouteAction = { const invalidSocketAction: IRouteAction = {
@@ -180,7 +180,7 @@ tap.test('Route Validation - validateRouteConfig', async () => {
expect(validResult.valid).toBeTrue(); expect(validResult.valid).toBeTrue();
expect(validResult.errors.length).toEqual(0); expect(validResult.errors.length).toEqual(0);
// Invalid route config (missing target) // Invalid route config (missing targets)
const invalidRoute: IRouteConfig = { const invalidRoute: IRouteConfig = {
match: { match: {
domains: 'example.com', domains: 'example.com',
@@ -309,16 +309,16 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => {
const actionOverride: Partial<IRouteConfig> = { const actionOverride: Partial<IRouteConfig> = {
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'new-host.local', host: 'new-host.local',
port: 5000 port: 5000
} }]
} }
}; };
const actionMergedRoute = mergeRouteConfigs(baseRoute, actionOverride); const actionMergedRoute = mergeRouteConfigs(baseRoute, actionOverride);
expect(actionMergedRoute.action.target.host).toEqual('new-host.local'); expect(actionMergedRoute.action.targets?.[0]?.host).toEqual('new-host.local');
expect(actionMergedRoute.action.target.port).toEqual(5000); expect(actionMergedRoute.action.targets?.[0]?.port).toEqual(5000);
// Test replacing action with socket handler // Test replacing action with socket handler
const typeChangeOverride: Partial<IRouteConfig> = { const typeChangeOverride: Partial<IRouteConfig> = {
@@ -336,7 +336,7 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => {
const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride); const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride);
expect(typeChangedRoute.action.type).toEqual('socket-handler'); expect(typeChangedRoute.action.type).toEqual('socket-handler');
expect(typeChangedRoute.action.socketHandler).toBeDefined(); expect(typeChangedRoute.action.socketHandler).toBeDefined();
expect(typeChangedRoute.action.target).toBeUndefined(); expect(typeChangedRoute.action.targets).toBeUndefined();
}); });
tap.test('Route Matching - routeMatchesDomain', async () => { tap.test('Route Matching - routeMatchesDomain', async () => {
@@ -379,10 +379,10 @@ tap.test('Route Matching - routeMatchesPort', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
} }
}; };
@@ -393,10 +393,10 @@ tap.test('Route Matching - routeMatchesPort', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
} }
}; };
@@ -427,10 +427,10 @@ tap.test('Route Matching - routeMatchesPath', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
} }
}; };
@@ -443,10 +443,10 @@ tap.test('Route Matching - routeMatchesPath', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
} }
}; };
@@ -458,10 +458,10 @@ tap.test('Route Matching - routeMatchesPath', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
} }
}; };
@@ -494,10 +494,10 @@ tap.test('Route Matching - routeMatchesHeaders', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }]
} }
}; };
@@ -641,7 +641,7 @@ tap.test('Route Utilities - cloneRoute', async () => {
expect(clonedRoute.name).toEqual(originalRoute.name); expect(clonedRoute.name).toEqual(originalRoute.name);
expect(clonedRoute.match.domains).toEqual(originalRoute.match.domains); expect(clonedRoute.match.domains).toEqual(originalRoute.match.domains);
expect(clonedRoute.action.type).toEqual(originalRoute.action.type); expect(clonedRoute.action.type).toEqual(originalRoute.action.type);
expect(clonedRoute.action.target.port).toEqual(originalRoute.action.target.port); expect(clonedRoute.action.targets?.[0]?.port).toEqual(originalRoute.action.targets?.[0]?.port);
// Modify the clone and check that the original is unchanged // Modify the clone and check that the original is unchanged
clonedRoute.name = 'Modified Clone'; clonedRoute.name = 'Modified Clone';
@@ -656,8 +656,8 @@ tap.test('Route Helpers - createHttpRoute', async () => {
expect(route.match.domains).toEqual('example.com'); expect(route.match.domains).toEqual('example.com');
expect(route.match.ports).toEqual(80); expect(route.match.ports).toEqual(80);
expect(route.action.type).toEqual('forward'); expect(route.action.type).toEqual('forward');
expect(route.action.target.host).toEqual('localhost'); expect(route.action.targets?.[0]?.host).toEqual('localhost');
expect(route.action.target.port).toEqual(3000); expect(route.action.targets?.[0]?.port).toEqual(3000);
const validationResult = validateRouteConfig(route); const validationResult = validateRouteConfig(route);
expect(validationResult.valid).toBeTrue(); expect(validationResult.valid).toBeTrue();
@@ -790,11 +790,11 @@ tap.test('Route Helpers - createLoadBalancerRoute', async () => {
expect(route.match.domains).toEqual('loadbalancer.example.com'); expect(route.match.domains).toEqual('loadbalancer.example.com');
expect(route.match.ports).toEqual(443); expect(route.match.ports).toEqual(443);
expect(route.action.type).toEqual('forward'); expect(route.action.type).toEqual('forward');
expect(Array.isArray(route.action.target.host)).toBeTrue(); expect(route.action.targets).toBeDefined();
if (Array.isArray(route.action.target.host)) { if (route.action.targets && Array.isArray(route.action.targets[0]?.host)) {
expect(route.action.target.host.length).toEqual(3); expect((route.action.targets[0].host as string[]).length).toEqual(3);
} }
expect(route.action.target.port).toEqual(8080); expect(route.action.targets?.[0]?.port).toEqual(8080);
expect(route.action.tls.mode).toEqual('terminate'); expect(route.action.tls.mode).toEqual('terminate');
const validationResult = validateRouteConfig(route); const validationResult = validateRouteConfig(route);
@@ -819,7 +819,7 @@ tap.test('Route Patterns - createApiGatewayRoute', async () => {
expect(apiGatewayRoute.match.domains).toEqual('api.example.com'); expect(apiGatewayRoute.match.domains).toEqual('api.example.com');
expect(apiGatewayRoute.match.path).toInclude('/v1'); expect(apiGatewayRoute.match.path).toInclude('/v1');
expect(apiGatewayRoute.action.type).toEqual('forward'); expect(apiGatewayRoute.action.type).toEqual('forward');
expect(apiGatewayRoute.action.target.port).toEqual(3000); expect(apiGatewayRoute.action.targets?.[0]?.port).toEqual(3000);
// Check TLS configuration // Check TLS configuration
if (apiGatewayRoute.action.tls) { if (apiGatewayRoute.action.tls) {
@@ -854,7 +854,7 @@ tap.test('Route Patterns - createWebSocketPattern', async () => {
expect(wsRoute.match.domains).toEqual('ws.example.com'); expect(wsRoute.match.domains).toEqual('ws.example.com');
expect(wsRoute.match.path).toEqual('/socket'); expect(wsRoute.match.path).toEqual('/socket');
expect(wsRoute.action.type).toEqual('forward'); expect(wsRoute.action.type).toEqual('forward');
expect(wsRoute.action.target.port).toEqual(3000); expect(wsRoute.action.targets?.[0]?.port).toEqual(3000);
// Check TLS configuration // Check TLS configuration
if (wsRoute.action.tls) { if (wsRoute.action.tls) {
@@ -891,8 +891,8 @@ tap.test('Route Patterns - createLoadBalancerRoute pattern', async () => {
expect(lbRoute.action.type).toEqual('forward'); expect(lbRoute.action.type).toEqual('forward');
// Check target hosts // Check target hosts
if (Array.isArray(lbRoute.action.target.host)) { if (lbRoute.action.targets && Array.isArray(lbRoute.action.targets[0]?.host)) {
expect(lbRoute.action.target.host.length).toEqual(3); expect((lbRoute.action.targets[0].host as string[]).length).toEqual(3);
} }
// Check TLS configuration // Check TLS configuration

View File

@@ -37,10 +37,10 @@ function createRouteConfig(
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: destinationIp, host: destinationIp,
port: destinationPort port: destinationPort
} }]
} }
}; };
} }

View File

@@ -38,15 +38,17 @@ tap.test('Per-IP connection limits validation', async () => {
// Track connections up to limit // Track connections up to limit
for (let i = 1; i <= 5; i++) { for (let i = 1; i <= 5; i++) {
securityManager.trackConnectionByIP(testIP, `conn${i}`); // Validate BEFORE tracking the connection (checking if we can add a new connection)
const result = securityManager.validateIP(testIP); const result = securityManager.validateIP(testIP);
expect(result.allowed).toBeTrue(); expect(result.allowed).toBeTrue();
// Now track the connection
securityManager.trackConnectionByIP(testIP, `conn${i}`);
} }
// Verify we're at the limit // Verify we're at the limit
expect(securityManager.getConnectionCountByIP(testIP)).toEqual(5); expect(securityManager.getConnectionCountByIP(testIP)).toEqual(5);
// Next connection should be rejected // Next connection should be rejected (we're already at 5)
const result = securityManager.validateIP(testIP); const result = securityManager.validateIP(testIP);
expect(result.allowed).toBeFalse(); expect(result.allowed).toBeFalse();
expect(result.reason).toInclude('Maximum connections per IP'); expect(result.reason).toInclude('Maximum connections per IP');
@@ -61,28 +63,23 @@ tap.test('Connection rate limiting', async () => {
const testIP = '192.168.1.102'; const testIP = '192.168.1.102';
// Make connections at the rate limit // Make connections at the rate limit
// Note: validateIP() already tracks timestamps internally for rate limiting
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const result = securityManager.validateIP(testIP); const result = securityManager.validateIP(testIP);
expect(result.allowed).toBeTrue(); expect(result.allowed).toBeTrue();
securityManager.trackConnectionByIP(testIP, `conn${i}`);
} }
// Next connection should exceed rate limit // Next connection should exceed rate limit
const result = securityManager.validateIP(testIP); const result = securityManager.validateIP(testIP);
expect(result.allowed).toBeFalse(); expect(result.allowed).toBeFalse();
expect(result.reason).toInclude('Connection rate limit'); expect(result.reason).toInclude('Connection rate limit');
// Clean up connections
for (let i = 0; i < 10; i++) {
securityManager.removeConnectionByIP(testIP, `conn${i}`);
}
}); });
tap.test('Route-level connection limits', async () => { tap.test('Route-level connection limits', async () => {
const route: IRouteConfig = { const route: IRouteConfig = {
name: 'test-route', name: 'test-route',
match: { ports: 443 }, match: { ports: 443 },
action: { type: 'forward', target: { host: 'localhost', port: 8080 } }, action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }] },
security: { security: {
maxConnections: 3 maxConnections: 3
} }
@@ -93,7 +90,8 @@ tap.test('Route-level connection limits', async () => {
clientIp: '192.168.1.103', clientIp: '192.168.1.103',
serverIp: '0.0.0.0', serverIp: '0.0.0.0',
timestamp: Date.now(), timestamp: Date.now(),
connectionId: 'test-conn' connectionId: 'test-conn',
isTls: true
}; };
// Test with connection counts below limit // Test with connection counts below limit

View File

@@ -15,10 +15,10 @@ tap.test('should create a SmartCertManager instance', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
}, }],
tls: { tls: {
mode: 'terminate', mode: 'terminate',
certificate: 'auto', certificate: 'auto',

View File

@@ -73,10 +73,10 @@ tap.test('setup port proxy test environment', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: TEST_SERVER_PORT port: TEST_SERVER_PORT
} }]
} }
} }
], ],
@@ -112,10 +112,10 @@ tap.test('should forward TCP connections to custom host', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: TEST_SERVER_PORT port: TEST_SERVER_PORT
} }]
} }
} }
], ],
@@ -157,10 +157,10 @@ tap.test('should forward connections to custom IP', async () => {
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: '127.0.0.1', host: '127.0.0.1',
port: targetServerPort port: targetServerPort
} }]
} }
} }
], ],
@@ -252,10 +252,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: PROXY_PORT + 5 port: PROXY_PORT + 5
} }]
} }
} }
], ],
@@ -273,10 +273,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: TEST_SERVER_PORT port: TEST_SERVER_PORT
} }]
} }
} }
], ],
@@ -311,10 +311,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: PROXY_PORT + 7 port: PROXY_PORT + 7
} }]
} }
} }
], ],
@@ -334,10 +334,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
}, },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: TEST_SERVER_PORT port: TEST_SERVER_PORT
} }]
} }
} }
], ],
@@ -377,10 +377,10 @@ tap.test('should use round robin for multiple target hosts in domain config', as
}, },
action: { action: {
type: 'forward' as const, type: 'forward' as const,
target: { targets: [{
host: ['hostA', 'hostB'], // Array of hosts for round-robin host: ['hostA', 'hostB'], // Array of hosts for round-robin
port: 80 port: 80
} }]
} }
}; };
@@ -400,9 +400,9 @@ tap.test('should use round robin for multiple target hosts in domain config', as
// For route-based approach, the actual round-robin logic happens in connection handling // For route-based approach, the actual round-robin logic happens in connection handling
// Just make sure our config has the expected hosts // Just make sure our config has the expected hosts
expect(Array.isArray(routeConfig.action.target.host)).toBeTrue(); expect(Array.isArray(routeConfig.action.targets![0].host)).toBeTrue();
expect(routeConfig.action.target.host).toContain('hostA'); expect(routeConfig.action.targets![0].host).toContain('hostA');
expect(routeConfig.action.target.host).toContain('hostB'); expect(routeConfig.action.targets![0].host).toContain('hostB');
}); });
// CLEANUP: Tear down all servers and proxies // CLEANUP: Tear down all servers and proxies

View File

@@ -30,7 +30,7 @@ tap.test('stuck connection cleanup - verify connections to hanging backends are
match: { ports: 8589 }, match: { ports: 8589 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9997 } targets: [{ host: 'localhost', port: 9997 }]
} }
}], }],
keepAlive: true, keepAlive: true,

View File

@@ -17,7 +17,7 @@ tap.test('websocket keep-alive settings for SNI passthrough', async (tools) => {
match: { ports: 8443, domains: 'test.local' }, match: { ports: 8443, domains: 'test.local' },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9443 }, targets: [{ host: 'localhost', port: 9443 }],
tls: { mode: 'passthrough' } tls: { mode: 'passthrough' }
} }
} }
@@ -108,7 +108,7 @@ tap.test('long-lived connection survival test', async (tools) => {
match: { ports: 8444 }, match: { ports: 8444 },
action: { action: {
type: 'forward', type: 'forward',
target: { host: 'localhost', port: 9444 } targets: [{ host: 'localhost', port: 9444 }]
} }
} }
] ]

View File

@@ -52,10 +52,10 @@ tap.test('zombie connection cleanup - verify inactivity check detects and cleans
match: { ports: 8591 }, match: { ports: 8591 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 9998 port: 9998
} }]
} }
}] }]
}); });
@@ -71,10 +71,10 @@ tap.test('zombie connection cleanup - verify inactivity check detects and cleans
match: { ports: 8590 }, match: { ports: 8590 },
action: { action: {
type: 'forward', type: 'forward',
target: { targets: [{
host: 'localhost', host: 'localhost',
port: 8591 port: 8591
} }]
} }
}] }]
}); });

View File

@@ -1,161 +1,44 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import { logger } from './logger.js'; import { logger } from './logger.js';
import { ProxyProtocolParser as ProtocolParser, type IProxyInfo, type IProxyParseResult } from '../../protocols/proxy/index.js';
/** // Re-export types from protocols for backward compatibility
* Interface representing parsed PROXY protocol information export type { IProxyInfo, IProxyParseResult } from '../../protocols/proxy/index.js';
*/
export interface IProxyInfo {
protocol: 'TCP4' | 'TCP6' | 'UNKNOWN';
sourceIP: string;
sourcePort: number;
destinationIP: string;
destinationPort: number;
}
/**
* Interface for parse result including remaining data
*/
export interface IProxyParseResult {
proxyInfo: IProxyInfo | null;
remainingData: Buffer;
}
/** /**
* Parser for PROXY protocol v1 (text format) * Parser for PROXY protocol v1 (text format)
* Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt * Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
*
* This class now delegates to the protocol parser but adds
* smartproxy-specific features like socket reading and logging
*/ */
export class ProxyProtocolParser { export class ProxyProtocolParser {
static readonly PROXY_V1_SIGNATURE = 'PROXY '; static readonly PROXY_V1_SIGNATURE = ProtocolParser.PROXY_V1_SIGNATURE;
static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header static readonly MAX_HEADER_LENGTH = ProtocolParser.MAX_HEADER_LENGTH;
static readonly HEADER_TERMINATOR = '\r\n'; static readonly HEADER_TERMINATOR = ProtocolParser.HEADER_TERMINATOR;
/** /**
* Parse PROXY protocol v1 header from buffer * Parse PROXY protocol v1 header from buffer
* Returns proxy info and remaining data after header * Returns proxy info and remaining data after header
*/ */
static parse(data: Buffer): IProxyParseResult { static parse(data: Buffer): IProxyParseResult {
// Check if buffer starts with PROXY signature // Delegate to protocol parser
if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) { return ProtocolParser.parse(data);
return {
proxyInfo: null,
remainingData: data
};
}
// Find header terminator
const headerEndIndex = data.indexOf(this.HEADER_TERMINATOR);
if (headerEndIndex === -1) {
// Header incomplete, need more data
if (data.length > this.MAX_HEADER_LENGTH) {
// Header too long, invalid
throw new Error('PROXY protocol header exceeds maximum length');
}
return {
proxyInfo: null,
remainingData: data
};
}
// Extract header line
const headerLine = data.toString('ascii', 0, headerEndIndex);
const remainingData = data.slice(headerEndIndex + 2); // Skip \r\n
// Parse header
const parts = headerLine.split(' ');
if (parts.length < 2) {
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
}
const [signature, protocol] = parts;
// Validate protocol
if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(protocol)) {
throw new Error(`Invalid PROXY protocol: ${protocol}`);
}
// For UNKNOWN protocol, ignore addresses
if (protocol === 'UNKNOWN') {
return {
proxyInfo: {
protocol: 'UNKNOWN',
sourceIP: '',
sourcePort: 0,
destinationIP: '',
destinationPort: 0
},
remainingData
};
}
// For TCP4/TCP6, we need all 6 parts
if (parts.length !== 6) {
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
}
const [, , srcIP, dstIP, srcPort, dstPort] = parts;
// Validate and parse ports
const sourcePort = parseInt(srcPort, 10);
const destinationPort = parseInt(dstPort, 10);
if (isNaN(sourcePort) || sourcePort < 0 || sourcePort > 65535) {
throw new Error(`Invalid source port: ${srcPort}`);
}
if (isNaN(destinationPort) || destinationPort < 0 || destinationPort > 65535) {
throw new Error(`Invalid destination port: ${dstPort}`);
}
// Validate IP addresses
const protocolType = protocol as 'TCP4' | 'TCP6' | 'UNKNOWN';
if (!this.isValidIP(srcIP, protocolType)) {
throw new Error(`Invalid source IP for ${protocol}: ${srcIP}`);
}
if (!this.isValidIP(dstIP, protocolType)) {
throw new Error(`Invalid destination IP for ${protocol}: ${dstIP}`);
}
return {
proxyInfo: {
protocol: protocol as 'TCP4' | 'TCP6',
sourceIP: srcIP,
sourcePort,
destinationIP: dstIP,
destinationPort
},
remainingData
};
} }
/** /**
* Generate PROXY protocol v1 header * Generate PROXY protocol v1 header
*/ */
static generate(info: IProxyInfo): Buffer { static generate(info: IProxyInfo): Buffer {
if (info.protocol === 'UNKNOWN') { // Delegate to protocol parser
return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii'); return ProtocolParser.generate(info);
}
const header = `PROXY ${info.protocol} ${info.sourceIP} ${info.destinationIP} ${info.sourcePort} ${info.destinationPort}\r\n`;
if (header.length > this.MAX_HEADER_LENGTH) {
throw new Error('Generated PROXY protocol header exceeds maximum length');
}
return Buffer.from(header, 'ascii');
} }
/** /**
* Validate IP address format * Validate IP address format
*/ */
private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean { private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean {
if (protocol === 'TCP4') { return ProtocolParser.isValidIP(ip, protocol);
return plugins.net.isIPv4(ip);
} else if (protocol === 'TCP6') {
return plugins.net.isIPv6(ip);
}
return false;
} }
/** /**

View File

@@ -13,7 +13,8 @@ import {
trackConnection, trackConnection,
removeConnection, removeConnection,
cleanupExpiredRateLimits, cleanupExpiredRateLimits,
parseBasicAuthHeader parseBasicAuthHeader,
normalizeIP
} from './security-utils.js'; } from './security-utils.js';
/** /**
@@ -78,7 +79,15 @@ export class SharedSecurityManager {
* @returns Number of connections from this IP * @returns Number of connections from this IP
*/ */
public getConnectionCountByIP(ip: string): number { public getConnectionCountByIP(ip: string): number {
return this.connectionsByIP.get(ip)?.connections.size || 0; // Check all normalized variants of the IP
const variants = normalizeIP(ip);
for (const variant of variants) {
const info = this.connectionsByIP.get(variant);
if (info) {
return info.connections.size;
}
}
return 0;
} }
/** /**
@@ -88,7 +97,19 @@ export class SharedSecurityManager {
* @param connectionId - The connection ID to associate * @param connectionId - The connection ID to associate
*/ */
public trackConnectionByIP(ip: string, connectionId: string): void { public trackConnectionByIP(ip: string, connectionId: string): void {
trackConnection(ip, connectionId, this.connectionsByIP); // Check if any variant already exists
const variants = normalizeIP(ip);
let existingKey: string | null = null;
for (const variant of variants) {
if (this.connectionsByIP.has(variant)) {
existingKey = variant;
break;
}
}
// Use existing key or the original IP
trackConnection(existingKey || ip, connectionId, this.connectionsByIP);
} }
/** /**
@@ -98,7 +119,15 @@ export class SharedSecurityManager {
* @param connectionId - The connection ID to remove * @param connectionId - The connection ID to remove
*/ */
public removeConnectionByIP(ip: string, connectionId: string): void { public removeConnectionByIP(ip: string, connectionId: string): void {
removeConnection(ip, connectionId, this.connectionsByIP); // Check all variants to find where the connection is tracked
const variants = normalizeIP(ip);
for (const variant of variants) {
if (this.connectionsByIP.has(variant)) {
removeConnection(variant, connectionId, this.connectionsByIP);
break;
}
}
} }
/** /**

View File

@@ -1,12 +1,13 @@
/** /**
* WebSocket utility functions * WebSocket utility functions
*
* This module provides smartproxy-specific WebSocket utilities
* and re-exports protocol utilities from the protocols module
*/ */
/** // Import and re-export from protocols
* Type for WebSocket RawData that can be different types in different environments import { getMessageSize as protocolGetMessageSize, toBuffer as protocolToBuffer } from '../../protocols/websocket/index.js';
* This matches the ws library's type definition export type { RawData } from '../../protocols/websocket/index.js';
*/
export type RawData = Buffer | ArrayBuffer | Buffer[] | any;
/** /**
* Get the length of a WebSocket message regardless of its type * Get the length of a WebSocket message regardless of its type
@@ -15,35 +16,9 @@ export type RawData = Buffer | ArrayBuffer | Buffer[] | any;
* @param data - The data message from WebSocket (could be any RawData type) * @param data - The data message from WebSocket (could be any RawData type)
* @returns The length of the data in bytes * @returns The length of the data in bytes
*/ */
export function getMessageSize(data: RawData): number { export function getMessageSize(data: import('../../protocols/websocket/index.js').RawData): number {
if (typeof data === 'string') { // Delegate to protocol implementation
// For string data, get the byte length return protocolGetMessageSize(data);
return Buffer.from(data, 'utf8').length;
} else if (data instanceof Buffer) {
// For Node.js Buffer
return data.length;
} else if (data instanceof ArrayBuffer) {
// For ArrayBuffer
return data.byteLength;
} else if (Array.isArray(data)) {
// For array of buffers, sum their lengths
return data.reduce((sum, chunk) => {
if (chunk instanceof Buffer) {
return sum + chunk.length;
} else if (chunk instanceof ArrayBuffer) {
return sum + chunk.byteLength;
}
return sum;
}, 0);
} else {
// For other types, try to determine the size or return 0
try {
return Buffer.from(data).length;
} catch (e) {
console.warn('Could not determine message size', e);
return 0;
}
}
} }
/** /**
@@ -52,30 +27,7 @@ export function getMessageSize(data: RawData): number {
* @param data - The data message from WebSocket (could be any RawData type) * @param data - The data message from WebSocket (could be any RawData type)
* @returns A Buffer containing the data * @returns A Buffer containing the data
*/ */
export function toBuffer(data: RawData): Buffer { export function toBuffer(data: import('../../protocols/websocket/index.js').RawData): Buffer {
if (typeof data === 'string') { // Delegate to protocol implementation
return Buffer.from(data, 'utf8'); return protocolToBuffer(data);
} else if (data instanceof Buffer) {
return data;
} else if (data instanceof ArrayBuffer) {
return Buffer.from(data);
} else if (Array.isArray(data)) {
// For array of buffers, concatenate them
return Buffer.concat(data.map(chunk => {
if (chunk instanceof Buffer) {
return chunk;
} else if (chunk instanceof ArrayBuffer) {
return Buffer.from(chunk);
}
return Buffer.from(chunk);
}));
} else {
// For other types, try to convert to Buffer or return empty Buffer
try {
return Buffer.from(data);
} catch (e) {
console.warn('Could not convert message to Buffer', e);
return Buffer.alloc(0);
}
}
} }

View File

@@ -0,0 +1,114 @@
/**
* HTTP Protocol Detector
*
* Simplified HTTP detection using the new architecture
*/
import type { IProtocolDetector } from '../models/interfaces.js';
import type { IDetectionResult, IDetectionOptions } from '../models/detection-types.js';
import type { IProtocolDetectionResult, IConnectionContext } from '../../protocols/common/types.js';
import type { THttpMethod } from '../../protocols/http/index.js';
import { QuickProtocolDetector } from './quick-detector.js';
import { RoutingExtractor } from './routing-extractor.js';
import { DetectionFragmentManager } from '../utils/fragment-manager.js';
/**
* Simplified HTTP detector
*/
export class HttpDetector implements IProtocolDetector {
private quickDetector = new QuickProtocolDetector();
private fragmentManager: DetectionFragmentManager;
constructor(fragmentManager?: DetectionFragmentManager) {
this.fragmentManager = fragmentManager || new DetectionFragmentManager();
}
/**
* Check if buffer can be handled by this detector
*/
canHandle(buffer: Buffer): boolean {
const result = this.quickDetector.quickDetect(buffer);
return result.protocol === 'http' && result.confidence > 50;
}
/**
* Get minimum bytes needed for detection
*/
getMinimumBytes(): number {
return 4; // "GET " minimum
}
/**
* Detect HTTP protocol from buffer
*/
detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null {
// Quick detection first
const quickResult = this.quickDetector.quickDetect(buffer);
if (quickResult.protocol !== 'http' || quickResult.confidence < 50) {
return null;
}
// Extract routing information
const routing = RoutingExtractor.extract(buffer, 'http');
// If we don't need full headers, we can return early
if (quickResult.confidence >= 95 && !options?.extractFullHeaders) {
return {
protocol: 'http',
connectionInfo: {
protocol: 'http',
method: quickResult.metadata?.method as THttpMethod,
domain: routing?.domain,
path: routing?.path
},
isComplete: true
};
}
// Check if we have complete headers
const headersEnd = buffer.indexOf('\r\n\r\n');
const isComplete = headersEnd !== -1;
return {
protocol: 'http',
connectionInfo: {
protocol: 'http',
domain: routing?.domain,
path: routing?.path,
method: quickResult.metadata?.method as THttpMethod
},
isComplete,
bytesNeeded: isComplete ? undefined : buffer.length + 512 // Need more for headers
};
}
/**
* Handle fragmented detection
*/
detectWithContext(
buffer: Buffer,
context: IConnectionContext,
options?: IDetectionOptions
): IDetectionResult | null {
const handler = this.fragmentManager.getHandler('http');
const connectionId = DetectionFragmentManager.createConnectionId(context);
// Add fragment
const result = handler.addFragment(connectionId, buffer);
if (result.error) {
handler.complete(connectionId);
return null;
}
// Try detection on accumulated buffer
const detectResult = this.detect(result.buffer!, options);
if (detectResult && detectResult.isComplete) {
handler.complete(connectionId);
}
return detectResult;
}
}

View File

@@ -0,0 +1,148 @@
/**
* Quick Protocol Detector
*
* Lightweight protocol identification based on minimal bytes
* No parsing, just identification
*/
import type { IProtocolDetector, IProtocolDetectionResult } from '../../protocols/common/types.js';
import { TlsRecordType } from '../../protocols/tls/index.js';
import { HttpParser } from '../../protocols/http/index.js';
/**
* Quick protocol detector for fast identification
*/
export class QuickProtocolDetector implements IProtocolDetector {
/**
* Check if this detector can handle the data
*/
canHandle(data: Buffer): boolean {
return data.length >= 1;
}
/**
* Perform quick detection based on first few bytes
*/
quickDetect(data: Buffer): IProtocolDetectionResult {
if (data.length === 0) {
return {
protocol: 'unknown',
confidence: 0,
requiresMoreData: true
};
}
// Check for TLS
const tlsResult = this.checkTls(data);
if (tlsResult.confidence > 80) {
return tlsResult;
}
// Check for HTTP
const httpResult = this.checkHttp(data);
if (httpResult.confidence > 80) {
return httpResult;
}
// Need more data or unknown
return {
protocol: 'unknown',
confidence: 0,
requiresMoreData: data.length < 20
};
}
/**
* Check if data looks like TLS
*/
private checkTls(data: Buffer): IProtocolDetectionResult {
if (data.length < 3) {
return {
protocol: 'tls',
confidence: 0,
requiresMoreData: true
};
}
const firstByte = data[0];
const secondByte = data[1];
// Check for valid TLS record type
const validRecordTypes = [
TlsRecordType.CHANGE_CIPHER_SPEC,
TlsRecordType.ALERT,
TlsRecordType.HANDSHAKE,
TlsRecordType.APPLICATION_DATA,
TlsRecordType.HEARTBEAT
];
if (!validRecordTypes.includes(firstByte)) {
return {
protocol: 'tls',
confidence: 0
};
}
// Check TLS version byte (0x03 for all TLS/SSL versions)
if (secondByte !== 0x03) {
return {
protocol: 'tls',
confidence: 0
};
}
// High confidence it's TLS
return {
protocol: 'tls',
confidence: 95,
metadata: {
recordType: firstByte
}
};
}
/**
* Check if data looks like HTTP
*/
private checkHttp(data: Buffer): IProtocolDetectionResult {
if (data.length < 3) {
return {
protocol: 'http',
confidence: 0,
requiresMoreData: true
};
}
// Quick check for HTTP methods
const start = data.subarray(0, Math.min(10, data.length)).toString('ascii');
// Check common HTTP methods
const httpMethods = ['GET ', 'POST ', 'PUT ', 'DELETE ', 'HEAD ', 'OPTIONS', 'PATCH ', 'CONNECT', 'TRACE '];
for (const method of httpMethods) {
if (start.startsWith(method)) {
return {
protocol: 'http',
confidence: 95,
metadata: {
method: method.trim()
}
};
}
}
// Check if it might be HTTP but need more data
if (HttpParser.isPrintableAscii(data, Math.min(20, data.length))) {
// Could be HTTP, but not sure
return {
protocol: 'http',
confidence: 30,
requiresMoreData: data.length < 20
};
}
return {
protocol: 'http',
confidence: 0
};
}
}

View File

@@ -0,0 +1,147 @@
/**
* Routing Information Extractor
*
* Extracts minimal routing information from protocols
* without full parsing
*/
import type { IRoutingInfo, IConnectionContext, TProtocolType } from '../../protocols/common/types.js';
import { SniExtraction } from '../../protocols/tls/sni/sni-extraction.js';
import { HttpParser } from '../../protocols/http/index.js';
/**
* Extracts routing information from protocol data
*/
export class RoutingExtractor {
/**
* Extract routing info based on protocol type
*/
static extract(
data: Buffer,
protocol: TProtocolType,
context?: IConnectionContext
): IRoutingInfo | null {
switch (protocol) {
case 'tls':
case 'https':
return this.extractTlsRouting(data, context);
case 'http':
return this.extractHttpRouting(data);
default:
return null;
}
}
/**
* Extract routing from TLS ClientHello (SNI)
*/
private static extractTlsRouting(
data: Buffer,
context?: IConnectionContext
): IRoutingInfo | null {
try {
// Quick SNI extraction without full parsing
const sni = SniExtraction.extractSNI(data);
if (sni) {
return {
domain: sni,
protocol: 'tls',
port: 443 // Default HTTPS port
};
}
return null;
} catch (error) {
// Extraction failed, return null
return null;
}
}
/**
* Extract routing from HTTP headers (Host header)
*/
private static extractHttpRouting(data: Buffer): IRoutingInfo | null {
try {
// Look for first line
const firstLineEnd = data.indexOf('\n');
if (firstLineEnd === -1) {
return null;
}
// Parse request line
const firstLine = data.subarray(0, firstLineEnd).toString('ascii').trim();
const requestLine = HttpParser.parseRequestLine(firstLine);
if (!requestLine) {
return null;
}
// Look for Host header
let pos = firstLineEnd + 1;
const maxSearch = Math.min(data.length, 4096); // Don't search too far
while (pos < maxSearch) {
const lineEnd = data.indexOf('\n', pos);
if (lineEnd === -1) break;
const line = data.subarray(pos, lineEnd).toString('ascii').trim();
// Empty line means end of headers
if (line.length === 0) break;
// Check for Host header
if (line.toLowerCase().startsWith('host:')) {
const hostValue = line.substring(5).trim();
const domain = HttpParser.extractDomainFromHost(hostValue);
return {
domain,
path: requestLine.path,
protocol: 'http',
port: 80 // Default HTTP port
};
}
pos = lineEnd + 1;
}
// No Host header found, but we have the path
return {
path: requestLine.path,
protocol: 'http',
port: 80
};
} catch (error) {
// Extraction failed
return null;
}
}
/**
* Try to extract domain from any protocol
*/
static extractDomain(data: Buffer, hint?: TProtocolType): string | null {
// If we have a hint, use it
if (hint) {
const routing = this.extract(data, hint);
return routing?.domain || null;
}
// Try TLS first (more specific)
const tlsRouting = this.extractTlsRouting(data);
if (tlsRouting?.domain) {
return tlsRouting.domain;
}
// Try HTTP
const httpRouting = this.extractHttpRouting(data);
if (httpRouting?.domain) {
return httpRouting.domain;
}
return null;
}
}

View File

@@ -0,0 +1,252 @@
/**
* TLS protocol detector
*/
// TLS detector doesn't need plugins imports
import type { IProtocolDetector } from '../models/interfaces.js';
import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../models/detection-types.js';
import { readUInt16BE, BufferAccumulator } from '../utils/buffer-utils.js';
import { tlsVersionToString } from '../utils/parser-utils.js';
// Import from protocols
import { TlsRecordType, TlsHandshakeType, TlsExtensionType } from '../../protocols/tls/index.js';
// Import TLS utilities for SNI extraction from protocols
import { SniExtraction } from '../../protocols/tls/sni/sni-extraction.js';
import { ClientHelloParser } from '../../protocols/tls/sni/client-hello-parser.js';
/**
* TLS detector implementation
*/
export class TlsDetector implements IProtocolDetector {
/**
* Minimum bytes needed to identify TLS (record header)
*/
private static readonly MIN_TLS_HEADER_SIZE = 5;
/**
* Fragment tracking for incomplete handshakes
*/
private static fragmentedBuffers = new Map<string, BufferAccumulator>();
/**
* Create connection ID from context
*/
private createConnectionId(context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number }): string {
return `${context.sourceIp || 'unknown'}:${context.sourcePort || 0}->${context.destIp || 'unknown'}:${context.destPort || 0}`;
}
/**
* Detect TLS protocol from buffer
*/
detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null {
// Check if buffer is too small
if (buffer.length < TlsDetector.MIN_TLS_HEADER_SIZE) {
return null;
}
// Check if this is a TLS record
if (!this.isTlsRecord(buffer)) {
return null;
}
// Extract basic TLS info
const recordType = buffer[0];
const tlsMajor = buffer[1];
const tlsMinor = buffer[2];
const recordLength = readUInt16BE(buffer, 3);
// Initialize connection info
const connectionInfo: IConnectionInfo = {
protocol: 'tls',
tlsVersion: tlsVersionToString(tlsMajor, tlsMinor) || undefined
};
// If it's a handshake, try to extract more info
if (recordType === TlsRecordType.HANDSHAKE && buffer.length >= 6) {
const handshakeType = buffer[5];
// For ClientHello, extract SNI and other info
if (handshakeType === TlsHandshakeType.CLIENT_HELLO) {
// Check if we have the complete handshake
const totalRecordLength = recordLength + 5; // Including TLS header
if (buffer.length >= totalRecordLength) {
// Extract SNI using existing logic
const sni = SniExtraction.extractSNI(buffer);
if (sni) {
connectionInfo.domain = sni;
connectionInfo.sni = sni;
}
// Parse ClientHello for additional info
const parseResult = ClientHelloParser.parseClientHello(buffer);
if (parseResult.isValid) {
// Extract ALPN if present
const alpnExtension = parseResult.extensions.find(
ext => ext.type === TlsExtensionType.APPLICATION_LAYER_PROTOCOL_NEGOTIATION
);
if (alpnExtension) {
connectionInfo.alpn = this.parseAlpnExtension(alpnExtension.data);
}
// Store cipher suites if needed
if (parseResult.cipherSuites && options?.extractFullHeaders) {
connectionInfo.cipherSuites = this.parseCipherSuites(parseResult.cipherSuites);
}
}
// Return complete result
return {
protocol: 'tls',
connectionInfo,
remainingBuffer: buffer.length > totalRecordLength
? buffer.subarray(totalRecordLength)
: undefined,
isComplete: true
};
} else {
// Incomplete handshake
return {
protocol: 'tls',
connectionInfo,
isComplete: false,
bytesNeeded: totalRecordLength
};
}
}
}
// For other TLS record types, just return basic info
return {
protocol: 'tls',
connectionInfo,
isComplete: true,
remainingBuffer: buffer.length > recordLength + 5
? buffer.subarray(recordLength + 5)
: undefined
};
}
/**
* Check if buffer can be handled by this detector
*/
canHandle(buffer: Buffer): boolean {
return buffer.length >= TlsDetector.MIN_TLS_HEADER_SIZE &&
this.isTlsRecord(buffer);
}
/**
* Get minimum bytes needed for detection
*/
getMinimumBytes(): number {
return TlsDetector.MIN_TLS_HEADER_SIZE;
}
/**
* Check if buffer contains a valid TLS record
*/
private isTlsRecord(buffer: Buffer): boolean {
const recordType = buffer[0];
// Check for valid record type
const validTypes = [
TlsRecordType.CHANGE_CIPHER_SPEC,
TlsRecordType.ALERT,
TlsRecordType.HANDSHAKE,
TlsRecordType.APPLICATION_DATA,
TlsRecordType.HEARTBEAT
];
if (!validTypes.includes(recordType)) {
return false;
}
// Check TLS version bytes (should be 0x03 0x0X)
if (buffer[1] !== 0x03) {
return false;
}
// Check record length is reasonable
const recordLength = readUInt16BE(buffer, 3);
if (recordLength > 16384) { // Max TLS record size
return false;
}
return true;
}
/**
* Parse ALPN extension data
*/
private parseAlpnExtension(data: Buffer): string[] {
const protocols: string[] = [];
if (data.length < 2) {
return protocols;
}
const listLength = readUInt16BE(data, 0);
let offset = 2;
while (offset < Math.min(2 + listLength, data.length)) {
const protoLength = data[offset];
offset++;
if (offset + protoLength <= data.length) {
const protocol = data.subarray(offset, offset + protoLength).toString('ascii');
protocols.push(protocol);
offset += protoLength;
} else {
break;
}
}
return protocols;
}
/**
* Parse cipher suites
*/
private parseCipherSuites(cipherData: Buffer): number[] {
const suites: number[] = [];
for (let i = 0; i < cipherData.length - 1; i += 2) {
const suite = readUInt16BE(cipherData, i);
suites.push(suite);
}
return suites;
}
/**
* Detect with context for fragmented data
*/
detectWithContext(
buffer: Buffer,
context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number },
options?: IDetectionOptions
): IDetectionResult | null {
const connectionId = this.createConnectionId(context);
// Get or create buffer accumulator for this connection
let accumulator = TlsDetector.fragmentedBuffers.get(connectionId);
if (!accumulator) {
accumulator = new BufferAccumulator();
TlsDetector.fragmentedBuffers.set(connectionId, accumulator);
}
// Add new data
accumulator.append(buffer);
// Try detection on accumulated data
const result = this.detect(accumulator.getBuffer(), options);
// If detection is complete or we have too much data, clean up
if (result?.isComplete || accumulator.length() > 65536) {
TlsDetector.fragmentedBuffers.delete(connectionId);
}
return result;
}
}

25
ts/detection/index.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Centralized Protocol Detection Module
*
* This module provides unified protocol detection capabilities for
* both TLS and HTTP protocols, extracting connection information
* without consuming the data stream.
*/
// Main detector
export * from './protocol-detector.js';
// Models
export * from './models/detection-types.js';
export * from './models/interfaces.js';
// Individual detectors
export * from './detectors/tls-detector.js';
export * from './detectors/http-detector.js';
export * from './detectors/quick-detector.js';
export * from './detectors/routing-extractor.js';
// Utilities
export * from './utils/buffer-utils.js';
export * from './utils/parser-utils.js';
export * from './utils/fragment-manager.js';

View File

@@ -0,0 +1,102 @@
/**
* Type definitions for protocol detection
*/
/**
* Supported protocol types that can be detected
*/
export type TProtocolType = 'tls' | 'http' | 'unknown';
/**
* HTTP method types
*/
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';
/**
* TLS version identifiers
*/
export type TTlsVersion = 'SSLv3' | 'TLSv1.0' | 'TLSv1.1' | 'TLSv1.2' | 'TLSv1.3';
/**
* Connection information extracted from protocol detection
*/
export interface IConnectionInfo {
/**
* The detected protocol type
*/
protocol: TProtocolType;
/**
* Domain/hostname extracted from the connection
* - For TLS: from SNI extension
* - For HTTP: from Host header
*/
domain?: string;
/**
* HTTP-specific fields
*/
method?: THttpMethod;
path?: string;
httpVersion?: string;
headers?: Record<string, string>;
/**
* TLS-specific fields
*/
tlsVersion?: TTlsVersion;
sni?: string;
alpn?: string[];
cipherSuites?: number[];
}
/**
* Result of protocol detection
*/
export interface IDetectionResult {
/**
* The detected protocol type
*/
protocol: TProtocolType;
/**
* Extracted connection information
*/
connectionInfo: IConnectionInfo;
/**
* Any remaining buffer data after detection headers
* This can be used to continue processing the stream
*/
remainingBuffer?: Buffer;
/**
* Whether the detection is complete or needs more data
*/
isComplete: boolean;
/**
* Minimum bytes needed for complete detection (if incomplete)
*/
bytesNeeded?: number;
}
/**
* Options for protocol detection
*/
export interface IDetectionOptions {
/**
* Maximum bytes to buffer for detection (default: 8192)
*/
maxBufferSize?: number;
/**
* Timeout for detection in milliseconds (default: 5000)
*/
timeout?: number;
/**
* Whether to extract full headers or just essential info
*/
extractFullHeaders?: boolean;
}

View File

@@ -0,0 +1,115 @@
/**
* Interface definitions for protocol detection components
*/
import type { IDetectionResult, IDetectionOptions } from './detection-types.js';
/**
* Interface for protocol detectors
*/
export interface IProtocolDetector {
/**
* Detect protocol from buffer data
* @param buffer The buffer to analyze
* @param options Detection options
* @returns Detection result or null if protocol cannot be determined
*/
detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null;
/**
* Check if buffer potentially contains this protocol
* @param buffer The buffer to check
* @returns True if buffer might contain this protocol
*/
canHandle(buffer: Buffer): boolean;
/**
* Get the minimum bytes needed for detection
*/
getMinimumBytes(): number;
}
/**
* Interface for connection tracking during fragmented detection
*/
export interface IConnectionTracker {
/**
* Connection identifier
*/
id: string;
/**
* Accumulated buffer data
*/
buffer: Buffer;
/**
* Timestamp of first data
*/
startTime: number;
/**
* Current detection state
*/
state: 'detecting' | 'complete' | 'failed';
/**
* Partial detection result (if any)
*/
partialResult?: Partial<IDetectionResult>;
}
/**
* Interface for buffer accumulator (handles fragmented data)
*/
export interface IBufferAccumulator {
/**
* Add data to accumulator
*/
append(data: Buffer): void;
/**
* Get accumulated buffer
*/
getBuffer(): Buffer;
/**
* Get buffer length
*/
length(): number;
/**
* Clear accumulated data
*/
clear(): void;
/**
* Check if accumulator has enough data
*/
hasMinimumBytes(minBytes: number): boolean;
}
/**
* Detection events
*/
export interface IDetectionEvents {
/**
* Emitted when protocol is successfully detected
*/
detected: (result: IDetectionResult) => void;
/**
* Emitted when detection fails
*/
failed: (error: Error) => void;
/**
* Emitted when detection times out
*/
timeout: () => void;
/**
* Emitted when more data is needed
*/
needMoreData: (bytesNeeded: number) => void;
}

View File

@@ -0,0 +1,230 @@
/**
* Protocol Detector
*
* Simplified protocol detection using the new architecture
*/
import type { IDetectionResult, IDetectionOptions } from './models/detection-types.js';
import type { IConnectionContext } from '../protocols/common/types.js';
import { TlsDetector } from './detectors/tls-detector.js';
import { HttpDetector } from './detectors/http-detector.js';
import { DetectionFragmentManager } from './utils/fragment-manager.js';
/**
* Main protocol detector class
*/
export class ProtocolDetector {
private static instance: ProtocolDetector;
private fragmentManager: DetectionFragmentManager;
private tlsDetector: TlsDetector;
private httpDetector: HttpDetector;
constructor() {
this.fragmentManager = new DetectionFragmentManager();
this.tlsDetector = new TlsDetector();
this.httpDetector = new HttpDetector(this.fragmentManager);
}
private static getInstance(): ProtocolDetector {
if (!this.instance) {
this.instance = new ProtocolDetector();
}
return this.instance;
}
/**
* Detect protocol from buffer data
*/
static async detect(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> {
return this.getInstance().detectInstance(buffer, options);
}
private async detectInstance(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> {
// Quick sanity check
if (!buffer || buffer.length === 0) {
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: true
};
}
// Try TLS detection first (more specific)
if (this.tlsDetector.canHandle(buffer)) {
const tlsResult = this.tlsDetector.detect(buffer, options);
if (tlsResult) {
return tlsResult;
}
}
// Try HTTP detection
if (this.httpDetector.canHandle(buffer)) {
const httpResult = this.httpDetector.detect(buffer, options);
if (httpResult) {
return httpResult;
}
}
// Neither TLS nor HTTP
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: true
};
}
/**
* Detect protocol with connection tracking for fragmented data
* @deprecated Use detectWithContext instead
*/
static async detectWithConnectionTracking(
buffer: Buffer,
connectionId: string,
options?: IDetectionOptions
): Promise<IDetectionResult> {
// Convert connection ID to context
const context: IConnectionContext = {
id: connectionId,
sourceIp: 'unknown',
sourcePort: 0,
destIp: 'unknown',
destPort: 0,
timestamp: Date.now()
};
return this.getInstance().detectWithContextInstance(buffer, context, options);
}
/**
* Detect protocol with connection context for fragmented data
*/
static async detectWithContext(
buffer: Buffer,
context: IConnectionContext,
options?: IDetectionOptions
): Promise<IDetectionResult> {
return this.getInstance().detectWithContextInstance(buffer, context, options);
}
private async detectWithContextInstance(
buffer: Buffer,
context: IConnectionContext,
options?: IDetectionOptions
): Promise<IDetectionResult> {
// Quick sanity check
if (!buffer || buffer.length === 0) {
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: true
};
}
// First peek to determine protocol type
if (this.tlsDetector.canHandle(buffer)) {
const result = this.tlsDetector.detectWithContext(buffer, context, options);
if (result) {
return result;
}
}
if (this.httpDetector.canHandle(buffer)) {
const result = this.httpDetector.detectWithContext(buffer, context, options);
if (result) {
return result;
}
}
// Can't determine protocol
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: false,
bytesNeeded: Math.max(
this.tlsDetector.getMinimumBytes(),
this.httpDetector.getMinimumBytes()
)
};
}
/**
* Clean up resources
*/
static cleanup(): void {
this.getInstance().cleanupInstance();
}
private cleanupInstance(): void {
this.fragmentManager.cleanup();
}
/**
* Destroy detector instance
*/
static destroy(): void {
this.getInstance().destroyInstance();
this.instance = null as any;
}
private destroyInstance(): void {
this.fragmentManager.destroy();
}
/**
* Clean up old connection tracking entries
*
* @param maxAge Maximum age in milliseconds (default: 30 seconds)
*/
static cleanupConnections(maxAge: number = 30000): void {
// Cleanup is now handled internally by the fragment manager
this.getInstance().fragmentManager.cleanup();
}
/**
* Extract domain from connection info
*/
static extractDomain(connectionInfo: any): string | undefined {
return connectionInfo.domain || connectionInfo.sni || connectionInfo.host;
}
/**
* Create a connection ID from connection parameters
* @deprecated Use createConnectionContext instead
*/
static createConnectionId(params: {
sourceIp?: string;
sourcePort?: number;
destIp?: string;
destPort?: number;
socketId?: string;
}): string {
// If socketId is provided, use it
if (params.socketId) {
return params.socketId;
}
// Otherwise create from connection tuple
const { sourceIp = 'unknown', sourcePort = 0, destIp = 'unknown', destPort = 0 } = params;
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
}
/**
* Create a connection context from parameters
*/
static createConnectionContext(params: {
sourceIp?: string;
sourcePort?: number;
destIp?: string;
destPort?: number;
socketId?: string;
}): IConnectionContext {
return {
id: params.socketId,
sourceIp: params.sourceIp || 'unknown',
sourcePort: params.sourcePort || 0,
destIp: params.destIp || 'unknown',
destPort: params.destPort || 0,
timestamp: Date.now()
};
}
}

View File

@@ -0,0 +1,141 @@
/**
* Buffer manipulation utilities for protocol detection
*/
// Import from protocols
import { HttpParser } from '../../protocols/http/index.js';
/**
* BufferAccumulator class for handling fragmented data
*/
export class BufferAccumulator {
private chunks: Buffer[] = [];
private totalLength = 0;
/**
* Append data to the accumulator
*/
append(data: Buffer): void {
this.chunks.push(data);
this.totalLength += data.length;
}
/**
* Get the accumulated buffer
*/
getBuffer(): Buffer {
if (this.chunks.length === 0) {
return Buffer.alloc(0);
}
if (this.chunks.length === 1) {
return this.chunks[0];
}
return Buffer.concat(this.chunks, this.totalLength);
}
/**
* Get current buffer length
*/
length(): number {
return this.totalLength;
}
/**
* Clear all accumulated data
*/
clear(): void {
this.chunks = [];
this.totalLength = 0;
}
/**
* Check if accumulator has minimum bytes
*/
hasMinimumBytes(minBytes: number): boolean {
return this.totalLength >= minBytes;
}
}
/**
* Read a big-endian 16-bit integer from buffer
*/
export function readUInt16BE(buffer: Buffer, offset: number): number {
if (offset + 2 > buffer.length) {
throw new Error('Buffer too short for UInt16BE read');
}
return (buffer[offset] << 8) | buffer[offset + 1];
}
/**
* Read a big-endian 24-bit integer from buffer
*/
export function readUInt24BE(buffer: Buffer, offset: number): number {
if (offset + 3 > buffer.length) {
throw new Error('Buffer too short for UInt24BE read');
}
return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2];
}
/**
* Find a byte sequence in a buffer
*/
export function findSequence(buffer: Buffer, sequence: Buffer, startOffset = 0): number {
if (sequence.length === 0) {
return startOffset;
}
const searchLength = buffer.length - sequence.length + 1;
for (let i = startOffset; i < searchLength; i++) {
let found = true;
for (let j = 0; j < sequence.length; j++) {
if (buffer[i + j] !== sequence[j]) {
found = false;
break;
}
}
if (found) {
return i;
}
}
return -1;
}
/**
* Extract a line from buffer (up to CRLF or LF)
*/
export function extractLine(buffer: Buffer, startOffset = 0): { line: string; nextOffset: number } | null {
// Delegate to protocol parser
return HttpParser.extractLine(buffer, startOffset);
}
/**
* Check if buffer starts with a string (case-insensitive)
*/
export function startsWithString(buffer: Buffer, str: string, offset = 0): boolean {
if (offset + str.length > buffer.length) {
return false;
}
const bufferStr = buffer.slice(offset, offset + str.length).toString('utf8');
return bufferStr.toLowerCase() === str.toLowerCase();
}
/**
* Safe buffer slice that doesn't throw on out-of-bounds
*/
export function safeSlice(buffer: Buffer, start: number, end?: number): Buffer {
const safeStart = Math.max(0, Math.min(start, buffer.length));
const safeEnd = end === undefined
? buffer.length
: Math.max(safeStart, Math.min(end, buffer.length));
return buffer.slice(safeStart, safeEnd);
}
/**
* Check if buffer contains printable ASCII
*/
export function isPrintableAscii(buffer: Buffer, length?: number): boolean {
// Delegate to protocol parser
return HttpParser.isPrintableAscii(buffer, length);
}

View File

@@ -0,0 +1,64 @@
/**
* Fragment Manager for Detection Module
*
* Manages fragmented protocol data using the shared fragment handler
*/
import { FragmentHandler, type IFragmentOptions } from '../../protocols/common/fragment-handler.js';
import type { IConnectionContext } from '../../protocols/common/types.js';
/**
* Detection-specific fragment manager
*/
export class DetectionFragmentManager {
private tlsFragments: FragmentHandler;
private httpFragments: FragmentHandler;
constructor() {
// Configure fragment handlers with appropriate limits
const tlsOptions: IFragmentOptions = {
maxBufferSize: 16384, // TLS record max size
timeout: 5000,
cleanupInterval: 30000
};
const httpOptions: IFragmentOptions = {
maxBufferSize: 8192, // HTTP header reasonable limit
timeout: 5000,
cleanupInterval: 30000
};
this.tlsFragments = new FragmentHandler(tlsOptions);
this.httpFragments = new FragmentHandler(httpOptions);
}
/**
* Get fragment handler for protocol type
*/
getHandler(protocol: 'tls' | 'http'): FragmentHandler {
return protocol === 'tls' ? this.tlsFragments : this.httpFragments;
}
/**
* Create connection ID from context
*/
static createConnectionId(context: IConnectionContext): string {
return context.id || `${context.sourceIp}:${context.sourcePort}-${context.destIp}:${context.destPort}`;
}
/**
* Clean up all handlers
*/
cleanup(): void {
this.tlsFragments.cleanup();
this.httpFragments.cleanup();
}
/**
* Destroy all handlers
*/
destroy(): void {
this.tlsFragments.destroy();
this.httpFragments.destroy();
}
}

View File

@@ -0,0 +1,77 @@
/**
* Parser utilities for protocol detection
* Now delegates to protocol modules for actual parsing
*/
import type { THttpMethod, TTlsVersion } from '../models/detection-types.js';
import { HttpParser, HTTP_METHODS, HTTP_VERSIONS } from '../../protocols/http/index.js';
import { tlsVersionToString as protocolTlsVersionToString } from '../../protocols/tls/index.js';
// Re-export constants for backward compatibility
export { HTTP_METHODS, HTTP_VERSIONS };
/**
* Parse HTTP request line
*/
export function parseHttpRequestLine(line: string): {
method: THttpMethod;
path: string;
version: string;
} | null {
// Delegate to protocol parser
const result = HttpParser.parseRequestLine(line);
return result ? {
method: result.method as THttpMethod,
path: result.path,
version: result.version
} : null;
}
/**
* Parse HTTP header line
*/
export function parseHttpHeader(line: string): { name: string; value: string } | null {
// Delegate to protocol parser
return HttpParser.parseHeaderLine(line);
}
/**
* Parse HTTP headers from lines
*/
export function parseHttpHeaders(lines: string[]): Record<string, string> {
// Delegate to protocol parser
return HttpParser.parseHeaders(lines);
}
/**
* Convert TLS version bytes to version string
*/
export function tlsVersionToString(major: number, minor: number): TTlsVersion | null {
// Delegate to protocol parser
return protocolTlsVersionToString(major, minor) as TTlsVersion;
}
/**
* Extract domain from Host header value
*/
export function extractDomainFromHost(hostHeader: string): string {
// Delegate to protocol parser
return HttpParser.extractDomainFromHost(hostHeader);
}
/**
* Validate domain name
*/
export function isValidDomain(domain: string): boolean {
// Delegate to protocol parser
return HttpParser.isValidDomain(domain);
}
/**
* Check if string is a valid HTTP method
*/
export function isHttpMethod(str: string): str is THttpMethod {
// Delegate to protocol parser
return HttpParser.isHttpMethod(str) && (str as THttpMethod) !== undefined;
}

View File

@@ -1,76 +0,0 @@
import type * as plugins from '../../plugins.js';
/**
* The primary forwarding types supported by SmartProxy
* Used for configuration compatibility
*/
export type TForwardingType =
| 'http-only' // HTTP forwarding only (no HTTPS)
| 'https-passthrough' // Pass-through TLS traffic (SNI forwarding)
| 'https-terminate-to-http' // Terminate TLS and forward to HTTP backend
| 'https-terminate-to-https'; // Terminate TLS and forward to HTTPS backend
/**
* Event types emitted by forwarding handlers
*/
export enum ForwardingHandlerEvents {
CONNECTED = 'connected',
DISCONNECTED = 'disconnected',
ERROR = 'error',
DATA_FORWARDED = 'data-forwarded',
HTTP_REQUEST = 'http-request',
HTTP_RESPONSE = 'http-response',
CERTIFICATE_NEEDED = 'certificate-needed',
CERTIFICATE_LOADED = 'certificate-loaded'
}
/**
* Base interface for forwarding handlers
*/
export interface IForwardingHandler extends plugins.EventEmitter {
initialize(): Promise<void>;
handleConnection(socket: plugins.net.Socket): void;
handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
}
// Route-based helpers are now available directly from route-patterns.ts
import {
createHttpRoute,
createHttpsTerminateRoute,
createHttpsPassthroughRoute,
createHttpToHttpsRedirect,
createCompleteHttpsServer,
createLoadBalancerRoute
} from '../../proxies/smart-proxy/utils/route-patterns.js';
export {
createHttpRoute,
createHttpsTerminateRoute,
createHttpsPassthroughRoute,
createHttpToHttpsRedirect,
createCompleteHttpsServer,
createLoadBalancerRoute
};
// Note: Legacy helper functions have been removed
// Please use the route-based helpers instead:
// - createHttpRoute
// - createHttpsTerminateRoute
// - createHttpsPassthroughRoute
// - createHttpToHttpsRedirect
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
// For backward compatibility, kept only the basic configuration interface
export interface IForwardConfig {
type: TForwardingType;
target: {
host: string | string[];
port: number | 'preserve' | ((ctx: any) => number);
};
http?: any;
https?: any;
acme?: any;
security?: any;
advanced?: any;
[key: string]: any;
}

View File

@@ -1,26 +0,0 @@
/**
* Forwarding configuration exports
*
* Note: The legacy domain-based configuration has been replaced by route-based configuration.
* See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration.
*/
export type {
TForwardingType,
IForwardConfig,
IForwardingHandler
} from './forwarding-types.js';
export {
ForwardingHandlerEvents
} from './forwarding-types.js';
// Import route helpers from route-patterns instead of deleted route-helpers
export {
createHttpRoute,
createHttpsTerminateRoute,
createHttpsPassthroughRoute,
createHttpToHttpsRedirect,
createCompleteHttpsServer,
createLoadBalancerRoute
} from '../../proxies/smart-proxy/utils/route-patterns.js';

View File

@@ -1,189 +0,0 @@
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandler } from '../handlers/base-handler.js';
import { HttpForwardingHandler } from '../handlers/http-handler.js';
import { HttpsPassthroughHandler } from '../handlers/https-passthrough-handler.js';
import { HttpsTerminateToHttpHandler } from '../handlers/https-terminate-to-http-handler.js';
import { HttpsTerminateToHttpsHandler } from '../handlers/https-terminate-to-https-handler.js';
/**
* Factory for creating forwarding handlers based on the configuration type
*/
export class ForwardingHandlerFactory {
/**
* Create a forwarding handler based on the configuration
* @param config The forwarding configuration
* @returns The appropriate forwarding handler
*/
public static createHandler(config: IForwardConfig): ForwardingHandler {
// Create the appropriate handler based on the forwarding type
switch (config.type) {
case 'http-only':
return new HttpForwardingHandler(config);
case 'https-passthrough':
return new HttpsPassthroughHandler(config);
case 'https-terminate-to-http':
return new HttpsTerminateToHttpHandler(config);
case 'https-terminate-to-https':
return new HttpsTerminateToHttpsHandler(config);
default:
// Type system should prevent this, but just in case:
throw new Error(`Unknown forwarding type: ${(config as any).type}`);
}
}
/**
* Apply default values to a forwarding configuration based on its type
* @param config The original forwarding configuration
* @returns A configuration with defaults applied
*/
public static applyDefaults(config: IForwardConfig): IForwardConfig {
// Create a deep copy of the configuration
const result: IForwardConfig = JSON.parse(JSON.stringify(config));
// Apply defaults based on forwarding type
switch (config.type) {
case 'http-only':
// Set defaults for HTTP-only mode
result.http = {
enabled: true,
...config.http
};
// Set default port and socket if not provided
if (!result.port) {
result.port = 80;
}
if (!result.socket) {
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
}
break;
case 'https-passthrough':
// Set defaults for HTTPS passthrough
result.https = {
forwardSni: true,
...config.https
};
// SNI forwarding doesn't do HTTP
result.http = {
enabled: false,
...config.http
};
// Set default port and socket if not provided
if (!result.port) {
result.port = 443;
}
if (!result.socket) {
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
}
break;
case 'https-terminate-to-http':
// Set defaults for HTTPS termination to HTTP
result.https = {
...config.https
};
// Support HTTP access by default in this mode
result.http = {
enabled: true,
redirectToHttps: true,
...config.http
};
// Enable ACME by default
result.acme = {
enabled: true,
maintenance: true,
...config.acme
};
// Set default port and socket if not provided
if (!result.port) {
result.port = 443;
}
if (!result.socket) {
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
}
break;
case 'https-terminate-to-https':
// Similar to terminate-to-http but with different target handling
result.https = {
...config.https
};
result.http = {
enabled: true,
redirectToHttps: true,
...config.http
};
result.acme = {
enabled: true,
maintenance: true,
...config.acme
};
// Set default port and socket if not provided
if (!result.port) {
result.port = 443;
}
if (!result.socket) {
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
}
break;
}
return result;
}
/**
* Validate a forwarding configuration
* @param config The configuration to validate
* @throws Error if the configuration is invalid
*/
public static validateConfig(config: IForwardConfig): void {
// Validate common properties
if (!config.target) {
throw new Error('Forwarding configuration must include a target');
}
if (!config.target.host || (Array.isArray(config.target.host) && config.target.host.length === 0)) {
throw new Error('Target must include a host or array of hosts');
}
// Validate port if it's a number
if (typeof config.target.port === 'number') {
if (config.target.port <= 0 || config.target.port > 65535) {
throw new Error('Target must include a valid port (1-65535)');
}
} else if (config.target.port !== 'preserve' && typeof config.target.port !== 'function') {
throw new Error('Target port must be a number, "preserve", or a function');
}
// Type-specific validation
switch (config.type) {
case 'http-only':
// HTTP-only needs http.enabled to be true
if (config.http?.enabled === false) {
throw new Error('HTTP-only forwarding must have HTTP enabled');
}
break;
case 'https-passthrough':
// HTTPS passthrough doesn't support HTTP
if (config.http?.enabled === true) {
throw new Error('HTTPS passthrough does not support HTTP');
}
// HTTPS passthrough doesn't work with ACME
if (config.acme?.enabled === true) {
throw new Error('HTTPS passthrough does not support ACME');
}
break;
case 'https-terminate-to-http':
case 'https-terminate-to-https':
// These modes support all options, nothing specific to validate
break;
}
}
}

View File

@@ -1,5 +0,0 @@
/**
* Forwarding factory implementations
*/
export { ForwardingHandlerFactory } from './forwarding-factory.js';

View File

@@ -1,155 +0,0 @@
import * as plugins from '../../plugins.js';
import type {
IForwardConfig,
IForwardingHandler
} from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
/**
* Base class for all forwarding handlers
*/
export abstract class ForwardingHandler extends plugins.EventEmitter implements IForwardingHandler {
/**
* Create a new ForwardingHandler
* @param config The forwarding configuration
*/
constructor(protected config: IForwardConfig) {
super();
}
/**
* Initialize the handler
* Base implementation does nothing, subclasses should override as needed
*/
public async initialize(): Promise<void> {
// Base implementation - no initialization needed
}
/**
* Handle a new socket connection
* @param socket The incoming socket connection
*/
public abstract handleConnection(socket: plugins.net.Socket): void;
/**
* Handle an HTTP request
* @param req The HTTP request
* @param res The HTTP response
*/
public abstract handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
/**
* Get a target from the configuration, supporting round-robin selection
* @param incomingPort Optional incoming port for 'preserve' mode
* @returns A resolved target object with host and port
*/
protected getTargetFromConfig(incomingPort: number = 80): { host: string, port: number } {
const { target } = this.config;
// Handle round-robin host selection
if (Array.isArray(target.host)) {
if (target.host.length === 0) {
throw new Error('No target hosts specified');
}
// Simple round-robin selection
const randomIndex = Math.floor(Math.random() * target.host.length);
return {
host: target.host[randomIndex],
port: this.resolvePort(target.port, incomingPort)
};
}
// Single host
return {
host: target.host,
port: this.resolvePort(target.port, incomingPort)
};
}
/**
* Resolves a port value, handling 'preserve' and function ports
* @param port The port value to resolve
* @param incomingPort Optional incoming port to use for 'preserve' mode
*/
protected resolvePort(
port: number | 'preserve' | ((ctx: any) => number),
incomingPort: number = 80
): number {
if (typeof port === 'function') {
try {
// Create a minimal context for the function that includes the incoming port
const ctx = { port: incomingPort };
return port(ctx);
} catch (err) {
console.error('Error resolving port function:', err);
return incomingPort; // Fall back to incoming port
}
} else if (port === 'preserve') {
return incomingPort; // Use the actual incoming port for 'preserve'
} else {
return port;
}
}
/**
* Redirect an HTTP request to HTTPS
* @param req The HTTP request
* @param res The HTTP response
*/
protected redirectToHttps(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
const host = req.headers.host || '';
const path = req.url || '/';
const redirectUrl = `https://${host}${path}`;
res.writeHead(301, {
'Location': redirectUrl,
'Cache-Control': 'no-cache'
});
res.end(`Redirecting to ${redirectUrl}`);
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
statusCode: 301,
headers: { 'Location': redirectUrl },
size: 0
});
}
/**
* Apply custom headers from configuration
* @param headers The original headers
* @param variables Variables to replace in the headers
* @returns The headers with custom values applied
*/
protected applyCustomHeaders(
headers: Record<string, string | string[] | undefined>,
variables: Record<string, string>
): Record<string, string | string[] | undefined> {
const customHeaders = this.config.advanced?.headers || {};
const result = { ...headers };
// Apply custom headers with variable substitution
for (const [key, value] of Object.entries(customHeaders)) {
if (typeof value !== 'string') continue;
let processedValue = value;
// Replace variables in the header value
for (const [varName, varValue] of Object.entries(variables)) {
processedValue = processedValue.replace(`{${varName}}`, varValue);
}
result[key] = processedValue;
}
return result;
}
/**
* Get the timeout for this connection from configuration
* @returns Timeout in milliseconds
*/
protected getTimeout(): number {
return this.config.advanced?.timeout || 60000; // Default: 60 seconds
}
}

View File

@@ -1,163 +0,0 @@
import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { setupSocketHandlers } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTP-only forwarding
*/
export class HttpForwardingHandler extends ForwardingHandler {
/**
* Create a new HTTP forwarding handler
* @param config The forwarding configuration
*/
constructor(config: IForwardConfig) {
super(config);
// Validate that this is an HTTP-only configuration
if (config.type !== 'http-only') {
throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`);
}
}
/**
* Initialize the handler
* HTTP handler doesn't need special initialization
*/
public async initialize(): Promise<void> {
// Basic initialization from parent class
await super.initialize();
}
/**
* Handle a raw socket connection
* HTTP handler doesn't do much with raw sockets as it mainly processes
* parsed HTTP requests
*/
public handleConnection(socket: plugins.net.Socket): void {
// For HTTP, we mainly handle parsed requests, but we can still set up
// some basic connection tracking
const remoteAddress = socket.remoteAddress || 'unknown';
const localPort = socket.localPort || 80;
// Set up socket handlers with proper cleanup
const handleClose = (reason: string) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
};
// Use custom timeout handler that doesn't close the socket
setupSocketHandlers(socket, handleClose, () => {
// For HTTP, we can be more aggressive with timeouts since connections are shorter
// But still don't close immediately - let the connection finish naturally
console.warn(`HTTP socket timeout from ${remoteAddress}`);
}, 'http');
socket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: error.message
});
});
this.emit(ForwardingHandlerEvents.CONNECTED, {
remoteAddress,
localPort
});
}
/**
* Handle an HTTP request
* @param req The HTTP request
* @param res The HTTP response
*/
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
// Get the local port from the request (for 'preserve' port handling)
const localPort = req.socket.localPort || 80;
// Get the target from configuration, passing the incoming port
const target = this.getTargetFromConfig(localPort);
// Create a custom headers object with variables for substitution
const variables = {
clientIp: req.socket.remoteAddress || 'unknown'
};
// Prepare headers, merging with any custom headers from config
const headers = this.applyCustomHeaders(req.headers, variables);
// Create the proxy request options
const options = {
hostname: target.host,
port: target.port,
path: req.url,
method: req.method,
headers
};
// Create the proxy request
const proxyReq = plugins.http.request(options, (proxyRes) => {
// Copy status code and headers from the proxied response
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
// Pipe the proxy response to the client response
proxyRes.pipe(res);
// Track bytes for logging
let responseSize = 0;
proxyRes.on('data', (chunk) => {
responseSize += chunk.length;
});
proxyRes.on('end', () => {
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
statusCode: proxyRes.statusCode,
headers: proxyRes.headers,
size: responseSize
});
});
});
// Handle errors in the proxy request
proxyReq.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress: req.socket.remoteAddress,
error: `Proxy request error: ${error.message}`
});
// Send an error response if headers haven't been sent yet
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end(`Error forwarding request: ${error.message}`);
} else {
// Just end the response if headers have already been sent
res.end();
}
});
// Track request details for logging
let requestSize = 0;
req.on('data', (chunk) => {
requestSize += chunk.length;
});
// Log the request
this.emit(ForwardingHandlerEvents.HTTP_REQUEST, {
method: req.method,
url: req.url,
headers: req.headers,
remoteAddress: req.socket.remoteAddress,
target: `${target.host}:${target.port}`
});
// Pipe the client request to the proxy request
if (req.readable) {
req.pipe(proxyReq);
} else {
proxyReq.end();
}
}
}

View File

@@ -1,185 +0,0 @@
import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTPS passthrough (SNI forwarding without termination)
*/
export class HttpsPassthroughHandler extends ForwardingHandler {
/**
* Create a new HTTPS passthrough handler
* @param config The forwarding configuration
*/
constructor(config: IForwardConfig) {
super(config);
// Validate that this is an HTTPS passthrough configuration
if (config.type !== 'https-passthrough') {
throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`);
}
}
/**
* Initialize the handler
* HTTPS passthrough handler doesn't need special initialization
*/
public async initialize(): Promise<void> {
// Basic initialization from parent class
await super.initialize();
}
/**
* Handle a TLS/SSL socket connection by forwarding it without termination
* @param clientSocket The incoming socket from the client
*/
public handleConnection(clientSocket: plugins.net.Socket): void {
// Get the target from configuration
const target = this.getTargetFromConfig();
// Log the connection
const remoteAddress = clientSocket.remoteAddress || 'unknown';
const remotePort = clientSocket.remotePort || 0;
this.emit(ForwardingHandlerEvents.CONNECTED, {
remoteAddress,
remotePort,
target: `${target.host}:${target.port}`
});
// Track data transfer for logging
let bytesSent = 0;
let bytesReceived = 0;
let serverSocket: plugins.net.Socket | null = null;
let cleanupClient: ((reason: string) => Promise<void>) | null = null;
let cleanupServer: ((reason: string) => Promise<void>) | null = null;
// Create a connection to the target server with immediate error handling
serverSocket = createSocketWithErrorHandler({
port: target.port,
host: target.host,
onError: async (error) => {
// Server connection failed - clean up client socket immediately
this.emit(ForwardingHandlerEvents.ERROR, {
error: error.message,
code: (error as any).code || 'UNKNOWN',
remoteAddress,
target: `${target.host}:${target.port}`
});
// Clean up the client socket since we can't forward
if (!clientSocket.destroyed) {
clientSocket.destroy();
}
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
bytesSent: 0,
bytesReceived: 0,
reason: `server_connection_failed: ${error.message}`
});
},
onConnect: () => {
// Connection successful - set up forwarding handlers
const handlers = createIndependentSocketHandlers(
clientSocket,
serverSocket!,
(reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
bytesSent,
bytesReceived,
reason
});
}
);
cleanupClient = handlers.cleanupClient;
cleanupServer = handlers.cleanupServer;
// Setup handlers with custom timeout handling that doesn't close connections
const timeout = this.getTimeout();
setupSocketHandlers(clientSocket, cleanupClient, (socket) => {
// Just reset timeout, don't close
socket.setTimeout(timeout);
}, 'client');
setupSocketHandlers(serverSocket!, cleanupServer, (socket) => {
// Just reset timeout, don't close
socket.setTimeout(timeout);
}, 'server');
// Forward data from client to server
clientSocket.on('data', (data) => {
bytesSent += data.length;
// Check if server socket is writable
if (serverSocket && serverSocket.writable) {
const flushed = serverSocket.write(data);
// Handle backpressure
if (!flushed) {
clientSocket.pause();
serverSocket.once('drain', () => {
clientSocket.resume();
});
}
}
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
direction: 'outbound',
bytes: data.length,
total: bytesSent
});
});
// Forward data from server to client
serverSocket!.on('data', (data) => {
bytesReceived += data.length;
// Check if client socket is writable
if (clientSocket.writable) {
const flushed = clientSocket.write(data);
// Handle backpressure
if (!flushed) {
serverSocket!.pause();
clientSocket.once('drain', () => {
serverSocket!.resume();
});
}
}
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
direction: 'inbound',
bytes: data.length,
total: bytesReceived
});
});
// Set initial timeouts - they will be reset on each timeout event
clientSocket.setTimeout(timeout);
serverSocket!.setTimeout(timeout);
}
});
}
/**
* Handle an HTTP request - HTTPS passthrough doesn't support HTTP
* @param req The HTTP request
* @param res The HTTP response
*/
public handleHttpRequest(_req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
// HTTPS passthrough doesn't support HTTP requests
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('HTTP not supported for this domain');
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
statusCode: 404,
headers: { 'Content-Type': 'text/plain' },
size: 'HTTP not supported for this domain'.length
});
}
}

View File

@@ -1,312 +0,0 @@
import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTPS termination with HTTP backend
*/
export class HttpsTerminateToHttpHandler extends ForwardingHandler {
private tlsServer: plugins.tls.Server | null = null;
private secureContext: plugins.tls.SecureContext | null = null;
/**
* Create a new HTTPS termination with HTTP backend handler
* @param config The forwarding configuration
*/
constructor(config: IForwardConfig) {
super(config);
// Validate that this is an HTTPS terminate to HTTP configuration
if (config.type !== 'https-terminate-to-http') {
throw new Error(`Invalid configuration type for HttpsTerminateToHttpHandler: ${config.type}`);
}
}
/**
* Initialize the handler, setting up TLS context
*/
public async initialize(): Promise<void> {
// We need to load or create TLS certificates
if (this.config.https?.customCert) {
// Use custom certificate from configuration
this.secureContext = plugins.tls.createSecureContext({
key: this.config.https.customCert.key,
cert: this.config.https.customCert.cert
});
this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, {
source: 'config',
domain: this.config.target.host
});
} else if (this.config.acme?.enabled) {
// Request certificate through ACME if needed
this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, {
domain: Array.isArray(this.config.target.host)
? this.config.target.host[0]
: this.config.target.host,
useProduction: this.config.acme.production || false
});
// In a real implementation, we would wait for the certificate to be issued
// For now, we'll use a dummy context
this.secureContext = plugins.tls.createSecureContext({
key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----',
cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----'
});
} else {
throw new Error('HTTPS termination requires either a custom certificate or ACME enabled');
}
}
/**
* Set the secure context for TLS termination
* Called when a certificate is available
* @param context The secure context
*/
public setSecureContext(context: plugins.tls.SecureContext): void {
this.secureContext = context;
}
/**
* Handle a TLS/SSL socket connection by terminating TLS and forwarding to HTTP backend
* @param clientSocket The incoming socket from the client
*/
public handleConnection(clientSocket: plugins.net.Socket): void {
// Make sure we have a secure context
if (!this.secureContext) {
clientSocket.destroy(new Error('TLS secure context not initialized'));
return;
}
const remoteAddress = clientSocket.remoteAddress || 'unknown';
const remotePort = clientSocket.remotePort || 0;
// Create a TLS socket using our secure context
const tlsSocket = new plugins.tls.TLSSocket(clientSocket, {
secureContext: this.secureContext,
isServer: true,
server: this.tlsServer || undefined
});
this.emit(ForwardingHandlerEvents.CONNECTED, {
remoteAddress,
remotePort,
tls: true
});
// Variables to track connections
let backendSocket: plugins.net.Socket | null = null;
let dataBuffer = Buffer.alloc(0);
let connectionEstablished = false;
let forwardingSetup = false;
// Set up initial error handling for TLS socket
const tlsCleanupHandler = (reason: string) => {
if (!forwardingSetup) {
// If forwarding not set up yet, emit disconnected and cleanup
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
dataBuffer = Buffer.alloc(0);
connectionEstablished = false;
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
if (backendSocket && !backendSocket.destroyed) {
backendSocket.destroy();
}
}
// If forwarding is setup, setupBidirectionalForwarding will handle cleanup
};
setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls');
// Set timeout
const timeout = this.getTimeout();
tlsSocket.setTimeout(timeout);
tlsSocket.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'TLS connection timeout'
});
tlsCleanupHandler('timeout');
});
// Handle TLS data
tlsSocket.on('data', (data) => {
// If backend connection already established, just forward the data
if (connectionEstablished && backendSocket && !backendSocket.destroyed) {
backendSocket.write(data);
return;
}
// Append to buffer
dataBuffer = Buffer.concat([dataBuffer, data]);
// Very basic HTTP parsing - in a real implementation, use http-parser
if (dataBuffer.includes(Buffer.from('\r\n\r\n')) && !connectionEstablished) {
const target = this.getTargetFromConfig();
// Create backend connection with immediate error handling
backendSocket = createSocketWithErrorHandler({
port: target.port,
host: target.host,
onError: (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
error: error.message,
code: (error as any).code || 'UNKNOWN',
remoteAddress,
target: `${target.host}:${target.port}`
});
// Clean up the TLS socket since we can't forward
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason: `backend_connection_failed: ${error.message}`
});
},
onConnect: () => {
connectionEstablished = true;
// Send buffered data
if (dataBuffer.length > 0) {
backendSocket!.write(dataBuffer);
dataBuffer = Buffer.alloc(0);
}
// Now set up bidirectional forwarding with proper cleanup
forwardingSetup = true;
setupBidirectionalForwarding(tlsSocket, backendSocket!, {
onCleanup: (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
dataBuffer = Buffer.alloc(0);
connectionEstablished = false;
forwardingSetup = false;
},
enableHalfOpen: false // Close both when one closes
});
}
});
// Additional error logging for backend socket
backendSocket.on('error', (error) => {
if (!connectionEstablished) {
// Connection failed during setup
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Target connection error: ${error.message}`
});
}
// If connected, setupBidirectionalForwarding handles cleanup
});
}
});
}
/**
* Handle an HTTP request by forwarding to the HTTP backend
* @param req The HTTP request
* @param res The HTTP response
*/
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
// Check if we should redirect to HTTPS
if (this.config.http?.redirectToHttps) {
this.redirectToHttps(req, res);
return;
}
// Get the target from configuration
const target = this.getTargetFromConfig();
// Create custom headers with variable substitution
const variables = {
clientIp: req.socket.remoteAddress || 'unknown'
};
// Prepare headers, merging with any custom headers from config
const headers = this.applyCustomHeaders(req.headers, variables);
// Create the proxy request options
const options = {
hostname: target.host,
port: target.port,
path: req.url,
method: req.method,
headers
};
// Create the proxy request
const proxyReq = plugins.http.request(options, (proxyRes) => {
// Copy status code and headers from the proxied response
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
// Pipe the proxy response to the client response
proxyRes.pipe(res);
// Track response size for logging
let responseSize = 0;
proxyRes.on('data', (chunk) => {
responseSize += chunk.length;
});
proxyRes.on('end', () => {
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
statusCode: proxyRes.statusCode,
headers: proxyRes.headers,
size: responseSize
});
});
});
// Handle errors in the proxy request
proxyReq.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress: req.socket.remoteAddress,
error: `Proxy request error: ${error.message}`
});
// Send an error response if headers haven't been sent yet
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end(`Error forwarding request: ${error.message}`);
} else {
// Just end the response if headers have already been sent
res.end();
}
});
// Track request details for logging
let requestSize = 0;
req.on('data', (chunk) => {
requestSize += chunk.length;
});
// Log the request
this.emit(ForwardingHandlerEvents.HTTP_REQUEST, {
method: req.method,
url: req.url,
headers: req.headers,
remoteAddress: req.socket.remoteAddress,
target: `${target.host}:${target.port}`
});
// Pipe the client request to the proxy request
if (req.readable) {
req.pipe(proxyReq);
} else {
proxyReq.end();
}
}
}

View File

@@ -1,297 +0,0 @@
import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTPS termination with HTTPS backend
*/
export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
private secureContext: plugins.tls.SecureContext | null = null;
/**
* Create a new HTTPS termination with HTTPS backend handler
* @param config The forwarding configuration
*/
constructor(config: IForwardConfig) {
super(config);
// Validate that this is an HTTPS terminate to HTTPS configuration
if (config.type !== 'https-terminate-to-https') {
throw new Error(`Invalid configuration type for HttpsTerminateToHttpsHandler: ${config.type}`);
}
}
/**
* Initialize the handler, setting up TLS context
*/
public async initialize(): Promise<void> {
// We need to load or create TLS certificates for termination
if (this.config.https?.customCert) {
// Use custom certificate from configuration
this.secureContext = plugins.tls.createSecureContext({
key: this.config.https.customCert.key,
cert: this.config.https.customCert.cert
});
this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, {
source: 'config',
domain: this.config.target.host
});
} else if (this.config.acme?.enabled) {
// Request certificate through ACME if needed
this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, {
domain: Array.isArray(this.config.target.host)
? this.config.target.host[0]
: this.config.target.host,
useProduction: this.config.acme.production || false
});
// In a real implementation, we would wait for the certificate to be issued
// For now, we'll use a dummy context
this.secureContext = plugins.tls.createSecureContext({
key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----',
cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----'
});
} else {
throw new Error('HTTPS termination requires either a custom certificate or ACME enabled');
}
}
/**
* Set the secure context for TLS termination
* Called when a certificate is available
* @param context The secure context
*/
public setSecureContext(context: plugins.tls.SecureContext): void {
this.secureContext = context;
}
/**
* Handle a TLS/SSL socket connection by terminating TLS and creating a new TLS connection to backend
* @param clientSocket The incoming socket from the client
*/
public handleConnection(clientSocket: plugins.net.Socket): void {
// Make sure we have a secure context
if (!this.secureContext) {
clientSocket.destroy(new Error('TLS secure context not initialized'));
return;
}
const remoteAddress = clientSocket.remoteAddress || 'unknown';
const remotePort = clientSocket.remotePort || 0;
// Create a TLS socket using our secure context
const tlsSocket = new plugins.tls.TLSSocket(clientSocket, {
secureContext: this.secureContext,
isServer: true
});
this.emit(ForwardingHandlerEvents.CONNECTED, {
remoteAddress,
remotePort,
tls: true
});
// Variable to track backend socket
let backendSocket: plugins.tls.TLSSocket | null = null;
let isConnectedToBackend = false;
// Set up initial error handling for TLS socket
const tlsCleanupHandler = (reason: string) => {
if (!isConnectedToBackend) {
// If backend not connected yet, just emit disconnected event
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
// Cleanup TLS socket if needed
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
}
// If connected to backend, setupBidirectionalForwarding will handle cleanup
};
setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls');
// Set timeout
const timeout = this.getTimeout();
tlsSocket.setTimeout(timeout);
tlsSocket.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'TLS connection timeout'
});
tlsCleanupHandler('timeout');
});
// Get the target from configuration
const target = this.getTargetFromConfig();
// Set up the connection to the HTTPS backend
const connectToBackend = () => {
backendSocket = plugins.tls.connect({
host: target.host,
port: target.port,
// In a real implementation, we would configure TLS options
rejectUnauthorized: false // For testing only, never use in production
}, () => {
isConnectedToBackend = true;
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
direction: 'outbound',
target: `${target.host}:${target.port}`,
tls: true
});
// Set up bidirectional forwarding with proper cleanup
setupBidirectionalForwarding(tlsSocket, backendSocket!, {
onCleanup: (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
},
enableHalfOpen: false // Close both when one closes
});
// Set timeout for backend socket
backendSocket!.setTimeout(timeout);
backendSocket!.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'Backend connection timeout'
});
// Let setupBidirectionalForwarding handle the cleanup
});
});
// Handle backend connection errors
backendSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Backend connection error: ${error.message}`
});
if (!isConnectedToBackend) {
// Connection failed, clean up TLS socket
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason: `backend_connection_failed: ${error.message}`
});
}
// If connected, let setupBidirectionalForwarding handle cleanup
});
};
// Wait for the TLS handshake to complete before connecting to backend
tlsSocket.on('secure', () => {
connectToBackend();
});
}
/**
* Handle an HTTP request by forwarding to the HTTPS backend
* @param req The HTTP request
* @param res The HTTP response
*/
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
// Check if we should redirect to HTTPS
if (this.config.http?.redirectToHttps) {
this.redirectToHttps(req, res);
return;
}
// Get the target from configuration
const target = this.getTargetFromConfig();
// Create custom headers with variable substitution
const variables = {
clientIp: req.socket.remoteAddress || 'unknown'
};
// Prepare headers, merging with any custom headers from config
const headers = this.applyCustomHeaders(req.headers, variables);
// Create the proxy request options
const options = {
hostname: target.host,
port: target.port,
path: req.url,
method: req.method,
headers,
// In a real implementation, we would configure TLS options
rejectUnauthorized: false // For testing only, never use in production
};
// Create the proxy request using HTTPS
const proxyReq = plugins.https.request(options, (proxyRes) => {
// Copy status code and headers from the proxied response
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
// Pipe the proxy response to the client response
proxyRes.pipe(res);
// Track response size for logging
let responseSize = 0;
proxyRes.on('data', (chunk) => {
responseSize += chunk.length;
});
proxyRes.on('end', () => {
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
statusCode: proxyRes.statusCode,
headers: proxyRes.headers,
size: responseSize
});
});
});
// Handle errors in the proxy request
proxyReq.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress: req.socket.remoteAddress,
error: `Proxy request error: ${error.message}`
});
// Send an error response if headers haven't been sent yet
if (!res.headersSent) {
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end(`Error forwarding request: ${error.message}`);
} else {
// Just end the response if headers have already been sent
res.end();
}
});
// Track request details for logging
let requestSize = 0;
req.on('data', (chunk) => {
requestSize += chunk.length;
});
// Log the request
this.emit(ForwardingHandlerEvents.HTTP_REQUEST, {
method: req.method,
url: req.url,
headers: req.headers,
remoteAddress: req.socket.remoteAddress,
target: `${target.host}:${target.port}`
});
// Pipe the client request to the proxy request
if (req.readable) {
req.pipe(proxyReq);
} else {
proxyReq.end();
}
}
}

View File

@@ -1,9 +0,0 @@
/**
* Forwarding handler implementations
*/
export { ForwardingHandler } from './base-handler.js';
export { HttpForwardingHandler } from './http-handler.js';
export { HttpsPassthroughHandler } from './https-passthrough-handler.js';
export { HttpsTerminateToHttpHandler } from './https-terminate-to-http-handler.js';
export { HttpsTerminateToHttpsHandler } from './https-terminate-to-https-handler.js';

View File

@@ -1,35 +0,0 @@
/**
* Forwarding system module
* Provides a flexible and type-safe way to configure and manage various forwarding strategies
*/
// Export handlers
export { ForwardingHandler } from './handlers/base-handler.js';
export * from './handlers/http-handler.js';
export * from './handlers/https-passthrough-handler.js';
export * from './handlers/https-terminate-to-http-handler.js';
export * from './handlers/https-terminate-to-https-handler.js';
// Export factory
export * from './factory/forwarding-factory.js';
// Export types - these include TForwardingType and IForwardConfig
export type {
TForwardingType,
IForwardConfig,
IForwardingHandler
} from './config/forwarding-types.js';
export {
ForwardingHandlerEvents
} from './config/forwarding-types.js';
// Export route helpers directly from route-patterns
export {
createHttpRoute,
createHttpsTerminateRoute,
createHttpsPassthroughRoute,
createHttpToHttpsRedirect,
createCompleteHttpsServer,
createLoadBalancerRoute
} from '../proxies/smart-proxy/utils/route-patterns.js';

View File

@@ -32,7 +32,8 @@ export * from './core/models/common-types.js';
export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js'; export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js';
// Modular exports for new architecture // Modular exports for new architecture
export * as forwarding from './forwarding/index.js';
// Certificate module has been removed - use SmartCertManager instead // Certificate module has been removed - use SmartCertManager instead
export * as tls from './tls/index.js'; export * as tls from './tls/index.js';
export * as routing from './routing/index.js'; export * as routing from './routing/index.js';
export * as detection from './detection/index.js';
export * as protocols from './protocols/index.js';

View File

@@ -0,0 +1,163 @@
/**
* Shared Fragment Handler for Protocol Detection
*
* Provides unified fragment buffering and reassembly for protocols
* that may span multiple TCP packets.
*/
import { Buffer } from 'buffer';
/**
* Fragment tracking information
*/
export interface IFragmentInfo {
buffer: Buffer;
timestamp: number;
connectionId: string;
}
/**
* Options for fragment handling
*/
export interface IFragmentOptions {
maxBufferSize?: number;
timeout?: number;
cleanupInterval?: number;
}
/**
* Result of fragment processing
*/
export interface IFragmentResult {
isComplete: boolean;
buffer?: Buffer;
needsMoreData: boolean;
error?: string;
}
/**
* Shared fragment handler for protocol detection
*/
export class FragmentHandler {
private fragments = new Map<string, IFragmentInfo>();
private cleanupTimer?: NodeJS.Timeout;
constructor(private options: IFragmentOptions = {}) {
// Start cleanup timer if not already running
if (options.cleanupInterval && !this.cleanupTimer) {
this.cleanupTimer = setInterval(
() => this.cleanup(),
options.cleanupInterval
);
}
}
/**
* Add a fragment for a connection
*/
addFragment(connectionId: string, fragment: Buffer): IFragmentResult {
const existing = this.fragments.get(connectionId);
if (existing) {
// Append to existing buffer
const newBuffer = Buffer.concat([existing.buffer, fragment]);
// Check size limit
const maxSize = this.options.maxBufferSize || 65536;
if (newBuffer.length > maxSize) {
this.fragments.delete(connectionId);
return {
isComplete: false,
needsMoreData: false,
error: 'Buffer size exceeded maximum allowed'
};
}
// Update fragment info
this.fragments.set(connectionId, {
buffer: newBuffer,
timestamp: Date.now(),
connectionId
});
return {
isComplete: false,
buffer: newBuffer,
needsMoreData: true
};
} else {
// New fragment
this.fragments.set(connectionId, {
buffer: fragment,
timestamp: Date.now(),
connectionId
});
return {
isComplete: false,
buffer: fragment,
needsMoreData: true
};
}
}
/**
* Get the current buffer for a connection
*/
getBuffer(connectionId: string): Buffer | undefined {
return this.fragments.get(connectionId)?.buffer;
}
/**
* Mark a connection as complete and clean up
*/
complete(connectionId: string): void {
this.fragments.delete(connectionId);
}
/**
* Check if we're tracking a connection
*/
hasConnection(connectionId: string): boolean {
return this.fragments.has(connectionId);
}
/**
* Clean up expired fragments
*/
cleanup(): void {
const now = Date.now();
const timeout = this.options.timeout || 5000;
for (const [connectionId, info] of this.fragments.entries()) {
if (now - info.timestamp > timeout) {
this.fragments.delete(connectionId);
}
}
}
/**
* Clear all fragments
*/
clear(): void {
this.fragments.clear();
}
/**
* Destroy the handler and clean up resources
*/
destroy(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
this.clear();
}
/**
* Get the number of tracked connections
*/
get size(): number {
return this.fragments.size;
}
}

View File

@@ -0,0 +1,8 @@
/**
* Common Protocol Infrastructure
*
* Shared utilities and types for protocol handling
*/
export * from './fragment-handler.js';
export * from './types.js';

View File

@@ -0,0 +1,76 @@
/**
* Common Protocol Types
*
* Shared types used across different protocol implementations
*/
/**
* Supported protocol types
*/
export type TProtocolType = 'tls' | 'http' | 'https' | 'websocket' | 'unknown';
/**
* Protocol detection result
*/
export interface IProtocolDetectionResult {
protocol: TProtocolType;
confidence: number; // 0-100
requiresMoreData?: boolean;
metadata?: {
version?: string;
method?: string;
[key: string]: any;
};
}
/**
* Routing information extracted from protocols
*/
export interface IRoutingInfo {
domain?: string;
port?: number;
path?: string;
protocol: TProtocolType;
}
/**
* Connection context for protocol operations
*/
export interface IConnectionContext {
id: string;
sourceIp?: string;
sourcePort?: number;
destIp?: string;
destPort?: number;
timestamp?: number;
}
/**
* Protocol detection options
*/
export interface IProtocolDetectionOptions {
quickMode?: boolean; // Only do minimal detection
extractRouting?: boolean; // Extract routing information
maxWaitTime?: number; // Max time to wait for complete data
maxBufferSize?: number; // Max buffer size for fragmented data
}
/**
* Base interface for protocol detectors
*/
export interface IProtocolDetector {
/**
* Check if this detector can handle the data
*/
canHandle(data: Buffer): boolean;
/**
* Perform quick detection (first few bytes only)
*/
quickDetect(data: Buffer): IProtocolDetectionResult;
/**
* Extract routing information if possible
*/
extractRouting?(data: Buffer, context?: IConnectionContext): IRoutingInfo | null;
}

View File

@@ -0,0 +1,219 @@
/**
* HTTP Protocol Constants
*/
/**
* HTTP methods
*/
export const HTTP_METHODS = [
'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE'
] as const;
export type THttpMethod = typeof HTTP_METHODS[number];
/**
* HTTP version strings
*/
export const HTTP_VERSIONS = ['HTTP/1.0', 'HTTP/1.1', 'HTTP/2', 'HTTP/3'] as const;
export type THttpVersion = typeof HTTP_VERSIONS[number];
/**
* HTTP status codes
*/
export enum HttpStatus {
// 1xx Informational
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLY_HINTS = 103,
// 2xx Success
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
ALREADY_REPORTED = 208,
IM_USED = 226,
// 3xx Redirection
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
USE_PROXY = 305,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
// 4xx Client Error
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
PAYLOAD_TOO_LARGE = 413,
URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
IM_A_TEAPOT = 418,
MISDIRECTED_REQUEST = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
TOO_EARLY = 425,
UPGRADE_REQUIRED = 426,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
// 5xx Server Error
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
VARIANT_ALSO_NEGOTIATES = 506,
INSUFFICIENT_STORAGE = 507,
LOOP_DETECTED = 508,
NOT_EXTENDED = 510,
NETWORK_AUTHENTICATION_REQUIRED = 511,
}
/**
* HTTP status text mapping
*/
export const HTTP_STATUS_TEXT: Record<HttpStatus, string> = {
// 1xx
[HttpStatus.CONTINUE]: 'Continue',
[HttpStatus.SWITCHING_PROTOCOLS]: 'Switching Protocols',
[HttpStatus.PROCESSING]: 'Processing',
[HttpStatus.EARLY_HINTS]: 'Early Hints',
// 2xx
[HttpStatus.OK]: 'OK',
[HttpStatus.CREATED]: 'Created',
[HttpStatus.ACCEPTED]: 'Accepted',
[HttpStatus.NON_AUTHORITATIVE_INFORMATION]: 'Non-Authoritative Information',
[HttpStatus.NO_CONTENT]: 'No Content',
[HttpStatus.RESET_CONTENT]: 'Reset Content',
[HttpStatus.PARTIAL_CONTENT]: 'Partial Content',
[HttpStatus.MULTI_STATUS]: 'Multi-Status',
[HttpStatus.ALREADY_REPORTED]: 'Already Reported',
[HttpStatus.IM_USED]: 'IM Used',
// 3xx
[HttpStatus.MULTIPLE_CHOICES]: 'Multiple Choices',
[HttpStatus.MOVED_PERMANENTLY]: 'Moved Permanently',
[HttpStatus.FOUND]: 'Found',
[HttpStatus.SEE_OTHER]: 'See Other',
[HttpStatus.NOT_MODIFIED]: 'Not Modified',
[HttpStatus.USE_PROXY]: 'Use Proxy',
[HttpStatus.TEMPORARY_REDIRECT]: 'Temporary Redirect',
[HttpStatus.PERMANENT_REDIRECT]: 'Permanent Redirect',
// 4xx
[HttpStatus.BAD_REQUEST]: 'Bad Request',
[HttpStatus.UNAUTHORIZED]: 'Unauthorized',
[HttpStatus.PAYMENT_REQUIRED]: 'Payment Required',
[HttpStatus.FORBIDDEN]: 'Forbidden',
[HttpStatus.NOT_FOUND]: 'Not Found',
[HttpStatus.METHOD_NOT_ALLOWED]: 'Method Not Allowed',
[HttpStatus.NOT_ACCEPTABLE]: 'Not Acceptable',
[HttpStatus.PROXY_AUTHENTICATION_REQUIRED]: 'Proxy Authentication Required',
[HttpStatus.REQUEST_TIMEOUT]: 'Request Timeout',
[HttpStatus.CONFLICT]: 'Conflict',
[HttpStatus.GONE]: 'Gone',
[HttpStatus.LENGTH_REQUIRED]: 'Length Required',
[HttpStatus.PRECONDITION_FAILED]: 'Precondition Failed',
[HttpStatus.PAYLOAD_TOO_LARGE]: 'Payload Too Large',
[HttpStatus.URI_TOO_LONG]: 'URI Too Long',
[HttpStatus.UNSUPPORTED_MEDIA_TYPE]: 'Unsupported Media Type',
[HttpStatus.RANGE_NOT_SATISFIABLE]: 'Range Not Satisfiable',
[HttpStatus.EXPECTATION_FAILED]: 'Expectation Failed',
[HttpStatus.IM_A_TEAPOT]: "I'm a teapot",
[HttpStatus.MISDIRECTED_REQUEST]: 'Misdirected Request',
[HttpStatus.UNPROCESSABLE_ENTITY]: 'Unprocessable Entity',
[HttpStatus.LOCKED]: 'Locked',
[HttpStatus.FAILED_DEPENDENCY]: 'Failed Dependency',
[HttpStatus.TOO_EARLY]: 'Too Early',
[HttpStatus.UPGRADE_REQUIRED]: 'Upgrade Required',
[HttpStatus.PRECONDITION_REQUIRED]: 'Precondition Required',
[HttpStatus.TOO_MANY_REQUESTS]: 'Too Many Requests',
[HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE]: 'Request Header Fields Too Large',
[HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS]: 'Unavailable For Legal Reasons',
// 5xx
[HttpStatus.INTERNAL_SERVER_ERROR]: 'Internal Server Error',
[HttpStatus.NOT_IMPLEMENTED]: 'Not Implemented',
[HttpStatus.BAD_GATEWAY]: 'Bad Gateway',
[HttpStatus.SERVICE_UNAVAILABLE]: 'Service Unavailable',
[HttpStatus.GATEWAY_TIMEOUT]: 'Gateway Timeout',
[HttpStatus.HTTP_VERSION_NOT_SUPPORTED]: 'HTTP Version Not Supported',
[HttpStatus.VARIANT_ALSO_NEGOTIATES]: 'Variant Also Negotiates',
[HttpStatus.INSUFFICIENT_STORAGE]: 'Insufficient Storage',
[HttpStatus.LOOP_DETECTED]: 'Loop Detected',
[HttpStatus.NOT_EXTENDED]: 'Not Extended',
[HttpStatus.NETWORK_AUTHENTICATION_REQUIRED]: 'Network Authentication Required',
};
/**
* Common HTTP headers
*/
export const HTTP_HEADERS = {
// Request headers
HOST: 'host',
USER_AGENT: 'user-agent',
ACCEPT: 'accept',
ACCEPT_LANGUAGE: 'accept-language',
ACCEPT_ENCODING: 'accept-encoding',
AUTHORIZATION: 'authorization',
CACHE_CONTROL: 'cache-control',
CONNECTION: 'connection',
CONTENT_TYPE: 'content-type',
CONTENT_LENGTH: 'content-length',
COOKIE: 'cookie',
// Response headers
SET_COOKIE: 'set-cookie',
LOCATION: 'location',
SERVER: 'server',
DATE: 'date',
EXPIRES: 'expires',
LAST_MODIFIED: 'last-modified',
ETAG: 'etag',
// CORS headers
ACCESS_CONTROL_ALLOW_ORIGIN: 'access-control-allow-origin',
ACCESS_CONTROL_ALLOW_METHODS: 'access-control-allow-methods',
ACCESS_CONTROL_ALLOW_HEADERS: 'access-control-allow-headers',
// Security headers
STRICT_TRANSPORT_SECURITY: 'strict-transport-security',
X_CONTENT_TYPE_OPTIONS: 'x-content-type-options',
X_FRAME_OPTIONS: 'x-frame-options',
X_XSS_PROTECTION: 'x-xss-protection',
CONTENT_SECURITY_POLICY: 'content-security-policy',
} as const;
/**
* Get HTTP status text
*/
export function getStatusText(status: HttpStatus): string {
return HTTP_STATUS_TEXT[status] || 'Unknown';
}

View File

@@ -0,0 +1,8 @@
/**
* HTTP Protocol Module
* Generic HTTP protocol knowledge and parsing utilities
*/
export * from './constants.js';
export * from './types.js';
export * from './parser.js';

219
ts/protocols/http/parser.ts Normal file
View File

@@ -0,0 +1,219 @@
/**
* HTTP Protocol Parser
* Generic HTTP parsing utilities
*/
import { HTTP_METHODS, type THttpMethod, type THttpVersion } from './constants.js';
import type { IHttpRequestLine, IHttpHeader } from './types.js';
/**
* HTTP parser utilities
*/
export class HttpParser {
/**
* Check if string is a valid HTTP method
*/
static isHttpMethod(str: string): str is THttpMethod {
return HTTP_METHODS.includes(str as THttpMethod);
}
/**
* Parse HTTP request line
*/
static parseRequestLine(line: string): IHttpRequestLine | null {
const parts = line.trim().split(' ');
if (parts.length !== 3) {
return null;
}
const [method, path, version] = parts;
// Validate method
if (!this.isHttpMethod(method)) {
return null;
}
// Validate version
if (!version.startsWith('HTTP/')) {
return null;
}
return {
method: method as THttpMethod,
path,
version: version as THttpVersion
};
}
/**
* Parse HTTP header line
*/
static parseHeaderLine(line: string): IHttpHeader | null {
const colonIndex = line.indexOf(':');
if (colonIndex === -1) {
return null;
}
const name = line.slice(0, colonIndex).trim();
const value = line.slice(colonIndex + 1).trim();
if (!name) {
return null;
}
return { name, value };
}
/**
* Parse HTTP headers from lines
*/
static parseHeaders(lines: string[]): Record<string, string> {
const headers: Record<string, string> = {};
for (const line of lines) {
const header = this.parseHeaderLine(line);
if (header) {
// Convert header names to lowercase for consistency
headers[header.name.toLowerCase()] = header.value;
}
}
return headers;
}
/**
* Extract domain from Host header value
*/
static extractDomainFromHost(hostHeader: string): string {
// Remove port if present
const colonIndex = hostHeader.lastIndexOf(':');
if (colonIndex !== -1) {
// Check if it's not part of IPv6 address
const beforeColon = hostHeader.slice(0, colonIndex);
if (!beforeColon.includes(']')) {
return beforeColon;
}
}
return hostHeader;
}
/**
* Validate domain name
*/
static isValidDomain(domain: string): boolean {
// Basic domain validation
if (!domain || domain.length > 253) {
return false;
}
// Check for valid characters and structure
const domainRegex = /^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*$/;
return domainRegex.test(domain);
}
/**
* Extract line from buffer
*/
static extractLine(buffer: Buffer, offset: number = 0): { line: string; nextOffset: number } | null {
// Look for CRLF
const crlfIndex = buffer.indexOf('\r\n', offset);
if (crlfIndex === -1) {
// Look for just LF
const lfIndex = buffer.indexOf('\n', offset);
if (lfIndex === -1) {
return null;
}
return {
line: buffer.slice(offset, lfIndex).toString('utf8'),
nextOffset: lfIndex + 1
};
}
return {
line: buffer.slice(offset, crlfIndex).toString('utf8'),
nextOffset: crlfIndex + 2
};
}
/**
* Check if buffer contains printable ASCII
*/
static isPrintableAscii(buffer: Buffer, length?: number): boolean {
const checkLength = Math.min(length || buffer.length, buffer.length);
for (let i = 0; i < checkLength; i++) {
const byte = buffer[i];
// Allow printable ASCII (32-126) plus tab (9), LF (10), and CR (13)
if (byte < 32 || byte > 126) {
if (byte !== 9 && byte !== 10 && byte !== 13) {
return false;
}
}
}
return true;
}
/**
* Quick check if buffer starts with HTTP method
*/
static quickCheck(buffer: Buffer): boolean {
if (buffer.length < 3) {
return false;
}
// Check common HTTP methods
const start = buffer.slice(0, 7).toString('ascii');
return start.startsWith('GET ') ||
start.startsWith('POST ') ||
start.startsWith('PUT ') ||
start.startsWith('DELETE ') ||
start.startsWith('HEAD ') ||
start.startsWith('OPTIONS') ||
start.startsWith('PATCH ') ||
start.startsWith('CONNECT') ||
start.startsWith('TRACE ');
}
/**
* Parse query string
*/
static parseQueryString(queryString: string): Record<string, string> {
const params: Record<string, string> = {};
if (!queryString) {
return params;
}
// Remove leading '?' if present
if (queryString.startsWith('?')) {
queryString = queryString.slice(1);
}
const pairs = queryString.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key) {
params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
}
}
return params;
}
/**
* Build query string from params
*/
static buildQueryString(params: Record<string, string>): string {
const pairs: string[] = [];
for (const [key, value] of Object.entries(params)) {
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
return pairs.length > 0 ? '?' + pairs.join('&') : '';
}
}

View File

@@ -0,0 +1,70 @@
/**
* HTTP Protocol Type Definitions
*/
import type { THttpMethod, THttpVersion, HttpStatus } from './constants.js';
/**
* HTTP request line structure
*/
export interface IHttpRequestLine {
method: THttpMethod;
path: string;
version: THttpVersion;
}
/**
* HTTP response line structure
*/
export interface IHttpResponseLine {
version: THttpVersion;
status: HttpStatus;
statusText: string;
}
/**
* HTTP header structure
*/
export interface IHttpHeader {
name: string;
value: string;
}
/**
* HTTP message structure (base for request and response)
*/
export interface IHttpMessage {
headers: Record<string, string>;
body?: Buffer;
}
/**
* HTTP request structure
*/
export interface IHttpRequest extends IHttpMessage {
method: THttpMethod;
path: string;
version: THttpVersion;
query?: Record<string, string>;
}
/**
* HTTP response structure
*/
export interface IHttpResponse extends IHttpMessage {
status: HttpStatus;
statusText: string;
version: THttpVersion;
}
/**
* Parsed URL structure
*/
export interface IParsedUrl {
protocol?: string;
hostname?: string;
port?: number;
path?: string;
query?: string;
fragment?: string;
}

12
ts/protocols/index.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* Protocol-specific modules for smartproxy
*
* This directory contains generic protocol knowledge separated from
* smartproxy-specific implementation details.
*/
export * as common from './common/index.js';
export * as tls from './tls/index.js';
export * as http from './http/index.js';
export * as proxy from './proxy/index.js';
export * as websocket from './websocket/index.js';

View File

@@ -0,0 +1,7 @@
/**
* PROXY Protocol Module
* HAProxy PROXY protocol implementation
*/
export * from './types.js';
export * from './parser.js';

View File

@@ -0,0 +1,183 @@
/**
* PROXY Protocol Parser
* Implementation of HAProxy PROXY protocol v1 (text format)
* Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
*/
import type { IProxyInfo, IProxyParseResult, TProxyProtocol } from './types.js';
/**
* PROXY protocol parser
*/
export class ProxyProtocolParser {
static readonly PROXY_V1_SIGNATURE = 'PROXY ';
static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header
static readonly HEADER_TERMINATOR = '\r\n';
/**
* Parse PROXY protocol v1 header from buffer
* Returns proxy info and remaining data after header
*/
static parse(data: Buffer): IProxyParseResult {
// Check if buffer starts with PROXY signature
if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) {
return {
proxyInfo: null,
remainingData: data
};
}
// Find header terminator
const headerEndIndex = data.indexOf(this.HEADER_TERMINATOR);
if (headerEndIndex === -1) {
// Header incomplete, need more data
if (data.length > this.MAX_HEADER_LENGTH) {
// Header too long, invalid
throw new Error('PROXY protocol header exceeds maximum length');
}
return {
proxyInfo: null,
remainingData: data
};
}
// Extract header line
const headerLine = data.toString('ascii', 0, headerEndIndex);
const remainingData = data.slice(headerEndIndex + 2); // Skip \r\n
// Parse header
const parts = headerLine.split(' ');
if (parts.length < 2) {
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
}
const [signature, protocol] = parts;
// Validate protocol
if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(protocol)) {
throw new Error(`Invalid PROXY protocol: ${protocol}`);
}
// For UNKNOWN protocol, ignore addresses
if (protocol === 'UNKNOWN') {
return {
proxyInfo: {
protocol: 'UNKNOWN',
sourceIP: '',
sourcePort: 0,
destinationIP: '',
destinationPort: 0
},
remainingData
};
}
// For TCP4/TCP6, we need all 6 parts
if (parts.length !== 6) {
throw new Error(`Invalid PROXY protocol header format: ${headerLine}`);
}
const [, , srcIP, dstIP, srcPort, dstPort] = parts;
// Validate and parse ports
const sourcePort = parseInt(srcPort, 10);
const destinationPort = parseInt(dstPort, 10);
if (isNaN(sourcePort) || sourcePort < 0 || sourcePort > 65535) {
throw new Error(`Invalid source port: ${srcPort}`);
}
if (isNaN(destinationPort) || destinationPort < 0 || destinationPort > 65535) {
throw new Error(`Invalid destination port: ${dstPort}`);
}
// Validate IP addresses
const protocolType = protocol as TProxyProtocol;
if (!this.isValidIP(srcIP, protocolType)) {
throw new Error(`Invalid source IP for ${protocol}: ${srcIP}`);
}
if (!this.isValidIP(dstIP, protocolType)) {
throw new Error(`Invalid destination IP for ${protocol}: ${dstIP}`);
}
return {
proxyInfo: {
protocol: protocolType,
sourceIP: srcIP,
sourcePort,
destinationIP: dstIP,
destinationPort
},
remainingData
};
}
/**
* Generate PROXY protocol v1 header
*/
static generate(info: IProxyInfo): Buffer {
if (info.protocol === 'UNKNOWN') {
return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii');
}
const header = `PROXY ${info.protocol} ${info.sourceIP} ${info.destinationIP} ${info.sourcePort} ${info.destinationPort}\r\n`;
if (header.length > this.MAX_HEADER_LENGTH) {
throw new Error('Generated PROXY protocol header exceeds maximum length');
}
return Buffer.from(header, 'ascii');
}
/**
* Validate IP address format
*/
static isValidIP(ip: string, protocol: TProxyProtocol): boolean {
if (protocol === 'TCP4') {
return this.isIPv4(ip);
} else if (protocol === 'TCP6') {
return this.isIPv6(ip);
}
return false;
}
/**
* Check if string is valid IPv4
*/
static isIPv4(ip: string): boolean {
const parts = ip.split('.');
if (parts.length !== 4) return false;
for (const part of parts) {
const num = parseInt(part, 10);
if (isNaN(num) || num < 0 || num > 255 || part !== num.toString()) {
return false;
}
}
return true;
}
/**
* Check if string is valid IPv6
*/
static isIPv6(ip: string): boolean {
// Basic IPv6 validation
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
return ipv6Regex.test(ip);
}
/**
* Create a connection ID string for tracking
*/
static createConnectionId(connectionInfo: {
sourceIp?: string;
sourcePort?: number;
destIp?: string;
destPort?: number;
}): string {
const { sourceIp, sourcePort, destIp, destPort } = connectionInfo;
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
}
}

View File

@@ -0,0 +1,53 @@
/**
* PROXY Protocol Type Definitions
* Based on HAProxy PROXY protocol specification
*/
/**
* PROXY protocol version
*/
export type TProxyProtocolVersion = 'v1' | 'v2';
/**
* Connection protocol type
*/
export type TProxyProtocol = 'TCP4' | 'TCP6' | 'UNKNOWN';
/**
* Interface representing parsed PROXY protocol information
*/
export interface IProxyInfo {
protocol: TProxyProtocol;
sourceIP: string;
sourcePort: number;
destinationIP: string;
destinationPort: number;
}
/**
* Interface for parse result including remaining data
*/
export interface IProxyParseResult {
proxyInfo: IProxyInfo | null;
remainingData: Buffer;
}
/**
* PROXY protocol v2 header format
*/
export interface IProxyV2Header {
signature: Buffer;
versionCommand: number;
family: number;
length: number;
}
/**
* Connection information for PROXY protocol
*/
export interface IProxyConnectionInfo {
sourceIp?: string;
sourcePort?: number;
destIp?: string;
destPort?: number;
}

View File

@@ -1,4 +1,4 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../../plugins.js';
import { TlsAlertLevel, TlsAlertDescription, TlsVersion } from '../utils/tls-utils.js'; import { TlsAlertLevel, TlsAlertDescription, TlsVersion } from '../utils/tls-utils.js';
/** /**

37
ts/protocols/tls/index.ts Normal file
View File

@@ -0,0 +1,37 @@
/**
* TLS Protocol Module
* Contains generic TLS protocol knowledge including parsers, constants, and utilities
*/
// Export all sub-modules
export * from './alerts/index.js';
export * from './sni/index.js';
export * from './utils/index.js';
// Re-export main utilities and types for convenience
export {
TlsUtils,
TlsRecordType,
TlsHandshakeType,
TlsExtensionType,
TlsAlertLevel,
TlsAlertDescription,
TlsVersion
} from './utils/tls-utils.js';
export { TlsAlert } from './alerts/tls-alert.js';
export { ClientHelloParser } from './sni/client-hello-parser.js';
export { SniExtraction } from './sni/sni-extraction.js';
// Export tlsVersionToString helper
export function tlsVersionToString(major: number, minor: number): string | null {
if (major === 0x03) {
switch (minor) {
case 0x00: return 'SSLv3';
case 0x01: return 'TLSv1.0';
case 0x02: return 'TLSv1.1';
case 0x03: return 'TLSv1.2';
case 0x04: return 'TLSv1.3';
}
}
return null;
}

Some files were not shown because too many files have changed in this diff Show More