Compare commits

...

47 Commits

Author SHA1 Message Date
b2a57ada5d 3.41.1
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 19:38:41 +00:00
62a3e1f4b7 fix(SniHandler): Improve TLS SNI session resumption handling: connections containing a session ticket are now only rejected when no SNI is present and allowSessionTicket is disabled. Updated return values and logging for clearer resumption detection. 2025-03-11 19:38:41 +00:00
3a1485213a 3.41.0
Some checks failed
Default (tags) / security (push) Failing after 10m42s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 19:31:20 +00:00
9dbf6fdeb5 feat(PortProxy/TLS): Add allowSessionTicket option to control TLS session ticket handling 2025-03-11 19:31:20 +00:00
9496dd5336 3.40.0
Some checks failed
Default (tags) / security (push) Failing after 11m44s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 18:05:20 +00:00
29d28fba93 feat(SniHandler): Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes 2025-03-11 18:05:20 +00:00
8196de4fa3 3.39.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:50:57 +00:00
6fddafe9fd feat(PortProxy): Add domain-specific NetworkProxy integration support to PortProxy 2025-03-11 17:50:56 +00:00
1e89062167 3.38.2
Some checks failed
Default (tags) / security (push) Successful in 22s
Default (tags) / test (push) Failing after 1m11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:38:32 +00:00
21a24fd95b fix(core): No code changes detected; bumping patch version for consistency. 2025-03-11 17:38:32 +00:00
03ef5e7f6e 3.38.1
Some checks failed
Default (tags) / security (push) Successful in 21s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:37:43 +00:00
415b82a84a fix(PortProxy): Improve SNI extraction handling in PortProxy by passing explicit connection info to extractSNIWithResumptionSupport for better TLS renegotiation and debug logging. 2025-03-11 17:37:43 +00:00
f304cc67b4 3.38.0
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:33:31 +00:00
0e12706176 feat(SniHandler): Enhance SNI extraction to support fragmented ClientHello messages, TLS 1.3 early data, and improved PSK parsing 2025-03-11 17:33:31 +00:00
6daf4c914d 3.37.3
Some checks failed
Default (tags) / security (push) Failing after 13m6s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 17:23:57 +00:00
36e4341315 fix(snihandler): Enhance SNI extraction to support TLS 1.3 PSK-based session resumption by adding a dedicated extractSNIFromPSKExtension method and improved logging for session resumption indicators. 2025-03-11 17:23:57 +00:00
474134d29c 3.37.2
Some checks failed
Default (tags) / security (push) Successful in 20s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:05:15 +00:00
43378becd2 fix(PortProxy): Improve buffering and data handling during connection setup in PortProxy to prevent data loss 2025-03-11 17:05:15 +00:00
5ba8eb778f 3.37.1
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:01:07 +00:00
87d26c86a1 fix(PortProxy/SNI): Refactor SNI extraction in PortProxy to use the dedicated SniHandler class 2025-03-11 17:01:07 +00:00
d81cf94876 3.37.0
Some checks failed
Default (tags) / security (push) Failing after 10m56s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 12:56:04 +00:00
8d06f1533e feat(portproxy): Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions 2025-03-11 12:56:03 +00:00
223be61c8d 3.35.0 2025-03-11 12:45:55 +00:00
6a693f4d86 feat(NetworkProxy): Integrate Port80Handler for automatic ACME certificate management
- Add ACME certificate management capabilities to NetworkProxy
- Implement automatic certificate issuance and renewal
- Add SNI support for serving the correct certificates
- Create certificate storage and caching system
- Enable dynamic certificate issuance for new domains
- Support automatic HTTP-to-HTTPS redirects for secured domains

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 12:45:22 +00:00
27a2bcb556 feat(NetworkProxy): Add support for array-based destinations and integration with PortProxy
- Update NetworkProxy to support new IReverseProxyConfig interface with destinationIps[] and destinationPorts[]
- Add load balancing with round-robin selection of destination endpoints
- Create automatic conversion of PortProxy domain configs to NetworkProxy configs
- Implement backward compatibility to ensure tests continue to work

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 12:34:24 +00:00
0674ca7163 3.34.0
Some checks failed
Default (tags) / security (push) Failing after 12m28s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 11:34:29 +00:00
e31c84493f feat(core): Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement. 2025-03-11 11:34:29 +00:00
d2ad659d37 3.33.0
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 14m16s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 09:57:06 +00:00
df7a12041e feat(portproxy): Add browser-friendly mode and SNI renegotiation configuration options to PortProxy 2025-03-11 09:57:06 +00:00
2b69150545 3.32.2
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 09:12:40 +00:00
85cc57ae10 fix(PortProxy): Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability. 2025-03-11 09:12:40 +00:00
e021b66898 3.32.1
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 04:39:17 +00:00
865d21b36a fix(portproxy): Relax TLS handshake and connection timeout settings for improved stability in chained proxy scenarios; update TLS session cache defaults and add keep-alive flags to connection records. 2025-03-11 04:39:17 +00:00
58ba0d9362 3.32.0
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 04:24:29 +00:00
ccccc5b8c8 feat(PortProxy): Enhance TLS session cache, SNI extraction, and chained proxy support in PortProxy. Improve handling of multiple and fragmented TLS records, and add new configuration options (isChainedProxy, chainPosition, aggressiveTlsRefresh, tlsSessionCache) for robust TLS certificate refresh. 2025-03-11 04:24:29 +00:00
d8466a866c 3.31.2
Some checks failed
Default (tags) / security (push) Successful in 28s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 03:56:09 +00:00
119b643690 fix(PortProxy): Improve SNI renegotiation handling by adding flexible domain configuration matching on rehandshake and session resumption events. 2025-03-11 03:56:09 +00:00
98f1e0df4c 3.31.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 03:48:10 +00:00
d6022c8f8a fix(PortProxy): Improve TLS handshake buffering and enhance debug logging for SNI forwarding in PortProxy 2025-03-11 03:48:10 +00:00
0ea0f02428 fix(PortProxy): Improve connection reliability for initial and resumed TLS sessions
Added enhanced connection handling to fix issues with both initial connections and TLS session resumption:

