Compare commits

...

416 Commits

Author SHA1 Message Date
b10f35be4b 4.2.2
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-18 00:29:17 +00:00
426249e70e fix(connectionhandler): Ensure proper termination of TLS connections without SNI by explicitly ending the socket after sending the unrecognized_name alert. This prevents the connection from hanging and avoids potential duplicate handling. 2025-03-18 00:29:17 +00:00
ba0d9d0b8e 4.2.1
Some checks failed
Default (tags) / security (push) Failing after 14m48s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-17 14:28:09 +00:00
151b8f498c fix(core): No uncommitted changes detected in the project. 2025-03-17 14:28:08 +00:00
0db4b07b22 4.2.0
Some checks failed
Default (tags) / security (push) Successful in 58s
Default (tags) / test (push) Failing after 14m46s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-17 14:27:10 +00:00
b55e2da23e feat(tlsalert): add sendForceSniSequence and sendFatalAndClose helper functions to TlsAlert for improved SNI enforcement 2025-03-17 14:27:10 +00:00
3593e411cf 4.1.16
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:37:48 +00:00
ca6f6de798 fix(tls): Improve TLS alert handling in connection handler: use the new TlsAlert class to send proper unrecognized_name alerts when a ClientHello is missing SNI and wait for a retry on the same connection before closing. Also, add alertFallbackTimeout tracking to connection records for better timeout management. 2025-03-17 13:37:48 +00:00
80d2f30804 4.1.15
Some checks failed
Default (tags) / security (push) Failing after 14m48s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-17 13:23:07 +00:00
22f46700f1 fix(connectionhandler): Delay socket termination in TLS session resumption handling to allow proper alert processing 2025-03-17 13:23:07 +00:00
1611f65455 4.1.14
Some checks failed
Default (tags) / security (push) Successful in 21s
Default (tags) / test (push) Failing after 1m9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:19:18 +00:00
c6350e271a fix(ConnectionHandler): Use the correct TLS alert data and increase the delay before socket termination when session resumption without SNI is detected. 2025-03-17 13:19:18 +00:00
0fb5e5ea50 4.1.13
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:15:12 +00:00
35f6739b3c fix(tls-handshake): Set certificate_expired TLS alert level to warning instead of fatal to allow graceful termination. 2025-03-17 13:15:12 +00:00
4634c68ea6 4.1.12
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:09:54 +00:00
e126032b61 fix(classes.pp.connectionhandler): Replace unrecognized_name alert data with certificate_expired alert in TLS handshake handling for session resumption without SNI 2025-03-17 13:09:54 +00:00
7797c799dd 4.1.11
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-17 13:00:02 +00:00
e8639e1b01 fix(connectionhandler): Increase delay before cleaning up connections when session resumption is blocked due to missing SNI, allowing more natural socket termination. 2025-03-17 13:00:02 +00:00
60a0ad106d 4.1.10
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 14:49:25 +00:00
a70c123007 fix(connectionhandler): Increase delay timings for TLS alert transmission in session ticket blocking to allow graceful socket termination 2025-03-16 14:49:25 +00:00
46aa7620b0 4.1.9
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 14:13:36 +00:00
f72db86e37 fix(ConnectionHandler): Replace closeNotify alert with handshake failure alert in TLS ClientHello handling to properly signal missing SNI and enforce session ticket restrictions. 2025-03-16 14:13:35 +00:00
d612df107e 4.1.8
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 14:02:18 +00:00
1c34578c36 fix(ConnectionHandler/tls): Change the TLS alert sent when a ClientHello lacks SNI: use the close_notify alert instead of handshake_failure to prompt immediate retry with SNI. 2025-03-16 14:02:18 +00:00
1f9943b5a7 4.1.7
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 13:47:34 +00:00
67ddf97547 fix(classes.pp.connectionhandler): Improve TLS alert handling in ClientHello when SNI is missing and session tickets are disallowed 2025-03-16 13:47:34 +00:00
8a96b45ece 4.1.6
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 13:28:48 +00:00
2b6464acd5 fix(tls): Refine TLS ClientHello handling when allowSessionTicket is false by replacing extensive alert timeout logic with a concise warning alert and short delay, encouraging immediate client retry with proper SNI 2025-03-16 13:28:48 +00:00
efbb4335d7 4.1.5
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-16 13:19:37 +00:00
9dd402054d fix(TLS/ConnectionHandler): Improve handling of TLS session resumption without SNI by sending an unrecognized_name alert instead of immediately terminating the connection. This change adds a grace period for the client to retry the handshake with proper SNI and cleans up the connection if no valid response is received. 2025-03-16 13:19:37 +00:00
6c1efc1dc0 4.1.4
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-15 19:10:54 +00:00
cad0e6a2b2 fix(ConnectionHandler): Refactor ConnectionHandler code formatting for improved readability and consistency in log messages and whitespace handling 2025-03-15 19:10:54 +00:00
794e1292e5 4.1.3
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-15 18:51:50 +00:00
ee79f9ab7c fix(connectionhandler): Improve handling of TLS ClientHello messages when allowSessionTicket is disabled and no SNI is provided by sending a warning alert (unrecognized_name, code 0x70) with a proper callback and delay to ensure the alert is transmitted before closing the connection. 2025-03-15 18:51:50 +00:00
107bc3b50b 4.1.2
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-15 17:16:18 +00:00
97982976c8 fix(connectionhandler): Send proper TLS alert before terminating connections when SNI is missing and session tickets are disallowed. 2025-03-15 17:16:18 +00:00
fe60f88746 4.1.1
Some checks failed
Default (tags) / security (push) Failing after 12m44s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-15 17:00:11 +00:00
252a987344 fix(tls): Enforce strict SNI handling in TLS connections by terminating ClientHello messages lacking SNI when session tickets are disallowed and removing legacy session cache code. 2025-03-15 17:00:10 +00:00
677d30563f 4.1.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-14 11:34:53 +00:00
9aa747b5d4 feat(SniHandler): Enhance SNI extraction to support session caching and tab reactivation by adding session cache initialization, cleanup and helper methods. Update processTlsPacket to use cached SNI for session resumption and connection racing scenarios. 2025-03-14 11:34:52 +00:00
1de9491e1d 4.0.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-14 09:53:25 +00:00
e2ee673197 BREAKING CHANGE(core): refactor: reorganize internal module structure to use classes.pp.* modules
- Renamed port proxy and SNI handler source files to classes.pp.portproxy.js and classes.pp.snihandler.js respectively
- Updated import paths in index.ts and test files (e.g. in test.ts and test.router.ts) to reference the new file names
- This refactor improves code organization but breaks direct imports from the old paths
2025-03-14 09:53:25 +00:00
985031e9ac 3.41.8
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 15:49:42 +00:00
4c0105ad09 fix(portproxy): Improve TLS handshake timeout handling and connection piping in PortProxy 2025-03-12 15:49:41 +00:00
06896b3102 3.41.7
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 12:19:36 +00:00
7fe455b4df fix(core): Refactor PortProxy and SniHandler: improve configuration handling, logging, and whitespace consistency 2025-03-12 12:19:36 +00:00
21801aa53d 3.41.6
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:54:24 +00:00
ddfbcdb1f3 fix(SniHandler): Refactor SniHandler: update whitespace, comment formatting, and consistent type definitions 2025-03-12 10:54:24 +00:00
b401d126bc 3.41.5
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:27:26 +00:00
baaee0ad4d fix(portproxy): Enforce TLS handshake and SNI validation on port 443 by blocking non-TLS connections and terminating session resumption attempts without SNI when allowSessionTicket is disabled. 2025-03-12 10:27:25 +00:00
fe7c4c2f5e 3.41.4
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:01:54 +00:00
ab1ec84832 fix(tls/sni): Improve logging for TLS session resumption by extracting and logging SNI values from ClientHello messages. 2025-03-12 10:01:54 +00:00
156abbf5b4 3.41.3
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-12 09:56:21 +00:00
1a90566622 fix(TLS/SNI): Improve TLS session resumption handling and logging. Now, session resumption attempts are always logged with details, and connections without a proper SNI are rejected when allowSessionTicket is disabled. In addition, empty SNI extensions are explicitly treated as missing, ensuring stricter and more consistent TLS handshake validation. 2025-03-12 09:56:21 +00:00
b48b90d613 3.41.2
Some checks failed
Default (tags) / security (push) Successful in 28s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 19:41:04 +00:00
124f8d48b7 fix(SniHandler): Refactor hasSessionResumption to return detailed session resumption info 2025-03-11 19:41:04 +00:00
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
99d28eafd1 3.30.7
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 02:25:59 +00:00
788b444fcc 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. 2025-03-11 02:25:58 +00:00
4225abe3c4 3.30.6
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 02:18:56 +00:00
74fdb58f84 fix(PortProxy): Improve TLS renegotiation handling in PortProxy by validating the new SNI against allowed domain configurations. If the new SNI is permitted based on existing IP rules, update the locked domain to allow connection reuse; otherwise, terminate the connection to prevent misrouting. 2025-03-11 02:18:56 +00:00
bffdaffe39 3.30.5
Some checks failed
Default (tags) / security (push) Successful in 20s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 22:36:28 +00:00
67a4228518 fix(internal): No uncommitted changes detected; project files and tests remain unchanged. 2025-03-10 22:36:28 +00:00
681209f2e1 3.30.4
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 22:35:34 +00:00
c415a6c361 fix(PortProxy): Fix TLS renegotiation handling and adjust TLS keep-alive timeouts in PortProxy implementation 2025-03-10 22:35:34 +00:00
009e3c4f0e 3.30.3
Some checks failed
Default (tags) / security (push) Failing after 14m48s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-10 22:07:12 +00:00
f9c42975dc fix(classes.portproxy.ts): Simplify timeout management in PortProxy and fix chained proxy certificate refresh issues 2025-03-10 22:07:12 +00:00
feef949afe 3.30.2
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 14:15:03 +00:00
8d3b07b1e6 fix(classes.portproxy.ts): Adjust TLS keep-alive timeout to refresh certificate context. 2025-03-10 14:15:03 +00:00
51fe935f1f 3.30.1
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 14:13:57 +00:00
146fac73cf fix(PortProxy): Improve TLS keep-alive management and fix whitespace formatting 2025-03-10 14:13:56 +00:00
4465cac807 3.30.0
Some checks failed
Default (tags) / security (push) Failing after 16m2s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-08 12:40:55 +00:00
9d7ed21cba feat(PortProxy): Add advanced TLS keep-alive handling and system sleep detection 2025-03-08 12:40:55 +00:00
54fbe5beac 3.29.3
Some checks failed
Default (tags) / security (push) Successful in 19s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 15:50:25 +00:00
0704853fa2 fix(core): Fix functional errors in the proxy setup and enhance pnpm configuration 2025-03-07 15:50:25 +00:00
8cf22ee38b 3.29.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 48s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 15:46:34 +00:00
f28e68e487 fix(PortProxy): Fix test for PortProxy handling of custom IPs in Docker/CI environments. 2025-03-07 15:46:34 +00:00
499aed19f6 3.29.1
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 50s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 14:34:49 +00:00
618b6fe2d1 fix(readme): Update readme for IPTablesProxy options 2025-03-07 14:34:49 +00:00
d6027c11c1 3.29.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 14:30:38 +00:00
bbdea52677 feat(IPTablesProxy): Enhanced IPTablesProxy with multi-port and IPv6 support 2025-03-07 14:30:38 +00:00
d8585975a8 3.28.6
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 11:16:45 +00:00
98c61cccbb fix(PortProxy): Adjust default timeout settings and enhance keep-alive connection handling in PortProxy. 2025-03-07 11:16:44 +00:00
b3dcc0ae22 3.28.5 2025-03-07 02:55:19 +00:00
b96d7dec98 fix(core): Ensure proper resource cleanup during server shutdown. 2025-03-07 02:55:19 +00:00
0d0a1c740b 3.28.4 2025-03-07 02:54:34 +00:00
9bd87b8437 fix(router): Improve path pattern matching and hostname prioritization in router 2025-03-07 02:54:34 +00:00
0e281b3243 3.28.3 2025-03-06 23:08:57 +00:00
a14b7802c4 fix(PortProxy): Ensure timeout values are within Node.js safe limits 2025-03-06 23:08:57 +00:00
138900ca8b 3.28.2 2025-03-06 23:00:24 +00:00
cb6c2503e2 fix(portproxy): Adjust safe timeout defaults in PortProxy to prevent overflow issues. 2025-03-06 23:00:24 +00:00
f3fd903231 3.28.1 2025-03-06 22:56:19 +00:00
0e605d9a9d fix(PortProxy): Improved code formatting and readability in PortProxy class by adjusting spacing and comments. 2025-03-06 22:56:18 +00:00
1718a3b2f2 3.28.0 2025-03-06 08:36:19 +00:00
568f77e65b feat(router): Add detailed routing tests and refactor ProxyRouter for improved path matching 2025-03-06 08:36:19 +00:00
e212dacbf3 3.27.0 2025-03-06 08:27:44 +00:00
eea8942670 feat(AcmeCertManager): Introduce AcmeCertManager for enhanced ACME certificate management 2025-03-06 08:27:44 +00:00
0574331b91 3.26.0 2025-03-05 18:47:38 +00:00
06e6c2eb52 feat(readme): Updated README with enhanced TLS handling, connection management, and troubleshooting sections. 2025-03-05 18:47:38 +00:00
edd9db31c2 3.25.4 2025-03-05 18:40:42 +00:00
d4251b2cf9 fix(portproxy): Improve connection timeouts and detailed logging for PortProxy 2025-03-05 18:40:42 +00:00
4ccc1db8a2 3.25.3 2025-03-05 18:25:01 +00:00
7e3ed93bc9 fix(core): Update dependencies and configuration improvements. 2025-03-05 18:25:01 +00:00
fa793f2c4a 3.25.2 2025-03-05 18:24:28 +00:00
fe8106f0c8 fix(PortProxy): Adjust timeout settings and handle inactivity properly in PortProxy. 2025-03-05 18:24:28 +00:00
b317ab8b3a 3.25.1 2025-03-05 18:07:40 +00:00
4fd5524a0f fix(PortProxy): Adjust inactivity threshold to a random value between 20 and 30 minutes for better variability 2025-03-05 18:07:39 +00:00
2013d03ac6 3.25.0 2025-03-05 17:46:26 +00:00
0e888c5add feat(PortProxy): Enhanced PortProxy with detailed logging, protocol detection, and rate limiting. 2025-03-05 17:46:25 +00:00
7f891a304c 3.24.0 2025-03-05 17:06:51 +00:00
f6cc665f12 feat(core): Enhance core functionalities and test coverage for NetworkProxy and PortProxy 2025-03-05 17:06:51 +00:00
48c5ea3b1d 3.23.1 2025-03-05 14:33:10 +00:00
bd9292bf47 fix(PortProxy): Enhanced connection setup to handle pending data buffering before establishing outgoing connection 2025-03-05 14:33:09 +00:00
6532e6f0e0 3.23.0 2025-03-03 03:18:49 +00:00
8791da83b4 feat(documentation): Updated documentation with architecture flow diagrams. 2025-03-03 03:18:49 +00:00
9ad08edf79 3.22.5 2025-03-03 03:05:50 +00:00
c0de8c59a2 fix(documentation): Refactored readme for clarity and consistency, fixed documentation typos 2025-03-03 03:05:49 +00:00
3748689c16 3.22.4 2025-03-03 02:16:48 +00:00
d0b3139fda fix(core): Addressed minor issues in the core modules to improve stability and performance. 2025-03-03 02:16:48 +00:00
fd4f731ada 3.22.3 2025-03-03 02:14:22 +00:00
ced9b5b27b fix(core): Improve connection management and error handling in PortProxy 2025-03-03 02:14:21 +00:00
eb70a86304 3.22.2 2025-03-03 02:03:24 +00:00
131d9d326e fix(portproxy): Refactored connection cleanup logic in PortProxy 2025-03-03 02:03:24 +00:00
12de96a7d5 3.22.1 2025-03-03 01:57:52 +00:00
296e1fcdc7 fix(PortProxy): Fix connection timeout and IP validation handling for PortProxy 2025-03-03 01:57:52 +00:00
8459e4013c 3.22.0 2025-03-03 01:50:30 +00:00
191c8ac0e6 feat(classes.portproxy): Enhanced PortProxy to support initial data timeout and improved IP handling 2025-03-03 01:50:30 +00:00
3ab483d164 3.21.0 2025-03-03 01:42:16 +00:00
fcd80dc56b feat(PortProxy): Enhancements to connection management in PortProxy 2025-03-03 01:42:16 +00:00
8ddffcd6e5 3.20.2 2025-03-01 20:31:50 +00:00
a5a7781c17 fix(PortProxy): Enhance connection cleanup handling in PortProxy 2025-03-01 20:31:50 +00:00
d647e77cdf 3.20.1 2025-03-01 17:32:31 +00:00
9161336197 fix(PortProxy): Improve IP allowance check for forced domains 2025-03-01 17:32:31 +00:00
2e63d13dd4 3.20.0 2025-03-01 17:19:27 +00:00
af6ed735d5 feat(PortProxy): Enhance PortProxy with advanced connection cleanup and logging 2025-03-01 17:19:27 +00:00
7d38f29ef3 3.19.0 2025-03-01 13:17:05 +00:00
0df26d4367 feat(PortProxy): Enhance PortProxy with default blocked IPs 2025-03-01 13:17:05 +00:00
f9a6e2d748 3.18.2 2025-02-27 21:25:03 +00:00
1cb6302750 fix(portproxy): Fixed typographical errors in comments within PortProxy class. 2025-02-27 21:25:03 +00:00
f336f25535 3.18.1 2025-02-27 21:19:34 +00:00
5d6b707440 fix(PortProxy): Refactor and enhance PortProxy test cases and handling 2025-02-27 21:19:34 +00:00
622ad2ff20 3.18.0 2025-02-27 20:59:29 +00:00
dd23efd28d feat(PortProxy): Add SNI-based renegotiation handling in PortProxy 2025-02-27 20:59:29 +00:00
0ddf68a919 3.17.1 2025-02-27 20:10:26 +00:00
ec08ca51f5 fix(PortProxy): Fix handling of SNI re-negotiation in PortProxy 2025-02-27 20:10:26 +00:00
29688d1379 3.17.0 2025-02-27 19:57:28 +00:00
c83f6fa278 feat(smartproxy): Enhance description clarity and improve SNI handling with domain locking. 2025-02-27 19:57:27 +00:00
60333b0a59 3.16.9 2025-02-27 15:46:14 +00:00
1aa409907b fix(portproxy): Extend domain input validation to support string arrays in port proxy configurations. 2025-02-27 15:46:14 +00:00
adee6afc76 3.16.8 2025-02-27 15:41:03 +00:00
4a0792142f fix(PortProxy): Fix IP filtering for domain and global default allowed lists and improve port-based routing logic. 2025-02-27 15:41:03 +00:00
f1b810a4fa 3.16.7 2025-02-27 15:32:06 +00:00
96b5877c5f fix(PortProxy): Improved IP validation logic in PortProxy to ensure correct domain matching and fallback 2025-02-27 15:32:06 +00:00
6d627f67f7 3.16.6 2025-02-27 15:30:20 +00:00
9af968b8e7 fix(PortProxy): Optimize connection cleanup logic in PortProxy by removing unnecessary delays. 2025-02-27 15:30:20 +00:00
b3ba0c21e8 3.16.5 2025-02-27 15:05:38 +00:00
ef707a5870 fix(PortProxy): Improved connection cleanup process with added asynchronous delays 2025-02-27 15:05:38 +00:00
6ca14edb38 3.16.4 2025-02-27 14:23:44 +00:00
5a5686b6b9 fix(PortProxy): Fix and enhance port proxy handling 2025-02-27 14:23:44 +00:00
2080f419cb 3.16.3 2025-02-27 13:04:01 +00:00
659aae297b fix(PortProxy): Refactored PortProxy to support multiple listening ports and improved modularity. 2025-02-27 13:04:01 +00:00
fcd0f61b5c 3.16.2 2025-02-27 12:54:15 +00:00
7ee35a98e3 fix(PortProxy): Fix port-based routing logic in PortProxy 2025-02-27 12:54:14 +00:00
ea0f6d2270 3.16.1 2025-02-27 12:42:50 +00:00
621ad9e681 fix(core): Updated minor version numbers in dependencies for patch release. 2025-02-27 12:42:50 +00:00
7cea5773ee 3.16.0 2025-02-27 12:41:20 +00:00
a2cb56ba65 feat(PortProxy): Enhancements made to PortProxy settings and capabilities 2025-02-27 12:41:20 +00:00
408b793149 3.15.0 2025-02-27 12:25:48 +00:00
f6c3d2d3d0 feat(classes.portproxy): Add support for port range-based routing with enhanced IP and port validation. 2025-02-27 12:25:48 +00:00
422eb5ec40 3.14.2 2025-02-26 19:00:09 +00:00
45390c4389 fix(PortProxy): Fix cleanup timer reset for PortProxy 2025-02-26 19:00:09 +00:00
0f2e6d688c 3.14.1 2025-02-26 12:56:00 +00:00
3bd7b70c19 fix(PortProxy): Increased default maxConnectionLifetime for PortProxy to 600000 ms 2025-02-26 12:56:00 +00:00
07a82a09be 3.14.0 2025-02-26 10:29:21 +00:00
23253a2731 feat(PortProxy): Introduce max connection lifetime feature 2025-02-26 10:29:21 +00:00
be31a9b553 3.13.0 2025-02-25 00:56:02 +00:00
a1051f78e8 feat(core): Add support for tagging iptables rules with comments and cleaning them up on process exit 2025-02-25 00:56:01 +00:00
aa756bd698 3.12.0 2025-02-24 23:27:48 +00:00
ff4f44d6fc feat(IPTablesProxy): Introduce IPTablesProxy class for managing iptables NAT rules 2025-02-24 23:27:48 +00:00
63ebad06ea 3.11.0 2025-02-24 10:00:57 +00:00
31e15b65ec feat(Port80Handler): Add automatic certificate issuance with ACME client 2025-02-24 10:00:57 +00:00
266895ccc5 3.10.5 2025-02-24 09:53:39 +00:00
dc3d56771b fix(portproxy): Fix incorrect import path in test file 2025-02-24 09:53:39 +00:00
38601a41bb 3.10.4 2025-02-23 17:38:23 +00:00
a53e6f1019 fix(PortProxy): Refactor connection tracking to utilize unified records in PortProxy 2025-02-23 17:38:22 +00:00
3de35f3b2c 3.10.3 2025-02-23 17:30:41 +00:00
b9210d891e fix(PortProxy): Refactor and optimize PortProxy for improved readability and maintainability 2025-02-23 17:30:41 +00:00
133d5a47e0 3.10.2 2025-02-23 11:43:21 +00:00
f2f4e47893 fix(PortProxy): Fix connection handling to include timeouts for SNI-enabled connections. 2025-02-23 11:43:21 +00:00
e47436608f 3.10.1 2025-02-22 13:22:26 +00:00
128f8203ac fix(PortProxy): Improve socket cleanup logic to prevent potential resource leaks 2025-02-22 13:22:26 +00:00
c7697eca84 3.10.0 2025-02-22 05:46:30 +00:00
71b5237cd4 feat(smartproxy.portproxy): Enhance PortProxy with detailed connection statistics and termination tracking 2025-02-22 05:46:30 +00:00
2df2f0ceaf 3.9.4 2025-02-22 05:41:29 +00:00
2b266ca779 fix(PortProxy): Ensure proper cleanup on connection rejection in PortProxy 2025-02-22 05:41:29 +00:00
c2547036fd 3.9.3 2025-02-21 23:57:54 +00:00
a8131ece26 fix(PortProxy): Fix handling of optional outgoing socket in PortProxy 2025-02-21 23:57:54 +00:00
ad8c667dec 3.9.2 2025-02-21 23:33:16 +00:00
942e0649c8 fix(PortProxy): Improve timeout handling for port proxy connections 2025-02-21 23:33:15 +00:00
59625167b4 3.9.1 2025-02-21 23:30:51 +00:00
385d984727 fix(dependencies): Ensure correct ordering of dependencies and improve logging format. 2025-02-21 23:30:51 +00:00
a959c2ad0e 3.9.0 2025-02-21 23:18:17 +00:00
88f5436c9a feat(smartproxy.portproxy): Add logging of connection durations to PortProxy 2025-02-21 23:18:17 +00:00
06101cd1b1 3.8.1 2025-02-21 23:11:14 +00:00
438d65107d fix(plugins): Simplified plugin import structure across codebase 2025-02-21 23:11:13 +00:00
233b26c308 3.8.0 2025-02-21 23:05:18 +00:00
ba787729e8 feat(PortProxy): Add active connection tracking and logging in PortProxy 2025-02-21 23:05:17 +00:00
4854d7c38d 3.7.3 2025-02-21 20:17:35 +00:00
e841bda003 fix(portproxy): Fix handling of connections in PortProxy to improve stability and performance. 2025-02-21 20:17:35 +00:00
477b930a37 3.7.2 2025-02-21 19:56:28 +00:00
935bd95723 fix(PortProxy): Improve SNICallback and connection handling in PortProxy 2025-02-21 19:56:28 +00:00
0e33ea4eb5 3.7.1 2025-02-21 19:53:20 +00:00
6181065963 fix(smartproxy.portproxy): Optimize SNI handling by simplifying context creation 2025-02-21 19:53:19 +00:00
1a586dcbd7 3.7.0 2025-02-21 19:44:59 +00:00
ee03224561 feat(PortProxy): Add optional source IP preservation support in PortProxy 2025-02-21 19:44:59 +00:00
483cbb3634 3.6.0 2025-02-21 19:39:52 +00:00
c77b31b72c feat(PortProxy): Add feature to preserve original client IP through chained proxies 2025-02-21 19:39:52 +00:00
8cb8fa1a52 3.5.0 2025-02-21 19:34:11 +00:00
8e5bb12edb feat(PortProxy): Enhance PortProxy to support domain-specific target IPs 2025-02-21 19:34:11 +00:00
9be9a426ad 3.4.4 2025-02-21 18:54:40 +00:00
32d875aed9 fix(PortProxy): Fixed handling of SNI domain connections and IP allowance checks 2025-02-21 18:54:40 +00:00
4747462cff 3.4.3 2025-02-21 18:48:39 +00:00
70f69ef1ea fix(PortProxy): Fixed indentation issue and ensured proper cleanup of sockets in PortProxy 2025-02-21 18:48:39 +00:00
2be1c57dd7 3.4.2 2025-02-21 18:47:18 +00:00
58bd6b4a85 fix(smartproxy): Enhance SSL/TLS handling with SNI and error logging 2025-02-21 18:47:18 +00:00
63e1cd48e8 3.4.1 2025-02-21 18:43:08 +00:00
5150ddc18e fix(PortProxy): Normalize IP addresses for port proxy to handle IPv4-mapped IPv6 addresses. 2025-02-21 18:43:08 +00:00
4bee483954 3.4.0 2025-02-21 17:01:02 +00:00
4328d4365f feat(PortProxy): Enhanced PortProxy with custom target host and improved testing 2025-02-21 17:01:02 +00:00
21e9d0fd0d 3.3.1 2025-02-21 15:17:20 +00:00
6c0c65bb1a fix(PortProxy): fixed import usage of net and tls libraries for PortProxy 2025-02-21 15:17:19 +00:00
23f61eb60b 3.3.0 2025-02-21 15:14:03 +00:00
a4ad6c59c1 feat(PortProxy): Enhanced PortProxy with domain and IP filtering, SNI support, and minimatch integration 2025-02-21 15:14:02 +00:00
e67eff0fcc 3.2.0 2025-02-04 01:24:38 +01:00
e5db2e171c feat(testing): Added a comprehensive test suite for the PortProxy class 2025-02-04 01:24:37 +01:00
7389072841 3.1.4 2025-02-04 01:11:54 +01:00
9dd56a9362 fix(core): No uncommitted changes. Preparing for potential minor improvements or bug fixes. 2025-02-04 01:11:54 +01:00
1e7c45918e 3.1.3 2025-02-04 01:10:58 +01:00
49b65508a5 fix(networkproxy): Refactor and improve WebSocket handling and request processing 2025-02-04 01:10:58 +01:00
3e66debb01 3.1.2 2025-02-04 00:38:39 +01:00
f1bb1702c1 fix(core): Refactor certificate handling across the project 2025-02-04 00:38:39 +01:00
5abc0d8a14 3.1.1 2025-02-03 23:41:14 +01:00
9150e8c5fc fix(workflow): Update Gitea workflow paths and dependencies 2025-02-03 23:41:13 +01:00
8e4d3b7565 3.1.0 2024-10-07 12:52:02 +02:00
459ee7130f feat(NetworkProxy): Introduce WebSocket heartbeat to maintain active connections in NetworkProxy 2024-10-07 12:52:01 +02:00
ceede84774 3.0.61 2024-10-07 12:29:49 +02:00
70bdf074a1 fix(networkproxy): Improve error handling for proxy requests 2024-10-07 12:29:49 +02:00
3eb4b576a7 update description 2024-05-29 14:15:35 +02:00
c1f2c64e8b update tsconfig 2024-04-14 18:10:41 +02:00
4ba943ee59 update tsconfig 2024-04-01 21:40:16 +02:00
4c50e9a556 update npmextra.json: githost 2024-04-01 19:59:21 +02:00
f2428412bb update npmextra.json: githost 2024-03-30 21:48:21 +01:00
f114968298 3.0.60 2023-07-27 16:28:26 +02:00
def6644e80 fix(core): update 2023-07-27 16:28:26 +02:00
c85f3da924 3.0.59 2023-07-27 16:27:51 +02:00
6cdb23ed66 fix(core): update 2023-07-27 16:27:50 +02:00
0adb32e7e9 switch to new org scheme 2023-07-11 01:25:18 +02:00
5d65e1668b switch to new org scheme 2023-07-10 10:17:33 +02:00
632015a7bd 3.0.58 2023-02-04 19:32:14 +01:00
972ee2af54 fix(core): update 2023-02-04 19:32:13 +01:00
9b1ff5eed8 3.0.57 2023-01-06 13:04:12 +01:00
0739d1093a fix(core): update 2023-01-06 13:04:11 +01:00
ee4f7fc48d 3.0.56 2023-01-06 13:00:11 +01:00
f6e656361b fix(core): update 2023-01-06 13:00:10 +01:00
e51c2a88cc 3.0.55 2023-01-06 12:56:51 +01:00
7f8112930d fix(core): update 2023-01-06 12:56:51 +01:00
b5c83b5c75 3.0.54 2023-01-06 12:53:58 +01:00
63ce1a44a4 fix(core): update 2023-01-06 12:53:58 +01:00
759f70b84d 3.0.53 2023-01-06 11:57:11 +01:00
45ce56b118 fix(core): update 2023-01-06 11:57:11 +01:00
0cc7184e58 3.0.52 2023-01-05 20:15:17 +01:00
392e241208 fix(core): update 2023-01-05 20:15:16 +01:00
32c6d77178 3.0.51 2023-01-05 19:33:16 +01:00
2c4316d2d3 fix(core): update 2023-01-05 19:33:15 +01:00
62e6387c1d 3.0.50 2023-01-05 19:21:03 +01:00
7fe22e962a fix(core): update 2023-01-05 19:21:02 +01:00
3f1f718308 3.0.49 2023-01-05 18:31:55 +01:00
ce94d283c1 fix(core): update 2023-01-05 18:31:55 +01:00
a1c4f3c341 3.0.48 2023-01-05 18:05:36 +01:00
8087bab197 fix(core): update 2023-01-05 18:05:36 +01:00
db63e7bf79 3.0.47 2023-01-05 18:00:51 +01:00
2615a0ebd4 fix(core): update 2023-01-05 18:00:50 +01:00
d5d77af98d 3.0.46 2023-01-05 17:28:55 +01:00
1f1bf77807 fix(core): update 2023-01-05 17:28:54 +01:00
d4269d290d 3.0.45 2023-01-05 17:21:42 +01:00
e05e5ede55 fix(core): update 2023-01-05 17:21:41 +01:00
b6c7f13baa 3.0.44 2023-01-05 17:00:32 +01:00
055d328bd0 fix(core): update 2023-01-05 17:00:31 +01:00
20b9a220fc 3.0.43 2023-01-05 15:53:17 +01:00
2170fe3518 fix(core): update 2023-01-05 15:53:17 +01:00
04b13e53b9 3.0.42 2023-01-05 15:26:36 +01:00
f1a4fae704 fix(core): update 2023-01-05 15:26:36 +01:00
5ee5147606 3.0.41 2023-01-05 14:57:55 +01:00
748c6e14e4 fix(core): update 2023-01-05 14:57:54 +01:00
f018957de4 3.0.40 2023-01-04 15:59:59 +01:00
a6583b037c fix(core): update 2023-01-04 15:59:58 +01:00
3ab4144c9a 3.0.39 2023-01-04 15:59:32 +01:00
0d2885ace4 fix(core): update 2023-01-04 15:59:31 +01:00
1723275215 3.0.38 2022-08-06 22:42:05 +02:00
977d8b0310 fix(core): update 2022-08-06 22:42:05 +02:00
5bb065f82b 3.0.37 2022-08-04 17:16:27 +02:00
942b812f97 fix(core): update 2022-08-04 17:16:26 +02:00
59a025b308 3.0.36 2022-08-04 14:21:06 +02:00
458e7d6b58 fix(core): update 2022-08-04 14:21:05 +02:00
7b0f824d29 3.0.35 2022-08-01 18:40:45 +02:00
b5796b86d5 fix(core): update 2022-08-01 18:40:44 +02:00
1f8ea59221 3.0.34 2022-08-01 13:00:47 +02:00
d717568572 fix(core): update 2022-08-01 13:00:46 +02:00
28d050851f 3.0.33 2022-08-01 12:56:15 +02:00
acbd109985 fix(core): update 2022-08-01 12:56:14 +02:00
cc38a6d10e 3.0.32 2022-08-01 12:55:01 +02:00
748b07efe2 fix(core): update 2022-08-01 12:55:00 +02:00
be4fd0978a 3.0.31 2022-08-01 12:31:30 +02:00
4521010b82 fix(core): update 2022-08-01 12:31:29 +02:00
bd1f1a4c1c 3.0.30 2022-07-31 08:30:13 +02:00
d3bdd56660 fix(core): update 2022-07-31 08:30:13 +02:00
c38a7c4c32 3.0.29 2022-07-30 23:00:23 +02:00
858628196a fix(core): update 2022-07-30 23:00:23 +02:00
4910679058 3.0.28 2022-07-30 23:00:15 +02:00
97db2012ca fix(core): update 2022-07-30 23:00:15 +02:00
0ee13b4e06 3.0.27 2022-07-30 22:40:08 +02:00
21f5882fa3 fix(core): update 2022-07-30 22:40:08 +02:00
48b43f9f0d 3.0.26 2022-07-30 22:29:31 +02:00
d3d476fd53 fix(core): update 2022-07-30 22:29:31 +02:00
b80b8a0a20 3.0.25 2022-07-30 22:20:31 +02:00
384943f697 fix(core): update 2022-07-30 22:20:31 +02:00
e9239ed978 3.0.24 2022-07-30 21:29:35 +02:00
baf1844866 fix(core): update 2022-07-30 21:29:34 +02:00
0b3d7f8a06 3.0.23 2022-07-30 21:28:08 +02:00
c38a2745e9 fix(core): update 2022-07-30 21:28:08 +02:00
a0f39d1c5b 3.0.22 2022-07-30 21:25:27 +02:00
c67ac868a5 fix(core): update 2022-07-30 21:25:27 +02:00
90e1a0453e 3.0.21 2022-07-30 19:28:24 +02:00
d7765fb5dc fix(core): update 2022-07-30 19:28:23 +02:00
0fdd17b430 3.0.20 2022-07-30 18:53:21 +02:00
0562de6aa1 fix(core): update 2022-07-30 18:53:21 +02:00
7b550a35aa 3.0.19 2022-07-30 18:49:14 +02:00
fb66aac6e7 fix(core): update 2022-07-30 18:49:14 +02:00
208790cfcf 3.0.18 2022-07-30 18:29:21 +02:00
5978bbaf66 fix(core): update 2022-07-30 18:29:20 +02:00
1c47eafe5f 3.0.17 2022-07-30 17:32:59 +02:00
69e3a71354 fix(core): update 2022-07-30 17:32:58 +02:00
21e92bf0c1 3.0.16 2022-07-30 17:26:36 +02:00
d732e6e7aa fix(core): update 2022-07-30 17:26:35 +02:00
5fdfcdb407 3.0.15 2022-07-30 13:30:25 +02:00
49e2e90bda fix(core): update 2022-07-30 13:30:25 +02:00
b8e53e7b42 3.0.14 2022-07-30 08:20:21 +02:00
1136841b3d fix(core): update 2022-07-30 08:20:20 +02:00
42cbc51d22 3.0.13 2022-07-30 02:07:32 +02:00
2d16403ad1 fix(core): update 2022-07-30 02:07:31 +02:00
afe847499a 3.0.12 2022-07-30 01:57:42 +02:00
f980bb70b4 fix(core): update 2022-07-30 01:57:42 +02:00
f192a8f041 3.0.11 2022-07-29 16:12:18 +02:00
64bf3aef6d fix(core): update 2022-07-29 16:12:18 +02:00
a5e3cbd05b 3.0.10 2022-07-29 15:26:03 +02:00
2f0fad999a fix(core): update 2022-07-29 15:26:02 +02:00
5e6477720d 3.0.9 2022-07-29 15:22:32 +02:00
bad8bf0688 fix(core): update 2022-07-29 15:22:31 +02:00
4f1db106fb 3.0.8 2022-07-29 04:25:01 +02:00
d47829a8b2 fix(core): update 2022-07-29 04:25:01 +02:00
ca55d06244 3.0.7 2022-07-29 03:39:05 +02:00
7284924b26 fix(core): update 2022-07-29 03:39:05 +02:00
10857aa12b 3.0.6 2022-07-29 02:30:15 +02:00
c968c7f844 fix(core): update 2022-07-29 02:30:15 +02:00
b07015f6c4 3.0.5 2022-07-29 01:52:34 +02:00
ca4ddade17 fix(core): update 2022-07-29 01:52:34 +02:00
17eaea4124 3.0.4 2022-07-29 01:37:47 +02:00
d3d8f6ff57 fix(core): update 2022-07-29 01:37:47 +02:00
906661c7f4 3.0.3 2022-07-29 01:31:52 +02:00
ea46caebb7 fix(core): update 2022-07-29 01:31:52 +02:00
47 changed files with 21851 additions and 15768 deletions

