Compare commits

...

76 Commits

Author SHA1 Message Date
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
17 changed files with 1631 additions and 190 deletions

View File

@ -1,5 +1,249 @@
# Changelog
## 2025-02-27 - 3.16.5 - fix(PortProxy)
Improved connection cleanup process with added asynchronous delays
- Connection cleanup now includes asynchronous delays for reliable order of operations.
## 2025-02-27 - 3.16.4 - fix(PortProxy)
Fix and enhance port proxy handling
- Ensure that all created proxy servers are correctly checked for listening state.
- Corrected the handling of ports and domain configurations within port proxy setups.
- Expanded test coverage for handling multiple concurrent and chained proxy connections.
## 2025-02-27 - 3.16.3 - fix(PortProxy)
Refactored PortProxy to support multiple listening ports and improved modularity.
- Updated PortProxy to allow multiple listening ports with flexible configuration.
- Moved helper functions for IP and port range checks outside the class for cleaner code structure.
## 2025-02-27 - 3.16.2 - fix(PortProxy)
Fix port-based routing logic in PortProxy
- Optimized the handling and checking of local ports in the global port range.
- Fixed the logic for rejecting or accepting connections based on predefined port ranges.
- Improved handling of the default and specific domain configurations during port-based connections.
## 2025-02-27 - 3.16.1 - fix(core)
Updated minor version numbers in dependencies for patch release.
- No specific file changes detected.
- Dependencies versioning adjusted for stability.
## 2025-02-27 - 3.16.0 - feat(PortProxy)
Enhancements made to PortProxy settings and capabilities
- Added 'forwardAllGlobalRanges' and 'targetIP' to IPortProxySettings.
- Improved PortProxy to forward connections based on domain-specific configurations.
- Added comprehensive handling for global port-range based connection forwarding.
- Enabled forwarding of all connections on global port ranges directly to global target IP.
## 2025-02-27 - 3.15.0 - feat(classes.portproxy)
Add support for port range-based routing with enhanced IP and port validation.
- Introduced globalPortRanges in IPortProxySettings for routing based on port ranges.
- Improved connection handling with port range and domain configuration validations.
- Updated connection logging to include the local port information.
## 2025-02-26 - 3.14.2 - fix(PortProxy)
Fix cleanup timer reset for PortProxy
- Resolved an issue where the cleanup timer in the PortProxy class did not reset correctly if both incoming and outgoing data events were triggered without clearing flags.
## 2025-02-26 - 3.14.1 - fix(PortProxy)
Increased default maxConnectionLifetime for PortProxy to 600000 ms
- Updated PortProxy settings to extend default maxConnectionLifetime to 10 minutes.
## 2025-02-26 - 3.14.0 - feat(PortProxy)
Introduce max connection lifetime feature
- Added an optional maxConnectionLifetime setting for PortProxy.
- Forces cleanup of long-lived connections based on inactivity or lifetime limit.
## 2025-02-25 - 3.13.0 - feat(core)
Add support for tagging iptables rules with comments and cleaning them up on process exit
- Extended IPTablesProxy class to include tagging rules with unique comments.
- Added feature to clean up iptables rules via comments during process exit.
## 2025-02-24 - 3.12.0 - feat(IPTablesProxy)
Introduce IPTablesProxy class for managing iptables NAT rules
- Added IPTablesProxy class to facilitate basic port forwarding using iptables.
- Introduced IIpTableProxySettings interface for configuring IPTablesProxy.
- Implemented start and stop methods for managing iptables rules dynamically.
## 2025-02-24 - 3.11.0 - feat(Port80Handler)
Add automatic certificate issuance with ACME client
- Implemented automatic certificate issuance using 'acme-client' for Port80Handler.
- Converts account key and CSR from Buffers to strings for processing.
- Implemented HTTP-01 challenge handling for certificate acquisition.
- New certificates are fetched and added dynamically.
## 2025-02-24 - 3.10.5 - fix(portproxy)
Fix incorrect import path in test file
- Change import path from '../ts/smartproxy.portproxy.js' to '../ts/classes.portproxy.js' in test/test.portproxy.ts
## 2025-02-23 - 3.10.4 - fix(PortProxy)
Refactor connection tracking to utilize unified records in PortProxy
- Implemented a unified record system for tracking incoming and outgoing connections.
- Replaced individual connection tracking sets with a Set of IConnectionRecord.
- Improved logging of connection activities and statistics.
## 2025-02-23 - 3.10.3 - fix(PortProxy)
Refactor and optimize PortProxy for improved readability and maintainability
- Simplified and clarified inline comments.
- Optimized the extractSNI function for better readability.
- Streamlined the cleanup process for connections in PortProxy.
- Improved handling and logging of incoming and outgoing connections.
## 2025-02-23 - 3.10.2 - fix(PortProxy)
Fix connection handling to include timeouts for SNI-enabled connections.
- Added initial data timeout for SNI-enabled connections to improve connection handling.
- Cleared timeout once data is received to prevent premature socket closure.
## 2025-02-22 - 3.10.1 - fix(PortProxy)
Improve socket cleanup logic to prevent potential resource leaks
- Updated socket cleanup in PortProxy to ensure sockets are forcefully destroyed if not already destroyed.
## 2025-02-22 - 3.10.0 - feat(smartproxy.portproxy)
Enhance PortProxy with detailed connection statistics and termination tracking
- Added tracking of termination statistics for incoming and outgoing connections
- Enhanced logging to include detailed termination statistics
- Introduced helpers to update and log termination stats
- Retained detailed connection duration and active connection logging
## 2025-02-22 - 3.9.4 - fix(PortProxy)
Ensure proper cleanup on connection rejection in PortProxy
- Added cleanup calls after socket end in connection rejection scenarios within PortProxy
## 2025-02-21 - 3.9.3 - fix(PortProxy)
Fix handling of optional outgoing socket in PortProxy
- Refactored the cleanUpSockets function to correctly handle cases where the outgoing socket may be undefined.
- Ensured correct handling of socket events with non-null assertions where applicable.
- Improved robustness in connection establishment and cleanup processes.
## 2025-02-21 - 3.9.2 - fix(PortProxy)
Improve timeout handling for port proxy connections
- Added console logging for both incoming and outgoing side timeouts in the PortProxy class.
- Updated the timeout event handlers to ensure proper cleanup of connections.
## 2025-02-21 - 3.9.1 - fix(dependencies)
Ensure correct ordering of dependencies and improve logging format.
- Reorder dependencies in package.json for better readability.
- Use pretty-ms for displaying time durations in logs.
## 2025-02-21 - 3.9.0 - feat(smartproxy.portproxy)
Add logging of connection durations to PortProxy
- Track start times for incoming and outgoing connections.
- Log duration of longest running incoming and outgoing connections every 10 seconds.
## 2025-02-21 - 3.8.1 - fix(plugins)
Simplified plugin import structure across codebase
- Consolidated plugin imports under a single 'plugins.ts' file.
- Replaced individual plugin imports in smartproxy files with the consolidated plugin imports.
- Fixed error handling for early socket errors in PortProxy setup.
## 2025-02-21 - 3.8.0 - feat(PortProxy)
Add active connection tracking and logging in PortProxy
- Implemented a feature to track active incoming connections in PortProxy.
- Active connections are now logged every 10 seconds for monitoring purposes.
- Refactored connection handling to ensure proper cleanup and logging.
## 2025-02-21 - 3.7.3 - fix(portproxy)
Fix handling of connections in PortProxy to improve stability and performance.
- Improved IP normalization and matching
- Better SNI extraction and handling for TLS
- Streamlined connection handling with robust error management
## 2025-02-21 - 3.7.2 - fix(PortProxy)
Improve SNICallback and connection handling in PortProxy
- Fixed SNICallback to create minimal TLS context for SNI.
- Changed connection setup to use net.connect for raw passthrough.
## 2025-02-21 - 3.7.1 - fix(smartproxy.portproxy)
Optimize SNI handling by simplifying context creation
- Removed unnecessary SecureContext creation for SNI requests in PortProxy
- Improved handling of SNI passthrough by acknowledging requests without context creation
## 2025-02-21 - 3.7.0 - feat(PortProxy)
Add optional source IP preservation support in PortProxy
- Added a feature to optionally preserve the client's source IP when proxying connections.
- Enhanced test cases to include scenarios for source IP preservation.
## 2025-02-21 - 3.6.0 - feat(PortProxy)
Add feature to preserve original client IP through chained proxies
- Added support to bind local address in PortProxy to preserve original client IP.
- Implemented test for chained proxies to ensure client IP is preserved.
## 2025-02-21 - 3.5.0 - feat(PortProxy)
Enhance PortProxy to support domain-specific target IPs
- Introduced support for domain-specific target IP configurations in PortProxy.
- Updated connection handling to prioritize domain-specific target IPs if provided.
- Added tests to verify forwarding based on domain-specific target IPs.
## 2025-02-21 - 3.4.4 - fix(PortProxy)
Fixed handling of SNI domain connections and IP allowance checks
- Improved logic for handling SNI domain checks, ensuring IPs are correctly verified.
- Fixed issue where default allowed IPs were not being checked correctly for non-SNI connections.
- Revised the SNICallback behavior to handle connections more gracefully when domain configurations are unavailable.
## 2025-02-21 - 3.4.3 - fix(PortProxy)
Fixed indentation issue and ensured proper cleanup of sockets in PortProxy
- Fixed inconsistent indentation in IP allowance check.
- Ensured proper cleanup of sockets on connection end in PortProxy.
## 2025-02-21 - 3.4.2 - fix(smartproxy)
Enhance SSL/TLS handling with SNI and error logging
- Improved handling for SNI-enabled and non-SNI connections
- Added detailed logging for connection establishment and rejections
- Introduced error logging for TLS client errors and server errors
## 2025-02-21 - 3.4.1 - fix(PortProxy)
Normalize IP addresses for port proxy to handle IPv4-mapped IPv6 addresses.
- Improved IP normalization logic in PortProxy to support IPv4-mapped IPv6 addresses.
- Updated isAllowed function to expand patterns for better matching accuracy.
## 2025-02-21 - 3.4.0 - feat(PortProxy)
Enhanced PortProxy with custom target host and improved testing
- PortProxy constructor now accepts 'fromPort', 'toPort', and optional 'toHost' directly from settings
- Refactored test cases to cover forwarding to the custom host
- Added support to handle multiple concurrent connections
- Refactored internal connection handling logic to utilize default configurations
## 2025-02-21 - 3.3.1 - fix(PortProxy)
fixed import usage of net and tls libraries for PortProxy
- Corrected the use of plugins for importing 'tls' and 'net' libraries in the PortProxy module.
- Updated the constructor of PortProxy to accept combined tls options with ProxySettings.
## 2025-02-21 - 3.3.0 - feat(PortProxy)
Enhanced PortProxy with domain and IP filtering, SNI support, and minimatch integration

