Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
5fbcf81c2c | |||
6eac957baf | |||
64f5fa62a9 | |||
4fea28ffb7 | |||
ffc04c5b85 | |||
a459d77b6f | |||
b6d8b73599 | |||
8936f4ad46 | |||
36068a6d92 | |||
d47b048517 | |||
c84947068c | |||
26f7431111 | |||
aa6ddbc4a6 | |||
6aa5f415c1 | |||
b26abbfd87 | |||
82df9a6f52 |
BIN
.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
BIN
.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
Binary file not shown.
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||||
|
# * For C, use cpp
|
||||||
|
# * For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
language: typescript
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed)on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "smartproxy"
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiryDate": "2025-10-01T02:31:27.435Z",
|
"expiryDate": "2025-11-12T14:20:10.043Z",
|
||||||
"issueDate": "2025-07-03T02:31:27.435Z",
|
"issueDate": "2025-08-14T14:20:10.043Z",
|
||||||
"savedAt": "2025-07-03T02:31:27.435Z"
|
"savedAt": "2025-08-14T14:20:10.044Z"
|
||||||
}
|
}
|
56
changelog.md
56
changelog.md
@@ -1,5 +1,61 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-14 - 21.1.4 - fix(security)
|
||||||
|
Critical security and stability fixes
|
||||||
|
|
||||||
|
- Fixed critical socket.emit override vulnerability that was breaking TLS connections
|
||||||
|
- Implemented comprehensive socket cleanup with new socket tracker utility
|
||||||
|
- Improved code organization by extracting RouteOrchestrator from SmartProxy
|
||||||
|
- Fixed IPv6 loopback detection for proper IPv6 support
|
||||||
|
- Added memory bounds to prevent unbounded collection growth
|
||||||
|
- Fixed certificate manager race conditions with proper synchronization
|
||||||
|
- Unreferenced long-lived timers to prevent process hanging
|
||||||
|
- Enhanced route validation for socket-handler actions
|
||||||
|
- Fixed header parsing when extractFullHeaders option is enabled
|
||||||
|
|
||||||
|
## 2025-07-22 - 21.1.1 - fix(detection)
|
||||||
|
Fix SNI detection in TLS detector
|
||||||
|
|
||||||
|
- Restored proper TLS detector implementation with ClientHello parsing
|
||||||
|
- Fixed imports to use new protocols module locations
|
||||||
|
- Added missing detectWithContext method for fragmented detection
|
||||||
|
- Fixed method names to match BufferAccumulator interface
|
||||||
|
- Removed unused import readUInt24BE
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "19.6.17",
|
"version": "21.1.4",
|
||||||
"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",
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"@git.zone/tsrun": "^1.2.44",
|
"@git.zone/tsrun": "^1.2.44",
|
||||||
"@git.zone/tstest": "^2.3.1",
|
"@git.zone/tstest": "^2.3.1",
|
||||||
"@types/node": "^22.15.29",
|
"@types/node": "^22.15.29",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3",
|
||||||
|
"why-is-node-running": "^3.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
@@ -51,7 +52,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"
|
||||||
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -78,6 +78,9 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.3
|
specifier: ^5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
|
why-is-node-running:
|
||||||
|
specifier: ^3.2.2
|
||||||
|
version: 3.2.2
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -4096,6 +4099,11 @@ packages:
|
|||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
why-is-node-running@3.2.2:
|
||||||
|
resolution: {integrity: sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==}
|
||||||
|
engines: {node: '>=20.11'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
winston-transport@4.9.0:
|
winston-transport@4.9.0:
|
||||||
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
|
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -10082,6 +10090,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe: 3.1.1
|
isexe: 3.1.1
|
||||||
|
|
||||||
|
why-is-node-running@3.2.2: {}
|
||||||
|
|
||||||
winston-transport@4.9.0:
|
winston-transport@4.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
logform: 2.7.0
|
logform: 2.7.0
|
||||||
|
BIN
readme.plan.md
BIN
readme.plan.md
Binary file not shown.
@@ -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 () => {
|
||||||
|
@@ -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: {
|
||||||
|
@@ -124,4 +124,4 @@ tap.test('should parse HTTP headers correctly', async (tools) => {
|
|||||||
await proxy.stop();
|
await proxy.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -159,4 +159,4 @@ tap.test('should return 404 for non-existent challenge tokens', async (tapTest)
|
|||||||
await proxy.stop();
|
await proxy.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
@@ -215,4 +215,4 @@ tap.test('should handle HTTP request parsing correctly', async (tools) => {
|
|||||||
await proxy.stop();
|
await proxy.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -117,4 +117,4 @@ tap.test('should configure ACME challenge route', async () => {
|
|||||||
expect(challengeRoute.action.socketHandler).toBeDefined();
|
expect(challengeRoute.action.socketHandler).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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',
|
||||||
@@ -119,4 +119,4 @@ tap.test('should defer certificate provisioning until ports are ready', async (t
|
|||||||
await proxy.stop();
|
await proxy.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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'
|
||||||
|
@@ -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',
|
||||||
|
@@ -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'
|
||||||
|
@@ -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',
|
||||||
@@ -238,4 +238,4 @@ tap.test('should renew certificates', async () => {
|
|||||||
await proxy.stop();
|
await proxy.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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',
|
||||||
@@ -57,4 +57,4 @@ tap.test('should handle socket handler route type', async () => {
|
|||||||
expect(route.action.socketHandler).toBeDefined();
|
expect(route.action.socketHandler).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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,
|
||||||
@@ -143,4 +143,4 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
|
|||||||
console.log('\n✓ Test complete: Cleanup queue now correctly processes all connections');
|
console.log('\n✓ Test complete: Cleanup queue now correctly processes all connections');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -239,4 +239,4 @@ tap.test('should handle clients that error during connection', async () => {
|
|||||||
console.log('\n✅ PASS: Connection error cleanup working correctly!');
|
console.log('\n✅ PASS: Connection error cleanup working correctly!');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -276,4 +276,4 @@ tap.test('comprehensive connection cleanup test - all scenarios', async () => {
|
|||||||
console.log('- NFTables connections');
|
console.log('- NFTables connections');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@@ -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'
|
||||||
}
|
}
|
||||||
@@ -296,4 +296,4 @@ tap.test('Cleanup and shutdown', async () => {
|
|||||||
allServers.length = 0;
|
allServers.length = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
146
test/test.detection.ts
Normal file
146
test/test.detection.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
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 () => {
|
||||||
|
// Create connection context
|
||||||
|
const context = smartproxy.detection.ProtocolDetector.createConnectionContext({
|
||||||
|
sourceIp: '127.0.0.1',
|
||||||
|
sourcePort: 12345,
|
||||||
|
destIp: '127.0.0.1',
|
||||||
|
destPort: 80,
|
||||||
|
socketId: 'test-connection-1'
|
||||||
|
});
|
||||||
|
|
||||||
|
// First fragment
|
||||||
|
const fragment1 = Buffer.from('GET /test HT');
|
||||||
|
let result = await smartproxy.detection.ProtocolDetector.detectWithContext(
|
||||||
|
fragment1,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
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.detectWithContext(
|
||||||
|
fragment2,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Clean up fragments
|
||||||
|
smartproxy.detection.ProtocolDetector.cleanupConnection(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
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.test('cleanup detection', async () => {
|
||||||
|
// Clean up the protocol detector instance
|
||||||
|
smartproxy.detection.ProtocolDetector.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -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',
|
||||||
@@ -79,4 +79,4 @@ tap.test('should verify certificate manager callback is preserved on updateRoute
|
|||||||
console.log('Fix verified: Certificate manager callback is preserved on updateRoutes');
|
console.log('Fix verified: Certificate manager callback is preserved on updateRoutes');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -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,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@@ -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 () => {
|
||||||
|
@@ -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();
|
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -180,4 +180,4 @@ tap.test('should handle ACME HTTP-01 challenges on port 80 with HttpProxy', asyn
|
|||||||
console.log('Test passed: ACME HTTP-01 challenges on port 80 use HttpProxy');
|
console.log('Test passed: ACME HTTP-01 challenges on port 80 use HttpProxy');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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' }
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -242,4 +242,4 @@ tap.test('should handle ACME challenges on port 8080 with improved port binding
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -117,4 +117,4 @@ tap.test('Cleanup HttpProxy SecurityManager', async () => {
|
|||||||
securityManager.clearIPTracking();
|
securityManager.clearIPTracking();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@@ -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'
|
||||||
},
|
},
|
||||||
|
@@ -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,
|
||||||
@@ -247,4 +247,4 @@ tap.test('keepalive support - verify keepalive connections are properly handled'
|
|||||||
console.log(' - Zombie detection respects keepalive settings');
|
console.log(' - Zombie detection respects keepalive settings');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -109,4 +109,4 @@ tap.test('Cleanup deduplicator', async () => {
|
|||||||
expect(deduplicator).toBeInstanceOf(LogDeduplicator);
|
expect(deduplicator).toBeInstanceOf(LogDeduplicator);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
@@ -149,4 +149,4 @@ tap.test('should not have memory leaks in long-running operations', async (tools
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Run with: node --expose-gc test.memory-leak-check.node.ts
|
// Run with: node --expose-gc test.memory-leak-check.node.ts
|
||||||
tap.start();
|
export default tap.start();
|
@@ -57,4 +57,4 @@ tap.test('memory leak fixes verification', async () => {
|
|||||||
console.log('\n✅ All memory leak fixes verified!');
|
console.log('\n✅ All memory leak fixes verified!');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -128,4 +128,4 @@ tap.test('memory leak fixes - unit tests', async () => {
|
|||||||
console.log('\n✅ All memory leak fixes verified!');
|
console.log('\n✅ All memory leak fixes verified!');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@@ -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'
|
||||||
}
|
}
|
||||||
@@ -258,4 +258,4 @@ tap.test('should clean up resources', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@@ -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
|
||||||
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -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'
|
||||||
};
|
};
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -192,4 +192,4 @@ tap.test('simple proxy chain test - identify connection accumulation', async ()
|
|||||||
expect(finalCounts.proxy2).toEqual(0);
|
expect(finalCounts.proxy2).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -130,4 +130,4 @@ tap.test('PROXY protocol v1 generator', async () => {
|
|||||||
// Skipping integration tests for now - focus on unit tests
|
// Skipping integration tests for now - focus on unit tests
|
||||||
// Integration tests would require more complex setup and teardown
|
// Integration tests would require more complex setup and teardown
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -198,4 +198,4 @@ tap.test('should handle routing failures without leaking connections', async ()
|
|||||||
console.log('\n✅ PASS: Routing failures cleaned up correctly!');
|
console.log('\n✅ PASS: Routing failures cleaned up correctly!');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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',
|
||||||
@@ -113,4 +113,4 @@ tap.test('should set update routes callback on certificate manager', async () =>
|
|||||||
await proxy.stop();
|
await proxy.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
}];
|
}];
|
||||||
|
@@ -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']
|
||||||
@@ -58,4 +58,4 @@ tap.test('route security should be correctly configured', async () => {
|
|||||||
expect(isBlockedIPAllowed).toBeFalse();
|
expect(isBlockedIPAllowed).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -336,4 +336,4 @@ tap.test('real code integration test - verify fix is applied', async () => {
|
|||||||
console.log('Real code integration test passed - fix is correctly applied!');
|
console.log('Real code integration test passed - fix is correctly applied!');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
|
@@ -37,10 +37,10 @@ function createRouteConfig(
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: destinationIp,
|
host: destinationIp,
|
||||||
port: destinationPort
|
port: destinationPort
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
@@ -156,4 +154,4 @@ tap.test('Cleanup SharedSecurityManager', async () => {
|
|||||||
securityManager.clearIPTracking();
|
securityManager.clearIPTracking();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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',
|
||||||
@@ -51,4 +51,4 @@ tap.test('should verify SmartAcme cert managers are accessible', async () => {
|
|||||||
expect(memoryCertManager).toBeDefined();
|
expect(memoryCertManager).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
@@ -141,4 +141,4 @@ tap.test('stuck connection cleanup - verify connections to hanging backends are
|
|||||||
console.log('✓ Test complete: Stuck connections are properly detected and cleaned up');
|
console.log('✓ Test complete: Stuck connections are properly detected and cleaned up');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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 }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -155,4 +155,4 @@ tap.test('long-lived connection survival test', async (tools) => {
|
|||||||
console.log('✅ Long-lived connection survived past 30-second timeout!');
|
console.log('✅ Long-lived connection survived past 30-second timeout!');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -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
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -303,4 +303,4 @@ tap.test('zombie connection cleanup - verify inactivity check detects and cleans
|
|||||||
expect(details.inner.halfZombies.length).toEqual(0);
|
expect(details.inner.halfZombies.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@@ -12,6 +12,11 @@ declare module 'net' {
|
|||||||
getTLSVersion?(): string; // Returns the TLS version (e.g., 'TLSv1.2', 'TLSv1.3')
|
getTLSVersion?(): string; // Returns the TLS version (e.g., 'TLSv1.2', 'TLSv1.3')
|
||||||
getPeerCertificate?(detailed?: boolean): any; // Returns the peer's certificate
|
getPeerCertificate?(detailed?: boolean): any; // Returns the peer's certificate
|
||||||
getSession?(): Buffer; // Returns the TLS session data
|
getSession?(): Buffer; // Returns the TLS session data
|
||||||
|
|
||||||
|
// Connection tracking properties (used by HttpProxy)
|
||||||
|
_connectionId?: string; // Unique identifier for the connection
|
||||||
|
_remoteIP?: string; // Remote IP address
|
||||||
|
_realRemoteIP?: string; // Real remote IP (when proxied)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
63
ts/core/utils/socket-tracker.ts
Normal file
63
ts/core/utils/socket-tracker.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Socket Tracker Utility
|
||||||
|
* Provides standardized socket cleanup with proper listener and timer management
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Socket } from 'net';
|
||||||
|
|
||||||
|
export type SocketTracked = {
|
||||||
|
cleanup: () => void;
|
||||||
|
addListener: <E extends string>(event: E, listener: (...args: any[]) => void) => void;
|
||||||
|
addTimer: (t: NodeJS.Timeout | null | undefined) => void;
|
||||||
|
safeDestroy: (reason?: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a socket tracker to manage listeners and timers
|
||||||
|
* Ensures proper cleanup and prevents memory leaks
|
||||||
|
*/
|
||||||
|
export function createSocketTracker(socket: Socket): SocketTracked {
|
||||||
|
const listeners: Array<{ event: string; listener: (...args: any[]) => void }> = [];
|
||||||
|
const timers: NodeJS.Timeout[] = [];
|
||||||
|
let cleaned = false;
|
||||||
|
|
||||||
|
const addListener = (event: string, listener: (...args: any[]) => void) => {
|
||||||
|
socket.on(event, listener);
|
||||||
|
listeners.push({ event, listener });
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTimer = (t: NodeJS.Timeout | null | undefined) => {
|
||||||
|
if (!t) return;
|
||||||
|
timers.push(t);
|
||||||
|
// Unref timer so it doesn't keep process alive
|
||||||
|
if (typeof t.unref === 'function') {
|
||||||
|
t.unref();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (cleaned) return;
|
||||||
|
cleaned = true;
|
||||||
|
|
||||||
|
// Clear all tracked timers
|
||||||
|
for (const t of timers) {
|
||||||
|
clearTimeout(t);
|
||||||
|
}
|
||||||
|
timers.length = 0;
|
||||||
|
|
||||||
|
// Remove all tracked listeners
|
||||||
|
for (const { event, listener } of listeners) {
|
||||||
|
socket.off(event, listener);
|
||||||
|
}
|
||||||
|
listeners.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeDestroy = (reason?: Error) => {
|
||||||
|
cleanup();
|
||||||
|
if (!socket.destroyed) {
|
||||||
|
socket.destroy(reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { cleanup, addListener, addTimer, safeDestroy };
|
||||||
|
}
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
127
ts/detection/detectors/http-detector.ts
Normal file
127
ts/detection/detectors/http-detector.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
import { HttpParser } from '../../protocols/http/parser.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have complete headers first
|
||||||
|
const headersEnd = buffer.indexOf('\r\n\r\n');
|
||||||
|
const isComplete = headersEnd !== -1;
|
||||||
|
|
||||||
|
// Extract routing information
|
||||||
|
const routing = RoutingExtractor.extract(buffer, 'http');
|
||||||
|
|
||||||
|
// Extract headers if requested and we have complete headers
|
||||||
|
let headers: Record<string, string> | undefined;
|
||||||
|
if (options?.extractFullHeaders && isComplete) {
|
||||||
|
const headerSection = buffer.slice(0, headersEnd).toString();
|
||||||
|
const lines = headerSection.split('\r\n');
|
||||||
|
if (lines.length > 1) {
|
||||||
|
// Skip the request line and parse headers
|
||||||
|
headers = HttpParser.parseHeaders(lines.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't need full headers and we have complete headers, we can return early
|
||||||
|
if (quickResult.confidence >= 95 && !options?.extractFullHeaders && isComplete) {
|
||||||
|
return {
|
||||||
|
protocol: 'http',
|
||||||
|
connectionInfo: {
|
||||||
|
protocol: 'http',
|
||||||
|
method: quickResult.metadata?.method as THttpMethod,
|
||||||
|
domain: routing?.domain,
|
||||||
|
path: routing?.path
|
||||||
|
},
|
||||||
|
isComplete: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
protocol: 'http',
|
||||||
|
connectionInfo: {
|
||||||
|
protocol: 'http',
|
||||||
|
domain: routing?.domain,
|
||||||
|
path: routing?.path,
|
||||||
|
method: quickResult.metadata?.method as THttpMethod,
|
||||||
|
headers: headers
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
148
ts/detection/detectors/quick-detector.ts
Normal file
148
ts/detection/detectors/quick-detector.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
147
ts/detection/detectors/routing-extractor.ts
Normal file
147
ts/detection/detectors/routing-extractor.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
223
ts/detection/detectors/tls-detector.ts
Normal file
223
ts/detection/detectors/tls-detector.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* 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 } 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;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// This method is deprecated - TLS detection should use the fragment manager
|
||||||
|
// from the parent detector system, not maintain its own fragments
|
||||||
|
return this.detect(buffer, options);
|
||||||
|
}
|
||||||
|
}
|
25
ts/detection/index.ts
Normal file
25
ts/detection/index.ts
Normal 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';
|
102
ts/detection/models/detection-types.ts
Normal file
102
ts/detection/models/detection-types.ts
Normal 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;
|
||||||
|
}
|
115
ts/detection/models/interfaces.ts
Normal file
115
ts/detection/models/interfaces.ts
Normal 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;
|
||||||
|
}
|
311
ts/detection/protocol-detector.ts
Normal file
311
ts/detection/protocol-detector.ts
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
private connectionProtocols: Map<string, 'tls' | 'http'> = new Map();
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionId = DetectionFragmentManager.createConnectionId(context);
|
||||||
|
|
||||||
|
// Check if we already know the protocol for this connection
|
||||||
|
const knownProtocol = this.connectionProtocols.get(connectionId);
|
||||||
|
|
||||||
|
if (knownProtocol === 'http') {
|
||||||
|
const result = this.httpDetector.detectWithContext(buffer, context, options);
|
||||||
|
if (result) {
|
||||||
|
if (result.isComplete) {
|
||||||
|
this.connectionProtocols.delete(connectionId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else if (knownProtocol === 'tls') {
|
||||||
|
// Handle TLS with fragment accumulation
|
||||||
|
const handler = this.fragmentManager.getHandler('tls');
|
||||||
|
const fragmentResult = handler.addFragment(connectionId, buffer);
|
||||||
|
|
||||||
|
if (fragmentResult.error) {
|
||||||
|
handler.complete(connectionId);
|
||||||
|
this.connectionProtocols.delete(connectionId);
|
||||||
|
return {
|
||||||
|
protocol: 'unknown',
|
||||||
|
connectionInfo: { protocol: 'unknown' },
|
||||||
|
isComplete: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.tlsDetector.detect(fragmentResult.buffer!, options);
|
||||||
|
if (result) {
|
||||||
|
if (result.isComplete) {
|
||||||
|
handler.complete(connectionId);
|
||||||
|
this.connectionProtocols.delete(connectionId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't know the protocol yet, try to detect it
|
||||||
|
if (!knownProtocol) {
|
||||||
|
// First peek to determine protocol type
|
||||||
|
if (this.tlsDetector.canHandle(buffer)) {
|
||||||
|
this.connectionProtocols.set(connectionId, 'tls');
|
||||||
|
// Handle TLS with fragment accumulation
|
||||||
|
const handler = this.fragmentManager.getHandler('tls');
|
||||||
|
const fragmentResult = handler.addFragment(connectionId, buffer);
|
||||||
|
|
||||||
|
if (fragmentResult.error) {
|
||||||
|
handler.complete(connectionId);
|
||||||
|
this.connectionProtocols.delete(connectionId);
|
||||||
|
return {
|
||||||
|
protocol: 'unknown',
|
||||||
|
connectionInfo: { protocol: 'unknown' },
|
||||||
|
isComplete: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.tlsDetector.detect(fragmentResult.buffer!, options);
|
||||||
|
if (result) {
|
||||||
|
if (result.isComplete) {
|
||||||
|
handler.complete(connectionId);
|
||||||
|
this.connectionProtocols.delete(connectionId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.httpDetector.canHandle(buffer)) {
|
||||||
|
this.connectionProtocols.set(connectionId, 'http');
|
||||||
|
const result = this.httpDetector.detectWithContext(buffer, context, options);
|
||||||
|
if (result) {
|
||||||
|
if (result.isComplete) {
|
||||||
|
this.connectionProtocols.delete(connectionId);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
this.connectionProtocols.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up fragments for a specific connection
|
||||||
|
*/
|
||||||
|
static cleanupConnection(context: IConnectionContext): void {
|
||||||
|
const instance = this.getInstance();
|
||||||
|
const connectionId = DetectionFragmentManager.createConnectionId(context);
|
||||||
|
|
||||||
|
// Clean up both TLS and HTTP fragments for this connection
|
||||||
|
instance.fragmentManager.getHandler('tls').complete(connectionId);
|
||||||
|
instance.fragmentManager.getHandler('http').complete(connectionId);
|
||||||
|
|
||||||
|
// Remove from connection protocols tracking
|
||||||
|
instance.connectionProtocols.delete(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
141
ts/detection/utils/buffer-utils.ts
Normal file
141
ts/detection/utils/buffer-utils.ts
Normal 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);
|
||||||
|
}
|
64
ts/detection/utils/fragment-manager.ts
Normal file
64
ts/detection/utils/fragment-manager.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
77
ts/detection/utils/parser-utils.ts
Normal file
77
ts/detection/utils/parser-utils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@@ -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;
|
|
||||||
}
|
|
@@ -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';
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* Forwarding factory implementations
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { ForwardingHandlerFactory } from './forwarding-factory.js';
|
|
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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';
|
|
@@ -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';
|
|
@@ -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';
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user