View File

@ -0,0 +1,66 @@
name: Default (not tags)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
- name: Run npm prepare
run: npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build

View File

@ -0,0 +1,124 @@
name: Default (tags)
on:
push:
tags:
- '*'
env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build
release:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Release
run: |
npmci node install stable
npmci npm publish
metadata:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Code quality
run: |
npmci command npm install -g typescript
npmci npm install
- name: Trigger
run: npmci trigger
- name: Build docs and upload artifacts
run: |
npmci node install stable
npmci npm install
pnpm install -g @git.zone/tsdoc
npmci command tsdoc
continue-on-error: true

3
.gitignore vendored
View File

@ -3,7 +3,6 @@
# artifacts
coverage/
public/
pages/
# installs
node_modules/
@ -17,4 +16,4 @@ node_modules/
dist/
dist_*/
# custom
#------# custom

View File

@ -1,137 +0,0 @@
# gitzone ci_default
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
cache:
paths:
- .npmci_cache/
key: '$CI_BUILD_STAGE'
stages:
- security
- test
- release
- metadata
# ====================
# security stage
# ====================
mirror:
stage: security
script:
- npmci git mirror
only:
- tags
tags:
- lossless
- docker
- notpriv
auditProductionDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci npm prepare
- npmci command npm install --production --ignore-scripts
- npmci command npm config set registry https://registry.npmjs.org
- npmci command npm audit --audit-level=high --only=prod --production
tags:
- docker
auditDevDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci npm prepare
- npmci command npm install --ignore-scripts
- npmci command npm config set registry https://registry.npmjs.org
- npmci command npm audit --audit-level=high --only=dev
tags:
- docker
allow_failure: true
# ====================
# test stage
# ====================
testStable:
stage: test
script:
- npmci npm prepare
- npmci node install stable
- npmci npm install
- npmci npm test
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- docker
testBuild:
stage: test
script:
- npmci npm prepare
- npmci node install stable
- npmci npm install
- npmci command npm run build
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- docker
release:
stage: release
script:
- npmci node install stable
- npmci npm publish
only:
- tags
tags:
- lossless
- docker
- notpriv
# ====================
# metadata stage
# ====================
codequality:
stage: metadata
allow_failure: true
only:
- tags
script:
- npmci command npm install -g tslint typescript
- npmci npm prepare
- npmci npm install
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
tags:
- lossless
- docker
- priv
trigger:
stage: metadata
script:
- npmci trigger
only:
- tags
tags:
- lossless
- docker
- notpriv
pages:
stage: metadata
script:
- npmci node install lts
- npmci command npm install -g @gitzone/tsdoc
- npmci npm prepare
- npmci npm install
- npmci command tsdoc
tags:
- lossless
- docker
- notpriv
only:
- tags
artifacts:
expire_in: 1 week
paths:
- public
allow_failure: true

24
.vscode/launch.json vendored
View File

@ -2,28 +2,10 @@
"version": "0.2.0",
"configurations": [
{
"name": "current file",
"type": "node",
"command": "npm test",
"name": "Run npm test",
"request": "launch",
"args": [
"${relativeFile}"
],
"runtimeArgs": ["-r", "@gitzone/tsrun"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "test.ts",
"type": "node",
"request": "launch",
"args": [
"test/test.ts"
],
"runtimeArgs": ["-r", "@gitzone/tsrun"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
"type": "node-terminal"
}
]
}

19
assets/certs/cert.pem Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIUPU4tviz3ZvsMDjCz1NZRT16b0Y4wDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAwwKcHVzaC5yb2NrczAeFw0yNTAyMDMyMzA5MzRaFw0yNjAy
MDMyMzA5MzRaMBUxEzARBgNVBAMMCnB1c2gucm9ja3MwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCZMkBYD/pYLBv9MiyHTLRT24kQyPeJBtZqryibi1jk
BT1ZgNl3yo5U6kjj/nYBU/oy7M4OFC0xyaJQ4wpvLHu7xzREqwT9N9WcDcxaahUi
P8+PsjGyznPrtXa1ASzGAYMNvXyWWp3351UWZHMEs6eY/Y7i8m4+0NwP5h8RNBCF
KSFS41Ee9rNAMCnQSHZv1vIzKeVYPmYnCVmL7X2kQb+gS6Rvq5sEGLLKMC5QtTwI
rdkPGpx4xZirIyf8KANbt0sShwUDpiCSuOCtpze08jMzoHLG9Nv97cJQjb/BhiES
hLL+YjfAUFjq0rQ38zFKLJ87QB9Jym05mY6IadGQLXVXAgMBAAGjUzBRMB0GA1Ud
DgQWBBQjpowWjrql/Eo2EVjl29xcjuCgkTAfBgNVHSMEGDAWgBQjpowWjrql/Eo2
EVjl29xcjuCgkTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAY
44vqbaf6ewFrZC0f3Kk4A10lC6qjWkcDFfw+JE8nzt+4+xPqp1eWgZKF2rONyAv2
nG41Xygt19ByancXLU44KB24LX8F1GV5Oo7CGBA+xtoSPc0JulXw9fGclZDC6XiR
P/+vhGgCHicbfP2O+N00pOifrTtf2tmOT4iPXRRo4TxmPzuCd+ZJTlBhPlKCmICq
yGdAiEo6HsSiP+M5qVlNx8s57MhQYk5TpgmI6FU4mO7zfDfSatFonlg+aDbrnaqJ
v/+km02M+oB460GmKwsSTnThHZgLNCLiKqD8bdziiCQjx5u0GjLI6468o+Aehb8l
l/x9vWTTk/QKq41X5hFk
-----END CERTIFICATE-----

28
assets/certs/key.pem Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCZMkBYD/pYLBv9
MiyHTLRT24kQyPeJBtZqryibi1jkBT1ZgNl3yo5U6kjj/nYBU/oy7M4OFC0xyaJQ
4wpvLHu7xzREqwT9N9WcDcxaahUiP8+PsjGyznPrtXa1ASzGAYMNvXyWWp3351UW
ZHMEs6eY/Y7i8m4+0NwP5h8RNBCFKSFS41Ee9rNAMCnQSHZv1vIzKeVYPmYnCVmL
7X2kQb+gS6Rvq5sEGLLKMC5QtTwIrdkPGpx4xZirIyf8KANbt0sShwUDpiCSuOCt
pze08jMzoHLG9Nv97cJQjb/BhiEShLL+YjfAUFjq0rQ38zFKLJ87QB9Jym05mY6I
adGQLXVXAgMBAAECggEARGCBBq1PBHbfoUH5TQSIAlvdEEBa9+602lZG7jIioVfT
W7Uem5Ctuan+kcDcY9hbNsqqZ+9KgsvoJmlIGXoF2jjeE/4vUmRO9AHWoc5yk2Be
4NjcxN3QMLdEfiLBnLlFCOd4CdX1ZxZ6TG3WRpV3a1pVIeeqHGB1sKT6Xd/atcwG
RvpiXzu0SutGxVb6WE9r6hovZ4fVERCyCRczUGrUH5ICbxf6E7L4u8xjEYR4uEKK
/8ZkDqrWdRASDAdPPMNqnHUEAho/WnxpNeb6B4lvvv2QWxIS9H1OikF/NzWPgVNS
oPpvtJgjyo5xdgLm3zE4lcSPNVSrh1TBXuAn9TG4WQKBgQDScPFkUNBqjC5iPMof
bqDHlhlptrHmiv9LC0lgjEDPgIEQfjLfdCugwDk32QyAcb5B60upDYeqCFDkfV/C
T536qxevYPjPAjahLPHqMxkWpjvtY6NOTgbbcpVtblU2Fj8R8qbyPNADG31LicU9
GVPtQ4YcVaMWCYbg5107+9dFWQKBgQC6XK+foKK+81RFdrqaNNgebTWTsANnBcZe
xl0bj6oL5yY0IzroxHvgcNS7UMriWCu+K2xfkUBdMmxU773VN5JQ5k15ezjgtrvc
8oAaEsxYP4su12JSTC/zsBANUgrNbFj8++qqKYWt2aQc2O/kbZ4MNfekIVFc8AjM
2X9PxvxKLwKBgHXL7QO3TQLnVyt8VbQEjBFMzwriznB7i+4o8jkOKVU93IEr8zQr
5iQElcLSR3I6uUJTALYvsaoXH5jXKVwujwL69LYiNQRDe+r6qqvrUHbiNJdsd8Rk
XuhGGqj34tD04Pcd+h+MtO+YWqmHBBZwcA9XBeIkebbjPFH2kLT8AwN5AoGAYQy9
hMJxnkE3hIkk+gNE/OtgeE20J+Vw/ZANkrnJEzPHyGUEW41e+W2oyvdzAFZsSTdx
037f5ujIU58Z27x53NliRT4vS4693H0Iyws5EUfeIoGVuUflvODWKymraHjhCrXh
6cV/0R5DAabTnsCbCr7b/MRBC8YQvyUQ0KnOXo8CgYBQYGpvJnSWyvsCjtb6apTP
drjcBhVd0aSBpLGtDdtUCV4oLl9HPy+cLzcGaqckBqCwEq5DKruhMEf7on56bUMd
m/3ItFk1TnhysAeJHb3zLqmJ9CKBitpqLlsOE7MEXVNmbTYeXU10Uo9yOfyt1i7T
su+nT5VtyPkmF/l4wZl5+g==
-----END PRIVATE KEY-----

1024
changelog.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,17 +2,37 @@
"gitzone": {
"projectType": "npm",
"module": {
"githost": "gitlab.com",
"gitscope": "pushrocks",
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smartproxy",
"shortDescription": "a proxy for handling high workloads of proxying",
"npmPackagename": "@pushrocks/smartproxy",
"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.",
"npmPackagename": "@push.rocks/smartproxy",
"license": "MIT",
"projectDomain": "push.rocks"
"projectDomain": "push.rocks",
"keywords": [
"proxy",
"network",
"traffic management",
"SSL",
"TLS",
"WebSocket",
"port proxying",
"dynamic routing",
"authentication",
"real-time applications",
"high workload",
"HTTPS",
"reverse proxy",
"server",
"network security"
]
}
},
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
}
}

14905
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{
"name": "@pushrocks/smartproxy",
"version": "3.0.2",
"name": "@push.rocks/smartproxy",
"version": "4.2.2",
"private": false,
"description": "a proxy for handling high workloads of proxying",
"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",
@ -11,23 +11,30 @@
"scripts": {
"test": "(tstest test/)",
"build": "(tsbuild --web --allowimplicitany)",
"format": "(gitzone format)"
"format": "(gitzone format)",
"buildDocs": "tsdoc"
},
"devDependencies": {
"@gitzone/tsbuild": "^2.1.63",
"@gitzone/tstest": "^1.0.72",
"@pushrocks/tapbundle": "^5.0.4",
"@types/node": "^18.6.2"
"@git.zone/tsbuild": "^2.2.6",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.0.77",
"@push.rocks/tapbundle": "^5.5.10",
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
},
"dependencies": {
"@pushrocks/lik": "^6.0.0",
"@pushrocks/smartnetwork": "^3.0.0",
"@pushrocks/smartpromise": "^3.1.7",
"@pushrocks/smartrequest": "^2.0.0",
"@pushrocks/smartstring": "^4.0.2",
"@tsclass/tsclass": "^4.0.17",
"@types/ws": "^8.5.3",
"ws": "^8.8.1"
"@push.rocks/lik": "^6.1.0",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartstring": "^4.0.15",
"@tsclass/tsclass": "^5.0.0",
"@types/minimatch": "^5.1.2",
"@types/ws": "^8.18.0",
"acme-client": "^5.4.0",
"minimatch": "^10.0.1",
"pretty-ms": "^9.2.0",
"ws": "^8.18.1"
},
"files": [
"ts/**/*",
@ -43,5 +50,38 @@
],
"browserslist": [
"last 1 chrome versions"
]
],
"keywords": [
"proxy",
"network",
"traffic management",
"SSL",
"TLS",
"WebSocket",
"port proxying",
"dynamic routing",
"authentication",
"real-time applications",
"high workload",
"HTTPS",
"reverse proxy",
"server",
"network security"
],
"homepage": "https://code.foss.global/push.rocks/smartproxy#readme",
"repository": {
"type": "git",
"url": "https://code.foss.global/push.rocks/smartproxy.git"
},
"bugs": {
"url": "https://code.foss.global/push.rocks/smartproxy/issues"
},
"pnpm": {
"overrides": {},
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer"
]
}
}

10140
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1
readme.hints.md Normal file
View File

@ -0,0 +1 @@

582
readme.md
View File