View File

@ -5,7 +5,7 @@
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smartproxy",
"description": "a proxy for handling high workloads of proxying",
"description": "A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.",
"npmPackagename": "@push.rocks/smartproxy",
"license": "MIT",
"projectDomain": "push.rocks",
@ -20,7 +20,11 @@
"ssl redirect",
"port mapping",
"reverse proxy",
"authentication"
"authentication",
"dynamic routing",
"sni",
"port forwarding",
"real-time applications"
]
}
},

View File

@ -1,8 +1,8 @@
{
"name": "@push.rocks/smartproxy",
"version": "3.3.0",
"version": "3.16.5",
"private": false,
"description": "a proxy for handling high workloads of proxying",
"description": "A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
@ -29,10 +29,12 @@
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartstring": "^4.0.15",
"@tsclass/tsclass": "^4.4.0",
"@types/minimatch": "^5.1.2",
"@types/ws": "^8.5.14",
"ws": "^8.18.0",
"acme-client": "^5.4.0",
"minimatch": "^9.0.3",
"@types/minimatch": "^5.1.2"
"pretty-ms": "^9.2.0",
"ws": "^8.18.0"
},
"files": [
"ts/**/*",
@ -60,7 +62,11 @@
"ssl redirect",
"port mapping",
"reverse proxy",
"authentication"
"authentication",
"dynamic routing",
"sni",
"port forwarding",
"real-time applications"
],
"homepage": "https://code.foss.global/push.rocks/smartproxy#readme",
"repository": {

190
pnpm-lock.yaml generated
View File

@ -32,9 +32,15 @@ importers:
'@types/ws':
specifier: ^8.5.14
version: 8.5.14
acme-client:
specifier: ^5.4.0
version: 5.4.0
minimatch:
specifier: ^9.0.3
version: 9.0.5
pretty-ms:
specifier: ^9.2.0
version: 9.2.0
ws:
specifier: ^8.18.0
version: 8.18.0
@ -680,6 +686,39 @@ packages:
'@pdf-lib/upng@1.0.1':
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
'@peculiar/asn1-cms@2.3.15':
resolution: {integrity: sha512-B+DoudF+TCrxoJSTjjcY8Mmu+lbv8e7pXGWrhNp2/EGJp9EEcpzjBCar7puU57sGifyzaRVM03oD5L7t7PghQg==}
'@peculiar/asn1-csr@2.3.15':
resolution: {integrity: sha512-caxAOrvw2hUZpxzhz8Kp8iBYKsHbGXZPl2KYRMIPvAfFateRebS3136+orUpcVwHRmpXWX2kzpb6COlIrqCumA==}
'@peculiar/asn1-ecc@2.3.15':
resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==}
'@peculiar/asn1-pfx@2.3.15':
resolution: {integrity: sha512-E3kzQe3J2xV9DP6SJS4X6/N1e4cYa2xOAK46VtvpaRk8jlheNri8v0rBezKFVPB1rz/jW8npO+u1xOvpATFMWg==}
'@peculiar/asn1-pkcs8@2.3.15':
resolution: {integrity: sha512-/PuQj2BIAw1/v76DV1LUOA6YOqh/UvptKLJHtec/DQwruXOCFlUo7k6llegn8N5BTeZTWMwz5EXruBw0Q10TMg==}
'@peculiar/asn1-pkcs9@2.3.15':
resolution: {integrity: sha512-yiZo/1EGvU1KiQUrbcnaPGWc0C7ElMMskWn7+kHsCFm+/9fU0+V1D/3a5oG0Jpy96iaXggQpA9tzdhnYDgjyFg==}
'@peculiar/asn1-rsa@2.3.15':
resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==}
'@peculiar/asn1-schema@2.3.15':
resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==}
'@peculiar/asn1-x509-attr@2.3.15':
resolution: {integrity: sha512-TWJVJhqc+IS4MTEML3l6W1b0sMowVqdsnI4dnojg96LvTuP8dga9f76fjP07MUuss60uSyT2ckoti/2qHXA10A==}
'@peculiar/asn1-x509@2.3.15':
resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==}
'@peculiar/x509@1.12.3':
resolution: {integrity: sha512-+Mzq+W7cNEKfkNZzyLl6A6ffqc3r21HGZUezgfKxpZrkORfOqgRXnS80Zu0IV6a9Ue9QBJeKD7kN0iWfc3bhRQ==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@ -1560,6 +1599,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
acme-client@5.4.0:
resolution: {integrity: sha512-mORqg60S8iML6XSmVjqjGHJkINrCGLMj2QvDmFzI9vIlv1RGlyjmw3nrzaINJjkNsYXC41XhhD5pfy7CtuGcbA==}
engines: {node: '>= 16'}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
@ -1623,6 +1666,10 @@ packages:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
asn1js@3.0.5:
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
engines: {node: '>=12.0.0'}
astral-regex@2.0.0:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
@ -1640,6 +1687,9 @@ packages:
resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==}
engines: {node: '>=4'}
axios@1.7.9:
resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
b4a@1.6.7:
resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
@ -3460,6 +3510,13 @@ packages:
resolution: {integrity: sha512-+vZPU8iBSdCx1Kn5hHas80fyo0TiVyMeqLGv/1dygX2HKhAZjO9YThadbRTCoTYq0yWw+w/CysldPsEekDtjDQ==}
engines: {node: '>=14.1.0'}
pvtsutils@1.3.6:
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
pvutils@1.1.3:
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
engines: {node: '>=6.0.0'}
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@ -3505,6 +3562,9 @@ packages:
resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==}
engines: {node: '>= 14.18.0'}
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@ -3894,6 +3954,10 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
tsyringe@4.8.0:
resolution: {integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==}
engines: {node: '>= 6.0.0'}
turndown-plugin-gfm@1.0.2:
resolution: {integrity: sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==}
@ -5209,6 +5273,96 @@ snapshots:
dependencies:
pako: 1.0.11
'@peculiar/asn1-cms@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
'@peculiar/asn1-x509-attr': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-csr@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-ecc@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-pfx@2.3.15':
dependencies:
'@peculiar/asn1-cms': 2.3.15
'@peculiar/asn1-pkcs8': 2.3.15
'@peculiar/asn1-rsa': 2.3.15
'@peculiar/asn1-schema': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-pkcs8@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-pkcs9@2.3.15':
dependencies:
'@peculiar/asn1-cms': 2.3.15
'@peculiar/asn1-pfx': 2.3.15
'@peculiar/asn1-pkcs8': 2.3.15
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
'@peculiar/asn1-x509-attr': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-rsa@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-schema@2.3.15':
dependencies:
asn1js: 3.0.5
pvtsutils: 1.3.6
tslib: 2.8.1
'@peculiar/asn1-x509-attr@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
asn1js: 3.0.5
tslib: 2.8.1
'@peculiar/asn1-x509@2.3.15':
dependencies:
'@peculiar/asn1-schema': 2.3.15
asn1js: 3.0.5
pvtsutils: 1.3.6
tslib: 2.8.1
'@peculiar/x509@1.12.3':
dependencies:
'@peculiar/asn1-cms': 2.3.15
'@peculiar/asn1-csr': 2.3.15
'@peculiar/asn1-ecc': 2.3.15
'@peculiar/asn1-pkcs9': 2.3.15
'@peculiar/asn1-rsa': 2.3.15
'@peculiar/asn1-schema': 2.3.15
'@peculiar/asn1-x509': 2.3.15
pvtsutils: 1.3.6
reflect-metadata: 0.2.2
tslib: 2.8.1
tsyringe: 4.8.0
'@pkgjs/parseargs@0.11.0':
optional: true
@ -6780,6 +6934,16 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
acme-client@5.4.0:
dependencies:
'@peculiar/x509': 1.12.3
asn1js: 3.0.5
axios: 1.7.9(debug@4.4.0)
debug: 4.4.0
node-forge: 1.3.1
transitivePeerDependencies:
- supports-color
agent-base@6.0.2:
dependencies:
debug: 4.3.4
@ -6831,6 +6995,12 @@ snapshots:
array-union@2.1.0: {}
asn1js@3.0.5:
dependencies:
pvtsutils: 1.3.6
pvutils: 1.1.3
tslib: 2.8.1
astral-regex@2.0.0: {}
async-mutex@0.3.2:
@ -6843,6 +7013,14 @@ snapshots:
axe-core@4.10.2: {}
axios@1.7.9(debug@4.4.0):
dependencies:
follow-redirects: 1.15.9(debug@4.4.0)
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
b4a@1.6.7: {}
bail@2.0.2: {}
@ -8951,6 +9129,12 @@ snapshots:
- supports-color
- utf-8-validate
pvtsutils@1.3.6:
dependencies:
tslib: 2.8.1
pvutils@1.1.3: {}
qs@6.13.0:
dependencies:
side-channel: 1.1.0
@ -9005,6 +9189,8 @@ snapshots:
readdirp@4.1.1: {}
reflect-metadata@0.2.2: {}
regenerator-runtime@0.14.1: {}
registry-auth-token@5.0.3:
@ -9485,6 +9671,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
tsyringe@4.8.0:
dependencies:
tslib: 1.14.1
turndown-plugin-gfm@1.0.2: {}
turndown@7.2.0:

189
readme.md
View File

@ -14,11 +14,11 @@ This will add `@push.rocks/smartproxy` to your project's dependencies.
## Usage
`@push.rocks/smartproxy` is a versatile package for setting up and handling proxies with various capabilities such as SSL redirection, port proxying, and creating network proxies with complex routing rules. Below is a comprehensive guide on using its features.
`@push.rocks/smartproxy` is a comprehensive and versatile package designed to handle complex and high-volume proxying tasks efficiently. It includes features such as SSL redirection, port proxying, WebSocket support, and customizable routing and authentication mechanisms. This guide will provide a detailed walkthrough of how to harness these capabilities effectively.
### Setting Up a Network Proxy
### Initial Setup
Create a network proxy to route incoming HTTPS requests to different local servers based on the hostname.
Before diving into specific features, let's start by configuring and setting up our basic proxy server:
```typescript
import { NetworkProxy } from '@push.rocks/smartproxy';
@ -26,7 +26,7 @@ import { NetworkProxy } from '@push.rocks/smartproxy';
// Instantiate the NetworkProxy with desired options
const myNetworkProxy = new NetworkProxy({ port: 443 });
// Define your reverse proxy configurations
// Define reverse proxy configurations
const proxyConfigs = [
{
destinationIp: '127.0.0.1',
@ -39,70 +39,187 @@ PRIVATE_KEY_CONTENT
CERTIFICATE_CONTENT
-----END CERTIFICATE-----`,
},
// Add more reverse proxy configurations here
// More configurations can be added here
];
// Start the network proxy
await myNetworkProxy.start();
// Update proxy configurations dynamically
// Apply proxy configurations
await myNetworkProxy.updateProxyConfigs(proxyConfigs);
// Optionally, add default headers to all responses
// Optionally add default headers to all responses
await myNetworkProxy.addDefaultHeaders({
'X-Powered-By': 'smartproxy',
});
```
### Port Proxying
### Configuring SSL Redirection
You can also set up a port proxy to forward traffic from one port to another, which is useful for dynamic port forwarding scenarios.
```typescript
import { PortProxy } from '@push.rocks/smartproxy';
// Create a PortProxy to forward traffic from port 5000 to port 3000
const myPortProxy = new PortProxy(5000, 3000);
// Start the port proxy
await myPortProxy.start();
// To stop the port proxy, simply call
await myPortProxy.stop();
```
### Enabling SSL Redirection
Easily redirect HTTP traffic to HTTPS using the `SslRedirect` class. This is particularly useful when ensuring all traffic uses encryption.
One essential capability of a robust proxy server is ensuring that all HTTP traffic is redirected to secure HTTPS endpoints. This can be effortlessly accomplished using the `SslRedirect` class within `smartproxy`. This class listens on port 80 (HTTP) and redirects all incoming requests to HTTPS:
```typescript
import { SslRedirect } from '@push.rocks/smartproxy';
// Instantiate the SslRedirect on port 80 (HTTP)
// Instantiate the SslRedirect for listening on port 80
const mySslRedirect = new SslRedirect(80);
// Start listening and redirecting to HTTPS
// Start listening and redirect HTTP traffic to HTTPS
await mySslRedirect.start();
// To stop the redirection, use
// To stop redirection, you can use the following command:
await mySslRedirect.stop();
```
### Advanced Usage
### Handling Complex Networking with Port Proxy
The package integrates seamlessly with TypeScript, allowing for advanced use cases, such as implementing custom routing logic, authentication mechanisms, and handling WebSocket connections through the network proxy.
Port proxying allows redirection of traffic from one port to another. This capability is crucial when dealing with services that need dynamic port forwarding, or when adapting to infrastructure changes without downtime. Smartproxy's `PortProxy` class handles this efficiently:
For a more advanced setup involving WebSocket proxying and dynamic configuration reloading, refer to the network proxy example provided above. The WebSocket support demonstrates how seamless it is to work with real-time applications.
```typescript
import { PortProxy } from '@push.rocks/smartproxy';
Remember, when dealing with certificates and private keys for HTTPS configurations, always secure your keys and store them appropriately.
// Create a PortProxy to directly forward traffic from port 5000 to 3000
const myPortProxy = new PortProxy(5000, 3000);
`@push.rocks/smartproxy` provides a solid foundation for handling high workloads and complex proxying requirements with ease, whether you're implementing SSL redirections, port forwarding, or extensive routing and WebSocket support in your network.
// Initiate the port proxy
await myPortProxy.start();
For more information on how to use the features, refer to the in-depth documentation available in the package's repository or the npm package description.
// To stop the port proxy mechanism:
await myPortProxy.stop();
```
Additionally, smartproxy's port proxying can support intricate scenarios where different forwarding rules are configured based on domain names or allowed IPs:
```typescript
import { PortProxy } from '@push.rocks/smartproxy';
const myComplexPortProxy = new PortProxy({
fromPort: 6000,
toPort: 3000,
domains: [
{
domain: 'api.example.com',
allowedIPs: ['192.168.0.*', '127.0.0.1'],
targetIP: '192.168.1.100'
}
// Define more domain-specific rules if needed
],
sniEnabled: true, // if SNI (Server Name Indication) is desired
defaultAllowedIPs: ['*']);
});
// Start listening for complex routing requests
await myComplexPortProxy.start();
```
### WebSocket Support and Load Handling
With the advent of real-time applications, efficient WebSocket handling in proxies is crucial. Smartproxy integrates WebSocket support seamlessly, enabling it to proxy WebSocket traffic while maintaining security and performance:
```typescript
import { NetworkProxy } from '@push.rocks/smartproxy';
const wsProxy = new NetworkProxy({ port: 443 });
// Assume reverse proxy configurations with WebSocket intentions
const wsProxyConfigs = [
{
destinationIp: '127.0.0.1',
destinationPort: '8080',
hostName: 'socket.example.com',
// Add further options such as keys for SSL if needed
}
];
// Start the network proxy with WebSocket capabilities
await wsProxy.start();
await wsProxy.updateProxyConfigs(wsProxyConfigs);
// Ensure WebSocket connections remain alive
wsProxy.heartbeatInterval = setInterval(() => {
// logic for keeping connections alive and healthy
}, 60000); // Every 60 seconds
// Gracefully handle server or connection errors to maintain uptime
wsProxy.httpsServer.on('error', (error) => console.log('Server Error:', error));
```
### Comprehensive Routing and Advanced Features
Smartproxy supports dynamic and customizable request routing based on the incoming request's destination. This feature enables extensive use-case scenarios, from simple API endpoint redirection to elaborate B2B service integrations:
```typescript
import { NetworkProxy } from '@push.rocks/smartproxy';
const dynamicRoutingProxy = new NetworkProxy({ port: 8443 });
dynamicRoutingProxy.router.setNewProxyConfigs([
{
destinationIp: '192.168.1.150',
destinationPort: '80',
hostName: 'dynamic.example.com',
authentication: {
type: 'Basic',
user: 'admin',
pass: 'password123'
}
}
]);
await dynamicRoutingProxy.start();
```
For those dealing with high volume or regulatory needs, the integration of tools like `iptables` allows broad control over network traffic:
```typescript
import { IPTablesProxy } from '@push.rocks/smartproxy';
// Setting up iptables for advanced network management
const ipTablesProxy = new IPTablesProxy({
fromPort: 8081,
toPort: 8080,
deleteOnExit: true // clean rules upon server shutdown
});
// Begin routing with IPTables
await ipTablesProxy.start();
```
### Combining with HTTP and HTTPS Credentials
When undertaking proxy configurations, handling sensitive data like SSL certificates and keys securely is imperative:
```typescript
import { loadDefaultCertificates } from '@push.rocks/smartproxy';
try {
const { privateKey, publicKey } = loadDefaultCertificates(); // adjust path as needed
console.log('Certificates loaded.');
// Use these certificates in your SSL-based configurations
} catch (error) {
console.error('Cannot load certificates:', error);
}
```
### Testing and Validation
Given these powerful capabilities, rigorous testing of configurations and functionality using frameworks like `tap` can ensure high-quality and reliable proxy configurations. Smartproxy integrates with Typescript test setups:
```typescript
import { expect, tap } from '@push.rocks/tapbundle';
import { NetworkProxy } from '@push.rocks/smartproxy';
tap.test('proxied request should return status 200', async () => {
// Your test logic here
});
tap.start();
```
In summary, `@push.rocks/smartproxy` offers a plethora of solutions tailored to both common and sophisticated proxying needs. Whether you're seeking straightforward port forwarding, secure SSL redirection, WebSocket management, or robust network routing controls, smartproxy provides the right tools for efficient and effective proxy operations. Through its integration simplicity and versatile configurations, developers can ensure high performance and secure proxying across various environments and applications.
## 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.
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.

View File

@ -1,6 +1,6 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { PortProxy } from '../ts/smartproxy.portproxy.js';
import { PortProxy } from '../ts/classes.portproxy.js';
let testServer: net.Server;
let portProxy: PortProxy;
@ -16,12 +16,10 @@ function createTestServer(port: number): Promise<net.Server> {
// Echo the received data back
socket.write(`Echo: ${data.toString()}`);
});
socket.on('error', (error) => {
console.error('[Test Server] Socket error:', error);
});
});
server.listen(port, () => {
console.log(`[Test Server] Listening on port ${port}`);
resolve(server);
@ -39,16 +37,13 @@ function createTestClient(port: number, data: string): Promise<string> {
console.log('[Test Client] Connected to server');
client.write(data);
});
client.on('data', (chunk) => {
response += chunk.toString();
client.end();
});
client.on('end', () => {
resolve(response);
});
client.on('error', (error) => {
reject(error);
});
@ -58,23 +53,96 @@ function createTestClient(port: number, data: string): Promise<string> {
// Setup test environment
tap.test('setup port proxy test environment', async () => {
testServer = await createTestServer(TEST_SERVER_PORT);
portProxy = new PortProxy(PROXY_PORT, TEST_SERVER_PORT, {
portProxy = new PortProxy({
fromPort: PROXY_PORT,
toPort: TEST_SERVER_PORT,
targetIP: 'localhost',
domains: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
});
tap.test('should start port proxy', async () => {
await portProxy.start();
expect(portProxy.netServer.listening).toBeTrue();
// Since netServers is private, we cast to any to verify that all created servers are listening.
expect((portProxy as any).netServers.every((server: net.Server) => server.listening)).toBeTrue();
});
tap.test('should forward TCP connections and data', async () => {
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}`);
});
tap.test('should forward TCP connections to custom host', async () => {
// Create a new proxy instance with a custom host (targetIP)
const customHostProxy = new PortProxy({
fromPort: PROXY_PORT + 1,
toPort: TEST_SERVER_PORT,
targetIP: '127.0.0.1',
domains: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
await customHostProxy.start();
const response = await createTestClient(PROXY_PORT + 1, TEST_DATA);
expect(response).toEqual(`Echo: ${TEST_DATA}`);
await customHostProxy.stop();
});
tap.test('should forward connections based on domain-specific target IP', async () => {
// Create a second test server on a different port
const TEST_SERVER_PORT_2 = TEST_SERVER_PORT + 100;
const testServer2 = await createTestServer(TEST_SERVER_PORT_2);
// Create a proxy with domain-specific target IPs
const domainProxy = new PortProxy({
fromPort: PROXY_PORT + 2,
toPort: TEST_SERVER_PORT, // default port (for non-port-range handling)
targetIP: 'localhost', // default target IP
domains: [{
domain: 'domain1.test',
allowedIPs: ['127.0.0.1'],
targetIP: '127.0.0.1'
}, {
domain: 'domain2.test',
allowedIPs: ['127.0.0.1'],
targetIP: 'localhost'
}],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
await domainProxy.start();
// Test default connection (should use default targetIP)
const response1 = await createTestClient(PROXY_PORT + 2, TEST_DATA);
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
// Create another proxy with a different default targetIP
const domainProxy2 = new PortProxy({
fromPort: PROXY_PORT + 3,
toPort: TEST_SERVER_PORT,
targetIP: '127.0.0.1',
domains: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
await domainProxy2.start();
const response2 = await createTestClient(PROXY_PORT + 3, TEST_DATA);
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
await domainProxy.stop();
await domainProxy2.stop();
await new Promise<void>((resolve) => testServer2.close(() => resolve()));
});
tap.test('should handle multiple concurrent connections', async () => {
const concurrentRequests = 5;
const requests = Array(concurrentRequests).fill(null).map((_, i) =>
@ -90,7 +158,6 @@ tap.test('should handle multiple concurrent connections', async () => {
tap.test('should handle connection timeouts', async () => {
const client = new net.Socket();
await new Promise<void>((resolve) => {
client.connect(PROXY_PORT, 'localhost', () => {
// Don't send any data, just wait for timeout
@ -103,10 +170,76 @@ tap.test('should handle connection timeouts', async () => {
tap.test('should stop port proxy', async () => {
await portProxy.stop();
expect(portProxy.netServer.listening).toBeFalse();
expect((portProxy as any).netServers.every((server: net.Server) => !server.listening)).toBeTrue();
});
// Cleanup chained proxies tests
tap.test('should support optional source IP preservation in chained proxies', async () => {
// Test 1: Without IP preservation (default behavior)
const firstProxyDefault = new PortProxy({
fromPort: PROXY_PORT + 4,
toPort: PROXY_PORT + 5,
targetIP: 'localhost',
domains: [],
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',
domains: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
globalPortRanges: []
});
await secondProxyDefault.start();
await firstProxyDefault.start();
// This should work because we explicitly allow both IPv4 and IPv6 formats
const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA);
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
await firstProxyDefault.stop();
await secondProxyDefault.stop();
// Test 2: With IP preservation
const firstProxyPreserved = new PortProxy({
fromPort: PROXY_PORT + 6,
toPort: PROXY_PORT + 7,
targetIP: 'localhost',
domains: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
preserveSourceIP: true,
globalPortRanges: []
});
const secondProxyPreserved = new PortProxy({
fromPort: PROXY_PORT + 7,
toPort: TEST_SERVER_PORT,
targetIP: 'localhost',
domains: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
preserveSourceIP: true,
globalPortRanges: []
});
await secondProxyPreserved.start();
await firstProxyPreserved.start();
// This should work with just IPv4 because source IP is preserved
const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA);
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
await firstProxyPreserved.stop();
await secondProxyPreserved.stop();
});
// Cleanup
tap.test('cleanup port proxy test environment', async () => {
await new Promise<void>((resolve) => testServer.close(() => resolve()));
});
@ -115,9 +248,10 @@ process.on('exit', () => {
if (testServer) {
testServer.close();
}
if (portProxy && portProxy.netServer) {
// Use a cast to access the private property for cleanup.
if (portProxy && (portProxy as any).netServers) {
portProxy.stop();
}
});
export default tap.start();
export default tap.start();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.3.0',
description: 'a proxy for handling high workloads of proxying'
version: '3.16.5',
description: 'A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.'
}

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

@ -0,0 +1,183 @@
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Settings for IPTablesProxy.
*/
export interface IIpTableProxySettings {
fromPort: number;
toPort: number;
toHost?: string; // Target host for proxying; defaults to 'localhost'
preserveSourceIP?: boolean; // If true, the original source IP is preserved.
deleteOnExit?: boolean; // If true, clean up marked iptables rules before process exit.
}
/**
* IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
* It only supports basic port forwarding and uses iptables comments to tag rules.
*/
export class IPTablesProxy {
public settings: IIpTableProxySettings;
private rulesInstalled: boolean = false;
private ruleTag: string;
constructor(settings: IIpTableProxySettings) {
this.settings = {
...settings,
toHost: settings.toHost || 'localhost',
};
// Generate a unique identifier for the rules added by this instance.
this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
// If deleteOnExit is true, register cleanup handlers.
if (this.settings.deleteOnExit) {
const cleanup = () => {
try {
IPTablesProxy.cleanSlateSync();
} 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();
});
}
}
/**
* Sets up iptables rules for port forwarding.
* The rules are tagged with a unique comment so that they can be identified later.
*/
public async start(): Promise<void> {
const dnatCmd = `iptables -t nat -A PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
try {
await execAsync(dnatCmd);
console.log(`Added iptables rule: ${dnatCmd}`);
this.rulesInstalled = true;
} catch (err) {
console.error(`Failed to add iptables DNAT rule: ${err}`);
throw err;
}
// If preserveSourceIP is false, add a MASQUERADE rule.
if (!this.settings.preserveSourceIP) {
const masqueradeCmd = `iptables -t nat -A POSTROUTING -p tcp -d ${this.settings.toHost} ` +
`--dport ${this.settings.toPort} -j MASQUERADE ` +
`-m comment --comment "${this.ruleTag}:MASQ"`;
try {
await execAsync(masqueradeCmd);
console.log(`Added iptables rule: ${masqueradeCmd}`);
} catch (err) {
console.error(`Failed to add iptables MASQUERADE rule: ${err}`);
// Roll back the DNAT rule if MASQUERADE fails.
try {
const rollbackCmd = `iptables -t nat -D PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
await execAsync(rollbackCmd);
this.rulesInstalled = false;
} catch (rollbackErr) {
console.error(`Rollback failed: ${rollbackErr}`);
}
throw err;
}
}
}
/**
* Removes the iptables rules that were added in start(), by matching the unique comment.
*/
public async stop(): Promise<void> {
if (!this.rulesInstalled) return;
const dnatDelCmd = `iptables -t nat -D PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
try {
await execAsync(dnatDelCmd);
console.log(`Removed iptables rule: ${dnatDelCmd}`);
} catch (err) {
console.error(`Failed to remove iptables DNAT rule: ${err}`);
}
if (!this.settings.preserveSourceIP) {
const masqueradeDelCmd = `iptables -t nat -D POSTROUTING -p tcp -d ${this.settings.toHost} ` +
`--dport ${this.settings.toPort} -j MASQUERADE ` +
`-m comment --comment "${this.ruleTag}:MASQ"`;
try {
await execAsync(masqueradeDelCmd);
console.log(`Removed iptables rule: ${masqueradeDelCmd}`);
} catch (err) {
console.error(`Failed to remove iptables MASQUERADE rule: ${err}`);
}
}
this.rulesInstalled = false;
}
/**
* 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> {
try {
const { stdout } = await execAsync('iptables-save -t nat');
const lines = stdout.split('\n');
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
for (const line of proxyLines) {
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 = `iptables -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);
}
}
}
} catch (err) {
console.error(`Failed to run iptables-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 {
try {
const stdout = execSync('iptables-save -t nat').toString();
const lines = stdout.split('\n');
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
for (const line of proxyLines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `iptables -t nat ${deleteRule}`;
try {
execSync(cmd);
console.log(`Cleaned up iptables rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables rule: ${cmd}`, err);
}
}
}
} catch (err) {
console.error(`Failed to run iptables-save: ${err}`);
}
}
}

View File

@ -1,5 +1,5 @@
import * as plugins from './smartproxy.plugins.js';
import { ProxyRouter } from './smartproxy.classes.router.js';
import * as plugins from './plugins.js';
import { ProxyRouter } from './classes.router.js';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';

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

@ -0,0 +1,214 @@
import * as http from 'http';
import * as acme from 'acme-client';
interface IDomainCertificate {
certObtained: boolean;
obtainingInProgress: boolean;
certificate?: string;
privateKey?: string;
challengeToken?: string;
challengeKeyAuthorization?: string;
}
export class Port80Handler {
private domainCertificates: Map<string, IDomainCertificate>;
private server: http.Server;
private acmeClient: acme.Client | null = null;
private accountKey: string | null = null;
constructor() {
this.domainCertificates = new Map<string, IDomainCertificate>();
// Create and start an HTTP server on port 80.
this.server = http.createServer((req, res) => this.handleRequest(req, res));
this.server.listen(80, () => {
console.log('Port80Handler is listening on port 80');
});
}
/**
* Adds a domain to be managed.
* @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}`);
}
}
/**
* Lazy initialization of the ACME client.
* Uses Lets Encrypts production directory (for testing you might switch to staging).
*/
private async getAcmeClient(): Promise<acme.Client> {
if (this.acmeClient) {
return this.acmeClient;
}
// Generate a new account key and convert Buffer to string.
this.accountKey = (await acme.forge.createPrivateKey()).toString();
this.acmeClient = new acme.Client({
directoryUrl: acme.directory.letsencrypt.production, // Use production for a real certificate
// For testing, you could use:
// directoryUrl: acme.directory.letsencrypt.staging,
accountKey: this.accountKey,
});
// Create a new account. Make sure to update the contact email.
await this.acmeClient.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:admin@example.com'],
});
return this.acmeClient;
}
/**
* Handles incoming HTTP requests on port 80.
* If the request is for an ACME challenge, it responds with the key authorization.
* If the domain has a certificate, it redirects to HTTPS; otherwise, it initiates certificate issuance.
*/
private handleRequest(req: http.IncomingMessage, res: 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 on port 443.
if (domainInfo.certObtained) {
const redirectUrl = `https://${domain}:443${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) {
domainInfo.obtainingInProgress = true;
this.obtainCertificate(domain).catch(err => {
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.
*/
private handleAcmeChallenge(req: http.IncomingMessage, res: 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');
}
}
/**
* Uses acme-client to perform a full ACME HTTP-01 challenge to obtain a certificate.
* On success, it stores the certificate and key in memory and clears challenge data.
*/
private async obtainCertificate(domain: string): Promise<void> {
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);
const domainInfo = this.domainCertificates.get(domain)!;
domainInfo.challengeToken = challenge.token;
domainInfo.challengeKeyAuthorization = keyAuthorization;
// Notify the ACME server that the challenge is ready.
// The acme-client examples show that verifyChallenge takes three arguments:
// (authorization, challenge, keyAuthorization). However, the official TypeScript
// types appear to be out-of-sync. As a workaround, we cast client to 'any'.
await (client as any).verifyChallenge(authz, challenge, keyAuthorization);
await client.completeChallenge(challenge);
// Wait until the challenge is validated.
await client.waitForValidStatus(challenge);
console.log(`HTTP-01 challenge completed for ${domain}`);
}
// Generate a CSR and a new private key for the domain.
// Convert the resulting Buffers to strings.
const [csrBuffer, privateKeyBuffer] = await acme.forge.createCsr({
commonName: domain,
});
const csr = csrBuffer.toString();
const privateKey = privateKeyBuffer.toString();
// Finalize the order and obtain the certificate.
await client.finalizeOrder(order, csr);
const certificate = await client.getCertificate(order);
const domainInfo = this.domainCertificates.get(domain)!;
domainInfo.certificate = certificate;
domainInfo.privateKey = privateKey;
domainInfo.certObtained = true;
domainInfo.obtainingInProgress = false;
delete domainInfo.challengeToken;
delete domainInfo.challengeKeyAuthorization;
console.log(`Certificate obtained for ${domain}`);
// In a production system, persist the certificate and key and reload your TLS server.
} catch (error) {
console.error(`Error during certificate issuance for ${domain}:`, error);
const domainInfo = this.domainCertificates.get(domain);
if (domainInfo) {
domainInfo.obtainingInProgress = false;
}
}
}
}

466
ts/classes.portproxy.ts Normal file
View File

@ -0,0 +1,466 @@
import * as plugins from './plugins.js';
/** Domain configuration with perdomain allowed port ranges */
export interface IDomainConfig {
domain: string; // Glob pattern for domain
allowedIPs: string[]; // Glob patterns for allowed IPs
targetIP?: string; // Optional target IP for this domain
portRanges?: Array<{ from: number; to: number }>; // Optional domain-specific allowed port ranges
}
/** Port proxy settings including global allowed port ranges */
export interface IPortProxySettings extends plugins.tls.TlsOptions {
fromPort: number;
toPort: number;
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
domains: IDomainConfig[];
sniEnabled?: boolean;
defaultAllowedIPs?: string[];
preserveSourceIP?: boolean;
maxConnectionLifetime?: number; // (ms) force cleanup of long-lived connections
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
}
/**
* Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
* @param buffer - Buffer containing the TLS ClientHello.
* @returns The server name if found, otherwise undefined.
*/
function extractSNI(buffer: Buffer): string | undefined {
let offset = 0;
if (buffer.length < 5) return undefined;
const recordType = buffer.readUInt8(0);
if (recordType !== 22) return undefined; // 22 = handshake
const recordLength = buffer.readUInt16BE(3);
if (buffer.length < 5 + recordLength) return undefined;
offset = 5;
const handshakeType = buffer.readUInt8(offset);
if (handshakeType !== 1) return undefined; // 1 = ClientHello
offset += 4; // Skip handshake header (type + length)
offset += 2 + 32; // Skip client version and random
const sessionIDLength = buffer.readUInt8(offset);
offset += 1 + sessionIDLength; // Skip session ID
const cipherSuitesLength = buffer.readUInt16BE(offset);
offset += 2 + cipherSuitesLength; // Skip cipher suites
const compressionMethodsLength = buffer.readUInt8(offset);
offset += 1 + compressionMethodsLength; // Skip compression methods
if (offset + 2 > buffer.length) return undefined;
const extensionsLength = buffer.readUInt16BE(offset);
offset += 2;
const extensionsEnd = offset + extensionsLength;
while (offset + 4 <= extensionsEnd) {
const extensionType = buffer.readUInt16BE(offset);
const extensionLength = buffer.readUInt16BE(offset + 2);
offset += 4;
if (extensionType === 0x0000) { // SNI extension
if (offset + 2 > buffer.length) return undefined;
const sniListLength = buffer.readUInt16BE(offset);
offset += 2;
const sniListEnd = offset + sniListLength;
while (offset + 3 < sniListEnd) {
const nameType = buffer.readUInt8(offset++);
const nameLen = buffer.readUInt16BE(offset);
offset += 2;
if (nameType === 0) { // host_name
if (offset + nameLen > buffer.length) return undefined;
return buffer.toString('utf8', offset, offset + nameLen);
}
offset += nameLen;
}
break;
} else {
offset += extensionLength;
}
}
return undefined;
}
interface IConnectionRecord {
incoming: plugins.net.Socket;
outgoing: plugins.net.Socket | null;
incomingStartTime: number;
outgoingStartTime?: number;
connectionClosed: boolean;
cleanupTimer?: NodeJS.Timeout; // Timer to force cleanup after max lifetime/inactivity
}
export class PortProxy {
private netServers: plugins.net.Server[] = [];
settings: IPortProxySettings;
// Unified record tracking each connection pair.
private connectionRecords: Set<IConnectionRecord> = new Set();
private connectionLogger: NodeJS.Timeout | null = null;
private terminationStats: {
incoming: Record<string, number>;
outgoing: Record<string, number>;
} = {
incoming: {},
outgoing: {},
};
constructor(settingsArg: IPortProxySettings) {
this.settings = {
...settingsArg,
targetIP: settingsArg.targetIP || 'localhost',
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
};
}
private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
}
public async start() {
// Define a unified connection handler for all listening ports.
const connectionHandler = (socket: plugins.net.Socket) => {
const remoteIP = socket.remoteAddress || '';
const localPort = socket.localPort; // The port on which this connection was accepted.
const connectionRecord: IConnectionRecord = {
incoming: socket,
outgoing: null,
incomingStartTime: Date.now(),
connectionClosed: false,
};
this.connectionRecords.add(connectionRecord);
console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
let initialDataReceived = false;
let incomingTerminationReason: string | null = null;
let outgoingTerminationReason: string | null = null;
// Ensure cleanup happens only once for the entire connection record.
const cleanupOnce = async () => {
if (!connectionRecord.connectionClosed) {
connectionRecord.connectionClosed = true;
await plugins.smartdelay.delayFor(0);
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
}
await plugins.smartdelay.delayFor(0);
if (!socket.destroyed) socket.destroy();
await plugins.smartdelay.delayFor(0);
if (connectionRecord.outgoing && !connectionRecord.outgoing.destroyed) connectionRecord.outgoing.destroy();
await plugins.smartdelay.delayFor(0);
this.connectionRecords.delete(connectionRecord);
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
}
};
// Helper to reject an incoming connection.
const rejectIncomingConnection = (reason: string, logMessage: string) => {
console.log(logMessage);
socket.end();
if (incomingTerminationReason === null) {
incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
}
cleanupOnce();
};
socket.on('error', (err: Error) => {
const errorMessage = initialDataReceived
? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
: `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
console.log(errorMessage);
});
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
const code = (err as any).code;
let reason = 'error';
if (code === 'ECONNRESET') {
reason = 'econnreset';
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
} else {
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
}
if (side === 'incoming' && incomingTerminationReason === null) {
incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
} else if (side === 'outgoing' && outgoingTerminationReason === null) {
outgoingTerminationReason = reason;
this.incrementTerminationStat('outgoing', reason);
}
cleanupOnce();
};
const handleClose = (side: 'incoming' | 'outgoing') => () => {
console.log(`Connection closed on ${side} side from ${remoteIP}`);
if (side === 'incoming' && incomingTerminationReason === null) {
incomingTerminationReason = 'normal';
this.incrementTerminationStat('incoming', 'normal');
} else if (side === 'outgoing' && outgoingTerminationReason === null) {
outgoingTerminationReason = 'normal';
this.incrementTerminationStat('outgoing', 'normal');
}
cleanupOnce();
};
/**
* Sets up the connection to the target host.
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
* @param initialChunk - Optional initial data chunk.
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
* @param overridePort - If provided, use this port for the outgoing connection (typically the same as the incoming port).
*/
const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig, overridePort?: number) => {
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
const domainConfig = forcedDomain ? forcedDomain : (serverName ? this.settings.domains.find(config => plugins.minimatch(serverName, config.domain)) : undefined);
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
if (!defaultAllowed && serverName && !forcedDomain) {
if (!domainConfig) {
return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
}
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
}
} else if (defaultAllowed && !serverName) {
console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
}
const targetHost = domainConfig?.targetIP || this.settings.targetIP!;
const connectionOptions: plugins.net.NetConnectOpts = {
host: targetHost,
port: overridePort !== undefined ? overridePort : this.settings.toPort,
};
if (this.settings.preserveSourceIP) {
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
}
const targetSocket = plugins.net.connect(connectionOptions);
connectionRecord.outgoing = targetSocket;
connectionRecord.outgoingStartTime = Date.now();
console.log(
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domain})` : ''}`
);
if (initialChunk) {
socket.unshift(initialChunk);
}
socket.setTimeout(120000);
socket.pipe(targetSocket);
targetSocket.pipe(socket);
// Attach error and close handlers.
socket.on('error', handleError('incoming'));
targetSocket.on('error', handleError('outgoing'));
socket.on('close', handleClose('incoming'));
targetSocket.on('close', handleClose('outgoing'));
socket.on('timeout', () => {
console.log(`Timeout on incoming side from ${remoteIP}`);
if (incomingTerminationReason === null) {
incomingTerminationReason = 'timeout';
this.incrementTerminationStat('incoming', 'timeout');
}
cleanupOnce();
});
targetSocket.on('timeout', () => {
console.log(`Timeout on outgoing side from ${remoteIP}`);
if (outgoingTerminationReason === null) {
outgoingTerminationReason = 'timeout';
this.incrementTerminationStat('outgoing', 'timeout');
}
cleanupOnce();
});
socket.on('end', handleClose('incoming'));
targetSocket.on('end', handleClose('outgoing'));
// Initialize a cleanup timer for max connection lifetime.
if (this.settings.maxConnectionLifetime) {
let incomingActive = false;
let outgoingActive = false;
const resetCleanupTimer = () => {
if (this.settings.maxConnectionLifetime) {
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
}
connectionRecord.cleanupTimer = setTimeout(() => {
console.log(`Connection from ${remoteIP} exceeded max lifetime with inactivity (${this.settings.maxConnectionLifetime}ms), forcing cleanup.`);
cleanupOnce();
}, this.settings.maxConnectionLifetime);
}
};
resetCleanupTimer();
socket.on('data', () => {
incomingActive = true;
if (incomingActive && outgoingActive) {
resetCleanupTimer();
incomingActive = false;
outgoingActive = false;
}
});
targetSocket.on('data', () => {
outgoingActive = true;
if (incomingActive && outgoingActive) {
resetCleanupTimer();
incomingActive = false;
outgoingActive = false;
}
});
}
};
// --- PORT RANGE-BASED HANDLING ---
// If the local port is one of the globally listened ports, we may have port-based rules.
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
// If forwardAllGlobalRanges is enabled, always forward using the global targetIP.
if (this.settings.forwardAllGlobalRanges) {
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
socket.end();
return;
}
console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
setupConnection('', undefined, {
domain: 'global',
allowedIPs: this.settings.defaultAllowedIPs || [],
targetIP: this.settings.targetIP,
portRanges: []
}, localPort);
return;
} else {
// Attempt to find a matching forced domain config based on the local port.
const forcedDomain = this.settings.domains.find(
domain => domain.portRanges && domain.portRanges.length > 0 && isPortInRanges(localPort, domain.portRanges)
);
if (forcedDomain) {
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
if (!defaultAllowed && !isAllowed(remoteIP, forcedDomain.allowedIPs)) {
console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domain} on port ${localPort}.`);
socket.end();
return;
}
console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domain}.`);
setupConnection('', undefined, forcedDomain, localPort);
return;
}
// Fall through to SNI/default handling if no forced domain config is found.
}
}
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
if (this.settings.sniEnabled) {
socket.setTimeout(5000, () => {
console.log(`Initial data timeout for ${remoteIP}`);
socket.end();
cleanupOnce();
});
socket.once('data', (chunk: Buffer) => {
socket.setTimeout(0);
initialDataReceived = true;
const serverName = extractSNI(chunk) || '';
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
setupConnection(serverName, chunk);
});
} else {
initialDataReceived = true;
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
}
setupConnection('');
}
};
// --- SETUP LISTENERS ---
// Determine which ports to listen on.
const listeningPorts = new Set<number>();
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
// Listen on every port defined by the global ranges.
for (const range of this.settings.globalPortRanges) {
for (let port = range.from; port <= range.to; port++) {
listeningPorts.add(port);
}
}
// Also ensure the default fromPort is listened to if it isnt already in the ranges.
listeningPorts.add(this.settings.fromPort);
} else {
listeningPorts.add(this.settings.fromPort);
}
// Create a server for each port.
for (const port of listeningPorts) {
const server = plugins.net
.createServer(connectionHandler)
.on('error', (err: Error) => {
console.log(`Server Error on port ${port}: ${err.message}`);
});
server.listen(port, () => {
console.log(`PortProxy -> OK: Now listening on port ${port}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
});
this.netServers.push(server);
}
// Log active connection count and longest running durations every 10 seconds.
this.connectionLogger = setInterval(() => {
const now = Date.now();
let maxIncoming = 0;
let maxOutgoing = 0;
for (const record of this.connectionRecords) {
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
if (record.outgoingStartTime) {
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
}
}
console.log(
`(Interval Log) Active connections: ${this.connectionRecords.size}. ` +
`Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
`Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, ` +
`(outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`
);
}, 10000);
}
public async stop() {
// Close all servers.
const closePromises: Promise<void>[] = this.netServers.map(
server =>
new Promise<void>((resolve) => {
server.close(() => resolve());
})
);
if (this.connectionLogger) {
clearInterval(this.connectionLogger);
this.connectionLogger = null;
}
await Promise.all(closePromises);
}
}
// Helper: Check if a port falls within any of the given port ranges.
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
return ranges.some(range => port >= range.from && port <= range.to);
};
// Helper: Check if a given IP matches any of the glob patterns.
const isAllowed = (ip: string, patterns: string[]): boolean => {
const normalizeIP = (ip: string): string[] => {
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);
const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some(ipVariant =>
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
);
};

View File

@ -1,4 +1,4 @@
import * as plugins from './smartproxy.plugins.js';
import * as plugins from './plugins.js';
export class ProxyRouter {
public reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];

View File

@ -1,4 +1,4 @@
import * as plugins from './smartproxy.plugins.js';
import * as plugins from './plugins.js';
export class SslRedirect {
httpServer: plugins.http.Server;

View File

@ -1,3 +1,5 @@
export * from './smartproxy.classes.networkproxy.js';
export * from './smartproxy.portproxy.js';
export * from './smartproxy.classes.sslredirect.js';
export * from './classes.iptablesproxy.js';
export * from './classes.networkproxy.js';
export * from './classes.portproxy.js';
export * from './classes.port80handler.js';
export * from './classes.sslredirect.js';

View File

@ -2,9 +2,10 @@
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 { http, https, net, url };
export { http, https, net, tls, url };
// tsclass scope
import * as tsclass from '@tsclass/tsclass';
@ -21,8 +22,9 @@ import * as smartstring from '@push.rocks/smartstring';
export { lik, smartdelay, smartrequest, smartpromise, smartstring };
// third party scope
import prettyMs from 'pretty-ms';
import * as ws from 'ws';
import wsDefault from 'ws';
import { minimatch } from 'minimatch';
export { wsDefault, ws, minimatch };
export { prettyMs, ws, wsDefault, minimatch };

View File

@ -1,121 +0,0 @@
import * as plugins from './smartproxy.plugins.js';
import * as net from 'net';
import * as tls from 'tls';
export interface DomainConfig {
domain: string; // glob pattern for domain
allowedIPs: string[]; // glob patterns for IPs allowed to access this domain
}
export interface ProxySettings {
domains: DomainConfig[];
sniEnabled?: boolean;
tlsOptions?: tls.TlsOptions;
defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found
}
export class PortProxy {
netServer: plugins.net.Server;
fromPort: number;
toPort: number;
settings: ProxySettings;
constructor(fromPortArg: number, toPortArg: number, settings: ProxySettings) {
this.fromPort = fromPortArg;
this.toPort = toPortArg;
this.settings = settings;
}
public async start() {
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();
};
const isAllowed = (value: string, patterns: string[]): boolean => {
return patterns.some(pattern => plugins.minimatch(value, pattern));
};
const findMatchingDomain = (serverName: string): DomainConfig | undefined => {
return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
};
const server = this.settings.sniEnabled ? tls.createServer(this.settings.tlsOptions || {}) : net.createServer();
this.netServer = server.on('connection', (from: net.Socket) => {
const remoteIP = from.remoteAddress || '';
if (this.settings.sniEnabled && from instanceof tls.TLSSocket) {
const serverName = (from as any).servername || '';
const domainConfig = findMatchingDomain(serverName);
if (!domainConfig) {
// If no matching domain config found, check default IPs if available
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`);
from.end();
return;
}
} else {
// Check if IP is allowed for this domain
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
from.end();
return;
}
}
} else if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
from.end();
return;
}
const to = net.createConnection({
host: 'localhost',
port: this.toPort,
});
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();
this.netServer.close(() => {
done.resolve();
});
await done.promise;
}
}