roster-server 3.0.2 → 3.2.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 +21 -4
- 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
|
+
New certificates use an `EC-P256` key by default, which makes each full TLS handshake much cheaper for the server than an RSA key. Clients that cannot negotiate ECDSA, such as Windows XP or Android 2.x, need `serverKeyType: 'RSA-2048'`. The key type applies to certificates issued after it is set: renewal reuses a certificate's existing key, so certificates issued before 3.2.0 keep their RSA key. To move an existing name to the configured 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` | `EC-P256` | Key for newly issued certificates: `EC-P256`, `EC-P384`, `RSA-2048`, `RSA-3072`, or `RSA-4096`. 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 ?? 'EC-P256';
|
|
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();
|
|
@@ -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/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({}), 'EC-P256');
|
|
826
|
+
assert.strictEqual(keyTypeFor({ serverKeyType: 'RSA-2048' }), 'RSA-2048');
|
|
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)', () => {
|