@ -1,41 +1,565 @@
# @pushrocks/smartproxy
a proxy for handling high workloads of proxying
# @push.rocks/smartproxy
## Availabililty and Links
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/smartproxy)
* [gitlab.com (source)](https://gitlab.com/pushrocks/smartproxy)
* [github.com (source mirror)](https://github.com/pushrocks/smartproxy)
* [docs (typedoc)](https://pushrocks.gitlab.io/smartproxy/)
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.
## Status for master
## Architecture & Flow Diagrams
Status Category | Status Badge
-- | --
GitLab Pipelines | [![pipeline status](https://gitlab.com/pushrocks/smartproxy/badges/master/pipeline.svg)](https://lossless.cloud)
GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/pushrocks/smartproxy/badges/master/coverage.svg)](https://lossless.cloud)
npm | [![npm downloads per month](https://badgen.net/npm/dy/@pushrocks/smartproxy)](https://lossless.cloud)
Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/pushrocks/smartproxy)](https://lossless.cloud)
TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud)
node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/)
Code Style | [![Code Style](https://badgen.net/badge/style/prettier/purple)](https://lossless.cloud)
PackagePhobia (total standalone install weight) | [![PackagePhobia](https://badgen.net/packagephobia/install/@pushrocks/smartproxy)](https://lossless.cloud)
PackagePhobia (package size on registry) | [![PackagePhobia](https://badgen.net/packagephobia/publish/@pushrocks/smartproxy)](https://lossless.cloud)
BundlePhobia (total size when bundled) | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@pushrocks/smartproxy)](https://lossless.cloud)
Platform support | [![Supports Windows 10](https://badgen.net/badge/supports%20Windows%2010/yes/green?icon=windows)](https://lossless.cloud) [![Supports Mac OS X](https://badgen.net/badge/supports%20Mac%20OS%20X/yes/green?icon=apple)](https://lossless.cloud)
### Component Architecture
The diagram below illustrates the main components of SmartProxy and how they interact:
```mermaid
flowchart TB
Client([Client])
subgraph "SmartProxy Components"
direction TB
HTTP80[HTTP Port 80\nSslRedirect]
HTTPS443[HTTPS Port 443\nNetworkProxy]
PortProxy[TCP Port Proxy\nwith SNI routing]
IPTables[IPTablesProxy]
Router[ProxyRouter]
ACME[Port80Handler\nACME/Let's Encrypt]
Certs[(SSL Certificates)]
end
subgraph "Backend Services"
Service1[Service 1]
Service2[Service 2]
Service3[Service 3]
end
Client -->|HTTP Request| HTTP80
HTTP80 -->|Redirect| Client
Client -->|HTTPS Request| HTTPS443
Client -->|TLS/TCP| PortProxy
HTTPS443 -->|Route Request| Router
Router -->|Proxy Request| Service1
Router -->|Proxy Request| Service2
PortProxy -->|Direct TCP| Service2
PortProxy -->|Direct TCP| Service3
IPTables -.->|Low-level forwarding| PortProxy
HTTP80 -.->|Challenge Response| ACME
ACME -.->|Generate/Manage| Certs
Certs -.->|Provide TLS Certs| HTTPS443
classDef component fill:#f9f,stroke:#333,stroke-width:2px;
classDef backend fill:#bbf,stroke:#333,stroke-width:1px;
classDef client fill:#dfd,stroke:#333,stroke-width:2px;
class Client client;
class HTTP80,HTTPS443,PortProxy,IPTables,Router,ACME component;
class Service1,Service2,Service3 backend;
```
### HTTPS Reverse Proxy Flow
This diagram shows how HTTPS requests are handled and proxied to backend services:
```mermaid
sequenceDiagram
participant Client
participant NetworkProxy
participant ProxyRouter
participant Backend
Client->>NetworkProxy: HTTPS Request
Note over NetworkProxy: TLS Termination
NetworkProxy->>ProxyRouter: Route Request
ProxyRouter->>ProxyRouter: Match hostname to config
alt Authentication Required
NetworkProxy->>Client: Request Authentication
Client->>NetworkProxy: Send Credentials
NetworkProxy->>NetworkProxy: Validate Credentials
end
NetworkProxy->>Backend: Forward Request
Backend->>NetworkProxy: Response
Note over NetworkProxy: Add Default Headers
NetworkProxy->>Client: Forward Response
alt WebSocket Request
Client->>NetworkProxy: Upgrade to WebSocket
NetworkProxy->>Backend: Upgrade to WebSocket
loop WebSocket Active
Client->>NetworkProxy: WebSocket Message
NetworkProxy->>Backend: Forward Message
Backend->>NetworkProxy: WebSocket Message
NetworkProxy->>Client: Forward Message
NetworkProxy-->>NetworkProxy: Heartbeat Check
end
end
```
### Port Proxy with SNI-based Routing
This diagram illustrates how TCP connections with SNI (Server Name Indication) are processed and forwarded:
```mermaid
sequenceDiagram
participant Client
participant PortProxy
participant Backend
Client->>PortProxy: TLS Connection
alt SNI Enabled
PortProxy->>Client: Accept Connection
Client->>PortProxy: TLS ClientHello with SNI
PortProxy->>PortProxy: Extract SNI Hostname
PortProxy->>PortProxy: Match Domain Config
PortProxy->>PortProxy: Validate Client IP
alt IP Allowed
PortProxy->>Backend: Forward Connection
Note over PortProxy,Backend: Bidirectional Data Flow
else IP Rejected
PortProxy->>Client: Close Connection
end
else Port-based Routing
PortProxy->>PortProxy: Match Port Range
PortProxy->>PortProxy: Find Domain Config
PortProxy->>PortProxy: Validate Client IP
alt IP Allowed
PortProxy->>Backend: Forward Connection
Note over PortProxy,Backend: Bidirectional Data Flow
else IP Rejected
PortProxy->>Client: Close Connection
end
end
loop Connection Active
PortProxy-->>PortProxy: Monitor Activity
PortProxy-->>PortProxy: Check Max Lifetime
alt Inactivity or Max Lifetime Exceeded
PortProxy->>Client: Close Connection
PortProxy->>Backend: Close Connection
end
end
```
### Let's Encrypt Certificate Acquisition
This diagram shows how certificates are automatically acquired through the ACME protocol:
```mermaid
sequenceDiagram
participant Client
participant Port80Handler
participant ACME as Let's Encrypt ACME
participant NetworkProxy
Client->>Port80Handler: HTTP Request for domain
alt Certificate Exists
Port80Handler->>Client: Redirect to HTTPS
else No Certificate
Port80Handler->>Port80Handler: Mark domain as obtaining cert
Port80Handler->>ACME: Create account & new order
ACME->>Port80Handler: Challenge information
Port80Handler->>Port80Handler: Store challenge token & key authorization
ACME->>Port80Handler: HTTP-01 Challenge Request
Port80Handler->>ACME: Challenge Response
ACME->>ACME: Validate domain ownership
ACME->>Port80Handler: Challenge validated
Port80Handler->>Port80Handler: Generate CSR
Port80Handler->>ACME: Submit CSR
ACME->>Port80Handler: Issue Certificate
Port80Handler->>Port80Handler: Store certificate & private key
Port80Handler->>Port80Handler: Mark certificate as obtained
Note over Port80Handler,NetworkProxy: Certificate available for use
Client->>Port80Handler: Another HTTP Request
Port80Handler->>Client: Redirect to HTTPS
Client->>NetworkProxy: HTTPS Request
Note over NetworkProxy: Uses new certificate
end
```
## Features
- **HTTPS Reverse Proxy** - Route traffic to backend services based on hostname with TLS termination
- **WebSocket Support** - Full WebSocket proxying with heartbeat monitoring
- **TCP Port Forwarding** - Advanced port forwarding with SNI inspection and domain-based routing
- **Enhanced TLS Handling** - Robust TLS handshake processing with improved certificate error handling
- **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
- **IP Filtering** - Control access with IP allow/block lists using glob patterns
- **IPTables Integration** - Direct manipulation of iptables for low-level port forwarding
- **Basic Authentication** - Support for basic auth on proxied routes
- **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
- **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
## Installation
```bash
npm install @push.rocks/smartproxy
```
## Usage
## Contribution
### Basic Reverse Proxy Setup
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
```typescript
import { NetworkProxy } from '@push.rocks/smartproxy';
## Contribution
// Create a reverse proxy listening on port 443
const proxy = new NetworkProxy({
port: 443
});
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
// Define reverse proxy configurations
const proxyConfigs = [
{
hostName: 'example.com',
destinationIp: '127.0.0.1',
destinationPort: 3000,
publicKey: 'your-cert-content',
privateKey: 'your-key-content'
},
{
hostName: 'api.example.com',
destinationIp: '127.0.0.1',
destinationPort: 4000,
publicKey: 'your-cert-content',
privateKey: 'your-key-content',
// Optional basic auth
authentication: {
type: 'Basic',
user: 'admin',
pass: 'secret'
}
}
];
For further information read the linked docs at the top of this readme.
// Start the proxy and update configurations
(async () => {
await proxy.start();
await proxy.updateProxyConfigs(proxyConfigs);
// Add default headers to all responses
await proxy.addDefaultHeaders({
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload'
});
})();
```
> MIT licensed | **&copy;** [Lossless GmbH](https://lossless.gmbh)
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
### HTTP to HTTPS Redirection
[![repo-footer](https://lossless.gitlab.io/publicrelations/repofooter.svg)](https://maintainedby.lossless.com)
```typescript
import { SslRedirect } from '@push.rocks/smartproxy';
// Create and start HTTP to HTTPS redirect service on port 80
const redirector = new SslRedirect(80);
redirector.start();
```
### TCP Port Forwarding with Domain-based Routing
```typescript
import { PortProxy } from '@push.rocks/smartproxy';
// Configure port proxy with domain-based routing
const portProxy = new PortProxy({
fromPort: 443,
toPort: 8443,
targetIP: 'localhost', // Default target host
sniEnabled: true, // Enable SNI inspection
// Enhanced reliability settings
initialDataTimeout: 60000, // 60 seconds for initial TLS handshake
socketTimeout: 3600000, // 1 hour socket timeout
maxConnectionLifetime: 3600000, // 1 hour connection lifetime
inactivityTimeout: 3600000, // 1 hour inactivity timeout
maxPendingDataSize: 10 * 1024 * 1024, // 10MB buffer for large TLS handshakes
// Browser compatibility enhancement
enableTlsDebugLogging: false, // Enable for troubleshooting TLS issues
// Port and IP configuration
globalPortRanges: [{ from: 443, to: 443 }],
defaultAllowedIPs: ['*'], // Allow all IPs by default
// Socket optimizations for better connection stability
noDelay: true, // Disable Nagle's algorithm
keepAlive: true, // Enable TCP keepalive
enableKeepAliveProbes: true, // Enhanced keepalive for stability
// Domain-specific routing configuration
domainConfigs: [
{
domains: ['example.com', '*.example.com'], // Glob patterns for matching domains
allowedIPs: ['192.168.1.*'], // Restrict access by IP
blockedIPs: ['192.168.1.100'], // Block specific IPs
targetIPs: ['10.0.0.1', '10.0.0.2'], // Round-robin between multiple targets
portRanges: [{ from: 443, to: 443 }],
connectionTimeout: 7200000 // Domain-specific timeout (2 hours)
}
],
preserveSourceIP: true
});
portProxy.start();
```
### IPTables Port Forwarding
```typescript
import { IPTablesProxy } from '@push.rocks/smartproxy';
// Basic usage - forward single port
const basicProxy = new IPTablesProxy({
fromPort: 80,
toPort: 8080,
toHost: 'localhost',
preserveSourceIP: true,
deleteOnExit: true // Automatically clean up rules on process exit
});
// Forward port ranges
const rangeProxy = new IPTablesProxy({
fromPort: { from: 3000, to: 3010 }, // Forward ports 3000-3010
toPort: { from: 8000, to: 8010 }, // To ports 8000-8010
protocol: 'tcp', // TCP protocol (default)
ipv6Support: true, // Enable IPv6 support
enableLogging: true // Enable detailed logging
});
// Multiple port specifications with IP filtering
const advancedProxy = new IPTablesProxy({
fromPort: [80, 443, { from: 8000, to: 8010 }], // Multiple ports/ranges
toPort: [8080, 8443, { from: 18000, to: 18010 }],
allowedSourceIPs: ['10.0.0.0/8', '192.168.1.0/24'], // Only allow these IPs
bannedSourceIPs: ['192.168.1.100'], // Explicitly block these IPs
addJumpRule: true, // Use custom chain for better management
checkExistingRules: true // Check for duplicate rules
});
// NetworkProxy integration for SSL termination
const sslProxy = new IPTablesProxy({
fromPort: 443,
toPort: 8443,
netProxyIntegration: {
enabled: true,
redirectLocalhost: true, // Redirect localhost traffic to NetworkProxy
sslTerminationPort: 8443 // Port where NetworkProxy handles SSL
}
});
// Start any of the proxies
await basicProxy.start();
```
### Automatic HTTPS Certificate Management
```typescript
import { Port80Handler } from '@push.rocks/smartproxy';
// Create an ACME handler for Let's Encrypt
const acmeHandler = new Port80Handler();
// Add domains to manage certificates for
acmeHandler.addDomain('example.com');
acmeHandler.addDomain('api.example.com');
```
## Configuration Options
### NetworkProxy Options
| Option | Description | Default |
|----------------|---------------------------------------------------|---------|
| `port` | Port to listen on for HTTPS connections | - |
### PortProxy Settings
| Option | Description | Default |
|---------------------------|--------------------------------------------------------|-------------|
| `fromPort` | Port to listen on | - |
| `toPort` | Destination port to forward to | - |
| `targetIP` | Default destination IP if not specified in domainConfig | 'localhost' |
| `sniEnabled` | Enable SNI inspection for TLS connections | false |
| `defaultAllowedIPs` | IP patterns allowed by default | - |
| `defaultBlockedIPs` | IP patterns blocked by default | - |
| `preserveSourceIP` | Preserve the original client IP | false |
| `maxConnectionLifetime` | Maximum time in ms to keep a connection open | 3600000 |
| `initialDataTimeout` | Timeout for initial data/handshake in ms | 60000 |
| `socketTimeout` | Socket inactivity timeout in ms | 3600000 |
| `inactivityTimeout` | Connection inactivity check timeout in ms | 3600000 |
| `inactivityCheckInterval` | How often to check for inactive connections in ms | 60000 |
| `maxPendingDataSize` | Maximum bytes to buffer during connection setup | 10485760 |
| `globalPortRanges` | Array of port ranges to listen on | - |
| `forwardAllGlobalRanges` | Forward all global range connections to targetIP | false |
| `gracefulShutdownTimeout` | Time in ms to wait during shutdown | 30000 |
| `noDelay` | Disable Nagle's algorithm | true |
| `keepAlive` | Enable TCP keepalive | true |
| `keepAliveInitialDelay` | Initial delay before sending keepalive probes in ms | 30000 |
| `enableKeepAliveProbes` | Enable enhanced TCP keep-alive probes | false |
| `enableTlsDebugLogging` | Enable detailed TLS handshake debugging | false |
| `enableDetailedLogging` | Enable detailed connection logging | false |
| `enableRandomizedTimeouts`| Randomize timeouts slightly to prevent thundering herd | true |
### IPTablesProxy Settings
| Option | Description | Default |
|-----------------------|---------------------------------------------------|-------------|
| `fromPort` | Source port(s) or range(s) to forward from | - |
| `toPort` | Destination port(s) or range(s) to forward to | - |
| `toHost` | Destination host to forward to | 'localhost' |
| `preserveSourceIP` | Preserve the original client IP | false |
| `deleteOnExit` | Remove iptables rules when process exits | false |
| `protocol` | Protocol to forward ('tcp', 'udp', or 'all') | 'tcp' |
| `enableLogging` | Enable detailed logging | false |
| `ipv6Support` | Enable IPv6 support with ip6tables | false |
| `allowedSourceIPs` | Array of IP addresses/CIDR allowed to connect | - |
| `bannedSourceIPs` | Array of IP addresses/CIDR blocked from connecting | - |
| `forceCleanSlate` | Clear all IPTablesProxy rules before starting | false |
| `addJumpRule` | Add a custom chain for cleaner rule management | false |
| `checkExistingRules` | Check if rules already exist before adding | true |
| `netProxyIntegration` | NetworkProxy integration options (object) | - |
#### IPTablesProxy NetworkProxy Integration Options
| Option | Description | Default |
|----------------------|---------------------------------------------------|---------|
| `enabled` | Enable NetworkProxy integration | false |
| `redirectLocalhost` | Redirect localhost traffic to NetworkProxy | false |
| `sslTerminationPort` | Port where NetworkProxy handles SSL termination | - |
## Advanced Features
### TLS Handshake Optimization
The enhanced `PortProxy` implementation includes significant improvements for TLS handshake handling:
- Robust SNI extraction with improved error handling
- Increased buffer size for complex TLS handshakes (10MB)
- Longer initial handshake timeout (60 seconds)
- Detection and tracking of TLS connection states
- Optional detailed TLS debug logging for troubleshooting
- Browser compatibility fixes for Chrome certificate errors
```typescript
// Example configuration to solve Chrome certificate errors
const portProxy = new PortProxy({
// ... other settings
initialDataTimeout: 60000, // Give browser more time for handshake
maxPendingDataSize: 10 * 1024 * 1024, // Larger buffer for complex handshakes
enableTlsDebugLogging: true, // Enable when troubleshooting
});
```
### Connection Management and Monitoring
The `PortProxy` class includes built-in connection tracking and monitoring:
- Automatic cleanup of idle connections with configurable timeouts
- Timeouts for connections that exceed maximum lifetime
- Detailed logging of connection states
- Termination statistics
- Randomized timeouts to prevent "thundering herd" problems
- Per-domain timeout configuration
### WebSocket Support
The `NetworkProxy` class provides WebSocket support with:
- WebSocket connection proxying
- Automatic heartbeat monitoring
- Connection cleanup for inactive WebSockets
### SNI-based Routing
The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS handshakes to route connections based on the requested domain:
- Multiple backend targets per domain
- Round-robin load balancing
- Domain-specific allowed IP ranges
- Protection against SNI renegotiation attacks
### Enhanced IPTables Management
The improved `IPTablesProxy` class offers advanced capabilities:
- Support for multiple port ranges and individual ports
- IPv6 support with ip6tables
- Source IP filtering with allow/block lists
- Custom chain creation for better rule organization
- NetworkProxy integration for SSL termination
- Automatic rule existence checking to prevent duplicates
- Comprehensive cleanup on shutdown
## Troubleshooting
### Browser Certificate Errors
If you experience certificate errors in browsers, especially in Chrome, try these solutions:
1. **Increase Initial Data Timeout**: Set `initialDataTimeout` to 60 seconds or higher
2. **Increase Buffer Size**: Set `maxPendingDataSize` to 10MB or higher
3. **Enable TLS Debug Logging**: Set `enableTlsDebugLogging: true` to troubleshoot handshake issues
4. **Enable Keep-Alive Probes**: Set `enableKeepAliveProbes: true` for better connection stability
5. **Check Certificate Chain**: Ensure your certificate chain is complete and in the correct order
```typescript
// Configuration to fix Chrome certificate errors
const portProxy = new PortProxy({
// ... other settings
initialDataTimeout: 60000,
maxPendingDataSize: 10 * 1024 * 1024,
enableTlsDebugLogging: true,
enableKeepAliveProbes: true
});
```
### Connection Stability
For improved connection stability in high-traffic environments:
1. **Set Appropriate Timeouts**: Use longer timeouts for long-lived connections
2. **Use Domain-Specific Timeouts**: Configure per-domain timeouts for different types of services
3. **Enable TCP Keep-Alive**: Ensure `keepAlive` is set to `true`
4. **Monitor Connection Statistics**: Enable detailed logging to track termination reasons
5. **Fine-tune Inactivity Checks**: Adjust `inactivityCheckInterval` based on your traffic patterns
### IPTables Troubleshooting
If you're experiencing issues with IPTablesProxy:
1. **Enable Detailed Logging**: Set `enableLogging: true` to see all rule operations
2. **Force Clean Slate**: Use `forceCleanSlate: true` to remove any lingering rules
3. **Use Custom Chains**: Enable `addJumpRule: true` for cleaner rule management
4. **Check Permissions**: Ensure your process has sufficient permissions to modify iptables
5. **Verify IPv6 Support**: If using `ipv6Support: true`, ensure ip6tables is available
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEljCCAn4CCQDY+ZbC9FASVjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJE
RTAeFw0xOTA5MjAxNjAxNDRaFw0yMDA5MTkxNjAxNDRaMA0xCzAJBgNVBAYTAkRF
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4thf9JEK/epoXt8hFr8t
pkRzmaEkgbSKoOga3uGXDLvdNf3BzSIxZ8pzRhZfUnutcmW1thdz3wre/pEJR7oN
QsfixbLL8/oS5QeXKiUGX0Ssfdg4W0TsoLcRva+1AZsf38MfiUPhzh1/UW/rMywW
asazQwRZdkkXb4nKJ2IFZx22qnAD4/5Sug+sfeKoFBF/rzI2yK7rognt7kW2LHv6
rswHnZ1Z2P/gbhlZ/EhG9hFVRZwRLDscWKcuWcxkePDt2J1pDNqD6SYa6ZjGC3AE
TJw5iEA1bLQ9YvjDNpVYcf6ZvcSilIFjSQu5cs9sUbHGeKTrS5HzfeJXh1PfJyL8
X0Hu7UBSjfSudso3baE9FGiBFBW2cnXZKDZGtV8eq/qxPetOOgS09pVbNP6508WV
BR+rz98/VDZLZqcbZ2UpOuz4+kAKmbYE9GplxKQZZO7wWEox7Mid/uUdcqEo4QKn
no6ujOuzQzn5a2oOS0k5Hk3uHapNJWlW9YI3LHtfADpYH+6cOR+/c3JWBzQJ6AD7
muvNzA9mWXeHqLxMMP4pkmb7otzZYrEkodUqJgAQxcYhGh6XsCPfJ/D9RN734OJc
gleVXFI8Kz455HxCW19XNfz16k7T6kqhZ/6SOBbkxEuqg7oEthAP109ZZzgx4oDo
hQsw24TjLkI4SPIc7nr60UUCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAu0+zrg0C
mlSv4Yi24OwB7TBvx+WHesl1IilCUdTiiUMo3NumvsU9Dr3Jkd0jGqYI0eyH4gIt
KrhAveXfEw7tAOEHiYicmAdIFtyzh++ZWb8mgbBeqij1MP/76Jv+cc0lUqpfRo/A
qytAsPAILuyL1o1jh28JHcq+v+WYn/FEhjUlH6emhGKGlsAjhUPjzK8MEshNolhj
t2UXw9WB5B2xWvrqlNMy0F3NAZBkZ/+k21HZo6FmVi+q6OEGcOo7wJt6wrH/lko9
LxX96GC1JoN1Pfr2FoTKy1WHzrSfyGmDIUCrbaYQ58UuMOR+5eIPPdkf/030u5eX
xXhF2fBujD57E2zQGh/l2MrOjamcSo0+wYhOqlX3WNdaKNAzPqloBnF6w7eqLYde
h9He39ySmxjENwv3miOjEP1sBeMBSRfL/ckEonfK5uJgYA5nVMQ3ojUeDMZzLfFE
Ue2WHt+uPyYk7mMZfOrK2uHzI2/Coqj7lbfRodFwj+fCArYBck2NZannDPKA6X8V
TzJTbTCteOUUJTrcfZ0gGhGkF4nYLmX5OI+TPqrDJf0fZ+mzAEHzDDVXcBYpYRDr
r8d9QwrK+WaqVi2ofbMfMByVF72jgeJNa4nxwT9bVbu/Q1T2Lt+YPb4pQ7yCoUgS
JNj2Dr5H0XoLFFnvuvzcRbhlJ9J67JzR+7g=
-----END CERTIFICATE-----

View File

@ -0,0 +1,37 @@
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import * as tls from 'tls';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export interface TestCertificates {
privateKey: string;
publicKey: string;
}
export function loadTestCertificates(): TestCertificates {
const certPath = path.join(__dirname, '..', '..', 'assets', 'certs', 'cert.pem');
const keyPath = path.join(__dirname, '..', '..', 'assets', 'certs', 'key.pem');
// Read certificates
const publicKey = fs.readFileSync(certPath, 'utf8');
const privateKey = fs.readFileSync(keyPath, 'utf8');
// Validate certificates
try {
// Try to create a secure context with the certificates
tls.createSecureContext({
cert: publicKey,
key: privateKey
});
} catch (error) {
throw new Error(`Invalid certificates: ${error.message}`);
}
return {
privateKey,
publicKey
};
}

View File

@ -1,52 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIJRQIBADANBgkqhkiG9w0BAQEFAASCCS8wggkrAgEAAoICAQDi2F/0kQr96mhe
3yEWvy2mRHOZoSSBtIqg6Bre4ZcMu901/cHNIjFnynNGFl9Se61yZbW2F3PfCt7+
kQlHug1Cx+LFssvz+hLlB5cqJQZfRKx92DhbROygtxG9r7UBmx/fwx+JQ+HOHX9R
b+szLBZqxrNDBFl2SRdviconYgVnHbaqcAPj/lK6D6x94qgUEX+vMjbIruuiCe3u
RbYse/quzAednVnY/+BuGVn8SEb2EVVFnBEsOxxYpy5ZzGR48O3YnWkM2oPpJhrp
mMYLcARMnDmIQDVstD1i+MM2lVhx/pm9xKKUgWNJC7lyz2xRscZ4pOtLkfN94leH
U98nIvxfQe7tQFKN9K52yjdtoT0UaIEUFbZyddkoNka1Xx6r+rE96046BLT2lVs0
/rnTxZUFH6vP3z9UNktmpxtnZSk67Pj6QAqZtgT0amXEpBlk7vBYSjHsyJ3+5R1y
oSjhAqeejq6M67NDOflrag5LSTkeTe4dqk0laVb1gjcse18AOlgf7pw5H79zclYH
NAnoAPua683MD2ZZd4eovEww/imSZvui3NlisSSh1SomABDFxiEaHpewI98n8P1E
3vfg4lyCV5VcUjwrPjnkfEJbX1c1/PXqTtPqSqFn/pI4FuTES6qDugS2EA/XT1ln
ODHigOiFCzDbhOMuQjhI8hzuevrRRQIDAQABAoICAQC7nU+HW6qmpQebZ5nbUVT1
Deo6Js+lwudg+3a13ghqzLnBXNW7zkrkV8mNLxW5h3bFhZ+LMcxwrXIPQ29Udmlf
USiacC1E5RBZgjSg86xYgNjU4E6EFfZLWf3/T2I6KM1s6NmdUppgOX9CoHj7grwr
pZk/lUpUjVEnu+OJPQXQ6f9Y6XoeSAqtvibgmuR+bJaZFMPAqQNTqjix99Aa7JNB
nJez4R8dXUuGY8tL349pFp7bCqAdX+oq3GJ2fJigekuM+2uV6OhunUhm6Sbq8MNt
hUwEB27oMA4RXENAUraq2XLYQ9hfUMAH+v1vGmSxEIJg561/e//RnrDbyR9oJARr
SbopI3Ut5yKxVKMYOTSqcFQXVLszTExhMhQCRoOh58BpIfhb9FLCKD9LH8E6eoQf
ygPWryey9AAJ7B2PQXVbitzcOML27rzC4DXS+mLe6AVL6t2IldaeMTlumlnc620d
Yuf5wSe8qe4xpKOlrE9emnBmbL0sGivsU+mpz9oSjxEpHGA7eoTIOmQiZnuzpkmi
1ZSU4OwqNavphy6cklONShQOmE8LMI0wRbunLjIFY8fme/8u+tVvWrTuJiCGPnXQ
F2lb0qwtDVRlexyM+GTPYstU5v7HxkQB3B+uwTgYuupCmTNmO8hjSCS/EYpHzmFe
YHDEN+Cj8f+vmKxN0F/6QQKCAQEA9+wTQU2GSoVX8IB0U6T+hX0BFhQq5ISH/s76
kWIEunY1MCkRL9YygvHkKW3dsXVOzsip/axiT36MhRcyZ27hF1tz3j//Z11E3Bfq
PkzyUVuU3jpWZkBE2VhXpDXlyW8xR/y1ZOaZZ//XcZTrZf57pGKFp30H/PlDPH3C
YtjEuQNmPCgnfz8iXx+vDYx8hwLHNv+DoX2WYuThUnul/QGSKL3xh3qWd8rotnUB
c8bV4ymk35fVJu/+pTZpPnMkYrFReso/uNn07y1iga/9mwkUBNrT+fWE7RzjT7H8
ykMMOGCK6bc7joCvALZaUDne714hNW3s9a7L1clehUA8/xwplQKCAQEA6jx/CIQd
RVdJFihSSZbqdrOAblVdl+WkjhALWNRMoRCCRniNubbgxgKfQ0scKUeubYxScBVk
rlUMl6/2Gr9uzuSC0WPVAE6OLvLNcQafw1mQ1UTJiEzYvczJKwipzXcgGQWO9Q9a
T3ETh6Be62si2r6fH4agQzbp4HkTEoWgPu6MJpqqcLoc8laty0d1huqU9du1TRzT
3etjopWRd0I3ID+WkkGKjYWRQ1bkKjvkkj1v7bHenX17nfIp5WU1aXTMYUCMMszm
pgVBDeJGKpPpP3scl7go5Y4KC6H+IeYaeCEk3hWW4robpHBzupkgpRLzmBopjRlN
v3+HQ7OkviX88QKCAQEAg5IJdfKKfindzYieM3WwjW8VkH4LdVLQSW3WlCkMkVgC
ShjBQj3OeKeeik4ABRlYRW1AqZs+YSmrsUXqPfIeCqNCDoSwKk7ZKGSYr49uWbbc
fkM/buxUnXPAryjbVddos+ds7KtkZkjkMSby9iHjxA11GLnF737pK8Uh0Atx+y3O
p8Y3j9QVjZ3m7K3NuGjFCG75kE5x7PHCkl+Ea4zV4EFNWLS5/cD1Vz8pEiRHhlKn
aPHO8OcUoOELYVUBzk6EC0IiJxukXPoc+O5JDGn48cqgDFs7vApEqBqxKTYD2jeC
AR54wNuSBDLCIylTIn016oD37DpjeoVvYBADTu/HMQKCAQEA1rFuajrVrWnMpo98
pNC7xOLQM9DwwToOMtwH2np0ZiiAj+ENXgx+R1+95Gsiu79k5Cn6oZsqNhPkP+Bb
fba69M1EDnInmGloLyYDIbbFlsMwWhn7cn+lJYpfVJ9TK+0lMWoD1yAkUa4+DVDz
z2naf466wKWfnRvnEAVJcu+hqizxrqySzlH4GDNUhn7P/UJkGFkx+yUSGFUZdLsM
orfBWUCPXSzPttmXBJbO+Nr+rP+86KvgdI/AT0vYFNdINomEjxsfpaxjOAaW0wfz
8jCyWKoZ0gJNEeK32GO5UA7dcgBHD3vQWa3lijo8COsznboaJe7M6PQpa/2S2H3+
4P5msQKCAQEAx7NP3y+5ttfTd/eQ7/cg1/0y2WxvpOYNLt6MWz4rPWyD6QwidzTG
pjuQFQ5Ods+BwJ/Jbirb7l4GMAxfIbEPAkPTHpvswO0xcncSYxl0sSP/WIA6sbcM
dp7B/scdORC8Y6i8oPdCyxyCTd2SBrmGr2krAXmQquT72eusyP5E8HFhCy1iYt22
aL68dZLv9/sRAF08t9Wy+eYjD/hCj67t7uGCZQT8wJbKr8aJcjwVwJgghh+3EydK
h+7fBVO49PLL0NWy+8GT8y7a04calFfLvZEA2UMaunBis3dE1KMFfJL/0JO+sKnF
2TkK01XDDJURK5Lhuvc7WrK2rSJ/fK+0GA==
-----END PRIVATE KEY-----

343
test/test.portproxy.ts Normal file
View File

@ -0,0 +1,343 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { PortProxy } from '../ts/classes.pp.portproxy.js';
let testServer: net.Server;
let portProxy: PortProxy;
const TEST_SERVER_PORT = 4000;
const PROXY_PORT = 4001;
const TEST_DATA = 'Hello through port proxy!';
// Track all created servers and proxies for proper cleanup
const allServers: net.Server[] = [];
const allProxies: PortProxy[] = [];
// Helper: Creates a test TCP server that listens on a given port and host.
function createTestServer(port: number, host: string = 'localhost'): Promise<net.Server> {
return new Promise((resolve) => {
const server = net.createServer((socket) => {
socket.on('data', (data) => {
// Echo the received data back with a prefix.
socket.write(`Echo: ${data.toString()}`);
});
socket.on('error', (error) => {
console.error(`[Test Server] Socket error on ${host}:${port}:`, error);
});
});
server.listen(port, host, () => {
console.log(`[Test Server] Listening on ${host}:${port}`);
allServers.push(server); // Track this server
resolve(server);
});
});
}
// Helper: Creates a test client connection.
function createTestClient(port: number, data: string): Promise<string> {
return new Promise((resolve, reject) => {
const client = new net.Socket();
let response = '';
const timeout = setTimeout(() => {
client.destroy();
reject(new Error(`Client connection timeout to port ${port}`));
}, 5000);
client.connect(port, 'localhost', () => {
console.log('[Test Client] Connected to server');
client.write(data);
});
client.on('data', (chunk) => {
response += chunk.toString();
client.end();
});
client.on('end', () => {
clearTimeout(timeout);
resolve(response);
});
client.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
}
// SETUP: Create a test server and a PortProxy instance.
tap.test('setup port proxy test environment', async () => {
testServer = await createTestServer(TEST_SERVER_PORT);
portProxy = new PortProxy({
fromPort: PROXY_PORT,
toPort: TEST_SERVER_PORT,
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
allProxies.push(portProxy); // Track this proxy
});
// Test that the proxy starts and its servers are listening.
tap.test('should start port proxy', async () => {
await portProxy.start();
expect((portProxy as any).netServers.every((server: net.Server) => server.listening)).toBeTrue();
});
// Test basic TCP forwarding.
tap.test('should forward TCP connections and data to localhost', async () => {
const response = await createTestClient(PROXY_PORT, TEST_DATA);
expect(response).toEqual(`Echo: ${TEST_DATA}`);
});
// Test proxy with a custom target host.
tap.test('should forward TCP connections to custom host', async () => {
const customHostProxy = new PortProxy({
fromPort: PROXY_PORT + 1,
toPort: TEST_SERVER_PORT,
targetIP: '127.0.0.1',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
allProxies.push(customHostProxy); // Track this proxy
await customHostProxy.start();
const response = await createTestClient(PROXY_PORT + 1, TEST_DATA);
expect(response).toEqual(`Echo: ${TEST_DATA}`);
await customHostProxy.stop();
// Remove from tracking after stopping
const index = allProxies.indexOf(customHostProxy);
if (index !== -1) allProxies.splice(index, 1);
});
// Test custom IP forwarding
// Modified to work in Docker/CI environments without needing 127.0.0.2
tap.test('should forward connections to custom IP', async () => {
// Set up ports that are FAR apart to avoid any possible confusion
const forcedProxyPort = PROXY_PORT + 2; // 4003 - The port that our proxy listens on
const targetServerPort = TEST_SERVER_PORT + 200; // 4200 - Target test server on different port
// Create a test server listening on a unique port on 127.0.0.1 (works in all environments)
const testServer2 = await createTestServer(targetServerPort, '127.0.0.1');
// We're simulating routing to a different IP by using a different port
// This tests the core functionality without requiring multiple IPs
const domainProxy = new PortProxy({
fromPort: forcedProxyPort, // 4003 - Listen on this port
toPort: targetServerPort, // 4200 - Forward to this port
targetIP: '127.0.0.1', // Always use localhost (works in Docker)
domainConfigs: [], // No domain configs to confuse things
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], // Allow localhost
// We'll test the functionality WITHOUT port ranges this time
globalPortRanges: []
});
allProxies.push(domainProxy); // Track this proxy
await domainProxy.start();
// Send a single test connection
const response = await createTestClient(forcedProxyPort, TEST_DATA);
expect(response).toEqual(`Echo: ${TEST_DATA}`);
await domainProxy.stop();
// Remove from tracking after stopping
const proxyIndex = allProxies.indexOf(domainProxy);
if (proxyIndex !== -1) allProxies.splice(proxyIndex, 1);
// Close the test server
await new Promise<void>((resolve) => testServer2.close(() => resolve()));
// Remove from tracking
const serverIndex = allServers.indexOf(testServer2);
if (serverIndex !== -1) allServers.splice(serverIndex, 1);
});
// Test handling of multiple concurrent connections.
tap.test('should handle multiple concurrent connections', async () => {
const concurrentRequests = 5;
const requests = Array(concurrentRequests).fill(null).map((_, i) =>
createTestClient(PROXY_PORT, `${TEST_DATA} ${i + 1}`)
);
const responses = await Promise.all(requests);
responses.forEach((response, i) => {
expect(response).toEqual(`Echo: ${TEST_DATA} ${i + 1}`);
});
});
// Test connection timeout handling.
tap.test('should handle connection timeouts', async () => {
const client = new net.Socket();
await new Promise<void>((resolve) => {
// Add a timeout to ensure we don't hang here
const timeout = setTimeout(() => {
client.destroy();
resolve();
}, 3000);
client.connect(PROXY_PORT, 'localhost', () => {
// Do not send any data to trigger a timeout.
client.on('close', () => {
clearTimeout(timeout);
resolve();
});
});
client.on('error', () => {
clearTimeout(timeout);
client.destroy();
resolve();
});
});
});
// Test stopping the port proxy.
tap.test('should stop port proxy', async () => {
await portProxy.stop();
expect((portProxy as any).netServers.every((server: net.Server) => !server.listening)).toBeTrue();
// Remove from tracking
const index = allProxies.indexOf(portProxy);
if (index !== -1) allProxies.splice(index, 1);
});
// Test chained proxies with and without source IP preservation.
tap.test('should support optional source IP preservation in chained proxies', async () => {
// Chained proxies without IP preservation.
const firstProxyDefault = new PortProxy({
fromPort: PROXY_PORT + 4,
toPort: PROXY_PORT + 5,
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
globalPortRanges: []
});
const secondProxyDefault = new PortProxy({
fromPort: PROXY_PORT + 5,
toPort: TEST_SERVER_PORT,
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
globalPortRanges: []
});
allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies
await secondProxyDefault.start();
await firstProxyDefault.start();
const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA);
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
await firstProxyDefault.stop();
await secondProxyDefault.stop();
// Remove from tracking
const index1 = allProxies.indexOf(firstProxyDefault);
if (index1 !== -1) allProxies.splice(index1, 1);
const index2 = allProxies.indexOf(secondProxyDefault);
if (index2 !== -1) allProxies.splice(index2, 1);
// Chained proxies with IP preservation.
const firstProxyPreserved = new PortProxy({
fromPort: PROXY_PORT + 6,
toPort: PROXY_PORT + 7,
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
preserveSourceIP: true,
globalPortRanges: []
});
const secondProxyPreserved = new PortProxy({
fromPort: PROXY_PORT + 7,
toPort: TEST_SERVER_PORT,
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
preserveSourceIP: true,
globalPortRanges: []
});
allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies
await secondProxyPreserved.start();
await firstProxyPreserved.start();
const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA);
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
await firstProxyPreserved.stop();
await secondProxyPreserved.stop();
// Remove from tracking
const index3 = allProxies.indexOf(firstProxyPreserved);
if (index3 !== -1) allProxies.splice(index3, 1);
const index4 = allProxies.indexOf(secondProxyPreserved);
if (index4 !== -1) allProxies.splice(index4, 1);
});
// Test round-robin behavior for multiple target IPs in a domain config.
tap.test('should use round robin for multiple target IPs in domain config', async () => {
const domainConfig = {
domains: ['rr.test'],
allowedIPs: ['127.0.0.1'],
targetIPs: ['hostA', 'hostB']
} as any;
const proxyInstance = new PortProxy({
fromPort: 0,
toPort: 0,
targetIP: 'localhost',
domainConfigs: [domainConfig],
sniEnabled: false,
defaultAllowedIPs: [],
globalPortRanges: []
});
// Don't track this proxy as it doesn't actually start or listen
const firstTarget = proxyInstance.domainConfigManager.getTargetIP(domainConfig);
const secondTarget = proxyInstance.domainConfigManager.getTargetIP(domainConfig);
expect(firstTarget).toEqual('hostA');
expect(secondTarget).toEqual('hostB');
});
// CLEANUP: Tear down all servers and proxies
tap.test('cleanup port proxy test environment', async () => {
// Stop all remaining proxies
for (const proxy of [...allProxies]) {
try {
await proxy.stop();
const index = allProxies.indexOf(proxy);
if (index !== -1) allProxies.splice(index, 1);
} catch (err) {
console.error(`Error stopping proxy: ${err}`);
}
}
// Close all remaining servers
for (const server of [...allServers]) {
try {
await new Promise<void>((resolve) => {
if (server.listening) {
server.close(() => resolve());
} else {
resolve();
}
});
const index = allServers.indexOf(server);
if (index !== -1) allServers.splice(index, 1);
} catch (err) {
console.error(`Error closing server: ${err}`);
}
}
// Verify all resources are cleaned up
expect(allProxies.length).toEqual(0);
expect(allServers.length).toEqual(0);
});
export default tap.start();

392
test/test.router.ts Normal file
View File

@ -0,0 +1,392 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as tsclass from '@tsclass/tsclass';
import * as http from 'http';
import { ProxyRouter, type IRouterResult } from '../ts/classes.router.js';
// Test proxies and configurations
let router: ProxyRouter;
// Sample hostname for testing
const TEST_DOMAIN = 'example.com';
const TEST_SUBDOMAIN = 'api.example.com';
const TEST_WILDCARD = '*.example.com';
// Helper: Creates a mock HTTP request for testing
function createMockRequest(host: string, url: string = '/'): http.IncomingMessage {
const req = {
headers: { host },
url,
socket: {
remoteAddress: '127.0.0.1'
}
} as any;
return req;
}
// Helper: Creates a test proxy configuration
function createProxyConfig(
hostname: string,
destinationIp: string = '10.0.0.1',
destinationPort: number = 8080
): tsclass.network.IReverseProxyConfig {
return {
hostName: hostname,
destinationIp,
destinationPort: destinationPort.toString(), // Convert to string for IReverseProxyConfig
publicKey: 'mock-cert',
privateKey: 'mock-key'
} as tsclass.network.IReverseProxyConfig;
}
// SETUP: Create a ProxyRouter instance
tap.test('setup proxy router test environment', async () => {
router = new ProxyRouter();
// Initialize with empty config
router.setNewProxyConfigs([]);
});
// Test basic routing by hostname
tap.test('should route requests by hostname', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
const req = createMockRequest(TEST_DOMAIN);
const result = router.routeReq(req);
expect(result).toBeTruthy();
expect(result).toEqual(config);
});
// Test handling of hostname with port number
tap.test('should handle hostname with port number', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
const req = createMockRequest(`${TEST_DOMAIN}:443`);
const result = router.routeReq(req);
expect(result).toBeTruthy();
expect(result).toEqual(config);
});
// Test case-insensitive hostname matching
tap.test('should perform case-insensitive hostname matching', async () => {
const config = createProxyConfig(TEST_DOMAIN.toLowerCase());
router.setNewProxyConfigs([config]);
const req = createMockRequest(TEST_DOMAIN.toUpperCase());
const result = router.routeReq(req);
expect(result).toBeTruthy();
expect(result).toEqual(config);
});
// Test handling of unmatched hostnames
tap.test('should return undefined for unmatched hostnames', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
const req = createMockRequest('unknown.domain.com');
const result = router.routeReq(req);
expect(result).toBeUndefined();
});
// Test adding path patterns
tap.test('should match requests using path patterns', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
// Add a path pattern to the config
router.setPathPattern(config, '/api/users');
// Test that path matches
const req1 = createMockRequest(TEST_DOMAIN, '/api/users');
const result1 = router.routeReqWithDetails(req1);
expect(result1).toBeTruthy();
expect(result1.config).toEqual(config);
expect(result1.pathMatch).toEqual('/api/users');
// Test that non-matching path doesn't match
const req2 = createMockRequest(TEST_DOMAIN, '/web/users');
const result2 = router.routeReqWithDetails(req2);
expect(result2).toBeUndefined();
});
// Test handling wildcard patterns
tap.test('should support wildcard path patterns', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
router.setPathPattern(config, '/api/*');
// Test with path that matches the wildcard pattern
const req = createMockRequest(TEST_DOMAIN, '/api/users/123');
const result = router.routeReqWithDetails(req);
expect(result).toBeTruthy();
expect(result.config).toEqual(config);
expect(result.pathMatch).toEqual('/api');
// Print the actual value to diagnose issues
console.log('Path remainder value:', result.pathRemainder);
expect(result.pathRemainder).toBeTruthy();
expect(result.pathRemainder).toEqual('/users/123');
});
// Test extracting path parameters
tap.test('should extract path parameters from URL', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
router.setPathPattern(config, '/users/:id/profile');
const req = createMockRequest(TEST_DOMAIN, '/users/123/profile');
const result = router.routeReqWithDetails(req);
expect(result).toBeTruthy();
expect(result.config).toEqual(config);
expect(result.pathParams).toBeTruthy();
expect(result.pathParams.id).toEqual('123');
});
// Test multiple configs for same hostname with different paths
tap.test('should support multiple configs for same hostname with different paths', async () => {
const apiConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001);
const webConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002);
// Add both configs
router.setNewProxyConfigs([apiConfig, webConfig]);
// Set different path patterns
router.setPathPattern(apiConfig, '/api');
router.setPathPattern(webConfig, '/web');
// Test API path routes to API config
const apiReq = createMockRequest(TEST_DOMAIN, '/api/users');
const apiResult = router.routeReq(apiReq);
expect(apiResult).toEqual(apiConfig);
// Test web path routes to web config
const webReq = createMockRequest(TEST_DOMAIN, '/web/dashboard');
const webResult = router.routeReq(webReq);
expect(webResult).toEqual(webConfig);
// Test unknown path returns undefined
const unknownReq = createMockRequest(TEST_DOMAIN, '/unknown');
const unknownResult = router.routeReq(unknownReq);
expect(unknownResult).toBeUndefined();
});
// Test wildcard subdomains
tap.test('should match wildcard subdomains', async () => {
const wildcardConfig = createProxyConfig(TEST_WILDCARD);
router.setNewProxyConfigs([wildcardConfig]);
// Test that subdomain.example.com matches *.example.com
const req = createMockRequest('subdomain.example.com');
const result = router.routeReq(req);
expect(result).toBeTruthy();
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('*');
const specificConfig = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([defaultConfig, specificConfig]);
// Test specific domain routes to specific config
const specificReq = createMockRequest(TEST_DOMAIN);
const specificResult = router.routeReq(specificReq);
expect(specificResult).toEqual(specificConfig);
// Test unknown domain falls back to default config
const unknownReq = createMockRequest('unknown.com');
const unknownResult = router.routeReq(unknownReq);
expect(unknownResult).toEqual(defaultConfig);
});
// Test priority between exact and wildcard matches
tap.test('should prioritize exact hostname over wildcard', async () => {
const wildcardConfig = createProxyConfig(TEST_WILDCARD);
const exactConfig = createProxyConfig(TEST_SUBDOMAIN);
router.setNewProxyConfigs([wildcardConfig, exactConfig]);
// Test that exact match takes priority
const req = createMockRequest(TEST_SUBDOMAIN);
const result = router.routeReq(req);
expect(result).toEqual(exactConfig);
});
// Test adding and removing configurations
tap.test('should manage configurations correctly', async () => {
router.setNewProxyConfigs([]);
// Add a config
const config = createProxyConfig(TEST_DOMAIN);
router.addProxyConfig(config);
// Verify routing works
const req = createMockRequest(TEST_DOMAIN);
let result = router.routeReq(req);
expect(result).toEqual(config);
// Remove the config and verify it no longer routes
const removed = router.removeProxyConfig(TEST_DOMAIN);
expect(removed).toBeTrue();
result = router.routeReq(req);
expect(result).toBeUndefined();
});
// Test path pattern specificity
tap.test('should prioritize more specific path patterns', async () => {
const genericConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001);
const specificConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002);
router.setNewProxyConfigs([genericConfig, specificConfig]);
router.setPathPattern(genericConfig, '/api/*');
router.setPathPattern(specificConfig, '/api/users');
// The more specific '/api/users' should match before the '/api/*' wildcard
const req = createMockRequest(TEST_DOMAIN, '/api/users');
const result = router.routeReq(req);
expect(result).toEqual(specificConfig);
});
// Test getHostnames method
tap.test('should retrieve all configured hostnames', async () => {
router.setNewProxyConfigs([
createProxyConfig(TEST_DOMAIN),
createProxyConfig(TEST_SUBDOMAIN)
]);
const hostnames = router.getHostnames();
expect(hostnames.length).toEqual(2);
expect(hostnames).toContain(TEST_DOMAIN.toLowerCase());
expect(hostnames).toContain(TEST_SUBDOMAIN.toLowerCase());
});
// Test handling missing host header
tap.test('should handle missing host header', async () => {
const defaultConfig = createProxyConfig('*');
router.setNewProxyConfigs([defaultConfig]);
const req = createMockRequest('');
req.headers.host = undefined;
const result = router.routeReq(req);
expect(result).toEqual(defaultConfig);
});
// Test complex path parameters
tap.test('should handle complex path parameters', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
router.setPathPattern(config, '/api/:version/users/:userId/posts/:postId');
const req = createMockRequest(TEST_DOMAIN, '/api/v1/users/123/posts/456');
const result = router.routeReqWithDetails(req);
expect(result).toBeTruthy();
expect(result.config).toEqual(config);
expect(result.pathParams).toBeTruthy();
expect(result.pathParams.version).toEqual('v1');
expect(result.pathParams.userId).toEqual('123');
expect(result.pathParams.postId).toEqual('456');
});
// Performance test
tap.test('should handle many configurations efficiently', async () => {
const configs = [];
// Create many configs with different hostnames
for (let i = 0; i < 100; i++) {
configs.push(createProxyConfig(`host-${i}.example.com`));
}
router.setNewProxyConfigs(configs);
// Test middle of the list to avoid best/worst case
const req = createMockRequest('host-50.example.com');
const result = router.routeReq(req);
expect(result).toEqual(configs[50]);
});
// Test cleanup
tap.test('cleanup proxy router test environment', async () => {
// Clear all configurations
router.setNewProxyConfigs([]);
// Verify empty state
expect(router.getHostnames().length).toEqual(0);
expect(router.getProxyConfigs().length).toEqual(0);
});
export default tap.start();

View File

@ -1,116 +1,513 @@
import { expect, tap } from '@pushrocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
import * as smartproxy from '../ts/index.js';
import { loadTestCertificates } from './helpers/certificates.js';
import * as https from 'https';
import * as http from 'http';
import { WebSocket, WebSocketServer } from 'ws';
let testProxy: smartproxy.NetworkProxy;
let testServer: http.Server;
let wsServer: WebSocketServer;
let testCertificates: { privateKey: string; publicKey: string };
tap.test('first test', async () => {
testProxy = new smartproxy.NetworkProxy({
port: 3001
// Helper function to make HTTPS requests
async function makeHttpsRequest(
options: https.RequestOptions,
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders; body: string }> {
console.log('[TEST] Making HTTPS request:', {
hostname: options.hostname,
port: options.port,
path: options.path,
method: options.method,
headers: options.headers,
});
expect(testProxy).toBeInstanceOf(smartproxy.NetworkProxy);
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
console.log('[TEST] Received HTTPS response:', {
statusCode: res.statusCode,
headers: res.headers,
});
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
console.log('[TEST] Response completed:', { data });
resolve({
statusCode: res.statusCode!,
headers: res.headers,
body: data,
});
});
});
req.on('error', (error) => {
console.error('[TEST] Request error:', error);
reject(error);
});
req.end();
});
}
// Setup test environment
tap.test('setup test environment', async () => {
// Load and validate certificates
console.log('[TEST] Loading and validating certificates');
testCertificates = loadTestCertificates();
console.log('[TEST] Certificates loaded and validated');
// Create a test HTTP server
testServer = http.createServer((req, res) => {
console.log('[TEST SERVER] Received HTTP request:', {
url: req.url,
method: req.method,
headers: req.headers,
});
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from test server!');
});
// Handle WebSocket upgrade requests
testServer.on('upgrade', (request, socket, head) => {
console.log('[TEST SERVER] Received WebSocket upgrade request:', {
url: request.url,
method: request.method,
headers: {
host: request.headers.host,
upgrade: request.headers.upgrade,
connection: request.headers.connection,
'sec-websocket-key': request.headers['sec-websocket-key'],
'sec-websocket-version': request.headers['sec-websocket-version'],
'sec-websocket-protocol': request.headers['sec-websocket-protocol'],
},
});
if (request.headers.upgrade?.toLowerCase() !== 'websocket') {
console.log('[TEST SERVER] Not a WebSocket upgrade request');
socket.destroy();
return;
}
console.log('[TEST SERVER] Handling WebSocket upgrade');
wsServer.handleUpgrade(request, socket, head, (ws) => {
console.log('[TEST SERVER] WebSocket connection upgraded');
wsServer.emit('connection', ws, request);
});
});
// Create a WebSocket server (for the test HTTP server)
console.log('[TEST SERVER] Creating WebSocket server');
wsServer = new WebSocketServer({
noServer: true,
perMessageDeflate: false,
clientTracking: true,
handleProtocols: () => 'echo-protocol',
});
wsServer.on('connection', (ws, request) => {
console.log('[TEST SERVER] WebSocket connection established:', {
url: request.url,
headers: {
host: request.headers.host,
upgrade: request.headers.upgrade,
connection: request.headers.connection,
'sec-websocket-key': request.headers['sec-websocket-key'],
'sec-websocket-version': request.headers['sec-websocket-version'],
'sec-websocket-protocol': request.headers['sec-websocket-protocol'],
},
});
// Set up connection timeout
const connectionTimeout = setTimeout(() => {
console.error('[TEST SERVER] WebSocket connection timed out');
ws.terminate();
}, 5000);
// Clear timeout when connection is properly closed
const clearConnectionTimeout = () => {
clearTimeout(connectionTimeout);
};
ws.on('message', (message) => {
const msg = message.toString();
console.log('[TEST SERVER] Received message:', msg);
try {
const response = `Echo: ${msg}`;
console.log('[TEST SERVER] Sending response:', response);
ws.send(response);
// Clear timeout on successful message exchange
clearConnectionTimeout();
} catch (error) {
console.error('[TEST SERVER] Error sending message:', error);
}
});
ws.on('error', (error) => {
console.error('[TEST SERVER] WebSocket error:', error);
clearConnectionTimeout();
});
ws.on('close', (code, reason) => {
console.log('[TEST SERVER] WebSocket connection closed:', {
code,
reason: reason.toString(),
wasClean: code === 1000 || code === 1001,
});
clearConnectionTimeout();
});
ws.on('ping', (data) => {
try {
console.log('[TEST SERVER] Received ping, sending pong');
ws.pong(data);
} catch (error) {
console.error('[TEST SERVER] Error sending pong:', error);
}
});
ws.on('pong', (data) => {
console.log('[TEST SERVER] Received pong');
});
});
wsServer.on('error', (error) => {
console.error('Test server: WebSocket server error:', error);
});
wsServer.on('headers', (headers) => {
console.log('Test server: WebSocket headers:', headers);
});
wsServer.on('close', () => {
console.log('Test server: WebSocket server closed');
});
await new Promise<void>((resolve) => testServer.listen(3000, resolve));
console.log('Test server listening on port 3000');
});
tap.test('should start the testproxy', async () => {
tap.test('should create proxy instance', async () => {
// Test with the original minimal options (only port)
testProxy = new smartproxy.NetworkProxy({
port: 3001,
});
expect(testProxy).toEqual(testProxy); // Instance equality check
});
tap.test('should create proxy instance with extended options', async () => {
// Test with extended options to verify backward compatibility
testProxy = new smartproxy.NetworkProxy({
port: 3001,
maxConnections: 5000,
keepAliveTimeout: 120000,
headersTimeout: 60000,
logLevel: 'info',
cors: {
allowOrigin: '*',
allowMethods: 'GET, POST, OPTIONS',
allowHeaders: 'Content-Type',
maxAge: 3600
}
});
expect(testProxy).toEqual(testProxy); // Instance equality check
expect(testProxy.options.port).toEqual(3001);
});
tap.test('should start the proxy server', async () => {
// Ensure any previous server is closed
if (testProxy && testProxy.httpsServer) {
await new Promise<void>((resolve) =>
testProxy.httpsServer.close(() => resolve())
);
}
console.log('[TEST] Starting the proxy server');
await testProxy.start();
});
console.log('[TEST] Proxy server started');
tap.test('should supply reverse proxy config', async () => {
testProxy.updateProxyConfigs([
// Configure proxy with test certificates
// Awaiting the update ensures that the SNI context is added before any requests come in.
await testProxy.updateProxyConfigs([
{
destinationIp: '127.0.0.1',
destinationPort: '3000',
destinationIps: ['127.0.0.1'],
destinationPorts: [3000],
hostName: 'push.rocks',
privateKey: `-----BEGIN PRIVATE KEY-----
MIIJRQIBADANBgkqhkiG9w0BAQEFAASCCS8wggkrAgEAAoICAQDi2F/0kQr96mhe
3yEWvy2mRHOZoSSBtIqg6Bre4ZcMu901/cHNIjFnynNGFl9Se61yZbW2F3PfCt7+
kQlHug1Cx+LFssvz+hLlB5cqJQZfRKx92DhbROygtxG9r7UBmx/fwx+JQ+HOHX9R
b+szLBZqxrNDBFl2SRdviconYgVnHbaqcAPj/lK6D6x94qgUEX+vMjbIruuiCe3u
RbYse/quzAednVnY/+BuGVn8SEb2EVVFnBEsOxxYpy5ZzGR48O3YnWkM2oPpJhrp
mMYLcARMnDmIQDVstD1i+MM2lVhx/pm9xKKUgWNJC7lyz2xRscZ4pOtLkfN94leH
U98nIvxfQe7tQFKN9K52yjdtoT0UaIEUFbZyddkoNka1Xx6r+rE96046BLT2lVs0
/rnTxZUFH6vP3z9UNktmpxtnZSk67Pj6QAqZtgT0amXEpBlk7vBYSjHsyJ3+5R1y
oSjhAqeejq6M67NDOflrag5LSTkeTe4dqk0laVb1gjcse18AOlgf7pw5H79zclYH
NAnoAPua683MD2ZZd4eovEww/imSZvui3NlisSSh1SomABDFxiEaHpewI98n8P1E
3vfg4lyCV5VcUjwrPjnkfEJbX1c1/PXqTtPqSqFn/pI4FuTES6qDugS2EA/XT1ln
ODHigOiFCzDbhOMuQjhI8hzuevrRRQIDAQABAoICAQC7nU+HW6qmpQebZ5nbUVT1
Deo6Js+lwudg+3a13ghqzLnBXNW7zkrkV8mNLxW5h3bFhZ+LMcxwrXIPQ29Udmlf
USiacC1E5RBZgjSg86xYgNjU4E6EFfZLWf3/T2I6KM1s6NmdUppgOX9CoHj7grwr
pZk/lUpUjVEnu+OJPQXQ6f9Y6XoeSAqtvibgmuR+bJaZFMPAqQNTqjix99Aa7JNB
nJez4R8dXUuGY8tL349pFp7bCqAdX+oq3GJ2fJigekuM+2uV6OhunUhm6Sbq8MNt
hUwEB27oMA4RXENAUraq2XLYQ9hfUMAH+v1vGmSxEIJg561/e//RnrDbyR9oJARr
SbopI3Ut5yKxVKMYOTSqcFQXVLszTExhMhQCRoOh58BpIfhb9FLCKD9LH8E6eoQf
ygPWryey9AAJ7B2PQXVbitzcOML27rzC4DXS+mLe6AVL6t2IldaeMTlumlnc620d
Yuf5wSe8qe4xpKOlrE9emnBmbL0sGivsU+mpz9oSjxEpHGA7eoTIOmQiZnuzpkmi
1ZSU4OwqNavphy6cklONShQOmE8LMI0wRbunLjIFY8fme/8u+tVvWrTuJiCGPnXQ
F2lb0qwtDVRlexyM+GTPYstU5v7HxkQB3B+uwTgYuupCmTNmO8hjSCS/EYpHzmFe
YHDEN+Cj8f+vmKxN0F/6QQKCAQEA9+wTQU2GSoVX8IB0U6T+hX0BFhQq5ISH/s76
kWIEunY1MCkRL9YygvHkKW3dsXVOzsip/axiT36MhRcyZ27hF1tz3j//Z11E3Bfq
PkzyUVuU3jpWZkBE2VhXpDXlyW8xR/y1ZOaZZ//XcZTrZf57pGKFp30H/PlDPH3C
YtjEuQNmPCgnfz8iXx+vDYx8hwLHNv+DoX2WYuThUnul/QGSKL3xh3qWd8rotnUB
c8bV4ymk35fVJu/+pTZpPnMkYrFReso/uNn07y1iga/9mwkUBNrT+fWE7RzjT7H8
ykMMOGCK6bc7joCvALZaUDne714hNW3s9a7L1clehUA8/xwplQKCAQEA6jx/CIQd
RVdJFihSSZbqdrOAblVdl+WkjhALWNRMoRCCRniNubbgxgKfQ0scKUeubYxScBVk
rlUMl6/2Gr9uzuSC0WPVAE6OLvLNcQafw1mQ1UTJiEzYvczJKwipzXcgGQWO9Q9a
T3ETh6Be62si2r6fH4agQzbp4HkTEoWgPu6MJpqqcLoc8laty0d1huqU9du1TRzT
3etjopWRd0I3ID+WkkGKjYWRQ1bkKjvkkj1v7bHenX17nfIp5WU1aXTMYUCMMszm
pgVBDeJGKpPpP3scl7go5Y4KC6H+IeYaeCEk3hWW4robpHBzupkgpRLzmBopjRlN
v3+HQ7OkviX88QKCAQEAg5IJdfKKfindzYieM3WwjW8VkH4LdVLQSW3WlCkMkVgC
ShjBQj3OeKeeik4ABRlYRW1AqZs+YSmrsUXqPfIeCqNCDoSwKk7ZKGSYr49uWbbc
fkM/buxUnXPAryjbVddos+ds7KtkZkjkMSby9iHjxA11GLnF737pK8Uh0Atx+y3O
p8Y3j9QVjZ3m7K3NuGjFCG75kE5x7PHCkl+Ea4zV4EFNWLS5/cD1Vz8pEiRHhlKn
aPHO8OcUoOELYVUBzk6EC0IiJxukXPoc+O5JDGn48cqgDFs7vApEqBqxKTYD2jeC
AR54wNuSBDLCIylTIn016oD37DpjeoVvYBADTu/HMQKCAQEA1rFuajrVrWnMpo98
pNC7xOLQM9DwwToOMtwH2np0ZiiAj+ENXgx+R1+95Gsiu79k5Cn6oZsqNhPkP+Bb
fba69M1EDnInmGloLyYDIbbFlsMwWhn7cn+lJYpfVJ9TK+0lMWoD1yAkUa4+DVDz
z2naf466wKWfnRvnEAVJcu+hqizxrqySzlH4GDNUhn7P/UJkGFkx+yUSGFUZdLsM
orfBWUCPXSzPttmXBJbO+Nr+rP+86KvgdI/AT0vYFNdINomEjxsfpaxjOAaW0wfz
8jCyWKoZ0gJNEeK32GO5UA7dcgBHD3vQWa3lijo8COsznboaJe7M6PQpa/2S2H3+
4P5msQKCAQEAx7NP3y+5ttfTd/eQ7/cg1/0y2WxvpOYNLt6MWz4rPWyD6QwidzTG
pjuQFQ5Ods+BwJ/Jbirb7l4GMAxfIbEPAkPTHpvswO0xcncSYxl0sSP/WIA6sbcM
dp7B/scdORC8Y6i8oPdCyxyCTd2SBrmGr2krAXmQquT72eusyP5E8HFhCy1iYt22
aL68dZLv9/sRAF08t9Wy+eYjD/hCj67t7uGCZQT8wJbKr8aJcjwVwJgghh+3EydK
h+7fBVO49PLL0NWy+8GT8y7a04calFfLvZEA2UMaunBis3dE1KMFfJL/0JO+sKnF
2TkK01XDDJURK5Lhuvc7WrK2rSJ/fK+0GA==
-----END PRIVATE KEY-----
`,
publicKey: `-----BEGIN CERTIFICATE-----
MIIEljCCAn4CCQDY+ZbC9FASVjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJE
RTAeFw0xOTA5MjAxNjAxNDRaFw0yMDA5MTkxNjAxNDRaMA0xCzAJBgNVBAYTAkRF
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4thf9JEK/epoXt8hFr8t
pkRzmaEkgbSKoOga3uGXDLvdNf3BzSIxZ8pzRhZfUnutcmW1thdz3wre/pEJR7oN
QsfixbLL8/oS5QeXKiUGX0Ssfdg4W0TsoLcRva+1AZsf38MfiUPhzh1/UW/rMywW
asazQwRZdkkXb4nKJ2IFZx22qnAD4/5Sug+sfeKoFBF/rzI2yK7rognt7kW2LHv6
rswHnZ1Z2P/gbhlZ/EhG9hFVRZwRLDscWKcuWcxkePDt2J1pDNqD6SYa6ZjGC3AE
TJw5iEA1bLQ9YvjDNpVYcf6ZvcSilIFjSQu5cs9sUbHGeKTrS5HzfeJXh1PfJyL8
X0Hu7UBSjfSudso3baE9FGiBFBW2cnXZKDZGtV8eq/qxPetOOgS09pVbNP6508WV
BR+rz98/VDZLZqcbZ2UpOuz4+kAKmbYE9GplxKQZZO7wWEox7Mid/uUdcqEo4QKn
no6ujOuzQzn5a2oOS0k5Hk3uHapNJWlW9YI3LHtfADpYH+6cOR+/c3JWBzQJ6AD7
muvNzA9mWXeHqLxMMP4pkmb7otzZYrEkodUqJgAQxcYhGh6XsCPfJ/D9RN734OJc
gleVXFI8Kz455HxCW19XNfz16k7T6kqhZ/6SOBbkxEuqg7oEthAP109ZZzgx4oDo
hQsw24TjLkI4SPIc7nr60UUCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAu0+zrg0C
mlSv4Yi24OwB7TBvx+WHesl1IilCUdTiiUMo3NumvsU9Dr3Jkd0jGqYI0eyH4gIt
KrhAveXfEw7tAOEHiYicmAdIFtyzh++ZWb8mgbBeqij1MP/76Jv+cc0lUqpfRo/A
qytAsPAILuyL1o1jh28JHcq+v+WYn/FEhjUlH6emhGKGlsAjhUPjzK8MEshNolhj
t2UXw9WB5B2xWvrqlNMy0F3NAZBkZ/+k21HZo6FmVi+q6OEGcOo7wJt6wrH/lko9
LxX96GC1JoN1Pfr2FoTKy1WHzrSfyGmDIUCrbaYQ58UuMOR+5eIPPdkf/030u5eX
xXhF2fBujD57E2zQGh/l2MrOjamcSo0+wYhOqlX3WNdaKNAzPqloBnF6w7eqLYde
h9He39ySmxjENwv3miOjEP1sBeMBSRfL/ckEonfK5uJgYA5nVMQ3ojUeDMZzLfFE
Ue2WHt+uPyYk7mMZfOrK2uHzI2/Coqj7lbfRodFwj+fCArYBck2NZannDPKA6X8V
TzJTbTCteOUUJTrcfZ0gGhGkF4nYLmX5OI+TPqrDJf0fZ+mzAEHzDDVXcBYpYRDr
r8d9QwrK+WaqVi2ofbMfMByVF72jgeJNa4nxwT9bVbu/Q1T2Lt+YPb4pQ7yCoUgS
JNj2Dr5H0XoLFFnvuvzcRbhlJ9J67JzR+7g=
-----END CERTIFICATE-----
`,
publicKey: testCertificates.publicKey,
privateKey: testCertificates.privateKey,
},
]);
console.log('[TEST] Proxy configuration updated');
});
tap.test('should wait for 60 seconds', async (tools) => {
await tools.delayFor(10000);
tap.test('should route HTTPS requests based on host header', async () => {
// IMPORTANT: Connect to localhost (where the proxy is listening) but use the Host header "push.rocks"
const response = await makeHttpsRequest({
hostname: 'localhost', // changed from 'push.rocks' to 'localhost'
port: 3001,
path: '/',
method: 'GET',
headers: {
host: 'push.rocks', // virtual host for routing
},
rejectUnauthorized: false,
});
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual('Hello from test server!');
});
tap.test('should close the testproxy', async () => {
tap.test('should handle unknown host headers', async () => {
// Connect to localhost but use an unknown host header.
const response = await makeHttpsRequest({
hostname: 'localhost', // connecting to localhost
port: 3001,
path: '/',
method: 'GET',
headers: {
host: 'unknown.host', // this should not match any proxy config
},
rejectUnauthorized: false,
});
// Expect a 404 response with the appropriate error message.
expect(response.statusCode).toEqual(404);
});
tap.test('should support WebSocket connections', async () => {
console.log('\n[TEST] ====== WebSocket Test Started ======');
console.log('[TEST] Test server port:', 3000);
console.log('[TEST] Proxy server port:', 3001);
console.log('\n[TEST] Starting WebSocket test');
// Reconfigure proxy with test certificates if necessary
await testProxy.updateProxyConfigs([
{
destinationIps: ['127.0.0.1'],
destinationPorts: [3000],
hostName: 'push.rocks',
publicKey: testCertificates.publicKey,
privateKey: testCertificates.privateKey,
},
]);
return new Promise<void>((resolve, reject) => {
console.log('[TEST] Creating WebSocket client');
// IMPORTANT: Connect to localhost but specify the SNI servername and Host header as "push.rocks"
const wsUrl = 'wss://localhost:3001'; // changed from 'wss://push.rocks:3001'
console.log('[TEST] Creating WebSocket connection to:', wsUrl);
const ws = new WebSocket(wsUrl, {
rejectUnauthorized: false, // Accept self-signed certificates
handshakeTimeout: 5000,
perMessageDeflate: false,
headers: {
Host: 'push.rocks', // required for SNI and routing on the proxy
Connection: 'Upgrade',
Upgrade: 'websocket',
'Sec-WebSocket-Version': '13',
},
protocol: 'echo-protocol',
agent: new https.Agent({
rejectUnauthorized: false, // Also needed for the underlying HTTPS connection
}),
});
console.log('[TEST] WebSocket client created');
let resolved = false;
const cleanup = () => {
if (!resolved) {
resolved = true;
try {
console.log('[TEST] Cleaning up WebSocket connection');
ws.close();
resolve();
} catch (error) {
console.error('[TEST] Error during cleanup:', error);
reject(error);
}
}
};
const timeout = setTimeout(() => {
console.error('[TEST] WebSocket test timed out');
cleanup();
reject(new Error('WebSocket test timed out after 5 seconds'));
}, 5000);
// Connection establishment events
ws.on('upgrade', (response) => {
console.log('[TEST] WebSocket upgrade response received:', {
headers: response.headers,
statusCode: response.statusCode,
});
});
ws.on('open', () => {
console.log('[TEST] WebSocket connection opened');
try {
console.log('[TEST] Sending test message');
ws.send('Hello WebSocket');
} catch (error) {
console.error('[TEST] Error sending message:', error);
cleanup();
reject(error);
}
});
ws.on('message', (message) => {
console.log('[TEST] Received message:', message.toString());
if (
message.toString() === 'Hello WebSocket' ||
message.toString() === 'Echo: Hello WebSocket'
) {
console.log('[TEST] Message received correctly');
clearTimeout(timeout);
cleanup();
}
});
ws.on('error', (error) => {
console.error('[TEST] WebSocket error:', error);
cleanup();
reject(error);
});
ws.on('close', (code, reason) => {
console.log('[TEST] WebSocket connection closed:', {
code,
reason: reason.toString(),
});
cleanup();
});
});
});
tap.test('should handle custom headers', async () => {
await testProxy.addDefaultHeaders({
'X-Proxy-Header': 'test-value',
});
const response = await makeHttpsRequest({
hostname: 'localhost', // changed to 'localhost'
port: 3001,
path: '/',
method: 'GET',
headers: {
host: 'push.rocks', // still routing to push.rocks
},
rejectUnauthorized: false,
});
expect(response.headers['x-proxy-header']).toEqual('test-value');
});
tap.test('should handle CORS preflight requests', async () => {
// Instead of creating a new proxy instance, let's update the options on the current one
// First ensure the existing proxy is working correctly
const initialResponse = await makeHttpsRequest({
hostname: 'localhost',
port: 3001,
path: '/',
method: 'GET',
headers: { host: 'push.rocks' },
rejectUnauthorized: false,
});
expect(initialResponse.statusCode).toEqual(200);
// Add CORS headers to the existing proxy
await testProxy.addDefaultHeaders({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
});
// Allow server to process the header changes
await new Promise(resolve => setTimeout(resolve, 100));
// Send OPTIONS request to simulate CORS preflight
const response = await makeHttpsRequest({
hostname: 'localhost',
port: 3001,
path: '/',
method: 'OPTIONS',
headers: {
host: 'push.rocks',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'Content-Type',
'Origin': 'https://example.com'
},
rejectUnauthorized: false,
});
// Verify the response has expected status code
expect(response.statusCode).toEqual(204);
});
tap.test('should track connections and metrics', async () => {
// Instead of creating a new proxy instance, let's just make requests to the existing one
// and verify the metrics are being tracked
// Get initial metrics counts
const initialRequestsServed = testProxy.requestsServed || 0;
// Make a few requests to ensure we have metrics to check
for (let i = 0; i < 3; i++) {
await makeHttpsRequest({
hostname: 'localhost',
port: 3001,
path: '/metrics-test-' + i,
method: 'GET',
headers: { host: 'push.rocks' },
rejectUnauthorized: false,
});
}
// Wait a bit to let metrics update
await new Promise(resolve => setTimeout(resolve, 100));
// Verify metrics tracking is working - should have at least 3 more requests than before
expect(testProxy.connectedClients).toBeDefined();
expect(typeof testProxy.requestsServed).toEqual('number');
expect(testProxy.requestsServed).toBeGreaterThan(initialRequestsServed + 2);
});
tap.test('cleanup', async () => {
console.log('[TEST] Starting cleanup');
// Clean up all servers
console.log('[TEST] Terminating WebSocket clients');
wsServer.clients.forEach((client) => {
client.terminate();
});
console.log('[TEST] Closing WebSocket server');
await new Promise<void>((resolve) =>
wsServer.close(() => {
console.log('[TEST] WebSocket server closed');
resolve();
})
);
console.log('[TEST] Closing test server');
await new Promise<void>((resolve) =>
testServer.close(() => {
console.log('[TEST] Test server closed');
resolve();
})
);
console.log('[TEST] Stopping proxy');
await testProxy.stop();
console.log('[TEST] Cleanup complete');
});
tap.start();
process.on('exit', () => {
console.log('[TEST] Shutting down test server');
testServer.close(() => console.log('[TEST] Test server shut down'));
wsServer.close(() => console.log('[TEST] WebSocket server shut down'));
testProxy.stop().then(() => console.log('[TEST] Proxy server stopped'));
});
tap.start();

View File

@ -1,8 +1,8 @@
/**
* autocreated commitinfo by @pushrocks/commitinfo
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@pushrocks/smartproxy',
version: '3.0.2',
description: 'a proxy for handling high workloads of proxying'
name: '@push.rocks/smartproxy',
version: '4.2.2',
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.'
}

901
ts/classes.iptablesproxy.ts Normal file
View File

@ -0,0 +1,901 @@
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Represents a port range for forwarding
*/
export interface IPortRange {
from: number;
to: number;
}
/**
* Settings for IPTablesProxy.
*/
export interface IIpTableProxySettings {
// Basic settings
fromPort: number | IPortRange | Array<number | IPortRange>; // Support single port, port range, or multiple ports/ranges
toPort: number | IPortRange | Array<number | IPortRange>;
toHost?: string; // Target host for proxying; defaults to 'localhost'
// Advanced settings
preserveSourceIP?: boolean; // If true, the original source IP is preserved
deleteOnExit?: boolean; // If true, clean up marked iptables rules before process exit
protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp'
enableLogging?: boolean; // Enable detailed logging
ipv6Support?: boolean; // Enable IPv6 support (ip6tables)
// Source filtering
allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
bannedSourceIPs?: string[]; // If provided, these IPs are blocked
// Rule management
forceCleanSlate?: boolean; // Clear all IPTablesProxy rules before starting
addJumpRule?: boolean; // Add a custom chain for cleaner rule management
checkExistingRules?: boolean; // Check if rules already exist before adding
// Integration with PortProxy/NetworkProxy
netProxyIntegration?: {
enabled: boolean;
redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy
sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination
};
}
/**
* Represents a rule added to iptables
*/
interface IpTablesRule {
table: string;
chain: string;
command: string;
tag: string;
added: boolean;
}
/**
* IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
* Enhanced with multi-port support, IPv6, and integration with PortProxy/NetworkProxy.
*/
export class IPTablesProxy {
public settings: IIpTableProxySettings;
private rules: IpTablesRule[] = [];
private ruleTag: string;
private customChain: string | null = null;
constructor(settings: IIpTableProxySettings) {
// Validate inputs to prevent command injection
this.validateSettings(settings);
// Set default settings
this.settings = {
...settings,
toHost: settings.toHost || 'localhost',
protocol: settings.protocol || 'tcp',
enableLogging: settings.enableLogging !== undefined ? settings.enableLogging : false,
ipv6Support: settings.ipv6Support !== undefined ? settings.ipv6Support : false,
checkExistingRules: settings.checkExistingRules !== undefined ? settings.checkExistingRules : true,
netProxyIntegration: settings.netProxyIntegration || { enabled: false }
};
// Generate a unique identifier for the rules added by this instance
this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
if (this.settings.addJumpRule) {
this.customChain = `IPTablesProxy_${Math.random().toString(36).substr(2, 5)}`;
}
// Register cleanup handlers if deleteOnExit is true
if (this.settings.deleteOnExit) {
const cleanup = () => {
try {
this.stopSync();
} catch (err) {
console.error('Error cleaning iptables rules on exit:', err);
}
};
process.on('exit', cleanup);
process.on('SIGINT', () => {
cleanup();
process.exit();
});
process.on('SIGTERM', () => {
cleanup();
process.exit();
});
}
}
/**
* Validates settings to prevent command injection and ensure valid values
*/
private validateSettings(settings: IIpTableProxySettings): void {
// Validate port numbers
const validatePorts = (port: number | IPortRange | Array<number | IPortRange>) => {
if (Array.isArray(port)) {
port.forEach(p => validatePorts(p));
return;
}
if (typeof port === 'number') {
if (port < 1 || port > 65535) {
throw new Error(`Invalid port number: ${port}`);
}
} else if (typeof port === 'object') {
if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) {
throw new Error(`Invalid port range: ${port.from}-${port.to}`);
}
}
};
validatePorts(settings.fromPort);
validatePorts(settings.toPort);
// Define regex patterns at the method level so they're available throughout
const ipRegex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/;
// Validate IP addresses
const validateIPs = (ips?: string[]) => {
if (!ips) return;
for (const ip of ips) {
if (!ipRegex.test(ip) && !ipv6Regex.test(ip)) {
throw new Error(`Invalid IP address format: ${ip}`);
}
}
};
validateIPs(settings.allowedSourceIPs);
validateIPs(settings.bannedSourceIPs);
// Validate toHost - only allow hostnames or IPs
if (settings.toHost) {
const hostRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
if (!hostRegex.test(settings.toHost) && !ipRegex.test(settings.toHost) && !ipv6Regex.test(settings.toHost)) {
throw new Error(`Invalid host format: ${settings.toHost}`);
}
}
}
/**
* Normalizes port specifications into an array of port ranges
*/
private normalizePortSpec(portSpec: number | IPortRange | Array<number | IPortRange>): IPortRange[] {
const result: IPortRange[] = [];
if (Array.isArray(portSpec)) {
// If it's an array, process each element
for (const spec of portSpec) {
result.push(...this.normalizePortSpec(spec));
}
} else if (typeof portSpec === 'number') {
// Single port becomes a range with the same start and end
result.push({ from: portSpec, to: portSpec });
} else {
// Already a range
result.push(portSpec);
}
return result;
}
/**
* Gets the appropriate iptables command based on settings
*/
private getIptablesCommand(isIpv6: boolean = false): string {
return isIpv6 ? 'ip6tables' : 'iptables';
}
/**
* Checks if a rule already exists in iptables
*/
private async ruleExists(table: string, command: string, isIpv6: boolean = false): Promise<boolean> {
try {
const iptablesCmd = this.getIptablesCommand(isIpv6);
const { stdout } = await execAsync(`${iptablesCmd}-save -t ${table}`);
// Convert the command to the format found in iptables-save output
// (This is a simplification - in reality, you'd need more parsing)
const rulePattern = command.replace(`${iptablesCmd} -t ${table} -A `, '-A ');
return stdout.split('\n').some(line => line.trim() === rulePattern);
} catch (err) {
this.log('error', `Failed to check if rule exists: ${err}`);
return false;
}
}
/**
* Sets up a custom chain for better rule management
*/
private async setupCustomChain(isIpv6: boolean = false): Promise<boolean> {
if (!this.customChain) return true;
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
try {
// Create the chain
await execAsync(`${iptablesCmd} -t ${table} -N ${this.customChain}`);
this.log('info', `Created custom chain: ${this.customChain}`);
// Add jump rule to PREROUTING chain
const jumpCommand = `${iptablesCmd} -t ${table} -A PREROUTING -j ${this.customChain} -m comment --comment "${this.ruleTag}:JUMP"`;
await execAsync(jumpCommand);
this.log('info', `Added jump rule to ${this.customChain}`);
// Store the jump rule
this.rules.push({
table,
chain: 'PREROUTING',
command: jumpCommand,
tag: `${this.ruleTag}:JUMP`,
added: true
});
return true;
} catch (err) {
this.log('error', `Failed to set up custom chain: ${err}`);
return false;
}
}
/**
* Add a source IP filter rule
*/
private async addSourceIPFilter(isIpv6: boolean = false): Promise<boolean> {
if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) {
return true;
}
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
const chain = this.customChain || 'PREROUTING';
try {
// Add banned IPs first (explicit deny)
if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
for (const ip of this.settings.bannedSourceIPs) {
const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -j DROP -m comment --comment "${this.ruleTag}:BANNED"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
continue;
}
await execAsync(command);
this.log('info', `Added banned IP rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:BANNED`,
added: true
});
}
}
// Add allowed IPs (explicit allow)
if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
// First add a default deny for all
const denyAllCommand = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} -j DROP -m comment --comment "${this.ruleTag}:DENY_ALL"`;
// Add allow rules for specific IPs
for (const ip of this.settings.allowedSourceIPs) {
const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -p ${this.settings.protocol} -j ACCEPT -m comment --comment "${this.ruleTag}:ALLOWED"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
continue;
}
await execAsync(command);
this.log('info', `Added allowed IP rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:ALLOWED`,
added: true
});
}
// Now add the default deny after all allows
if (this.settings.checkExistingRules && await this.ruleExists(table, denyAllCommand, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${denyAllCommand}`);
} else {
await execAsync(denyAllCommand);
this.log('info', `Added default deny rule: ${denyAllCommand}`);
this.rules.push({
table,
chain,
command: denyAllCommand,
tag: `${this.ruleTag}:DENY_ALL`,
added: true
});
}
}
return true;
} catch (err) {
this.log('error', `Failed to add source IP filter rules: ${err}`);
return false;
}
}
/**
* Adds a port forwarding rule
*/
private async addPortForwardingRule(
fromPortRange: IPortRange,
toPortRange: IPortRange,
isIpv6: boolean = false
): Promise<boolean> {
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
const chain = this.customChain || 'PREROUTING';
try {
// Handle single port case
if (fromPortRange.from === fromPortRange.to && toPortRange.from === toPortRange.to) {
// Single port forward
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
} else {
await execAsync(command);
this.log('info', `Added port forwarding rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:DNAT`,
added: true
});
}
} else if (fromPortRange.to - fromPortRange.from === toPortRange.to - toPortRange.from) {
// Port range forward with equal ranges
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from}:${fromPortRange.to} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from}-${toPortRange.to} ` +
`-m comment --comment "${this.ruleTag}:DNAT_RANGE"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
} else {
await execAsync(command);
this.log('info', `Added port range forwarding rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:DNAT_RANGE`,
added: true
});
}
} else {
// Unequal port ranges need individual rules
for (let i = 0; i <= fromPortRange.to - fromPortRange.from; i++) {
const fromPort = fromPortRange.from + i;
const toPort = toPortRange.from + i % (toPortRange.to - toPortRange.from + 1);
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT_INDIVIDUAL"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
continue;
}
await execAsync(command);
this.log('info', `Added individual port forwarding rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:DNAT_INDIVIDUAL`,
added: true
});
}
}
// If preserveSourceIP is false, add a MASQUERADE rule
if (!this.settings.preserveSourceIP) {
// For port range
const masqCommand = `${iptablesCmd} -t nat -A POSTROUTING -p ${this.settings.protocol} -d ${this.settings.toHost} ` +
`--dport ${toPortRange.from}:${toPortRange.to} -j MASQUERADE ` +
`-m comment --comment "${this.ruleTag}:MASQ"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists('nat', masqCommand, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${masqCommand}`);
} else {
await execAsync(masqCommand);
this.log('info', `Added MASQUERADE rule: ${masqCommand}`);
this.rules.push({
table: 'nat',
chain: 'POSTROUTING',
command: masqCommand,
tag: `${this.ruleTag}:MASQ`,
added: true
});
}
}
return true;
} catch (err) {
this.log('error', `Failed to add port forwarding rule: ${err}`);
// Try to roll back any rules that were already added
await this.rollbackRules();
return false;
}
}
/**
* Special handling for NetworkProxy integration
*/
private async setupNetworkProxyIntegration(isIpv6: boolean = false): Promise<boolean> {
if (!this.settings.netProxyIntegration?.enabled) {
return true;
}
const netProxyConfig = this.settings.netProxyIntegration;
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
const chain = this.customChain || 'PREROUTING';
try {
// If redirectLocalhost is true, set up special rule to redirect localhost traffic to NetworkProxy
if (netProxyConfig.redirectLocalhost && netProxyConfig.sslTerminationPort) {
const redirectCommand = `${iptablesCmd} -t ${table} -A OUTPUT -p tcp -d 127.0.0.1 -j REDIRECT ` +
`--to-port ${netProxyConfig.sslTerminationPort} ` +
`-m comment --comment "${this.ruleTag}:NETPROXY_REDIRECT"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, redirectCommand, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${redirectCommand}`);
} else {
await execAsync(redirectCommand);
this.log('info', `Added NetworkProxy redirection rule: ${redirectCommand}`);
this.rules.push({
table,
chain: 'OUTPUT',
command: redirectCommand,
tag: `${this.ruleTag}:NETPROXY_REDIRECT`,
added: true
});
}
}
return true;
} catch (err) {
this.log('error', `Failed to set up NetworkProxy integration: ${err}`);
return false;
}
}
/**
* Rolls back rules that were added in case of error
*/
private async rollbackRules(): Promise<void> {
// Process rules in reverse order (LIFO)
for (let i = this.rules.length - 1; i >= 0; i--) {
const rule = this.rules[i];
if (rule.added) {
try {
// Convert -A (add) to -D (delete)
const deleteCommand = rule.command.replace('-A', '-D');
await execAsync(deleteCommand);
this.log('info', `Rolled back rule: ${deleteCommand}`);
rule.added = false;
} catch (err) {
this.log('error', `Failed to roll back rule: ${err}`);
}
}
}
}
/**
* Sets up iptables rules for port forwarding with enhanced features
*/
public async start(): Promise<void> {
// Optionally clean the slate first
if (this.settings.forceCleanSlate) {
await IPTablesProxy.cleanSlate();
}
// First set up any custom chains
if (this.settings.addJumpRule) {
const chainSetupSuccess = await this.setupCustomChain();
if (!chainSetupSuccess) {
throw new Error('Failed to set up custom chain');
}
// For IPv6 if enabled
if (this.settings.ipv6Support) {
const chainSetupSuccessIpv6 = await this.setupCustomChain(true);
if (!chainSetupSuccessIpv6) {
this.log('warn', 'Failed to set up IPv6 custom chain, continuing with IPv4 only');
}
}
}
// Add source IP filters
await this.addSourceIPFilter();
if (this.settings.ipv6Support) {
await this.addSourceIPFilter(true);
}
// Set up NetworkProxy integration if enabled
if (this.settings.netProxyIntegration?.enabled) {
const netProxySetupSuccess = await this.setupNetworkProxyIntegration();
if (!netProxySetupSuccess) {
this.log('warn', 'Failed to set up NetworkProxy integration');
}
if (this.settings.ipv6Support) {
await this.setupNetworkProxyIntegration(true);
}
}
// Normalize port specifications
const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
const toPortRanges = this.normalizePortSpec(this.settings.toPort);
// Handle the case where fromPort and toPort counts don't match
if (fromPortRanges.length !== toPortRanges.length) {
if (toPortRanges.length === 1) {
// If there's only one toPort, use it for all fromPorts
for (const fromRange of fromPortRanges) {
await this.addPortForwardingRule(fromRange, toPortRanges[0]);
if (this.settings.ipv6Support) {
await this.addPortForwardingRule(fromRange, toPortRanges[0], true);
}
}
} else {
throw new Error('Mismatched port counts: fromPort and toPort arrays must have equal length or toPort must be a single value');
}
} else {
// Add port forwarding rules for each port specification
for (let i = 0; i < fromPortRanges.length; i++) {
await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i]);
if (this.settings.ipv6Support) {
await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i], true);
}
}
}
// Final check - ensure we have at least one rule added
if (this.rules.filter(r => r.added).length === 0) {
throw new Error('No rules were added');
}
}
/**
* Removes all added iptables rules
*/
public async stop(): Promise<void> {
// Process rules in reverse order (LIFO)
for (let i = this.rules.length - 1; i >= 0; i--) {
const rule = this.rules[i];
if (rule.added) {
try {
// Convert -A (add) to -D (delete)
const deleteCommand = rule.command.replace('-A', '-D');
await execAsync(deleteCommand);
this.log('info', `Removed rule: ${deleteCommand}`);
rule.added = false;
} catch (err) {
this.log('error', `Failed to remove rule: ${err}`);
}
}
}
// If we created a custom chain, we need to clean it up
if (this.customChain) {
try {
// First flush the chain
await execAsync(`iptables -t nat -F ${this.customChain}`);
this.log('info', `Flushed custom chain: ${this.customChain}`);
// Then delete it
await execAsync(`iptables -t nat -X ${this.customChain}`);
this.log('info', `Deleted custom chain: ${this.customChain}`);
// Same for IPv6 if enabled
if (this.settings.ipv6Support) {
try {
await execAsync(`ip6tables -t nat -F ${this.customChain}`);
await execAsync(`ip6tables -t nat -X ${this.customChain}`);
this.log('info', `Deleted IPv6 custom chain: ${this.customChain}`);
} catch (err) {
this.log('error', `Failed to delete IPv6 custom chain: ${err}`);
}
}
} catch (err) {
this.log('error', `Failed to delete custom chain: ${err}`);
}
}
// Clear rules array
this.rules = [];
}
/**
* Synchronous version of stop, for use in exit handlers
*/
public stopSync(): void {
// Process rules in reverse order (LIFO)
for (let i = this.rules.length - 1; i >= 0; i--) {
const rule = this.rules[i];
if (rule.added) {
try {
// Convert -A (add) to -D (delete)
const deleteCommand = rule.command.replace('-A', '-D');
execSync(deleteCommand);
this.log('info', `Removed rule: ${deleteCommand}`);
rule.added = false;
} catch (err) {
this.log('error', `Failed to remove rule: ${err}`);
}
}
}
// If we created a custom chain, we need to clean it up
if (this.customChain) {
try {
// First flush the chain
execSync(`iptables -t nat -F ${this.customChain}`);
// Then delete it
execSync(`iptables -t nat -X ${this.customChain}`);
this.log('info', `Deleted custom chain: ${this.customChain}`);
// Same for IPv6 if enabled
if (this.settings.ipv6Support) {
try {
execSync(`ip6tables -t nat -F ${this.customChain}`);
execSync(`ip6tables -t nat -X ${this.customChain}`);
} catch (err) {
// IPv6 failures are non-critical
}
}
} catch (err) {
this.log('error', `Failed to delete custom chain: ${err}`);
}
}
// Clear rules array
this.rules = [];
}
/**
* Asynchronously cleans up any iptables rules in the nat table that were added by this module.
* It looks for rules with comments containing "IPTablesProxy:".
*/
public static async cleanSlate(): Promise<void> {
await IPTablesProxy.cleanSlateInternal();
// Also clean IPv6 rules
await IPTablesProxy.cleanSlateInternal(true);
}
/**
* Internal implementation of cleanSlate with IPv6 support
*/
private static async cleanSlateInternal(isIpv6: boolean = false): Promise<void> {
const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
try {
const { stdout } = await execAsync(`${iptablesCmd}-save -t nat`);
const lines = stdout.split('\n');
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
// First, find and remove any custom chains
const customChains = new Set<string>();
const jumpRules: string[] = [];
for (const line of proxyLines) {
if (line.includes('IPTablesProxy:JUMP')) {
// Extract chain name from jump rule
const match = line.match(/\s+-j\s+(\S+)\s+/);
if (match && match[1].startsWith('IPTablesProxy_')) {
customChains.add(match[1]);
jumpRules.push(line);
}
}
}
// Remove jump rules first
for (const line of jumpRules) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
// Replace the "-A" with "-D" to form a deletion command
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
await execAsync(cmd);
console.log(`Cleaned up iptables jump rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
}
}
}
// Then remove all other rules
for (const line of proxyLines) {
if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
// Replace the "-A" with "-D" to form a deletion command
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
await execAsync(cmd);
console.log(`Cleaned up iptables rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables rule: ${cmd}`, err);
}
}
}
}
// Finally clean up custom chains
for (const chain of customChains) {
try {
// Flush the chain
await execAsync(`${iptablesCmd} -t nat -F ${chain}`);
console.log(`Flushed custom chain: ${chain}`);
// Delete the chain
await execAsync(`${iptablesCmd} -t nat -X ${chain}`);
console.log(`Deleted custom chain: ${chain}`);
} catch (err) {
console.error(`Failed to delete custom chain ${chain}:`, err);
}
}
} catch (err) {
console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
}
}
/**
* Synchronously cleans up any iptables rules in the nat table that were added by this module.
* It looks for rules with comments containing "IPTablesProxy:".
* This method is intended for use in process exit handlers.
*/
public static cleanSlateSync(): void {
IPTablesProxy.cleanSlateSyncInternal();
// Also clean IPv6 rules
IPTablesProxy.cleanSlateSyncInternal(true);
}
/**
* Internal implementation of cleanSlateSync with IPv6 support
*/
private static cleanSlateSyncInternal(isIpv6: boolean = false): void {
const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
try {
const stdout = execSync(`${iptablesCmd}-save -t nat`).toString();
const lines = stdout.split('\n');
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
// First, find and remove any custom chains
const customChains = new Set<string>();
const jumpRules: string[] = [];
for (const line of proxyLines) {
if (line.includes('IPTablesProxy:JUMP')) {
// Extract chain name from jump rule
const match = line.match(/\s+-j\s+(\S+)\s+/);
if (match && match[1].startsWith('IPTablesProxy_')) {
customChains.add(match[1]);
jumpRules.push(line);
}
}
}
// Remove jump rules first
for (const line of jumpRules) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
// Replace the "-A" with "-D" to form a deletion command
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
execSync(cmd);
console.log(`Cleaned up iptables jump rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
}
}
}
// Then remove all other rules
for (const line of proxyLines) {
if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
execSync(cmd);
console.log(`Cleaned up iptables rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables rule: ${cmd}`, err);
}
}
}
}
// Finally clean up custom chains
for (const chain of customChains) {
try {
// Flush the chain
execSync(`${iptablesCmd} -t nat -F ${chain}`);
// Delete the chain
execSync(`${iptablesCmd} -t nat -X ${chain}`);
console.log(`Deleted custom chain: ${chain}`);
} catch (err) {
console.error(`Failed to delete custom chain ${chain}:`, err);
}
}
} catch (err) {
console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
}
}
/**
* Logging utility that respects the enableLogging setting
*/
private log(level: 'info' | 'warn' | 'error', message: string): void {
if (!this.settings.enableLogging && level === 'info') {
return;
}
const timestamp = new Date().toISOString();
switch (level) {
case 'info':
console.log(`[${timestamp}] [INFO] ${message}`);
break;
case 'warn':
console.warn(`[${timestamp}] [WARN] ${message}`);
break;
case 'error':
console.error(`[${timestamp}] [ERROR] ${message}`);
break;
}
}
}

