@lopatnov/express-reverse-proxy 2.0.0 → 4.0.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/README.md CHANGED
@@ -20,11 +20,19 @@
20
20
  - [CLI Options](#cli-options)
21
21
  - [Configuration](#configuration)
22
22
  - [port](#port)
23
+ - [logging](#logging)
24
+ - [hotReload](#hotreload)
25
+ - [compression](#compression)
26
+ - [helmet](#helmet)
27
+ - [cors](#cors)
28
+ - [favicon](#favicon)
29
+ - [responseTime](#responsetime)
23
30
  - [headers](#headers)
24
31
  - [folders](#folders)
25
32
  - [proxy](#proxy)
26
33
  - [unhandled](#unhandled)
27
34
  - [host](#host)
35
+ - [ssl](#ssl)
28
36
  - [Configuration Recipes](#configuration-recipes)
29
37
  - [Docker & PM2](#docker--pm2)
30
38
  - [Testing](#testing)
@@ -154,6 +162,15 @@ Static files always take priority over proxy rules. Proxies are checked only whe
154
162
 
155
163
  ## CLI Options
156
164
 
165
+ The package installs two equivalent commands — use whichever you prefer:
166
+
167
+ ```shell
168
+ express-reverse-proxy [options]
169
+ lerp [options]
170
+ ```
171
+
172
+ `lerp` is a short alias for **L**opatnov **E**xpress **R**everse **P**roxy.
173
+
157
174
  | Option | Description |
158
175
  | ------------------------- | ----------------------------------------------------------------------------------------------- |
159
176
  | `--help` | Print help and exit |
@@ -218,6 +235,55 @@ The port the server listens on. Defaults to `8000`. Can also be set via the `POR
218
235
  }
219
236
  ```
220
237
 
238
+ ### logging
239
+
240
+ Controls HTTP request logging (Morgan). Enabled by default. Set to `false` to silence per-request log lines — useful in production behind another proxy, or to keep console output clean.
241
+
242
+ ```json
243
+ {
244
+ "port": 8080,
245
+ "logging": false,
246
+ "folders": "www"
247
+ }
248
+ ```
249
+
250
+ ### hotReload
251
+
252
+ Watches the `folders` directories for file changes and automatically reloads connected browser tabs. Uses Server-Sent Events (SSE). Intended for local development only.
253
+
254
+ ```json
255
+ {
256
+ "port": 8080,
257
+ "hotReload": true,
258
+ "folders": "www"
259
+ }
260
+ ```
261
+
262
+ The server exposes two endpoints when hot reload is enabled:
263
+
264
+ | Endpoint | Description |
265
+ | --------------------------------- | ------------------------------------------ |
266
+ | `GET /__hot-reload__` | SSE stream — browsers subscribe here |
267
+ | `GET /__hot-reload__/client.js` | Ready-to-use client script |
268
+
269
+ #### Connecting the client
270
+
271
+ **Option A — plain HTML project**: add a script tag to your page. The file is served directly by the dev server, no installation needed:
272
+
273
+ ```html
274
+ <script src="/__hot-reload__/client.js"></script>
275
+ ```
276
+
277
+ **Option B — bundled project** (Vite, webpack, etc.): import the client module. The bundler resolves it through the package `exports` field:
278
+
279
+ ```js
280
+ import '@lopatnov/express-reverse-proxy/hot-reload-client';
281
+ ```
282
+
283
+ Both options connect to `/__hot-reload__` and call `location.reload()` when a file change is detected. The connection is re-established automatically after 3 seconds if the server restarts.
284
+
285
+ > **PM2 note:** hot reload works best with a single process (`node server.js`). If using PM2, set `instances: 1` in your ecosystem config — each worker maintains its own file watcher and SSE client list independently.
286
+
221
287
  ### headers
222
288
 
223
289
  Add headers to every response — useful for CORS in development.
@@ -385,6 +451,136 @@ To use multi-site mode, make the config file an **array** instead of an object.
385
451
  >
386
452
  > Two entries with the same `host` **and** `port` cause a startup error. The same `host` on different ports is allowed.
387
453
 
454
+ ### ssl
455
+
456
+ Enable HTTPS on a port by adding an `ssl` object to any site config for that port. All sites sharing the same port use the same certificate.
457
+
458
+ | Field | Type | Description |
459
+ | ------ | -------- | -------------------------------------------------------- |
460
+ | `key` | `string` | Path to the private key file (PEM format) |
461
+ | `cert` | `string` | Path to the certificate file (PEM format) |
462
+ | `ca` | `string` | *(optional)* Path to the CA bundle for client validation |
463
+
464
+ Paths are resolved **relative to the config file**, not the current working directory.
465
+
466
+ ```json
467
+ {
468
+ "port": 443,
469
+ "ssl": {
470
+ "key": "./certs/key.pem",
471
+ "cert": "./certs/cert.pem"
472
+ },
473
+ "folders": "./public",
474
+ "proxy": {
475
+ "/api": "http://localhost:4000"
476
+ }
477
+ }
478
+ ```
479
+
480
+ > All site configs on the same port must either all have `ssl` or none — mixing is a startup error.
481
+
482
+ ### compression
483
+
484
+ Enable gzip/deflate response compression. Reduces the size of HTML, CSS, JS, and JSON responses sent to the browser. Set to `true` for defaults, or pass an options object.
485
+
486
+ ```json
487
+ {
488
+ "port": 8080,
489
+ "compression": true,
490
+ "folders": "www"
491
+ }
492
+ ```
493
+
494
+ With custom options (see [compression docs](https://github.com/expressjs/compression#options)):
495
+
496
+ ```json
497
+ {
498
+ "compression": { "level": 6, "threshold": 1024 }
499
+ }
500
+ ```
501
+
502
+ > Compression is applied per-site. Assets that are already compressed (images, fonts, video) are not affected — the browser signals it accepts compressed responses via the `Accept-Encoding` header.
503
+
504
+ ### helmet
505
+
506
+ Set security-related HTTP response headers. Protects against common web vulnerabilities by configuring headers such as `Content-Security-Policy`, `X-Frame-Options`, `Strict-Transport-Security`, and others.
507
+
508
+ ```json
509
+ {
510
+ "port": 8080,
511
+ "helmet": true,
512
+ "folders": "www"
513
+ }
514
+ ```
515
+
516
+ Disable a specific header (see [helmet docs](https://helmetjs.github.io/) for all options):
517
+
518
+ ```json
519
+ {
520
+ "helmet": { "contentSecurityPolicy": false }
521
+ }
522
+ ```
523
+
524
+ > When `helmet: true` is set, the default helmet configuration is applied. This may block inline scripts and cross-origin resources. Adjust `contentSecurityPolicy` or other options as needed for your project.
525
+
526
+ ### cors
527
+
528
+ Enable CORS (Cross-Origin Resource Sharing) headers and handle preflight `OPTIONS` requests automatically. Useful when your front-end on one origin calls an API on a different origin.
529
+
530
+ ```json
531
+ {
532
+ "port": 8080,
533
+ "cors": true,
534
+ "proxy": { "/api": "http://localhost:4000" }
535
+ }
536
+ ```
537
+
538
+ Restrict to a specific origin (see [cors docs](https://github.com/expressjs/cors#configuration-options)):
539
+
540
+ ```json
541
+ {
542
+ "cors": { "origin": "https://app.example.com" }
543
+ }
544
+ ```
545
+
546
+ > The `cors` middleware handles `OPTIONS` preflight requests that the `headers` option cannot respond to. Use `cors` when you need to allow requests from JavaScript on a different domain — for example a React app calling this proxy's API routes.
547
+
548
+ ### favicon
549
+
550
+ Serve a favicon file efficiently. The file is read into memory at startup and served from there on every `/favicon.ico` request — before static folder scanning or proxy rules run.
551
+
552
+ ```json
553
+ {
554
+ "port": 8080,
555
+ "favicon": "./public/favicon.ico",
556
+ "folders": "www"
557
+ }
558
+ ```
559
+
560
+ The path is resolved **relative to the config file**, consistent with the `ssl` option. Absolute paths are also accepted.
561
+
562
+ > If your favicon already lives inside a directory listed in `folders`, this option is not needed — `express.static` will serve it automatically.
563
+
564
+ ### responseTime
565
+
566
+ Add an `X-Response-Time` header to every response, recording how long the server took to handle the request. Useful for performance monitoring and debugging.
567
+
568
+ ```json
569
+ {
570
+ "port": 8080,
571
+ "responseTime": true,
572
+ "folders": "www"
573
+ }
574
+ ```
575
+
576
+ With custom precision (see [response-time docs](https://github.com/expressjs/response-time#options)):
577
+
578
+ ```json
579
+ {
580
+ "responseTime": { "digits": 0, "suffix": false }
581
+ }
582
+ ```
583
+
388
584
  ---
389
585
 
390
586
  ## Configuration Recipes
@@ -422,6 +618,57 @@ Only `/api/*` requests go to the back-end; everything else stays local.
422
618
  - `GET /api/users` → proxied to `http://localhost:4000/users`
423
619
  - `GET /missing` → 404 Not Found
424
620
 
621
+ ### HTTPS with a self-signed certificate (local dev)
622
+
623
+ ```shell
624
+ mkdir certs
625
+ openssl req -x509 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem \
626
+ -days 365 -nodes -subj "/CN=localhost"
627
+ ```
628
+
629
+ ```json
630
+ {
631
+ "port": 8443,
632
+ "ssl": {
633
+ "key": "./certs/key.pem",
634
+ "cert": "./certs/cert.pem"
635
+ },
636
+ "folders": "www",
637
+ "proxy": {
638
+ "/api": "http://localhost:4000"
639
+ }
640
+ }
641
+ ```
642
+
643
+ Start and open in browser (accept the self-signed cert warning):
644
+
645
+ ```shell
646
+ node server.js --config server-config.json
647
+ # [listen] https://localhost:8443
648
+ ```
649
+
650
+ ---
651
+
652
+ ### Production hardening (helmet + cors + compression)
653
+
654
+ Enable security headers, CORS, and response compression in one config:
655
+
656
+ ```json
657
+ {
658
+ "port": 8080,
659
+ "compression": true,
660
+ "helmet": true,
661
+ "cors": { "origin": "https://app.example.com" },
662
+ "responseTime": true,
663
+ "folders": "www",
664
+ "proxy": {
665
+ "/api": "http://localhost:4000"
666
+ }
667
+ }
668
+ ```
669
+
670
+ ---
671
+
425
672
  ### CORS headers + rich error responses
426
673
 
427
674
  ```json
@@ -519,6 +766,56 @@ express-reverse-proxy --cluster status
519
766
  express-reverse-proxy --cluster stop
520
767
  ```
521
768
 
769
+ ### Behind a reverse proxy
770
+
771
+ For production deployments it is common to place a dedicated reverse proxy in front of `express-reverse-proxy` to handle TLS termination, HTTP/2, gzip compression, and rate limiting. In this setup the Node.js server listens on a local port over plain HTTP, while the outer proxy terminates HTTPS connections from the internet:
772
+
773
+ ```
774
+ Internet (HTTPS / HTTP/2)
775
+
776
+ Nginx or Caddy — TLS, HTTP/2, gzip, rate limiting
777
+ ↓ HTTP/1.1 (localhost)
778
+ express-reverse-proxy — PM2 cluster, routing, static files, API proxy
779
+
780
+ Backend API servers
781
+ ```
782
+
783
+ **No `ssl` config needed** in `server-config.json` when the outer proxy handles TLS.
784
+
785
+ #### Nginx
786
+
787
+ ```nginx
788
+ server {
789
+ listen 443 ssl;
790
+ server_name example.com;
791
+
792
+ ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
793
+ ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
794
+
795
+ location / {
796
+ proxy_pass http://127.0.0.1:8080;
797
+ proxy_set_header Host $host;
798
+ proxy_set_header X-Real-IP $remote_addr;
799
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
800
+ proxy_set_header X-Forwarded-Proto $scheme;
801
+ }
802
+ }
803
+ ```
804
+
805
+ Free certificates can be obtained with [Certbot](https://certbot.eff.org/): `certbot --nginx -d example.com`.
806
+
807
+ #### Caddy
808
+
809
+ [Caddy](https://caddyserver.com/) provisions and renews Let's Encrypt certificates automatically — no extra tooling needed:
810
+
811
+ ```
812
+ example.com {
813
+ reverse_proxy 127.0.0.1:8080
814
+ }
815
+ ```
816
+
817
+ Start with `caddy run --config Caddyfile`.
818
+
522
819
  ---
523
820
 
524
821
  ## Testing
@@ -649,6 +946,11 @@ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before
649
946
  - [Express](https://expressjs.com/) — HTTP server framework
650
947
  - [express-http-proxy](https://github.com/villadora/express-http-proxy) — reverse proxy middleware
651
948
  - [Morgan](https://github.com/expressjs/morgan) — HTTP request logger
949
+ - [compression](https://github.com/expressjs/compression) — gzip/deflate response compression
950
+ - [helmet](https://helmetjs.github.io/) — security HTTP headers
951
+ - [cors](https://github.com/expressjs/cors) — CORS headers and preflight handling
952
+ - [serve-favicon](https://github.com/expressjs/serve-favicon) — efficient favicon serving
953
+ - [response-time](https://github.com/expressjs/response-time) — X-Response-Time header
652
954
  - [PM2](https://pm2.keymetrics.io/) — production process manager with clustering
653
955
  - [Biome](https://biomejs.dev/) — fast linter and formatter (Rust-based)
654
956
  - [Cypress](https://www.cypress.io/) — E2E testing framework
@@ -1,24 +1,24 @@
1
- const path = require('node:path');
2
-
3
- module.exports = {
4
- apps: [
5
- {
6
- name: 'express-reverse-proxy',
7
- script: path.join(__dirname, 'server.js'),
8
- instances: 'max',
9
- exec_mode: 'cluster',
10
- wait_ready: true,
11
- listen_timeout: 30000,
12
- kill_timeout: 5000,
13
- shutdown_with_message: true,
14
- vizion: false, // disable git metadata collection
15
- pmx: false, // disable PM2 metrics (avoids wmic ENOENT on Windows 11)
16
- env: {
17
- NODE_ENV: 'production',
18
- },
19
- env_development: {
20
- NODE_ENV: 'development',
21
- },
22
- },
23
- ],
24
- };
1
+ const path = require('node:path');
2
+
3
+ module.exports = {
4
+ apps: [
5
+ {
6
+ name: 'express-reverse-proxy',
7
+ script: path.join(__dirname, 'server.js'),
8
+ instances: 'max',
9
+ exec_mode: 'cluster',
10
+ wait_ready: true,
11
+ listen_timeout: 30000,
12
+ kill_timeout: 5000,
13
+ shutdown_with_message: true,
14
+ vizion: false, // disable git metadata collection
15
+ pmx: false, // disable PM2 metrics (avoids wmic ENOENT on Windows 11)
16
+ env: {
17
+ NODE_ENV: 'production',
18
+ },
19
+ env_development: {
20
+ NODE_ENV: 'development',
21
+ },
22
+ },
23
+ ],
24
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Hot Reload Client — connect via SSE and reload the page when files change.
3
+ *
4
+ * Usage (choose one):
5
+ *
6
+ * <!-- as a plain script tag -->
7
+ * <script src="/__hot-reload__/client.js"></script>
8
+ *
9
+ * <!-- as an ES module (bundler resolves via package.json exports) -->
10
+ * import '@lopatnov/express-reverse-proxy/hot-reload-client';
11
+ *
12
+ * The server must have `"hotReload": true` in its server-config.json.
13
+ */
14
+ (function hotReloadClient() {
15
+ var url = '/__hot-reload__';
16
+
17
+ function connect() {
18
+ var es = new EventSource(url);
19
+
20
+ es.onmessage = () => {
21
+ location.reload();
22
+ };
23
+
24
+ es.onerror = () => {
25
+ es.close();
26
+ setTimeout(connect, 3000);
27
+ };
28
+ }
29
+
30
+ connect();
31
+ })();
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@lopatnov/express-reverse-proxy",
3
3
  "email": "oleksandr@lopatnov.cv.ua",
4
- "version": "2.0.0",
4
+ "version": "4.0.0",
5
5
  "description": "Node.js CLI tool to serve static front-end files with a reverse proxy for back-end APIs",
6
6
  "type": "module",
7
7
  "author": "lopatnov",
8
8
  "main": "server.js",
9
9
  "exports": {
10
- ".": "./server.js"
10
+ ".": "./server.js",
11
+ "./hot-reload-client": "./hot-reload-client.js"
11
12
  },
12
13
  "bin": {
13
- "express-reverse-proxy": "./server.js"
14
+ "express-reverse-proxy": "./server.js",
15
+ "lerp": "./server.js"
14
16
  },
15
17
  "engines": {
16
18
  "node": ">=18.0.0"
@@ -57,6 +59,7 @@
57
59
  "homepage": "https://lopatnov.github.io/express-reverse-proxy/",
58
60
  "files": [
59
61
  "server.js",
62
+ "hot-reload-client.js",
60
63
  "ecosystem.config.cjs",
61
64
  "README.md",
62
65
  "LICENSE"
@@ -65,10 +68,15 @@
65
68
  "access": "public"
66
69
  },
67
70
  "dependencies": {
71
+ "compression": "^1.7.5",
72
+ "cors": "^2.8.5",
68
73
  "express": "^5.2.1",
69
74
  "express-http-proxy": "^2.1.2",
75
+ "helmet": "^8.0.0",
70
76
  "morgan": "^1.10.1",
71
- "pm2": "^6.0.14"
77
+ "pm2": "^6.0.14",
78
+ "response-time": "^2.3.2",
79
+ "serve-favicon": "^2.5.0"
72
80
  },
73
81
  "devDependencies": {
74
82
  "@biomejs/biome": "^2.4.2",
package/server.js CHANGED
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
+ import https from 'node:https';
4
5
  import path from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
7
+ import compression from 'compression';
8
+ import cors from 'cors';
6
9
  import express from 'express';
7
10
  import proxy from 'express-http-proxy';
11
+ import helmet from 'helmet';
8
12
  import morgan from 'morgan';
13
+ import responseTime from 'response-time';
14
+ import favicon from 'serve-favicon';
9
15
 
10
16
  const __filename = fileURLToPath(import.meta.url);
11
17
  const __dirname = path.dirname(__filename);
@@ -151,6 +157,7 @@ if (serverArgs['--config']) {
151
157
  configFile = path.join(configFile, './server-config.json');
152
158
  }
153
159
  }
160
+ const configDir = path.dirname(path.resolve(configFile));
154
161
 
155
162
  const DEFAULT_CONFIG = { port: 8000, folders: '.' };
156
163
 
@@ -194,6 +201,21 @@ configs.forEach((c) => {
194
201
  configsByPort.get(p).push(c);
195
202
  });
196
203
 
204
+ // Validate: cannot mix SSL and non-SSL site configs on the same port
205
+ for (const [p, group] of configsByPort) {
206
+ const sslCount = group.filter((c) => c.ssl).length;
207
+ if (sslCount > 0 && sslCount < group.length) {
208
+ exitError(`Port ${p}: cannot mix SSL and non-SSL site configs on the same port.`, 1);
209
+ }
210
+ }
211
+
212
+ function collectFolderPaths(folders) {
213
+ if (typeof folders === 'string') return [folders];
214
+ if (Array.isArray(folders)) return folders.flatMap(collectFolderPaths);
215
+ if (folders instanceof Object) return Object.values(folders).flatMap(collectFolderPaths);
216
+ return [];
217
+ }
218
+
197
219
  function addStaticFolderByName(router, port, urlPath, folder) {
198
220
  let folderPath = folder;
199
221
  if (!path.isAbsolute(folder)) {
@@ -290,7 +312,47 @@ const servers = [];
290
312
 
291
313
  configsByPort.forEach((portConfigs, p) => {
292
314
  const app = express();
293
- app.use(morgan('combined'));
315
+ const loggingEnabled = portConfigs.every((c) => c.logging !== false);
316
+ if (loggingEnabled) {
317
+ app.use(morgan('combined'));
318
+ }
319
+
320
+ // Hot reload via SSE
321
+ const hotReloadEnabled = portConfigs.some((c) => c.hotReload === true);
322
+ if (hotReloadEnabled) {
323
+ const sseClients = new Set();
324
+ let reloadTimer = null;
325
+
326
+ app.get('/__hot-reload__', (req, res) => {
327
+ res.setHeader('Content-Type', 'text/event-stream');
328
+ res.setHeader('Cache-Control', 'no-cache');
329
+ res.setHeader('Connection', 'keep-alive');
330
+ res.flushHeaders();
331
+ sseClients.add(res);
332
+ req.on('close', () => sseClients.delete(res));
333
+ });
334
+
335
+ app.get('/__hot-reload__/client.js', (_req, res) => {
336
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
337
+ res.sendFile(path.join(__dirname, 'hot-reload-client.js'));
338
+ });
339
+
340
+ const watchPaths = [
341
+ ...new Set(portConfigs.flatMap((c) => collectFolderPaths(c.folders || []))),
342
+ ];
343
+ for (const folder of watchPaths) {
344
+ const absPath = path.isAbsolute(folder) ? folder : path.join(process.cwd(), folder);
345
+ if (fs.existsSync(absPath)) {
346
+ fs.watch(absPath, { recursive: true }, () => {
347
+ clearTimeout(reloadTimer);
348
+ reloadTimer = setTimeout(() => {
349
+ for (const client of sseClients) client.write('data: reload\n\n');
350
+ }, 100);
351
+ });
352
+ }
353
+ }
354
+ console.log(`[hot-reload] watching ${watchPaths.length} folder(s) on port ${p}`);
355
+ }
294
356
 
295
357
  // Specific hosts first, catch-all last
296
358
  const sorted = [
@@ -304,6 +366,30 @@ configsByPort.forEach((portConfigs, p) => {
304
366
 
305
367
  console.log(`[host] ${siteHost} → :${p}`);
306
368
 
369
+ if (siteConfig.responseTime) {
370
+ const opts = typeof siteConfig.responseTime === 'object' ? siteConfig.responseTime : {};
371
+ router.use(responseTime(opts));
372
+ }
373
+
374
+ if (siteConfig.cors) {
375
+ const opts = typeof siteConfig.cors === 'object' ? siteConfig.cors : {};
376
+ router.use(cors(opts));
377
+ }
378
+
379
+ if (siteConfig.compression) {
380
+ const opts = typeof siteConfig.compression === 'object' ? siteConfig.compression : {};
381
+ router.use(compression(opts));
382
+ }
383
+
384
+ if (siteConfig.helmet) {
385
+ const opts = typeof siteConfig.helmet === 'object' ? siteConfig.helmet : {};
386
+ router.use(helmet(opts));
387
+ }
388
+
389
+ if (siteConfig.favicon) {
390
+ router.use(favicon(path.resolve(configDir, siteConfig.favicon)));
391
+ }
392
+
307
393
  if (siteConfig.headers) {
308
394
  router.use((_req, res, next) => {
309
395
  for (const h of Object.keys(siteConfig.headers)) res.setHeader(h, siteConfig.headers[h]);
@@ -341,10 +427,31 @@ configsByPort.forEach((portConfigs, p) => {
341
427
  }
342
428
  });
343
429
 
344
- const server = app.listen(p, () => {
345
- console.log(`[listen] http://localhost:${p}`);
346
- if (process.send) process.send('ready');
347
- });
430
+ const sslConfig = portConfigs.find((c) => c.ssl)?.ssl;
431
+ let server;
432
+ if (sslConfig) {
433
+ let sslOptions;
434
+ try {
435
+ sslOptions = {
436
+ key: fs.readFileSync(path.resolve(configDir, sslConfig.key)),
437
+ cert: fs.readFileSync(path.resolve(configDir, sslConfig.cert)),
438
+ };
439
+ if (sslConfig.ca) {
440
+ sslOptions.ca = fs.readFileSync(path.resolve(configDir, sslConfig.ca));
441
+ }
442
+ } catch (err) {
443
+ exitError(`SSL cert/key error on port ${p}: ${err.message}`, 1);
444
+ }
445
+ server = https.createServer(sslOptions, app).listen(p, () => {
446
+ console.log(`[listen] https://localhost:${p}`);
447
+ if (process.send) process.send('ready');
448
+ });
449
+ } else {
450
+ server = app.listen(p, () => {
451
+ console.log(`[listen] http://localhost:${p}`);
452
+ if (process.send) process.send('ready');
453
+ });
454
+ }
348
455
  server.on('error', (err) => {
349
456
  if (err.code === 'EADDRINUSE') {
350
457
  exitError(`Port ${p} is already in use`, 1);