Compare commits

...

6 Commits

Author SHA1 Message Date
db2ac5bae3 18.2.0
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 59m10s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-05-18 15:56:52 +00:00
e224f34a81 feat(smartproxy/certificate): Integrate HTTP-01 challenge handler into ACME certificate provisioning workflow 2025-05-18 15:56:52 +00:00
538d22f81b update 2025-05-18 15:51:09 +00:00
01b4a79e1a fix(certificates): simplify approach 2025-05-18 15:38:07 +00:00
8dc6b5d849 add new plan 2025-05-18 15:12:36 +00:00
4e78dade64 new plan 2025-05-18 15:03:11 +00:00
17 changed files with 2811 additions and 1706 deletions

View File

@ -1,5 +1,12 @@
# Changelog
## 2025-05-18 - 18.2.0 - feat(smartproxy/certificate)
Integrate HTTP-01 challenge handler into ACME certificate provisioning workflow
- Added integration of SmartAcme HTTP01 handler to dynamically add and remove a challenge route for ACME certificate requests
- Updated certificate-manager to use the challenge handler for both initial provisioning and renewal
- Improved error handling and logging during certificate issuance, with clear status updates and cleanup of challenge routes
## 2025-05-15 - 18.1.1 - fix(network-proxy/websocket)
Improve WebSocket connection closure and update router integration

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartproxy",
"version": "18.1.1",
"version": "18.2.0",
"private": false,
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
"main": "dist_ts/index.js",
@ -17,15 +17,17 @@
"devDependencies": {
"@git.zone/tsbuild": "^2.5.1",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.2.0",
"@git.zone/tstest": "^1.9.0",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.18",
"typescript": "^5.8.3"
},
"dependencies": {
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartacme": "^7.3.3",
"@push.rocks/smartacme": "^7.3.4",
"@push.rocks/smartcrypto": "^2.0.4",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^11.2.0",
"@push.rocks/smartnetwork": "^4.0.1",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.1.0",

325
pnpm-lock.yaml generated
View File

@ -12,11 +12,17 @@ importers:
specifier: ^6.2.2
version: 6.2.2
'@push.rocks/smartacme':
specifier: ^7.3.3
version: 7.3.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
specifier: ^7.3.4
version: 7.3.4(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
'@push.rocks/smartcrypto':
specifier: ^2.0.4
version: 2.0.4
'@push.rocks/smartdelay':
specifier: ^3.0.5
version: 3.0.5
'@push.rocks/smartfile':
specifier: ^11.2.0
version: 11.2.0
'@push.rocks/smartnetwork':
specifier: ^4.0.1
version: 4.0.1
@ -58,8 +64,8 @@ importers:
specifier: ^1.2.44
version: 1.3.3
'@git.zone/tstest':
specifier: ^1.2.0
version: 1.2.0(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)(typescript@5.8.3)
specifier: ^1.9.0
version: 1.9.0(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)(typescript@5.8.3)
'@push.rocks/tapbundle':
specifier: ^6.0.3
version: 6.0.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
@ -81,9 +87,6 @@ packages:
'@api.global/typedrequest@3.1.10':
resolution: {integrity: sha512-EiCp44XVcMjBvEs4oM1nMUaeY4ySU0Pzt3+mDwVG5DNP6EV87Nwancbr2jKScvaFNel9eeDgGtgEnFBKjOnApA==}
'@api.global/typedserver@3.0.68':
resolution: {integrity: sha512-7o6fkz60ed8q2lmEe44hsu/6kNqG4j5WVgWwmY+a1MmSOUtuu5+VTYYNyc8KrSgtbRBzx4+2A2N31l4wDjcy3w==}
'@api.global/typedserver@3.0.74':
resolution: {integrity: sha512-lrXaCPaVZLihlF9w39pEqTw2kiHFCheRKTZuK07S7gTGyfdXKPmccVR/EK4ox58E1gjh9A2K8yY8ZWGcjuSJkw==}
@ -356,15 +359,12 @@ packages:
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.23.4':
resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==}
'@babel/runtime@7.27.0':
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
'@cloudflare/workers-types@4.20250303.0':
resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==}
'@cloudflare/workers-types@4.20250515.0':
resolution: {integrity: sha512-KoHFMH04gOXp3KEI+wrFIU+3ZfoSXnwqZTpybNQjalHoN3pWjtWBb/030cCRAZ639YX+DAHAxNF7AvEYGz1oaA==}
'@cloudflare/workers-types@4.20250517.0':
resolution: {integrity: sha512-DYttpORVcvKGXI+zQDHBO6r0pDsAc8fkmJg0x1PK7+t0ijfc6nMVnUF/IVJIllScOWWHHCBknCbCmV0eGq2njA==}
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
@ -382,8 +382,8 @@ packages:
'@design.estate/dees-domtools@2.3.2':
resolution: {integrity: sha512-RfXR2t67M9kaCoF6CBkKJtVdsdp6p1O7S1OaWjrs8V0S3277ch4bSYfO+8f+QYweXKkI6Tr2PKaq3PIlwFSC1g==}
'@design.estate/dees-element@2.0.39':
resolution: {integrity: sha512-AQdGU/+GmWmU5M5pDf+GWT7GU8UN073WZvtIkfqQZemxd35HYU1vpi629m8/PjKd5dIHAU7QN2mKb6IQ8anPgw==}
'@design.estate/dees-element@2.0.42':
resolution: {integrity: sha512-1PzHP6q/PtSiu4P0nCxjSeHtRHn62zoSouMy8JFW2h29FT/CSDVaTUAUqYqnvwE/U98aLNivWTmerZitDF7kBQ==}
'@esbuild/aix-ppc64@0.24.2':
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
@ -704,8 +704,8 @@ packages:
resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==}
hasBin: true
'@git.zone/tstest@1.2.0':
resolution: {integrity: sha512-H4/7YKjJLzz0uIO88dB9EcP0r8j/CoDqAWlHVWK78tEHM8foV6EIIcu+zsadZuBWW5SnR77p62YoJFenRdTnGA==}
'@git.zone/tstest@1.9.0':
resolution: {integrity: sha512-UENg0cuXJLhxOJCCVzCn7y3CEY8OMHvqQSuDazZxd5fj5tNXx6RJIYsAEMVoeJeDWQSXYgbt2JW8Yz85kre5MA==}
hasBin: true
'@hapi/bourne@3.0.0':
@ -880,8 +880,8 @@ packages:
'@push.rocks/qenv@6.1.0':
resolution: {integrity: sha512-1FUFMlSVwFSFg8LbqfkzJ2LLP4lMGApUtgOpsvrde6+AxBmB4gjoNgCUH7z3xXfDAtYqcrtSELXBNE0xVL1MqQ==}
'@push.rocks/smartacme@7.3.3':
resolution: {integrity: sha512-48g9V4EpZI8B/YiPseIuB/balH22IMp/p26+DAE57jxvv1hh+PSV4I/UPWCPWP/z7OTtKF+EfEco9TEb5BF2Lw==}
'@push.rocks/smartacme@7.3.4':
resolution: {integrity: sha512-kpXGrz0SeChLGYoXsqszsHnAyZ/8RMt0PhKSO8W+UaokHN/mZ0Nsro0XkMJhLuZyzYQ35SW+ccUblQliok/AoQ==}
'@push.rocks/smartarchive@3.0.8':
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
@ -967,6 +967,9 @@ packages:
'@push.rocks/smartlog@3.0.9':
resolution: {integrity: sha512-B/YIJrwXsbxPkAJly8+55yx3Eqm5bIaCZ/xD2oe6fD8Zp58VLF2P8hpoQZJOiSO+KI7wXVlTEFHsmt8fpRZIVA==}
'@push.rocks/smartlog@3.1.2':
resolution: {integrity: sha512-krjWramvM8R+dY69KoBBsUtsMHKtw7eCdvcg/uYsU6e8gzOfGiQOuWeat39d6doPHbzGuxh6lSOWGUpUTTu6aw==}
'@push.rocks/smartmanifest@2.0.2':
resolution: {integrity: sha512-QGc5C9vunjfUbYsPGz5bynV/mVmPHkrQDkWp8ZO8VJtK1GZe+njgbrNyxn2SUHR0IhSAbSXl1j4JvBqYf5eTVg==}
@ -1039,8 +1042,8 @@ packages:
'@push.rocks/smartsitemap@2.0.3':
resolution: {integrity: sha512-jIcms8V1b2mt3dS4PKNlLR1DRC8pCDWMRVbnyM/2+snZOJZonQRlQzAyX8No0EfLbfdrfnxv2IjPX13X29Re6g==}
'@push.rocks/smartsocket@2.0.27':
resolution: {integrity: sha512-planM3EkBvx/+guLLWHLBNnNp4mcWDwp1k9G0It0ul7IhdaBQsSWzKSQJxym3Bx2dypgA2UIeT3/hRvSa5Nqdw==}
'@push.rocks/smartsocket@2.1.0':
resolution: {integrity: sha512-etOGyfiDFQz/1WJnD3jFL2N7ykujTjiudAz6qZTz82xE5oabKuKX+Cn8SdM9dOwzyWmBUKbUdll8QhovAXjn+g==}
'@push.rocks/smartspawn@3.0.3':
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
@ -1620,14 +1623,11 @@ packages:
'@types/convert-source-map@2.0.3':
resolution: {integrity: sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==}
'@types/cookie@0.4.1':
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
'@types/cookies@0.9.0':
resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==}
'@types/cors@2.8.17':
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
'@types/cors@2.8.18':
resolution: {integrity: sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==}
'@types/debounce@1.2.4':
resolution: {integrity: sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==}
@ -1650,20 +1650,14 @@ packages:
'@types/elliptic@6.4.18':
resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
'@types/express-serve-static-core@4.19.6':
resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==}
'@types/express-serve-static-core@5.0.6':
resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==}
'@types/express@4.17.21':
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
'@types/express@5.0.0':
resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==}
'@types/express@5.0.1':
resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==}
'@types/express@5.0.2':
resolution: {integrity: sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==}
'@types/fast-json-stable-stringify@2.1.2':
resolution: {integrity: sha512-vsxcbfLDdjytnCnHXtinE40Xl46Wr7l/VGRGt7ewJwCPMKEHOdEsTxXX8xwgoR7cbc+6dE8SB4jlMrOV2zAg7g==}
@ -2045,8 +2039,8 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
broadcast-channel@7.0.0:
resolution: {integrity: sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==}
broadcast-channel@7.1.0:
resolution: {integrity: sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==}
brorand@1.1.0:
resolution: {integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=}
@ -2274,14 +2268,14 @@ packages:
cookie-signature@1.0.6:
resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=}
cookie@0.4.2:
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
engines: {node: '>= 0.6'}
cookie@0.7.1:
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
engines: {node: '>= 0.6'}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
cookies@0.9.1:
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
engines: {node: '>= 0.8'}
@ -2507,15 +2501,15 @@ packages:
end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
engine.io-client@6.5.4:
resolution: {integrity: sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==}
engine.io-client@6.6.3:
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
engine.io-parser@5.2.3:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
engine.io@6.5.4:
resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==}
engine.io@6.6.4:
resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==}
engines: {node: '>=10.2.0'}
entities@2.2.0:
@ -2648,8 +2642,8 @@ packages:
resolution: {integrity: sha512-cB507r5T3D55DfclY01GLkninZLfU7HXV/mhVRTnTRm5k2u+fY7Fof2dBkr80p5t7G7dlA/G5dI87QiMdPpMCQ==}
engines: {node: '>=18'}
fake-indexeddb@6.0.0:
resolution: {integrity: sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ==}
fake-indexeddb@6.0.1:
resolution: {integrity: sha512-He2AjQGHe46svIFq5+L2Nx/eHDTI1oKgoevBP+TthnjymXiKkeJQ3+ITeWey99Y5+2OaPFbI1qEsx/5RsGtWnQ==}
engines: {node: '>=18'}
fast-deep-equal@3.1.3:
@ -3255,8 +3249,8 @@ packages:
kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
lenis@1.2.3:
resolution: {integrity: sha512-H3VUn62jvQPfyxGW2F0STJPhP1VzX5r05KtiZ0uxHYD9xtbfTvj2eX/Km26+x13zlaKfHacEe1DTC3ouFrxw+g==}
lenis@1.3.3:
resolution: {integrity: sha512-DOopj/UKHS54E9l2g4BOpDUvsyvkd1zkv+ECtHxQ9Fto8ozzKSz7MccqT+KOyG0ABA/OHXZ7l9INx0peUoQ8rQ==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
react: '>=17.0.0'
@ -3969,8 +3963,8 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
property-information@7.0.0:
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
proto-list@1.2.4:
resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=}
@ -4261,16 +4255,16 @@ packages:
socket.io-adapter@2.5.5:
resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
socket.io-client@4.7.5:
resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==}
socket.io-client@4.8.1:
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
engines: {node: '>=10.0.0'}
socket.io-parser@4.2.4:
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
engines: {node: '>=10.0.0'}
socket.io@4.7.5:
resolution: {integrity: sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==}
socket.io@4.8.1:
resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==}
engines: {node: '>=10.2.0'}
socks-proxy-agent@8.0.5:
@ -4687,18 +4681,6 @@ packages:
utf-8-validate:
optional: true
ws@8.11.0:
resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
@ -4739,8 +4721,8 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xmlhttprequest-ssl@2.0.0:
resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==}
xmlhttprequest-ssl@2.1.2:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
xtend@4.0.2:
@ -4797,57 +4779,12 @@ snapshots:
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstream': 1.0.10
'@api.global/typedserver@3.0.68':
dependencies:
'@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.0.1
'@cloudflare/workers-types': 4.20250303.0
'@design.estate/dees-comms': 1.0.27
'@push.rocks/lik': 6.2.2
'@push.rocks/smartchok': 1.0.34
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12
'@push.rocks/smartfeed': 1.0.11
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2
'@push.rocks/smartmatch': 2.0.0
'@push.rocks/smartmime': 2.0.4
'@push.rocks/smartntml': 2.0.8
'@push.rocks/smartopen': 2.0.0
'@push.rocks/smartpath': 5.0.18
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartsitemap': 2.0.3
'@push.rocks/smartstream': 3.2.5
'@push.rocks/smarttime': 4.1.1
'@push.rocks/taskbuffer': 3.1.7
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstore': 2.0.20
'@tsclass/tsclass': 4.4.4
'@types/express': 4.17.21
body-parser: 1.20.3
cors: 2.8.5
express: 4.21.2
express-force-ssl: 0.3.2
lit: 3.2.1
transitivePeerDependencies:
- '@nuxt/kit'
- react
- supports-color
- vue
'@api.global/typedserver@3.0.74':
dependencies:
'@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.0.1
'@cloudflare/workers-types': 4.20250515.0
'@cloudflare/workers-types': 4.20250517.0
'@design.estate/dees-comms': 1.0.27
'@push.rocks/lik': 6.2.2
'@push.rocks/smartchok': 1.0.34
@ -4856,7 +4793,7 @@ snapshots:
'@push.rocks/smartfeed': 1.0.11
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartlog': 3.0.9
'@push.rocks/smartlog': 3.1.2
'@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2
@ -4875,7 +4812,7 @@ snapshots:
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstore': 2.0.20
'@tsclass/tsclass': 8.2.1
'@types/express': 5.0.1
'@types/express': 5.0.2
body-parser: 1.20.3
cors: 2.8.5
express: 4.21.2
@ -4895,8 +4832,8 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartsocket': 2.0.27
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartsocket': 2.1.0
'@push.rocks/smartstring': 4.0.15
'@push.rocks/smarturl': 3.1.0
transitivePeerDependencies:
@ -4910,7 +4847,7 @@ snapshots:
'@apiclient.xyz/cloudflare@6.4.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.0.9
'@push.rocks/smartlog': 3.1.2
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.0.15
@ -5760,13 +5697,11 @@ snapshots:
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/runtime@7.23.4':
'@babel/runtime@7.27.0':
dependencies:
regenerator-runtime: 0.14.1
'@cloudflare/workers-types@4.20250303.0': {}
'@cloudflare/workers-types@4.20250515.0': {}
'@cloudflare/workers-types@4.20250517.0': {}
'@colors/colors@1.6.0': {}
@ -5785,7 +5720,7 @@ snapshots:
'@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/smartdelay': 3.0.5
broadcast-channel: 7.0.0
broadcast-channel: 7.1.0
'@design.estate/dees-domtools@2.3.2':
dependencies:
@ -5797,15 +5732,15 @@ snapshots:
'@push.rocks/smartmarkdown': 3.0.3
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrouter': 1.3.2
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstate': 2.0.19
'@push.rocks/smartstring': 4.0.15
'@push.rocks/smarturl': 3.1.0
'@push.rocks/webrequest': 3.0.37
'@push.rocks/websetup': 3.0.19
'@push.rocks/webstore': 2.0.20
lenis: 1.2.3
lit: 3.2.1
lenis: 1.3.3
lit: 3.3.0
sweet-scroll: 4.0.0
transitivePeerDependencies:
- '@nuxt/kit'
@ -5813,12 +5748,12 @@ snapshots:
- supports-color
- vue
'@design.estate/dees-element@2.0.39':
'@design.estate/dees-element@2.0.42':
dependencies:
'@design.estate/dees-domtools': 2.3.2
'@push.rocks/isounique': 1.0.5
'@push.rocks/smartrx': 3.0.7
lit: 3.2.1
'@push.rocks/smartrx': 3.0.10
lit: 3.3.0
transitivePeerDependencies:
- '@nuxt/kit'
- react
@ -5999,7 +5934,7 @@ snapshots:
'@push.rocks/smartcli': 4.0.11
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartlog': 3.0.9
'@push.rocks/smartlog': 3.1.2
'@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartpath': 5.0.18
'@push.rocks/smartpromise': 4.2.3
@ -6030,19 +5965,28 @@ snapshots:
'@push.rocks/smartshell': 3.2.3
tsx: 4.19.3
'@git.zone/tstest@1.2.0(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)(typescript@5.8.3)':
'@git.zone/tstest@1.9.0(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)(typescript@5.8.3)':
dependencies:
'@api.global/typedserver': 3.0.74
'@git.zone/tsbundle': 2.2.5
'@git.zone/tsrun': 1.3.3
'@push.rocks/consolecolor': 2.0.2
'@push.rocks/qenv': 6.1.0
'@push.rocks/smartbrowser': 2.0.8(typescript@5.8.3)
'@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12
'@push.rocks/smartexpect': 2.4.2
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartlog': 3.0.9
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartlog': 3.1.2
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
'@push.rocks/smartpath': 5.0.18
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@push.rocks/smarts3': 2.2.5
'@push.rocks/smartshell': 3.2.3
'@push.rocks/tapbundle': 6.0.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
'@push.rocks/smarttime': 4.1.1
'@types/ws': 8.18.1
figures: 6.1.0
ws: 8.18.2
@ -6379,7 +6323,7 @@ snapshots:
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartpath': 5.0.18
'@push.rocks/smartacme@7.3.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
'@push.rocks/smartacme@7.3.4(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
dependencies:
'@api.global/typedserver': 3.0.74
'@apiclient.xyz/cloudflare': 6.4.1
@ -6388,7 +6332,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartdns': 6.2.2
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartlog': 3.0.9
'@push.rocks/smartlog': 3.1.2
'@push.rocks/smartnetwork': 4.0.1
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
@ -6401,8 +6345,6 @@ snapshots:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- '@nuxt/kit'
- aws-crt
- bufferutil
- encoding
- gcp-metadata
- kerberos
@ -6411,7 +6353,6 @@ snapshots:
- snappy
- socks
- supports-color
- utf-8-validate
- vue
'@push.rocks/smartarchive@3.0.8':
@ -6476,7 +6417,7 @@ snapshots:
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartrx': 3.0.10
'@tempfix/watcher': 2.3.0
'@push.rocks/smartcli@4.0.11':
@ -6664,6 +6605,19 @@ snapshots:
'@tsclass/tsclass': 9.2.0
ora: 8.2.0
'@push.rocks/smartlog@3.1.2':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/consolecolor': 2.0.2
'@push.rocks/isounique': 1.0.5
'@push.rocks/smartclickhouse': 2.0.17
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smarthash': 3.0.4
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smarttime': 4.1.1
'@push.rocks/webrequest': 3.0.37
'@tsclass/tsclass': 9.2.0
'@push.rocks/smartmanifest@2.0.2': {}
'@push.rocks/smartmarkdown@3.0.3':
@ -6751,10 +6705,10 @@ snapshots:
'@push.rocks/smartntml@2.0.8':
dependencies:
'@design.estate/dees-element': 2.0.39
'@design.estate/dees-element': 2.0.42
'@happy-dom/global-registrator': 15.11.7
'@push.rocks/smartpromise': 4.2.3
fake-indexeddb: 6.0.0
fake-indexeddb: 6.0.1
transitivePeerDependencies:
- '@nuxt/kit'
- react
@ -6783,7 +6737,7 @@ snapshots:
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.8.3)
'@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 4.4.4
'@types/express': 5.0.1
'@types/express': 5.0.2
express: 4.21.2
pdf-lib: 1.17.1
pdf2json: 3.1.5
@ -6824,7 +6778,7 @@ snapshots:
'@push.rocks/smartrouter@1.3.2':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartrx': 3.0.10
path-to-regexp: 8.2.0
'@push.rocks/smartrx@3.0.10':
@ -6867,23 +6821,23 @@ snapshots:
'@push.rocks/webrequest': 3.0.37
'@tsclass/tsclass': 4.4.4
'@push.rocks/smartsocket@2.0.27':
'@push.rocks/smartsocket@2.1.0':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedserver': 3.0.68
'@api.global/typedserver': 3.0.74
'@push.rocks/isohash': 2.0.1
'@push.rocks/isounique': 1.0.5
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartlog': 3.1.2
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1
engine.io: 6.5.4
socket.io: 4.7.5
socket.io-client: 4.7.5
engine.io: 6.6.4
socket.io: 4.8.1
socket.io-client: 4.8.1
transitivePeerDependencies:
- '@nuxt/kit'
- bufferutil
@ -6907,7 +6861,7 @@ snapshots:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartrx': 3.0.10
'@push.rocks/webstore': 2.0.20
'@push.rocks/smartstream@2.0.8':
@ -6924,7 +6878,7 @@ snapshots:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 5.0.12
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstring@4.0.15':
dependencies:
@ -7871,8 +7825,6 @@ snapshots:
'@types/convert-source-map@2.0.3': {}
'@types/cookie@0.4.1': {}
'@types/cookies@0.9.0':
dependencies:
'@types/connect': 3.4.38
@ -7880,7 +7832,7 @@ snapshots:
'@types/keygrip': 1.0.6
'@types/node': 22.15.18
'@types/cors@2.8.17':
'@types/cors@2.8.18':
dependencies:
'@types/node': 22.15.18
@ -7904,13 +7856,6 @@ snapshots:
dependencies:
'@types/bn.js': 5.1.6
'@types/express-serve-static-core@4.19.6':
dependencies:
'@types/node': 22.15.18
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 22.15.18
@ -7918,13 +7863,6 @@ snapshots:
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
'@types/express@4.17.21':
dependencies:
'@types/body-parser': 1.19.5
'@types/express-serve-static-core': 4.19.6
'@types/qs': 6.9.18
'@types/serve-static': 1.15.7
'@types/express@5.0.0':
dependencies:
'@types/body-parser': 1.19.5
@ -7932,7 +7870,7 @@ snapshots:
'@types/qs': 6.9.18
'@types/serve-static': 1.15.7
'@types/express@5.0.1':
'@types/express@5.0.2':
dependencies:
'@types/body-parser': 1.19.5
'@types/express-serve-static-core': 5.0.6
@ -8390,9 +8328,9 @@ snapshots:
dependencies:
fill-range: 7.1.1
broadcast-channel@7.0.0:
broadcast-channel@7.1.0:
dependencies:
'@babel/runtime': 7.23.4
'@babel/runtime': 7.27.0
oblivious-set: 1.4.0
p-queue: 6.6.2
unload: 2.4.1
@ -8625,10 +8563,10 @@ snapshots:
cookie-signature@1.0.6: {}
cookie@0.4.2: {}
cookie@0.7.1: {}
cookie@0.7.2: {}
cookies@0.9.1:
dependencies:
depd: 2.0.0
@ -8813,13 +8751,13 @@ snapshots:
dependencies:
once: 1.4.0
engine.io-client@6.5.4:
engine.io-client@6.6.3:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-parser: 5.2.3
ws: 8.17.1
xmlhttprequest-ssl: 2.0.0
xmlhttprequest-ssl: 2.1.2
transitivePeerDependencies:
- bufferutil
- supports-color
@ -8827,18 +8765,17 @@ snapshots:
engine.io-parser@5.2.3: {}
engine.io@6.5.4:
engine.io@6.6.4:
dependencies:
'@types/cookie': 0.4.1
'@types/cors': 2.8.17
'@types/cors': 2.8.18
'@types/node': 22.15.18
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.4.2
cookie: 0.7.2
cors: 2.8.5
debug: 4.3.7
engine.io-parser: 5.2.3
ws: 8.11.0
ws: 8.17.1
transitivePeerDependencies:
- bufferutil
- supports-color
@ -9037,7 +8974,7 @@ snapshots:
fake-indexeddb@5.0.2: {}
fake-indexeddb@6.0.0: {}
fake-indexeddb@6.0.1: {}
fast-deep-equal@3.1.3: {}
@ -9371,7 +9308,7 @@ snapshots:
hast-util-whitespace: 3.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
property-information: 7.0.0
property-information: 7.1.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
zwitch: 2.0.4
@ -9753,7 +9690,7 @@ snapshots:
kuler@2.0.0: {}
lenis@1.2.3: {}
lenis@1.3.3: {}
lines-and-columns@1.2.4: {}
@ -10213,7 +10150,7 @@ snapshots:
micromark@4.0.2:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.0
debug: 4.4.1
decode-named-character-reference: 1.1.0
devlop: 1.1.0
micromark-core-commonmark: 2.0.3
@ -10612,7 +10549,7 @@ snapshots:
progress@2.0.3: {}
property-information@7.0.0: {}
property-information@7.1.0: {}
proto-list@1.2.4: {}
@ -11002,11 +10939,11 @@ snapshots:
- supports-color
- utf-8-validate
socket.io-client@4.7.5:
socket.io-client@4.8.1:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.3.7
engine.io-client: 6.5.4
engine.io-client: 6.6.3
socket.io-parser: 4.2.4
transitivePeerDependencies:
- bufferutil
@ -11020,13 +10957,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
socket.io@4.7.5:
socket.io@4.8.1:
dependencies:
accepts: 1.3.8
base64id: 2.0.0
cors: 2.8.5
debug: 4.3.7
engine.io: 6.5.4
engine.io: 6.6.4
socket.io-adapter: 2.5.5
socket.io-parser: 4.2.4
transitivePeerDependencies:
@ -11448,8 +11385,6 @@ snapshots:
ws@7.5.10: {}
ws@8.11.0: {}
ws@8.17.1: {}
ws@8.18.2: {}
@ -11472,7 +11407,7 @@ snapshots:
xmlbuilder@11.0.1: {}
xmlhttprequest-ssl@2.0.0: {}
xmlhttprequest-ssl@2.1.2: {}
xtend@4.0.2: {}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
# ACME/Certificate Simplification Summary
## What Was Done
We successfully implemented the ACME/Certificate simplification plan for SmartProxy:
### 1. Created New Certificate Management System
- **SmartCertManager** (`ts/proxies/smart-proxy/certificate-manager.ts`): A unified certificate manager that handles both ACME and static certificates
- **CertStore** (`ts/proxies/smart-proxy/cert-store.ts`): File-based certificate storage system
### 2. Updated Route Types
- Added `IRouteAcme` interface for ACME configuration
- Added `IStaticResponse` interface for static route responses
- Extended `IRouteTls` with comprehensive certificate options
- Added `handler` property to `IRouteAction` for static routes
### 3. Implemented Static Route Handler
- Added `handleStaticAction` method to route-connection-handler.ts
- Added support for 'static' route type in the action switch statement
- Implemented proper HTTP response formatting
### 4. Updated SmartProxy Integration
- Removed old CertProvisioner and Port80Handler dependencies
- Added `initializeCertificateManager` method
- Updated `start` and `stop` methods to use new certificate manager
- Added `provisionCertificate`, `renewCertificate`, and `getCertificateStatus` methods
### 5. Simplified NetworkProxyBridge
- Removed all certificate-related logic
- Simplified to only handle network proxy forwarding
- Updated to use port-based matching for network proxy routes
### 6. Cleaned Up HTTP Module
- Removed exports for port80 subdirectory
- Kept only router and redirect functionality
### 7. Created Tests
- Created simplified test for certificate functionality
- Test demonstrates static route handling and basic certificate configuration
## Key Improvements
1. **No Backward Compatibility**: Clean break from legacy implementations
2. **Direct SmartAcme Integration**: Uses @push.rocks/smartacme directly without custom wrappers
3. **Route-Based ACME Challenges**: No separate HTTP server needed
4. **Simplified Architecture**: Removed unnecessary abstraction layers
5. **Unified Configuration**: Certificate configuration is part of route definitions
## Configuration Example
```typescript
const proxy = new SmartProxy({
routes: [{
name: 'secure-site',
match: { ports: 443, domains: 'example.com' },
action: {
type: 'forward',
target: { host: 'backend', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'admin@example.com',
useProduction: true
}
}
}
}]
});
```
## Next Steps
1. Remove old certificate module and port80 directory
2. Update documentation with new configuration format
3. Test with real ACME certificates in staging environment
4. Add more comprehensive tests for renewal and edge cases
The implementation is complete and builds successfully!