1709
ts/classes.networkproxy.ts Normal file

File diff suppressed because it is too large Load Diff

559
ts/classes.port80handler.ts Normal file
View File

@ -0,0 +1,559 @@
import * as plugins from './plugins.js';
/**
* Represents a domain certificate with various status information
*/
interface IDomainCertificate {
certObtained: boolean;
obtainingInProgress: boolean;
certificate?: string;
privateKey?: string;
challengeToken?: string;
challengeKeyAuthorization?: string;
expiryDate?: Date;
lastRenewalAttempt?: Date;
}
/**
* Configuration options for the ACME Certificate Manager
*/
interface IAcmeCertManagerOptions {
port?: number;
contactEmail?: string;
useProduction?: boolean;
renewThresholdDays?: number;
httpsRedirectPort?: number;
renewCheckIntervalHours?: number;
}
/**
* Certificate data that can be emitted via events or set from outside
*/
interface ICertificateData {
domain: string;
certificate: string;
privateKey: string;
expiryDate: Date;
}
/**
* Events emitted by the ACME Certificate Manager
*/
export enum CertManagerEvents {
CERTIFICATE_ISSUED = 'certificate-issued',
CERTIFICATE_RENEWED = 'certificate-renewed',
CERTIFICATE_FAILED = 'certificate-failed',
CERTIFICATE_EXPIRING = 'certificate-expiring',
MANAGER_STARTED = 'manager-started',
MANAGER_STOPPED = 'manager-stopped',
}
/**
* Improved ACME Certificate Manager with event emission and external certificate management
*/
export class AcmeCertManager extends plugins.EventEmitter {
private domainCertificates: Map<string, IDomainCertificate>;
private server: plugins.http.Server | null = null;
private acmeClient: plugins.acme.Client | null = null;
private accountKey: string | null = null;
private renewalTimer: NodeJS.Timeout | null = null;
private isShuttingDown: boolean = false;
private options: Required<IAcmeCertManagerOptions>;
/**
* Creates a new ACME Certificate Manager
* @param options Configuration options
*/
constructor(options: IAcmeCertManagerOptions = {}) {
super();
this.domainCertificates = new Map<string, IDomainCertificate>();
// Default options
this.options = {
port: options.port ?? 80,
contactEmail: options.contactEmail ?? 'admin@example.com',
useProduction: options.useProduction ?? false, // Safer default: staging
renewThresholdDays: options.renewThresholdDays ?? 30,
httpsRedirectPort: options.httpsRedirectPort ?? 443,
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
};
}
/**
* Starts the HTTP server for ACME challenges
*/
public async start(): Promise<void> {
if (this.server) {
throw new Error('Server is already running');
}
if (this.isShuttingDown) {
throw new Error('Server is shutting down');
}
return new Promise((resolve, reject) => {
try {
this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
this.server.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EACCES') {
reject(new Error(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`));
} else if (error.code === 'EADDRINUSE') {
reject(new Error(`Port ${this.options.port} is already in use.`));
} else {
reject(error);
}
});
this.server.listen(this.options.port, () => {
console.log(`AcmeCertManager is listening on port ${this.options.port}`);
this.startRenewalTimer();
this.emit(CertManagerEvents.MANAGER_STARTED, this.options.port);
resolve();
});
} catch (error) {
reject(error);
}
});
}
/**
* Stops the HTTP server and renewal timer
*/
public async stop(): Promise<void> {
if (!this.server) {
return;
}
this.isShuttingDown = true;
// Stop the renewal timer
if (this.renewalTimer) {
clearInterval(this.renewalTimer);
this.renewalTimer = null;
}
return new Promise<void>((resolve) => {
if (this.server) {
this.server.close(() => {
this.server = null;
this.isShuttingDown = false;
this.emit(CertManagerEvents.MANAGER_STOPPED);
resolve();
});
} else {
this.isShuttingDown = false;
resolve();
}
});
}
/**
* Adds a domain to be managed for certificates
* @param domain The domain to add
*/
public addDomain(domain: string): void {
if (!this.domainCertificates.has(domain)) {
this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false });
console.log(`Domain added: ${domain}`);
}
}
/**
* Removes a domain from management
* @param domain The domain to remove
*/
public removeDomain(domain: string): void {
if (this.domainCertificates.delete(domain)) {
console.log(`Domain removed: ${domain}`);
}
}
/**
* Sets a certificate for a domain directly (for externally obtained certificates)
* @param domain The domain for the certificate
* @param certificate The certificate (PEM format)
* @param privateKey The private key (PEM format)
* @param expiryDate Optional expiry date
*/
public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
let domainInfo = this.domainCertificates.get(domain);
if (!domainInfo) {
domainInfo = { certObtained: false, obtainingInProgress: false };
this.domainCertificates.set(domain, domainInfo);
}
domainInfo.certificate = certificate;
domainInfo.privateKey = privateKey;
domainInfo.certObtained = true;
domainInfo.obtainingInProgress = false;
if (expiryDate) {
domainInfo.expiryDate = expiryDate;
} else {
// Try to extract expiry date from certificate
try {
// This is a simplistic approach - in a real implementation, use a proper
// certificate parsing library like node-forge or x509
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
if (matches && matches[1]) {
domainInfo.expiryDate = new Date(matches[1]);
}
} catch (error) {
console.warn(`Failed to extract expiry date from certificate for ${domain}`);
}
}
console.log(`Certificate set for ${domain}`);
// Emit certificate event
this.emitCertificateEvent(CertManagerEvents.CERTIFICATE_ISSUED, {
domain,
certificate,
privateKey,
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
});
}
/**
* Gets the certificate for a domain if it exists
* @param domain The domain to get the certificate for
*/
public getCertificate(domain: string): ICertificateData | null {
const domainInfo = this.domainCertificates.get(domain);
if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) {
return null;
}
return {
domain,
certificate: domainInfo.certificate,
privateKey: domainInfo.privateKey,
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
};
}
/**
* Lazy initialization of the ACME client
* @returns An ACME client instance
*/
private async getAcmeClient(): Promise<plugins.acme.Client> {
if (this.acmeClient) {
return this.acmeClient;
}
// Generate a new account key
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
this.acmeClient = new plugins.acme.Client({
directoryUrl: this.options.useProduction
? plugins.acme.directory.letsencrypt.production
: plugins.acme.directory.letsencrypt.staging,
accountKey: this.accountKey,
});
// Create a new account
await this.acmeClient.createAccount({
termsOfServiceAgreed: true,
contact: [`mailto:${this.options.contactEmail}`],
});
return this.acmeClient;
}
/**
* Handles incoming HTTP requests
* @param req The HTTP request
* @param res The HTTP response
*/
private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
const hostHeader = req.headers.host;
if (!hostHeader) {
res.statusCode = 400;
res.end('Bad Request: Host header is missing');
return;
}
// Extract domain (ignoring any port in the Host header)
const domain = hostHeader.split(':')[0];
// If the request is for an ACME HTTP-01 challenge, handle it
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
this.handleAcmeChallenge(req, res, domain);
return;
}
if (!this.domainCertificates.has(domain)) {
res.statusCode = 404;
res.end('Domain not configured');
return;
}
const domainInfo = this.domainCertificates.get(domain)!;
// If certificate exists, redirect to HTTPS
if (domainInfo.certObtained) {
const httpsPort = this.options.httpsRedirectPort;
const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
res.statusCode = 301;
res.setHeader('Location', redirectUrl);
res.end(`Redirecting to ${redirectUrl}`);
} else {
// Trigger certificate issuance if not already running
if (!domainInfo.obtainingInProgress) {
this.obtainCertificate(domain).catch(err => {
this.emit(CertManagerEvents.CERTIFICATE_FAILED, { domain, error: err.message });
console.error(`Error obtaining certificate for ${domain}:`, err);
});
}
res.statusCode = 503;
res.end('Certificate issuance in progress, please try again later.');
}
}
/**
* Serves the ACME HTTP-01 challenge response
* @param req The HTTP request
* @param res The HTTP response
* @param domain The domain for the challenge
*/
private handleAcmeChallenge(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, domain: string): void {
const domainInfo = this.domainCertificates.get(domain);
if (!domainInfo) {
res.statusCode = 404;
res.end('Domain not configured');
return;
}
// The token is the last part of the URL
const urlParts = req.url?.split('/');
const token = urlParts ? urlParts[urlParts.length - 1] : '';
if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(domainInfo.challengeKeyAuthorization);
console.log(`Served ACME challenge response for ${domain}`);
} else {
res.statusCode = 404;
res.end('Challenge token not found');
}
}
/**
* Obtains a certificate for a domain using ACME HTTP-01 challenge
* @param domain The domain to obtain a certificate for
* @param isRenewal Whether this is a renewal attempt
*/
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
// Get the domain info
const domainInfo = this.domainCertificates.get(domain);
if (!domainInfo) {
throw new Error(`Domain not found: ${domain}`);
}
// Prevent concurrent certificate issuance
if (domainInfo.obtainingInProgress) {
console.log(`Certificate issuance already in progress for ${domain}`);
return;
}
domainInfo.obtainingInProgress = true;
domainInfo.lastRenewalAttempt = new Date();
try {
const client = await this.getAcmeClient();
// Create a new order for the domain
const order = await client.createOrder({
identifiers: [{ type: 'dns', value: domain }],
});
// Get the authorizations for the order
const authorizations = await client.getAuthorizations(order);
for (const authz of authorizations) {
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
if (!challenge) {
throw new Error('HTTP-01 challenge not found');
}
// Get the key authorization for the challenge
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
// Store the challenge data
domainInfo.challengeToken = challenge.token;
domainInfo.challengeKeyAuthorization = keyAuthorization;
// ACME client type definition workaround - use compatible approach
// First check if challenge verification is needed
const authzUrl = authz.url;
try {
// Check if authzUrl exists and perform verification
if (authzUrl) {
await client.verifyChallenge(authz, challenge);
}
// Complete the challenge
await client.completeChallenge(challenge);
// Wait for validation
await client.waitForValidStatus(challenge);
console.log(`HTTP-01 challenge completed for ${domain}`);
} catch (error) {
console.error(`Challenge error for ${domain}:`, error);
throw error;
}
}
// Generate a CSR and private key
const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
commonName: domain,
});
const csr = csrBuffer.toString();
const privateKey = privateKeyBuffer.toString();
// Finalize the order with our CSR
await client.finalizeOrder(order, csr);
// Get the certificate with the full chain
const certificate = await client.getCertificate(order);
// Store the certificate and key
domainInfo.certificate = certificate;
domainInfo.privateKey = privateKey;
domainInfo.certObtained = true;
// Clear challenge data
delete domainInfo.challengeToken;
delete domainInfo.challengeKeyAuthorization;
// Extract expiry date from certificate
try {
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
if (matches && matches[1]) {
domainInfo.expiryDate = new Date(matches[1]);
console.log(`Certificate for ${domain} will expire on ${domainInfo.expiryDate.toISOString()}`);
}
} catch (error) {
console.warn(`Failed to extract expiry date from certificate for ${domain}`);
}
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
// Emit the appropriate event
const eventType = isRenewal
? CertManagerEvents.CERTIFICATE_RENEWED
: CertManagerEvents.CERTIFICATE_ISSUED;
this.emitCertificateEvent(eventType, {
domain,
certificate,
privateKey,
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
});
} catch (error: any) {
// Check for rate limit errors
if (error.message && (
error.message.includes('rateLimited') ||
error.message.includes('too many certificates') ||
error.message.includes('rate limit')
)) {
console.error(`Rate limit reached for ${domain}. Waiting before retry.`);
} else {
console.error(`Error during certificate issuance for ${domain}:`, error);
}
// Emit failure event
this.emit(CertManagerEvents.CERTIFICATE_FAILED, {
domain,
error: error.message || 'Unknown error',
isRenewal
});
} finally {
// Reset flag whether successful or not
domainInfo.obtainingInProgress = false;
}
}
/**
* Starts the certificate renewal timer
*/
private startRenewalTimer(): void {
if (this.renewalTimer) {
clearInterval(this.renewalTimer);
}
// Convert hours to milliseconds
const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
// Prevent the timer from keeping the process alive
if (this.renewalTimer.unref) {
this.renewalTimer.unref();
}
console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
}
/**
* Checks for certificates that need renewal
*/
private checkForRenewals(): void {
if (this.isShuttingDown) {
return;
}
console.log('Checking for certificates that need renewal...');
const now = new Date();
const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
// Skip domains without certificates or already in renewal
if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
continue;
}
// Skip domains without expiry dates
if (!domainInfo.expiryDate) {
continue;
}
const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
// Check if certificate is near expiry
if (timeUntilExpiry <= renewThresholdMs) {
console.log(`Certificate for ${domain} expires soon, renewing...`);
this.emit(CertManagerEvents.CERTIFICATE_EXPIRING, {
domain,
expiryDate: domainInfo.expiryDate,
daysRemaining: Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000))
});
// Start renewal process
this.obtainCertificate(domain, true).catch(err => {
console.error(`Error renewing certificate for ${domain}:`, err);
});
}
}
}
/**
* Emits a certificate event with the certificate data
* @param eventType The event type to emit
* @param data The certificate data
*/
private emitCertificateEvent(eventType: CertManagerEvents, data: ICertificateData): void {
this.emit(eventType, data);
}
}

View File

@ -0,0 +1,149 @@
import type { IPortProxySettings } from './classes.pp.interfaces.js';
import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
/**
* Manages ACME certificate operations
*/
export class AcmeManager {
constructor(
private settings: IPortProxySettings,
private networkProxyBridge: NetworkProxyBridge
) {}
/**
* Get current ACME settings
*/
public getAcmeSettings(): IPortProxySettings['acme'] {
return this.settings.acme;
}
/**
* Check if ACME is enabled
*/
public isAcmeEnabled(): boolean {
return !!this.settings.acme?.enabled;
}
/**
* Update ACME certificate settings
*/
public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
console.log('Updating ACME certificate settings');
// Check if enabled state is changing
const enabledChanging = this.settings.acme?.enabled !== acmeSettings.enabled;
// Update settings
this.settings.acme = {
...this.settings.acme,
...acmeSettings,
};
// Get NetworkProxy instance
const networkProxy = this.networkProxyBridge.getNetworkProxy();
if (!networkProxy) {
console.log('Cannot update ACME settings - NetworkProxy not initialized');
return;
}
try {
// If enabled state changed, we need to restart NetworkProxy
if (enabledChanging) {
console.log(`ACME enabled state changed to: ${acmeSettings.enabled}`);
// Stop the current NetworkProxy
await this.networkProxyBridge.stop();
// Reinitialize with new settings
await this.networkProxyBridge.initialize();
// Start NetworkProxy with new settings
await this.networkProxyBridge.start();
} else {
// Just update the settings in the existing NetworkProxy
console.log('Updating ACME settings in NetworkProxy without restart');
// Update settings in NetworkProxy
if (networkProxy.options && networkProxy.options.acme) {
networkProxy.options.acme = { ...this.settings.acme };
// For certificate renewals, we might want to trigger checks with the new settings
if (acmeSettings.renewThresholdDays !== undefined) {
console.log(`Setting new renewal threshold to ${acmeSettings.renewThresholdDays} days`);
networkProxy.options.acme.renewThresholdDays = acmeSettings.renewThresholdDays;
}
// Update other settings that might affect certificate operations
if (acmeSettings.useProduction !== undefined) {
console.log(`Setting ACME to ${acmeSettings.useProduction ? 'production' : 'staging'} mode`);
}
if (acmeSettings.autoRenew !== undefined) {
console.log(`Setting auto-renewal to ${acmeSettings.autoRenew ? 'enabled' : 'disabled'}`);
}
}
}
} catch (err) {
console.log(`Error updating ACME settings: ${err}`);
}
}
/**
* Request a certificate for a specific domain
*/
public async requestCertificate(domain: string): Promise<boolean> {
// Validate domain format
if (!this.isValidDomain(domain)) {
console.log(`Invalid domain format: ${domain}`);
return false;
}
// Delegate to NetworkProxyManager
return this.networkProxyBridge.requestCertificate(domain);
}
/**
* Basic domain validation
*/
private isValidDomain(domain: string): boolean {
// Very basic domain validation
if (!domain || domain.length === 0) {
return false;
}
// Check for wildcard domains (they can't get ACME certs)
if (domain.includes('*')) {
console.log(`Wildcard domains like "${domain}" are not supported for ACME certificates`);
return false;
}
// Check if domain has at least one dot and no invalid characters
const validDomainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
if (!validDomainRegex.test(domain)) {
console.log(`Domain "${domain}" has invalid format`);
return false;
}
return true;
}
/**
* Get eligible domains for ACME certificates
*/
public getEligibleDomains(): string[] {
// Collect all eligible domains from domain configs
const domains: string[] = [];
for (const config of this.settings.domainConfigs) {
// Skip domains that can't be used with ACME
const eligibleDomains = config.domains.filter(domain =>
!domain.includes('*') && this.isValidDomain(domain)
);
domains.push(...eligibleDomains);
}
return domains;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,446 @@
import * as plugins from './plugins.js';
import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js';
import { SecurityManager } from './classes.pp.securitymanager.js';
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
/**
* Manages connection lifecycle, tracking, and cleanup
*/
export class ConnectionManager {
private connectionRecords: Map<string, IConnectionRecord> = new Map();
private terminationStats: {
incoming: Record<string, number>;
outgoing: Record<string, number>;
} = { incoming: {}, outgoing: {} };
constructor(
private settings: IPortProxySettings,
private securityManager: SecurityManager,
private timeoutManager: TimeoutManager
) {}
/**
* Generate a unique connection ID
*/
public generateConnectionId(): string {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
/**
* Create and track a new connection
*/
public createConnection(socket: plugins.net.Socket): IConnectionRecord {
const connectionId = this.generateConnectionId();
const remoteIP = socket.remoteAddress || '';
const localPort = socket.localPort || 0;
const record: IConnectionRecord = {
id: connectionId,
incoming: socket,
outgoing: null,
incomingStartTime: Date.now(),
lastActivity: Date.now(),
connectionClosed: false,
pendingData: [],
pendingDataSize: 0,
bytesReceived: 0,
bytesSent: 0,
remoteIP,
localPort,
isTLS: false,
tlsHandshakeComplete: false,
hasReceivedInitialData: false,
hasKeepAlive: false,
incomingTerminationReason: null,
outgoingTerminationReason: null,
usingNetworkProxy: false,
isBrowserConnection: false,
domainSwitches: 0
};
this.trackConnection(connectionId, record);
return record;
}
/**
* Track an existing connection
*/
public trackConnection(connectionId: string, record: IConnectionRecord): void {
this.connectionRecords.set(connectionId, record);
this.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
}
/**
* Get a connection by ID
*/
public getConnection(connectionId: string): IConnectionRecord | undefined {
return this.connectionRecords.get(connectionId);
}
/**
* Get all active connections
*/
public getConnections(): Map<string, IConnectionRecord> {
return this.connectionRecords;
}
/**
* Get count of active connections
*/
public getConnectionCount(): number {
return this.connectionRecords.size;
}
/**
* Initiates cleanup once for a connection
*/
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
}
if (
record.incomingTerminationReason === null ||
record.incomingTerminationReason === undefined
) {
record.incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
}
this.cleanupConnection(record, reason);
}
/**
* Clean up a connection record
*/
public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
if (!record.connectionClosed) {
record.connectionClosed = true;
// Track connection termination
this.securityManager.removeConnectionByIP(record.remoteIP, record.id);
if (record.cleanupTimer) {
clearTimeout(record.cleanupTimer);
record.cleanupTimer = undefined;
}
// Detailed logging data
const duration = Date.now() - record.incomingStartTime;
const bytesReceived = record.bytesReceived;
const bytesSent = record.bytesSent;
// Remove all data handlers to make sure we clean up properly
if (record.incoming) {
try {
// Remove our safe data handler
record.incoming.removeAllListeners('data');
// Reset the handler references
record.renegotiationHandler = undefined;
} catch (err) {
console.log(`[${record.id}] Error removing data handlers: ${err}`);
}
}
// Handle incoming socket
this.cleanupSocket(record, 'incoming', record.incoming);
// Handle outgoing socket
if (record.outgoing) {
this.cleanupSocket(record, 'outgoing', record.outgoing);
}
// Clear pendingData to avoid memory leaks
record.pendingData = [];
record.pendingDataSize = 0;
// Remove the record from the tracking map
this.connectionRecords.delete(record.id);
// Log connection details
if (this.settings.enableDetailedLogging) {
console.log(
`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
);
} else {
console.log(
`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`
);
}
}
}
/**
* Helper method to clean up a socket
*/
private cleanupSocket(record: IConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void {
try {
if (!socket.destroyed) {
// Try graceful shutdown first, then force destroy after a short timeout
socket.end();
const socketTimeout = setTimeout(() => {
try {
if (!socket.destroyed) {
socket.destroy();
}
} catch (err) {
console.log(`[${record.id}] Error destroying ${side} socket: ${err}`);
}
}, 1000);
// Ensure the timeout doesn't block Node from exiting
if (socketTimeout.unref) {
socketTimeout.unref();
}
}
} catch (err) {
console.log(`[${record.id}] Error closing ${side} socket: ${err}`);
try {
if (!socket.destroyed) {
socket.destroy();
}
} catch (destroyErr) {
console.log(`[${record.id}] Error destroying ${side} socket: ${destroyErr}`);
}
}
}
/**
* Creates a generic error handler for incoming or outgoing sockets
*/
public handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
return (err: Error) => {
const code = (err as any).code;
let reason = 'error';
const now = Date.now();
const connectionDuration = now - record.incomingStartTime;
const lastActivityAge = now - record.lastActivity;
if (code === 'ECONNRESET') {
reason = 'econnreset';
console.log(
`[${record.id}] ECONNRESET on ${side} side from ${record.remoteIP}: ${err.message}. ` +
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`
);
} else if (code === 'ETIMEDOUT') {
reason = 'etimedout';
console.log(
`[${record.id}] ETIMEDOUT on ${side} side from ${record.remoteIP}: ${err.message}. ` +
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`
);
} else {
console.log(
`[${record.id}] Error on ${side} side from ${record.remoteIP}: ${err.message}. ` +
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`
);
}
if (side === 'incoming' && record.incomingTerminationReason === null) {
record.incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = reason;
this.incrementTerminationStat('outgoing', reason);
}
this.initiateCleanupOnce(record, reason);
};
}
/**
* Creates a generic close handler for incoming or outgoing sockets
*/
public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
return () => {
if (this.settings.enableDetailedLogging) {
console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
}
if (side === 'incoming' && record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'normal';
this.incrementTerminationStat('incoming', 'normal');
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'normal';
this.incrementTerminationStat('outgoing', 'normal');
// Record the time when outgoing socket closed.
record.outgoingClosedTime = Date.now();
}
this.initiateCleanupOnce(record, 'closed_' + side);
};
}
/**
* Increment termination statistics
*/
public incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
}
/**
* Get termination statistics
*/
public getTerminationStats(): { incoming: Record<string, number>; outgoing: Record<string, number> } {
return this.terminationStats;
}
/**
* Check for stalled/inactive connections
*/
public performInactivityCheck(): void {
const now = Date.now();
const connectionIds = [...this.connectionRecords.keys()];
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (!record) continue;
// Skip inactivity check if disabled or for immortal keep-alive connections
if (
this.settings.disableInactivityCheck ||
(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
) {
continue;
}
const inactivityTime = now - record.lastActivity;
// Use extended timeout for extended-treatment keep-alive connections
let effectiveTimeout = this.settings.inactivityTimeout!;
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
effectiveTimeout = effectiveTimeout * multiplier;
}
if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
// For keep-alive connections, issue a warning first
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
console.log(
`[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${
plugins.prettyMs(inactivityTime)
}. Will close in 10 minutes if no activity.`
);
// Set warning flag and add grace period
record.inactivityWarningIssued = true;
record.lastActivity = now - (effectiveTimeout - 600000);
// Try to stimulate activity with a probe packet
if (record.outgoing && !record.outgoing.destroyed) {
try {
record.outgoing.write(Buffer.alloc(0));
if (this.settings.enableDetailedLogging) {
console.log(`[${id}] Sent probe packet to test keep-alive connection`);
}
} catch (err) {
console.log(`[${id}] Error sending probe packet: ${err}`);
}
}
} else {
// For non-keep-alive or after warning, close the connection
console.log(
`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
`for ${plugins.prettyMs(inactivityTime)}.` +
(record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
);
this.cleanupConnection(record, 'inactivity');
}
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
// If activity detected after warning, clear the warning
if (this.settings.enableDetailedLogging) {
console.log(
`[${id}] Connection activity detected after inactivity warning, resetting warning`
);
}
record.inactivityWarningIssued = false;
}
// Parity check: if outgoing socket closed and incoming remains active
if (
record.outgoingClosedTime &&
!record.incoming.destroyed &&
!record.connectionClosed &&
now - record.outgoingClosedTime > 120000
) {
console.log(
`[${id}] Parity check: Incoming socket for ${record.remoteIP} still active ${
plugins.prettyMs(now - record.outgoingClosedTime)
} after outgoing closed.`
);
this.cleanupConnection(record, 'parity_check');
}
}
}
/**
* Clear all connections (for shutdown)
*/
public clearConnections(): void {
// Create a copy of the keys to avoid modification during iteration
const connectionIds = [...this.connectionRecords.keys()];
// First pass: End all connections gracefully
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (record) {
try {
// Clear any timers
if (record.cleanupTimer) {
clearTimeout(record.cleanupTimer);
record.cleanupTimer = undefined;
}
// End sockets gracefully
if (record.incoming && !record.incoming.destroyed) {
record.incoming.end();
}
if (record.outgoing && !record.outgoing.destroyed) {
record.outgoing.end();
}
} catch (err) {
console.log(`Error during graceful connection end for ${id}: ${err}`);
}
}
}
// Short delay to allow graceful ends to process
setTimeout(() => {
// Second pass: Force destroy everything
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (record) {
try {
// Remove all listeners to prevent memory leaks
if (record.incoming) {
record.incoming.removeAllListeners();
if (!record.incoming.destroyed) {
record.incoming.destroy();
}
}
if (record.outgoing) {
record.outgoing.removeAllListeners();
if (!record.outgoing.destroyed) {
record.outgoing.destroy();
}
}
} catch (err) {
console.log(`Error during forced connection destruction for ${id}: ${err}`);
}
}
}
// Clear all maps
this.connectionRecords.clear();
this.terminationStats = { incoming: {}, outgoing: {} };
}, 100);
}
}

View File

@ -0,0 +1,123 @@
import * as plugins from './plugins.js';
import type { IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js';
/**
* Manages domain configurations and target selection
*/
export class DomainConfigManager {
// Track round-robin indices for domain configs
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
constructor(private settings: IPortProxySettings) {}
/**
* Updates the domain configurations
*/
public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void {
this.settings.domainConfigs = newDomainConfigs;
// Reset target indices for removed configs
const currentConfigSet = new Set(newDomainConfigs);
for (const [config] of this.domainTargetIndices) {
if (!currentConfigSet.has(config)) {
this.domainTargetIndices.delete(config);
}
}
}
/**
* Get all domain configurations
*/
public getDomainConfigs(): IDomainConfig[] {
return this.settings.domainConfigs;
}
/**
* Find domain config matching a server name
*/
public findDomainConfig(serverName: string): IDomainConfig | undefined {
if (!serverName) return undefined;
return this.settings.domainConfigs.find((config) =>
config.domains.some((d) => plugins.minimatch(serverName, d))
);
}
/**
* Find domain config for a specific port
*/
public findDomainConfigForPort(port: number): IDomainConfig | undefined {
return this.settings.domainConfigs.find(
(domain) =>
domain.portRanges &&
domain.portRanges.length > 0 &&
this.isPortInRanges(port, domain.portRanges)
);
}
/**
* Check if a port is within any of the given ranges
*/
public isPortInRanges(port: number, ranges: Array<{ from: number; to: number }>): boolean {
return ranges.some((range) => port >= range.from && port <= range.to);
}
/**
* Get target IP with round-robin support
*/
public getTargetIP(domainConfig: IDomainConfig): string {
if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
const ip = domainConfig.targetIPs[currentIndex % domainConfig.targetIPs.length];
this.domainTargetIndices.set(domainConfig, currentIndex + 1);
return ip;
}
return this.settings.targetIP || 'localhost';
}
/**
* Checks if a domain should use NetworkProxy
*/
public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean {
return !!domainConfig.useNetworkProxy;
}
/**
* Gets the NetworkProxy port for a domain
*/
public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined {
return domainConfig.useNetworkProxy
? (domainConfig.networkProxyPort || this.settings.networkProxyPort)
: undefined;
}
/**
* Get effective allowed and blocked IPs for a domain
*/
public getEffectiveIPRules(domainConfig: IDomainConfig): {
allowedIPs: string[],
blockedIPs: string[]
} {
return {
allowedIPs: [
...domainConfig.allowedIPs,
...(this.settings.defaultAllowedIPs || [])
],
blockedIPs: [
...(domainConfig.blockedIPs || []),
...(this.settings.defaultBlockedIPs || [])
]
};
}
/**
* Get connection timeout for a domain
*/
public getConnectionTimeout(domainConfig?: IDomainConfig): number {
if (domainConfig?.connectionTimeout) {
return domainConfig.connectionTimeout;
}
return this.settings.maxConnectionLifetime || 86400000; // 24 hours default
}
}

137
ts/classes.pp.interfaces.ts Normal file
View File

@ -0,0 +1,137 @@
import * as plugins from './plugins.js';
/** Domain configuration with per-domain allowed port ranges */
export interface IDomainConfig {
domains: string[]; // Glob patterns for domain(s)
allowedIPs: string[]; // Glob patterns for allowed IPs
blockedIPs?: string[]; // Glob patterns for blocked IPs
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
// Allow domain-specific timeout override
connectionTimeout?: number; // Connection timeout override (ms)
// NetworkProxy integration options for this specific domain
useNetworkProxy?: boolean; // Whether to use NetworkProxy for this domain
networkProxyPort?: number; // Override default NetworkProxy port for this domain
}
/** Port proxy settings including global allowed port ranges */
export interface IPortProxySettings {
fromPort: number;
toPort: number;
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
domainConfigs: IDomainConfig[];
sniEnabled?: boolean;
defaultAllowedIPs?: string[];
defaultBlockedIPs?: string[];
preserveSourceIP?: boolean;
// TLS options
pfx?: Buffer;
key?: string | Buffer | Array<Buffer | string>;
passphrase?: string;
cert?: string | Buffer | Array<string | Buffer>;
ca?: string | Buffer | Array<string | Buffer>;
ciphers?: string;
honorCipherOrder?: boolean;
rejectUnauthorized?: boolean;
secureProtocol?: string;
servername?: string;
minVersion?: string;
maxVersion?: string;
// Timeout settings
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h)
inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
// Socket optimization settings
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
keepAlive?: boolean; // Enable TCP keepalive (default: true)
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
// Enhanced features
disableInactivityCheck?: boolean; // Disable inactivity checking entirely
enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
enableDetailedLogging?: boolean; // Enable detailed connection logging
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true)
// Rate limiting and security
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
// Enhanced keep-alive settings
keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
// NetworkProxy integration
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
// 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
};
}
/**
* Enhanced connection record
*/
export interface IConnectionRecord {
id: string; // Unique connection identifier
incoming: plugins.net.Socket;
outgoing: plugins.net.Socket | null;
incomingStartTime: number;
outgoingStartTime?: number;
outgoingClosedTime?: number;
lockedDomain?: string; // Used to lock this connection to the initial SNI
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert
lastActivity: number; // Last activity timestamp for inactivity detection
pendingData: Buffer[]; // Buffer to hold data during connection setup
pendingDataSize: number; // Track total size of pending data
// Enhanced tracking fields
bytesReceived: number; // Total bytes received
bytesSent: number; // Total bytes sent
remoteIP: string; // Remote IP (cached for logging after socket close)
localPort: number; // Local port (cached for logging)
isTLS: boolean; // Whether this connection is a TLS connection
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
hasReceivedInitialData: boolean; // Whether initial data has been received
domainConfig?: IDomainConfig; // Associated domain config for this connection
// Keep-alive tracking
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
incomingTerminationReason?: string | null; // Reason for incoming termination
outgoingTerminationReason?: string | null; // Reason for outgoing termination
// NetworkProxy tracking
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
// Renegotiation handler
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
// Browser connection tracking
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
domainSwitches?: number; // Number of times the domain has been switched on this connection
}

