underpost 2.8.885 → 2.8.886
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +3 -0
- package/.github/workflows/ghpkg.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +1 -1
- package/.github/workflows/publish.ci.yml +5 -5
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/CHANGELOG.md +145 -1
- package/Dockerfile +1 -1
- package/README.md +3 -3
- package/bin/build.js +18 -9
- package/bin/deploy.js +93 -187
- package/cli.md +2 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +54 -54
- package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
- package/manifests/lxd/underpost-setup.sh +5 -5
- package/package.json +3 -3
- package/scripts/ssl.sh +164 -0
- package/src/cli/baremetal.js +7 -7
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +10 -3
- package/src/cli/cron.js +1 -1
- package/src/cli/db.js +1 -1
- package/src/cli/deploy.js +33 -1
- package/src/cli/fs.js +2 -2
- package/src/cli/image.js +7 -0
- package/src/cli/monitor.js +33 -1
- package/src/cli/run.js +315 -51
- package/src/cli/script.js +32 -0
- package/src/cli/secrets.js +34 -0
- package/src/cli/test.js +42 -1
- package/src/client/components/core/Css.js +0 -8
- package/src/client/components/core/windowGetDimensions.js +229 -162
- package/src/index.js +2 -2
- package/src/mailer/MailerProvider.js +1 -0
- package/src/runtime/express/Express.js +12 -4
- package/src/runtime/lampp/Dockerfile +1 -1
- package/src/server/backup.js +20 -0
- package/src/server/client-build-live.js +12 -10
- package/src/server/client-build.js +136 -91
- package/src/server/client-dev-server.js +16 -2
- package/src/server/client-icons.js +19 -0
- package/src/server/conf.js +470 -60
- package/src/server/dns.js +184 -42
- package/src/server/downloader.js +65 -24
- package/src/server/object-layer.js +260 -162
- package/src/server/peer.js +2 -8
- package/src/server/proxy.js +93 -76
- package/src/server/runtime.js +15 -16
- package/src/server/ssr.js +4 -4
- package/src/server/tls.js +251 -0
- package/src/server/valkey.js +11 -10
- package/src/ws/IoInterface.js +2 -1
- package/src/ws/IoServer.js +2 -1
- package/src/ws/core/core.ws.connection.js +1 -1
- package/src/ws/core/core.ws.emit.js +1 -1
- package/src/ws/core/core.ws.server.js +1 -1
- package/manifests/maas/lxd-preseed.yaml +0 -32
- package/src/server/ssl.js +0 -108
- /package/{manifests/maas → scripts}/device-scan.sh +0 -0
- /package/{manifests/maas → scripts}/gpu-diag.sh +0 -0
- /package/{manifests/maas → scripts}/maas-setup.sh +0 -0
- /package/{manifests/maas → scripts}/nat-iptables.sh +0 -0
- /package/{manifests/maas → scripts}/nvim.sh +0 -0
- /package/{manifests/maas → scripts}/snap-clean.sh +0 -0
- /package/{manifests/maas → scripts}/ssh-cluster-info.sh +0 -0
package/src/server/proxy.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages the creation and configuration of the reverse proxy server,
|
|
3
|
+
* including handling HTTP/HTTPS listeners and routing based on host configuration.
|
|
4
|
+
* @module src/server/proxy.js
|
|
5
|
+
* @namespace ProxyService
|
|
6
|
+
*/
|
|
1
7
|
'use strict';
|
|
2
8
|
|
|
3
9
|
import express from 'express';
|
|
@@ -5,7 +11,7 @@ import dotenv from 'dotenv';
|
|
|
5
11
|
|
|
6
12
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
7
13
|
import { loggerFactory, loggerMiddleware } from './logger.js';
|
|
8
|
-
import {
|
|
14
|
+
import { TLS } from './tls.js';
|
|
9
15
|
import { buildPortProxyRouter, buildProxyRouter } from './conf.js';
|
|
10
16
|
import UnderpostStartUp from './start.js';
|
|
11
17
|
|
|
@@ -13,81 +19,92 @@ dotenv.config();
|
|
|
13
19
|
|
|
14
20
|
const logger = loggerFactory(import.meta);
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Main class for building and running the proxy server.
|
|
24
|
+
* All utility methods are implemented as static to serve as a namespace container.
|
|
25
|
+
* @class Proxy
|
|
26
|
+
* @augments Proxy
|
|
27
|
+
* @memberof ProxyService
|
|
28
|
+
*/
|
|
29
|
+
class Proxy {
|
|
30
|
+
/**
|
|
31
|
+
* Initializes and starts the reverse proxy server for all configured ports and hosts.
|
|
32
|
+
* @async
|
|
33
|
+
* @static
|
|
34
|
+
* @memberof ProxyService
|
|
35
|
+
* @returns {Promise<void>}
|
|
36
|
+
* @memberof ProxyService
|
|
37
|
+
*/
|
|
38
|
+
static async buildProxy() {
|
|
39
|
+
// Start a default Express listener on process.env.PORT (potentially unused, but ensures Express is initialized)
|
|
40
|
+
express().listen(process.env.PORT);
|
|
41
|
+
|
|
42
|
+
const proxyRouter = buildProxyRouter();
|
|
43
|
+
|
|
44
|
+
for (let port of Object.keys(proxyRouter)) {
|
|
45
|
+
port = parseInt(port);
|
|
46
|
+
const hosts = proxyRouter[port];
|
|
47
|
+
const proxyPath = '/';
|
|
48
|
+
const proxyHost = 'localhost';
|
|
49
|
+
const runningData = { host: proxyHost, path: proxyPath, client: null, runtime: 'nodejs', meta: import.meta };
|
|
50
|
+
const app = express();
|
|
51
|
+
|
|
52
|
+
// Set logger middleware
|
|
53
|
+
app.use(loggerMiddleware(import.meta));
|
|
54
|
+
|
|
55
|
+
// Proxy middleware options
|
|
56
|
+
/** @type {import('http-proxy-middleware/dist/types').Options} */
|
|
57
|
+
const options = {
|
|
58
|
+
ws: true, // Enable websocket proxying
|
|
59
|
+
target: `http://localhost:${process.env.PORT}`, // Default target (should be overridden by router)
|
|
60
|
+
router: {},
|
|
61
|
+
xfwd: true, // Adds x-forward headers (Host, Proto, etc.)
|
|
62
|
+
onProxyReq: (proxyReq, req, res, options) => {
|
|
63
|
+
// Use the static method from the TLS class for redirection logic
|
|
64
|
+
TLS.sslRedirectMiddleware(req, res, port, proxyRouter);
|
|
65
|
+
},
|
|
66
|
+
pathRewrite: {
|
|
67
|
+
// Add path rewrite rules here if necessary
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
options.router = buildPortProxyRouter(port, proxyRouter, { orderByPathLength: true });
|
|
72
|
+
|
|
73
|
+
const filter = proxyPath; // Use '/' as the general filter
|
|
74
|
+
app.use(proxyPath, createProxyMiddleware(filter, options));
|
|
75
|
+
|
|
76
|
+
// Determine which server to start (HTTP or HTTPS) based on port and environment
|
|
77
|
+
switch (process.env.NODE_ENV) {
|
|
78
|
+
case 'production':
|
|
79
|
+
switch (port) {
|
|
80
|
+
case 443:
|
|
81
|
+
// For port 443 (HTTPS), create the SSL server
|
|
82
|
+
const { ServerSSL } = await TLS.createSslServer(app, hosts);
|
|
83
|
+
await UnderpostStartUp.API.listenPortController(ServerSSL, port, runningData);
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
default:
|
|
87
|
+
// For other ports in production, use standard HTTP
|
|
88
|
+
await UnderpostStartUp.API.listenPortController(app, port, runningData);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
default:
|
|
94
|
+
// In non-production, always use standard HTTP listener
|
|
95
|
+
await UnderpostStartUp.API.listenPortController(app, port, runningData);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
logger.info('Proxy running', { port, options });
|
|
88
99
|
}
|
|
89
|
-
logger.info('Proxy running', { port, options });
|
|
90
100
|
}
|
|
91
|
-
}
|
|
101
|
+
}
|
|
92
102
|
|
|
93
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Backward compatibility export
|
|
105
|
+
* @type {function(): Promise<void>}
|
|
106
|
+
* @memberof ProxyService
|
|
107
|
+
*/
|
|
108
|
+
const buildProxy = Proxy.buildProxy;
|
|
109
|
+
|
|
110
|
+
export { Proxy, buildProxy };
|
package/src/server/runtime.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @description The main runtime orchestrator responsible for reading configuration,
|
|
2
|
+
* The main runtime orchestrator responsible for reading configuration,
|
|
4
3
|
* initializing services (Prometheus, Ports, DB, Mailer), and building the
|
|
5
4
|
* specific server runtime for each host/path (e.g., nodejs, lampp).
|
|
5
|
+
* @module src/server/runtime.js
|
|
6
|
+
* @namespace Runtime
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import fs from 'fs-extra';
|
|
@@ -27,11 +28,12 @@ const logger = loggerFactory(import.meta);
|
|
|
27
28
|
*
|
|
28
29
|
* @memberof Runtime
|
|
29
30
|
* @returns {Promise<void>}
|
|
31
|
+
* @function buildRuntime
|
|
30
32
|
*/
|
|
31
33
|
const buildRuntime = async () => {
|
|
32
34
|
const deployId = process.env.DEPLOY_ID;
|
|
33
35
|
|
|
34
|
-
//
|
|
36
|
+
// Initialize Prometheus Metrics
|
|
35
37
|
const collectDefaultMetrics = promClient.collectDefaultMetrics;
|
|
36
38
|
collectDefaultMetrics();
|
|
37
39
|
|
|
@@ -45,16 +47,13 @@ const buildRuntime = async () => {
|
|
|
45
47
|
const initPort = parseInt(process.env.PORT) + 1;
|
|
46
48
|
let currentPort = initPort;
|
|
47
49
|
|
|
48
|
-
//
|
|
50
|
+
// Load Configuration
|
|
49
51
|
const confServer = JSON.parse(fs.readFileSync(`./conf/conf.server.json`, 'utf8'));
|
|
50
52
|
const confSSR = JSON.parse(fs.readFileSync(`./conf/conf.ssr.json`, 'utf8'));
|
|
51
|
-
const singleReplicaHosts = [];
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
+
// Iterate through hosts and paths
|
|
54
55
|
for (const host of Object.keys(confServer)) {
|
|
55
|
-
|
|
56
|
-
currentPort += singleReplicaHosts.reduce((accumulator, currentValue) => accumulator + currentValue.replicas, 0);
|
|
57
|
-
|
|
56
|
+
let singleReplicaOffsetPortCount = 0;
|
|
58
57
|
const rootHostPath = `/public/${host}`;
|
|
59
58
|
for (const path of Object.keys(confServer[host])) {
|
|
60
59
|
confServer[host][path].port = newInstance(currentPort);
|
|
@@ -74,21 +73,19 @@ const buildRuntime = async () => {
|
|
|
74
73
|
replicas,
|
|
75
74
|
valkey,
|
|
76
75
|
apiBaseHost,
|
|
76
|
+
useLocalSsl,
|
|
77
77
|
} = confServer[host][path];
|
|
78
78
|
|
|
79
79
|
// Calculate context data
|
|
80
|
-
const { redirectTarget,
|
|
80
|
+
const { redirectTarget, singleReplicaOffsetPortSum } = await getInstanceContext({
|
|
81
|
+
deployId,
|
|
81
82
|
redirect,
|
|
82
|
-
singleReplicaHosts,
|
|
83
83
|
singleReplica,
|
|
84
84
|
replicas,
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
host,
|
|
90
|
-
replicas: replicas.length,
|
|
91
|
-
});
|
|
87
|
+
if (singleReplicaOffsetPortSum > 0) {
|
|
88
|
+
singleReplicaOffsetPortCount += singleReplicaOffsetPortSum;
|
|
92
89
|
continue;
|
|
93
90
|
}
|
|
94
91
|
|
|
@@ -113,6 +110,7 @@ const buildRuntime = async () => {
|
|
|
113
110
|
apis,
|
|
114
111
|
origins,
|
|
115
112
|
directory,
|
|
113
|
+
useLocalSsl,
|
|
116
114
|
ws,
|
|
117
115
|
mailer,
|
|
118
116
|
db,
|
|
@@ -156,6 +154,7 @@ const buildRuntime = async () => {
|
|
|
156
154
|
}
|
|
157
155
|
currentPort++;
|
|
158
156
|
}
|
|
157
|
+
currentPort += singleReplicaOffsetPortCount;
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
if (Lampp.enabled() && Lampp.router) Lampp.initService();
|
package/src/server/ssr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module for managing server side rendering
|
|
3
3
|
* @module src/server/ssr.js
|
|
4
|
-
* @namespace
|
|
4
|
+
* @namespace ServerSideRendering
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import fs from 'fs-extra';
|
|
@@ -23,7 +23,7 @@ const logger = loggerFactory(import.meta);
|
|
|
23
23
|
* It reads the component file, formats it, and executes it in a sandboxed Node.js VM context to extract the component.
|
|
24
24
|
* @param {string} [componentPath='./src/client/ssr/Render.js'] - The path to the SSR component file.
|
|
25
25
|
* @returns {Promise<Function>} A promise that resolves to the SSR component function.
|
|
26
|
-
* @memberof
|
|
26
|
+
* @memberof ServerSideRendering
|
|
27
27
|
*/
|
|
28
28
|
const ssrFactory = async (componentPath = `./src/client/ssr/Render.js`) => {
|
|
29
29
|
const context = { SrrComponent: () => {}, npm_package_version: Underpost.version };
|
|
@@ -39,7 +39,7 @@ const ssrFactory = async (componentPath = `./src/client/ssr/Render.js`) => {
|
|
|
39
39
|
* @param {object} req - The Express request object.
|
|
40
40
|
* @param {string} html - The HTML string to sanitize.
|
|
41
41
|
* @returns {string} The sanitized HTML string with nonces.
|
|
42
|
-
* @memberof
|
|
42
|
+
* @memberof ServerSideRendering
|
|
43
43
|
*/
|
|
44
44
|
const sanitizeHtml = (res, req, html) => {
|
|
45
45
|
const nonce = res.locals.nonce;
|
|
@@ -58,7 +58,7 @@ const sanitizeHtml = (res, req, html) => {
|
|
|
58
58
|
* @param {string} options.rootHostPath - The root path for the host's public files.
|
|
59
59
|
* @param {string} options.path - The base path for the instance.
|
|
60
60
|
* @returns {Promise<{error500: Function, error400: Function}>} A promise that resolves to an object containing the 500 and 404 error handling middleware.
|
|
61
|
-
* @memberof
|
|
61
|
+
* @memberof ServerSideRendering
|
|
62
62
|
*/
|
|
63
63
|
const ssrMiddlewareFactory = async ({ app, directory, rootHostPath, path }) => {
|
|
64
64
|
const Render = await ssrFactory();
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides utilities for managing, building, and serving SSL/TLS contexts,
|
|
3
|
+
* primarily using Certbot files and creating HTTPS servers.
|
|
4
|
+
* @module src/server/tls.js
|
|
5
|
+
* @namespace TransportLayerSecurity
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import https from 'https';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import dotenv from 'dotenv';
|
|
12
|
+
import { loggerFactory } from './logger.js';
|
|
13
|
+
|
|
14
|
+
dotenv.config();
|
|
15
|
+
const logger = loggerFactory(import.meta);
|
|
16
|
+
|
|
17
|
+
const DEFAULT_HOST = 'localhost';
|
|
18
|
+
const SSL_BASE = (host = DEFAULT_HOST) => path.resolve(`./engine-private/ssl/${host}`);
|
|
19
|
+
|
|
20
|
+
// Common filename candidates for certs/keys produced by various tools (mkcert, certbot, openssl scripts).
|
|
21
|
+
const CERT_CANDIDATES = [
|
|
22
|
+
'fullchain.pem',
|
|
23
|
+
'cert.pem',
|
|
24
|
+
'ca_bundle.crt',
|
|
25
|
+
'crt.crt',
|
|
26
|
+
`${DEFAULT_HOST}.pem`,
|
|
27
|
+
`${DEFAULT_HOST}.crt`,
|
|
28
|
+
`${DEFAULT_HOST}-fullchain.pem`,
|
|
29
|
+
];
|
|
30
|
+
const KEY_CANDIDATES = [
|
|
31
|
+
'privkey.pem',
|
|
32
|
+
'key.key',
|
|
33
|
+
'private.key',
|
|
34
|
+
'key.pem',
|
|
35
|
+
`${DEFAULT_HOST}-key.pem`,
|
|
36
|
+
`${DEFAULT_HOST}.key`,
|
|
37
|
+
];
|
|
38
|
+
const ROOT_CANDIDATES = ['rootCA.pem', 'ca.pem', 'ca.crt', 'root.pem'];
|
|
39
|
+
|
|
40
|
+
class TLS {
|
|
41
|
+
/**
|
|
42
|
+
* Look for existing SSL files under engine-private/ssl/<host> and return canonical paths.
|
|
43
|
+
* It attempts to be permissive: accepts cert-only, cert+ca, or fullchain.
|
|
44
|
+
* @param {string} host
|
|
45
|
+
* @returns {{key?:string, cert?:string, fullchain?:string, ca?:string, dir:string}}
|
|
46
|
+
* @memberof TransportLayerSecurity
|
|
47
|
+
*/
|
|
48
|
+
static locateSslFiles(host = DEFAULT_HOST) {
|
|
49
|
+
const dir = SSL_BASE(host);
|
|
50
|
+
const result = { dir };
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(dir)) {
|
|
53
|
+
logger.warn('SSL dir does not exist', { dir });
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// find key
|
|
58
|
+
for (const name of KEY_CANDIDATES) {
|
|
59
|
+
const p = path.join(dir, name);
|
|
60
|
+
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
|
|
61
|
+
result.key = p;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// find fullchain first
|
|
67
|
+
for (const name of CERT_CANDIDATES) {
|
|
68
|
+
const p = path.join(dir, name);
|
|
69
|
+
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
|
|
70
|
+
// treat fullchain.pem / ca_bundle.crt as fullchain if name indicates so
|
|
71
|
+
if (
|
|
72
|
+
['fullchain.pem', 'ca_bundle.crt', `${host}-fullchain.pem`].includes(name) ||
|
|
73
|
+
name.endsWith('fullchain.pem')
|
|
74
|
+
) {
|
|
75
|
+
result.fullchain = p;
|
|
76
|
+
result.cert = p; // fullchain will be used as cert when building context
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
// otherwise candidate may be leaf cert
|
|
80
|
+
if (!result.cert) result.cert = p;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// find root/ca if not using fullchain
|
|
85
|
+
if (!result.fullchain) {
|
|
86
|
+
// check for direct ca bundle (cert + ca combined) names
|
|
87
|
+
const caCandidates = ROOT_CANDIDATES.concat(['ca_bundle.crt']);
|
|
88
|
+
for (const name of caCandidates) {
|
|
89
|
+
const p = path.join(dir, name);
|
|
90
|
+
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
|
|
91
|
+
result.ca = p;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// if no dedicated ca found but cert looks like leaf and there is separate ca under other known names,
|
|
96
|
+
// try to detect cert + ca in a single file (not trivial) — we prefer explicit ca
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate that a secure context can be built for host (key + cert or fullchain present)
|
|
104
|
+
* @param {string} host
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
* @memberof TransportLayerSecurity
|
|
107
|
+
*/
|
|
108
|
+
static validateSecureContext(host = DEFAULT_HOST) {
|
|
109
|
+
const files = TLS.locateSslFiles(host);
|
|
110
|
+
return Boolean((files.key && files.cert) || (files.key && files.fullchain));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build a Node.js https.createServer options object (key, cert, ca) for the given host.
|
|
115
|
+
* If a fullchain is available it will be used for cert and ca will be omitted (fullchain already includes chain).
|
|
116
|
+
* If separate cert + ca are found, they will be used accordingly.
|
|
117
|
+
* @param {string} host
|
|
118
|
+
* @returns {{key:string, cert:string, ca?:string}} options
|
|
119
|
+
* @memberof TransportLayerSecurity
|
|
120
|
+
*/
|
|
121
|
+
static buildSecureContext(host = DEFAULT_HOST) {
|
|
122
|
+
const files = TLS.locateSslFiles(host);
|
|
123
|
+
if (!files.key) throw new Error(`SSL key not found for host ${host} (looked in ${files.dir})`);
|
|
124
|
+
if (!files.cert) throw new Error(`SSL certificate not found for host ${host} (looked in ${files.dir})`);
|
|
125
|
+
|
|
126
|
+
const key = fs.readFileSync(files.key, 'utf8');
|
|
127
|
+
const cert = fs.readFileSync(files.cert, 'utf8');
|
|
128
|
+
|
|
129
|
+
// If we have a root CA file (explicit) and cert is leaf-only, include ca
|
|
130
|
+
if (files.ca && files.ca !== files.cert) {
|
|
131
|
+
const ca = fs.readFileSync(files.ca, 'utf8');
|
|
132
|
+
return { key, cert, ca };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If cert is fullchain (already contains chain), just return key/cert
|
|
136
|
+
return { key, cert };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convenience: ensure default host directory exists and copy any matching cert/key files into it using canonical names.
|
|
141
|
+
* This is useful if your generator produced nonstandard names and you want to normalize them.
|
|
142
|
+
* The function will copy existing discovered files to: key.key, crt.crt, ca_bundle.crt when possible.
|
|
143
|
+
* @param {string} host
|
|
144
|
+
* @returns {boolean} true if at least key+cert exist after operation
|
|
145
|
+
* @memberof TransportLayerSecurity
|
|
146
|
+
*/
|
|
147
|
+
static async buildLocalSSL(host = DEFAULT_HOST) {
|
|
148
|
+
const dir = SSL_BASE(host);
|
|
149
|
+
await fs.ensureDir(dir);
|
|
150
|
+
const files = TLS.locateSslFiles(host);
|
|
151
|
+
|
|
152
|
+
// If key+cert already exist under canonical names, done
|
|
153
|
+
const canonicalKey = path.join(dir, 'key.key');
|
|
154
|
+
const canonicalCert = path.join(dir, 'crt.crt');
|
|
155
|
+
const canonicalCa = path.join(dir, 'ca_bundle.crt');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
if (files.key && files.key !== canonicalKey) await fs.copy(files.key, canonicalKey, { overwrite: true });
|
|
159
|
+
if (files.cert && files.cert !== canonicalCert) await fs.copy(files.cert, canonicalCert, { overwrite: true });
|
|
160
|
+
if (files.ca && files.ca !== canonicalCa) await fs.copy(files.ca, canonicalCa, { overwrite: true });
|
|
161
|
+
|
|
162
|
+
// If we had a fullchain but not a separate ca, write fullchain also to ca_bundle if missing
|
|
163
|
+
if (files.fullchain && !fs.existsSync(canonicalCa)) {
|
|
164
|
+
await fs.copy(files.fullchain, canonicalCa, { overwrite: false });
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logger.warn('buildLocalSSL copy step failed', { err: err.message });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return TLS.validateSecureContext(host);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create an HTTPS server (first host) and/or attach SNI contexts for additional hosts.
|
|
175
|
+
* hosts param is an object whose keys are hostnames (e.g. { 'localhost': {...} }).
|
|
176
|
+
* Returns the created https.Server instance (or undefined if none created).
|
|
177
|
+
* @param {import('express').Application} app
|
|
178
|
+
* @param {Object<string, any>} hosts
|
|
179
|
+
* @returns {{ServerSSL?: https.Server}}
|
|
180
|
+
* @memberof TransportLayerSecurity
|
|
181
|
+
*/
|
|
182
|
+
static async createSslServer(app, hosts = { [DEFAULT_HOST]: {} }) {
|
|
183
|
+
let server;
|
|
184
|
+
for (const host of Object.keys(hosts)) {
|
|
185
|
+
// ensure canonical files exist (copies where possible)
|
|
186
|
+
await TLS.buildLocalSSL(host);
|
|
187
|
+
if (!TLS.validate_secure_context_check(host)) {
|
|
188
|
+
// backward compatibility: some callers expect validateSecureContext
|
|
189
|
+
if (!TLS.validateSecureContext(host)) {
|
|
190
|
+
logger.error('Invalid SSL context, skipping host', { host });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// build secure context options
|
|
196
|
+
try {
|
|
197
|
+
const ctx = TLS.buildSecureContext(host);
|
|
198
|
+
if (!server) {
|
|
199
|
+
server = https.createServer(ctx, app);
|
|
200
|
+
logger.info('Created HTTPS server for host', { host });
|
|
201
|
+
} else {
|
|
202
|
+
server.addContext(host, ctx);
|
|
203
|
+
logger.info('Added SNI context for host', { host });
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
logger.error('Failed to build secure context', { host, message: err.message });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { ServerSSL: server };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Middleware that redirects HTTP -> HTTPS in production for recognized hosts.
|
|
215
|
+
* Skips ACME challenge paths.
|
|
216
|
+
* @param {import('express').Request} req
|
|
217
|
+
* @param {import('express').Response} res
|
|
218
|
+
* @param {number} port
|
|
219
|
+
* @param {Object<string, any>} proxyRouter
|
|
220
|
+
* @returns {import('express').RequestHandler}
|
|
221
|
+
* @memberof TransportLayerSecurity
|
|
222
|
+
*/
|
|
223
|
+
static sslRedirectMiddleware(req, res, port = 80, proxyRouter = {}) {
|
|
224
|
+
const sslRedirectUrl = `https://${req.headers.host}${req.url}`;
|
|
225
|
+
if (
|
|
226
|
+
process.env.NODE_ENV === 'production' &&
|
|
227
|
+
port !== 443 &&
|
|
228
|
+
!req.secure &&
|
|
229
|
+
!req.url.startsWith('/.well-known/acme-challenge') &&
|
|
230
|
+
proxyRouter[443] &&
|
|
231
|
+
Object.keys(proxyRouter[443]).find((host) => {
|
|
232
|
+
const [hostSSL] = host.split('/');
|
|
233
|
+
return sslRedirectUrl.match(hostSSL) && TLS.validateSecureContext(hostSSL);
|
|
234
|
+
})
|
|
235
|
+
) {
|
|
236
|
+
return res.status(302).redirect(sslRedirectUrl);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// small helper for internal backward compatibility check name typo in older code
|
|
242
|
+
TLS.validate_secure_context_check = TLS.validateSecureContext;
|
|
243
|
+
|
|
244
|
+
// Backward compatibility exports
|
|
245
|
+
const buildSSL = TLS.buildLocalSSL;
|
|
246
|
+
const buildSecureContext = TLS.buildSecureContext;
|
|
247
|
+
const validateSecureContext = TLS.validateSecureContext;
|
|
248
|
+
const createSslServer = TLS.createSslServer;
|
|
249
|
+
const sslRedirectMiddleware = TLS.sslRedirectMiddleware;
|
|
250
|
+
|
|
251
|
+
export { TLS, buildSSL, buildSecureContext, validateSecureContext, createSslServer, sslRedirectMiddleware };
|
package/src/server/valkey.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module for managing Valkey
|
|
3
3
|
* @module src/server/valkey.js
|
|
4
|
-
* @namespace
|
|
4
|
+
* @namespace ValkeyService
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import Valkey from 'iovalkey';
|
|
@@ -20,7 +20,7 @@ const ValkeyStatus = {}; // 'connected' | 'dummy' | 'error' | undefined
|
|
|
20
20
|
* Checks if any Valkey instance is connected.
|
|
21
21
|
* This is a backward-compatible overall flag.
|
|
22
22
|
* @returns {boolean} True if any instance has a 'connected' status.
|
|
23
|
-
* @memberof
|
|
23
|
+
* @memberof ValkeyService
|
|
24
24
|
*/
|
|
25
25
|
const isValkeyEnable = () => Object.values(ValkeyStatus).some((s) => s === 'connected');
|
|
26
26
|
|
|
@@ -31,7 +31,7 @@ const isValkeyEnable = () => Object.values(ValkeyStatus).some((s) => s === 'conn
|
|
|
31
31
|
* @param {string} [opts.path=''] - The path of the instance.
|
|
32
32
|
* @returns {string} The instance key.
|
|
33
33
|
* @private
|
|
34
|
-
* @memberof
|
|
34
|
+
* @memberof ValkeyService
|
|
35
35
|
*/
|
|
36
36
|
const _instanceKey = (opts = { host: '', path: '' }) => `${opts.host || ''}${opts.path || ''}`;
|
|
37
37
|
|
|
@@ -43,7 +43,7 @@ const _instanceKey = (opts = { host: '', path: '' }) => `${opts.host || ''}${opt
|
|
|
43
43
|
* @param {string} [instance.path=''] - The path of the instance.
|
|
44
44
|
* @param {object} [valkeyServerConnectionOptions={ host: '', path: '' }] - Connection options for the iovalkey client.
|
|
45
45
|
* @returns {Promise<Valkey|undefined>} A promise that resolves to the Valkey client instance, or undefined if creation fails.
|
|
46
|
-
* @memberof
|
|
46
|
+
* @memberof ValkeyService
|
|
47
47
|
*/
|
|
48
48
|
const createValkeyConnection = async (
|
|
49
49
|
instance = { host: '', path: '' },
|
|
@@ -108,7 +108,7 @@ const createValkeyConnection = async (
|
|
|
108
108
|
* @param {object} payload - The source object.
|
|
109
109
|
* @param {object} select - An object where keys are field names and values are 1 to include them.
|
|
110
110
|
* @returns {object} A new object containing only the selected fields from the payload.
|
|
111
|
-
* @memberof
|
|
111
|
+
* @memberof ValkeyService
|
|
112
112
|
*/
|
|
113
113
|
const selectDtoFactory = (payload, select) => {
|
|
114
114
|
const result = {};
|
|
@@ -122,7 +122,7 @@ const selectDtoFactory = (payload, select) => {
|
|
|
122
122
|
* Factory function to create a new Valkey client instance.
|
|
123
123
|
* @param {object} options - Connection options for the iovalkey client.
|
|
124
124
|
* @returns {Promise<Valkey>} A promise that resolves to a new Valkey client.
|
|
125
|
-
* @memberof
|
|
125
|
+
* @memberof ValkeyService
|
|
126
126
|
*/
|
|
127
127
|
const valkeyClientFactory = async (options) => {
|
|
128
128
|
const valkey = new Valkey({
|
|
@@ -154,7 +154,7 @@ const valkeyClientFactory = async (options) => {
|
|
|
154
154
|
* @param {object} [options={ host: '', path: '' }] - The instance identifier.
|
|
155
155
|
* @param {string} [key=''] - The key of the object to retrieve.
|
|
156
156
|
* @returns {Promise<object|string|null>} A promise that resolves to the retrieved object, string, or null if not found.
|
|
157
|
-
* @memberof
|
|
157
|
+
* @memberof ValkeyService
|
|
158
158
|
*/
|
|
159
159
|
const getValkeyObject = async (options = { host: '', path: '' }, key = '') => {
|
|
160
160
|
const k = _instanceKey(options);
|
|
@@ -185,7 +185,7 @@ const getValkeyObject = async (options = { host: '', path: '' }, key = '') => {
|
|
|
185
185
|
* @param {string} [key=''] - The key under which to store the payload.
|
|
186
186
|
* @param {object|string} [payload={}] - The data to store.
|
|
187
187
|
* @returns {Promise<string>} A promise that resolves to 'OK' on success.
|
|
188
|
-
* @memberof
|
|
188
|
+
* @memberof ValkeyService
|
|
189
189
|
*/
|
|
190
190
|
const setValkeyObject = async (options = { host: '', path: '' }, key = '', payload = {}) => {
|
|
191
191
|
const k = _instanceKey(options);
|
|
@@ -212,7 +212,7 @@ const setValkeyObject = async (options = { host: '', path: '' }, key = '', paylo
|
|
|
212
212
|
* @param {string} [key=''] - The key of the object to update.
|
|
213
213
|
* @param {object} [payload={}] - The new data to merge into the object.
|
|
214
214
|
* @returns {Promise<string>} A promise that resolves to the result of the set operation.
|
|
215
|
-
* @memberof
|
|
215
|
+
* @memberof ValkeyService
|
|
216
216
|
*/
|
|
217
217
|
const updateValkeyObject = async (options = { host: '', path: '' }, key = '', payload = {}) => {
|
|
218
218
|
let base = await getValkeyObject(options, key);
|
|
@@ -230,7 +230,7 @@ const updateValkeyObject = async (options = { host: '', path: '' }, key = '', pa
|
|
|
230
230
|
* @param {object} [options.object={}] - An initial object to extend.
|
|
231
231
|
* @param {string} [model=''] - The name of the model schema to use (e.g., 'user').
|
|
232
232
|
* @returns {Promise<object>} A promise that resolves to the newly created object.
|
|
233
|
-
* @memberof
|
|
233
|
+
* @memberof ValkeyService
|
|
234
234
|
*/
|
|
235
235
|
const valkeyObjectFactory = async (options = { host: 'localhost', path: '', object: {} }, model = '') => {
|
|
236
236
|
const idoDate = new Date().toISOString();
|
|
@@ -268,6 +268,7 @@ const valkeyObjectFactory = async (options = { host: 'localhost', path: '', obje
|
|
|
268
268
|
/**
|
|
269
269
|
* A collection of Valkey-related API functions.
|
|
270
270
|
* @type {object}
|
|
271
|
+
* @memberof ValkeyServiceService
|
|
271
272
|
*/
|
|
272
273
|
const ValkeyAPI = {
|
|
273
274
|
valkeyClientFactory,
|
package/src/ws/IoInterface.js
CHANGED
|
@@ -17,10 +17,11 @@ const logger = loggerFactory(import.meta);
|
|
|
17
17
|
* @property {function(Socket, Object.<string, Socket>, any, string, any[]): Promise<void>} [controller] - Handler for incoming channel messages.
|
|
18
18
|
* @property {function(Socket, Object.<string, Socket>, string, string): Promise<void>} [disconnect] - Handler on client disconnection.
|
|
19
19
|
* @property {boolean} [stream=false] - Whether the channel should treat the message as a raw stream (no JSON parsing).
|
|
20
|
+
* @memberof SocketIoInterface
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
|
-
* @class
|
|
24
|
+
* @class IoChannel
|
|
24
25
|
* @alias IoChannel
|
|
25
26
|
* @memberof SocketIoInterface
|
|
26
27
|
* @classdesc Manages the logic, client map, and event listeners for a specific WebSocket channel,
|