View File

@ -1,390 +1,141 @@
/**
* Tests for certificate provisioning with route-based configuration
*/
import { expect, tap } from '@push.rocks/tapbundle';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import * as plugins from '../ts/plugins.js';
// Import from core modules
import { CertProvisioner } from '../ts/certificate/providers/cert-provisioner.js';
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
import { createCertificateProvisioner } from '../ts/certificate/index.js';
import type { ISmartProxyOptions } from '../ts/proxies/smart-proxy/models/interfaces.js';
import { expect, tap } from '@push.rocks/tapbundle';
// Extended options interface for testing - allows us to map ports for testing
interface TestSmartProxyOptions extends ISmartProxyOptions {
portMap?: Record<number, number>; // Map standard ports to non-privileged ones for testing
}
// Import route helpers
import {
createHttpsTerminateRoute,
createCompleteHttpsServer,
createHttpRoute
} from '../ts/proxies/smart-proxy/utils/route-helpers.js';
// Import test helpers
import { loadTestCertificates } from './helpers/certificates.js';
// Create temporary directory for certificates
const tempDir = path.join(os.tmpdir(), `smartproxy-test-${Date.now()}`);
fs.mkdirSync(tempDir, { recursive: true });
// Mock Port80Handler class that extends EventEmitter
class MockPort80Handler extends plugins.EventEmitter {
public domainsAdded: string[] = [];
addDomain(opts: { domainName: string; sslRedirect: boolean; acmeMaintenance: boolean }) {
this.domainsAdded.push(opts.domainName);
return true;
}
async renewCertificate(domain: string): Promise<void> {
// In a real implementation, this would trigger certificate renewal
console.log(`Mock certificate renewal for ${domain}`);
}
}
// Mock NetworkProxyBridge
class MockNetworkProxyBridge {
public appliedCerts: any[] = [];
applyExternalCertificate(cert: any) {
this.appliedCerts.push(cert);
}
}
tap.test('CertProvisioner: Should extract certificate domains from routes', async () => {
// Create routes with domains requiring certificates
const routes = [
createHttpsTerminateRoute('example.com', { host: 'localhost', port: 8080 }, {
certificate: 'auto'
}),
createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 8081 }, {
certificate: 'auto'
}),
createHttpsTerminateRoute('api.example.com', { host: 'localhost', port: 8082 }, {
certificate: 'auto'
}),
// This route shouldn't require a certificate (passthrough)
createHttpsTerminateRoute('passthrough.example.com', { host: 'localhost', port: 8083 }, {
certificate: 'auto', // Will be ignored for passthrough
httpsPort: 4443,
const testProxy = new SmartProxy({
routes: [{
name: 'test-route',
match: { ports: 443, domains: 'test.example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'passthrough'
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'test@example.com',
useProduction: false
}
}
}),
// This route shouldn't require a certificate (static certificate provided)
createHttpsTerminateRoute('static-cert.example.com', { host: 'localhost', port: 8084 }, {
certificate: {
key: 'test-key',
cert: 'test-cert'
}
})
];
// Create mocks
const mockPort80 = new MockPort80Handler();
const mockBridge = new MockNetworkProxyBridge();
// Create certificate provisioner
const certProvisioner = new CertProvisioner(
routes,
mockPort80 as any,
mockBridge as any
);
// Get routes that require certificate provisioning
const extractedDomains = (certProvisioner as any).extractCertificateRoutesFromRoutes(routes);
// Validate extraction
expect(extractedDomains).toBeInstanceOf(Array);
expect(extractedDomains.length).toBeGreaterThan(0); // Should extract at least some domains
// Check that the correct domains were extracted
const domains = extractedDomains.map(item => item.domain);
expect(domains).toInclude('example.com');
expect(domains).toInclude('secure.example.com');
expect(domains).toInclude('api.example.com');
// NOTE: Since we're now using createHttpsTerminateRoute for the passthrough domain
// and we've set certificate: 'auto', the domain will be included
// but will use passthrough mode for TLS
expect(domains).toInclude('passthrough.example.com');
// NOTE: The current implementation extracts all domains with terminate mode,
// including those with static certificates. This is different from our expectation,
// but we'll update the test to match the actual implementation.
expect(domains).toInclude('static-cert.example.com');
});
tap.test('CertProvisioner: Should handle wildcard domains in routes', async () => {
// Create routes with wildcard domains
const routes = [
createHttpsTerminateRoute('*.example.com', { host: 'localhost', port: 8080 }, {
certificate: 'auto'
}),
createHttpsTerminateRoute('example.org', { host: 'localhost', port: 8081 }, {
certificate: 'auto'
}),
createHttpsTerminateRoute(['api.example.net', 'app.example.net'], { host: 'localhost', port: 8082 }, {
certificate: 'auto'
})
];
// Create mocks
const mockPort80 = new MockPort80Handler();
const mockBridge = new MockNetworkProxyBridge();
// Create custom certificate provisioner function
const customCertFunc = async (domain: string) => {
// Always return a static certificate for testing
return {
domainName: domain,
publicKey: 'TEST-CERT',
privateKey: 'TEST-KEY',
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000,
created: Date.now(),
csr: 'TEST-CSR',
id: 'TEST-ID',
};
};
// Create certificate provisioner with custom cert function
const certProvisioner = new CertProvisioner(
routes,
mockPort80 as any,
mockBridge as any,
customCertFunc
);
// Get routes that require certificate provisioning
const extractedDomains = (certProvisioner as any).extractCertificateRoutesFromRoutes(routes);
// Validate extraction
expect(extractedDomains).toBeInstanceOf(Array);
// Check that the correct domains were extracted
const domains = extractedDomains.map(item => item.domain);
expect(domains).toInclude('*.example.com');
expect(domains).toInclude('example.org');
expect(domains).toInclude('api.example.net');
expect(domains).toInclude('app.example.net');
});
tap.test('CertProvisioner: Should provision certificates for routes', async () => {
const testCerts = loadTestCertificates();
// Create the custom provisioner function
const mockProvisionFunction = async (domain: string) => {
return {
domainName: domain,
publicKey: testCerts.publicKey,
privateKey: testCerts.privateKey,
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000,
created: Date.now(),
csr: 'TEST-CSR',
id: 'TEST-ID',
};
};
// Create routes with domains requiring certificates
const routes = [
createHttpsTerminateRoute('example.com', { host: 'localhost', port: 8080 }, {
certificate: 'auto'
}),
createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 8081 }, {
certificate: 'auto'
})
];
// Create mocks
const mockPort80 = new MockPort80Handler();
const mockBridge = new MockNetworkProxyBridge();
// Create certificate provisioner with mock provider
const certProvisioner = new CertProvisioner(
routes,
mockPort80 as any,
mockBridge as any,
mockProvisionFunction
);
// Create an events array to catch certificate events
const events: any[] = [];
certProvisioner.on('certificate', (event) => {
events.push(event);
});
// Start the provisioner (which will trigger initial provisioning)
await certProvisioner.start();
// Verify certificates were provisioned (static provision flow)
expect(mockBridge.appliedCerts.length).toBeGreaterThanOrEqual(2);
expect(events.length).toBeGreaterThanOrEqual(2);
// Check that each domain received a certificate
const certifiedDomains = events.map(e => e.domain);
expect(certifiedDomains).toInclude('example.com');
expect(certifiedDomains).toInclude('secure.example.com');
// Important: stop the provisioner to clean up any timers or listeners
await certProvisioner.stop();
});
tap.test('SmartProxy: Should handle certificate provisioning through routes', async () => {
// Skip this test in CI environments where we can't bind to the needed ports
if (process.env.CI) {
console.log('Skipping SmartProxy certificate test in CI environment');
return;
}
// Create test certificates
const testCerts = loadTestCertificates();
// Create mock cert provision function
const mockProvisionFunction = async (domain: string) => {
return {
domainName: domain,
publicKey: testCerts.publicKey,
privateKey: testCerts.privateKey,
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000,
created: Date.now(),
csr: 'TEST-CSR',
id: 'TEST-ID',
};
};
// Create routes for testing
const routes = [
// HTTPS with auto certificate
createHttpsTerminateRoute('auto.example.com', { host: 'localhost', port: 8080 }, {
certificate: 'auto'
}),
// HTTPS with static certificate
createHttpsTerminateRoute('static.example.com', { host: 'localhost', port: 8081 }, {
certificate: {
key: testCerts.privateKey,
cert: testCerts.publicKey
}
}),
// Complete HTTPS server with auto certificate
...createCompleteHttpsServer('auto-complete.example.com', { host: 'localhost', port: 8082 }, {
certificate: 'auto'
}),
// API route with auto certificate - using createHttpRoute with HTTPS options
createHttpsTerminateRoute('auto-api.example.com', { host: 'localhost', port: 8083 }, {
certificate: 'auto',
match: { path: '/api/*' }
})
];
try {
// Create a minimal server to act as a target for testing
// This will be used in unit testing only, not in production
const mockTarget = new class {
server = plugins.http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Mock target server');
});
start() {
return new Promise<void>((resolve) => {
this.server.listen(8080, () => resolve());
});
}
stop() {
return new Promise<void>((resolve) => {
this.server.close(() => resolve());
});
}
};
// Start the mock target
await mockTarget.start();
// Create a SmartProxy instance that can avoid binding to privileged ports
// and using a mock certificate provisioner for testing
const proxy = new SmartProxy({
// Configure routes
routes,
// Certificate provisioning settings
certProvisionFunction: mockProvisionFunction,
acme: {
enabled: true,
accountEmail: 'test@bleu.de',
useProduction: false, // Use staging
certificateStore: tempDir
}
});
// Track certificate events
const events: any[] = [];
proxy.on('certificate', (event) => {
events.push(event);
});
// Instead of starting the actual proxy which tries to bind to ports,
// just test the initialization part that handles the certificate configuration
// We can't access private certProvisioner directly,
// so just use dummy events for testing
console.log(`Test would provision certificates if actually started`);
// Add some dummy events for testing
proxy.emit('certificate', {
domain: 'auto.example.com',
certificate: 'test-cert',
privateKey: 'test-key',
expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
source: 'test'
});
proxy.emit('certificate', {
domain: 'auto-complete.example.com',
certificate: 'test-cert',
privateKey: 'test-key',
expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
source: 'test'
});
// Give time for events to finalize
await new Promise(resolve => setTimeout(resolve, 100));
// Verify certificates were set up - this test might be skipped due to permissions
// For unit testing, we're only testing the routes are set up properly
// The errors in the log are expected in non-root environments and can be ignored
// Stop the mock target server
await mockTarget.stop();
// Instead of directly accessing the private certProvisioner property,
// we'll call the public stop method which will clean up internal resources
await proxy.stop();
} catch (err) {
if (err.code === 'EACCES') {
console.log('Skipping test: EACCES error (needs privileged ports)');
} else {
console.error('Error in SmartProxy test:', err);
throw err;
}
}
}]
});
tap.test('cleanup', async () => {
try {
fs.rmSync(tempDir, { recursive: true, force: true });
console.log('Temporary directory cleaned up:', tempDir);
} catch (err) {
console.error('Error cleaning up:', err);
}
tap.test('should provision certificate automatically', async () => {
await testProxy.start();
// Wait for certificate provisioning
await new Promise(resolve => setTimeout(resolve, 5000));
const status = testProxy.getCertificateStatus('test-route');
expect(status).toBeDefined();
expect(status.status).toEqual('valid');
expect(status.source).toEqual('acme');
await testProxy.stop();
});
export default tap.start();
tap.test('should handle static certificates', async () => {
const proxy = new SmartProxy({
routes: [{
name: 'static-route',
match: { ports: 443, domains: 'static.example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: {
certFile: './test/fixtures/cert.pem',
keyFile: './test/fixtures/key.pem'
}
}
}
}]
});
await proxy.start();
const status = proxy.getCertificateStatus('static-route');
expect(status).toBeDefined();
expect(status.status).toEqual('valid');
expect(status.source).toEqual('static');
await proxy.stop();
});
tap.test('should handle ACME challenge routes', async () => {
const proxy = new SmartProxy({
routes: [{
name: 'auto-cert-route',
match: { ports: 443, domains: 'acme.example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'acme@example.com',
useProduction: false,
challengePort: 80
}
}
}
}, {
name: 'port-80-route',
match: { ports: 80, domains: 'acme.example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 }
}
}]
});
await proxy.start();
// The SmartCertManager should automatically add challenge routes
// Let's verify the route manager sees them
const routes = proxy.routeManager.getAllRoutes();
const challengeRoute = routes.find(r => r.name === 'acme-challenge');
expect(challengeRoute).toBeDefined();
expect(challengeRoute?.match.path).toEqual('/.well-known/acme-challenge/*');
expect(challengeRoute?.priority).toEqual(1000);
await proxy.stop();
});
tap.test('should renew certificates', async () => {
const proxy = new SmartProxy({
routes: [{
name: 'renew-route',
match: { ports: 443, domains: 'renew.example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'renew@example.com',
useProduction: false,
renewBeforeDays: 30
}
}
}
}]
});
await proxy.start();
// Force renewal
await proxy.renewCertificate('renew-route');
const status = proxy.getCertificateStatus('renew-route');
expect(status).toBeDefined();
expect(status.status).toEqual('valid');
await proxy.stop();
});
tap.start();