1. Improved debugging for connection setup with detailed logging
2. Added explicit timeout for backend connections to prevent hanging connections
3. Enhanced error recovery for connection failures with faster client notification
4. Added detailed session tracking to maintain domain context across TLS sessions
5. Fixed handling of TLS renegotiation with improved activity timestamp updates

This should address the issue where initial connections may fail but subsequent retries succeed,
as well as ensuring proper certificate selection for resumed TLS sessions.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 03:33:03 +00:00
e452f55203 3.31.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 03:16:04 +00:00
55f25f1976 feat(PortProxy): Improve TLS handshake SNI extraction and add session resumption tracking in PortProxy 2025-03-11 03:16:04 +00:00
98b7f3ed7f 3.30.8
Some checks failed
Default (tags) / security (push) Failing after 11m56s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 02:50:01 +00:00
cb83caeafd fix(core): No changes in this commit. 2025-03-11 02:50:01 +00:00
7850a80452 fix(PortProxy): Fix TypeScript errors by using correct variable names
Fixed TypeScript errors caused by using 'connectionRecord' instead of 'record' in TLS renegotiation handlers.
The variable name mistake occurred when moving and restructuring the TLS handshake detection code.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 02:47:57 +00:00
ef8f583a90 fix(PortProxy): Move TLS renegotiation detection before socket piping
Fundamentally restructured TLS renegotiation handling to ensure handshake packets are properly detected. The previous implementation attached event handlers after pipe() was established, which might have caused handshake packets to bypass detection. Key changes:

1. Moved renegotiation detection before pipe() to ensure all TLS handshake packets are detected
2. Added explicit lockedDomain setting for all SNI connections
3. Simplified the NetworkProxy TLS handshake detection
4. Removed redundant data handlers that could interfere with each other

These changes should make renegotiation detection more reliable regardless of how Node.js internal pipe() implementation handles data events.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 02:45:51 +00:00
2bdd6f8c1f fix(PortProxy): Update activity timestamp during TLS renegotiation to prevent connection timeouts
Ensures that TLS renegotiation packets properly update the connection's activity timestamp even when no SNI is present or when there are errors processing the renegotiation. This prevents connections from being closed due to inactivity during legitimate TLS renegotiation.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 02:40:08 +00:00
10 changed files with 3117 additions and 1017 deletions

View File

