roster-server 2.4.12 → 2.4.13

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.
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ const { it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const https = require('node:https');
9
+ const { execFileSync } = require('node:child_process');
10
+ const Roster = require('../index.js');
11
+
12
+ async function createRoster(t) {
13
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-https-'));
14
+ const roster = new Roster({
15
+ local: false, autoCertificates: false,
16
+ wwwPath: path.join(root, 'www'), greenlockStorePath: root
17
+ });
18
+ t.after(async () => {
19
+ await roster.close();
20
+ fs.rmSync(root, { recursive: true, force: true });
21
+ });
22
+ roster.register('example.com', () => (req, res) => res.end('apex'));
23
+ roster.register('*.example.com', () => (req, res) => res.end('wildcard'));
24
+ roster.register('api.example.com', () => (req, res) => res.end('exact'));
25
+ await roster.init();
26
+ return roster;
27
+ }
28
+
29
+ function certificate(roster, subject, serial) {
30
+ const directory = path.join(roster.greenlockStorePath, 'live', subject);
31
+ fs.mkdirSync(directory, { recursive: true });
32
+ const key = path.join(directory, 'privkey.pem');
33
+ const cert = path.join(directory, 'cert.pem');
34
+ const keyArgs = fs.existsSync(key) ? ['-key', key] : ['-newkey', 'rsa:2048', '-nodes', '-keyout', key];
35
+ execFileSync('openssl', ['req', '-x509', ...keyArgs, '-out', cert,
36
+ '-subj', '/CN=example.com', '-days', '1', '-set_serial', String(serial)], { stdio: 'ignore' });
37
+ fs.writeFileSync(path.join(directory, 'chain.pem'), '');
38
+ return directory;
39
+ }
40
+
41
+ async function listen(t, server) {
42
+ t.after(() => new Promise(resolve => {
43
+ server.close(resolve);
44
+ server.closeAllConnections();
45
+ }));
46
+ await new Promise((resolve, reject) => {
47
+ server.once('error', reject);
48
+ server.listen(0, '127.0.0.1', resolve);
49
+ });
50
+ return server;
51
+ }
52
+
53
+ function request(server, host, servername = host) {
54
+ return new Promise((resolve, reject) => {
55
+ https.get({
56
+ hostname: '127.0.0.1', port: server.address().port, servername,
57
+ headers: { host }, rejectUnauthorized: false, agent: false
58
+ }, res => {
59
+ const serial = res.socket.getPeerCertificate().serialNumber;
60
+ let body = '';
61
+ res.on('data', chunk => { body += chunk; });
62
+ res.on('end', () => resolve({ status: res.statusCode, body, serial }));
63
+ res.on('error', reject);
64
+ }).on('error', reject);
65
+ });
66
+ }
67
+
68
+ it('routes real HTTPS requests using SNI, wildcard storage, and exact host precedence', { timeout: 10000 }, async t => {
69
+ const roster = await createRoster(t);
70
+ certificate(roster, 'example.com', 1);
71
+ certificate(roster, '_wildcard_.example.com', 2);
72
+ certificate(roster, 'api.example.com', 3);
73
+ const server = await listen(t, await roster.createServingHttpsServer({ servername: 'example.com' }));
74
+ assert.deepEqual(await request(server, 'example.com'), { status: 200, body: 'apex', serial: '01' });
75
+ assert.deepEqual(await request(server, 'shop.example.com'), { status: 200, body: 'wildcard', serial: '02' });
76
+ assert.deepEqual(await request(server, 'api.example.com'), { status: 200, body: 'exact', serial: '03' });
77
+ assert.equal((await request(server, 'unknown.test', 'example.com')).status, 404);
78
+ assert.deepEqual(await request(server, 'example.com', ''), { status: 200, body: 'apex', serial: '01' });
79
+ await roster.close();
80
+ assert.equal(server.listening, true);
81
+ assert.equal(server.listenerCount('request'), 0);
82
+ assert.equal(server.listenerCount('upgrade'), 0);
83
+ });
84
+
85
+ it('reloads certificates on fresh TLS handshakes and recovers from invalid PEM files', { timeout: 10000 }, async t => {
86
+ const roster = await createRoster(t);
87
+ const directory = certificate(roster, 'example.com', 1);
88
+ const server = await listen(t, await roster.createServingHttpsServer({ servername: 'example.com' }));
89
+ assert.equal((await request(server, 'example.com')).serial, '01');
90
+ fs.writeFileSync(path.join(directory, 'cert.pem'), 'invalid certificate');
91
+ await assert.rejects(request(server, 'example.com'));
92
+ certificate(roster, 'example.com', 2);
93
+ assert.deepEqual(await request(server, 'example.com'), { status: 200, body: 'apex', serial: '02' });
94
+ fs.unlinkSync(path.join(directory, 'cert.pem'));
95
+ await assert.rejects(request(server, 'example.com'));
96
+ certificate(roster, 'example.com', 3);
97
+ assert.equal((await request(server, 'example.com')).serial, '03');
98
+ });
99
+
100
+ it('coalesces managed certificate issuance and serves the resulting certificate', { timeout: 10000 }, async t => {
101
+ const roster = await createRoster(t);
102
+ const issuance = t.mock.fn(async () => { certificate(roster, 'example.com', 4); });
103
+ roster._greenlockRuntime = { get: issuance };
104
+ const servers = await Promise.all(Array.from({ length: 3 }, () =>
105
+ roster.createManagedHttpsServer({ servername: 'EXAMPLE.COM' })
106
+ ));
107
+ for (const server of servers) await listen(t, server);
108
+ assert.equal(issuance.mock.callCount(), 1);
109
+ assert.deepEqual(issuance.mock.calls[0].arguments, [{ servername: 'example.com' }]);
110
+ for (const server of servers) {
111
+ assert.deepEqual(await request(server, 'example.com'), { status: 200, body: 'apex', serial: '04' });
112
+ }
113
+ await roster.ensureCertificate('example.com');
114
+ assert.equal(issuance.mock.callCount(), 1);
115
+ });
116
+
117
+ it('reports issuance failure or missing output and permits a subsequent successful check', { timeout: 10000 }, async t => {
118
+ const roster = await createRoster(t);
119
+ const failure = new Error('certificate authority unavailable');
120
+ roster._greenlockRuntime = { get: t.mock.fn(async () => { throw failure; }) };
121
+ await assert.rejects(roster.ensureCertificate('example.com'), error => error === failure);
122
+ roster._greenlockRuntime.get = t.mock.fn(async () => {});
123
+ await assert.rejects(roster.ensureCertificate('example.com'), /no PEM files/);
124
+ roster._greenlockRuntime.get = t.mock.fn(async () => { certificate(roster, 'example.com', 5); });
125
+ const pems = await roster.ensureCertificate('example.com');
126
+ assert.ok(pems.cert.includes('BEGIN CERTIFICATE'));
127
+ assert.equal(roster._greenlockRuntime.get.mock.callCount(), 1);
128
+ });
129
+
130
+ it('serving-only creation never issues a missing certificate', async t => {
131
+ const roster = await createRoster(t);
132
+ const issuance = t.mock.fn(async () => { throw new Error('Unexpected issuance'); });
133
+ roster._greenlockRuntime = { get: issuance };
134
+ await assert.rejects(roster.createServingHttpsServer({ servername: 'example.com' }), /No certificate files/);
135
+ assert.equal(issuance.mock.callCount(), 0);
136
+ });
137
+
138
+ it('resolves a missing SNI certificate through the shared issuer', { timeout: 10000 }, async t => {
139
+ const roster = await createRoster(t);
140
+ certificate(roster, 'example.com', 1);
141
+ const issuance = t.mock.fn(async ({ servername }) => { certificate(roster, servername, 6); });
142
+ roster._greenlockRuntime = { get: issuance };
143
+ const server = await listen(t, await roster.createServingHttpsServer({ servername: 'example.com' }));
144
+ const result = await request(server, 'example.com', 'other.test');
145
+ assert.deepEqual(result, { status: 200, body: 'apex', serial: '06' });
146
+ assert.deepEqual(issuance.mock.calls[0].arguments, [{ servername: 'other.test' }]);
147
+ await request(server, 'example.com', 'other.test');
148
+ assert.equal(issuance.mock.callCount(), 1);
149
+ });
@@ -0,0 +1,469 @@
1
+ 'use strict';
2
+
3
+ const { it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const http = require('node:http');
9
+ const net = require('node:net');
10
+ const { once } = require('node:events');
11
+ const Roster = require('../index.js');
12
+
13
+ function createRoster(t, options = {}) {
14
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-lifecycle-'));
15
+ const roster = new Roster({ local: true, wwwPath: directory, ...options });
16
+ t.after(async () => {
17
+ await roster.close().catch(() => {});
18
+ fs.rmSync(directory, { recursive: true, force: true });
19
+ });
20
+ return roster;
21
+ }
22
+
23
+ async function serve(t, roster) {
24
+ await roster.init();
25
+ const server = http.createServer();
26
+ roster.attach(server);
27
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
28
+ t.after(() => new Promise(resolve => {
29
+ server.close(resolve);
30
+ server.closeAllConnections();
31
+ }));
32
+ return server;
33
+ }
34
+
35
+ function request(server, options = {}) {
36
+ return new Promise((resolve, reject) => {
37
+ const req = http.request({
38
+ hostname: server.address().address, port: server.address().port, agent: false,
39
+ headers: { host: 'example.com' }, ...options
40
+ }, res => {
41
+ let body = '';
42
+ res.on('data', chunk => { body += chunk; });
43
+ res.on('end', () => resolve({ status: res.statusCode, body, headers: res.headers }));
44
+ res.on('error', reject);
45
+ });
46
+ req.on('error', reject);
47
+ req.end();
48
+ });
49
+ }
50
+
51
+ it('shares initialization and runs each factory once', async t => {
52
+ const roster = createRoster(t);
53
+ let calls = 0;
54
+ roster.register('example.com', () => { calls++; return (req, res) => res.end('ok'); });
55
+ const first = roster.init();
56
+ assert.equal(first, roster.init());
57
+ await first;
58
+ assert.equal(calls, 1);
59
+ });
60
+
61
+ it('cleans initialized sites when a subsequent factory throws', async t => {
62
+ const roster = createRoster(t);
63
+ let cleaned = false;
64
+ roster.register('example.com', server => {
65
+ server.onClose(async () => { await Promise.resolve(); cleaned = true; });
66
+ return () => {};
67
+ });
68
+ roster.register('broken.example.com', () => { throw new Error('factory failed'); });
69
+ await assert.rejects(roster.init(), /factory failed/);
70
+ assert.equal(cleaned, true);
71
+ });
72
+
73
+ it('waits for listening, shares start, and closes owned servers once', async t => {
74
+ const roster = createRoster(t);
75
+ roster.assignPortToDomain = () => 0;
76
+ let closed = 0;
77
+ roster.register('example.com', server => {
78
+ server.onClose(() => { closed++; });
79
+ return (req, res) => res.end('ok');
80
+ });
81
+ const start = roster.start();
82
+ assert.equal(start, roster.start());
83
+ await start;
84
+ const server = roster.portServers[0];
85
+ assert.equal(server.listening, true);
86
+ assert.equal((await request(server)).body, 'ok');
87
+ const closing = roster.close();
88
+ assert.equal(closing, roster.close());
89
+ await closing;
90
+ assert.equal(server.listening, false);
91
+ assert.equal(closed, 1);
92
+ await assert.rejects(roster.start(), /closing or closed/);
93
+ });
94
+
95
+ it('rejects bind failures and rolls back already opened ports', async t => {
96
+ const blocker = http.createServer();
97
+ await new Promise(resolve => blocker.listen(0, 'localhost', resolve));
98
+ t.after(() => new Promise(resolve => blocker.close(resolve)));
99
+ const roster = createRoster(t);
100
+ const ports = [0, blocker.address().port];
101
+ roster.assignPortToDomain = () => ports.shift();
102
+ let cleaned = 0;
103
+ for (const domain of ['example.com', 'second.example.com']) {
104
+ roster.register(domain, server => {
105
+ server.onClose(() => { cleaned++; });
106
+ return () => {};
107
+ });
108
+ }
109
+ await assert.rejects(roster.start(), { code: 'EADDRINUSE' });
110
+ assert.equal(roster.portServers[0].listening, false);
111
+ assert.equal(cleaned, 2);
112
+ });
113
+
114
+ it('close during initialization prevents factories from starting', async t => {
115
+ const roster = createRoster(t);
116
+ let release;
117
+ roster.loadSites = () => new Promise(resolve => { release = resolve; });
118
+ let called = false;
119
+ roster.register('example.com', () => { called = true; });
120
+ const initializing = roster.init();
121
+ const rejected = assert.rejects(initializing, /closing or closed/);
122
+ const closing = roster.close();
123
+ release();
124
+ await Promise.all([rejected, closing]);
125
+ assert.equal(called, false);
126
+ });
127
+
128
+ it('drains active HTTP before cleanup and preserves external server ownership', async t => {
129
+ const roster = createRoster(t);
130
+ let release;
131
+ let entered;
132
+ const started = new Promise(resolve => { entered = resolve; });
133
+ let cleaned = false;
134
+ roster.register('example.com', server => {
135
+ server.onClose(() => { cleaned = true; });
136
+ return async (req, res) => {
137
+ entered();
138
+ await new Promise(resolve => { release = resolve; });
139
+ assert.equal(cleaned, false);
140
+ res.end('drained');
141
+ };
142
+ });
143
+ const server = await serve(t, roster);
144
+ const pending = request(server);
145
+ await started;
146
+ const closing = roster.close();
147
+ assert.equal((await request(server)).status, 503);
148
+ assert.equal(cleaned, false);
149
+ release();
150
+ assert.equal((await pending).body, 'drained');
151
+ await closing;
152
+ assert.equal(cleaned, true);
153
+ assert.equal(server.listening, true);
154
+ assert.equal(server.listenerCount('request'), 0);
155
+ server.on('request', (req, res) => res.end('external'));
156
+ assert.equal((await request(server)).body, 'external');
157
+ });
158
+
159
+ it('runs all async hooks and aggregates errors', async t => {
160
+ const roster = createRoster(t);
161
+ const calls = [];
162
+ roster.register('example.com', server => {
163
+ server.onClose(() => { calls.push('first'); throw new Error('cleanup failed'); });
164
+ server.onClose(async () => { await Promise.resolve(); calls.push('second'); });
165
+ return () => {};
166
+ });
167
+ await roster.init();
168
+ await assert.rejects(roster.close(), error => {
169
+ assert.ok(error instanceof AggregateError);
170
+ assert.equal(error.errors[0].message, 'cleanup failed');
171
+ return true;
172
+ });
173
+ assert.deepEqual(calls, ['first', 'second']);
174
+ });
175
+
176
+ it('notifies virtual servers before draining long polling requests', async t => {
177
+ const roster = createRoster(t, { closeTimeoutMs: 1000 });
178
+ let entered;
179
+ const started = new Promise(resolve => { entered = resolve; });
180
+ let pendingResponse;
181
+ let cleaned = false;
182
+ roster.register('example.com', virtual => {
183
+ virtual.on('close', () => pendingResponse.end('poll closed'));
184
+ virtual.onClose(() => { cleaned = true; });
185
+ return (req, res) => { pendingResponse = res; entered(); };
186
+ });
187
+ const server = await serve(t, roster);
188
+ const pending = request(server);
189
+ await started;
190
+ await roster.close();
191
+ assert.equal((await pending).body, 'poll closed');
192
+ assert.equal(cleaned, true);
193
+ });
194
+
195
+ it('bounds a hanging cleanup hook', async t => {
196
+ const roster = createRoster(t, { closeTimeoutMs: 20 });
197
+ roster.register('example.com', server => {
198
+ server.onClose(() => new Promise(() => {}));
199
+ return () => {};
200
+ });
201
+ await roster.init();
202
+ await assert.rejects(roster.close(), error => error.errors.some(item => /timed out/.test(item.message)));
203
+ });
204
+
205
+ it('forces a stalled request closed at the deadline and still runs cleanup', async t => {
206
+ const roster = createRoster(t, { closeTimeoutMs: 30 });
207
+ let entered;
208
+ const started = new Promise(resolve => { entered = resolve; });
209
+ let cleaned = false;
210
+ roster.register('example.com', server => {
211
+ server.onClose(() => { cleaned = true; });
212
+ return () => entered();
213
+ });
214
+ const server = await serve(t, roster);
215
+ const pending = assert.rejects(request(server));
216
+ await started;
217
+ await assert.rejects(roster.close(), error => error.errors.some(item => /timed out/.test(item.message)));
218
+ await pending;
219
+ assert.equal(cleaned, true);
220
+ });
221
+
222
+ it('closes upgraded sockets without closing an attached server', async t => {
223
+ const roster = createRoster(t);
224
+ let upgraded;
225
+ const ready = new Promise(resolve => { upgraded = resolve; });
226
+ roster.register('example.com', server => {
227
+ server.on('upgrade', (req, socket) => {
228
+ socket.write('HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\n');
229
+ upgraded();
230
+ });
231
+ return (req, res) => res.end('ok');
232
+ });
233
+ const server = await serve(t, roster);
234
+ const client = net.connect(server.address().port, '127.0.0.1');
235
+ client.on('error', () => {});
236
+ client.resume();
237
+ t.after(() => client.destroy());
238
+ await once(client, 'connect');
239
+ client.write('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\n');
240
+ await ready;
241
+ const closed = once(client, 'close');
242
+ await roster.close();
243
+ await closed;
244
+ assert.equal(server.listening, true);
245
+ });
246
+
247
+ it('does not fall back while an asynchronous request listener is responding', async t => {
248
+ const roster = createRoster(t);
249
+ roster.register('example.com', server => {
250
+ server.on('request', (req, res) => { setImmediate(() => res.end('async listener')); });
251
+ return (req, res) => res.end('wrong fallback');
252
+ });
253
+ const server = await serve(t, roster);
254
+ assert.equal((await request(server)).body, 'async listener');
255
+ });
256
+
257
+ it('allows Socket.IO-style wrappers to capture the returned HTTP fallback', async t => {
258
+ const roster = createRoster(t);
259
+ roster.register('example.com', server => {
260
+ const listeners = server.listeners('request');
261
+ server.removeAllListeners('request');
262
+ server.on('request', (req, res) => {
263
+ if (req.url === '/socket.io') setImmediate(() => res.end('socket'));
264
+ else listeners.forEach(listener => listener(req, res));
265
+ });
266
+ return async (req, res) => { await Promise.resolve(); res.end('http fallback'); };
267
+ });
268
+ const server = await serve(t, roster);
269
+ assert.equal((await request(server, { path: '/socket.io' })).body, 'socket');
270
+ assert.equal((await request(server, { path: '/' })).body, 'http fallback');
271
+ });
272
+
273
+ it('handles rejected and thrown request errors without bringing down other sites', async t => {
274
+ const roster = createRoster(t);
275
+ roster.register('example.com', () => async () => { throw new Error('async failure'); });
276
+ roster.register('other.example.com', () => () => { throw new Error('sync failure'); });
277
+ const server = await serve(t, roster);
278
+ assert.equal((await request(server)).status, 500);
279
+ assert.equal((await request(server, { headers: { host: 'other.example.com' } })).status, 500);
280
+ });
281
+
282
+ it('aborts a partially written response on rejection and keeps other sites available', { timeout: 5000 }, async t => {
283
+ const roster = createRoster(t);
284
+ let release;
285
+ const fail = new Promise(resolve => { release = resolve; });
286
+ roster.register('example.com', () => async (req, res) => {
287
+ res.write('partial');
288
+ await fail;
289
+ throw new Error('streaming handler failed');
290
+ });
291
+ roster.register('healthy.example.com', () => (req, res) => res.end('healthy'));
292
+ const server = await serve(t, roster);
293
+ await new Promise((resolve, reject) => {
294
+ http.get({ hostname: '127.0.0.1', port: server.address().port,
295
+ headers: { host: 'example.com' }, agent: false }, res => {
296
+ assert.equal(res.statusCode, 200);
297
+ res.once('data', () => release());
298
+ res.once('aborted', resolve);
299
+ res.once('error', error => { if (error.code !== 'ECONNRESET') reject(error); });
300
+ res.once('end', () => reject(new Error('Partial response must not complete successfully')));
301
+ }).on('error', reject);
302
+ });
303
+ assert.equal((await request(server, { headers: { host: 'healthy.example.com' } })).body, 'healthy');
304
+ });
305
+
306
+ it('closes every virtual server when the same domain is registered on multiple ports', async t => {
307
+ const roster = createRoster(t);
308
+ const closed = [];
309
+ const cleaned = [];
310
+ for (const port of [443, 8880, 8882]) {
311
+ roster.register(`example.com:${port}`, virtual => {
312
+ virtual.on('close', () => closed.push(port));
313
+ virtual.onClose(async () => { await Promise.resolve(); cleaned.push(port); });
314
+ return () => {};
315
+ });
316
+ }
317
+ await roster.init();
318
+ await Promise.all([roster.close(), roster.close()]);
319
+ assert.deepEqual(closed.sort((a, b) => a - b), [443, 8880, 8882]);
320
+ assert.deepEqual(cleaned.sort((a, b) => a - b), [443, 8880, 8882]);
321
+ });
322
+
323
+ it('preserves initialization and cleanup errors when both fail', async t => {
324
+ const roster = createRoster(t);
325
+ const cleanupError = new Error('cleanup failed');
326
+ const initError = new Error('factory failed');
327
+ roster.register('example.com', virtual => {
328
+ virtual.onClose(() => { throw cleanupError; });
329
+ return () => {};
330
+ });
331
+ roster.register('broken.example.com', () => { throw initError; });
332
+ await assert.rejects(roster.init(), error => {
333
+ assert.ok(error instanceof AggregateError);
334
+ assert.equal(error.errors[0], initError);
335
+ assert.deepEqual(error.errors[1].errors, [cleanupError]);
336
+ return true;
337
+ });
338
+ });
339
+
340
+ it('cancels a pending listen without opening a port when DNS resolves after shutdown', { timeout: 5000 }, async t => {
341
+ const dns = require('node:dns');
342
+ let resolveLookup;
343
+ t.mock.method(dns, 'lookup', (hostname, options, callback) => { resolveLookup = callback; });
344
+ const roster = createRoster(t);
345
+ const server = http.createServer();
346
+ t.after(() => { server.close(); server.closeAllConnections(); });
347
+ const listening = assert.rejects(roster._listen(server, 0, 'pending.test'), /closing or closed/);
348
+ await roster.close();
349
+ await listening;
350
+ resolveLookup(null, '127.0.0.1', 4);
351
+ await new Promise(resolve => setImmediate(resolve));
352
+ assert.equal(server.listening, false);
353
+ assert.equal(server.address(), null);
354
+ });
355
+
356
+ it('preserves the custom port in www redirects', async t => {
357
+ const roster = createRoster(t);
358
+ roster.register('example.com', () => () => {});
359
+ const server = await serve(t, roster);
360
+ const response = await request(server, { headers: { host: 'www.example.com:8880' }, path: '/test' });
361
+ assert.equal(response.headers.location, 'http://example.com:8880/test');
362
+ });
363
+
364
+ it('attaches once and rejects conflicting port attachments', async t => {
365
+ const roster = createRoster(t);
366
+ roster.register('example.com', () => () => {});
367
+ const server = await serve(t, roster);
368
+ roster.attach(server);
369
+ assert.equal(server.listenerCount('request'), 1);
370
+ assert.equal(server.listenerCount('upgrade'), 1);
371
+ assert.throws(() => roster.attach(server, { port: 8880 }), /another port/);
372
+ });
373
+
374
+ it('supports virtual close callbacks and close events exactly once', async t => {
375
+ const roster = createRoster(t);
376
+ const server = roster.createVirtualServer('example.com');
377
+ let events = 0;
378
+ server.on('close', () => { events++; });
379
+ await new Promise(resolve => server.close(resolve));
380
+ await new Promise(resolve => server.close(resolve));
381
+ assert.equal(events, 1);
382
+ });
383
+
384
+ it('cancels renewal and retry timers on close', async t => {
385
+ const roster = createRoster(t);
386
+ roster.register('example.com', () => () => {});
387
+ await roster.init();
388
+ let calls = 0;
389
+ roster._greenlockRuntime = { get: async () => { calls++; } };
390
+ roster.certificateRenewIntervalMs = 10;
391
+ roster._startCertificateRenewLoop();
392
+ roster._retryTimers.add(setTimeout(() => { calls++; }, 10));
393
+ await roster.close();
394
+ await new Promise(resolve => setTimeout(resolve, 25));
395
+ assert.equal(calls, 0);
396
+ assert.equal(roster._retryTimers.size, 0);
397
+ });
398
+
399
+ it('rejects an exhausted local port range instead of looping indefinitely', t => {
400
+ const roster = createRoster(t, { minLocalPort: 19000, maxLocalPort: 19000 });
401
+ roster.assignPortToDomain('example.com');
402
+ assert.throws(() => roster.assignPortToDomain('other.example.com'), /exhausted/);
403
+ });
404
+
405
+ it('handles errors from virtual listeners and invalid async plugins', async t => {
406
+ const roster = createRoster(t);
407
+ roster.register('example.com', server => {
408
+ server.on('request', async () => { throw new Error('listener failed'); });
409
+ });
410
+ const server = await serve(t, roster);
411
+ assert.equal((await request(server)).status, 500);
412
+ roster.use(async () => { throw new Error('invalid plugin'); });
413
+ assert.equal((await request(server)).status, 500);
414
+ });
415
+
416
+ it('uses one certificate runtime and waits for production listeners', async t => {
417
+ const shim = require('../vendor/greenlock-express/greenlock-shim.js');
418
+ const greenlock = require('../vendor/greenlock-express/greenlock-express.js');
419
+ const roster = createRoster(t, { local: false });
420
+ roster.greenlockStorePath = path.join(roster.wwwPath, 'certificates');
421
+ let creates = 0;
422
+ const runtime = { get: async () => null };
423
+ t.mock.method(shim, 'create', options => {
424
+ assert.equal(options.renew, false);
425
+ creates++;
426
+ return runtime;
427
+ });
428
+ const acme = http.createServer();
429
+ t.mock.method(greenlock, 'init', options => {
430
+ assert.equal(options.greenlock, runtime);
431
+ return {
432
+ ready(callback) {
433
+ setImmediate(() => callback({
434
+ httpServer: () => acme,
435
+ httpsServer: (options, handler) => http.createServer(handler)
436
+ }));
437
+ return this;
438
+ }
439
+ };
440
+ });
441
+ const listen = roster._listen.bind(roster);
442
+ roster._listen = server => listen(server, 0, '127.0.0.1');
443
+ roster.register('example.com', () => (req, res) => res.end('production route'));
444
+ await roster.start();
445
+ assert.equal(creates, 1);
446
+ assert.equal(acme.listening, true);
447
+ assert.equal(roster.portServers[443].listening, true);
448
+ assert.equal((await request(roster.portServers[443])).body, 'production route');
449
+ await roster.close();
450
+ assert.equal(acme.listening, false);
451
+ assert.equal(roster.portServers[443].listening, false);
452
+ });
453
+
454
+ it('propagates an ACME listener bind failure through the real Greenlock adapter', async t => {
455
+ const shim = require('../vendor/greenlock-express/greenlock-shim.js');
456
+ t.mock.method(shim, 'create', options => options.greenlock || { get: async () => null });
457
+ const exit = t.mock.method(process, 'exit', () => { throw new Error('Unexpected process.exit'); });
458
+ const blocker = http.createServer();
459
+ await new Promise(resolve => blocker.listen(0, '127.0.0.1', resolve));
460
+ t.after(() => new Promise(resolve => blocker.close(resolve)));
461
+ const roster = createRoster(t, { local: false });
462
+ roster.greenlockStorePath = path.join(roster.wwwPath, 'certificates');
463
+ roster.register('example.com', () => () => {});
464
+ const listen = roster._listen.bind(roster);
465
+ roster._listen = server => listen(server, blocker.address().port, '127.0.0.1');
466
+ await assert.rejects(roster.start(), { code: 'EADDRINUSE' });
467
+ assert.equal(exit.mock.callCount(), 0);
468
+ assert.equal(roster._certificateRenewTimer, null);
469
+ });