@interopio/gateway-server 0.20.0 → 0.22.0
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/bin/gateway-server.cjs +726 -0
- package/changelog.md +27 -0
- package/dist/config.cjs +2 -0
- package/dist/config.cjs.map +7 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +7 -0
- package/dist/index.cjs +17 -3
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +17 -3
- package/dist/index.js.map +4 -4
- package/dist/metrics/publisher/rest.js +1 -1
- package/dist/metrics/publisher/rest.js.map +3 -3
- package/dist/tools.cjs +2 -0
- package/dist/tools.cjs.map +7 -0
- package/dist/tools.js +2 -0
- package/dist/tools.js.map +7 -0
- package/dist/web/test.js +13 -2
- package/dist/web/test.js.map +4 -4
- package/gateway-server.d.ts +3 -116
- package/package.json +11 -5
- package/readme.md +58 -0
- package/types/config.d.ts +147 -0
- package/types/manage.d.ts +46 -0
- package/types/path.d.ts +20 -0
- package/types/tools.d.ts +1 -2
- package/types/web/server.d.ts +11 -3
- package/types/web/test.d.ts +6 -2
- package/dist/metrics/publisher/rest.cjs +0 -2
- package/dist/metrics/publisher/rest.cjs.map +0 -7
- package/dist/tools/index.js +0 -2
- package/dist/tools/index.js.map +0 -7
- package/gateway-server +0 -442
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { parseArgs } = require('node:util');
|
|
6
|
+
const { configure } = require('@interopio/gateway/logging/core');
|
|
7
|
+
|
|
8
|
+
// import { GatewayServer } from './src/index.ts';
|
|
9
|
+
// import { mkcert, argon2, manage } from './src/tools/index.ts';
|
|
10
|
+
|
|
11
|
+
const { mkcert, argon2, manage } = require('@interopio/gateway-server/tools');
|
|
12
|
+
const { GatewayServer } = require('@interopio/gateway-server');
|
|
13
|
+
|
|
14
|
+
function showHelp() {
|
|
15
|
+
console.log(`
|
|
16
|
+
Usage: npx @interopio/gateway-server <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
run Start the gateway server
|
|
20
|
+
manage Send management commands to a running gateway server
|
|
21
|
+
passwd Generate password hash (default using Argon2)
|
|
22
|
+
mkcert Generate client and/or server certificate signed by Dev CA
|
|
23
|
+
|
|
24
|
+
run options:
|
|
25
|
+
-p, --port <port> Specify port or port range to bind the server to (default: 0 i.e. random)
|
|
26
|
+
examples: 8385, 8000-8100, 3000,4000-4050
|
|
27
|
+
-H, --host <host> Network address to bind to (default: unspecified, listens on all)
|
|
28
|
+
examples: localhost, 127.0.0.1, 0.0.0.0, ::1
|
|
29
|
+
-u, --user <name> Enables basic authentication and sets the admin username
|
|
30
|
+
--no-auth Disable authentication (overrides config, sets auth type to 'none')
|
|
31
|
+
-S, --ssl, --tls Enable HTTPS. Auto-generates Dev CA and/or server certificates if not
|
|
32
|
+
present. Enables x509 client cert auth if --user not specified.
|
|
33
|
+
--cert <file> File with SSL/TLS certificate (default: ./gateway-server.crt)
|
|
34
|
+
--key <file> File with SSL/TLS private key (default: ./gateway-server.key)
|
|
35
|
+
--ca <file> File with CA certificates (default: ./gateway-ca.crt)
|
|
36
|
+
--ca-key <file> File with CA private key (default: ./gateway-ca.key)
|
|
37
|
+
--no-ssl, --no-tls Disable SSL/TLS (overrides config)
|
|
38
|
+
-g, --gateway Enable gateway endpoint (default route: /)
|
|
39
|
+
--no-gateway Disable gateway endpoint (overrides config)
|
|
40
|
+
-s, --static <location> Serve static files from specified location (e.g., ./public)
|
|
41
|
+
-c, --config <file> Server configuration file (JSON format)
|
|
42
|
+
--debug Enable debug logging (default: info level)
|
|
43
|
+
|
|
44
|
+
manage options:
|
|
45
|
+
--path <path> Path to the gateway control socket (named pipe on Windows, Unix socket)
|
|
46
|
+
examples: \\\\.\\pipe\\glue42-gateway-xxx, /tmp/gateway.sock
|
|
47
|
+
-p, --port <port> TCP port of the management server
|
|
48
|
+
--timeout <ms> Connection timeout in milliseconds (default: 5000)
|
|
49
|
+
<command> Management command to execute: info, shutdown
|
|
50
|
+
[key=value...] Additional command parameters as key=value pairs
|
|
51
|
+
Values are parsed as JSON (numbers, booleans, arrays, objects)
|
|
52
|
+
or kept as strings if not valid JSON
|
|
53
|
+
<json> Alternatively, pass entire command as a JSON object
|
|
54
|
+
example: '{"command":"update-auth","type":"basic"}'
|
|
55
|
+
|
|
56
|
+
passwd options:
|
|
57
|
+
(no args) Prompt for password interactively (masked input)
|
|
58
|
+
--stdin Read password from stdin (for piping, e.g., echo "pass" | ...)
|
|
59
|
+
|
|
60
|
+
mkcert options:
|
|
61
|
+
--client Generate client certificate (default: server certificate)
|
|
62
|
+
-u, --user <name> Common Name for the certificate (default: dev-user for client certs)
|
|
63
|
+
(--client only)
|
|
64
|
+
--ca <file> File with CA certificate (default: ./gateway-ca.crt)
|
|
65
|
+
--ca-key <file> File with CA private key (default: ./gateway-ca.key)
|
|
66
|
+
--key <file> Output file for private key (default: ./gateway-server.key for server,
|
|
67
|
+
./gateway-client.key for client)
|
|
68
|
+
--cert <file> Output file for certificate (default: ./gateway-server.crt for server,
|
|
69
|
+
./gateway-client.crt for client)
|
|
70
|
+
[name...] DNS names, IP addresses, or email addresses for subjectAltName
|
|
71
|
+
(DNS by default, prefix with 'IP:' or 'EMAIL:' for other types)
|
|
72
|
+
|
|
73
|
+
Global Options:
|
|
74
|
+
-v, --version Show version information and exit
|
|
75
|
+
--help Show this help message and exit
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
gateway-server run -p 3000
|
|
79
|
+
gateway-server run -u admin --port 8385,8388 --debug
|
|
80
|
+
gateway-server run -p 8443 --ssl --user admin --gateway
|
|
81
|
+
gateway-server run --port 42443 --tls --ca-key ./ssl/ca.key --host example.com --gateway
|
|
82
|
+
gateway-server run -p 3000 --static ./public --config ./server-config.json
|
|
83
|
+
gateway-server run --config ./server-config.json --no-gateway --no-ssl --no-auth
|
|
84
|
+
gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx info
|
|
85
|
+
gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx shutdown
|
|
86
|
+
gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx custom-cmd key1=value1 count=42 'data={"user":"test"}' --timeout 10000
|
|
87
|
+
gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx '{"command":"update-auth","auth":{"token":"my-token"}}}'
|
|
88
|
+
gateway-server passwd
|
|
89
|
+
echo "mySecret123" | gateway-server passwd --stdin
|
|
90
|
+
gateway-server mkcert
|
|
91
|
+
gateway-server mkcert localhost 127.0.0.1 IP:192.168.1.100
|
|
92
|
+
gateway-server mkcert --client EMAIL:john.doe@example.com
|
|
93
|
+
gateway-server mkcert example.com *.example.com --key ./gateway-server.key --cert ./gateway-server.crt
|
|
94
|
+
`);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function showVersion() {
|
|
99
|
+
console.log(GatewayServer.VERSION);
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse command-line arguments
|
|
104
|
+
const { values: globalValues, positionals } = parseArgs({
|
|
105
|
+
args: process.argv.slice(2),
|
|
106
|
+
options: {
|
|
107
|
+
version: { type: 'boolean', short: 'v' },
|
|
108
|
+
help: { type: 'boolean', short: 'h' },
|
|
109
|
+
},
|
|
110
|
+
strict: false,
|
|
111
|
+
allowPositionals: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Check for global flags first
|
|
115
|
+
if (globalValues.version) {
|
|
116
|
+
showVersion();
|
|
117
|
+
}
|
|
118
|
+
if (globalValues.help) {
|
|
119
|
+
showHelp();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Determine command (required)
|
|
123
|
+
if (positionals.length === 0) {
|
|
124
|
+
console.error('Error: Command is required');
|
|
125
|
+
console.log('Use --help to see available commands');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const command = positionals[0];
|
|
130
|
+
|
|
131
|
+
// Route to appropriate command handler
|
|
132
|
+
switch (command) {
|
|
133
|
+
case 'run':
|
|
134
|
+
runServer().catch(e => {console.error(e); process.exit(1);});
|
|
135
|
+
break;
|
|
136
|
+
case 'manage':
|
|
137
|
+
manageCommand().catch(e => {console.error(e); process.exit(1);});
|
|
138
|
+
break;
|
|
139
|
+
case 'passwd':
|
|
140
|
+
passwdCommand().catch(e => {console.error(e); process.exit(1);});
|
|
141
|
+
break;
|
|
142
|
+
case 'mkcert':
|
|
143
|
+
mkcertCommand().catch(e => {console.error(e); process.exit(1);});
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
console.error(`Error: Unknown command "${command}"`);
|
|
147
|
+
console.log('Use --help to see available commands');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Command: manage - Send management commands to a running gateway server
|
|
152
|
+
async function manageCommand() {
|
|
153
|
+
const { values, positionals } = parseArgs({
|
|
154
|
+
args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'manage'
|
|
155
|
+
options: {
|
|
156
|
+
path: { type: 'string' },
|
|
157
|
+
port: { type: 'string', short: 'p' },
|
|
158
|
+
timeout: { type: 'string' },
|
|
159
|
+
},
|
|
160
|
+
strict: true,
|
|
161
|
+
allowPositionals: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const socketPath = values.path;
|
|
165
|
+
const port = values.port ? parseInt(values.port, 10) : undefined;
|
|
166
|
+
const timeout = values.timeout ? parseInt(values.timeout, 10) : undefined;
|
|
167
|
+
const firstArg = positionals[0];
|
|
168
|
+
|
|
169
|
+
if (!socketPath && !port) {
|
|
170
|
+
console.error('Error: --path or --port is required');
|
|
171
|
+
console.error('Example: gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx info');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!firstArg) {
|
|
176
|
+
console.error('Error: management command is required');
|
|
177
|
+
console.error('Available commands: info, shutdown');
|
|
178
|
+
console.error('Or pass a JSON object: gateway-server manage --path ... \'{"command":"info"}\'');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Try to parse first argument as JSON (entire command object)
|
|
183
|
+
let commandParams;
|
|
184
|
+
if (firstArg.startsWith('{')) {
|
|
185
|
+
try {
|
|
186
|
+
commandParams = JSON.parse(firstArg);
|
|
187
|
+
if (!commandParams.command) {
|
|
188
|
+
console.error('Error: JSON command must have a "command" property');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.error(`Error: invalid JSON: ${e.message}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Parse as command name + key=value pairs
|
|
199
|
+
commandParams = { command: firstArg };
|
|
200
|
+
for (let i = 1; i < positionals.length; i++) {
|
|
201
|
+
const arg = positionals[i];
|
|
202
|
+
const eqIndex = arg.indexOf('=');
|
|
203
|
+
if (eqIndex === -1) {
|
|
204
|
+
console.error(`Error: invalid parameter "${arg}". Expected format: key=value`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const key = arg.slice(0, eqIndex);
|
|
208
|
+
let value = arg.slice(eqIndex + 1);
|
|
209
|
+
|
|
210
|
+
// Try to parse as JSON for numbers, booleans, arrays, objects
|
|
211
|
+
try {
|
|
212
|
+
value = JSON.parse(value);
|
|
213
|
+
} catch {
|
|
214
|
+
// Keep as string if not valid JSON
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
commandParams[key] = value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const server = socketPath ? { path: socketPath } : { port };
|
|
222
|
+
try {
|
|
223
|
+
const result = await manage.sendCommand(server, commandParams, { timeout });
|
|
224
|
+
if (result === undefined) {
|
|
225
|
+
console.log('<no response>');
|
|
226
|
+
}
|
|
227
|
+
else if (typeof result === 'string') {
|
|
228
|
+
console.log(result);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
console.log(JSON.stringify(result, null, 2));
|
|
232
|
+
}
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
console.error(`Error: ${e.message || e}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Command: passwd - Generate password hash
|
|
242
|
+
async function passwdCommand() {
|
|
243
|
+
const { values } = parseArgs({
|
|
244
|
+
args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'passwd'
|
|
245
|
+
options: {
|
|
246
|
+
stdin: { type: 'boolean' },
|
|
247
|
+
},
|
|
248
|
+
strict: true,
|
|
249
|
+
allowPositionals: false,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
let password/*: string = undefined!*/;
|
|
253
|
+
|
|
254
|
+
// Read password from stdin if specified
|
|
255
|
+
if (values.stdin) {
|
|
256
|
+
password = await readPasswordFromStdin();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// If no password provided, prompt interactively
|
|
260
|
+
if (!password) {
|
|
261
|
+
password = await promptPassword('Enter password: ');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!password) {
|
|
265
|
+
console.error('Error: password is required');
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const hashValue = await argon2.hash(password);
|
|
270
|
+
const hash = `{argon2id}${hashValue}`;
|
|
271
|
+
console.log(hash);
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Read password from piped stdin (non-interactive)
|
|
276
|
+
async function readPasswordFromStdin() {
|
|
277
|
+
// Check if stdin is a TTY (interactive terminal)
|
|
278
|
+
if (process.stdin.isTTY) {
|
|
279
|
+
// Interactive mode - use readline with hidden input
|
|
280
|
+
return promptPassword('Password: ');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Piped mode - read until EOF or newline
|
|
284
|
+
const chunks/*: Uint8Array[]*/ = [];
|
|
285
|
+
for await (const chunk of process.stdin) {
|
|
286
|
+
chunks.push(chunk);
|
|
287
|
+
}
|
|
288
|
+
const content = Buffer.concat(chunks).toString('utf8');
|
|
289
|
+
// Strip trailing newline
|
|
290
|
+
return content.replace(/\r?\n$/, '');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Prompt for password with masked input
|
|
294
|
+
async function promptPassword(prompt/*: string*/) {
|
|
295
|
+
return new Promise/*<string>*/((resolve) => {
|
|
296
|
+
process.stdout.write(prompt);
|
|
297
|
+
|
|
298
|
+
// Only set raw mode if stdin is a TTY
|
|
299
|
+
if (!process.stdin.isTTY) {
|
|
300
|
+
console.error('Error: Interactive password prompt requires a terminal');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
process.stdin.setRawMode(true);
|
|
305
|
+
process.stdin.resume();
|
|
306
|
+
process.stdin.setEncoding('utf8');
|
|
307
|
+
|
|
308
|
+
let password = '';
|
|
309
|
+
|
|
310
|
+
const cleanup = () => {
|
|
311
|
+
process.stdin.setRawMode(false);
|
|
312
|
+
process.stdin.pause();
|
|
313
|
+
process.stdin.removeListener('data', onData);
|
|
314
|
+
process.stdout.write('\n');
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const onData = (char/*: string*/) => {
|
|
318
|
+
// Ctrl+C
|
|
319
|
+
if (char === '\u0003') {
|
|
320
|
+
cleanup();
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Enter
|
|
325
|
+
if (char === '\r' || char === '\n') {
|
|
326
|
+
cleanup();
|
|
327
|
+
resolve(password);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Backspace
|
|
332
|
+
if (char === '\u007f' || char === '\b') {
|
|
333
|
+
if (password.length > 0) {
|
|
334
|
+
password = password.slice(0, -1);
|
|
335
|
+
process.stdout.write('\b \b');
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Regular character
|
|
341
|
+
password += char;
|
|
342
|
+
process.stdout.write('*');
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
process.stdin.on('data', onData);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Command: mkcert - Generate client or server certificate
|
|
350
|
+
async function mkcertCommand() {
|
|
351
|
+
const { values, positionals } = parseArgs({
|
|
352
|
+
args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'mkcert'
|
|
353
|
+
options: {
|
|
354
|
+
client: { type: 'boolean' },
|
|
355
|
+
user: { type: 'string', short: 'u' },
|
|
356
|
+
ca: { type: 'string' },
|
|
357
|
+
'ca-key': { type: 'string' },
|
|
358
|
+
key: { type: 'string' },
|
|
359
|
+
cert: { type: 'string' },
|
|
360
|
+
},
|
|
361
|
+
strict: true,
|
|
362
|
+
allowPositionals: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const isClientCert = values.client || false;
|
|
366
|
+
const user = values.user || 'dev-user';
|
|
367
|
+
const caCert = values.ca || './gateway-ca.crt';
|
|
368
|
+
const caKey = values['ca-key'] || './gateway-ca.key';
|
|
369
|
+
let keyPath = values.key;
|
|
370
|
+
let certPath = values.cert;
|
|
371
|
+
const sanEntries = positionals;
|
|
372
|
+
|
|
373
|
+
// Default output paths
|
|
374
|
+
// Users typically specify just --cert, and key should go in the same file
|
|
375
|
+
if (!keyPath && !certPath) {
|
|
376
|
+
// Neither specified: use defaults based on certificate type
|
|
377
|
+
if (isClientCert) {
|
|
378
|
+
// Client certificate: combined file by default
|
|
379
|
+
keyPath = './gateway-client.key';
|
|
380
|
+
certPath = './gateway-client.crt';
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Server certificate: separate files by default
|
|
384
|
+
keyPath = './gateway-server.key';
|
|
385
|
+
certPath = './gateway-server.crt';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (keyPath && !certPath) {
|
|
389
|
+
// Only --key specified: cert goes in the same file (combined)
|
|
390
|
+
certPath = keyPath;
|
|
391
|
+
}
|
|
392
|
+
else if (!keyPath && certPath) {
|
|
393
|
+
// Only --cert specified: key goes in the same file (combined)
|
|
394
|
+
keyPath = certPath;
|
|
395
|
+
}
|
|
396
|
+
// else: both specified, use as-is
|
|
397
|
+
|
|
398
|
+
if (sanEntries.length === 0) {
|
|
399
|
+
if (isClientCert) {
|
|
400
|
+
sanEntries.push(user);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
// Default SAN entry for server certs
|
|
404
|
+
sanEntries.push('localhost');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const { readFile, writeFile } = await import('node:fs/promises');
|
|
409
|
+
const { KEYUTIL, X509 } = await import('jsrsasign');
|
|
410
|
+
|
|
411
|
+
// Load CA key
|
|
412
|
+
let caKeyObj/*: ReturnType<typeof KEYUTIL.getKey>*/;
|
|
413
|
+
try {
|
|
414
|
+
const caKeyPem = await readFile(caKey, 'utf8');
|
|
415
|
+
caKeyObj = KEYUTIL.getKey(caKeyPem);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error(`Error: Cannot read CA key from ${caKey}`);
|
|
418
|
+
console.error(error?.['message']);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Extract issuer from CA certificate
|
|
423
|
+
let issuer/*: string*/;
|
|
424
|
+
try {
|
|
425
|
+
const caCertPem = await readFile(caCert, 'utf8');
|
|
426
|
+
const caCertObj = new X509();
|
|
427
|
+
caCertObj.readCertPEM(caCertPem);
|
|
428
|
+
issuer = caCertObj.getSubjectString();
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(`Error: Cannot read CA certificate from ${caCert}`);
|
|
431
|
+
console.error(error?.['message']);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Generate certificate using SAN entries
|
|
436
|
+
const cert = mkcert.generateCert(caKeyObj, issuer, sanEntries, isClientCert);
|
|
437
|
+
|
|
438
|
+
// Write files
|
|
439
|
+
try {
|
|
440
|
+
if (keyPath === certPath) {
|
|
441
|
+
// Concatenate cert and key into single file (cert first, then key)
|
|
442
|
+
const combined = cert.cert + cert.key;
|
|
443
|
+
await writeFile(keyPath, combined, { mode: 0o600 });
|
|
444
|
+
console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`);
|
|
445
|
+
console.log(` Combined (cert+key): ${keyPath}`);
|
|
446
|
+
if (sanEntries.length > 0) {
|
|
447
|
+
console.log(` SAN: ${sanEntries.join(', ')}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
// Write separate files
|
|
452
|
+
await writeFile(keyPath, cert.key, { mode: 0o600 });
|
|
453
|
+
await writeFile(certPath, cert.cert, { mode: 0o644 });
|
|
454
|
+
console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`);
|
|
455
|
+
console.log(` Private key: ${keyPath}`);
|
|
456
|
+
console.log(` Certificate: ${certPath}`);
|
|
457
|
+
if (sanEntries.length > 0) {
|
|
458
|
+
console.log(` SAN: ${sanEntries.join(', ')}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
console.error(`Error: Cannot write certificate files`);
|
|
466
|
+
console.error(error?.['message']);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Command: run - Start the server
|
|
472
|
+
async function runServer() {
|
|
473
|
+
const { values } = parseArgs({
|
|
474
|
+
args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'run'
|
|
475
|
+
options: {
|
|
476
|
+
port: { type: 'string', short: 'p' },
|
|
477
|
+
host: { type: 'string', short: 'H' },
|
|
478
|
+
user: { type: 'string', short: 'u' },
|
|
479
|
+
debug: { type: 'boolean' },
|
|
480
|
+
ssl: { type: 'boolean', short: 'S' },
|
|
481
|
+
tls: { type: 'boolean' },
|
|
482
|
+
cert: { type: 'string' },
|
|
483
|
+
key: { type: 'string' },
|
|
484
|
+
ca: { type: 'string' },
|
|
485
|
+
'ca-key': { type: 'string' },
|
|
486
|
+
config: { type: 'string', short: 'c' },
|
|
487
|
+
gateway: { type: 'boolean', short: 'g' },
|
|
488
|
+
static: { type: 'string', short: 's', multiple: true },
|
|
489
|
+
auth: { type: 'boolean' }
|
|
490
|
+
},
|
|
491
|
+
strict: true,
|
|
492
|
+
allowNegative: true,
|
|
493
|
+
allowPositionals: false,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const options = {
|
|
497
|
+
port: values.port,
|
|
498
|
+
host: values.host,
|
|
499
|
+
user: values.user,
|
|
500
|
+
debug: values.debug || false,
|
|
501
|
+
ssl: values.ssl || values.tls || false,
|
|
502
|
+
noSsl: values.ssl === false, // --no-ssl explicitly set
|
|
503
|
+
cert: values.cert,
|
|
504
|
+
key: values.key,
|
|
505
|
+
ca: values.ca,
|
|
506
|
+
caKey: values['ca-key'],
|
|
507
|
+
config: values.config,
|
|
508
|
+
gateway: values.gateway,
|
|
509
|
+
noGateway: values.gateway === false, // --no-gateway explicitly set
|
|
510
|
+
static: values.static,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
configure({
|
|
514
|
+
level: options.debug ? {
|
|
515
|
+
gateway: 'debug',
|
|
516
|
+
"gateway.node": 'info',
|
|
517
|
+
} : 'info',
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Load server configuration from file if specified
|
|
521
|
+
let serverConfig/*: GatewayServer.ServerConfig*/ = {};
|
|
522
|
+
|
|
523
|
+
const { access, constants } = await import('node:fs/promises');
|
|
524
|
+
const { resolve, extname } = await import('node:path');
|
|
525
|
+
|
|
526
|
+
let configPath/*: string*/;
|
|
527
|
+
for (const fileName of
|
|
528
|
+
[
|
|
529
|
+
'gateway-server.config.js',
|
|
530
|
+
'gateway-server.config.mjs',
|
|
531
|
+
'gateway-server.config.cjs',
|
|
532
|
+
'gateway-server.config.ts',
|
|
533
|
+
'gateway-server.config.mts',
|
|
534
|
+
'gateway-server.config.cts',
|
|
535
|
+
'gateway-server.config.json'
|
|
536
|
+
]) {
|
|
537
|
+
configPath = resolve(process.cwd(), fileName);
|
|
538
|
+
try {
|
|
539
|
+
await access(configPath, constants.R_OK);
|
|
540
|
+
// found readable config file, load it
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
configPath = undefined;
|
|
545
|
+
// continue to next candidate
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (options.config) {
|
|
550
|
+
configPath = options.config;
|
|
551
|
+
}
|
|
552
|
+
if (configPath) {
|
|
553
|
+
try {
|
|
554
|
+
if (['.json'].includes(extname(configPath))) {
|
|
555
|
+
// JSON config file - read and parse manually
|
|
556
|
+
const { readFile } = await import('node:fs/promises');
|
|
557
|
+
const configContent = await readFile(configPath, 'utf8');
|
|
558
|
+
serverConfig = JSON.parse(configContent);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
// JS/TS config file - import dynamically
|
|
562
|
+
const { pathToFileURL } = await import('node:url');
|
|
563
|
+
const importedConfig = await import(pathToFileURL(configPath));
|
|
564
|
+
serverConfig = importedConfig.default || importedConfig;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
console.error(`Error: Cannot read server config from ${configPath}`);
|
|
569
|
+
console.error(error?.['message']);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Apply CLI overrides to server config
|
|
575
|
+
// CLI arguments take precedence over config file
|
|
576
|
+
|
|
577
|
+
// Override port if specified
|
|
578
|
+
if (values.port !== undefined) {
|
|
579
|
+
serverConfig.port = options.port;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Override host if specified
|
|
583
|
+
if (values.host !== undefined) {
|
|
584
|
+
serverConfig.host = options.host;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Override SSL configuration
|
|
588
|
+
if (options.noSsl) {
|
|
589
|
+
// --no-ssl: delete SSL config completely
|
|
590
|
+
delete serverConfig.ssl;
|
|
591
|
+
options.ssl = false;
|
|
592
|
+
} else if (options.ssl) {
|
|
593
|
+
// --ssl specified: merge CLI args with existing config
|
|
594
|
+
if (!serverConfig.ssl) {
|
|
595
|
+
serverConfig.ssl = {};
|
|
596
|
+
}
|
|
597
|
+
// Only set properties if CLI options are explicitly provided
|
|
598
|
+
if (options.key !== undefined) {
|
|
599
|
+
serverConfig.ssl.key = options.key;
|
|
600
|
+
}
|
|
601
|
+
if (options.cert !== undefined) {
|
|
602
|
+
serverConfig.ssl.cert = options.cert;
|
|
603
|
+
}
|
|
604
|
+
if (options.ca !== undefined) {
|
|
605
|
+
serverConfig.ssl.ca = options.ca;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// else: use whatever is in serverConfig (if any)
|
|
609
|
+
|
|
610
|
+
// Determine effective SSL state
|
|
611
|
+
const effectiveSsl = options.ssl || (!options.noSsl && serverConfig?.ssl !== undefined);
|
|
612
|
+
|
|
613
|
+
// Configure authentication
|
|
614
|
+
let auth/*: GatewayServer.AuthConfig*/;
|
|
615
|
+
if (values.auth === false) {
|
|
616
|
+
// --no-auth: delete auth config from server config and set to 'none'
|
|
617
|
+
delete serverConfig.auth;
|
|
618
|
+
auth = { type: 'none' };
|
|
619
|
+
}
|
|
620
|
+
else if (serverConfig.auth) {
|
|
621
|
+
// Use auth from config file if present
|
|
622
|
+
auth = serverConfig.auth;
|
|
623
|
+
|
|
624
|
+
// Allow --user CLI option to override auth.user.name from config
|
|
625
|
+
if (options.user) {
|
|
626
|
+
if (!auth.user) {
|
|
627
|
+
auth.user = {name: options.user};
|
|
628
|
+
}
|
|
629
|
+
auth.user.name = options.user;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
// No auth in config file, determine from CLI args or defaults
|
|
634
|
+
auth = {
|
|
635
|
+
type: options.user ? 'basic' : effectiveSsl ? 'x509' : 'none'
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
if (options.user) {
|
|
639
|
+
auth.user = { name: options.user };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (effectiveSsl && auth.type === 'x509') {
|
|
643
|
+
auth.x509 = {
|
|
644
|
+
key: options.caKey || './gateway-ca.key',
|
|
645
|
+
// principalAltName: 'email',
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Ensure SSL config exists and has proper mutual TLS settings
|
|
649
|
+
if (!serverConfig.ssl) {
|
|
650
|
+
serverConfig.ssl = {};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Enable client certificate verification with proper mutual TLS
|
|
654
|
+
serverConfig.ssl.requestCert = true;
|
|
655
|
+
|
|
656
|
+
// Enforce trust verification to prevent certificate spoofing
|
|
657
|
+
// rejectUnauthorized must be true to ensure only trusted client certs are accepted
|
|
658
|
+
serverConfig.ssl.rejectUnauthorized = true;
|
|
659
|
+
|
|
660
|
+
// Set CA for client certificate verification (required for mutual TLS)
|
|
661
|
+
// Use the same CA that signs client certificates
|
|
662
|
+
if (!serverConfig.ssl.ca) {
|
|
663
|
+
serverConfig.ssl.ca = options.ca || './gateway-ca.crt';
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Handle SSL certificate auto-generation when --ssl is specified via CLI
|
|
669
|
+
// but config file doesn't have server certificates
|
|
670
|
+
if (options.ssl && effectiveSsl && serverConfig.ssl) {
|
|
671
|
+
const hasCert = serverConfig.ssl.cert || serverConfig.ssl.key;
|
|
672
|
+
if (!hasCert) {
|
|
673
|
+
// SSL enabled but no server cert/key provided
|
|
674
|
+
// We need a CA key for auto-generating server certificates
|
|
675
|
+
// Set default CA key path if not already configured
|
|
676
|
+
if (!auth.x509) {
|
|
677
|
+
auth.x509 = {};
|
|
678
|
+
}
|
|
679
|
+
if (!auth.x509.key) {
|
|
680
|
+
// Use CLI option, or default to ./gateway-ca.key
|
|
681
|
+
auth.x509.key = options.caKey || './gateway-ca.key';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Override gateway configuration
|
|
687
|
+
if (options.noGateway) {
|
|
688
|
+
// --no-gateway: delete gateway config completely
|
|
689
|
+
delete serverConfig.gateway;
|
|
690
|
+
} else if (options.gateway) {
|
|
691
|
+
// --gateway specified: enable gateway endpoint, respect existing config
|
|
692
|
+
if (!serverConfig.gateway) {
|
|
693
|
+
serverConfig.gateway = {};
|
|
694
|
+
}
|
|
695
|
+
// Set defaults only if not already present in config
|
|
696
|
+
if (!serverConfig.gateway.route) {
|
|
697
|
+
serverConfig.gateway.route = '/';
|
|
698
|
+
}
|
|
699
|
+
if (!serverConfig.gateway.authorize) {
|
|
700
|
+
serverConfig.gateway.authorize = { access: auth.type !== 'none' ? 'authenticated' : 'permitted' };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// else: use whatever is in serverConfig (if any)
|
|
704
|
+
|
|
705
|
+
// Override static resources if specified
|
|
706
|
+
if (options.static && options.static.length > 0) {
|
|
707
|
+
if (!serverConfig.resources) {
|
|
708
|
+
serverConfig.resources = {locations: options.static};
|
|
709
|
+
}
|
|
710
|
+
serverConfig.resources.locations = options.static;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Set auth in server config
|
|
714
|
+
serverConfig.auth = auth;
|
|
715
|
+
|
|
716
|
+
const server = await GatewayServer.Factory({
|
|
717
|
+
...(serverConfig)
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
process.on('SIGINT', async () => {
|
|
721
|
+
await server.close();
|
|
722
|
+
process.exit(0);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// process.title = `${GatewayServer.VERSION} ${server.address.port}`;
|
|
726
|
+
}
|