View File

@ -0,0 +1,258 @@
import * as plugins from './plugins.js';
import { NetworkProxy } from './classes.networkproxy.js';
import type { IConnectionRecord, IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
/**
* Manages NetworkProxy integration for TLS termination
*/
export class NetworkProxyBridge {
private networkProxy: NetworkProxy | null = null;
constructor(private settings: IPortProxySettings) {}
/**
* Initialize NetworkProxy instance
*/
public async initialize(): Promise<void> {
if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
// Configure NetworkProxy options based on PortProxy settings
const networkProxyOptions: any = {
port: this.settings.networkProxyPort!,
portProxyIntegration: true,
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info',
};
// Add ACME settings if configured
if (this.settings.acme) {
networkProxyOptions.acme = { ...this.settings.acme };
}
this.networkProxy = new NetworkProxy(networkProxyOptions);
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
// Convert and apply domain configurations to NetworkProxy
await this.syncDomainConfigsToNetworkProxy();
}
}
/**
* Get the NetworkProxy instance
*/
public getNetworkProxy(): NetworkProxy | null {
return this.networkProxy;
}
/**
* Get the NetworkProxy port
*/
public getNetworkProxyPort(): number {
return this.networkProxy ? this.networkProxy.getListeningPort() : this.settings.networkProxyPort || 8443;
}
/**
* Start NetworkProxy
*/
public async start(): Promise<void> {
if (this.networkProxy) {
await this.networkProxy.start();
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
// Log ACME status
if (this.settings.acme?.enabled) {
console.log(
`ACME certificate management is enabled (${
this.settings.acme.useProduction ? 'Production' : 'Staging'
} mode)`
);
console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
// Register domains for ACME certificates if enabled
if (this.networkProxy.options.acme?.enabled) {
console.log('Registering domains with ACME certificate manager...');
// The NetworkProxy will handle this internally via registerDomainsWithAcmeManager()
}
}
}
}
/**
* Stop NetworkProxy
*/
public async stop(): Promise<void> {
if (this.networkProxy) {
try {
console.log('Stopping NetworkProxy...');
await this.networkProxy.stop();
console.log('NetworkProxy stopped successfully');
// Log ACME shutdown if it was enabled
if (this.settings.acme?.enabled) {
console.log('ACME certificate manager stopped');
}
} catch (err) {
console.log(`Error stopping NetworkProxy: ${err}`);
}
}
}
/**
* Forwards a TLS connection to a NetworkProxy for handling
*/
public forwardToNetworkProxy(
connectionId: string,
socket: plugins.net.Socket,
record: IConnectionRecord,
initialData: Buffer,
customProxyPort?: number,
onError?: (reason: string) => void
): void {
// Ensure NetworkProxy is initialized
if (!this.networkProxy) {
console.log(
`[${connectionId}] NetworkProxy not initialized. Cannot forward connection.`
);
if (onError) {
onError('network_proxy_not_initialized');
}
return;
}
// Use the custom port if provided, otherwise use the default NetworkProxy port
const proxyPort = customProxyPort || this.networkProxy.getListeningPort();
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
);
}
// Create a connection to the NetworkProxy
const proxySocket = plugins.net.connect({
host: proxyHost,
port: proxyPort,
});
// Store the outgoing socket in the record
record.outgoing = proxySocket;
record.outgoingStartTime = Date.now();
record.usingNetworkProxy = true;
// Set up error handlers
proxySocket.on('error', (err) => {
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
if (onError) {
onError('network_proxy_connect_error');
}
});
// Handle connection to NetworkProxy
proxySocket.on('connect', () => {
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
}
// First send the initial data that contains the TLS ClientHello
proxySocket.write(initialData);
// Now set up bidirectional piping between client and NetworkProxy
socket.pipe(proxySocket);
proxySocket.pipe(socket);
// Update activity on data transfer (caller should handle this)
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`);
}
});
}
/**
* Synchronizes domain configurations to NetworkProxy
*/
public async syncDomainConfigsToNetworkProxy(): Promise<void> {
if (!this.networkProxy) {
console.log('Cannot sync configurations - NetworkProxy not initialized');
return;
}
try {
// Get SSL certificates from assets
// Import fs directly since it's not in plugins
const fs = await import('fs');
let certPair;
try {
certPair = {
key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'),
};
} catch (certError) {
console.log(`Warning: Could not read default certificates: ${certError}`);
console.log(
'Using empty certificate placeholders - ACME will generate proper certificates if enabled'
);
// Use empty placeholders - NetworkProxy will use its internal defaults
// or ACME will generate proper ones if enabled
certPair = {
key: '',
cert: '',
};
}
// Convert domain configs to NetworkProxy configs
const proxyConfigs = this.networkProxy.convertPortProxyConfigs(
this.settings.domainConfigs,
certPair
);
// Log ACME-eligible domains if ACME is enabled
if (this.settings.acme?.enabled) {
const acmeEligibleDomains = proxyConfigs
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
.map((config) => config.hostName);
if (acmeEligibleDomains.length > 0) {
console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
} else {
console.log('No domains eligible for ACME certificates found in configuration');
}
}
// Update NetworkProxy with the converted configs
await this.networkProxy.updateProxyConfigs(proxyConfigs);
console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`);
} catch (err) {
console.log(`Failed to sync configurations: ${err}`);
}
}
/**
* Request a certificate for a specific domain
*/
public async requestCertificate(domain: string): Promise<boolean> {
if (!this.networkProxy) {
console.log('Cannot request certificate - NetworkProxy not initialized');
return false;
}
if (!this.settings.acme?.enabled) {
console.log('Cannot request certificate - ACME is not enabled');
return false;
}
try {
const result = await this.networkProxy.requestCertificate(domain);
if (result) {
console.log(`Certificate request for ${domain} submitted successfully`);
} else {
console.log(`Certificate request for ${domain} failed`);
}
return result;
} catch (err) {
console.log(`Error requesting certificate: ${err}`);
return false;
}
}
}

344
ts/classes.pp.portproxy.ts Normal file
View File

@ -0,0 +1,344 @@
import * as plugins from './plugins.js';
import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
import { ConnectionManager } from './classes.pp.connectionmanager.js';
import { SecurityManager } from './classes.pp.securitymanager.js';
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
import { TlsManager } from './classes.pp.tlsmanager.js';
import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
import { AcmeManager } from './classes.pp.acmemanager.js';
import { PortRangeManager } from './classes.pp.portrangemanager.js';
import { ConnectionHandler } from './classes.pp.connectionhandler.js';
/**
* PortProxy - Main class that coordinates all components
*/
export class PortProxy {
private netServers: plugins.net.Server[] = [];
private connectionLogger: NodeJS.Timeout | null = null;
private isShuttingDown: boolean = false;
// Component managers
private connectionManager: ConnectionManager;
private securityManager: SecurityManager;
public domainConfigManager: DomainConfigManager;
private tlsManager: TlsManager;
private networkProxyBridge: NetworkProxyBridge;
private timeoutManager: TimeoutManager;
private acmeManager: AcmeManager;
private portRangeManager: PortRangeManager;
private connectionHandler: ConnectionHandler;
constructor(settingsArg: IPortProxySettings) {
// Set reasonable defaults for all settings
this.settings = {
...settingsArg,
targetIP: settingsArg.targetIP || 'localhost',
initialDataTimeout: settingsArg.initialDataTimeout || 120000,
socketTimeout: settingsArg.socketTimeout || 3600000,
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000,
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000,
inactivityTimeout: settingsArg.inactivityTimeout || 14400000,
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000,
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024,
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
enableKeepAliveProbes:
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
allowSessionTicket:
settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
networkProxyPort: settingsArg.networkProxyPort || 8443,
acme: settingsArg.acme || {
enabled: false,
port: 80,
contactEmail: 'admin@example.com',
useProduction: false,
renewThresholdDays: 30,
autoRenew: true,
certificateStore: './certs',
skipConfiguredCerts: false,
},
};
// Initialize component managers
this.timeoutManager = new TimeoutManager(this.settings);
this.securityManager = new SecurityManager(this.settings);
this.connectionManager = new ConnectionManager(
this.settings,
this.securityManager,
this.timeoutManager
);
this.domainConfigManager = new DomainConfigManager(this.settings);
this.tlsManager = new TlsManager(this.settings);
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
this.portRangeManager = new PortRangeManager(this.settings);
this.acmeManager = new AcmeManager(this.settings, this.networkProxyBridge);
// Initialize connection handler
this.connectionHandler = new ConnectionHandler(
this.settings,
this.connectionManager,
this.securityManager,
this.domainConfigManager,
this.tlsManager,
this.networkProxyBridge,
this.timeoutManager,
this.portRangeManager
);
}
/**
* The settings for the port proxy
*/
public settings: IPortProxySettings;
/**
* Start the proxy server
*/
public async start() {
// Don't start if already shutting down
if (this.isShuttingDown) {
console.log("Cannot start PortProxy while it's shutting down");
return;
}
// Initialize and start NetworkProxy if needed
if (
this.settings.useNetworkProxy &&
this.settings.useNetworkProxy.length > 0
) {
await this.networkProxyBridge.initialize();
await this.networkProxyBridge.start();
}
// Validate port configuration
const configWarnings = this.portRangeManager.validateConfiguration();
if (configWarnings.length > 0) {
console.log("Port configuration warnings:");
for (const warning of configWarnings) {
console.log(` - ${warning}`);
}
}
// Get listening ports from PortRangeManager
const listeningPorts = this.portRangeManager.getListeningPorts();
// Create servers for each port
for (const port of listeningPorts) {
const server = plugins.net.createServer((socket) => {
// Check if shutting down
if (this.isShuttingDown) {
socket.end();
socket.destroy();
return;
}
// Delegate to connection handler
this.connectionHandler.handleConnection(socket);
}).on('error', (err: Error) => {
console.log(`Server Error on port ${port}: ${err.message}`);
});
server.listen(port, () => {
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
console.log(
`PortProxy -> OK: Now listening on port ${port}${
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
);
});
this.netServers.push(server);
}
// Set up periodic connection logging and inactivity checks
this.connectionLogger = setInterval(() => {
// Immediately return if shutting down
if (this.isShuttingDown) return;
// Perform inactivity check
this.connectionManager.performInactivityCheck();
// Log connection statistics
const now = Date.now();
let maxIncoming = 0;
let maxOutgoing = 0;
let tlsConnections = 0;
let nonTlsConnections = 0;
let completedTlsHandshakes = 0;
let pendingTlsHandshakes = 0;
let keepAliveConnections = 0;
let networkProxyConnections = 0;
// Get connection records for analysis
const connectionRecords = this.connectionManager.getConnections();
// Analyze active connections
for (const record of connectionRecords.values()) {
// Track connection stats
if (record.isTLS) {
tlsConnections++;
if (record.tlsHandshakeComplete) {
completedTlsHandshakes++;
} else {
pendingTlsHandshakes++;
}
} else {
nonTlsConnections++;
}
if (record.hasKeepAlive) {
keepAliveConnections++;
}
if (record.usingNetworkProxy) {
networkProxyConnections++;
}
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
if (record.outgoingStartTime) {
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
}
}
// Get termination stats
const terminationStats = this.connectionManager.getTerminationStats();
// Log detailed stats
console.log(
`Active connections: ${connectionRecords.size}. ` +
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
`Termination stats: ${JSON.stringify({
IN: terminationStats.incoming,
OUT: terminationStats.outgoing,
})}`
);
}, this.settings.inactivityCheckInterval || 60000);
// Make sure the interval doesn't keep the process alive
if (this.connectionLogger.unref) {
this.connectionLogger.unref();
}
}
/**
* Stop the proxy server
*/
public async stop() {
console.log('PortProxy shutting down...');
this.isShuttingDown = true;
// Stop accepting new connections
const closeServerPromises: Promise<void>[] = this.netServers.map(
(server) =>
new Promise<void>((resolve) => {
if (!server.listening) {
resolve();
return;
}
server.close((err) => {
if (err) {
console.log(`Error closing server: ${err.message}`);
}
resolve();
});
})
);
// Stop the connection logger
if (this.connectionLogger) {
clearInterval(this.connectionLogger);
this.connectionLogger = null;
}
// Wait for servers to close
await Promise.all(closeServerPromises);
console.log('All servers closed. Cleaning up active connections...');
// Clean up all active connections
this.connectionManager.clearConnections();
// Stop NetworkProxy
await this.networkProxyBridge.stop();
// Clear all servers
this.netServers = [];
console.log('PortProxy shutdown complete.');
}
/**
* Updates the domain configurations for the proxy
*/
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
// Update domain configs in DomainConfigManager
this.domainConfigManager.updateDomainConfigs(newDomainConfigs);
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) {
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
}
}
/**
* Updates the ACME certificate settings
*/
public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
console.log('Updating ACME certificate settings');
// Delegate to AcmeManager
await this.acmeManager.updateAcmeSettings(acmeSettings);
}
/**
* Requests a certificate for a specific domain
*/
public async requestCertificate(domain: string): Promise<boolean> {
// Delegate to AcmeManager
return this.acmeManager.requestCertificate(domain);
}
/**
* Get statistics about current connections
*/
public getStatistics(): any {
const connectionRecords = this.connectionManager.getConnections();
const terminationStats = this.connectionManager.getTerminationStats();
let tlsConnections = 0;
let nonTlsConnections = 0;
let keepAliveConnections = 0;
let networkProxyConnections = 0;
// Analyze active connections
for (const record of connectionRecords.values()) {
if (record.isTLS) tlsConnections++;
else nonTlsConnections++;
if (record.hasKeepAlive) keepAliveConnections++;
if (record.usingNetworkProxy) networkProxyConnections++;
}
return {
activeConnections: connectionRecords.size,
tlsConnections,
nonTlsConnections,
keepAliveConnections,
networkProxyConnections,
terminationStats
};
}
}

View File

@ -0,0 +1,214 @@
import type{ IPortProxySettings } from './classes.pp.interfaces.js';
/**
* Manages port ranges and port-based configuration
*/
export class PortRangeManager {
constructor(private settings: IPortProxySettings) {}
/**
* Get all ports that should be listened on
*/
public getListeningPorts(): Set<number> {
const listeningPorts = new Set<number>();
// Always include the main fromPort
listeningPorts.add(this.settings.fromPort);
// Add ports from global port ranges if defined
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
for (const range of this.settings.globalPortRanges) {
for (let port = range.from; port <= range.to; port++) {
listeningPorts.add(port);
}
}
}
return listeningPorts;
}
/**
* Check if a port should use NetworkProxy for forwarding
*/
public shouldUseNetworkProxy(port: number): boolean {
return !!this.settings.useNetworkProxy && this.settings.useNetworkProxy.includes(port);
}
/**
* Check if port should use global forwarding
*/
public shouldUseGlobalForwarding(port: number): boolean {
return (
!!this.settings.forwardAllGlobalRanges &&
this.isPortInGlobalRanges(port)
);
}
/**
* Check if a port is in global ranges
*/
public isPortInGlobalRanges(port: number): boolean {
return (
this.settings.globalPortRanges &&
this.isPortInRanges(port, this.settings.globalPortRanges)
);
}
/**
* Check if a port falls within the specified ranges
*/
public isPortInRanges(port: number, ranges: Array<{ from: number; to: number }>): boolean {
return ranges.some((range) => port >= range.from && port <= range.to);
}
/**
* Get forwarding port for a specific listening port
* This determines what port to connect to on the target
*/
public getForwardingPort(listeningPort: number): number {
// If using global forwarding, forward to the original port
if (this.settings.forwardAllGlobalRanges && this.isPortInGlobalRanges(listeningPort)) {
return listeningPort;
}
// Otherwise use the configured toPort
return this.settings.toPort;
}
/**
* Find domain-specific port ranges that include a given port
*/
public findDomainPortRange(port: number): {
domainIndex: number,
range: { from: number, to: number }
} | undefined {
for (let i = 0; i < this.settings.domainConfigs.length; i++) {
const domain = this.settings.domainConfigs[i];
if (domain.portRanges) {
for (const range of domain.portRanges) {
if (port >= range.from && port <= range.to) {
return { domainIndex: i, range };
}
}
}
}
return undefined;
}
/**
* Get a list of all configured ports
* This includes the fromPort, NetworkProxy ports, and ports from all ranges
*/
public getAllConfiguredPorts(): number[] {
const ports = new Set<number>();
// Add main listening port
ports.add(this.settings.fromPort);
// Add NetworkProxy port if configured
if (this.settings.networkProxyPort) {
ports.add(this.settings.networkProxyPort);
}
// Add NetworkProxy ports
if (this.settings.useNetworkProxy) {
for (const port of this.settings.useNetworkProxy) {
ports.add(port);
}
}
// Add ACME HTTP challenge port if enabled
if (this.settings.acme?.enabled && this.settings.acme.port) {
ports.add(this.settings.acme.port);
}
// Add global port ranges
if (this.settings.globalPortRanges) {
for (const range of this.settings.globalPortRanges) {
for (let port = range.from; port <= range.to; port++) {
ports.add(port);
}
}
}
// Add domain-specific port ranges
for (const domain of this.settings.domainConfigs) {
if (domain.portRanges) {
for (const range of domain.portRanges) {
for (let port = range.from; port <= range.to; port++) {
ports.add(port);
}
}
}
// Add domain-specific NetworkProxy port if configured
if (domain.useNetworkProxy && domain.networkProxyPort) {
ports.add(domain.networkProxyPort);
}
}
return Array.from(ports);
}
/**
* Validate port configuration
* Returns array of warning messages
*/
public validateConfiguration(): string[] {
const warnings: string[] = [];
// Check for overlapping port ranges
const portMappings = new Map<number, string[]>();
// Track global port ranges
if (this.settings.globalPortRanges) {
for (const range of this.settings.globalPortRanges) {
for (let port = range.from; port <= range.to; port++) {
if (!portMappings.has(port)) {
portMappings.set(port, []);
}
portMappings.get(port)!.push('Global Port Range');
}
}
}
// Track domain-specific port ranges
for (const domain of this.settings.domainConfigs) {
if (domain.portRanges) {
for (const range of domain.portRanges) {
for (let port = range.from; port <= range.to; port++) {
if (!portMappings.has(port)) {
portMappings.set(port, []);
}
portMappings.get(port)!.push(`Domain: ${domain.domains.join(', ')}`);
}
}
}
}
// Check for ports with multiple mappings
for (const [port, mappings] of portMappings.entries()) {
if (mappings.length > 1) {
warnings.push(`Port ${port} has multiple mappings: ${mappings.join(', ')}`);
}
}
// Check if main ports are used elsewhere
if (portMappings.has(this.settings.fromPort) && portMappings.get(this.settings.fromPort)!.length > 0) {
warnings.push(`Main listening port ${this.settings.fromPort} is also used in port ranges`);
}
if (this.settings.networkProxyPort && portMappings.has(this.settings.networkProxyPort)) {
warnings.push(`NetworkProxy port ${this.settings.networkProxyPort} is also used in port ranges`);
}
// Check ACME port
if (this.settings.acme?.enabled && this.settings.acme.port) {
if (portMappings.has(this.settings.acme.port)) {
warnings.push(`ACME HTTP challenge port ${this.settings.acme.port} is also used in port ranges`);
}
}
return warnings;
}
}

View File

@ -0,0 +1,147 @@
import * as plugins from './plugins.js';
import type { IPortProxySettings } from './classes.pp.interfaces.js';
/**
* Handles security aspects like IP tracking, rate limiting, and authorization
*/
export class SecurityManager {
private connectionsByIP: Map<string, Set<string>> = new Map();
private connectionRateByIP: Map<string, number[]> = new Map();
constructor(private settings: IPortProxySettings) {}
/**
* Get connections count by IP
*/
public getConnectionCountByIP(ip: string): number {
return this.connectionsByIP.get(ip)?.size || 0;
}
/**
* Check and update connection rate for an IP
* @returns true if within rate limit, false if exceeding limit
*/
public checkConnectionRate(ip: string): boolean {
const now = Date.now();
const minute = 60 * 1000;
if (!this.connectionRateByIP.has(ip)) {
this.connectionRateByIP.set(ip, [now]);
return true;
}
// Get timestamps and filter out entries older than 1 minute
const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
timestamps.push(now);
this.connectionRateByIP.set(ip, timestamps);
// Check if rate exceeds limit
return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
}
/**
* Track connection by IP
*/
public trackConnectionByIP(ip: string, connectionId: string): void {
if (!this.connectionsByIP.has(ip)) {
this.connectionsByIP.set(ip, new Set());
}
this.connectionsByIP.get(ip)!.add(connectionId);
}
/**
* Remove connection tracking for an IP
*/
public removeConnectionByIP(ip: string, connectionId: string): void {
if (this.connectionsByIP.has(ip)) {
const connections = this.connectionsByIP.get(ip)!;
connections.delete(connectionId);
if (connections.size === 0) {
this.connectionsByIP.delete(ip);
}
}
}
/**
* Check if an IP is allowed using glob patterns
*/
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
// Skip IP validation if allowedIPs is empty
if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
return true;
}
// First check if IP is blocked
if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
return false;
}
// Then check if IP is allowed
return this.isGlobIPMatch(ip, allowedIPs);
}
/**
* Check if the IP matches any of the glob patterns
*/
private isGlobIPMatch(ip: string, patterns: string[]): boolean {
if (!ip || !patterns || patterns.length === 0) return false;
const normalizeIP = (ip: string): string[] => {
if (!ip) return [];
if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7);
return [ip, ipv4];
}
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
return [ip, `::ffff:${ip}`];
}
return [ip];
};
const normalizedIPVariants = normalizeIP(ip);
if (normalizedIPVariants.length === 0) return false;
const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some((ipVariant) =>
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
);
}
/**
* Check if IP should be allowed considering connection rate and max connections
* @returns Object with result and reason
*/
public validateIP(ip: string): { allowed: boolean; reason?: string } {
// Check connection count limit
if (
this.settings.maxConnectionsPerIP &&
this.getConnectionCountByIP(ip) >= this.settings.maxConnectionsPerIP
) {
return {
allowed: false,
reason: `Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
};
}
// Check connection rate limit
if (
this.settings.connectionRateLimitPerMinute &&
!this.checkConnectionRate(ip)
) {
return {
allowed: false,
reason: `Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
};
}
return { allowed: true };
}
/**
* Clears all IP tracking data (for shutdown)
*/
public clearIPTracking(): void {
this.connectionsByIP.clear();
this.connectionRateByIP.clear();
}
}

1281
ts/classes.pp.snihandler.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,190 @@
import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js';
/**
* Manages timeouts and inactivity tracking for connections
*/
export class TimeoutManager {
constructor(private settings: IPortProxySettings) {}
/**
* Ensure timeout values don't exceed Node.js max safe integer
*/
public ensureSafeTimeout(timeout: number): number {
const MAX_SAFE_TIMEOUT = 2147483647; // Maximum safe value (2^31 - 1)
return Math.min(Math.floor(timeout), MAX_SAFE_TIMEOUT);
}
/**
* Generate a slightly randomized timeout to prevent thundering herd
*/
public randomizeTimeout(baseTimeout: number, variationPercent: number = 5): number {
const safeBaseTimeout = this.ensureSafeTimeout(baseTimeout);
const variation = safeBaseTimeout * (variationPercent / 100);
return this.ensureSafeTimeout(
safeBaseTimeout + Math.floor(Math.random() * variation * 2) - variation
);
}
/**
* Update connection activity timestamp
*/
public updateActivity(record: IConnectionRecord): void {
record.lastActivity = Date.now();
// Clear any inactivity warning
if (record.inactivityWarningIssued) {
record.inactivityWarningIssued = false;
}
}
/**
* Calculate effective inactivity timeout based on connection type
*/
public getEffectiveInactivityTimeout(record: IConnectionRecord): number {
let effectiveTimeout = this.settings.inactivityTimeout || 14400000; // 4 hours default
// For immortal keep-alive connections, use an extremely long timeout
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
return Number.MAX_SAFE_INTEGER;
}
// For extended keep-alive connections, apply multiplier
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
effectiveTimeout = effectiveTimeout * multiplier;
}
return this.ensureSafeTimeout(effectiveTimeout);
}
/**
* Calculate effective max lifetime based on connection type
*/
public getEffectiveMaxLifetime(record: IConnectionRecord): number {
// Use domain-specific timeout if available
const baseTimeout = record.domainConfig?.connectionTimeout ||
this.settings.maxConnectionLifetime ||
86400000; // 24 hours default
// For immortal keep-alive connections, use an extremely long lifetime
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
return Number.MAX_SAFE_INTEGER;
}
// For extended keep-alive connections, use the extended lifetime setting
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
return this.ensureSafeTimeout(
this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000 // 7 days default
);
}
// Apply randomization if enabled
if (this.settings.enableRandomizedTimeouts) {
return this.randomizeTimeout(baseTimeout);
}
return this.ensureSafeTimeout(baseTimeout);
}
/**
* Setup connection timeout
* @returns The cleanup timer
*/
public setupConnectionTimeout(
record: IConnectionRecord,
onTimeout: (record: IConnectionRecord, reason: string) => void
): NodeJS.Timeout {
// Clear any existing timer
if (record.cleanupTimer) {
clearTimeout(record.cleanupTimer);
}
// Calculate effective timeout
const effectiveLifetime = this.getEffectiveMaxLifetime(record);
// Set up the timeout
const timer = setTimeout(() => {
// Call the provided callback
onTimeout(record, 'connection_timeout');
}, effectiveLifetime);
// Make sure timeout doesn't keep the process alive
if (timer.unref) {
timer.unref();
}
return timer;
}
/**
* Check for inactivity on a connection
* @returns Object with check results
*/
public checkInactivity(record: IConnectionRecord): {
isInactive: boolean;
shouldWarn: boolean;
inactivityTime: number;
effectiveTimeout: number;
} {
// Skip for connections with inactivity check disabled
if (this.settings.disableInactivityCheck) {
return {
isInactive: false,
shouldWarn: false,
inactivityTime: 0,
effectiveTimeout: 0
};
}
// Skip for immortal keep-alive connections
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
return {
isInactive: false,
shouldWarn: false,
inactivityTime: 0,
effectiveTimeout: 0
};
}
const now = Date.now();
const inactivityTime = now - record.lastActivity;
const effectiveTimeout = this.getEffectiveInactivityTimeout(record);
// Check if inactive
const isInactive = inactivityTime > effectiveTimeout;
// For keep-alive connections, we should warn first
const shouldWarn = record.hasKeepAlive &&
isInactive &&
!record.inactivityWarningIssued;
return {
isInactive,
shouldWarn,
inactivityTime,
effectiveTimeout
};
}
/**
* Apply socket timeout settings
*/
public applySocketTimeouts(record: IConnectionRecord): void {
// Skip for immortal keep-alive connections
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
// Disable timeouts completely for immortal connections
record.incoming.setTimeout(0);
if (record.outgoing) {
record.outgoing.setTimeout(0);
}
return;
}
// Apply normal timeouts
const timeout = this.ensureSafeTimeout(this.settings.socketTimeout || 3600000); // 1 hour default
record.incoming.setTimeout(timeout);
if (record.outgoing) {
record.outgoing.setTimeout(timeout);
}
}
}

258
ts/classes.pp.tlsalert.ts Normal file
View File

@ -0,0 +1,258 @@
import * as net from 'net';
/**
* TlsAlert class for managing TLS alert messages
*/
export class TlsAlert {
// TLS Alert Levels
static readonly LEVEL_WARNING = 0x01;
static readonly LEVEL_FATAL = 0x02;
// TLS Alert Description Codes - RFC 8446 (TLS 1.3) / RFC 5246 (TLS 1.2)
static readonly CLOSE_NOTIFY = 0x00;
static readonly UNEXPECTED_MESSAGE = 0x0A;
static readonly BAD_RECORD_MAC = 0x14;
static readonly DECRYPTION_FAILED = 0x15; // TLS 1.0 only
static readonly RECORD_OVERFLOW = 0x16;
static readonly DECOMPRESSION_FAILURE = 0x1E; // TLS 1.2 and below
static readonly HANDSHAKE_FAILURE = 0x28;
static readonly NO_CERTIFICATE = 0x29; // SSLv3 only
static readonly BAD_CERTIFICATE = 0x2A;
static readonly UNSUPPORTED_CERTIFICATE = 0x2B;
static readonly CERTIFICATE_REVOKED = 0x2C;
static readonly CERTIFICATE_EXPIRED = 0x2F;
static readonly CERTIFICATE_UNKNOWN = 0x30;
static readonly ILLEGAL_PARAMETER = 0x2F;
static readonly UNKNOWN_CA = 0x30;
static readonly ACCESS_DENIED = 0x31;
static readonly DECODE_ERROR = 0x32;
static readonly DECRYPT_ERROR = 0x33;
static readonly EXPORT_RESTRICTION = 0x3C; // TLS 1.0 only
static readonly PROTOCOL_VERSION = 0x46;
static readonly INSUFFICIENT_SECURITY = 0x47;
static readonly INTERNAL_ERROR = 0x50;
static readonly INAPPROPRIATE_FALLBACK = 0x56;
static readonly USER_CANCELED = 0x5A;
static readonly NO_RENEGOTIATION = 0x64; // TLS 1.2 and below
static readonly MISSING_EXTENSION = 0x6D; // TLS 1.3
static readonly UNSUPPORTED_EXTENSION = 0x6E; // TLS 1.3
static readonly CERTIFICATE_REQUIRED = 0x6F; // TLS 1.3
static readonly UNRECOGNIZED_NAME = 0x70;
static readonly BAD_CERTIFICATE_STATUS_RESPONSE = 0x71;
static readonly BAD_CERTIFICATE_HASH_VALUE = 0x72; // TLS 1.2 and below
static readonly UNKNOWN_PSK_IDENTITY = 0x73;
static readonly CERTIFICATE_REQUIRED_1_3 = 0x74; // TLS 1.3
static readonly NO_APPLICATION_PROTOCOL = 0x78;
/**
* Create a TLS alert buffer with the specified level and description code
*
* @param level Alert level (warning or fatal)
* @param description Alert description code
* @param tlsVersion TLS version bytes (default is TLS 1.2: 0x0303)
* @returns Buffer containing the TLS alert message
*/
static create(
level: number,
description: number,
tlsVersion: [number, number] = [0x03, 0x03]
): Buffer {
return Buffer.from([
0x15, // Alert record type
tlsVersion[0],
tlsVersion[1], // TLS version (default to TLS 1.2: 0x0303)
0x00,
0x02, // Length
level, // Alert level
description, // Alert description
]);
}
/**
* Create a warning-level TLS alert
*
* @param description Alert description code
* @returns Buffer containing the warning-level TLS alert message
*/
static createWarning(description: number): Buffer {
return this.create(this.LEVEL_WARNING, description);
}
/**
* Create a fatal-level TLS alert
*
* @param description Alert description code
* @returns Buffer containing the fatal-level TLS alert message
*/
static createFatal(description: number): Buffer {
return this.create(this.LEVEL_FATAL, description);
}
/**
* Send a TLS alert to a socket and optionally close the connection
*
* @param socket The socket to send the alert to
* @param level Alert level (warning or fatal)
* @param description Alert description code
* @param closeAfterSend Whether to close the connection after sending the alert
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
* @returns Promise that resolves when the alert has been sent
*/
static async send(
socket: net.Socket,
level: number,
description: number,
closeAfterSend: boolean = false,
closeDelay: number = 200
): Promise<void> {
const alert = this.create(level, description);
return new Promise<void>((resolve, reject) => {
try {
// Ensure the alert is written as a single packet
socket.cork();
const writeSuccessful = socket.write(alert, (err) => {
if (err) {
reject(err);
return;
}
if (closeAfterSend) {
setTimeout(() => {
socket.end();
resolve();
}, closeDelay);
} else {
resolve();
}
});
socket.uncork();
// If write wasn't successful immediately, wait for drain
if (!writeSuccessful && !closeAfterSend) {
socket.once('drain', () => {
resolve();
});
}
} catch (err) {
reject(err);
}
});
}
/**
* Pre-defined TLS alert messages
*/
static readonly alerts = {
// Warning level alerts
closeNotify: TlsAlert.createWarning(TlsAlert.CLOSE_NOTIFY),
unsupportedExtension: TlsAlert.createWarning(TlsAlert.UNSUPPORTED_EXTENSION),
certificateRequired: TlsAlert.createWarning(TlsAlert.CERTIFICATE_REQUIRED),
unrecognizedName: TlsAlert.createWarning(TlsAlert.UNRECOGNIZED_NAME),
noRenegotiation: TlsAlert.createWarning(TlsAlert.NO_RENEGOTIATION),
userCanceled: TlsAlert.createWarning(TlsAlert.USER_CANCELED),
// Warning level alerts for session resumption
certificateExpiredWarning: TlsAlert.createWarning(TlsAlert.CERTIFICATE_EXPIRED),
handshakeFailureWarning: TlsAlert.createWarning(TlsAlert.HANDSHAKE_FAILURE),
insufficientSecurityWarning: TlsAlert.createWarning(TlsAlert.INSUFFICIENT_SECURITY),
// Fatal level alerts
unexpectedMessage: TlsAlert.createFatal(TlsAlert.UNEXPECTED_MESSAGE),
badRecordMac: TlsAlert.createFatal(TlsAlert.BAD_RECORD_MAC),
recordOverflow: TlsAlert.createFatal(TlsAlert.RECORD_OVERFLOW),
handshakeFailure: TlsAlert.createFatal(TlsAlert.HANDSHAKE_FAILURE),
badCertificate: TlsAlert.createFatal(TlsAlert.BAD_CERTIFICATE),
certificateExpired: TlsAlert.createFatal(TlsAlert.CERTIFICATE_EXPIRED),
certificateUnknown: TlsAlert.createFatal(TlsAlert.CERTIFICATE_UNKNOWN),
illegalParameter: TlsAlert.createFatal(TlsAlert.ILLEGAL_PARAMETER),
unknownCA: TlsAlert.createFatal(TlsAlert.UNKNOWN_CA),
accessDenied: TlsAlert.createFatal(TlsAlert.ACCESS_DENIED),
decodeError: TlsAlert.createFatal(TlsAlert.DECODE_ERROR),
decryptError: TlsAlert.createFatal(TlsAlert.DECRYPT_ERROR),
protocolVersion: TlsAlert.createFatal(TlsAlert.PROTOCOL_VERSION),
insufficientSecurity: TlsAlert.createFatal(TlsAlert.INSUFFICIENT_SECURITY),
internalError: TlsAlert.createFatal(TlsAlert.INTERNAL_ERROR),
unrecognizedNameFatal: TlsAlert.createFatal(TlsAlert.UNRECOGNIZED_NAME),
};
/**
* Utility method to send a warning-level unrecognized_name alert
* Specifically designed for SNI issues to encourage the client to retry with SNI
*
* @param socket The socket to send the alert to
* @returns Promise that resolves when the alert has been sent
*/
static async sendSniRequired(socket: net.Socket): Promise<void> {
return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME);
}
/**
* Utility method to send a close_notify alert and close the connection
*
* @param socket The socket to send the alert to
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
* @returns Promise that resolves when the alert has been sent and the connection closed
*/
static async sendCloseNotify(socket: net.Socket, closeDelay: number = 200): Promise<void> {
return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay);
}
/**
* Utility method to send a certificate_expired alert to force new TLS session
*
* @param socket The socket to send the alert to
* @param fatal Whether to send as a fatal alert (default: false)
* @param closeAfterSend Whether to close the connection after sending the alert (default: true)
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
* @returns Promise that resolves when the alert has been sent
*/
static async sendCertificateExpired(
socket: net.Socket,
fatal: boolean = false,
closeAfterSend: boolean = true,
closeDelay: number = 200
): Promise<void> {
const level = fatal ? this.LEVEL_FATAL : this.LEVEL_WARNING;
return this.send(socket, level, this.CERTIFICATE_EXPIRED, closeAfterSend, closeDelay);
}
/**
* Send a sequence of alerts to force SNI from clients
* This combines multiple alerts to ensure maximum browser compatibility
*
* @param socket The socket to send the alerts to
* @returns Promise that resolves when all alerts have been sent
*/
static async sendForceSniSequence(socket: net.Socket): Promise<void> {
try {
// Send unrecognized_name (warning)
socket.cork();
socket.write(this.alerts.unrecognizedName);
socket.uncork();
// Give the socket time to send the alert
return new Promise((resolve) => {
setTimeout(resolve, 50);
});
} catch (err) {
return Promise.reject(err);
}
}
/**
* Send a fatal level alert that immediately terminates the connection
*
* @param socket The socket to send the alert to
* @param description Alert description code
* @param closeDelay Milliseconds to wait before closing the connection (default: 100ms)
* @returns Promise that resolves when the alert has been sent and the connection closed
*/
static async sendFatalAndClose(
socket: net.Socket,
description: number,
closeDelay: number = 100
): Promise<void> {
return this.send(socket, this.LEVEL_FATAL, description, true, closeDelay);
}
}

206
ts/classes.pp.tlsmanager.ts Normal file
View File

@ -0,0 +1,206 @@
import * as plugins from './plugins.js';
import type { IPortProxySettings } from './classes.pp.interfaces.js';
import { SniHandler } from './classes.pp.snihandler.js';
/**
* Interface for connection information used for SNI extraction
*/
interface IConnectionInfo {
sourceIp: string;
sourcePort: number;
destIp: string;
destPort: number;
}
/**
* Manages TLS-related operations including SNI extraction and validation
*/
export class TlsManager {
constructor(private settings: IPortProxySettings) {}
/**
* Check if a data chunk appears to be a TLS handshake
*/
public isTlsHandshake(chunk: Buffer): boolean {
return SniHandler.isTlsHandshake(chunk);
}
/**
* Check if a data chunk appears to be a TLS ClientHello
*/
public isClientHello(chunk: Buffer): boolean {
return SniHandler.isClientHello(chunk);
}
/**
* Extract Server Name Indication (SNI) from TLS handshake
*/
public extractSNI(
chunk: Buffer,
connInfo: IConnectionInfo,
previousDomain?: string
): string | undefined {
// Use the SniHandler to process the TLS packet
return SniHandler.processTlsPacket(
chunk,
connInfo,
this.settings.enableTlsDebugLogging || false,
previousDomain
);
}
/**
* Handle session resumption attempts
*/
public handleSessionResumption(
chunk: Buffer,
connectionId: string,
hasSNI: boolean
): { shouldBlock: boolean; reason?: string } {
// Skip if session tickets are allowed
if (this.settings.allowSessionTicket !== false) {
return { shouldBlock: false };
}
// Check for session resumption attempt
const resumptionInfo = SniHandler.hasSessionResumption(
chunk,
this.settings.enableTlsDebugLogging || false
);
// If this is a resumption attempt without SNI, block it
if (resumptionInfo.isResumption && !hasSNI && !resumptionInfo.hasSNI) {
if (this.settings.enableTlsDebugLogging) {
console.log(
`[${connectionId}] Session resumption detected without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.`
);
}
return {
shouldBlock: true,
reason: 'session_ticket_blocked'
};
}
return { shouldBlock: false };
}
/**
* Check for SNI mismatch during renegotiation
*/
public checkRenegotiationSNI(
chunk: Buffer,
connInfo: IConnectionInfo,
expectedDomain: string,
connectionId: string
): { hasMismatch: boolean; extractedSNI?: string } {
// Only process if this looks like a TLS ClientHello
if (!this.isClientHello(chunk)) {
return { hasMismatch: false };
}
try {
// Extract SNI with renegotiation support
const newSNI = SniHandler.extractSNIWithResumptionSupport(
chunk,
connInfo,
this.settings.enableTlsDebugLogging || false
);
// Skip if no SNI was found
if (!newSNI) return { hasMismatch: false };
// Check for SNI mismatch
if (newSNI !== expectedDomain) {
if (this.settings.enableTlsDebugLogging) {
console.log(
`[${connectionId}] Renegotiation with different SNI: ${expectedDomain} -> ${newSNI}. ` +
`Terminating connection - SNI domain switching is not allowed.`
);
}
return { hasMismatch: true, extractedSNI: newSNI };
} else if (this.settings.enableTlsDebugLogging) {
console.log(
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
);
}
} catch (err) {
console.log(
`[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
);
}
return { hasMismatch: false };
}
/**
* Create a renegotiation handler function for a connection
*/
public createRenegotiationHandler(
connectionId: string,
lockedDomain: string,
connInfo: IConnectionInfo,
onMismatch: (connectionId: string, reason: string) => void
): (chunk: Buffer) => void {
return (chunk: Buffer) => {
const result = this.checkRenegotiationSNI(chunk, connInfo, lockedDomain, connectionId);
if (result.hasMismatch) {
onMismatch(connectionId, 'sni_mismatch');
}
};
}
/**
* Analyze TLS connection for browser fingerprinting
* This helps identify browser vs non-browser connections
*/
public analyzeClientHello(chunk: Buffer): {
isBrowserConnection: boolean;
isRenewal: boolean;
hasSNI: boolean;
} {
// Default result
const result = {
isBrowserConnection: false,
isRenewal: false,
hasSNI: false
};
try {
// Check if it's a ClientHello
if (!this.isClientHello(chunk)) {
return result;
}
// Check for session resumption
const resumptionInfo = SniHandler.hasSessionResumption(
chunk,
this.settings.enableTlsDebugLogging || false
);
// Extract SNI
const sni = SniHandler.extractSNI(
chunk,
this.settings.enableTlsDebugLogging || false
);
// Update result
result.isRenewal = resumptionInfo.isResumption;
result.hasSNI = !!sni;
// Browsers typically:
// 1. Send SNI extension
// 2. Have a variety of extensions (ALPN, etc.)
// 3. Use standard cipher suites
// ...more complex heuristics could be implemented here
// Simple heuristic: presence of SNI suggests browser
result.isBrowserConnection = !!sni;
return result;
} catch (err) {
console.log(`Error analyzing ClientHello: ${err}`);
return result;
}
}
}

432
ts/classes.router.ts Normal file
View File

@ -0,0 +1,432 @@
import * as http from 'http';
import * as url from 'url';
import * as tsclass from '@tsclass/tsclass';
/**
* Optional path pattern configuration that can be added to proxy configs
*/
export interface IPathPatternConfig {
pathPattern?: string;
}
/**
* Interface for router result with additional metadata
*/
export interface IRouterResult {
config: tsclass.network.IReverseProxyConfig;
pathMatch?: string;
pathParams?: Record<string, string>;
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[] = [];
// Default config to use when no match is found (optional)
private defaultConfig?: tsclass.network.IReverseProxyConfig;
// Store path patterns separately since they're not in the original interface
private pathPatterns: Map<tsclass.network.IReverseProxyConfig, string> = new Map();
// Logger interface
private logger: {
error: (message: string, data?: any) => void;
warn: (message: string, data?: any) => void;
info: (message: string, data?: any) => void;
debug: (message: string, data?: any) => void;
};
constructor(
configs?: tsclass.network.IReverseProxyConfig[],
logger?: {
error: (message: string, data?: any) => void;
warn: (message: string, data?: any) => void;
info: (message: string, data?: any) => void;
debug: (message: string, data?: any) => void;
}
) {
this.logger = logger || console;
if (configs) {
this.setNewProxyConfigs(configs);
}
}
/**
* Sets a new set of reverse configs to be routed to
* @param reverseCandidatesArg Array of reverse proxy configurations
*/
public setNewProxyConfigs(reverseCandidatesArg: tsclass.network.IReverseProxyConfig[]): void {
this.reverseProxyConfigs = [...reverseCandidatesArg];
// Find default config if any (config with "*" as hostname)
this.defaultConfig = this.reverseProxyConfigs.find(config => config.hostName === '*');
this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.getHostnames().length} unique hosts)`);
}
/**
* Routes a request based on hostname and path
* @param req The incoming HTTP request
* @returns The matching proxy config or undefined if no match found
*/
public routeReq(req: http.IncomingMessage): tsclass.network.IReverseProxyConfig {
const result = this.routeReqWithDetails(req);
return result ? result.config : undefined;
}
/**
* Routes a request with detailed matching information
* @param req The incoming HTTP request
* @returns Detailed routing result including matched config and path information
*/
public routeReqWithDetails(req: http.IncomingMessage): IRouterResult | undefined {
// Extract and validate host header
const originalHost = req.headers.host;
if (!originalHost) {
this.logger.error('No host header found in request');
return this.defaultConfig ? { config: this.defaultConfig } : undefined;
}
// Parse URL for path matching
const parsedUrl = url.parse(req.url || '/');
const urlPath = parsedUrl.pathname || '/';
// Extract hostname without port
const hostWithoutPort = originalHost.split(':')[0].toLowerCase();
// First try exact hostname match
const exactConfig = this.findConfigForHost(hostWithoutPort, urlPath);
if (exactConfig) {
return exactConfig;
}
// 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);
if (wildcardConfig) {
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
if (this.defaultConfig) {
this.logger.warn(`No specific config found for host: ${hostWithoutPort}, using default`);
return { config: this.defaultConfig };
}
this.logger.error(`No config found for host: ${hostWithoutPort}`);
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
*/
private findConfigForHost(hostname: string, path: string): IRouterResult | undefined {
// Find all configs for this hostname
const configs = this.reverseProxyConfigs.filter(
config => config.hostName.toLowerCase() === hostname.toLowerCase()
);
if (configs.length === 0) {
return undefined;
}
// First try configs with path patterns
const configsWithPaths = configs.filter(config => this.pathPatterns.has(config));
// Sort by path pattern specificity - more specific first
configsWithPaths.sort((a, b) => {
const aPattern = this.pathPatterns.get(a) || '';
const bPattern = this.pathPatterns.get(b) || '';
// Exact patterns come before wildcard patterns
const aHasWildcard = aPattern.includes('*');
const bHasWildcard = bPattern.includes('*');
if (aHasWildcard && !bHasWildcard) return 1;
if (!aHasWildcard && bHasWildcard) return -1;
// Longer patterns are considered more specific
return bPattern.length - aPattern.length;
});
// Check each config with path pattern
for (const config of configsWithPaths) {
const pathPattern = this.pathPatterns.get(config);
if (pathPattern) {
const pathMatch = this.matchPath(path, pathPattern);
if (pathMatch) {
return {
config,
pathMatch: pathMatch.matched,
pathParams: pathMatch.params,
pathRemainder: pathMatch.remainder
};
}
}
}
// If no path pattern matched, use the first config without a path pattern
const configWithoutPath = configs.find(config => !this.pathPatterns.has(config));
if (configWithoutPath) {
return { config: configWithoutPath };
}
return undefined;
}
/**
* Matches a URL path against a pattern
* Supports:
* - Exact matches: /users/profile
* - Wildcards: /api/* (matches any path starting with /api/)
* - Path parameters: /users/:id (captures id as a parameter)
*
* @param path The URL path to match
* @param pattern The pattern to match against
* @returns Match result with params and remainder, or null if no match
*/
private matchPath(path: string, pattern: string): {
matched: string;
params: Record<string, string>;
remainder: string;
} | null {
// Handle exact match
if (path === pattern) {
return {
matched: pattern,
params: {},
remainder: ''
};
}
// Handle wildcard match
if (pattern.endsWith('/*')) {
const prefix = pattern.slice(0, -2);
if (path === prefix || path.startsWith(`${prefix}/`)) {
return {
matched: prefix,
params: {},
remainder: path.slice(prefix.length)
};
}
return null;
}
// Handle path parameters
const patternParts = pattern.split('/').filter(p => p);
const pathParts = path.split('/').filter(p => p);
// Too few path parts to match
if (pathParts.length < patternParts.length) {
return null;
}
const params: Record<string, string> = {};
// Compare each part
for (let i = 0; i < patternParts.length; i++) {
const patternPart = patternParts[i];
const pathPart = pathParts[i];
// Handle parameter
if (patternPart.startsWith(':')) {
const paramName = patternPart.slice(1);
params[paramName] = pathPart;
continue;
}
// Handle wildcard at the end
if (patternPart === '*' && i === patternParts.length - 1) {
break;
}
// Handle exact match for this part
if (patternPart !== pathPart) {
return null;
}
}
// Calculate the remainder - the unmatched path parts
const remainderParts = pathParts.slice(patternParts.length);
const remainder = remainderParts.length ? '/' + remainderParts.join('/') : '';
// Calculate the matched path
const matchedParts = patternParts.map((part, i) => {
return part.startsWith(':') ? pathParts[i] : part;
});
const matched = '/' + matchedParts.join('/');
return {
matched,
params,
remainder
};
}
/**
* Gets all currently active proxy configurations
* @returns Array of all active configurations
*/
public getProxyConfigs(): tsclass.network.IReverseProxyConfig[] {
return [...this.reverseProxyConfigs];
}
/**
* Gets all hostnames that this router is configured to handle
* @returns Array of hostnames
*/
public getHostnames(): string[] {
const hostnames = new Set<string>();
for (const config of this.reverseProxyConfigs) {
if (config.hostName !== '*') {
hostnames.add(config.hostName.toLowerCase());
}
}
return Array.from(hostnames);
}
/**
* Adds a single new proxy configuration
* @param config The configuration to add
* @param pathPattern Optional path pattern for route matching
*/
public addProxyConfig(
config: tsclass.network.IReverseProxyConfig,
pathPattern?: string
): void {
this.reverseProxyConfigs.push(config);
// Store path pattern if provided
if (pathPattern) {
this.pathPatterns.set(config, pathPattern);
}
}
/**
* Sets a path pattern for an existing config
* @param config The existing configuration
* @param pathPattern The path pattern to set
* @returns Boolean indicating if the config was found and updated
*/
public setPathPattern(
config: tsclass.network.IReverseProxyConfig,
pathPattern: string
): boolean {
const exists = this.reverseProxyConfigs.includes(config);
if (exists) {
this.pathPatterns.set(config, pathPattern);
return true;
}
return false;
}
/**
* Removes a proxy configuration by hostname
* @param hostname The hostname to remove
* @returns Boolean indicating whether any configs were removed
*/
public removeProxyConfig(hostname: string): boolean {
const initialCount = this.reverseProxyConfigs.length;
// Find configs to remove
const configsToRemove = this.reverseProxyConfigs.filter(
config => config.hostName === hostname
);
// Remove them from the patterns map
for (const config of configsToRemove) {
this.pathPatterns.delete(config);
}
// Filter them out of the configs array
this.reverseProxyConfigs = this.reverseProxyConfigs.filter(
config => config.hostName !== hostname
);
return this.reverseProxyConfigs.length !== initialCount;
}
}

32
ts/classes.sslredirect.ts Normal file
View File

@ -0,0 +1,32 @@
import * as plugins from './plugins.js';
export class SslRedirect {
httpServer: plugins.http.Server;
port: number;
constructor(portArg: number) {
this.port = portArg;
}
public async start() {
this.httpServer = plugins.http.createServer((request, response) => {
const requestUrl = new URL(request.url, `http://${request.headers.host}`);
const completeUrlWithoutProtocol = `${requestUrl.host}${requestUrl.pathname}${requestUrl.search}`;
const redirectUrl = `https://${completeUrlWithoutProtocol}`;
console.log(`Got http request for http://${completeUrlWithoutProtocol}`);
console.log(`Redirecting to ${redirectUrl}`);
response.writeHead(302, {
Location: redirectUrl,
});
response.end();
});
this.httpServer.listen(this.port);
}
public async stop() {
const done = plugins.smartpromise.defer();
this.httpServer.close(() => {
done.resolve();
});
await done.promise;
}
}