@ -1,5 +1,158 @@
# Changelog
## 2025-03-11 - 3.41.1 - fix(SniHandler)
Improve TLS SNI session resumption handling: connections containing a session ticket are now only rejected when no SNI is present and allowSessionTicket is disabled. Updated return values and logging for clearer resumption detection.
- Changed SniHandler.hasSessionResumption to return an object with 'isResumption' and 'hasSNI' flags.
- Adjusted PortProxy logic to only terminate connections when a session ticket is detected without an accompanying SNI (when allowSessionTicket is false).
- Enhanced debug logging to clearly differentiate between session resumption scenarios with and without SNI.
## 2025-03-11 - 3.41.0 - feat(PortProxy/TLS)
Add allowSessionTicket option to control TLS session ticket handling
- Introduce 'allowSessionTicket' flag (default true) in PortProxy settings to enable or disable TLS session resumption via session tickets.
- Update SniHandler with a new hasSessionResumption method to detect session ticket and PSK extensions in ClientHello messages.
- Force connection cleanup during renegotiation and initial handshake when allowSessionTicket is set to false and a session ticket is detected.
## 2025-03-11 - 3.40.0 - feat(SniHandler)
Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes
- Introduce a session cache mechanism to store and retrieve cached SNI values based on client IP (and optionally client random) to better handle tab reactivation scenarios.
- Implement functions to initialize, update, and clean up the session cache for TLS ClientHello messages.
- Enhance SNI extraction logic to check for tab reactivation handshakes and to return cached SNI for resumed connections or 0-RTT scenarios.
- Update PSK extension handling to safely skip over obfuscated ticket age bytes.
## 2025-03-11 - 3.39.0 - feat(PortProxy)
Add domain-specific NetworkProxy integration support to PortProxy
- Introduced new properties 'useNetworkProxy' and 'networkProxyPort' in domain configurations.
- Updated forwardToNetworkProxy to accept an optional custom proxy port parameter.
- Enhanced TLS handshake processing to extract SNI and, if a matching domain config specifies NetworkProxy usage, forward the connection using the domain-specific port.
- Refined connection routing logic to check for domain-specific NetworkProxy settings before falling back to default behavior.
## 2025-03-11 - 3.38.2 - fix(core)
No code changes detected; bumping patch version for consistency.
## 2025-03-11 - 3.38.1 - fix(PortProxy)
Improve SNI extraction handling in PortProxy by passing explicit connection info to extractSNIWithResumptionSupport for better TLS renegotiation and debug logging.
- In the renegotiation handler, create and pass a connection info object (sourceIp, sourcePort, destIp, destPort) instead of a boolean flag.
- Update the TLS handshake processing to construct a connection info object for detailed SNI extraction and logging.
- Enhance consistency by using processTlsPacket with cached SNI hints during fallback.
## 2025-03-11 - 3.38.0 - feat(SniHandler)
Enhance SNI extraction to support fragmented ClientHello messages, TLS 1.3 early data, and improved PSK parsing
- Added isTlsApplicationData method for detecting TLS application data packets
- Implemented handleFragmentedClientHello to buffer and reassemble fragmented ClientHello messages
- Extended extractSNIWithResumptionSupport to accept connection information and use reassembled data
- Added detection for TLS 1.3 early data (0-RTT) in the ClientHello, supporting session resumption scenarios
- Improved logging and heuristics for handling potential connection racing in modern browsers
## 2025-03-11 - 3.37.3 - fix(snihandler)
Enhance SNI extraction to support TLS 1.3 PSK-based session resumption by adding a dedicated extractSNIFromPSKExtension method and improved logging for session resumption indicators.
- Defined TLS_PSK_EXTENSION_TYPE and TLS_PSK_KE_MODES_EXTENSION_TYPE constants.
- Added extractSNIFromPSKExtension method to handle ClientHello messages containing PSK identities.
- Improved logging to indicate when session resumption indicators (ticket or PSK) are present but no standard SNI is found.
- Enhanced extractSNIWithResumptionSupport to attempt PSK extraction if standard SNI extraction fails.
## 2025-03-11 - 3.37.2 - fix(PortProxy)
Improve buffering and data handling during connection setup in PortProxy to prevent data loss
- Added a safeDataHandler and processDataQueue to buffer incoming data reliably during the TLS handshake phase
- Introduced a queue with pause/resume logic to avoid exceeding maxPendingDataSize and ensure all pending data is flushed before piping begins
- Refactored the piping setup to install the renegotiation handler only after proper data flushing
## 2025-03-11 - 3.37.1 - fix(PortProxy/SNI)
Refactor SNI extraction in PortProxy to use the dedicated SniHandler class
- Removed local SNI extraction and handshake detection functions from classes.portproxy.ts
- Introduced a standalone SniHandler class in ts/classes.snihandler.ts for robust SNI extraction and improved logging
- Replaced inlined calls to isTlsHandshake and extractSNI with calls to SniHandler methods
- Ensured consistency in handling TLS ClientHello messages across the codebase
## 2025-03-11 - 3.37.0 - feat(portproxy)
Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions
- Bumped version in package.json from 3.34.0 to 3.36.0 and updated commitinfo accordingly
- Updated dependencies: @push.rocks/tapbundle to ^5.5.10, @types/node to ^22.13.10, and @tsclass/tsclass to ^5.0.0
- Added ACME certificate management configuration to PortProxy settings (acme options, updateAcmeSettings, requestCertificate)
- Enhanced sync of domain configs to NetworkProxy with fallback for missing default certificates
## 2025-03-11 - 3.34.0 - feat(core)
Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement.
- Added support for TLD wildcard matching (e.g., 'example.*') to improve domain routing.
- Implemented complex wildcard pattern matching (e.g., '*.lossless*') in the router.
- Enhanced NetworkProxy integration by initializing a single NetworkProxy instance and forwarding TLS connections accordingly.
- Refactored TLS renegotiation handling to terminate connections on SNI mismatch for stricter enforcement.
- Updated tests to cover the new wildcard matching scenarios.
## 2025-03-11 - 3.33.0 - feat(portproxy)
Add browser-friendly mode and SNI renegotiation configuration options to PortProxy
- Introduce new properties: browserFriendlyMode (default true) to optimize handling for browser connections.
- Add allowRenegotiationWithDifferentSNI (default false) to enable or disable SNI changes during renegotiation.
- Include relatedDomainPatterns to define patterns for related domains that can share connections.
- Update TypeScript interfaces and internal renegotiation logic to support these options.
## 2025-03-11 - 3.32.2 - fix(PortProxy)
Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability.
- Removed legacy and deprecated fields related to chained proxy configurations (isChainedProxy, chainPosition, aggressiveTlsRefresh).
- Refactored the extractSNI functions to use a simpler, more robust approach for TLS ClientHello processing.
- Adjusted default timeout and keep-alive settings to more standard values (e.g. initialDataTimeout set to 60s, socketTimeout to 1h).
- Eliminated redundant TLS session cache and deep TLS refresh logic.
- Improved logging and error handling during connection setup and renegotiation phases.
## 2025-03-11 - 3.32.1 - fix(portproxy)
Relax TLS handshake and connection timeout settings for improved stability in chained proxy scenarios; update TLS session cache defaults and add keep-alive flags to connection records.
- Increased TLS session cache maximum entries from 10,000 to 20,000, expiry time from 24 hours to 7 days, and cleanup interval from 10 minutes to 30 minutes
- Relaxed socket timeouts: standalone connections now use up to 6 hours, with chained proxies adjusted for 56 hours based on proxy position
- Updated inactivity, connection, and initial handshake timeouts to provide a more relaxed behavior under high-traffic chained proxy scenarios
- Increased keepAliveInitialDelay from 10 seconds to 30 seconds and introduced separate incoming and outgoing keep-alive flags
- Enhanced TLS renegotiation handling with more detailed logging and temporary processing flags to avoid duplicate processing
- Updated NetworkProxy integration to use optimized connection settings and more aggressive application-level keep-alive probes
## 2025-03-11 - 3.32.0 - feat(PortProxy)
Enhance TLS session cache, SNI extraction, and chained proxy support in PortProxy. Improve handling of multiple and fragmented TLS records, and add new configuration options (isChainedProxy, chainPosition, aggressiveTlsRefresh, tlsSessionCache) for robust TLS certificate refresh.
- Implement TlsSessionCache with configurable cleanup, eviction, and statistics.
- Improve extractSNIInfo to process multiple TLS records and partial handshake data.
- Add new settings to detect chained proxy scenarios and adjust timeouts accordingly.
- Enhance TLS state refresh with aggressive probing and deep refresh sequence.
## 2025-03-11 - 3.31.2 - fix(PortProxy)
Improve SNI renegotiation handling by adding flexible domain configuration matching on rehandshake and session resumption events.
- When a rehandshake is detected with a changed SNI, first check existing domain config rules and log if allowed.
- If the exact domain config is not found, additionally attempt flexible matching using parent domain and wildcard patterns.
- For resumed sessions, try an exact match first and then use fallback logic to select a similar domain config based on matching target IP.
- Enhanced logging added to help diagnose missing or mismatched domain configurations.
## 2025-03-11 - 3.31.1 - fix(PortProxy)
Improve TLS handshake buffering and enhance debug logging for SNI forwarding in PortProxy
- Explicitly copy the initial TLS handshake data to prevent mutation before buffering
- Log buffered TLS handshake data with SNI information for better diagnostics
- Add detailed error logs on TLS connection failures, including server and domain config status
- Output additional debug messages during ClientHello forwarding to verify proper TLS handshake processing
## 2025-03-11 - 3.31.0 - feat(PortProxy)
Improve TLS handshake SNI extraction and add session resumption tracking in PortProxy
- Added ITlsSessionInfo interface and a global tlsSessionCache to track TLS session IDs for session resumption
- Implemented a cleanup timer for the TLS session cache with startSessionCleanupTimer and stopSessionCleanupTimer
- Enhanced extractSNIInfo to return detailed SNI information including session IDs, ticket details, and resumption status
- Updated renegotiation handlers to use extractSNIInfo for proper SNI extraction during TLS rehandshake
## 2025-03-11 - 3.30.8 - fix(core)
No changes in this commit.
## 2025-03-11 - 3.30.7 - fix(PortProxy)
Improve TLS renegotiation SNI handling by first checking if the new SNI is allowed under the existing domain config. If not, attempt to find an alternative domain config and update the locked domain accordingly; otherwise, terminate the connection on SNI mismatch.

