roster-server 2.0.0 → 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.
- package/README.md +112 -0
- package/index.js +107 -62
- package/package.json +1 -1
- package/vendor/greenlock-express/greenlock-shim.js +3 -3
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
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
SNICallback: (
|
|
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
|
-
|
|
711
|
+
attachHttpsListeners(httpsServer, portNum, upgradeHandler);
|
|
712
|
+
this.portServers[portNum] = httpsServer;
|
|
667
713
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
@@ -44,16 +44,16 @@ module.exports.create = function(opts) {
|
|
|
44
44
|
console.warn(" npx greenlock add --subject example.com --altnames example.com");
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
console.info("Ready to Serve:");
|
|
47
|
+
// console.info("Ready to Serve:");
|
|
48
48
|
|
|
49
49
|
var max = 3;
|
|
50
50
|
if (sites.length >= 1) {
|
|
51
51
|
sites.slice(0, max).forEach(function(site) {
|
|
52
|
-
console.info("\t", site.altnames.join(" "));
|
|
52
|
+
// console.info("\t", site.altnames.join(" "));
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
if (sites.length > max) {
|
|
56
|
-
console.info("and %d others", sites.length - max);
|
|
56
|
+
// console.info("and %d others", sites.length - max);
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
|