View File

@ -0,0 +1,65 @@
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
import { expect, tap } from '@push.rocks/tapbundle';
tap.test('should create SmartProxy with certificate routes', async () => {
const proxy = new SmartProxy({
routes: [{
name: 'test-route',
match: { ports: 8443, domains: 'test.example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'test@example.com',
useProduction: false
}
}
}
}]
});
expect(proxy).toBeDefined();
expect(proxy.settings.routes.length).toEqual(1);
});
tap.test('should handle static route type', async () => {
// Create a test route with static handler
const testResponse = {
status: 200,
headers: { 'Content-Type': 'text/plain' },
body: 'Hello from static route'
};
const proxy = new SmartProxy({
routes: [{
name: 'static-test',
match: { ports: 8080, path: '/test' },
action: {
type: 'static',
handler: async () => testResponse
}
}]
});
const route = proxy.settings.routes[0];
expect(route.action.type).toEqual('static');
expect(route.action.handler).toBeDefined();
// Test the handler
const result = await route.action.handler!({
port: 8080,
path: '/test',
clientIp: '127.0.0.1',
serverIp: '127.0.0.1',
isTls: false,
timestamp: Date.now(),
connectionId: 'test-123'
});
expect(result).toEqual(testResponse);
});
tap.start();

View File

@ -0,0 +1,50 @@
import * as plugins from '../ts/plugins.js';
import { tap } from '@push.rocks/tapbundle';
import { SmartCertManager } from '../ts/proxies/smart-proxy/certificate-manager.js';
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
let certManager: SmartCertManager;
tap.test('should create a SmartCertManager instance', async () => {
const routes: IRouteConfig[] = [
{
name: 'test-acme-route',
match: {
domains: ['test.example.com']
},
action: {
type: 'proxy',
target: 'http://localhost:3000',
tls: {
mode: 'terminate',
certificate: 'auto'
},
acme: {
email: 'test@example.com'
}
}
}
];
certManager = new SmartCertManager(routes, './test-certs', {
email: 'test@example.com',
useProduction: false
});
// Just verify it creates without error
expect(certManager).toBeInstanceOf(SmartCertManager);
});
tap.test('should verify SmartAcme handlers are accessible', async () => {
// Test that we can access SmartAcme handlers
const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
expect(http01Handler).toBeDefined();
});
tap.test('should verify SmartAcme cert managers are accessible', async () => {
// Test that we can access SmartAcme cert managers
const memoryCertManager = new plugins.smartacme.certmanagers.MemoryCertManager();
expect(memoryCertManager).toBeDefined();
});
tap.start();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '18.1.1',
version: '18.2.0',
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
}

