roster-server 2.0.2 → 2.0.4

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.
Files changed (3) hide show
  1. package/README.md +112 -0
  2. package/index.js +107 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -176,6 +176,118 @@ When creating a new `RosterServer` instance, you can pass the following options:
176
176
  - `local` (boolean): Set to `true` to run in local development mode.
177
177
  - `minLocalPort` (number): Minimum port for local mode (default: 4000).
178
178
  - `maxLocalPort` (number): Maximum port for local mode (default: 9999).
179
+ - `tlsMode` (string): TLS backend to use — `'auto'` (default), `'greenlock'`, or `'static'`. See [TLS Configuration](#-tls-configuration) below.
180
+ - `tlsDomain` (string): Domain whose cert files are pre-loaded as the server default in static mode (optional).
181
+ - `tls` (object): Additional TLS options passed to `https.createServer` (e.g. `minVersion`, `maxVersion`, `ciphers`).
182
+
183
+ ## 🔒 TLS Configuration
184
+
185
+ RosterServer supports three TLS backends, selectable with the `tlsMode` option.
186
+
187
+ ### Behavior matrix
188
+
189
+ | `tlsMode` | Runtime | HTTPS server |
190
+ |-----------|---------|-------------|
191
+ | `'auto'` (default) | Node.js | Greenlock SNI — certs managed and auto-renewed automatically |
192
+ | `'auto'` (default) | Bun | Static file certs from `greenlockStorePath/live/<domain>/` |
193
+ | `'greenlock'` | any | Always Greenlock SNI |
194
+ | `'static'` | any | Always static file certs |
195
+
196
+ Bun's TLS stack does not support Greenlock's async SNICallback, causing a `tlsv1 alert protocol version` error. `'auto'` mode detects the runtime and picks the correct backend transparently.
197
+
198
+ ### Static mode cert layout
199
+
200
+ In `'auto'` (Bun) or `'static'` mode, certs are read per-domain from:
201
+
202
+ ```
203
+ greenlockStorePath/
204
+ live/
205
+ example.com/
206
+ privkey.pem
207
+ cert.pem
208
+ chain.pem
209
+ api.example.com/
210
+ privkey.pem
211
+ cert.pem
212
+ chain.pem
213
+ ```
214
+
215
+ Greenlock populates this layout automatically when it renews certificates, so no extra tooling is required.
216
+
217
+ ### Default TLS options
218
+
219
+ The static-cert path enforces secure defaults that can be overridden with the `tls` option:
220
+
221
+ ```javascript
222
+ { minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3' }
223
+ ```
224
+
225
+ ### Examples
226
+
227
+ **Default — works on both Node and Bun without changes:**
228
+
229
+ ```javascript
230
+ const roster = new Roster({
231
+ email: 'admin@example.com',
232
+ wwwPath: '/srv/www'
233
+ // tlsMode defaults to 'auto'
234
+ });
235
+ roster.start();
236
+ ```
237
+
238
+ **Force Greenlock on all runtimes:**
239
+
240
+ ```javascript
241
+ const roster = new Roster({
242
+ email: 'admin@example.com',
243
+ wwwPath: '/srv/www',
244
+ tlsMode: 'greenlock'
245
+ });
246
+ roster.start();
247
+ ```
248
+
249
+ **Force static certs with a pre-loaded default cert (avoids SNI-less connection failures):**
250
+
251
+ ```javascript
252
+ const roster = new Roster({
253
+ email: 'admin@example.com',
254
+ wwwPath: '/srv/www',
255
+ tlsMode: 'static',
256
+ tlsDomain: 'example.com' // loaded as the server's fallback cert
257
+ });
258
+ roster.start();
259
+ ```
260
+
261
+ **Custom TLS options (e.g. restrict ciphers):**
262
+
263
+ ```javascript
264
+ const roster = new Roster({
265
+ email: 'admin@example.com',
266
+ wwwPath: '/srv/www',
267
+ tls: {
268
+ minVersion: 'TLSv1.2',
269
+ ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'
270
+ }
271
+ });
272
+ roster.start();
273
+ ```
274
+
275
+ ### Smoke test
276
+
277
+ After deploying, verify TLS is working:
278
+
279
+ ```bash
280
+ curl -v https://example.com
281
+ # Should show TLSv1.2 or TLSv1.3 in the handshake — no "alert protocol version" errors
282
+ ```
283
+
284
+ Server logs will display the active mode on startup:
285
+
286
+ ```
287
+ Runtime: bun | TLS mode: static
288
+ HTTPS port 443: using static certs from /srv/greenlock.d/live [static]
289
+ HTTPS server listening on port 443
290
+ ```
179
291
 
180
292
  ## 🏠 Local Development Mode
181
293
 
package/index.js CHANGED
@@ -172,6 +172,78 @@ class Roster {
172
172
  throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
173
173
  }
174
174
  this.defaultPort = port;
175
+
176
+ const validTlsModes = ['auto', 'greenlock', 'static'];
177
+ this.tlsMode = options.tlsMode || 'auto';
178
+ if (!validTlsModes.includes(this.tlsMode)) {
179
+ throw new Error(`Invalid tlsMode "${this.tlsMode}". Must be one of: ${validTlsModes.join(', ')}`);
180
+ }
181
+ this.tlsDomain = options.tlsDomain || null;
182
+ this.tlsOptions = options.tls || {};
183
+ }
184
+
185
+ detectRuntime() {
186
+ return typeof Bun !== 'undefined' ? 'bun' : 'node';
187
+ }
188
+
189
+ getEffectiveTlsMode() {
190
+ if (this.tlsMode !== 'auto') {
191
+ return this.tlsMode;
192
+ }
193
+ return this.detectRuntime() === 'bun' ? 'static' : 'greenlock';
194
+ }
195
+
196
+ getDefaultTlsOptions() {
197
+ return Object.assign({ minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3' }, this.tlsOptions);
198
+ }
199
+
200
+ createSNICallback() {
201
+ return (domain, callback) => {
202
+ try {
203
+ const certPath = path.join(this.greenlockStorePath, 'live', domain);
204
+ const keyPath = path.join(certPath, 'privkey.pem');
205
+ const certFilePath = path.join(certPath, 'cert.pem');
206
+ const chainPath = path.join(certPath, 'chain.pem');
207
+
208
+ if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
209
+ const key = fs.readFileSync(keyPath, 'utf8');
210
+ const cert = fs.readFileSync(certFilePath, 'utf8');
211
+ const chain = fs.readFileSync(chainPath, 'utf8');
212
+
213
+ callback(null, tls.createSecureContext({ key, cert: cert + chain }));
214
+ } else {
215
+ callback(new Error(`No certificate files available for ${domain} at ${certPath}`));
216
+ }
217
+ } catch (error) {
218
+ callback(error);
219
+ }
220
+ };
221
+ }
222
+
223
+ createStaticHttpsServer(dispatcher) {
224
+ const tlsOpts = Object.assign(this.getDefaultTlsOptions(), {
225
+ SNICallback: this.createSNICallback()
226
+ });
227
+
228
+ if (this.tlsDomain) {
229
+ const certPath = path.join(this.greenlockStorePath, 'live', this.tlsDomain);
230
+ const keyPath = path.join(certPath, 'privkey.pem');
231
+ const certFilePath = path.join(certPath, 'cert.pem');
232
+ const chainPath = path.join(certPath, 'chain.pem');
233
+
234
+ const missing = [keyPath, certFilePath, chainPath].filter(p => !fs.existsSync(p));
235
+ if (missing.length > 0) {
236
+ throw new Error(
237
+ `Static TLS cert files missing for domain "${this.tlsDomain}":\n` +
238
+ missing.map(p => ` - ${p}`).join('\n')
239
+ );
240
+ }
241
+
242
+ tlsOpts.key = fs.readFileSync(keyPath, 'utf8');
243
+ tlsOpts.cert = fs.readFileSync(certFilePath, 'utf8') + fs.readFileSync(chainPath, 'utf8');
244
+ }
245
+
246
+ return https.createServer(tlsOpts, dispatcher);
175
247
  }
176
248
 
177
249
  async loadSites() {
@@ -579,6 +651,10 @@ class Roster {
579
651
  };
580
652
  };
581
653
 
654
+ const runtime = this.detectRuntime();
655
+ const effectiveTlsMode = this.getEffectiveTlsMode();
656
+ log.info(`Runtime: ${runtime} | TLS mode: ${effectiveTlsMode}`);
657
+
582
658
  httpServer.listen(80, this.hostname, () => {
583
659
  log.info('HTTP server listening on port 80');
584
660
  });
@@ -595,84 +671,53 @@ class Roster {
595
671
  if (virtualServer) {
596
672
  virtualServer.processUpgrade(req, socket, head);
597
673
  } else {
598
- // No virtual server found, destroy the socket
599
674
  socket.destroy();
600
675
  }
601
676
  };
602
677
  };
