@interopio/gateway-server 0.19.4 → 0.21.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/gateway-server ADDED
@@ -0,0 +1,572 @@
1
+ #!/usr/bin/env node
2
+
3
+ `use strict`;
4
+
5
+ import { writeFileSync } from 'node:fs';
6
+ import { parseArgs } from 'node:util';
7
+ import { configure } from '@interopio/gateway/logging/core';
8
+
9
+ // import { GatewayServer } from './src/index.ts';
10
+ // import { mkcert, argon2 } from './src/tools/index.ts';
11
+
12
+ import { GatewayServer } from './dist/index.js';
13
+ import { mkcert, argon2 } from './dist/tools/index.js';
14
+
15
+ function showHelp() {
16
+ console.log(`
17
+ Usage: npx @interopio/gateway-server <command> [options]
18
+
19
+ Commands:
20
+ run Start the 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
+ passwd options:
45
+ (no args) Prompt for password interactively (masked input)
46
+ --stdin Read password from stdin (for piping, e.g., echo "pass" | ...)
47
+
48
+ mkcert options:
49
+ --client Generate client certificate (default: server certificate)
50
+ -u, --user <name> Common Name for the certificate (default: dev-user for client certs)
51
+ (--client only)
52
+ --ca <file> File with CA certificate (default: ./gateway-ca.crt)
53
+ --ca-key <file> File with CA private key (default: ./gateway-ca.key)
54
+ --key <file> Output file for private key (default: ./gateway-server.key for server,
55
+ ./gateway-client.key for client)
56
+ --cert <file> Output file for certificate (default: ./gateway-server.crt for server,
57
+ ./gateway-client.crt for client)
58
+ [name...] DNS names, IP addresses, or email addresses for subjectAltName
59
+ (DNS by default, prefix with 'IP:' or 'EMAIL:' for other types)
60
+
61
+ Global Options:
62
+ -v, --version Show version information and exit
63
+ --help Show this help message and exit
64
+
65
+ Examples:
66
+ gateway-server run -p 3000
67
+ gateway-server run -u admin --port 8385,8388 --debug
68
+ gateway-server run -p 8443 --ssl --user admin --gateway
69
+ gateway-server run --port 42443 --tls --ca-key ./ssl/ca.key --host medes --gateway
70
+ gateway-server run -p 3000 --static ./public --config ./server-config.json
71
+ gateway-server run --config ./server-config.json --no-gateway --no-ssl --no-auth
72
+ gateway-server passwd
73
+ echo "mySecret123" | gateway-server passwd --stdin
74
+ gateway-server mkcert
75
+ gateway-server mkcert localhost 127.0.0.1 IP:192.168.1.100
76
+ gateway-server mkcert --client EMAIL:john.doe@example.com
77
+ gateway-server mkcert example.com *.example.com --key ./gateway-server.key --cert ./gateway-server.crt
78
+ `);
79
+ process.exit(0);
80
+ }
81
+
82
+ function showVersion() {
83
+ console.log(GatewayServer.VERSION);
84
+ process.exit(0);
85
+ }
86
+
87
+ // Parse command-line arguments
88
+ const { values: globalValues, positionals } = parseArgs({
89
+ args: process.argv.slice(2),
90
+ options: {
91
+ version: { type: 'boolean', short: 'v' },
92
+ help: { type: 'boolean', short: 'h' },
93
+ },
94
+ strict: false,
95
+ allowPositionals: true,
96
+ });
97
+
98
+ // Check for global flags first
99
+ if (globalValues.version) {
100
+ showVersion();
101
+ }
102
+ if (globalValues.help) {
103
+ showHelp();
104
+ }
105
+
106
+ // Determine command (required)
107
+ if (positionals.length === 0) {
108
+ console.error('Error: Command is required');
109
+ console.log('Use --help to see available commands');
110
+ process.exit(1);
111
+ }
112
+
113
+ const command = positionals[0];
114
+
115
+ // Route to appropriate command handler
116
+ switch (command) {
117
+ case 'run':
118
+ await runServer();
119
+ break;
120
+ case 'passwd':
121
+ await passwdCommand();
122
+ break;
123
+ case 'mkcert':
124
+ await mkcertCommand();
125
+ break;
126
+ default:
127
+ console.error(`Error: Unknown command "${command}"`);
128
+ console.log('Use --help to see available commands');
129
+ process.exit(1);
130
+ }
131
+
132
+ // Command: passwd - Generate password hash
133
+ async function passwdCommand() {
134
+ const { values } = parseArgs({
135
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'passwd'
136
+ options: {
137
+ stdin: { type: 'boolean' },
138
+ },
139
+ strict: true,
140
+ allowPositionals: false,
141
+ });
142
+
143
+ let password = undefined;
144
+
145
+ // Read password from stdin if specified
146
+ if (values.stdin) {
147
+ password = await readPasswordFromStdin();
148
+ }
149
+
150
+ // If no password provided, prompt interactively
151
+ if (!password) {
152
+ password = await promptPassword('Enter password: ');
153
+ }
154
+
155
+ if (!password) {
156
+ console.error('Error: password is required');
157
+ process.exit(1);
158
+ }
159
+
160
+ const hashValue = await argon2.hash(password);
161
+ const hash = `{argon2id}${hashValue}`;
162
+ console.log(hash);
163
+ process.exit(0);
164
+ }
165
+
166
+ // Read password from piped stdin (non-interactive)
167
+ async function readPasswordFromStdin() {
168
+ // Check if stdin is a TTY (interactive terminal)
169
+ if (process.stdin.isTTY) {
170
+ // Interactive mode - use readline with hidden input
171
+ return promptPassword('Password: ');
172
+ }
173
+
174
+ // Piped mode - read until EOF or newline
175
+ const chunks = [];
176
+ for await (const chunk of process.stdin) {
177
+ chunks.push(chunk);
178
+ }
179
+ const content = Buffer.concat(chunks).toString('utf8');
180
+ // Strip trailing newline
181
+ return content.replace(/\r?\n$/, '');
182
+ }
183
+
184
+ // Prompt for password with masked input
185
+ async function promptPassword(prompt) {
186
+ return new Promise((resolve) => {
187
+ process.stdout.write(prompt);
188
+
189
+ // Only set raw mode if stdin is a TTY
190
+ if (!process.stdin.isTTY) {
191
+ console.error('Error: Interactive password prompt requires a terminal');
192
+ process.exit(1);
193
+ }
194
+
195
+ process.stdin.setRawMode(true);
196
+ process.stdin.resume();
197
+ process.stdin.setEncoding('utf8');
198
+
199
+ let password = '';
200
+
201
+ const cleanup = () => {
202
+ process.stdin.setRawMode(false);
203
+ process.stdin.pause();
204
+ process.stdin.removeListener('data', onData);
205
+ process.stdout.write('\n');
206
+ };
207
+
208
+ const onData = (char) => {
209
+ // Ctrl+C
210
+ if (char === '\u0003') {
211
+ cleanup();
212
+ process.exit(1);
213
+ }
214
+
215
+ // Enter
216
+ if (char === '\r' || char === '\n') {
217
+ cleanup();
218
+ resolve(password);
219
+ return;
220
+ }
221
+
222
+ // Backspace
223
+ if (char === '\u007f' || char === '\b') {
224
+ if (password.length > 0) {
225
+ password = password.slice(0, -1);
226
+ process.stdout.write('\b \b');
227
+ }
228
+ return;
229
+ }
230
+
231
+ // Regular character
232
+ password += char;
233
+ process.stdout.write('*');
234
+ };
235
+
236
+ process.stdin.on('data', onData);
237
+ });
238
+ }
239
+
240
+ // Command: mkcert - Generate client or server certificate
241
+ async function mkcertCommand() {
242
+ const { values, positionals } = parseArgs({
243
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'mkcert'
244
+ options: {
245
+ client: { type: 'boolean' },
246
+ user: { type: 'string', short: 'u' },
247
+ ca: { type: 'string' },
248
+ 'ca-key': { type: 'string' },
249
+ key: { type: 'string' },
250
+ cert: { type: 'string' },
251
+ },
252
+ strict: true,
253
+ allowPositionals: true,
254
+ });
255
+
256
+ const isClientCert = values.client || false;
257
+ const user = values.user || 'dev-user';
258
+ const caCert = values.ca || './gateway-ca.crt';
259
+ const caKey = values['ca-key'] || './gateway-ca.key';
260
+ let keyPath = values.key;
261
+ let certPath = values.cert;
262
+ const sanEntries = positionals;
263
+
264
+ // Default output paths
265
+ // Users typically specify just --cert, and key should go in the same file
266
+ if (!keyPath && !certPath) {
267
+ // Neither specified: use defaults based on certificate type
268
+ if (isClientCert) {
269
+ // Client certificate: combined file by default
270
+ keyPath = './gateway-client.key';
271
+ certPath = './gateway-client.crt';
272
+ } else {
273
+ // Server certificate: separate files by default
274
+ keyPath = './gateway-server.key';
275
+ certPath = './gateway-server.crt';
276
+ }
277
+ } else if (keyPath && !certPath) {
278
+ // Only --key specified: cert goes in the same file (combined)
279
+ certPath = keyPath;
280
+ } else if (!keyPath && certPath) {
281
+ // Only --cert specified: key goes in the same file (combined)
282
+ keyPath = certPath;
283
+ }
284
+ // else: both specified, use as-is
285
+
286
+ if (sanEntries.length === 0) {
287
+ if (isClientCert) {
288
+ sanEntries.push(user);
289
+ }
290
+ else {
291
+ // Default SAN entry for server certs
292
+ sanEntries.push('localhost');
293
+ }
294
+ }
295
+
296
+ const { readFileSync } = await import('node:fs');
297
+ const { KEYUTIL, X509 } = await import('jsrsasign');
298
+
299
+ // Load CA key
300
+ let caKeyObj;
301
+ try {
302
+ const caKeyPem = readFileSync(caKey, 'utf8');
303
+ caKeyObj = KEYUTIL.getKey(caKeyPem);
304
+ } catch (error) {
305
+ console.error(`Error: Cannot read CA key from ${caKey}`);
306
+ console.error(error.message);
307
+ process.exit(1);
308
+ }
309
+
310
+ // Extract issuer from CA certificate
311
+ let issuer;
312
+ try {
313
+ const caCertPem = readFileSync(caCert, 'utf8');
314
+ const caCertObj = new X509();
315
+ caCertObj.readCertPEM(caCertPem);
316
+ issuer = caCertObj.getSubjectString();
317
+ } catch (error) {
318
+ console.error(`Error: Cannot read CA certificate from ${caCert}`);
319
+ console.error(error.message);
320
+ process.exit(1);
321
+ }
322
+
323
+ // Generate certificate using SAN entries
324
+ const cert = mkcert.generateCert(caKeyObj, issuer, sanEntries, isClientCert);
325
+
326
+ // Write files
327
+ try {
328
+ if (keyPath === certPath) {
329
+ // Concatenate cert and key into single file (cert first, then key)
330
+ const combined = cert.cert + cert.key;
331
+ writeFileSync(keyPath, combined, { mode: 0o600 });
332
+ console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`);
333
+ console.log(` Combined (cert+key): ${keyPath}`);
334
+ if (sanEntries.length > 0) {
335
+ console.log(` SAN: ${sanEntries.join(', ')}`);
336
+ }
337
+ } else {
338
+ // Write separate files
339
+ writeFileSync(keyPath, cert.key, { mode: 0o600 });
340
+ writeFileSync(certPath, cert.cert, { mode: 0o644 });
341
+ console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`);
342
+ console.log(` Private key: ${keyPath}`);
343
+ console.log(` Certificate: ${certPath}`);
344
+ if (sanEntries.length > 0) {
345
+ console.log(` SAN: ${sanEntries.join(', ')}`);
346
+ }
347
+ }
348
+
349
+ process.exit(0);
350
+ } catch (error) {
351
+ console.error(`Error: Cannot write certificate files`);
352
+ console.error(error.message);
353
+ process.exit(1);
354
+ }
355
+ }
356
+
357
+ // Command: run - Start the server
358
+ async function runServer() {
359
+ const { values } = parseArgs({
360
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'run'
361
+ options: {
362
+ port: { type: 'string', short: 'p' },
363
+ host: { type: 'string', short: 'H' },
364
+ user: { type: 'string', short: 'u' },
365
+ debug: { type: 'boolean' },
366
+ ssl: { type: 'boolean', short: 'S' },
367
+ tls: { type: 'boolean' },
368
+ cert: { type: 'string' },
369
+ key: { type: 'string' },
370
+ ca: { type: 'string' },
371
+ 'ca-key': { type: 'string' },
372
+ config: { type: 'string', short: 'c' },
373
+ gateway: { type: 'boolean', short: 'g' },
374
+ static: { type: 'string', short: 's', multiple: true },
375
+ auth: { type: 'boolean' }
376
+ },
377
+ strict: true,
378
+ allowNegative: true,
379
+ allowPositionals: false,
380
+ });
381
+
382
+ const options = {
383
+ port: values.port,
384
+ host: values.host,
385
+ user: values.user,
386
+ debug: values.debug || false,
387
+ ssl: values.ssl || values.tls || false,
388
+ noSsl: values.ssl === false, // --no-ssl explicitly set
389
+ cert: values.cert,
390
+ key: values.key,
391
+ ca: values.ca,
392
+ caKey: values['ca-key'],
393
+ config: values.config,
394
+ gateway: values.gateway,
395
+ noGateway: values.gateway === false, // --no-gateway explicitly set
396
+ static: values.static,
397
+ };
398
+
399
+ configure({
400
+ level: options.debug ? 'debug' : 'info',
401
+ });
402
+
403
+ // Load server configuration from file if specified
404
+ let serverConfig = {};
405
+ if (options.config) {
406
+ try {
407
+ const { readFileSync } = await import('node:fs');
408
+ const configContent = readFileSync(options.config, 'utf8');
409
+ serverConfig = JSON.parse(configContent);
410
+ } catch (error) {
411
+ console.error(`Error: Cannot read server config from ${options.config}`);
412
+ console.error(error.message);
413
+ process.exit(1);
414
+ }
415
+ }
416
+
417
+ // Apply CLI overrides to server config
418
+ // CLI arguments take precedence over config file
419
+
420
+ // Override port if specified
421
+ if (values.port !== undefined) {
422
+ serverConfig.port = options.port;
423
+ }
424
+
425
+ // Override host if specified
426
+ if (values.host !== undefined) {
427
+ serverConfig.host = options.host;
428
+ }
429
+
430
+ // Override SSL configuration
431
+ if (options.noSsl) {
432
+ // --no-ssl: delete SSL config completely
433
+ delete serverConfig.ssl;
434
+ options.ssl = false;
435
+ } else if (options.ssl) {
436
+ // --ssl specified: merge CLI args with existing config
437
+ if (!serverConfig.ssl) {
438
+ serverConfig.ssl = {};
439
+ }
440
+ // Only set properties if CLI options are explicitly provided
441
+ if (options.key !== undefined) {
442
+ serverConfig.ssl.key = options.key;
443
+ }
444
+ if (options.cert !== undefined) {
445
+ serverConfig.ssl.cert = options.cert;
446
+ }
447
+ if (options.ca !== undefined) {
448
+ serverConfig.ssl.ca = options.ca;
449
+ }
450
+ }
451
+ // else: use whatever is in serverConfig (if any)
452
+
453
+ // Determine effective SSL state
454
+ const effectiveSsl = options.ssl || (!options.noSsl && serverConfig.ssl !== undefined);
455
+
456
+ // Configure authentication
457
+ let auth;
458
+ if (values.auth === false) {
459
+ // --no-auth: delete auth config from server config and set to 'none'
460
+ delete serverConfig.auth;
461
+ auth = { type: 'none' };
462
+ }
463
+ else if (serverConfig.auth) {
464
+ // Use auth from config file if present
465
+ auth = serverConfig.auth;
466
+
467
+ // Allow --user CLI option to override auth.user.name from config
468
+ if (options.user) {
469
+ if (!auth.user) {
470
+ auth.user = {};
471
+ }
472
+ auth.user.name = options.user;
473
+ }
474
+ }
475
+ else {
476
+ // No auth in config file, determine from CLI args or defaults
477
+ auth = {
478
+ type: options.user ? 'basic' : effectiveSsl ? 'x509' : 'none'
479
+ };
480
+
481
+ if (options.user) {
482
+ auth.user = { name: options.user };
483
+ }
484
+
485
+ if (effectiveSsl && auth.type === 'x509') {
486
+ auth.x509 = {
487
+ key: options.caKey || './gateway-ca.key',
488
+ // principalAltName: 'email',
489
+ };
490
+
491
+ // Ensure SSL config exists and has proper mutual TLS settings
492
+ if (!serverConfig.ssl) {
493
+ serverConfig.ssl = {};
494
+ }
495
+
496
+ // Enable client certificate verification with proper mutual TLS
497
+ serverConfig.ssl.requestCert = true;
498
+
499
+ // Enforce trust verification to prevent certificate spoofing
500
+ // rejectUnauthorized must be true to ensure only trusted client certs are accepted
501
+ serverConfig.ssl.rejectUnauthorized = true;
502
+
503
+ // Set CA for client certificate verification (required for mutual TLS)
504
+ // Use the same CA that signs client certificates
505
+ if (!serverConfig.ssl.ca) {
506
+ serverConfig.ssl.ca = options.ca || './gateway-ca.crt';
507
+ }
508
+ }
509
+ }
510
+
511
+ // Handle SSL certificate auto-generation when --ssl is specified via CLI
512
+ // but config file doesn't have server certificates
513
+ if (options.ssl && effectiveSsl && serverConfig.ssl) {
514
+ const hasCert = serverConfig.ssl.cert || serverConfig.ssl.key;
515
+ if (!hasCert) {
516
+ // SSL enabled but no server cert/key provided
517
+ // We need a CA key for auto-generating server certificates
518
+ // Set default CA key path if not already configured
519
+ if (!auth.x509) {
520
+ auth.x509 = {};
521
+ }
522
+ if (!auth.x509.key) {
523
+ // Use CLI option, or default to ./gateway-ca.key
524
+ auth.x509.key = options.caKey || './gateway-ca.key';
525
+ }
526
+ }
527
+ }
528
+
529
+ // Override gateway configuration
530
+ if (options.noGateway) {
531
+ // --no-gateway: delete gateway config completely
532
+ delete serverConfig.gateway;
533
+ } else if (options.gateway) {
534
+ // --gateway specified: enable gateway endpoint, respect existing config
535
+ if (!serverConfig.gateway) {
536
+ serverConfig.gateway = {};
537
+ }
538
+ // Set defaults only if not already present in config
539
+ if (!serverConfig.gateway.route) {
540
+ serverConfig.gateway.route = '/';
541
+ }
542
+ if (!serverConfig.gateway.authorize) {
543
+ serverConfig.gateway.authorize = { access: auth.type !== 'none' ? 'authenticated' : 'permitted' };
544
+ }
545
+ }
546
+ // else: use whatever is in serverConfig (if any)
547
+
548
+ // Override static resources if specified
549
+ if (options.static && options.static.length > 0) {
550
+ if (!serverConfig.resources) {
551
+ serverConfig.resources = {};
552
+ }
553
+ serverConfig.resources.locations = options.static;
554
+ }
555
+
556
+ // Set auth in server config
557
+ serverConfig.auth = auth;
558
+
559
+ const server = await GatewayServer.Factory({
560
+ ...(serverConfig),
561
+ app: async (_configurer) => {
562
+ // No custom app logic for now
563
+ },
564
+ });
565
+
566
+ process.on('SIGINT', async () => {
567
+ await server.close();
568
+ process.exit(0);
569
+ });
570
+
571
+ // process.title = `${GatewayServer.VERSION} ${server.address.port}`;
572
+ }