View File

@ -0,0 +1,30 @@
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export interface ICertificates {
privateKey: string;
publicKey: string;
}
export function loadDefaultCertificates(): ICertificates {
try {
const certPath = path.join(__dirname, '..', 'assets', 'certs');
const privateKey = fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8');
const publicKey = fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8');
if (!privateKey || !publicKey) {
throw new Error('Failed to load default certificates');
}
return {
privateKey,
publicKey
};
} catch (error) {
console.error('Error loading default certificates:', error);
throw error;
}
}

View File

@ -1,2 +1,6 @@
export * from './smartproxy.classes.networkproxy.js'
export * from './smartproxy.portproxy.js';
export * from './classes.iptablesproxy.js';
export * from './classes.networkproxy.js';
export * from './classes.pp.portproxy.js';
export * from './classes.port80handler.js';
export * from './classes.sslredirect.js';
export * from './classes.pp.snihandler.js';

33
ts/plugins.ts Normal file
View File

@ -0,0 +1,33 @@
// node native scope
import { EventEmitter } from 'events';
import * as http from 'http';
import * as https from 'https';
import * as net from 'net';
import * as tls from 'tls';
import * as url from 'url';
export { EventEmitter, http, https, net, tls, url };
// tsclass scope
import * as tsclass from '@tsclass/tsclass';
export { tsclass };
// pushrocks scope
import * as lik from '@push.rocks/lik';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartstring from '@push.rocks/smartstring';
export { lik, smartdelay, smartrequest, smartpromise, smartstring };
// third party scope
import * as acme from 'acme-client';
import prettyMs from 'pretty-ms';
import * as ws from 'ws';
import wsDefault from 'ws';
import { minimatch } from 'minimatch';
export { acme, prettyMs, ws, wsDefault, minimatch };

View File

@ -1,323 +0,0 @@
import * as plugins from './smartproxy.plugins.js';
import { ProxyRouter } from './smartproxy.classes.router.js';
export interface INetworkProxyOptions {
port: number;
}
export class NetworkProxy {
// INSTANCE
public options: INetworkProxyOptions;
public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
public httpsServer: plugins.https.Server;
public router = new ProxyRouter();
public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
public defaultHeaders: { [key: string]: string } = {};
public alreadyAddedReverseConfigs: {
[hostName: string]: plugins.tsclass.network.IReverseProxyConfig;
} = {};
constructor(optionsArg: INetworkProxyOptions) {
this.options = optionsArg;
}
/**
* starts the proxyInstance
*/
public async start() {
this.httpsServer = plugins.https.createServer(
// ================
// Spotted this keypair in the code?
// Don't get exited:
// It is an invalid default keypair.
// For proper requests custom domain level keypairs are used that are provided in the reverse config
// ================
{
key: `-----BEGIN PRIVATE KEY-----
MIIJRQIBADANBgkqhkiG9w0BAQEFAASCCS8wggkrAgEAAoICAQDi2F/0kQr96mhe
3yEWvy2mRHOZoSSBtIqg6Bre4ZcMu901/cHNIjFnynNGFl9Se61yZbW2F3PfCt7+
kQlHug1Cx+LFssvz+hLlB5cqJQZfRKx92DhbROygtxG9r7UBmx/fwx+JQ+HOHX9R
b+szLBZqxrNDBFl2SRdviconYgVnHbaqcAPj/lK6D6x94qgUEX+vMjbIruuiCe3u
RbYse/quzAednVnY/+BuGVn8SEb2EVVFnBEsOxxYpy5ZzGR48O3YnWkM2oPpJhrp
mMYLcARMnDmIQDVstD1i+MM2lVhx/pm9xKKUgWNJC7lyz2xRscZ4pOtLkfN94leH
U98nIvxfQe7tQFKN9K52yjdtoT0UaIEUFbZyddkoNka1Xx6r+rE96046BLT2lVs0
/rnTxZUFH6vP3z9UNktmpxtnZSk67Pj6QAqZtgT0amXEpBlk7vBYSjHsyJ3+5R1y
oSjhAqeejq6M67NDOflrag5LSTkeTe4dqk0laVb1gjcse18AOlgf7pw5H79zclYH
NAnoAPua683MD2ZZd4eovEww/imSZvui3NlisSSh1SomABDFxiEaHpewI98n8P1E
3vfg4lyCV5VcUjwrPjnkfEJbX1c1/PXqTtPqSqFn/pI4FuTES6qDugS2EA/XT1ln
ODHigOiFCzDbhOMuQjhI8hzuevrRRQIDAQABAoICAQC7nU+HW6qmpQebZ5nbUVT1
Deo6Js+lwudg+3a13ghqzLnBXNW7zkrkV8mNLxW5h3bFhZ+LMcxwrXIPQ29Udmlf
USiacC1E5RBZgjSg86xYgNjU4E6EFfZLWf3/T2I6KM1s6NmdUppgOX9CoHj7grwr
pZk/lUpUjVEnu+OJPQXQ6f9Y6XoeSAqtvibgmuR+bJaZFMPAqQNTqjix99Aa7JNB
nJez4R8dXUuGY8tL349pFp7bCqAdX+oq3GJ2fJigekuM+2uV6OhunUhm6Sbq8MNt
hUwEB27oMA4RXENAUraq2XLYQ9hfUMAH+v1vGmSxEIJg561/e//RnrDbyR9oJARr
SbopI3Ut5yKxVKMYOTSqcFQXVLszTExhMhQCRoOh58BpIfhb9FLCKD9LH8E6eoQf
ygPWryey9AAJ7B2PQXVbitzcOML27rzC4DXS+mLe6AVL6t2IldaeMTlumlnc620d
Yuf5wSe8qe4xpKOlrE9emnBmbL0sGivsU+mpz9oSjxEpHGA7eoTIOmQiZnuzpkmi
1ZSU4OwqNavphy6cklONShQOmE8LMI0wRbunLjIFY8fme/8u+tVvWrTuJiCGPnXQ
F2lb0qwtDVRlexyM+GTPYstU5v7HxkQB3B+uwTgYuupCmTNmO8hjSCS/EYpHzmFe
YHDEN+Cj8f+vmKxN0F/6QQKCAQEA9+wTQU2GSoVX8IB0U6T+hX0BFhQq5ISH/s76
kWIEunY1MCkRL9YygvHkKW3dsXVOzsip/axiT36MhRcyZ27hF1tz3j//Z11E3Bfq
PkzyUVuU3jpWZkBE2VhXpDXlyW8xR/y1ZOaZZ//XcZTrZf57pGKFp30H/PlDPH3C
YtjEuQNmPCgnfz8iXx+vDYx8hwLHNv+DoX2WYuThUnul/QGSKL3xh3qWd8rotnUB
c8bV4ymk35fVJu/+pTZpPnMkYrFReso/uNn07y1iga/9mwkUBNrT+fWE7RzjT7H8
ykMMOGCK6bc7joCvALZaUDne714hNW3s9a7L1clehUA8/xwplQKCAQEA6jx/CIQd
RVdJFihSSZbqdrOAblVdl+WkjhALWNRMoRCCRniNubbgxgKfQ0scKUeubYxScBVk
rlUMl6/2Gr9uzuSC0WPVAE6OLvLNcQafw1mQ1UTJiEzYvczJKwipzXcgGQWO9Q9a
T3ETh6Be62si2r6fH4agQzbp4HkTEoWgPu6MJpqqcLoc8laty0d1huqU9du1TRzT
3etjopWRd0I3ID+WkkGKjYWRQ1bkKjvkkj1v7bHenX17nfIp5WU1aXTMYUCMMszm
pgVBDeJGKpPpP3scl7go5Y4KC6H+IeYaeCEk3hWW4robpHBzupkgpRLzmBopjRlN
v3+HQ7OkviX88QKCAQEAg5IJdfKKfindzYieM3WwjW8VkH4LdVLQSW3WlCkMkVgC
ShjBQj3OeKeeik4ABRlYRW1AqZs+YSmrsUXqPfIeCqNCDoSwKk7ZKGSYr49uWbbc
fkM/buxUnXPAryjbVddos+ds7KtkZkjkMSby9iHjxA11GLnF737pK8Uh0Atx+y3O
p8Y3j9QVjZ3m7K3NuGjFCG75kE5x7PHCkl+Ea4zV4EFNWLS5/cD1Vz8pEiRHhlKn
aPHO8OcUoOELYVUBzk6EC0IiJxukXPoc+O5JDGn48cqgDFs7vApEqBqxKTYD2jeC
AR54wNuSBDLCIylTIn016oD37DpjeoVvYBADTu/HMQKCAQEA1rFuajrVrWnMpo98
pNC7xOLQM9DwwToOMtwH2np0ZiiAj+ENXgx+R1+95Gsiu79k5Cn6oZsqNhPkP+Bb
fba69M1EDnInmGloLyYDIbbFlsMwWhn7cn+lJYpfVJ9TK+0lMWoD1yAkUa4+DVDz
z2naf466wKWfnRvnEAVJcu+hqizxrqySzlH4GDNUhn7P/UJkGFkx+yUSGFUZdLsM
orfBWUCPXSzPttmXBJbO+Nr+rP+86KvgdI/AT0vYFNdINomEjxsfpaxjOAaW0wfz
8jCyWKoZ0gJNEeK32GO5UA7dcgBHD3vQWa3lijo8COsznboaJe7M6PQpa/2S2H3+
4P5msQKCAQEAx7NP3y+5ttfTd/eQ7/cg1/0y2WxvpOYNLt6MWz4rPWyD6QwidzTG
pjuQFQ5Ods+BwJ/Jbirb7l4GMAxfIbEPAkPTHpvswO0xcncSYxl0sSP/WIA6sbcM
dp7B/scdORC8Y6i8oPdCyxyCTd2SBrmGr2krAXmQquT72eusyP5E8HFhCy1iYt22
aL68dZLv9/sRAF08t9Wy+eYjD/hCj67t7uGCZQT8wJbKr8aJcjwVwJgghh+3EydK
h+7fBVO49PLL0NWy+8GT8y7a04calFfLvZEA2UMaunBis3dE1KMFfJL/0JO+sKnF
2TkK01XDDJURK5Lhuvc7WrK2rSJ/fK+0GA==
-----END PRIVATE KEY-----
`,
cert: `-----BEGIN CERTIFICATE-----
MIIEljCCAn4CCQDY+ZbC9FASVjANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJE
RTAeFw0xOTA5MjAxNjAxNDRaFw0yMDA5MTkxNjAxNDRaMA0xCzAJBgNVBAYTAkRF
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4thf9JEK/epoXt8hFr8t
pkRzmaEkgbSKoOga3uGXDLvdNf3BzSIxZ8pzRhZfUnutcmW1thdz3wre/pEJR7oN
QsfixbLL8/oS5QeXKiUGX0Ssfdg4W0TsoLcRva+1AZsf38MfiUPhzh1/UW/rMywW
asazQwRZdkkXb4nKJ2IFZx22qnAD4/5Sug+sfeKoFBF/rzI2yK7rognt7kW2LHv6
rswHnZ1Z2P/gbhlZ/EhG9hFVRZwRLDscWKcuWcxkePDt2J1pDNqD6SYa6ZjGC3AE
TJw5iEA1bLQ9YvjDNpVYcf6ZvcSilIFjSQu5cs9sUbHGeKTrS5HzfeJXh1PfJyL8
X0Hu7UBSjfSudso3baE9FGiBFBW2cnXZKDZGtV8eq/qxPetOOgS09pVbNP6508WV
BR+rz98/VDZLZqcbZ2UpOuz4+kAKmbYE9GplxKQZZO7wWEox7Mid/uUdcqEo4QKn
no6ujOuzQzn5a2oOS0k5Hk3uHapNJWlW9YI3LHtfADpYH+6cOR+/c3JWBzQJ6AD7
muvNzA9mWXeHqLxMMP4pkmb7otzZYrEkodUqJgAQxcYhGh6XsCPfJ/D9RN734OJc
gleVXFI8Kz455HxCW19XNfz16k7T6kqhZ/6SOBbkxEuqg7oEthAP109ZZzgx4oDo
hQsw24TjLkI4SPIc7nr60UUCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAu0+zrg0C
mlSv4Yi24OwB7TBvx+WHesl1IilCUdTiiUMo3NumvsU9Dr3Jkd0jGqYI0eyH4gIt
KrhAveXfEw7tAOEHiYicmAdIFtyzh++ZWb8mgbBeqij1MP/76Jv+cc0lUqpfRo/A
qytAsPAILuyL1o1jh28JHcq+v+WYn/FEhjUlH6emhGKGlsAjhUPjzK8MEshNolhj
t2UXw9WB5B2xWvrqlNMy0F3NAZBkZ/+k21HZo6FmVi+q6OEGcOo7wJt6wrH/lko9
LxX96GC1JoN1Pfr2FoTKy1WHzrSfyGmDIUCrbaYQ58UuMOR+5eIPPdkf/030u5eX
xXhF2fBujD57E2zQGh/l2MrOjamcSo0+wYhOqlX3WNdaKNAzPqloBnF6w7eqLYde
h9He39ySmxjENwv3miOjEP1sBeMBSRfL/ckEonfK5uJgYA5nVMQ3ojUeDMZzLfFE
Ue2WHt+uPyYk7mMZfOrK2uHzI2/Coqj7lbfRodFwj+fCArYBck2NZannDPKA6X8V
TzJTbTCteOUUJTrcfZ0gGhGkF4nYLmX5OI+TPqrDJf0fZ+mzAEHzDDVXcBYpYRDr
r8d9QwrK+WaqVi2ofbMfMByVF72jgeJNa4nxwT9bVbu/Q1T2Lt+YPb4pQ7yCoUgS
JNj2Dr5H0XoLFFnvuvzcRbhlJ9J67JzR+7g=
-----END CERTIFICATE-----
`,
},
async (req, res) => {
console.log('got request');
const destinationConfig = this.router.routeReq(req);
/**
* endRequest function
* can be used to prematurely end a request
*/
const endRequest = (
statusArg: number = 404,
messageArg: string = 'This route is not available on this server.',
headers: plugins.http.OutgoingHttpHeaders = {}
) => {
res.writeHead(statusArg, messageArg);
res.end(messageArg);
};
// authentication
if (destinationConfig.authentication) {
const authInfo = destinationConfig.authentication;
switch (authInfo.type) {
case 'Basic':
const authHeader = req.headers.authorization;
if (authHeader) {
if (!authHeader.includes('Basic ')) {
return endRequest(401, 'Authentication required', {
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
});
}
const authStringBase64 = req.headers.authorization.replace('Basic ', '');
const authString: string = plugins.smartstring.base64.decode(authStringBase64);
const userPassArray = authString.split(':');
const user = userPassArray[0];
const pass = userPassArray[1];
if (user === authInfo.user && pass === authInfo.pass) {
console.log('request successfully authenticated');
} else {
return endRequest(403, 'Forbidden: Wrong credentials');
}
}
break;
default:
return endRequest(
403,
'Forbidden: unsupported authentication method configured. Please report to the admin.'
);
}
}
let destinationUrl: string;
if (destinationConfig) {
destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${req.url}`;
} else {
return endRequest();
}
console.log(destinationUrl);
const response = await plugins.smartrequest.request(
destinationUrl,
{
method: req.method,
headers: req.headers,
keepAlive: true,
},
true, // lets make this streaming
(request) => {
req.on('data', (data) => {
request.write(data);
});
req.on('end', (data) => {
request.end();
});
}
);
res.statusCode = response.statusCode;
console.log(response.statusCode);
for (const defaultHeader of Object.keys(this.defaultHeaders)) {
res.setHeader(defaultHeader, this.defaultHeaders[defaultHeader]);
}
for (const header of Object.keys(response.headers)) {
res.setHeader(header, response.headers[header]);
}
response.on('data', (data) => {
res.write(data);
});
response.on('end', () => {
res.end();
});
}
);
// Enable websockets
const wss = new plugins.ws.WebSocketServer({ server: this.httpsServer });
wss.on('connection', (ws: plugins.wsDefault) => {
console.log('got connection for wsc');
const wscConnected = plugins.smartpromise.defer();
const wsc = new plugins.wsDefault(this.router.routeWs(ws), {
headers: {
Host: ws.url,
},
});
wsc.on('open', () => {
wscConnected.resolve();
});
ws.on('message', async (message) => {
await wscConnected.promise;
wsc.emit('message', message);
});
wsc.on('message', (message) => {
ws.emit('message', message);
});
// handle closing
const cleanUp = () => {
ws.removeAllListeners();
ws.close();
ws.terminate();
wsc.removeAllListeners();
wsc.close();
wsc.terminate();
};
ws.on('close', (message) => {
cleanUp();
});
wsc.on('close', (message) => {
cleanUp();
});
});
this.httpsServer.keepAliveTimeout = 61000;
this.httpsServer.headersTimeout = 65000;
this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
connection.setTimeout(120000);
this.socketMap.add(connection);
const cleanupConnection = () => {
if (this.socketMap.checkForObject(connection)) {
this.socketMap.remove(connection);
connection.end();
connection.removeAllListeners();
connection.destroy();
}
};
connection.on('close', () => {
cleanupConnection();
});
connection.on('error', () => {
cleanupConnection();
});
connection.on('end', () => {
cleanupConnection();
});
connection.on('timeout', () => {
cleanupConnection();
});
});
this.httpsServer.listen(this.options.port);
console.log(`ProxyWorker -> OK: now listening for new connections on port ${this.options.port}`);
}
public async updateProxyConfigs(proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]) {
this.proxyConfigs = proxyConfigsArg;
this.router.setNewProxyConfigs(proxyConfigsArg);
for (const hostCandidate of this.proxyConfigs) {
// console.log(hostCandidate);
const existingHostNameConfig = this.alreadyAddedReverseConfigs[hostCandidate.hostName];
if (!existingHostNameConfig) {
this.alreadyAddedReverseConfigs[hostCandidate.hostName] = hostCandidate;
} else {
if (
existingHostNameConfig.publicKey === hostCandidate.publicKey &&
existingHostNameConfig.privateKey === hostCandidate.privateKey
) {
continue;
} else {
this.alreadyAddedReverseConfigs[hostCandidate.hostName] = hostCandidate;
}
}
this.httpsServer.addContext(hostCandidate.hostName, {
cert: hostCandidate.publicKey,
key: hostCandidate.privateKey,
});
this.httpsServer;
}
/* this.httpsServer.close();
this.httpsServer.listen(this.port); */
}
public async stop() {
const done = plugins.smartpromise.defer();
this.httpsServer.close(() => {
done.resolve();
});
await this.socketMap.forEach(async (socket) => {
socket.destroy();
});
await done.promise;
}
}

View File

@ -1,32 +0,0 @@
import * as plugins from './smartproxy.plugins.js';
export class ProxyRouter {
public reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
/**
* sets a new set of reverse configs to be routed to
* @param reverseCandidatesArg
*/
public setNewProxyConfigs(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]) {
this.reverseProxyConfigs = reverseCandidatesArg;
}
/**
* routes a request
*/
public routeReq(req: plugins.http.IncomingMessage): plugins.tsclass.network.IReverseProxyConfig {
const originalHost = req.headers.host;
const correspodingReverseProxyConfig = this.reverseProxyConfigs.find((reverseConfig) => {
return reverseConfig.hostName === originalHost;
});
return correspodingReverseProxyConfig;
}
public routeWs(ws: plugins.wsDefault) {
const originalHost = plugins.url.parse(ws.url).host;
const correspodingReverseProxyConfig = this.reverseProxyConfigs.find((reverseConfig) => {
return reverseConfig.hostName === originalHost;
});
return correspodingReverseProxyConfig.destinationIp;
}
}

View File

@ -1,26 +0,0 @@
// node native scope
import * as http from 'http';
import * as https from 'https';
import * as net from 'net';
import * as url from 'url';
export { http, https, net, url };
// tsclass scope
import * as tsclass from '@tsclass/tsclass';
export { tsclass };
// pushrocks scope
import * as lik from '@pushrocks/lik';
import * as smartpromise from '@pushrocks/smartpromise';
import * as smartrequest from '@pushrocks/smartrequest';
import * as smartstring from '@pushrocks/smartstring';
export { lik, smartrequest, smartpromise, smartstring };
// third party scope
import * as ws from 'ws';
import wsDefault from 'ws';
export { wsDefault, ws };

View File

@ -1,85 +0,0 @@
import * as plugins from './smartproxy.plugins.js';
import * as net from 'net';
let netServer: plugins.net.Server;
let httpServer: plugins.http.Server;
export class PortProxy {
fromPort: number;
toPort: number;
constructor(fromPortArg: number, toPortArg: number) {
this.fromPort = fromPortArg;
this.toPort = toPortArg
}
public async start () {
httpServer = plugins.http.createServer((request, response) => {
const requestUrl = new URL(request.url, `http://${request.headers.host}`);
const completeUrlWithoutProtocol = `${requestUrl.host}${requestUrl.pathname}${requestUrl.search}`;
const redirectUrl = `https://${completeUrlWithoutProtocol}`;
console.log(`Got http request for http://${completeUrlWithoutProtocol}`);
console.log(`Redirecting to ${redirectUrl}`);
response.writeHead(302, {
Location: redirectUrl,
});
response.end();
});
httpServer.listen(this.fromPort);
const cleanUpSockets = (from: plugins.net.Socket, to: plugins.net.Socket) => {
from.end();
to.end();
from.removeAllListeners();
to.removeAllListeners();
from.unpipe();
to.unpipe();
from.destroy();
to.destroy();
}
netServer = net
.createServer((from) => {
const to = net.createConnection({
host: 'localhost',
port: this.toPort,
});
from.setTimeout(120000);
from.pipe(to);
to.pipe(from);
from.on('error', () => {
cleanUpSockets(from, to);
});
to.on('error', () => {
cleanUpSockets(from, to);
});
from.on('close', () => {
cleanUpSockets(from, to);
});
to.on('close', () => {
cleanUpSockets(from, to);
});
from.on('timeout', () => {
cleanUpSockets(from, to);
});
to.on('timeout', () => {
cleanUpSockets(from, to);
});
from.on('end', () => {
cleanUpSockets(from, to);
})
to.on('end', () => {
cleanUpSockets(from, to);
})
})
.listen(this.fromPort);
console.log(`PortProxy -> OK: Now listening on port ${this.fromPort}`);
}
public async stop() {
const done = plugins.smartpromise.defer();
httpServer.close(() => {
netServer.close(() => {
done.resolve();
});
});
await done.promise;
}
};

View File

@ -3,8 +3,14 @@
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "nodenext",
"esModuleInterop": true
}
}
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
},
"exclude": [
"dist_*/**/*.d.ts"
]
}