roster-server 3.0.1 → 3.1.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 +4 -0
- package/index.js +22 -5
- package/lib/isolated-site.js +4 -1
- package/lib/site-worker.js +2 -2
- package/package.json +1 -1
- package/test/isolation.test.js +41 -2
- package/test/roster-server.test.js +28 -0
package/README.md
CHANGED
|
@@ -272,6 +272,8 @@ const dnsChallenge = {
|
|
|
272
272
|
|
|
273
273
|
Pass this as the constructor's `dnsChallenge` option. To use another provider, install its Greenlock DNS plugin in the consuming application and supply its module name and provider options. Configure DNS timing on that object with `propagationDelay`, `dryRunDelay`, `dnsPollIntervalMs`, and `dnsPollTimeoutMs`; the wrapper's polling interval defaults to `15000` ms.
|
|
274
274
|
|
|
275
|
+
`serverKeyType: 'EC-P256'` makes each full TLS handshake much cheaper for the server than the default RSA key. It applies to certificates issued after the change: renewal reuses a certificate's existing key. To move an existing name to the new key type, remove its directory under `greenlockStorePath/live/` and restart; Roster issues a new certificate on the first connection for that name.
|
|
276
|
+
|
|
275
277
|
Roster's file-based SNI resolvers cache TLS contexts and detect changed certificate files through metadata, including updates from another process. Concurrent requests for the same certificate name share loads and checks. Standalone startup reuses the initialized certificate runtime and Roster's cancellable renewal loop. Loading an existing certificate does not itself force renewal.
|
|
276
278
|
|
|
277
279
|
## Configuration reference
|
|
@@ -296,6 +298,8 @@ Pass operational settings through the constructor, using the consuming applicati
|
|
|
296
298
|
| `handleSignals` | `false` | Close this instance on `SIGINT`/`SIGTERM`; does not force process exit. |
|
|
297
299
|
| `isolation` | `true` (`false` with `cluster`) | One process per site directory under `start()`; `false` or `{ restartDelayMs, backlog, helloTimeoutMs }` (defaults `1000`, `511`, `10000`). See [site isolation](#site-isolation). |
|
|
298
300
|
| `tlsMinVersion`, `tlsMaxVersion` | `TLSv1.2`, `TLSv1.3` | Protocol limits for created HTTPS servers. |
|
|
301
|
+
| `keepAliveTimeoutMs` | Node.js default (`5000`) | Idle time before a site server closes a keep-alive connection, in isolated and main-process sites. Set it above the interval at which clients repeat requests so they reuse the connection instead of paying a new TLS handshake. |
|
|
302
|
+
| `serverKeyType` | `RSA-2048` | Key for newly issued certificates: `RSA-2048`, `RSA-3072`, `RSA-4096`, `EC-P256`, or `EC-P384`. See [certificates](#certificates-and-dns). |
|
|
299
303
|
| `skipLocalCheck` | `true` | Skips Greenlock dry-run/local challenge checks. |
|
|
300
304
|
| `dnsChallenge` | CLI wrapper | DNS-01 options or `false`; see [certificates](#certificates-and-dns). |
|
|
301
305
|
| `disableWildcard`, `combineWildcardCerts` | `false`, `false` | Wildcard registration and certificate behavior. |
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const { IsolatedSite } = require('./lib/isolated-site.js');
|
|
|
13
13
|
const { parseClientHello } = require('./lib/client-hello.js');
|
|
14
14
|
const log = require('lemonlog')('roster');
|
|
15
15
|
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
16
|
+
const SERVER_KEY_TYPES = ['RSA-2048', 'RSA-3072', 'RSA-4096', 'EC-P256', 'EC-P384'];
|
|
16
17
|
// A request plugin carrying { module, export, options } under this key can be recreated in isolated site processes.
|
|
17
18
|
const PLUGIN_SOURCE = Symbol.for('roster-server.plugin-source');
|
|
18
19
|
|
|
@@ -355,6 +356,14 @@ class Roster {
|
|
|
355
356
|
this.maxLocalPort = options.maxLocalPort || 9999;
|
|
356
357
|
this.tlsMinVersion = options.tlsMinVersion ?? 'TLSv1.2';
|
|
357
358
|
this.tlsMaxVersion = options.tlsMaxVersion ?? 'TLSv1.3';
|
|
359
|
+
this.keepAliveTimeoutMs = options.keepAliveTimeoutMs;
|
|
360
|
+
if (this.keepAliveTimeoutMs !== undefined && (!Number.isFinite(this.keepAliveTimeoutMs) || this.keepAliveTimeoutMs <= 0)) {
|
|
361
|
+
throw new TypeError('keepAliveTimeoutMs must be a positive finite number');
|
|
362
|
+
}
|
|
363
|
+
this.serverKeyType = options.serverKeyType ?? 'RSA-2048';
|
|
364
|
+
if (!SERVER_KEY_TYPES.includes(this.serverKeyType)) {
|
|
365
|
+
throw new TypeError(`serverKeyType must be one of ${SERVER_KEY_TYPES.join(', ')}`);
|
|
366
|
+
}
|
|
358
367
|
this.disableWildcard = parseBooleanFlag(options.disableWildcard, false);
|
|
359
368
|
this.combineWildcardCerts = parseBooleanFlag(options.combineWildcardCerts, false);
|
|
360
369
|
if (isBunRuntime && this.combineWildcardCerts) {
|
|
@@ -607,7 +616,7 @@ class Roster {
|
|
|
607
616
|
renewOffset: "-45d",
|
|
608
617
|
renewStagger: "3d",
|
|
609
618
|
accountKeyType: "EC-P256",
|
|
610
|
-
serverKeyType:
|
|
619
|
+
serverKeyType: this.serverKeyType,
|
|
611
620
|
subscriberEmail: this.email
|
|
612
621
|
},
|
|
613
622
|
sites: sitesConfig
|
|
@@ -937,6 +946,7 @@ class Roster {
|
|
|
937
946
|
greenlockStorePath: this.greenlockStorePath,
|
|
938
947
|
tlsMinVersion: this.tlsMinVersion,
|
|
939
948
|
tlsMaxVersion: this.tlsMaxVersion,
|
|
949
|
+
keepAliveTimeoutMs: this.keepAliveTimeoutMs,
|
|
940
950
|
closeTimeoutMs: this.closeTimeoutMs
|
|
941
951
|
};
|
|
942
952
|
const sitesByPath = new Map();
|
|
@@ -985,7 +995,7 @@ class Roster {
|
|
|
985
995
|
socket.setTimeout(0);
|
|
986
996
|
const site = servername && this._isolatedSiteFor(routes, servername);
|
|
987
997
|
if (site) {
|
|
988
|
-
site.handoff(port, head, socket);
|
|
998
|
+
site.handoff(port, head, socket, () => this._sockets.delete(socket));
|
|
989
999
|
return;
|
|
990
1000
|
}
|
|
991
1001
|
socket.unshift(head);
|
|
@@ -994,7 +1004,14 @@ class Roster {
|
|
|
994
1004
|
socket.on('data', read);
|
|
995
1005
|
}
|
|
996
1006
|
|
|
1007
|
+
// Applies the options shared by every server that serves sites.
|
|
1008
|
+
_configureSiteServer(server) {
|
|
1009
|
+
if (this.keepAliveTimeoutMs !== undefined) server.keepAliveTimeout = this.keepAliveTimeoutMs;
|
|
1010
|
+
return server;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
997
1013
|
_listenSite(server, port) {
|
|
1014
|
+
this._configureSiteServer(server);
|
|
998
1015
|
const routes = this._isolatedRoutes[port];
|
|
999
1016
|
if (!routes) {
|
|
1000
1017
|
this.portServers[port] = server;
|
|
@@ -1545,14 +1562,14 @@ class Roster {
|
|
|
1545
1562
|
? await this.ensureCertificate(normalizedServername)
|
|
1546
1563
|
: this.loadCertificate(normalizedServername);
|
|
1547
1564
|
|
|
1548
|
-
const server = https.createServer({
|
|
1565
|
+
const server = this._configureSiteServer(https.createServer({
|
|
1549
1566
|
minVersion: this.tlsMinVersion,
|
|
1550
1567
|
maxVersion: this.tlsMaxVersion,
|
|
1551
1568
|
...tlsOptions,
|
|
1552
1569
|
key: pems.key,
|
|
1553
1570
|
cert: pems.cert,
|
|
1554
1571
|
SNICallback: this.sniCallback()
|
|
1555
|
-
});
|
|
1572
|
+
}));
|
|
1556
1573
|
|
|
1557
1574
|
this.attach(server, { port });
|
|
1558
1575
|
return server;
|
|
@@ -1591,7 +1608,7 @@ class Roster {
|
|
|
1591
1608
|
}
|
|
1592
1609
|
};
|
|
1593
1610
|
|
|
1594
|
-
const httpServer = http.createServer(dispatcher);
|
|
1611
|
+
const httpServer = this._configureSiteServer(http.createServer(dispatcher));
|
|
1595
1612
|
this.portServers[port] = httpServer;
|
|
1596
1613
|
|
|
1597
1614
|
httpServer.on('upgrade', (req, socket, head) => {
|
package/lib/isolated-site.js
CHANGED
|
@@ -108,8 +108,10 @@ class IsolatedSite {
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Pass an accepted client socket, with the bytes already read from it, to the site process.
|
|
111
|
+
* `onHandedOff` runs once the site process owns the connection; the main process never
|
|
112
|
+
* sees a 'close' event for it.
|
|
111
113
|
*/
|
|
112
|
-
handoff(port, head, socket) {
|
|
114
|
+
handoff(port, head, socket, onHandedOff) {
|
|
113
115
|
const child = this.child;
|
|
114
116
|
if (!this.ready || this.pending >= this.isolation.backlog) {
|
|
115
117
|
socket.destroy();
|
|
@@ -119,6 +121,7 @@ class IsolatedSite {
|
|
|
119
121
|
child.send({ type: 'connection', port, head: head.toString('base64') }, socket, error => {
|
|
120
122
|
if (this.child === child) this.pending--;
|
|
121
123
|
if (error) socket.destroy();
|
|
124
|
+
else onHandedOff();
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
|
package/lib/site-worker.js
CHANGED
|
@@ -67,13 +67,13 @@ class SiteWorkerRoster extends Roster {
|
|
|
67
67
|
for (const port of ports) {
|
|
68
68
|
// Only the default port issues missing certificates, as in the main process.
|
|
69
69
|
const issue = port === this.defaultPort;
|
|
70
|
-
const server = https.createServer({
|
|
70
|
+
const server = this._configureSiteServer(https.createServer({
|
|
71
71
|
minVersion: this.tlsMinVersion,
|
|
72
72
|
maxVersion: this.tlsMaxVersion,
|
|
73
73
|
SNICallback: (servername, callback) => {
|
|
74
74
|
this._getSecureContext(servername, issue).then(context => callback(null, context), callback);
|
|
75
75
|
}
|
|
76
|
-
});
|
|
76
|
+
}));
|
|
77
77
|
this.attach(server, { port });
|
|
78
78
|
this._ownedServers.add(server);
|
|
79
79
|
this._httpsServers.set(port, server);
|
package/package.json
CHANGED
package/test/isolation.test.js
CHANGED
|
@@ -52,11 +52,13 @@ function createWww(t, sites) {
|
|
|
52
52
|
return { root, wwwPath };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async function startProduction(t, sites, { certificates = [], issue = async () => {}, configure = () => {} } = {}) {
|
|
55
|
+
async function startProduction(t, sites, { certificates = [], issue = async () => {}, configure = () => {}, options = {} } = {}) {
|
|
56
56
|
const { root, wwwPath } = createWww(t, sites);
|
|
57
57
|
const store = path.join(root, 'greenlock.d');
|
|
58
58
|
for (const subject of certificates) installCertificate(store, subject);
|
|
59
|
-
const roster = new Roster({
|
|
59
|
+
const roster = new Roster({
|
|
60
|
+
wwwPath, greenlockStorePath: store, isolation: { restartDelayMs: 50 }, closeTimeoutMs: 5000, ...options
|
|
61
|
+
});
|
|
60
62
|
t.mock.method(shim, 'create', () => ({ get: issue }));
|
|
61
63
|
t.mock.method(greenlock, 'init', () => ({
|
|
62
64
|
ready(callback) {
|
|
@@ -154,6 +156,43 @@ it('keeps serving in the same worker when a connection arrives without a socket'
|
|
|
154
156
|
assert.equal(child.exitCode, null);
|
|
155
157
|
});
|
|
156
158
|
|
|
159
|
+
it('stops tracking connections in the main process once they are handed to a site process', { timeout: 20000 }, async t => {
|
|
160
|
+
const { roster, port } = await startProduction(t, { 'handoff.example.com': REPORT_SITE }, {
|
|
161
|
+
certificates: ['handoff.example.com']
|
|
162
|
+
});
|
|
163
|
+
for (let request = 0; request < 20; request++) {
|
|
164
|
+
assert.equal((await get(port, 'handoff.example.com')).status, 200);
|
|
165
|
+
}
|
|
166
|
+
for (let attempt = 0; attempt < 50 && roster._sockets.size > 0; attempt++) {
|
|
167
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
168
|
+
}
|
|
169
|
+
assert.equal(roster._sockets.size, 0, 'handed-off sockets are still tracked by the main process');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Resolves with how long the server kept the connection open after answering one request.
|
|
173
|
+
function idleKeepAliveMs(port, servername) {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const socket = tls.connect({ host: '127.0.0.1', port, servername, rejectUnauthorized: false }, () => {
|
|
176
|
+
socket.write(`GET / HTTP/1.1\r\nHost: ${servername}\r\nConnection: keep-alive\r\n\r\n`);
|
|
177
|
+
});
|
|
178
|
+
let answeredAt = null;
|
|
179
|
+
socket.on('data', () => { answeredAt ??= Date.now(); });
|
|
180
|
+
socket.on('close', () => answeredAt ? resolve(Date.now() - answeredAt) : reject(new Error('closed before answering')));
|
|
181
|
+
socket.on('error', reject);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const isolation of [true, false]) {
|
|
186
|
+
it(`closes idle keep-alive connections after keepAliveTimeoutMs (isolation ${isolation})`, { timeout: 20000 }, async t => {
|
|
187
|
+
const { port } = await startProduction(t, { 'idle.example.com': REPORT_SITE }, {
|
|
188
|
+
certificates: ['idle.example.com'],
|
|
189
|
+
options: { keepAliveTimeoutMs: 300, ...(isolation ? {} : { isolation: false }) }
|
|
190
|
+
});
|
|
191
|
+
const idleMs = await idleKeepAliveMs(port, 'idle.example.com');
|
|
192
|
+
assert.ok(idleMs >= 250 && idleMs < 2500, `connection stayed open ${idleMs} ms`);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
157
196
|
it('keeps serving other sites while one blocks its event loop', { timeout: 20000 }, async t => {
|
|
158
197
|
const { port } = await startProduction(t, {
|
|
159
198
|
'busy.example.com': `module.exports = () => (req, res) => {
|
|
@@ -813,6 +813,34 @@ describe('Roster generateConfigJson', () => {
|
|
|
813
813
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
814
814
|
}
|
|
815
815
|
});
|
|
816
|
+
it('issues certificates with the configured server key type', () => {
|
|
817
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-config-'));
|
|
818
|
+
try {
|
|
819
|
+
const keyTypeFor = (options) => {
|
|
820
|
+
const roster = new Roster({ local: false, greenlockStorePath: tmpDir, ...options });
|
|
821
|
+
roster.domains = ['example.com', 'www.example.com'];
|
|
822
|
+
roster.generateConfigJson();
|
|
823
|
+
return JSON.parse(fs.readFileSync(path.join(tmpDir, 'config.json'), 'utf8')).defaults.serverKeyType;
|
|
824
|
+
};
|
|
825
|
+
assert.strictEqual(keyTypeFor({}), 'RSA-2048');
|
|
826
|
+
assert.strictEqual(keyTypeFor({ serverKeyType: 'EC-P256' }), 'EC-P256');
|
|
827
|
+
} finally {
|
|
828
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
describe('Roster TLS and keep-alive options', () => {
|
|
834
|
+
it('rejects unsupported server key types', () => {
|
|
835
|
+
for (const serverKeyType of ['EC-P521', 'DSA-1024', 'ec-p256', '']) {
|
|
836
|
+
assert.throws(() => new Roster({ serverKeyType }), TypeError);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
it('rejects keep-alive timeouts that are not positive finite numbers', () => {
|
|
840
|
+
for (const keepAliveTimeoutMs of [0, -1, Infinity, '30000']) {
|
|
841
|
+
assert.throws(() => new Roster({ keepAliveTimeoutMs }), TypeError);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
816
844
|
});
|
|
817
845
|
|
|
818
846
|
describe('Roster init() (cluster-friendly API)', () => {
|