underpost 2.8.881 → 2.8.883
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/.github/workflows/release.cd.yml +1 -2
- package/README.md +50 -36
- package/bin/db.js +1 -4
- package/cli.md +86 -86
- package/conf.js +1 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +6 -6
- package/manifests/maas/device-scan.sh +1 -1
- package/package.json +1 -1
- package/src/api/document/document.service.js +9 -1
- package/src/cli/repository.js +2 -0
- package/src/client/components/core/Auth.js +258 -89
- package/src/client/components/core/BtnIcon.js +10 -1
- package/src/client/components/core/CssCore.js +36 -27
- package/src/client/components/core/Docs.js +188 -85
- package/src/client/components/core/LoadingAnimation.js +5 -10
- package/src/client/components/core/Modal.js +262 -120
- package/src/client/components/core/ObjectLayerEngine.js +154 -158
- package/src/client/components/core/Panel.js +2 -0
- package/src/client/components/core/PanelForm.js +94 -60
- package/src/client/components/core/Router.js +15 -15
- package/src/client/components/core/ToolTip.js +83 -19
- package/src/client/components/core/Translate.js +1 -1
- package/src/client/components/core/VanillaJs.js +4 -3
- package/src/client/components/core/windowGetDimensions.js +202 -0
- package/src/client/components/default/MenuDefault.js +11 -0
- package/src/client/ssr/Render.js +1 -1
- package/src/index.js +1 -1
- package/src/runtime/lampp/Lampp.js +253 -128
- package/src/server/auth.js +68 -17
- package/src/server/crypto.js +195 -76
- package/src/server/peer.js +47 -5
- package/src/server/process.js +85 -1
- package/src/server/runtime.js +13 -32
- package/test/crypto.test.js +117 -0
- package/src/runtime/xampp/Xampp.js +0 -83
|
@@ -4,120 +4,230 @@ import { loggerFactory } from '../../server/logger.js';
|
|
|
4
4
|
|
|
5
5
|
const logger = loggerFactory(import.meta);
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @class LamppService
|
|
9
|
+
* @description Provides utilities for managing the XAMPP (Lampp) service on Linux,
|
|
10
|
+
* including initialization, router configuration, and virtual host creation.
|
|
11
|
+
* It manages the server's configuration files and controls the service process.
|
|
12
|
+
*/
|
|
13
|
+
class LamppService {
|
|
14
|
+
/**
|
|
15
|
+
* @private
|
|
16
|
+
* @type {string | undefined}
|
|
17
|
+
* @description Stores the accumulated Apache virtual host configuration (router definition).
|
|
18
|
+
*/
|
|
19
|
+
router;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @public
|
|
23
|
+
* @type {number[]}
|
|
24
|
+
* @description A list of ports currently configured and listened to by the Lampp service.
|
|
25
|
+
*/
|
|
26
|
+
ports;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates an instance of LamppService.
|
|
30
|
+
* Initializes the router configuration and ports list.
|
|
31
|
+
*/
|
|
32
|
+
constructor() {
|
|
33
|
+
this.router = undefined;
|
|
34
|
+
this.ports = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks if the XAMPP (Lampp) service appears to be installed based on the presence of its main configuration file.
|
|
39
|
+
*
|
|
40
|
+
* @memberof LamppService
|
|
41
|
+
* @returns {boolean} True if the configuration file exists, indicating Lampp is likely installed.
|
|
42
|
+
*/
|
|
43
|
+
enabled() {
|
|
44
|
+
return fs.existsSync('/opt/lampp/etc/httpd.conf');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initializes or restarts the Lampp Apache service.
|
|
49
|
+
* This method configures virtual hosts, disables default ports (80/443) in the main config
|
|
50
|
+
* to avoid conflicts, and starts or stops the service using shell commands.
|
|
51
|
+
*
|
|
52
|
+
* @memberof LamppService.prototype
|
|
53
|
+
* @param {object} [options={daemon: false}] - Options for service initialization.
|
|
54
|
+
* @param {boolean} [options.daemon=false] - Flag to indicate if the service should be run as a daemon (currently unused in logic).
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
async initService(options = { daemon: false }) {
|
|
10
58
|
let cmd;
|
|
11
|
-
|
|
12
|
-
//
|
|
59
|
+
|
|
60
|
+
// 1. Write the current virtual host router configuration
|
|
13
61
|
fs.writeFileSync(`/opt/lampp/etc/extra/httpd-vhosts.conf`, this.router || '', 'utf8');
|
|
62
|
+
|
|
63
|
+
// 2. Ensure the vhosts file is included in the main httpd.conf
|
|
64
|
+
const httpdConfPath = `/opt/lampp/etc/httpd.conf`;
|
|
14
65
|
fs.writeFileSync(
|
|
15
|
-
|
|
66
|
+
httpdConfPath,
|
|
16
67
|
fs
|
|
17
|
-
.readFileSync(
|
|
68
|
+
.readFileSync(httpdConfPath, 'utf8')
|
|
18
69
|
.replace(`#Include etc/extra/httpd-vhosts.conf`, `Include etc/extra/httpd-vhosts.conf`),
|
|
19
70
|
'utf8',
|
|
20
71
|
);
|
|
21
72
|
|
|
73
|
+
// 3. Stop the service before making port changes
|
|
22
74
|
cmd = `sudo /opt/lampp/lampp stop`;
|
|
23
|
-
|
|
75
|
+
shellExec(cmd);
|
|
76
|
+
|
|
77
|
+
// 4. Comment out default port Listen directives (80 and 443) to prevent conflicts
|
|
78
|
+
// Modify httpd.conf (port 80)
|
|
79
|
+
if (!fs.readFileSync(httpdConfPath, 'utf8').match(/# Listen 80/))
|
|
24
80
|
fs.writeFileSync(
|
|
25
|
-
|
|
26
|
-
fs.readFileSync(
|
|
81
|
+
httpdConfPath,
|
|
82
|
+
fs.readFileSync(httpdConfPath, 'utf8').replace(`Listen 80`, `# Listen 80`),
|
|
27
83
|
'utf8',
|
|
28
84
|
);
|
|
29
|
-
|
|
85
|
+
|
|
86
|
+
// Modify httpd-ssl.conf (port 443)
|
|
87
|
+
const httpdSslConfPath = `/opt/lampp/etc/extra/httpd-ssl.conf`;
|
|
88
|
+
if (fs.existsSync(httpdSslConfPath) && !fs.readFileSync(httpdSslConfPath, 'utf8').match(/# Listen 443/))
|
|
30
89
|
fs.writeFileSync(
|
|
31
|
-
|
|
32
|
-
fs.readFileSync(
|
|
90
|
+
httpdSslConfPath,
|
|
91
|
+
fs.readFileSync(httpdSslConfPath, 'utf8').replace(`Listen 443`, `# Listen 443`),
|
|
33
92
|
'utf8',
|
|
34
93
|
);
|
|
35
|
-
|
|
94
|
+
|
|
95
|
+
// 5. Modify the lampp startup script to bypass port checking for 80 and 443
|
|
96
|
+
const lamppScriptPath = `/opt/lampp/lampp`;
|
|
97
|
+
if (!fs.readFileSync(lamppScriptPath, 'utf8').match(/testport 443 && false/))
|
|
36
98
|
fs.writeFileSync(
|
|
37
|
-
|
|
38
|
-
fs.readFileSync(
|
|
99
|
+
lamppScriptPath,
|
|
100
|
+
fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 443`, `testport 443 && false`),
|
|
39
101
|
'utf8',
|
|
40
102
|
);
|
|
41
|
-
if (!fs.readFileSync(
|
|
103
|
+
if (!fs.readFileSync(lamppScriptPath, 'utf8').match(/testport 80 && false/))
|
|
42
104
|
fs.writeFileSync(
|
|
43
|
-
|
|
44
|
-
fs.readFileSync(
|
|
105
|
+
lamppScriptPath,
|
|
106
|
+
fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 80`, `testport 80 && false`),
|
|
45
107
|
'utf8',
|
|
46
108
|
);
|
|
47
109
|
|
|
48
|
-
|
|
110
|
+
// 6. Start the service
|
|
49
111
|
cmd = `sudo /opt/lampp/lampp start`;
|
|
50
112
|
if (this.router) fs.writeFileSync(`./tmp/lampp-router.conf`, this.router, 'utf-8');
|
|
51
113
|
shellExec(cmd);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Appends new Apache VirtualHost configuration content to the internal router string.
|
|
118
|
+
* If a router config file exists from a previous run, it loads it first.
|
|
119
|
+
*
|
|
120
|
+
* @memberof LamppService.prototype
|
|
121
|
+
* @param {string} render - The new VirtualHost configuration string to append.
|
|
122
|
+
* @returns {string} The complete, updated router configuration string.
|
|
123
|
+
*/
|
|
124
|
+
appendRouter(render) {
|
|
55
125
|
if (!this.router) {
|
|
56
|
-
if (fs.existsSync(`./tmp/lampp-router.conf`))
|
|
57
|
-
|
|
126
|
+
if (fs.existsSync(`./tmp/lampp-router.conf`)) {
|
|
127
|
+
this.router = fs.readFileSync(`./tmp/lampp-router.conf`, 'utf-8');
|
|
128
|
+
return this.router + render;
|
|
129
|
+
}
|
|
58
130
|
return (this.router = render);
|
|
59
131
|
}
|
|
60
132
|
return (this.router += render);
|
|
61
|
-
}
|
|
62
|
-
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resets the internal router configuration and removes the temporary configuration file.
|
|
137
|
+
*
|
|
138
|
+
* @memberof LamppService.prototype
|
|
139
|
+
* @returns {void}
|
|
140
|
+
*/
|
|
141
|
+
removeRouter() {
|
|
63
142
|
this.router = undefined;
|
|
64
143
|
if (fs.existsSync(`./tmp/lampp-router.conf`)) fs.rmSync(`./tmp/lampp-router.conf`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Installs and configures the Lampp service on Linux.
|
|
148
|
+
* This includes downloading the installer, running it, and setting up initial configurations.
|
|
149
|
+
* Only runs on the 'linux' platform.
|
|
150
|
+
*
|
|
151
|
+
* @memberof LamppService.prototype
|
|
152
|
+
* @returns {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
async install() {
|
|
155
|
+
if (process.platform === 'linux') {
|
|
156
|
+
if (!fs.existsSync(`./engine-private/setup`)) fs.mkdirSync(`./engine-private/setup`, { recursive: true });
|
|
157
|
+
|
|
158
|
+
shellCd(`./engine-private/setup`);
|
|
159
|
+
|
|
160
|
+
if (!process.argv.includes(`server`)) {
|
|
161
|
+
// Download and run the XAMPP installer
|
|
162
|
+
shellExec(
|
|
163
|
+
`curl -Lo xampp-linux-installer.run https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/7.4.30/xampp-linux-x64-7.4.30-1-installer.run?from_af=true`,
|
|
164
|
+
);
|
|
165
|
+
shellExec(`sudo chmod +x xampp-linux-installer.run`);
|
|
166
|
+
shellExec(
|
|
167
|
+
`sudo ./xampp-linux-installer.run --mode unattended && \\` +
|
|
168
|
+
// Create symlink for easier access
|
|
169
|
+
`ln -sf /opt/lampp/lampp /usr/bin/lampp && \\` +
|
|
170
|
+
// Allow all access to xampp config (security measure override)
|
|
171
|
+
`sed -i.bak s'/Require local/Require all granted/g' /opt/lampp/etc/extra/httpd-xampp.conf && \\` +
|
|
172
|
+
// Enable display errors in PHP
|
|
173
|
+
`sed -i.bak s'/display_errors=Off/display_errors=On/g' /opt/lampp/etc/php.ini && \\` +
|
|
174
|
+
// Allow including custom Apache configuration files
|
|
175
|
+
`mkdir /opt/lampp/apache2/conf.d && \\` +
|
|
176
|
+
`echo "IncludeOptional /opt/lampp/apache2/conf.d/*.conf" >> /opt/lampp/etc/httpd.conf && \\` +
|
|
177
|
+
// Create /www directory and symlink it to htdocs
|
|
178
|
+
`mkdir /www && \\` +
|
|
179
|
+
`ln -s /www /opt/lampp/htdocs`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Copy log files to standard names for easier consumption
|
|
184
|
+
if (fs.existsSync(`/opt/lampp/logs/access_log`))
|
|
185
|
+
fs.copySync(`/opt/lampp/logs/access_log`, `/opt/lampp/logs/access.log`);
|
|
186
|
+
if (fs.existsSync(`/opt/lampp/logs/error_log`))
|
|
187
|
+
fs.copySync(`/opt/lampp/logs/error_log`, `/opt/lampp/logs/error.log`);
|
|
188
|
+
if (fs.existsSync(`/opt/lampp/logs/php_error_log`))
|
|
189
|
+
fs.copySync(`/opt/lampp/logs/php_error_log`, `/opt/lampp/logs/php_error.log`);
|
|
190
|
+
if (fs.existsSync(`/opt/lampp/logs/ssl_request_log`))
|
|
191
|
+
fs.copySync(`/opt/lampp/logs/ssl_request_log`, `/opt/lampp/logs/ssl_request.log`);
|
|
192
|
+
|
|
193
|
+
// Initialize the service after installation
|
|
194
|
+
await this.initService({ daemon: true });
|
|
107
195
|
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Creates and appends a new Apache VirtualHost entry to the router configuration for a web application.
|
|
200
|
+
* The router is then applied by calling {@link LamppService#initService}.
|
|
201
|
+
*
|
|
202
|
+
* @memberof LamppService.prototype
|
|
203
|
+
* @param {object} options - Configuration options for the new web application.
|
|
204
|
+
* @param {number} options.port - The port the VirtualHost should listen on.
|
|
205
|
+
* @param {string} options.host - The ServerName/host for the VirtualHost.
|
|
206
|
+
* @param {string} options.path - The base path for error documents (e.g., '/app').
|
|
207
|
+
* @param {string} [options.directory] - Optional absolute path to the document root.
|
|
208
|
+
* @param {string} [options.rootHostPath] - Relative path from the root directory to the document root, used if `directory` is not provided.
|
|
209
|
+
* @param {boolean} [options.redirect] - If true, enables RewriteEngine for redirection.
|
|
210
|
+
* @param {string} [options.redirectTarget] - The target URL for redirection.
|
|
211
|
+
* @param {boolean} [options.resetRouter] - If true, clears the existing router configuration before appending the new one.
|
|
212
|
+
* @returns {{disabled: boolean}} An object indicating if the service is disabled.
|
|
213
|
+
*/
|
|
214
|
+
createApp({ port, host, path, directory, rootHostPath, redirect, redirectTarget, resetRouter }) {
|
|
215
|
+
if (!this.enabled()) return { disabled: true };
|
|
216
|
+
|
|
217
|
+
if (!this.ports.includes(port)) this.ports.push(port);
|
|
218
|
+
if (resetRouter) this.removeRouter();
|
|
219
|
+
|
|
220
|
+
const documentRoot = directory ? directory : `${getRootDirectory()}${rootHostPath}`;
|
|
221
|
+
|
|
222
|
+
// Append the new VirtualHost configuration
|
|
223
|
+
this.appendRouter(`
|
|
114
224
|
Listen ${port}
|
|
115
225
|
|
|
116
226
|
<VirtualHost *:${port}>
|
|
117
|
-
DocumentRoot "${
|
|
227
|
+
DocumentRoot "${documentRoot}"
|
|
118
228
|
ServerName ${host}:${port}
|
|
119
229
|
|
|
120
|
-
<Directory "${
|
|
230
|
+
<Directory "${documentRoot}">
|
|
121
231
|
Options Indexes FollowSymLinks MultiViews
|
|
122
232
|
AllowOverride All
|
|
123
233
|
Require all granted
|
|
@@ -128,12 +238,14 @@ Listen ${port}
|
|
|
128
238
|
? `
|
|
129
239
|
RewriteEngine on
|
|
130
240
|
|
|
241
|
+
# Exclude the ACME challenge path for certificate renewals
|
|
131
242
|
RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge
|
|
132
243
|
RewriteRule ^(.*)$ ${redirectTarget}%{REQUEST_URI} [R=302,L]
|
|
133
244
|
`
|
|
134
245
|
: ''
|
|
135
246
|
}
|
|
136
247
|
|
|
248
|
+
# Custom Error Documents
|
|
137
249
|
ErrorDocument 400 ${path === '/' ? '' : path}/400.html
|
|
138
250
|
ErrorDocument 404 ${path === '/' ? '' : path}/400.html
|
|
139
251
|
ErrorDocument 500 ${path === '/' ? '' : path}/500.html
|
|
@@ -144,56 +256,69 @@ Listen ${port}
|
|
|
144
256
|
</VirtualHost>
|
|
145
257
|
|
|
146
258
|
`);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
259
|
+
|
|
260
|
+
return { disabled: false };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @namespace LamppService
|
|
266
|
+
* @description Exported singleton instance of the LamppService class.
|
|
267
|
+
* This object is used to interact with the Lampp configuration and service.
|
|
268
|
+
* @type {LamppService}
|
|
269
|
+
*/
|
|
270
|
+
const Lampp = new LamppService();
|
|
271
|
+
|
|
272
|
+
// -- helper info --
|
|
273
|
+
|
|
274
|
+
// ERR too many redirects:
|
|
275
|
+
// Check: SELECT * FROM database.wp_options where option_name = 'siteurl' or option_name = 'home';
|
|
276
|
+
// Check: wp-config.php
|
|
277
|
+
// if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
|
278
|
+
// $_SERVER['HTTPS'] = 'on';
|
|
279
|
+
// }
|
|
280
|
+
// For plugins:
|
|
281
|
+
// define( 'FS_METHOD', 'direct' );
|
|
282
|
+
|
|
283
|
+
// ErrorDocument 404 /custom_404.html
|
|
284
|
+
// ErrorDocument 500 /custom_50x.html
|
|
285
|
+
// ErrorDocument 502 /custom_50x.html
|
|
286
|
+
// ErrorDocument 503 /custom_50x.html
|
|
287
|
+
// ErrorDocument 504 /custom_50x.html
|
|
288
|
+
|
|
289
|
+
// Respond When Error Pages are Directly Requested
|
|
290
|
+
|
|
291
|
+
// <Files "custom_404.html">
|
|
292
|
+
// <If "-z %{ENV:REDIRECT_STATUS}">
|
|
293
|
+
// RedirectMatch 404 ^/custom_404.html$
|
|
294
|
+
// </If>
|
|
295
|
+
// </Files>
|
|
296
|
+
|
|
297
|
+
// <Files "custom_50x.html">
|
|
298
|
+
// <If "-z %{ENV:REDIRECT_STATUS}">
|
|
299
|
+
// RedirectMatch 404 ^/custom_50x.html$
|
|
300
|
+
// </If>
|
|
301
|
+
// </Files>
|
|
302
|
+
|
|
303
|
+
// Add www or https with htaccess rewrite
|
|
304
|
+
|
|
305
|
+
// Options +FollowSymLinks
|
|
306
|
+
// RewriteEngine On
|
|
307
|
+
// RewriteCond %{HTTP_HOST} ^example.com [NC]
|
|
308
|
+
// RewriteRule ^(.*)$ http://example.com/$1 [R=301,L]
|
|
309
|
+
|
|
310
|
+
// Redirect http to https with htaccess rewrite
|
|
311
|
+
|
|
312
|
+
// RewriteEngine On
|
|
313
|
+
// RewriteCond %{SERVER_PORT} 80
|
|
314
|
+
// RewriteRule ^(.*)$ https://www.example.com/$1 [R,L]
|
|
315
|
+
|
|
316
|
+
// Redirect to HTTPS with www subdomain
|
|
317
|
+
|
|
318
|
+
// RewriteEngine On
|
|
319
|
+
// RewriteCond %{HTTPS} off [OR]
|
|
320
|
+
// RewriteCond %{HTTP_HOST} ^www\. [NC]
|
|
321
|
+
// RewriteCond %{HTTP_HOST} ^(?:www\.)?(.+)$ [NC]
|
|
322
|
+
// RewriteRule ^ https://%1%{REQUEST_URI} [L,NE,R=301]
|
|
198
323
|
|
|
199
324
|
export { Lampp };
|
package/src/server/auth.js
CHANGED
|
@@ -306,6 +306,61 @@ const validatePasswordMiddleware = (req) => {
|
|
|
306
306
|
};
|
|
307
307
|
|
|
308
308
|
// ---------- Session & Refresh token management ----------
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Creates cookie options for the refresh token.
|
|
312
|
+
* @param {import('express').Request} req The Express request object.
|
|
313
|
+
* @returns {object} Cookie options.
|
|
314
|
+
* @memberof Auth
|
|
315
|
+
*/
|
|
316
|
+
const cookieOptionsFactory = (req) => {
|
|
317
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
318
|
+
|
|
319
|
+
// Determine hostname safely:
|
|
320
|
+
// Prefer origin header if present (it contains protocol + host)
|
|
321
|
+
let candidateHost = undefined;
|
|
322
|
+
try {
|
|
323
|
+
if (req.headers && req.headers.origin) {
|
|
324
|
+
candidateHost = new URL(req.headers.origin).hostname;
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
/* ignore parse error */
|
|
328
|
+
logger.error(e);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// fallback to req.hostname (Express sets this; ensure trust proxy if behind proxy)
|
|
332
|
+
if (!candidateHost) candidateHost = (req.hostname || '').split(':')[0];
|
|
333
|
+
|
|
334
|
+
candidateHost = (candidateHost || '').trim().replace(/^www\./i, '');
|
|
335
|
+
|
|
336
|
+
// Do not set domain for localhost, 127.x.x.x, or plain IPs
|
|
337
|
+
const isIpOrLocal = /^(localhost|127(?:\.\d+){0,2}\.\d+|\[::1\]|\d+\.\d+\.\d+\.\d+)$/i.test(candidateHost);
|
|
338
|
+
const domain = isProduction && candidateHost && !isIpOrLocal ? `.${candidateHost}` : undefined;
|
|
339
|
+
|
|
340
|
+
// Determine if request is secure: respect X-Forwarded-Proto when behind proxy
|
|
341
|
+
const forwardedProto = (req.headers && req.headers['x-forwarded-proto']) || '';
|
|
342
|
+
const reqIsSecure = Boolean(req.secure || forwardedProto.split(',')[0] === 'https');
|
|
343
|
+
|
|
344
|
+
// secure must be true for SameSite=None to work across sites
|
|
345
|
+
const secure = isProduction ? reqIsSecure : false;
|
|
346
|
+
const sameSite = secure ? 'None' : 'Lax';
|
|
347
|
+
|
|
348
|
+
// Safe parse of maxAge minutes
|
|
349
|
+
const minutes = Number.parseInt(process.env.REFRESH_EXPIRE_MINUTES, 10);
|
|
350
|
+
const maxAge = Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : undefined;
|
|
351
|
+
|
|
352
|
+
const opts = {
|
|
353
|
+
httpOnly: true,
|
|
354
|
+
secure,
|
|
355
|
+
sameSite,
|
|
356
|
+
path: '/',
|
|
357
|
+
};
|
|
358
|
+
if (typeof maxAge !== 'undefined') opts.maxAge = maxAge;
|
|
359
|
+
if (domain) opts.domain = domain;
|
|
360
|
+
|
|
361
|
+
return opts;
|
|
362
|
+
};
|
|
363
|
+
|
|
309
364
|
/**
|
|
310
365
|
* Create session and set refresh cookie. Rotating and hashed stored token.
|
|
311
366
|
* @param {object} user The user object.
|
|
@@ -339,13 +394,7 @@ async function createSessionAndUserToken(user, User, req, res, options = { host:
|
|
|
339
394
|
const jwtid = session._id.toString();
|
|
340
395
|
|
|
341
396
|
// Secure cookie settings
|
|
342
|
-
res.cookie('refreshToken', refreshToken,
|
|
343
|
-
httpOnly: true,
|
|
344
|
-
secure: process.env.NODE_ENV === 'production',
|
|
345
|
-
sameSite: 'Lax',
|
|
346
|
-
maxAge: parseInt(process.env.REFRESH_EXPIRE_MINUTES) * 60 * 1000,
|
|
347
|
-
path: '/',
|
|
348
|
-
});
|
|
397
|
+
res.cookie('refreshToken', refreshToken, cookieOptionsFactory(req));
|
|
349
398
|
|
|
350
399
|
return { jwtid };
|
|
351
400
|
}
|
|
@@ -439,13 +488,7 @@ async function refreshSessionAndToken(req, res, User, options = { host: '', path
|
|
|
439
488
|
|
|
440
489
|
logger.warn('Refreshed session for user ' + user.email);
|
|
441
490
|
|
|
442
|
-
res.cookie('refreshToken', refreshToken,
|
|
443
|
-
httpOnly: true,
|
|
444
|
-
secure: process.env.NODE_ENV === 'production',
|
|
445
|
-
sameSite: 'Lax',
|
|
446
|
-
maxAge: parseInt(process.env.REFRESH_EXPIRE_MINUTES) * 60 * 1000,
|
|
447
|
-
path: '/',
|
|
448
|
-
});
|
|
491
|
+
res.cookie('refreshToken', refreshToken, cookieOptionsFactory(req));
|
|
449
492
|
|
|
450
493
|
return jwtSign(
|
|
451
494
|
UserDto.auth.payload(user, session._id.toString(), req.ip, req.headers['user-agent'], options.host, options.path),
|
|
@@ -533,14 +576,22 @@ function applySecurity(app, opts = {}) {
|
|
|
533
576
|
blockAllMixedContent: [],
|
|
534
577
|
fontSrc: ["'self'", httpDirective, 'data:'],
|
|
535
578
|
frameAncestors: frameAncestors,
|
|
536
|
-
imgSrc: ["'self'", 'data:', httpDirective],
|
|
579
|
+
imgSrc: ["'self'", 'data:', httpDirective, 'https:', 'blob:'],
|
|
537
580
|
objectSrc: ["'none'"],
|
|
538
581
|
// script-src and script-src-elem include dynamic nonce
|
|
539
|
-
scriptSrc: [
|
|
582
|
+
scriptSrc: [
|
|
583
|
+
"'self'",
|
|
584
|
+
(req, res) => `'nonce-${res.locals.nonce}'`,
|
|
585
|
+
(req, res) => (res.locals.isSwagger ? "'unsafe-inline'" : ''),
|
|
586
|
+
],
|
|
540
587
|
scriptSrcElem: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
|
|
541
588
|
// style-src: avoid 'unsafe-inline' when possible; if you must inline styles,
|
|
542
589
|
// use a nonce for them too (or hash).
|
|
543
|
-
styleSrc: [
|
|
590
|
+
styleSrc: [
|
|
591
|
+
"'self'",
|
|
592
|
+
httpDirective,
|
|
593
|
+
(req, res) => (res.locals.isSwagger ? "'unsafe-inline'" : `'nonce-${res.locals.nonce}'`),
|
|
594
|
+
],
|
|
544
595
|
// deny plugins
|
|
545
596
|
objectSrc: ["'none'"],
|
|
546
597
|
},
|