603
678
 
679
+ const attachHttpsListeners = (httpsServer, portNum, upgradeHandler) => {
680
+ httpsServer.on('upgrade', upgradeHandler);
681
+ httpsServer.on('error', (error) => {
682
+ log.error(`HTTPS server error on port ${portNum}:`, error.message);
683
+ });
684
+ httpsServer.on('tlsClientError', (error) => {
685
+ if (!error.message.includes('http request')) {
686
+ log.error(`TLS error on port ${portNum}:`, error.message);
687
+ }
688
+ });
689
+ };
690
+
604
691
  // Handle different port types
605
692
  for (const [port, portData] of Object.entries(sitesByPort)) {
606
693
  const portNum = parseInt(port);
607
694
  const dispatcher = createDispatcher(portData);
608
695
  const upgradeHandler = createUpgradeHandler(portData);
609
696
 
610
- if (portNum === this.defaultPort) {
611
- // Use Greenlock for default port (443) with SSL
612
- const httpsServer = glx.httpsServer(null, dispatcher);
613
- this.portServers[portNum] = httpsServer;
697
+ let httpsServer;
614
698
 
615
- // Handle WebSocket upgrade events
616
- httpsServer.on('upgrade', upgradeHandler);
617
-
618
- httpsServer.listen(portNum, this.hostname, () => {
619
- log.info(`HTTPS server listening on port ${portNum}`);
620
- });
699
+ if (portNum === this.defaultPort && effectiveTlsMode === 'greenlock') {
700
+ log.info(`HTTPS port ${portNum}: using Greenlock SNI (certs managed automatically)`);
701
+ httpsServer = glx.httpsServer(null, dispatcher);
621
702
  } else {
622
- // Create HTTPS server for custom ports using Greenlock certificates
623
- const httpsOptions = {
624
- // SNI callback to get certificates dynamically
625
- SNICallback: (domain, callback) => {
626
- try {
627
- const certPath = path.join(this.greenlockStorePath, 'live', domain);
628
- const keyPath = path.join(certPath, 'privkey.pem');
629
- const certFilePath = path.join(certPath, 'cert.pem');
630
- const chainPath = path.join(certPath, 'chain.pem');
631
-
632
- if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
633
- const key = fs.readFileSync(keyPath, 'utf8');
634
- const cert = fs.readFileSync(certFilePath, 'utf8');
635
- const chain = fs.readFileSync(chainPath, 'utf8');
636
-
637
- callback(null, tls.createSecureContext({
638
- key: key,
639
- cert: cert + chain
640
- }));
641
- } else {
642
- callback(new Error(`No certificate files available for ${domain}`));
643
- }
644
- } catch (error) {
645
- callback(error);
646
- }
647
- }
648
- };
649
-
650
- const httpsServer = https.createServer(httpsOptions, dispatcher);
651
-
652
- // Handle WebSocket upgrade events
653
- httpsServer.on('upgrade', upgradeHandler);
654
-
655
- httpsServer.on('error', (error) => {
656
- log.error(`HTTPS server error on port ${portNum}:`, error.message);
657
- });
658
-
659
- httpsServer.on('tlsClientError', (error) => {
660
- // Suppress HTTP request errors to avoid log spam
661
- if (!error.message.includes('http request')) {
662
- log.error(`TLS error on port ${portNum}:`, error.message);
663
- }
703
+ const modeLabel = portNum === this.defaultPort ? effectiveTlsMode : 'static';
704
+ log.info(`HTTPS port ${portNum}: using static certs from ${path.join(this.greenlockStorePath, 'live')} [${modeLabel}]`);
705
+ const httpsOptions = Object.assign(this.getDefaultTlsOptions(), {
706
+ SNICallback: this.createSNICallback()
664
707
  });
708
+ httpsServer = https.createServer(httpsOptions, dispatcher);
709
+ }
665
710
 
666
- this.portServers[portNum] = httpsServer;
711
+ attachHttpsListeners(httpsServer, portNum, upgradeHandler);
712
+ this.portServers[portNum] = httpsServer;
667
713
 
668
- httpsServer.listen(portNum, this.hostname, (error) => {
669
- if (error) {
670
- log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
671
- } else {
672
- log.info(`HTTPS server listening on port ${portNum}`);
673
- }
674
- });
675
- }
714
+ httpsServer.listen(portNum, this.hostname, (error) => {
715
+ if (error) {
716
+ log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
717
+ } else {
718
+ log.info(`HTTPS server listening on port ${portNum}`);
719
+ }
720
+ });
676
721
  }
677
722
  });
678
723
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {