Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
422eb5ec40 | |||
45390c4389 | |||
0f2e6d688c | |||
3bd7b70c19 | |||
07a82a09be | |||
23253a2731 | |||
be31a9b553 | |||
a1051f78e8 | |||
aa756bd698 | |||
ff4f44d6fc | |||
63ebad06ea | |||
31e15b65ec | |||
266895ccc5 | |||
dc3d56771b | |||
38601a41bb | |||
a53e6f1019 |
49
changelog.md
49
changelog.md
@ -1,5 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
11
package.json
11
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "3.10.3",
|
||||
"version": "3.14.2",
|
||||
"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",
|
||||
@ -31,6 +31,7 @@
|
||||
"@tsclass/tsclass": "^4.4.0",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/ws": "^8.5.14",
|
||||
"acme-client": "^5.4.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"ws": "^8.18.0"
|
||||
@ -61,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": {
|
||||
|
187
pnpm-lock.yaml
generated
187
pnpm-lock.yaml
generated
@ -32,6 +32,9 @@ 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
|
||||
@ -683,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'}
|
||||
@ -1563,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'}
|
||||
@ -1626,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'}
|
||||
@ -1643,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==}
|
||||
|
||||
@ -3463,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'}
|
||||
@ -3508,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==}
|
||||
|
||||
@ -3897,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==}
|
||||
|
||||
@ -5212,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
|
||||
|
||||
@ -6783,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
|
||||
@ -6834,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:
|
||||
@ -6846,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: {}
|
||||
@ -8954,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
|
||||
@ -9008,6 +9189,8 @@ snapshots:
|
||||
|
||||
readdirp@4.1.1: {}
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
regenerator-runtime@0.14.1: {}
|
||||
|
||||
registry-auth-token@5.0.3:
|
||||
@ -9488,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
189
readme.md
@ -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.
|
||||
|
||||
|
@ -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;
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '3.10.3',
|
||||
description: 'a proxy for handling high workloads of proxying'
|
||||
version: '3.14.2',
|
||||
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
183
ts/classes.iptablesproxy.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ProxyRouter } from './smartproxy.classes.router.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
214
ts/classes.port80handler.ts
Normal 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 Let’s Encrypt’s 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ export interface IDomainConfig {
|
||||
targetIP?: string; // Optional target IP for this domain
|
||||
}
|
||||
|
||||
export interface IProxySettings extends plugins.tls.TlsOptions {
|
||||
export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
||||
fromPort: number;
|
||||
toPort: number;
|
||||
toHost?: string; // Target host to proxy to, defaults to 'localhost'
|
||||
@ -14,6 +14,7 @@ export interface IProxySettings extends plugins.tls.TlsOptions {
|
||||
sniEnabled?: boolean;
|
||||
defaultAllowedIPs?: string[];
|
||||
preserveSourceIP?: boolean;
|
||||
maxConnectionLifetime?: number; // New option (in milliseconds) to force cleanup of long-lived connections
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,12 +80,20 @@ function extractSNI(buffer: Buffer): string | undefined {
|
||||
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 {
|
||||
netServer: plugins.net.Server;
|
||||
settings: IProxySettings;
|
||||
private activeConnections: Set<plugins.net.Socket> = new Set();
|
||||
private incomingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
|
||||
private outgoingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
|
||||
settings: IPortProxySettings;
|
||||
// Unified record tracking each connection pair.
|
||||
private connectionRecords: Set<IConnectionRecord> = new Set();
|
||||
private connectionLogger: NodeJS.Timeout | null = null;
|
||||
|
||||
private terminationStats: {
|
||||
@ -95,10 +104,11 @@ export class PortProxy {
|
||||
outgoing: {},
|
||||
};
|
||||
|
||||
constructor(settings: IProxySettings) {
|
||||
constructor(settingsArg: IPortProxySettings) {
|
||||
this.settings = {
|
||||
...settings,
|
||||
toHost: settings.toHost || 'localhost',
|
||||
...settingsArg,
|
||||
toHost: settingsArg.toHost || 'localhost',
|
||||
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
|
||||
};
|
||||
}
|
||||
|
||||
@ -140,29 +150,29 @@ export class PortProxy {
|
||||
|
||||
this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
|
||||
const remoteIP = socket.remoteAddress || '';
|
||||
this.activeConnections.add(socket);
|
||||
this.incomingConnectionTimes.set(socket, Date.now());
|
||||
console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
|
||||
const connectionRecord: IConnectionRecord = {
|
||||
incoming: socket,
|
||||
outgoing: null,
|
||||
incomingStartTime: Date.now(),
|
||||
connectionClosed: false,
|
||||
};
|
||||
this.connectionRecords.add(connectionRecord);
|
||||
console.log(`New connection from ${remoteIP}. Active connections: ${this.connectionRecords.size}`);
|
||||
|
||||
let initialDataReceived = false;
|
||||
let incomingTerminationReason: string | null = null;
|
||||
let outgoingTerminationReason: string | null = null;
|
||||
let targetSocket: plugins.net.Socket | null = null;
|
||||
let connectionClosed = false;
|
||||
|
||||
// Ensure cleanup happens only once.
|
||||
// Ensure cleanup happens only once for the entire connection record.
|
||||
const cleanupOnce = () => {
|
||||
if (!connectionClosed) {
|
||||
connectionClosed = true;
|
||||
cleanUpSockets(socket, targetSocket || undefined);
|
||||
this.incomingConnectionTimes.delete(socket);
|
||||
if (targetSocket) {
|
||||
this.outgoingConnectionTimes.delete(targetSocket);
|
||||
}
|
||||
if (this.activeConnections.has(socket)) {
|
||||
this.activeConnections.delete(socket);
|
||||
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.activeConnections.size}`);
|
||||
if (!connectionRecord.connectionClosed) {
|
||||
connectionRecord.connectionClosed = true;
|
||||
if (connectionRecord.cleanupTimer) {
|
||||
clearTimeout(connectionRecord.cleanupTimer);
|
||||
}
|
||||
cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined);
|
||||
this.connectionRecords.delete(connectionRecord);
|
||||
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -242,10 +252,10 @@ export class PortProxy {
|
||||
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
|
||||
}
|
||||
|
||||
targetSocket = plugins.net.connect(connectionOptions);
|
||||
if (targetSocket) {
|
||||
this.outgoingConnectionTimes.set(targetSocket, Date.now());
|
||||
}
|
||||
const targetSocket = plugins.net.connect(connectionOptions);
|
||||
connectionRecord.outgoing = targetSocket;
|
||||
connectionRecord.outgoingStartTime = Date.now();
|
||||
|
||||
console.log(
|
||||
`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
|
||||
`${serverName ? ` (SNI: ${serverName})` : ''}`
|
||||
@ -258,6 +268,7 @@ export class PortProxy {
|
||||
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'));
|
||||
@ -280,6 +291,44 @@ export class PortProxy {
|
||||
});
|
||||
socket.on('end', handleClose('incoming'));
|
||||
targetSocket.on('end', handleClose('outgoing'));
|
||||
|
||||
// If maxConnectionLifetime is set, initialize a cleanup timer that will be reset on data flow.
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// Start the cleanup timer.
|
||||
resetCleanupTimer();
|
||||
|
||||
// Listen for data events on both sides and reset the timer when both are active.
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (this.settings.sniEnabled) {
|
||||
@ -314,20 +363,19 @@ export class PortProxy {
|
||||
);
|
||||
});
|
||||
|
||||
// Log active connection count, longest running connection durations,
|
||||
// and termination statistics every 10 seconds.
|
||||
// Every 10 seconds log active connection count and longest running durations.
|
||||
this.connectionLogger = setInterval(() => {
|
||||
const now = Date.now();
|
||||
let maxIncoming = 0;
|
||||
for (const startTime of this.incomingConnectionTimes.values()) {
|
||||
maxIncoming = Math.max(maxIncoming, now - startTime);
|
||||
}
|
||||
let maxOutgoing = 0;
|
||||
for (const startTime of this.outgoingConnectionTimes.values()) {
|
||||
maxOutgoing = Math.max(maxOutgoing, now - startTime);
|
||||
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.activeConnections.size}. ` +
|
||||
`(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)}`
|
@ -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';
|
||||
|
Reference in New Issue
Block a user