View File

@ -1,8 +1,8 @@
{
"name": "@push.rocks/smartproxy",
"version": "3.30.7",
"version": "3.41.1",
"private": false,
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
@ -18,8 +18,8 @@
"@git.zone/tsbuild": "^2.2.6",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.0.77",
"@push.rocks/tapbundle": "^5.5.6",
"@types/node": "^22.13.9",
"@push.rocks/tapbundle": "^5.5.10",
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
},
"dependencies": {
@ -28,7 +28,7 @@
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartstring": "^4.0.15",
"@tsclass/tsclass": "^4.4.3",
"@tsclass/tsclass": "^5.0.0",
"@types/minimatch": "^5.1.2",
"@types/ws": "^8.18.0",
"acme-client": "^5.4.0",

119
pnpm-lock.yaml generated
View File

@ -24,8 +24,8 @@ importers:
specifier: ^4.0.15
version: 4.0.15
'@tsclass/tsclass':
specifier: ^4.4.3
version: 4.4.3
specifier: ^5.0.0
version: 5.0.0
'@types/minimatch':
specifier: ^5.1.2
version: 5.1.2
@ -55,11 +55,11 @@ importers:
specifier: ^1.0.77
version: 1.0.96(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)(typescript@5.8.2)
'@push.rocks/tapbundle':
specifier: ^5.5.6
version: 5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
specifier: ^5.5.10
version: 5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
'@types/node':
specifier: ^22.13.9
version: 22.13.9
specifier: ^22.13.10
version: 22.13.10
typescript:
specifier: ^5.8.2
version: 5.8.2
@ -941,8 +941,8 @@ packages:
'@push.rocks/smartyaml@2.0.5':
resolution: {integrity: sha512-tBcf+HaOIfeEsTMwgUZDtZERCxXQyRsWO8Ar5DjBdiSRchbhVGZQEBzXswMS0W5ZoRenjgPK+4tPW3JQGRTfbg==}
'@push.rocks/tapbundle@5.5.6':
resolution: {integrity: sha512-V6u+nZwt4fNccxbm3ztZgHr/QAj/uKhaaOUFgtaae0jzYdds4jNEI+mXLpfXuNMgm7Nx93Lk5XUxWKTI8drjNw==}
'@push.rocks/tapbundle@5.5.10':
resolution: {integrity: sha512-vTGzd3/kzKp8s6jrREGIKGG+87fy7grcTZIelVDpyZMtBdIi9Fe7g8EIw/reVx8oTAFSuUEAEaAT8B5NDIFStg==}
'@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@ -1315,8 +1315,11 @@ packages:
'@tsclass/tsclass@3.0.48':
resolution: {integrity: sha512-hC65UvDlp9qvsl6OcIZXz0JNiWZ0gyzsTzbXpg215sGxopgbkOLCr6E0s4qCTnweYm95gt2AdY95uP7M7kExaQ==}
'@tsclass/tsclass@4.4.3':
resolution: {integrity: sha512-Vhp+B1UsYlwXLhIeds++CXEeCwFgRzpput4YNM7Qyhr+UQgIMFRFAs2HSI3jEE5r9c1hR9G6MkSxi2U/CLyiaA==}
'@tsclass/tsclass@4.4.4':
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
'@tsclass/tsclass@5.0.0':
resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
'@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@ -1472,8 +1475,8 @@ packages:
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
'@types/node@22.13.9':
resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==}
'@types/node@22.13.10':
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
'@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
@ -4373,7 +4376,7 @@ snapshots:
'@push.rocks/taskbuffer': 3.1.7
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstore': 2.0.20
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@types/express': 4.17.21
body-parser: 1.20.3
cors: 2.8.5
@ -5231,7 +5234,7 @@ snapshots:
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartshell': 3.2.3
'@push.rocks/tapbundle': 5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
'@push.rocks/tapbundle': 5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
'@types/ws': 8.18.0
figures: 6.1.0
ws: 8.18.1
@ -5281,7 +5284,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/yargs': 17.0.33
chalk: 4.1.2
@ -5529,7 +5532,7 @@ snapshots:
'@push.rocks/smartstring': 4.0.15
'@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.1.7
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
transitivePeerDependencies:
- aws-crt
@ -5551,7 +5554,7 @@ snapshots:
'@pushrocks/smartjson': 4.0.6
'@pushrocks/smartpath': 5.0.5
'@pushrocks/smartpromise': 3.1.10
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
mongodb: 4.17.2
transitivePeerDependencies:
- aws-crt
@ -5602,7 +5605,7 @@ snapshots:
'@push.rocks/smartstream': 3.2.5
'@push.rocks/smartstring': 4.0.15
'@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
transitivePeerDependencies:
- aws-crt
@ -5652,7 +5655,7 @@ snapshots:
'@push.rocks/smarttime': 4.1.1
'@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.1.7
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
mongodb: 6.14.2(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
@ -5764,7 +5767,7 @@ snapshots:
'@push.rocks/smartlog-interfaces@3.0.2':
dependencies:
'@api.global/typedrequest-interfaces': 2.0.2
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@push.rocks/smartlog@3.0.7':
dependencies:
@ -5879,7 +5882,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.8.2)
'@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@types/express': 5.0.0
express: 4.21.2
pdf-lib: 1.17.1
@ -5929,7 +5932,7 @@ snapshots:
'@push.rocks/smartbucket': 3.3.7
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartpath': 5.0.18
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@types/s3rver': 3.7.4
s3rver: 3.7.1
transitivePeerDependencies:
@ -5952,7 +5955,7 @@ snapshots:
'@push.rocks/smartxml': 1.1.1
'@push.rocks/smartyaml': 2.0.5
'@push.rocks/webrequest': 3.0.37
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@push.rocks/smartsocket@2.0.27':
dependencies:
@ -6058,7 +6061,7 @@ snapshots:
'@types/js-yaml': 3.12.10
js-yaml: 3.14.1
'@push.rocks/tapbundle@5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)':
'@push.rocks/tapbundle@5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)':
dependencies:
'@open-wc/testing': 4.0.0
'@push.rocks/consolecolor': 2.0.2
@ -6112,7 +6115,7 @@ snapshots:
dependencies:
'@pushrocks/smartdelay': 3.0.1
'@pushrocks/smartpromise': 4.0.2
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@push.rocks/webstore@2.0.20':
dependencies:
@ -6654,20 +6657,24 @@ snapshots:
dependencies:
type-fest: 2.19.0
'@tsclass/tsclass@4.4.3':
'@tsclass/tsclass@4.4.4':
dependencies:
type-fest: 4.37.0
'@tsclass/tsclass@5.0.0':
dependencies:
type-fest: 4.37.0
'@types/accepts@1.3.7':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/babel__code-frame@7.0.6': {}
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/buffer-json@2.0.3': {}
@ -6683,17 +6690,17 @@ snapshots:
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
source-map: 0.6.1
'@types/co-body@6.1.3':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/qs': 6.9.18
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/content-disposition@0.5.8': {}
@ -6706,11 +6713,11 @@ snapshots:
'@types/connect': 3.4.38
'@types/express': 5.0.0
'@types/keygrip': 1.0.6
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/cors@2.8.17':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/debounce@1.2.4': {}
@ -6724,14 +6731,14 @@ snapshots:
'@types/express-serve-static-core@4.19.6':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@ -6756,30 +6763,30 @@ snapshots:
'@types/from2@2.3.5':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/gunzip-maybe@1.4.2':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/hast@3.0.4':
dependencies:
@ -6813,7 +6820,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/keygrip@1.0.6': {}
@ -6830,7 +6837,7 @@ snapshots:
'@types/http-errors': 2.0.4
'@types/keygrip': 1.0.6
'@types/koa-compose': 3.2.8
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/mdast@4.0.4':
dependencies:
@ -6848,9 +6855,9 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/node@22.13.9':
'@types/node@22.13.10':
dependencies:
undici-types: 6.20.0
@ -6868,19 +6875,19 @@ snapshots:
'@types/s3rver@3.7.4':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/semver@7.5.8': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/send': 0.17.4
'@types/sinon-chai@3.2.12':
@ -6900,11 +6907,11 @@ snapshots:
'@types/tar-stream@2.2.3':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/through2@2.0.41':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/triple-beam@1.3.5': {}
@ -6928,18 +6935,18 @@ snapshots:
'@types/whatwg-url@8.2.2':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {}
'@types/ws@7.4.7':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/ws@8.18.0':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/yargs-parser@21.0.3': {}
@ -6949,7 +6956,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
optional: true
'@ungap/structured-clone@1.3.0': {}
@ -7598,7 +7605,7 @@ snapshots:
dependencies:
'@types/cookie': 0.4.1
'@types/cors': 2.8.17
'@types/node': 22.13.9
'@types/node': 22.13.10
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.4.2
@ -8370,7 +8377,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.13.9
'@types/node': 22.13.10
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11

View File

@ -197,6 +197,52 @@ tap.test('should match wildcard subdomains', async () => {
expect(result).toEqual(wildcardConfig);
});
// Test TLD wildcards (example.*)
tap.test('should match TLD wildcards', async () => {
const tldWildcardConfig = createProxyConfig('example.*');
router.setNewProxyConfigs([tldWildcardConfig]);
// Test that example.com matches example.*
const req1 = createMockRequest('example.com');
const result1 = router.routeReq(req1);
expect(result1).toBeTruthy();
expect(result1).toEqual(tldWildcardConfig);
// Test that example.org matches example.*
const req2 = createMockRequest('example.org');
const result2 = router.routeReq(req2);
expect(result2).toBeTruthy();
expect(result2).toEqual(tldWildcardConfig);
// Test that subdomain.example.com doesn't match example.*
const req3 = createMockRequest('subdomain.example.com');
const result3 = router.routeReq(req3);
expect(result3).toBeUndefined();
});
// Test complex pattern matching (*.lossless*)
tap.test('should match complex wildcard patterns', async () => {
const complexWildcardConfig = createProxyConfig('*.lossless*');
router.setNewProxyConfigs([complexWildcardConfig]);
// Test that sub.lossless.com matches *.lossless*
const req1 = createMockRequest('sub.lossless.com');
const result1 = router.routeReq(req1);
expect(result1).toBeTruthy();
expect(result1).toEqual(complexWildcardConfig);
// Test that api.lossless.org matches *.lossless*
const req2 = createMockRequest('api.lossless.org');
const result2 = router.routeReq(req2);
expect(result2).toBeTruthy();
expect(result2).toEqual(complexWildcardConfig);
// Test that losslessapi.com matches *.lossless*
const req3 = createMockRequest('losslessapi.com');
const result3 = router.routeReq(req3);
expect(result3).toBeUndefined(); // Should not match as it doesn't have a subdomain
});
// Test default configuration fallback
tap.test('should fall back to default configuration', async () => {
const defaultConfig = createProxyConfig('*');

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.30.7',
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
version: '3.41.1',
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
}

View File

@ -1,5 +1,6 @@
import * as plugins from './plugins.js';
import { ProxyRouter } from './classes.router.js';
import { AcmeCertManager, CertManagerEvents } from './classes.port80handler.js';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
@ -20,6 +21,18 @@ export interface INetworkProxyOptions {
// New settings for PortProxy integration
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
// ACME certificate management options
acme?: {
enabled?: boolean; // Whether to enable automatic certificate management
port?: number; // Port to listen on for ACME challenges (default: 80)
contactEmail?: string; // Email for Let's Encrypt account
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
certificateStore?: string; // Directory to store certificates (default: ./certs)
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
};
}
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
@ -59,12 +72,19 @@ export class NetworkProxy {
private defaultCertificates: { key: string; cert: string };
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
// ACME certificate manager
private certManager: AcmeCertManager | null = null;
private certificateStoreDir: string;
// New connection pool for backend connections
private connectionPool: Map<string, Array<{
socket: plugins.net.Socket;
lastUsed: number;
isIdle: boolean;
}>> = new Map();
// Track round-robin positions for load balancing
private roundRobinPositions: Map<string, number> = new Map();
/**
* Creates a new NetworkProxy instance
@ -85,9 +105,33 @@ export class NetworkProxy {
},
// New defaults for PortProxy integration
connectionPoolSize: optionsArg.connectionPoolSize || 50,
portProxyIntegration: optionsArg.portProxyIntegration || false
portProxyIntegration: optionsArg.portProxyIntegration || false,
// Default ACME options
acme: {
enabled: optionsArg.acme?.enabled || false,
port: optionsArg.acme?.port || 80,
contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
certificateStore: optionsArg.acme?.certificateStore || './certs',
skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
}
};
// Set up certificate store directory
this.certificateStoreDir = path.resolve(this.options.acme.certificateStore);
// Ensure certificate store directory exists
try {
if (!fs.existsSync(this.certificateStoreDir)) {
fs.mkdirSync(this.certificateStoreDir, { recursive: true });
this.log('info', `Created certificate store directory: ${this.certificateStoreDir}`);
}
} catch (error) {
this.log('warn', `Failed to create certificate store directory: ${error}`);
}
this.loadDefaultCertificates();
}
@ -330,17 +374,230 @@ export class NetworkProxy {
}
}
/**
* Initializes the ACME certificate manager for automatic certificate issuance
* @private
*/
private async initializeAcmeManager(): Promise<void> {
if (!this.options.acme.enabled) {
return;
}
// Create certificate manager
this.certManager = new AcmeCertManager({
port: this.options.acme.port,
contactEmail: this.options.acme.contactEmail,
useProduction: this.options.acme.useProduction,
renewThresholdDays: this.options.acme.renewThresholdDays,
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
renewCheckIntervalHours: 24 // Check daily for renewals
});
// Register event handlers
this.certManager.on(CertManagerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
this.certManager.on(CertManagerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
this.certManager.on(CertManagerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
this.certManager.on(CertManagerEvents.CERTIFICATE_EXPIRING, (data) => {
this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
});
// Start the manager
try {
await this.certManager.start();
this.log('info', `ACME Certificate Manager started on port ${this.options.acme.port}`);
// Add domains from proxy configs
this.registerDomainsWithAcmeManager();
} catch (error) {
this.log('error', `Failed to start ACME Certificate Manager: ${error}`);
this.certManager = null;
}
}
/**
* Registers domains from proxy configs with the ACME manager
* @private
*/
private registerDomainsWithAcmeManager(): void {
if (!this.certManager) return;
// Get all hostnames from proxy configs
this.proxyConfigs.forEach(config => {
const hostname = config.hostName;
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
if (hostname.includes('*')) {
this.log('info', `Skipping wildcard domain for ACME: ${hostname}`);
return;
}
// Skip domains already with certificates if configured to do so
if (this.options.acme.skipConfiguredCerts) {
const cachedCert = this.certificateCache.get(hostname);
if (cachedCert) {
this.log('info', `Skipping domain with existing certificate: ${hostname}`);
return;
}
}
// Check for existing certificate in the store
const certPath = path.join(this.certificateStoreDir, `${hostname}.cert.pem`);
const keyPath = path.join(this.certificateStoreDir, `${hostname}.key.pem`);
try {
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
// Load existing certificate and key
const cert = fs.readFileSync(certPath, 'utf8');
const key = fs.readFileSync(keyPath, 'utf8');
// Extract expiry date from certificate if possible
let expiryDate: Date | undefined;
try {
const matches = cert.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
if (matches && matches[1]) {
expiryDate = new Date(matches[1]);
}
} catch (error) {
this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`);
}
// Update the certificate in the manager
this.certManager.setCertificate(hostname, cert, key, expiryDate);
// Also update our own certificate cache
this.updateCertificateCache(hostname, cert, key, expiryDate);
this.log('info', `Loaded existing certificate for ${hostname}`);
} else {
// Register the domain for certificate issuance
this.certManager.addDomain(hostname);
this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`);
}
} catch (error) {
this.log('error', `Error registering domain ${hostname} with ACME manager: ${error}`);
}
});
}
/**
* Handles newly issued or renewed certificates from ACME manager
* @private
*/
private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
const { domain, certificate, privateKey, expiryDate } = data;
this.log('info', `Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
// Update certificate in HTTPS server
this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
// Save the certificate to the filesystem
this.saveCertificateToStore(domain, certificate, privateKey);
}
/**
* Handles certificate issuance failures
* @private
*/
private handleCertificateFailed(data: { domain: string; error: string }): void {
this.log('error', `Certificate issuance failed for ${data.domain}: ${data.error}`);
}
/**
* Saves certificate and private key to the filesystem
* @private
*/
private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
try {
const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
fs.writeFileSync(certPath, certificate);
fs.writeFileSync(keyPath, privateKey);
// Ensure private key has restricted permissions
try {
fs.chmodSync(keyPath, 0o600);
} catch (error) {
this.log('warn', `Failed to set permissions on private key for ${domain}: ${error}`);
}
this.log('info', `Saved certificate for ${domain} to ${certPath}`);
} catch (error) {
this.log('error', `Failed to save certificate for ${domain}: ${error}`);
}
}
/**
* Handles SNI (Server Name Indication) for TLS connections
* Used by the HTTPS server to select the correct certificate for each domain
* @private
*/
private handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
this.log('debug', `SNI request for domain: ${domain}`);
// Check if we have a certificate for this domain
const certs = this.certificateCache.get(domain);
if (certs) {
try {
// Create TLS context with the cached certificate
const context = plugins.tls.createSecureContext({
key: certs.key,
cert: certs.cert
});
this.log('debug', `Using cached certificate for ${domain}`);
cb(null, context);
return;
} catch (err) {
this.log('error', `Error creating secure context for ${domain}:`, err);
}
}
// Check if we should trigger certificate issuance
if (this.options.acme?.enabled && this.certManager && !domain.includes('*')) {
// Check if this domain is already registered
const certData = this.certManager.getCertificate(domain);
if (!certData) {
this.log('info', `No certificate found for ${domain}, registering for issuance`);
this.certManager.addDomain(domain);
}
}
// Fall back to default certificate
try {
const context = plugins.tls.createSecureContext({
key: this.defaultCertificates.key,
cert: this.defaultCertificates.cert
});
this.log('debug', `Using default certificate for ${domain}`);
cb(null, context);
} catch (err) {
this.log('error', `Error creating default secure context:`, err);
cb(new Error('Cannot create secure context'), null);
}
}
/**
* Starts the proxy server
*/
public async start(): Promise<void> {
this.startTime = Date.now();
// Initialize ACME certificate manager if enabled
if (this.options.acme.enabled) {
await this.initializeAcmeManager();
}
// Create the HTTPS server
this.httpsServer = plugins.https.createServer(
{
key: this.defaultCertificates.key,
cert: this.defaultCertificates.cert
cert: this.defaultCertificates.cert,
SNICallback: (domain, cb) => this.handleSNI(domain, cb)
},
(req, res) => this.handleRequest(req, res)
);
@ -556,7 +813,10 @@ export class NetworkProxy {
const outGoingDeferred = plugins.smartpromise.defer();
try {
const wsTarget = `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`;
// Select destination IP and port for WebSocket
const wsDestinationIp = this.selectDestinationIp(wsDestinationConfig);
const wsDestinationPort = this.selectDestinationPort(wsDestinationConfig);
const wsTarget = `ws://${wsDestinationIp}:${wsDestinationPort}${reqArg.url}`;
this.log('debug', `Proxying WebSocket to ${wsTarget}`);
wsOutgoing = new plugins.wsDefault(wsTarget);
@ -688,8 +948,12 @@ export class NetworkProxy {
const useConnectionPool = this.options.portProxyIntegration &&
originRequest.socket.remoteAddress?.includes('127.0.0.1');
// Select destination IP and port from the arrays
const destinationIp = this.selectDestinationIp(destinationConfig);
const destinationPort = this.selectDestinationPort(destinationConfig);
// Construct destination URL
const destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
const destinationUrl = `http://${destinationIp}:${destinationPort}${originRequest.url}`;
if (useConnectionPool) {
this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
@ -697,8 +961,8 @@ export class NetworkProxy {
reqId,
originRequest,
originResponse,
destinationConfig.destinationIp,
destinationConfig.destinationPort,
destinationIp,
destinationPort,
originRequest.url
);
} else {
@ -1084,6 +1348,80 @@ export class NetworkProxy {
}
}
/**
* Selects a destination IP from the array using round-robin
* @param config The proxy configuration
* @returns A destination IP address
*/
private selectDestinationIp(config: plugins.tsclass.network.IReverseProxyConfig): string {
// For array-based configs
if (Array.isArray(config.destinationIps) && config.destinationIps.length > 0) {
// Get the current position or initialize it
const key = `ip_${config.hostName}`;
let position = this.roundRobinPositions.get(key) || 0;
// Select the IP using round-robin
const selectedIp = config.destinationIps[position];
// Update the position for next time
position = (position + 1) % config.destinationIps.length;
this.roundRobinPositions.set(key, position);
return selectedIp;
}
// For backward compatibility with test suites that rely on specific behavior
// Check if there's a proxyConfigs entry that matches this hostname
const matchingConfig = this.proxyConfigs.find(cfg =>
cfg.hostName === config.hostName &&
(cfg as any).destinationIp
);
if (matchingConfig) {
return (matchingConfig as any).destinationIp;
}
// Fallback to localhost
return 'localhost';
}
/**
* Selects a destination port from the array using round-robin
* @param config The proxy configuration
* @returns A destination port number
*/
private selectDestinationPort(config: plugins.tsclass.network.IReverseProxyConfig): number {
// For array-based configs
if (Array.isArray(config.destinationPorts) && config.destinationPorts.length > 0) {
// Get the current position or initialize it
const key = `port_${config.hostName}`;
let position = this.roundRobinPositions.get(key) || 0;
// Select the port using round-robin
const selectedPort = config.destinationPorts[position];
// Update the position for next time
position = (position + 1) % config.destinationPorts.length;
this.roundRobinPositions.set(key, position);
return selectedPort;
}
// For backward compatibility with test suites that rely on specific behavior
// Check if there's a proxyConfigs entry that matches this hostname
const matchingConfig = this.proxyConfigs.find(cfg =>
cfg.hostName === config.hostName &&
(cfg as any).destinationPort
);
if (matchingConfig) {
return parseInt((matchingConfig as any).destinationPort, 10);
}
// Fallback to port 80
return 80;
}
/**
* Updates proxy configurations
*/
@ -1144,6 +1482,48 @@ export class NetworkProxy {
}
}
/**
* Converts PortProxy domain configurations to NetworkProxy configs
* @param domainConfigs PortProxy domain configs
* @param sslKeyPair Default SSL key pair to use if not specified
* @returns Array of NetworkProxy configs
*/
public convertPortProxyConfigs(
domainConfigs: Array<{
domains: string[];
targetIPs?: string[];
allowedIPs?: string[];
}>,
sslKeyPair?: { key: string; cert: string }
): plugins.tsclass.network.IReverseProxyConfig[] {
const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
// Use default certificates if not provided
const sslKey = sslKeyPair?.key || this.defaultCertificates.key;
const sslCert = sslKeyPair?.cert || this.defaultCertificates.cert;
for (const domainConfig of domainConfigs) {
// Each domain in the domains array gets its own config
for (const domain of domainConfig.domains) {
// Skip non-hostname patterns (like IP addresses)
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
continue;
}
proxyConfigs.push({
hostName: domain,
destinationIps: domainConfig.targetIPs || ['localhost'],
destinationPorts: [this.options.port], // Use the NetworkProxy port
privateKey: sslKey,
publicKey: sslCert
});
}
}
this.log('info', `Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
return proxyConfigs;
}
/**
* Adds default headers to be included in all responses
*/
@ -1208,6 +1588,16 @@ export class NetworkProxy {
}
this.connectionPool.clear();
// Stop ACME certificate manager if it's running
if (this.certManager) {
try {
await this.certManager.stop();
this.log('info', 'ACME Certificate Manager stopped');
} catch (error) {
this.log('error', 'Error stopping ACME Certificate Manager', error);
}
}
// Close the HTTPS server
return new Promise((resolve) => {
this.httpsServer.close(() => {
@ -1217,6 +1607,71 @@ export class NetworkProxy {
});
}
/**
* Requests a new certificate for a domain
* This can be used to manually trigger certificate issuance
* @param domain The domain to request a certificate for
* @returns A promise that resolves when the request is submitted (not when the certificate is issued)
*/
public async requestCertificate(domain: string): Promise<boolean> {
if (!this.options.acme.enabled) {
this.log('warn', 'ACME certificate management is not enabled');
return false;
}
if (!this.certManager) {
this.log('error', 'ACME certificate manager is not initialized');
return false;
}
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
if (domain.includes('*')) {
this.log('error', `Cannot request certificate for wildcard domain: ${domain}`);
return false;
}
try {
this.certManager.addDomain(domain);
this.log('info', `Certificate request submitted for domain: ${domain}`);
return true;
} catch (error) {
this.log('error', `Error requesting certificate for domain ${domain}:`, error);
return false;
}
}
/**
* Updates the certificate cache for a domain
* @param domain The domain name
* @param certificate The certificate (PEM format)
* @param privateKey The private key (PEM format)
* @param expiryDate Optional expiry date
*/
private updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
// Update certificate context in HTTPS server if it's running
if (this.httpsServer) {
try {
this.httpsServer.addContext(domain, {
key: privateKey,
cert: certificate
});
this.log('debug', `Updated SSL context for domain: ${domain}`);
} catch (error) {
this.log('error', `Error updating SSL context for domain ${domain}:`, error);
}
}
// Update certificate in cache
this.certificateCache.set(domain, {
key: privateKey,
cert: certificate,
expires: expiryDate
});
// Add to active contexts set
this.activeContexts.add(domain);
}
/**
* Logs a message according to the configured log level
*/

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,21 @@ export interface IRouterResult {
pathRemainder?: string;
}
/**
* Router for HTTP reverse proxy requests
*
* Supports the following domain matching patterns:
* - Exact matches: "example.com"
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
* - Default fallback: "*" (matches any unmatched domain)
*
* Also supports path pattern matching for each domain:
* - Exact path: "/api/users"
* - Wildcard paths: "/api/*"
* - Path parameters: "/users/:id/profile"
*/
export class ProxyRouter {
// Store original configs for reference
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
@ -98,9 +113,11 @@ export class ProxyRouter {
return exactConfig;
}
// Try wildcard subdomain
// Try various wildcard patterns
if (hostWithoutPort.includes('.')) {
const domainParts = hostWithoutPort.split('.');
// Try wildcard subdomain (*.example.com)
if (domainParts.length > 2) {
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
@ -108,6 +125,23 @@ export class ProxyRouter {
return wildcardConfig;
}
}
// Try TLD wildcard (example.*)
const baseDomain = domainParts.slice(0, -1).join('.');
const tldWildcardDomain = `${baseDomain}.*`;
const tldWildcardConfig = this.findConfigForHost(tldWildcardDomain, urlPath);
if (tldWildcardConfig) {
return tldWildcardConfig;
}
// Try complex wildcard patterns
const wildcardPatterns = this.findWildcardMatches(hostWithoutPort);
for (const pattern of wildcardPatterns) {
const wildcardConfig = this.findConfigForHost(pattern, urlPath);
if (wildcardConfig) {
return wildcardConfig;
}
}
}
// Fall back to default config if available
@ -120,6 +154,53 @@ export class ProxyRouter {
return undefined;
}
/**
* Find potential wildcard patterns that could match a given hostname
* Handles complex patterns like "*.lossless*" or other partial matches
* @param hostname The hostname to find wildcard matches for
* @returns Array of potential wildcard patterns that could match
*/
private findWildcardMatches(hostname: string): string[] {
const patterns: string[] = [];
const hostnameParts = hostname.split('.');
// Find all configured hostnames that contain wildcards
const wildcardConfigs = this.reverseProxyConfigs.filter(
config => config.hostName.includes('*')
);
// Extract unique wildcard patterns
const wildcardPatterns = [...new Set(
wildcardConfigs.map(config => config.hostName.toLowerCase())
)];
// For each wildcard pattern, check if it could match the hostname
// using simplified regex pattern matching
for (const pattern of wildcardPatterns) {
// Skip the default wildcard '*'
if (pattern === '*') continue;
// Skip already checked patterns (*.domain.com and domain.*)
if (pattern.startsWith('*.') && pattern.indexOf('*', 2) === -1) continue;
if (pattern.endsWith('.*') && pattern.indexOf('*') === pattern.length - 1) continue;
// Convert wildcard pattern to regex
const regexPattern = pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*'); // Convert * to .* for regex
// Create regex object with case insensitive flag
const regex = new RegExp(`^${regexPattern}$`, 'i');
// If hostname matches this complex pattern, add it to the list
if (regex.test(hostname)) {
patterns.push(pattern);
}
}
return patterns;
}
/**
* Find a config for a specific host and path
*/

1285
ts/classes.snihandler.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,3 +3,4 @@ export * from './classes.networkproxy.js';
export * from './classes.portproxy.js';
export * from './classes.port80handler.js';
export * from './classes.sslredirect.js';
export * from './classes.snihandler.js';