View File

@ -5,19 +5,12 @@
// Export types and models
export * from './models/http-types.js';
// Export submodules
export * from './port80/index.js';
// Export submodules (remove port80 export)
export * from './router/index.js';
export * from './redirects/index.js';
// REMOVED: export * from './port80/index.js';
// Import the components we need for the namespace
import { Port80Handler } from './port80/port80-handler.js';
import { ChallengeResponder } from './port80/challenge-responder.js';
// Convenience namespace exports
// Convenience namespace exports (no more Port80)
export const Http = {
Port80: {
Handler: Port80Handler,
ChallengeResponder: ChallengeResponder
}
};
// Only router and redirect functionality remain
};

View File

@ -21,7 +21,8 @@ import * as smartdelay from '@push.rocks/smartdelay';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartstring from '@push.rocks/smartstring';
import * as smartfile from '@push.rocks/smartfile';
import * as smartcrypto from '@push.rocks/smartcrypto';
import * as smartacme from '@push.rocks/smartacme';
import * as smartacmePlugins from '@push.rocks/smartacme/dist_ts/smartacme.plugins.js';
import * as smartacmeHandlers from '@push.rocks/smartacme/dist_ts/handlers/index.js';
@ -33,6 +34,8 @@ export {
smartrequest,
smartpromise,
smartstring,
smartfile,
smartcrypto,
smartacme,
smartacmePlugins,
smartacmeHandlers,

View File

@ -0,0 +1,86 @@
import * as plugins from '../../plugins.js';
import type { ICertificateData } from './certificate-manager.js';
export class CertStore {
constructor(private certDir: string) {}
public async initialize(): Promise<void> {
await plugins.smartfile.fs.ensureDirSync(this.certDir);
}
public async getCertificate(routeName: string): Promise<ICertificateData | null> {
const certPath = this.getCertPath(routeName);
const metaPath = `${certPath}/meta.json`;
if (!await plugins.smartfile.fs.fileExistsSync(metaPath)) {
return null;
}
try {
const metaFile = await plugins.smartfile.SmartFile.fromFilePath(metaPath);
const meta = JSON.parse(metaFile.contents.toString());
const certFile = await plugins.smartfile.SmartFile.fromFilePath(`${certPath}/cert.pem`);
const cert = certFile.contents.toString();
const keyFile = await plugins.smartfile.SmartFile.fromFilePath(`${certPath}/key.pem`);
const key = keyFile.contents.toString();
let ca: string | undefined;
const caPath = `${certPath}/ca.pem`;
if (await plugins.smartfile.fs.fileExistsSync(caPath)) {
const caFile = await plugins.smartfile.SmartFile.fromFilePath(caPath);
ca = caFile.contents.toString();
}
return {
cert,
key,
ca,
expiryDate: new Date(meta.expiryDate),
issueDate: new Date(meta.issueDate)
};
} catch (error) {
console.error(`Failed to load certificate for ${routeName}: ${error}`);
return null;
}
}
public async saveCertificate(
routeName: string,
certData: ICertificateData
): Promise<void> {
const certPath = this.getCertPath(routeName);
await plugins.smartfile.fs.ensureDirSync(certPath);
// Save certificate files
await plugins.smartfile.memory.toFs(certData.cert, `${certPath}/cert.pem`);
await plugins.smartfile.memory.toFs(certData.key, `${certPath}/key.pem`);
if (certData.ca) {
await plugins.smartfile.memory.toFs(certData.ca, `${certPath}/ca.pem`);
}
// Save metadata
const meta = {
expiryDate: certData.expiryDate.toISOString(),
issueDate: certData.issueDate.toISOString(),
savedAt: new Date().toISOString()
};
await plugins.smartfile.memory.toFs(JSON.stringify(meta, null, 2), `${certPath}/meta.json`);
}
public async deleteCertificate(routeName: string): Promise<void> {
const certPath = this.getCertPath(routeName);
if (await plugins.smartfile.fs.fileExistsSync(certPath)) {
await plugins.smartfile.fs.removeManySync([certPath]);
}
}
private getCertPath(routeName: string): string {
// Sanitize route name for filesystem
const safeName = routeName.replace(/[^a-zA-Z0-9-_]/g, '_');
return `${this.certDir}/${safeName}`;
}
}

View File

@ -0,0 +1,506 @@
import * as plugins from '../../plugins.js';
import { NetworkProxy } from '../network-proxy/index.js';
import type { IRouteConfig, IRouteTls } from './models/route-types.js';
import { CertStore } from './cert-store.js';
export interface ICertStatus {
domain: string;
status: 'valid' | 'pending' | 'expired' | 'error';
expiryDate?: Date;
issueDate?: Date;
source: 'static' | 'acme';
error?: string;
}
export interface ICertificateData {
cert: string;
key: string;
ca?: string;
expiryDate: Date;
issueDate: Date;
}
export class SmartCertManager {
private certStore: CertStore;
private smartAcme: plugins.smartacme.SmartAcme | null = null;
private networkProxy: NetworkProxy | null = null;
private renewalTimer: NodeJS.Timeout | null = null;
private pendingChallenges: Map<string, string> = new Map();
private challengeRoute: IRouteConfig | null = null;
// Track certificate status by route name
private certStatus: Map<string, ICertStatus> = new Map();
// Callback to update SmartProxy routes for challenges
private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
constructor(
private routes: IRouteConfig[],
private certDir: string = './certs',
private acmeOptions?: {
email?: string;
useProduction?: boolean;
port?: number;
}
) {
this.certStore = new CertStore(certDir);
}
public setNetworkProxy(networkProxy: NetworkProxy): void {
this.networkProxy = networkProxy;
}
/**
* Set callback for updating routes (used for challenge routes)
*/
public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
this.updateRoutesCallback = callback;
}
/**
* Initialize certificate manager and provision certificates for all routes
*/
public async initialize(): Promise<void> {
// Create certificate directory if it doesn't exist
await this.certStore.initialize();
// Initialize SmartAcme if we have any ACME routes
const hasAcmeRoutes = this.routes.some(r =>
r.action.tls?.certificate === 'auto'
);
if (hasAcmeRoutes && this.acmeOptions?.email) {
// Create HTTP-01 challenge handler
const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
// Set up challenge handler integration with our routing
this.setupChallengeHandler(http01Handler);
// Create SmartAcme instance with built-in MemoryCertManager and HTTP-01 handler
this.smartAcme = new plugins.smartacme.SmartAcme({
accountEmail: this.acmeOptions.email,
environment: this.acmeOptions.useProduction ? 'production' : 'integration',
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
challengeHandlers: [http01Handler]
});
await this.smartAcme.start();
}
// Provision certificates for all routes
await this.provisionAllCertificates();
// Start renewal timer
this.startRenewalTimer();
}
/**
* Provision certificates for all routes that need them
*/
private async provisionAllCertificates(): Promise<void> {
const certRoutes = this.routes.filter(r =>
r.action.tls?.mode === 'terminate' ||
r.action.tls?.mode === 'terminate-and-reencrypt'
);
for (const route of certRoutes) {
try {
await this.provisionCertificate(route);
} catch (error) {
console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
}
}
}
/**
* Provision certificate for a single route
*/
public async provisionCertificate(route: IRouteConfig): Promise<void> {
const tls = route.action.tls;
if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
return;
}
const domains = this.extractDomainsFromRoute(route);
if (domains.length === 0) {
console.warn(`Route ${route.name} has TLS termination but no domains`);
return;
}
const primaryDomain = domains[0];
if (tls.certificate === 'auto') {
// ACME certificate
await this.provisionAcmeCertificate(route, domains);
} else if (typeof tls.certificate === 'object') {
// Static certificate
await this.provisionStaticCertificate(route, primaryDomain, tls.certificate);
}
}
/**
* Provision ACME certificate
*/
private async provisionAcmeCertificate(
route: IRouteConfig,
domains: string[]
): Promise<void> {
if (!this.smartAcme) {
throw new Error('SmartAcme not initialized');
}
const primaryDomain = domains[0];
const routeName = route.name || primaryDomain;
// Check if we already have a valid certificate
const existingCert = await this.certStore.getCertificate(routeName);
if (existingCert && this.isCertificateValid(existingCert)) {
console.log(`Using existing valid certificate for ${primaryDomain}`);
await this.applyCertificate(primaryDomain, existingCert);
this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
return;
}
console.log(`Requesting ACME certificate for ${domains.join(', ')}`);
this.updateCertStatus(routeName, 'pending', 'acme');
try {
// Add challenge route before requesting certificate
await this.addChallengeRoute();
try {
// Use smartacme to get certificate
const cert = await this.smartAcme.getCertificateForDomain(primaryDomain);
// SmartAcme's Cert object has these properties:
// - publicKey: The certificate PEM string
// - privateKey: The private key PEM string
// - csr: Certificate signing request
// - validUntil: Timestamp in milliseconds
// - domainName: The domain name
const certData: ICertificateData = {
cert: cert.publicKey,
key: cert.privateKey,
ca: cert.publicKey, // Use same as cert for now
expiryDate: new Date(cert.validUntil),
issueDate: new Date(cert.created)
};
await this.certStore.saveCertificate(routeName, certData);
await this.applyCertificate(primaryDomain, certData);
this.updateCertStatus(routeName, 'valid', 'acme', certData);
console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
} catch (error) {
console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
throw error;
} finally {
// Always remove challenge route after provisioning
await this.removeChallengeRoute();
}
} catch (error) {
// Handle outer try-catch from adding challenge route
console.error(`Failed to setup ACME challenge for ${primaryDomain}: ${error}`);
this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
throw error;
}
}
/**
* Provision static certificate
*/
private async provisionStaticCertificate(
route: IRouteConfig,
domain: string,
certConfig: { key: string; cert: string; keyFile?: string; certFile?: string }
): Promise<void> {
const routeName = route.name || domain;
try {
let key: string = certConfig.key;
let cert: string = certConfig.cert;
// Load from files if paths are provided
if (certConfig.keyFile) {
const keyFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.keyFile);
key = keyFile.contents.toString();
}
if (certConfig.certFile) {
const certFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.certFile);
cert = certFile.contents.toString();
}
// Parse certificate to get dates
// Parse certificate to get dates - for now just use defaults
// TODO: Implement actual certificate parsing if needed
const certInfo = { validTo: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), validFrom: new Date() };
const certData: ICertificateData = {
cert,
key,
expiryDate: certInfo.validTo,
issueDate: certInfo.validFrom
};
// Save to store for consistency
await this.certStore.saveCertificate(routeName, certData);
await this.applyCertificate(domain, certData);
this.updateCertStatus(routeName, 'valid', 'static', certData);
console.log(`Successfully loaded static certificate for ${domain}`);
} catch (error) {
console.error(`Failed to provision static certificate for ${domain}: ${error}`);
this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
throw error;
}
}
/**
* Apply certificate to NetworkProxy
*/
private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
if (!this.networkProxy) {
console.warn('NetworkProxy not set, cannot apply certificate');
return;
}
// Apply certificate to NetworkProxy
this.networkProxy.updateCertificate(domain, certData.cert, certData.key);
// Also apply for wildcard if it's a subdomain
if (domain.includes('.') && !domain.startsWith('*.')) {
const parts = domain.split('.');
if (parts.length >= 2) {
const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
}
}
}
/**
* Extract domains from route configuration
*/
private extractDomainsFromRoute(route: IRouteConfig): string[] {
if (!route.match.domains) {
return [];
}
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
// Filter out wildcards and patterns
return domains.filter(d =>
!d.includes('*') &&
!d.includes('{') &&
d.includes('.')
);
}
/**
* Check if certificate is valid
*/
private isCertificateValid(cert: ICertificateData): boolean {
const now = new Date();
const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
return cert.expiryDate > expiryThreshold;
}
/**
* Add challenge route to SmartProxy
*/
private async addChallengeRoute(): Promise<void> {
if (!this.updateRoutesCallback) {
throw new Error('No route update callback set');
}
if (!this.challengeRoute) {
throw new Error('Challenge route not initialized');
}
const challengeRoute = this.challengeRoute;
const updatedRoutes = [...this.routes, challengeRoute];
await this.updateRoutesCallback(updatedRoutes);
}
/**
* Remove challenge route from SmartProxy
*/
private async removeChallengeRoute(): Promise<void> {
if (!this.updateRoutesCallback) {
return;
}
const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
await this.updateRoutesCallback(filteredRoutes);
}
/**
* Start renewal timer
*/
private startRenewalTimer(): void {
// Check for renewals every 12 hours
this.renewalTimer = setInterval(() => {
this.checkAndRenewCertificates();
}, 12 * 60 * 60 * 1000);
// Also do an immediate check
this.checkAndRenewCertificates();
}
/**
* Check and renew certificates that are expiring
*/
private async checkAndRenewCertificates(): Promise<void> {
for (const route of this.routes) {
if (route.action.tls?.certificate === 'auto') {
const routeName = route.name || this.extractDomainsFromRoute(route)[0];
const cert = await this.certStore.getCertificate(routeName);
if (cert && !this.isCertificateValid(cert)) {
console.log(`Certificate for ${routeName} needs renewal`);
try {
await this.provisionCertificate(route);
} catch (error) {
console.error(`Failed to renew certificate for ${routeName}: ${error}`);
}
}
}
}
}
/**
* Update certificate status
*/
private updateCertStatus(
routeName: string,
status: ICertStatus['status'],
source: ICertStatus['source'],
certData?: ICertificateData,
error?: string
): void {
this.certStatus.set(routeName, {
domain: routeName,
status,
source,
expiryDate: certData?.expiryDate,
issueDate: certData?.issueDate,
error
});
}
/**
* Get certificate status for a route
*/
public getCertificateStatus(routeName: string): ICertStatus | undefined {
return this.certStatus.get(routeName);
}
/**
* Force renewal of a certificate
*/
public async renewCertificate(routeName: string): Promise<void> {
const route = this.routes.find(r => r.name === routeName);
if (!route) {
throw new Error(`Route ${routeName} not found`);
}
// Remove existing certificate to force renewal
await this.certStore.deleteCertificate(routeName);
await this.provisionCertificate(route);
}
/**
* Setup challenge handler integration with SmartProxy routing
*/
private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
// Create a challenge route that delegates to SmartAcme's HTTP-01 handler
const challengeRoute: IRouteConfig = {
name: 'acme-challenge',
priority: 1000, // High priority
match: {
ports: 80,
path: '/.well-known/acme-challenge/*'
},
action: {
type: 'static',
handler: async (context) => {
// Extract the token from the path
const token = context.path?.split('/').pop();
if (!token) {
return { status: 404, body: 'Not found' };
}
// Create mock request/response objects for SmartAcme
const mockReq = {
url: context.path,
method: 'GET',
headers: context.headers || {}
};
let responseData: any = null;
const mockRes = {
statusCode: 200,
setHeader: (name: string, value: string) => {},
end: (data: any) => {
responseData = data;
}
};
// Use SmartAcme's handler
const handled = await new Promise<boolean>((resolve) => {
http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
resolve(false);
});
// Give it a moment to process
setTimeout(() => resolve(true), 100);
});
if (handled && responseData) {
return {
status: mockRes.statusCode,
headers: { 'Content-Type': 'text/plain' },
body: responseData
};
} else {
return { status: 404, body: 'Not found' };
}
}
}
};
// Store the challenge route to add it when needed
this.challengeRoute = challengeRoute;
}
/**
* Stop certificate manager
*/
public async stop(): Promise<void> {
if (this.renewalTimer) {
clearInterval(this.renewalTimer);
this.renewalTimer = null;
}
if (this.smartAcme) {
await this.smartAcme.stop();
}
// Remove any active challenge routes
if (this.pendingChallenges.size > 0) {
this.pendingChallenges.clear();
await this.removeChallengeRoute();
}
}
/**
* Get ACME options (for recreating after route updates)
*/
public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
return this.acmeOptions;
}
}

View File

@ -73,15 +73,42 @@ export interface IRouteTarget {
port: number | 'preserve' | ((context: IRouteContext) => number); // Port with optional function for dynamic mapping (use 'preserve' to keep the incoming port)
}
/**
* ACME configuration for automatic certificate provisioning
*/
export interface IRouteAcme {
email: string; // Contact email for ACME account
useProduction?: boolean; // Use production ACME servers (default: false)
challengePort?: number; // Port for HTTP-01 challenges (default: 80)
renewBeforeDays?: number; // Days before expiry to renew (default: 30)
}
/**
* Static route handler response
*/
export interface IStaticResponse {
status: number;
headers?: Record<string, string>;
body: string | Buffer;
}
/**
* TLS configuration for route actions
*/
export interface IRouteTls {
mode: TTlsMode;
certificate?: 'auto' | { // Auto = use ACME
key: string;
cert: string;
certificate?: 'auto' | { // Auto = use ACME
key: string; // PEM-encoded private key
cert: string; // PEM-encoded certificate
ca?: string; // PEM-encoded CA chain
keyFile?: string; // Path to key file (overrides key)
certFile?: string; // Path to cert file (overrides cert)
};
acme?: IRouteAcme; // ACME options when certificate is 'auto'
versions?: string[]; // Allowed TLS versions (e.g., ['TLSv1.2', 'TLSv1.3'])
ciphers?: string; // OpenSSL cipher string
honorCipherOrder?: boolean; // Use server's cipher preferences
sessionTimeout?: number; // TLS session timeout in seconds
}
/**
@ -266,6 +293,9 @@ export interface IRouteAction {
// NFTables-specific options
nftables?: INfTablesOptions;
// Handler function for static routes
handler?: (context: IRouteContext) => Promise<IStaticResponse>;
}
/**

View File

@ -1,100 +1,13 @@
import * as plugins from '../../plugins.js';
import { NetworkProxy } from '../network-proxy/index.js';
import { Port80Handler } from '../../http/port80/port80-handler.js';
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
import type { ICertificateData } from '../../certificate/models/certificate-types.js';
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
import type { IRouteConfig } from './models/route-types.js';
/**
* Manages NetworkProxy integration for TLS termination
*
* NetworkProxyBridge connects SmartProxy with NetworkProxy to handle TLS termination.
* It directly passes route configurations to NetworkProxy and manages the physical
* connection piping between SmartProxy and NetworkProxy for TLS termination.
*
* It is used by SmartProxy for routes that have:
* - TLS mode of 'terminate' or 'terminate-and-reencrypt'
* - Certificate set to 'auto' or custom certificate
*/
export class NetworkProxyBridge {
private networkProxy: NetworkProxy | null = null;
private port80Handler: Port80Handler | null = null;
constructor(private settings: ISmartProxyOptions) {}
/**
* Set the Port80Handler to use for certificate management
*/
public setPort80Handler(handler: Port80Handler): void {
this.port80Handler = handler;
// Subscribe to certificate events
subscribeToPort80Handler(handler, {
onCertificateIssued: this.handleCertificateEvent.bind(this),
onCertificateRenewed: this.handleCertificateEvent.bind(this)
});
// If NetworkProxy is already initialized, connect it with Port80Handler
if (this.networkProxy) {
this.networkProxy.setExternalPort80Handler(handler);
}
console.log('Port80Handler connected to NetworkProxyBridge');
}
/**
* Initialize NetworkProxy instance
*/
public async initialize(): Promise<void> {
if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
// Configure NetworkProxy options based on SmartProxy settings
const networkProxyOptions: any = {
port: this.settings.networkProxyPort!,
portProxyIntegration: true,
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info',
useExternalPort80Handler: !!this.port80Handler // Use Port80Handler if available
};
this.networkProxy = new NetworkProxy(networkProxyOptions);
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
// Connect Port80Handler if available
if (this.port80Handler) {
this.networkProxy.setExternalPort80Handler(this.port80Handler);
}
// Apply route configurations to NetworkProxy
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
}
}
/**
* Handle certificate issuance or renewal events
*/
private handleCertificateEvent(data: ICertificateData): void {
if (!this.networkProxy) return;
console.log(`Received certificate for ${data.domain} from Port80Handler, updating NetworkProxy`);
// Apply certificate directly to NetworkProxy
this.networkProxy.updateCertificate(data.domain, data.certificate, data.privateKey);
}
/**
* Apply an external (static) certificate into NetworkProxy
*/
public applyExternalCertificate(data: ICertificateData): void {
if (!this.networkProxy) {
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
return;
}
// Apply certificate directly to NetworkProxy
this.networkProxy.updateCertificate(data.domain, data.certificate, data.privateKey);
}
/**
* Get the NetworkProxy instance
*/
@ -103,10 +16,119 @@ export class NetworkProxyBridge {
}
/**
* Get the NetworkProxy port
* Initialize NetworkProxy instance
*/
public getNetworkProxyPort(): number {
return this.networkProxy ? this.networkProxy.getListeningPort() : this.settings.networkProxyPort || 8443;
public async initialize(): Promise<void> {
if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
const networkProxyOptions: any = {
port: this.settings.networkProxyPort!,
portProxyIntegration: true,
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
};
this.networkProxy = new NetworkProxy(networkProxyOptions);
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
// Apply route configurations to NetworkProxy
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
}
}
/**
* Sync routes to NetworkProxy
*/
public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> {
if (!this.networkProxy) return;
// Convert routes to NetworkProxy format
const networkProxyConfigs = routes
.filter(route => {
// Check if this route matches any of the specified network proxy ports
const routePorts = Array.isArray(route.match.ports)
? route.match.ports
: [route.match.ports];
return routePorts.some(port =>
this.settings.useNetworkProxy?.includes(port)
);
})
.map(route => this.routeToNetworkProxyConfig(route));
// Apply configurations to NetworkProxy
await this.networkProxy.updateRouteConfigs(networkProxyConfigs);
}
/**
* Convert route to NetworkProxy configuration
*/
private routeToNetworkProxyConfig(route: IRouteConfig): any {
// Convert route to NetworkProxy domain config format
return {
domain: route.match.domains?.[0] || '*',
target: route.action.target,
tls: route.action.tls,
security: route.action.security
};
}
/**
* Check if connection should use NetworkProxy
*/
public shouldUseNetworkProxy(connection: IConnectionRecord, routeMatch: any): boolean {
// Only use NetworkProxy for TLS termination
return (
routeMatch.route.action.tls?.mode === 'terminate' ||
routeMatch.route.action.tls?.mode === 'terminate-and-reencrypt'
) && this.networkProxy !== null;
}
/**
* Forward connection to NetworkProxy
*/
public async forwardToNetworkProxy(
connectionId: string,
socket: plugins.net.Socket,
record: IConnectionRecord,
initialChunk: Buffer,
networkProxyPort: number,
cleanupCallback: (reason: string) => void
): Promise<void> {
if (!this.networkProxy) {
throw new Error('NetworkProxy not initialized');
}
const proxySocket = new plugins.net.Socket();
await new Promise<void>((resolve, reject) => {
proxySocket.connect(networkProxyPort, 'localhost', () => {
console.log(`[${connectionId}] Connected to NetworkProxy for termination`);
resolve();
});
proxySocket.on('error', reject);
});
// Send initial chunk if present
if (initialChunk) {
proxySocket.write(initialChunk);
}
// Pipe the sockets together
socket.pipe(proxySocket);
proxySocket.pipe(socket);
// Handle cleanup
const cleanup = (reason: string) => {
socket.unpipe(proxySocket);
proxySocket.unpipe(socket);
proxySocket.destroy();
cleanupCallback(reason);
};
socket.on('end', () => cleanup('socket_end'));
socket.on('error', () => cleanup('socket_error'));
proxySocket.on('end', () => cleanup('proxy_end'));
proxySocket.on('error', () => cleanup('proxy_error'));
}
/**
@ -115,7 +137,6 @@ export class NetworkProxyBridge {
public async start(): Promise<void> {
if (this.networkProxy) {
await this.networkProxy.start();
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
}
}
@ -124,182 +145,8 @@ export class NetworkProxyBridge {
*/
public async stop(): Promise<void> {
if (this.networkProxy) {
try {
console.log('Stopping NetworkProxy...');
await this.networkProxy.stop();
console.log('NetworkProxy stopped successfully');
} catch (err) {
console.log(`Error stopping NetworkProxy: ${err}`);
}
}
}
/**
* Forwards a TLS connection to a NetworkProxy for handling
*/
public forwardToNetworkProxy(
connectionId: string,
socket: plugins.net.Socket,
record: IConnectionRecord,
initialData: Buffer,
customProxyPort?: number,
onError?: (reason: string) => void
): void {
// Ensure NetworkProxy is initialized
if (!this.networkProxy) {
console.log(
`[${connectionId}] NetworkProxy not initialized. Cannot forward connection.`
);
if (onError) {
onError('network_proxy_not_initialized');
}
return;
}
// Use the custom port if provided, otherwise use the default NetworkProxy port
const proxyPort = customProxyPort || this.networkProxy.getListeningPort();
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
);
}
// Create a connection to the NetworkProxy
const proxySocket = plugins.net.connect({
host: proxyHost,
port: proxyPort,
});
// Store the outgoing socket in the record
record.outgoing = proxySocket;
record.outgoingStartTime = Date.now();
record.usingNetworkProxy = true;
// Set up error handlers
proxySocket.on('error', (err) => {
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
if (onError) {
onError('network_proxy_connect_error');
}
});
// Handle connection to NetworkProxy
proxySocket.on('connect', () => {
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
}
// First send the initial data that contains the TLS ClientHello
proxySocket.write(initialData);
// Now set up bidirectional piping between client and NetworkProxy
socket.pipe(proxySocket);
proxySocket.pipe(socket);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`);
}
});
}
/**
* Synchronizes routes to NetworkProxy
*
* This method directly passes route configurations to NetworkProxy without any
* intermediate conversion. NetworkProxy natively understands route configurations.
*
* @param routes The route configurations to sync to NetworkProxy
*/
public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> {
if (!this.networkProxy) {
console.log('Cannot sync configurations - NetworkProxy not initialized');
return;
}
try {
// Filter only routes that are applicable to NetworkProxy (TLS termination)
const networkProxyRoutes = routes.filter(route => {
return (
route.action.type === 'forward' &&
route.action.tls &&
(route.action.tls.mode === 'terminate' || route.action.tls.mode === 'terminate-and-reencrypt')
);
});
// Pass routes directly to NetworkProxy
await this.networkProxy.updateRouteConfigs(networkProxyRoutes);
console.log(`Synced ${networkProxyRoutes.length} routes directly to NetworkProxy`);
} catch (err) {
console.log(`Error syncing routes to NetworkProxy: ${err}`);
}
}
/**
* Request a certificate for a specific domain
*
* @param domain The domain to request a certificate for
* @param routeName Optional route name to associate with this certificate
*/
public async requestCertificate(domain: string, routeName?: string): Promise<boolean> {
// Delegate to Port80Handler if available
if (this.port80Handler) {
try {
// Check if the domain is already registered
const cert = this.port80Handler.getCertificate(domain);
if (cert) {
console.log(`Certificate already exists for ${domain}`);
return true;
}
// Build the domain options
const domainOptions: any = {
domainName: domain,
sslRedirect: true,
acmeMaintenance: true,
};
// Add route reference if available
if (routeName) {
domainOptions.routeReference = {
routeName
};
}
// Register the domain for certificate issuance
this.port80Handler.addDomain(domainOptions);
console.log(`Domain ${domain} registered for certificate issuance`);
return true;
} catch (err) {
console.log(`Error requesting certificate: ${err}`);
return false;
}
}
// Fall back to NetworkProxy if Port80Handler is not available
if (!this.networkProxy) {
console.log('Cannot request certificate - NetworkProxy not initialized');
return false;
}
if (!this.settings.acme?.enabled) {
console.log('Cannot request certificate - ACME is not enabled');
return false;
}
try {
const result = await this.networkProxy.requestCertificate(domain);
if (result) {
console.log(`Certificate request for ${domain} submitted successfully`);
} else {
console.log(`Certificate request for ${domain} failed`);
}
return result;
} catch (err) {
console.log(`Error requesting certificate: ${err}`);
return false;
await this.networkProxy.stop();
this.networkProxy = null;
}
}
}

View File

@ -365,6 +365,10 @@ export class RouteConnectionHandler {
case 'block':
return this.handleBlockAction(socket, record, route);
case 'static':
this.handleStaticAction(socket, record, route);
return;
default:
console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
socket.end();
@ -528,7 +532,7 @@ export class RouteConnectionHandler {
// If we have an initial chunk with TLS data, start processing it
if (initialChunk && record.isTLS) {
return this.networkProxyBridge.forwardToNetworkProxy(
this.networkProxyBridge.forwardToNetworkProxy(
connectionId,
socket,
record,
@ -536,6 +540,7 @@ export class RouteConnectionHandler {
this.settings.networkProxyPort,
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
);
return;
}
// This shouldn't normally happen - we should have TLS data at this point
@ -706,6 +711,64 @@ export class RouteConnectionHandler {
this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
}
/**
* Handle a static action for a route
*/
private async handleStaticAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig
): Promise<void> {
const connectionId = record.id;
if (!route.action.handler) {
console.error(`[${connectionId}] Static route '${route.name}' has no handler`);
socket.end();
this.connectionManager.cleanupConnection(record, 'no_handler');
return;
}
try {
// Build route context
const context: IRouteContext = {
port: record.localPort,
domain: record.lockedDomain,
clientIp: record.remoteIP,
serverIp: socket.localAddress!,
path: undefined, // Will need to be extracted from HTTP request
isTls: record.isTLS,
tlsVersion: record.tlsVersion,
routeName: route.name,
routeId: route.name,
timestamp: Date.now(),
connectionId
};
// Call the handler
const response = await route.action.handler(context);
// Send HTTP response
const headers = response.headers || {};
headers['Content-Length'] = Buffer.byteLength(response.body).toString();
let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
for (const [key, value] of Object.entries(headers)) {
httpResponse += `${key}: ${value}\r\n`;
}
httpResponse += '\r\n';
socket.write(httpResponse);
socket.write(response.body);
socket.end();
this.connectionManager.cleanupConnection(record, 'completed');
} catch (error) {
console.error(`[${connectionId}] Error in static handler: ${error}`);
socket.end();
this.connectionManager.cleanupConnection(record, 'handler_error');
}
}
/**
* Sets up a direct connection to the target
*/
@ -1131,4 +1194,14 @@ export class RouteConnectionHandler {
}
});
}
}
// Helper function for status text
function getStatusText(status: number): string {
const statusTexts: Record<number, string> = {
200: 'OK',
404: 'Not Found',
500: 'Internal Server Error'
};
return statusTexts[status] || 'Unknown';
}

View File

@ -11,12 +11,8 @@ import { RouteManager } from './route-manager.js';
import { RouteConnectionHandler } from './route-connection-handler.js';
import { NFTablesManager } from './nftables-manager.js';
// External dependencies
import { Port80Handler } from '../../http/port80/port80-handler.js';
import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js';
import type { ICertificateData } from '../../certificate/models/certificate-types.js';
import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
import { createPort80HandlerOptions } from '../../common/port80-adapter.js';
// Certificate manager
import { SmartCertManager, type ICertStatus } from './certificate-manager.js';
// Import types and utilities
import type {
@ -53,10 +49,8 @@ export class SmartProxy extends plugins.EventEmitter {
private routeConnectionHandler: RouteConnectionHandler;
private nftablesManager: NFTablesManager;
// Port80Handler for ACME certificate management
private port80Handler: Port80Handler | null = null;
// CertProvisioner for unified certificate workflows
private certProvisioner?: CertProvisioner;
// Certificate manager for ACME and static certificates
private certManager: SmartCertManager | null = null;
/**
* Constructor for SmartProxy
@ -180,29 +174,53 @@ export class SmartProxy extends plugins.EventEmitter {
public settings: ISmartProxyOptions;
/**
* Initialize the Port80Handler for ACME certificate management
* Initialize certificate manager
*/
private async initializePort80Handler(): Promise<void> {
const config = this.settings.acme!;
if (!config.enabled) {
console.log('ACME is disabled in configuration');
private async initializeCertificateManager(): Promise<void> {
// Extract global ACME options if any routes use auto certificates
const autoRoutes = this.settings.routes.filter(r =>
r.action.tls?.certificate === 'auto'
);
if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) {
console.log('No routes require certificate management');
return;
}
try {
// Build and start the Port80Handler
this.port80Handler = buildPort80Handler({
...config,
httpsRedirectPort: config.httpsRedirectPort || 443
});
// Share Port80Handler with NetworkProxyBridge before start
this.networkProxyBridge.setPort80Handler(this.port80Handler);
await this.port80Handler.start();
console.log(`Port80Handler started on port ${config.port}`);
} catch (err) {
console.log(`Error initializing Port80Handler: ${err}`);
// Use the first auto route's ACME config as defaults
const defaultAcme = autoRoutes[0]?.action.tls?.acme;
this.certManager = new SmartCertManager(
this.settings.routes,
'./certs', // Certificate directory
defaultAcme ? {
email: defaultAcme.email,
useProduction: defaultAcme.useProduction,
port: defaultAcme.challengePort || 80
} : undefined
);
// Connect with NetworkProxy
if (this.networkProxyBridge.getNetworkProxy()) {
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
}
// Set route update callback for ACME challenges
this.certManager.setUpdateRoutesCallback(async (routes) => {
await this.updateRoutes(routes);
});
await this.certManager.initialize();
}
/**
* Check if we have routes with static certificates
*/
private hasStaticCertRoutes(): boolean {
return this.settings.routes.some(r =>
r.action.tls?.certificate &&
r.action.tls.certificate !== 'auto'
);
}
/**
@ -215,51 +233,18 @@ export class SmartProxy extends plugins.EventEmitter {
return;
}
// Pure route-based configuration - no domain configs needed
// Initialize Port80Handler if enabled
await this.initializePort80Handler();
// Initialize CertProvisioner for unified certificate workflows
if (this.port80Handler) {
const acme = this.settings.acme!;
// Setup route forwards
const routeForwards = acme.routeForwards?.map(f => f) || [];
// Create CertProvisioner with appropriate parameters
// No longer need to support multiple configuration types
// Just pass the routes directly
this.certProvisioner = new CertProvisioner(
this.settings.routes,
this.port80Handler,
this.networkProxyBridge,
this.settings.certProvisionFunction,
acme.renewThresholdDays!,
acme.renewCheckIntervalHours!,
acme.autoRenew!,
routeForwards
);
// Register certificate event handler
this.certProvisioner.on('certificate', (certData) => {
this.emit('certificate', {
domain: certData.domain,
publicKey: certData.certificate,
privateKey: certData.privateKey,
expiryDate: certData.expiryDate,
source: certData.source,
isRenewal: certData.isRenewal
});
});
await this.certProvisioner.start();
console.log('CertProvisioner started');
}
// Initialize certificate manager before starting servers
await this.initializeCertificateManager();
// Initialize and start NetworkProxy if needed
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
await this.networkProxyBridge.initialize();
// Connect NetworkProxy with certificate manager
if (this.certManager) {
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
}
await this.networkProxyBridge.start();
}
@ -371,27 +356,16 @@ export class SmartProxy extends plugins.EventEmitter {
this.isShuttingDown = true;
this.portManager.setShuttingDown(true);
// Stop CertProvisioner if active
if (this.certProvisioner) {
await this.certProvisioner.stop();
console.log('CertProvisioner stopped');
// Stop certificate manager
if (this.certManager) {
await this.certManager.stop();
console.log('Certificate manager stopped');
}
// Stop NFTablesManager
await this.nftablesManager.stop();
console.log('NFTablesManager stopped');
// Stop the Port80Handler if running
if (this.port80Handler) {
try {
await this.port80Handler.stop();
console.log('Port80Handler stopped');
this.port80Handler = null;
} catch (err) {
console.log(`Error stopping Port80Handler: ${err}`);
}
}
// Stop the connection logger
if (this.connectionLogger) {
clearInterval(this.connectionLogger);
@ -498,104 +472,60 @@ export class SmartProxy extends plugins.EventEmitter {
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
}
// If Port80Handler is running, provision certificates based on routes
if (this.port80Handler && this.settings.acme?.enabled) {
// Register all eligible domains from routes
this.port80Handler.addDomainsFromRoutes(newRoutes);
// Handle static certificates from certProvisionFunction if available
if (this.settings.certProvisionFunction) {
for (const route of newRoutes) {
// Skip routes without domains
if (!route.match.domains) continue;
// Skip non-forward routes
if (route.action.type !== 'forward') continue;
// Skip routes without TLS termination
if (!route.action.tls ||
route.action.tls.mode === 'passthrough' ||
!route.action.target) continue;
// Skip certificate provisioning if certificate is not auto
if (route.action.tls.certificate !== 'auto') continue;
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
for (const domain of domains) {
try {
const provision = await this.settings.certProvisionFunction(domain);
// Skip http01 as those are handled by Port80Handler
if (provision !== 'http01') {
// Handle static certificate (e.g., DNS-01 provisioned)
const certObj = provision as plugins.tsclass.network.ICert;
const certData: ICertificateData = {
domain: certObj.domainName,
certificate: certObj.publicKey,
privateKey: certObj.privateKey,
expiryDate: new Date(certObj.validUntil),
routeReference: {
routeName: route.name
}
};
this.networkProxyBridge.applyExternalCertificate(certData);
console.log(`Applied static certificate for ${domain} from certProvider`);
}
} catch (err) {
console.log(`certProvider error for ${domain}: ${err}`);
}
}
}
// Update certificate manager with new routes
if (this.certManager) {
await this.certManager.stop();
this.certManager = new SmartCertManager(
newRoutes,
'./certs',
this.certManager.getAcmeOptions()
);
if (this.networkProxyBridge.getNetworkProxy()) {
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
}
console.log('Provisioned certificates for new routes');
await this.certManager.initialize();
}
}
/**
* Request a certificate for a specific domain
*
* @param domain The domain to request a certificate for
* @param routeName Optional route name to associate with the certificate
* Manually provision a certificate for a route
*/
public async requestCertificate(domain: string, routeName?: string): Promise<boolean> {
// Validate domain format
if (!this.isValidDomain(domain)) {
console.log(`Invalid domain format: ${domain}`);
return false;
}
// Use Port80Handler if available
if (this.port80Handler) {
try {
// Check if we already have a certificate
const cert = this.port80Handler.getCertificate(domain);
if (cert) {
console.log(`Certificate already exists for ${domain}, valid until ${cert.expiryDate.toISOString()}`);
return true;
}
// Register domain for certificate issuance
this.port80Handler.addDomain({
domain,
sslRedirect: true,
acmeMaintenance: true,
routeReference: routeName ? { routeName } : undefined
});
console.log(`Domain ${domain} registered for certificate issuance` + (routeName ? ` for route '${routeName}'` : ''));
return true;
} catch (err) {
console.log(`Error registering domain with Port80Handler: ${err}`);
return false;
}
public async provisionCertificate(routeName: string): Promise<void> {
if (!this.certManager) {
throw new Error('Certificate manager not initialized');
}
// Fall back to NetworkProxyBridge
return this.networkProxyBridge.requestCertificate(domain);
const route = this.settings.routes.find(r => r.name === routeName);
if (!route) {
throw new Error(`Route ${routeName} not found`);
}
await this.certManager.provisionCertificate(route);
}
/**
* Force renewal of a certificate
*/
public async renewCertificate(routeName: string): Promise<void> {
if (!this.certManager) {
throw new Error('Certificate manager not initialized');
}
await this.certManager.renewCertificate(routeName);
}
/**
* Get certificate status for a route
*/
public getCertificateStatus(routeName: string): ICertStatus | undefined {
if (!this.certManager) {
return undefined;
}
return this.certManager.getCertificateStatus(routeName);
}
/**
@ -685,8 +615,8 @@ export class SmartProxy extends plugins.EventEmitter {
keepAliveConnections,
networkProxyConnections,
terminationStats,
acmeEnabled: !!this.port80Handler,
port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null,
acmeEnabled: !!this.certManager,
port80HandlerPort: this.certManager ? 80 : null,
routes: this.routeManager.getListeningPorts().length,
listeningPorts: this.portManager.getListeningPorts(),
activePorts: this.portManager.getListeningPorts().length
@ -735,51 +665,4 @@ export class SmartProxy extends plugins.EventEmitter {
return this.nftablesManager.getStatus();
}
/**
* Get status of certificates managed by Port80Handler
*/
public getCertificateStatus(): any {
if (!this.port80Handler) {
return {
enabled: false,
message: 'Port80Handler is not enabled'
};
}
// Get eligible domains
const eligibleDomains = this.getEligibleDomainsForCertificates();
const certificateStatus: Record<string, any> = {};
// Check each domain
for (const domain of eligibleDomains) {
const cert = this.port80Handler.getCertificate(domain);
if (cert) {
const now = new Date();
const expiryDate = cert.expiryDate;
const daysRemaining = Math.floor((expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000));
certificateStatus[domain] = {
status: 'valid',
expiryDate: expiryDate.toISOString(),
daysRemaining,
renewalNeeded: daysRemaining <= (this.settings.acme?.renewThresholdDays ?? 0)
};
} else {
certificateStatus[domain] = {
status: 'missing',
message: 'No certificate found'
};
}
}
const acme = this.settings.acme!;
return {
enabled: true,
port: acme.port!,
useProduction: acme.useProduction!,
autoRenew: acme.autoRenew!,
certificates: certificateStatus
};
}
}