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.
- package/README.md +192 -404
- package/index.js +417 -126
- package/lib/static-site-handler.js +36 -27
- package/package.json +1 -1
- package/skills/roster-server/SKILL.md +50 -338
- package/test/https-integration.test.js +149 -0
- package/test/lifecycle.test.js +469 -0
- package/test/roster-server.test.js +22 -22
- package/test/scanner-blocker.test.js +3 -6
- package/test/static-and-tls.test.js +273 -0
- package/vendor/greenlock-express/greenlock-express.js +1 -1
- package/vendor/greenlock-express/servers.js +2 -1
- package/vendor/greenlock-express/single.js +1 -1
- package/vendor/greenlock-express/worker.js +2 -2
|
@@ -16,14 +16,8 @@ const {
|
|
|
16
16
|
buildCertLookupCandidates
|
|
17
17
|
} = require('../index.js');
|
|
18
18
|
|
|
19
|
-
function closePortServers(roster) {
|
|
20
|
-
|
|
21
|
-
for (const server of Object.values(roster.portServers)) {
|
|
22
|
-
try {
|
|
23
|
-
server.close();
|
|
24
|
-
} catch (_) {}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
19
|
+
async function closePortServers(roster) {
|
|
20
|
+
await roster.close();
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
function httpGet(host, port, pathname = '/') {
|
|
@@ -490,7 +484,7 @@ describe('Roster local mode (local: true)', () => {
|
|
|
490
484
|
assert.strictEqual(probe.statusCode, 404);
|
|
491
485
|
assert.strictEqual(probe.body, 'Not Found');
|
|
492
486
|
} finally {
|
|
493
|
-
closePortServers(roster);
|
|
487
|
+
await closePortServers(roster);
|
|
494
488
|
}
|
|
495
489
|
});
|
|
496
490
|
|
|
@@ -507,7 +501,7 @@ describe('Roster local mode (local: true)', () => {
|
|
|
507
501
|
assert.ok(url && url.startsWith('http://localhost:'));
|
|
508
502
|
assert.ok(roster.domainPorts['geturltest.example'] !== undefined);
|
|
509
503
|
} finally {
|
|
510
|
-
closePortServers(roster);
|
|
504
|
+
await closePortServers(roster);
|
|
511
505
|
}
|
|
512
506
|
});
|
|
513
507
|
});
|
|
@@ -533,26 +527,32 @@ describe('Roster loadSites', () => {
|
|
|
533
527
|
try {
|
|
534
528
|
await roster.init();
|
|
535
529
|
|
|
536
|
-
const
|
|
530
|
+
const { Writable } = require('stream');
|
|
531
|
+
const dispatch = async (port) => {
|
|
537
532
|
let statusCode;
|
|
538
533
|
let body = '';
|
|
539
|
-
const res = {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
};
|
|
534
|
+
const res = new Writable({
|
|
535
|
+
write(chunk, encoding, done) { body += chunk.toString(); done(); }
|
|
536
|
+
});
|
|
537
|
+
res.writeHead = status => { statusCode = status; };
|
|
538
|
+
const finished = new Promise((resolve, reject) => {
|
|
539
|
+
res.once('finish', resolve);
|
|
540
|
+
res.once('error', reject);
|
|
541
|
+
});
|
|
543
542
|
roster.requestHandler(port)(
|
|
544
543
|
{ headers: { host: 'persist.example.com' }, method: 'GET', url: '/' },
|
|
545
544
|
res
|
|
546
545
|
);
|
|
546
|
+
await finished;
|
|
547
547
|
return { statusCode, body };
|
|
548
548
|
};
|
|
549
549
|
|
|
550
|
-
assert.deepStrictEqual(dispatch(443), {
|
|
550
|
+
assert.deepStrictEqual(await dispatch(443), {
|
|
551
551
|
statusCode: 200,
|
|
552
552
|
body: '<html>static-443</html>'
|
|
553
553
|
});
|
|
554
|
-
assert.deepStrictEqual(dispatch(8880), { statusCode: 200, body: 'app-8880' });
|
|
555
|
-
assert.deepStrictEqual(dispatch(8882), { statusCode: 200, body: 'socket-8882' });
|
|
554
|
+
assert.deepStrictEqual(await dispatch(8880), { statusCode: 200, body: 'app-8880' });
|
|
555
|
+
assert.deepStrictEqual(await dispatch(8882), { statusCode: 200, body: 'socket-8882' });
|
|
556
556
|
} finally {
|
|
557
557
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
558
558
|
}
|
|
@@ -691,7 +691,7 @@ describe('Roster loadSites', () => {
|
|
|
691
691
|
assert.strictEqual(result.statusCode, 200);
|
|
692
692
|
assert.ok(result.body.includes('static ok'));
|
|
693
693
|
} finally {
|
|
694
|
-
closePortServers(roster);
|
|
694
|
+
await closePortServers(roster);
|
|
695
695
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
696
696
|
}
|
|
697
697
|
});
|
|
@@ -711,7 +711,7 @@ describe('Roster loadSites', () => {
|
|
|
711
711
|
const result = await httpGet('localhost', port, '/nonexistent.html');
|
|
712
712
|
assert.strictEqual(result.statusCode, 404);
|
|
713
713
|
} finally {
|
|
714
|
-
closePortServers(roster);
|
|
714
|
+
await closePortServers(roster);
|
|
715
715
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
716
716
|
}
|
|
717
717
|
});
|
|
@@ -738,7 +738,7 @@ describe('Roster loadSites', () => {
|
|
|
738
738
|
assert.strictEqual(resultNoSlash.statusCode, 200);
|
|
739
739
|
assert.ok(resultNoSlash.body.includes('en page'));
|
|
740
740
|
} finally {
|
|
741
|
-
closePortServers(roster);
|
|
741
|
+
await closePortServers(roster);
|
|
742
742
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
743
743
|
}
|
|
744
744
|
});
|
|
@@ -860,7 +860,7 @@ describe('Roster init() (cluster-friendly API)', () => {
|
|
|
860
860
|
assert.strictEqual(result.statusCode, 200);
|
|
861
861
|
assert.strictEqual(result.body, 'after-init');
|
|
862
862
|
} finally {
|
|
863
|
-
closePortServers(roster);
|
|
863
|
+
await closePortServers(roster);
|
|
864
864
|
}
|
|
865
865
|
});
|
|
866
866
|
});
|
|
@@ -191,11 +191,8 @@ describe('Roster request plugins', () => {
|
|
|
191
191
|
asyncRoster.use(async () => false);
|
|
192
192
|
asyncRoster.register('example.com', () => () => {});
|
|
193
193
|
await asyncRoster.init();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{ writeHead() {}, end() {} }
|
|
198
|
-
);
|
|
199
|
-
}, /must be synchronous/);
|
|
194
|
+
const result = invoke(asyncRoster.requestHandler(), { headers: { host: 'example.com' } });
|
|
195
|
+
assert.strictEqual(result.statusCode, 500);
|
|
196
|
+
assert.strictEqual(result.body, 'Internal Server Error');
|
|
200
197
|
});
|
|
201
198
|
});
|
|
@@ -0,0 +1,273 @@
|
|
|
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 { Writable } = require('node:stream');
|
|
10
|
+
const { execFileSync } = require('node:child_process');
|
|
11
|
+
const { createStaticHandler } = require('../lib/static-site-handler.js');
|
|
12
|
+
const Roster = require('../index.js');
|
|
13
|
+
|
|
14
|
+
function temporaryDirectory(t) {
|
|
15
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-files-'));
|
|
16
|
+
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
17
|
+
return directory;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function response() {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
const res = new Writable({ write(chunk, encoding, done) { chunks.push(chunk); done(); } });
|
|
23
|
+
res.writeHead = (status, headers) => { res.status = status; res.headers = headers; res.headersSent = true; };
|
|
24
|
+
return { res, body: () => Buffer.concat(chunks) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it('streams a static file without synchronous filesystem calls', async t => {
|
|
28
|
+
const root = temporaryDirectory(t);
|
|
29
|
+
const content = Buffer.alloc(1024 * 1024, 'a');
|
|
30
|
+
fs.writeFileSync(path.join(root, 'index.html'), content);
|
|
31
|
+
const handler = createStaticHandler(root)();
|
|
32
|
+
const result = response();
|
|
33
|
+
for (const method of ['readFileSync', 'statSync', 'existsSync']) {
|
|
34
|
+
t.mock.method(fs, method, () => { throw new Error(`Unexpected ${method}`); });
|
|
35
|
+
}
|
|
36
|
+
await handler({ method: 'GET', url: '/' }, result.res);
|
|
37
|
+
assert.equal(result.res.status, 200);
|
|
38
|
+
assert.equal(result.res.headers['Content-Length'], content.length);
|
|
39
|
+
assert.deepEqual(result.body(), content);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('serves HEAD using metadata without reading file contents', async t => {
|
|
43
|
+
const root = temporaryDirectory(t);
|
|
44
|
+
fs.writeFileSync(path.join(root, 'index.html'), 'head content');
|
|
45
|
+
const open = fs.promises.open.bind(fs.promises);
|
|
46
|
+
t.mock.method(fs.promises, 'open', async (...args) => {
|
|
47
|
+
const file = await open(...args);
|
|
48
|
+
file.createReadStream = () => { throw new Error('HEAD must not stream'); };
|
|
49
|
+
file.readFile = () => { throw new Error('HEAD must not read'); };
|
|
50
|
+
return file;
|
|
51
|
+
});
|
|
52
|
+
const result = response();
|
|
53
|
+
await createStaticHandler(root)()({ method: 'HEAD', url: '/' }, result.res);
|
|
54
|
+
assert.equal(result.res.status, 200);
|
|
55
|
+
assert.equal(result.res.headers['Content-Length'], 12);
|
|
56
|
+
assert.equal(result.body().length, 0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('handles directory indexes, missing files, and an open race', async t => {
|
|
60
|
+
const root = temporaryDirectory(t);
|
|
61
|
+
fs.mkdirSync(path.join(root, 'en'));
|
|
62
|
+
fs.writeFileSync(path.join(root, 'en', 'index.html'), 'directory');
|
|
63
|
+
const handler = createStaticHandler(root)();
|
|
64
|
+
const directory = response();
|
|
65
|
+
await handler({ method: 'GET', url: '/en/?a=b' }, directory.res);
|
|
66
|
+
assert.equal(directory.body().toString(), 'directory');
|
|
67
|
+
const missing = response();
|
|
68
|
+
await handler({ method: 'GET', url: '/missing' }, missing.res);
|
|
69
|
+
assert.equal(missing.res.status, 404);
|
|
70
|
+
t.mock.method(fs.promises, 'open', async () => { throw Object.assign(new Error('removed'), { code: 'ENOENT' }); });
|
|
71
|
+
const removed = response();
|
|
72
|
+
await handler({ method: 'GET', url: '/en/' }, removed.res);
|
|
73
|
+
assert.equal(removed.res.status, 404);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('closes static file handles when the client disconnects mid-stream', async t => {
|
|
77
|
+
const root = temporaryDirectory(t);
|
|
78
|
+
fs.writeFileSync(path.join(root, 'large.bin'), Buffer.alloc(8 * 1024 * 1024));
|
|
79
|
+
const handler = createStaticHandler(root)();
|
|
80
|
+
const handles = [];
|
|
81
|
+
const open = fs.promises.open.bind(fs.promises);
|
|
82
|
+
t.mock.method(fs.promises, 'open', async (...args) => {
|
|
83
|
+
const file = await open(...args);
|
|
84
|
+
handles.push(file);
|
|
85
|
+
return file;
|
|
86
|
+
});
|
|
87
|
+
let handled;
|
|
88
|
+
const complete = new Promise(resolve => { handled = resolve; });
|
|
89
|
+
const server = http.createServer((req, res) => {
|
|
90
|
+
handler(req, res).then(handled);
|
|
91
|
+
});
|
|
92
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
93
|
+
t.after(() => new Promise(resolve => { server.close(resolve); server.closeAllConnections(); }));
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const req = http.get({ hostname: '127.0.0.1', port: server.address().port, path: '/large.bin' }, res => {
|
|
96
|
+
res.once('data', () => { res.destroy(); resolve(); });
|
|
97
|
+
});
|
|
98
|
+
req.on('error', reject);
|
|
99
|
+
});
|
|
100
|
+
await complete;
|
|
101
|
+
assert.equal(handles.length, 1);
|
|
102
|
+
assert.equal(handles[0].fd, -1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects non-regular files before opening them', async t => {
|
|
106
|
+
const root = temporaryDirectory(t);
|
|
107
|
+
t.mock.method(fs.promises, 'stat', async () => ({ isDirectory: () => false, isFile: () => false }));
|
|
108
|
+
t.mock.method(fs.promises, 'open', () => { throw new Error('Must not open special files'); });
|
|
109
|
+
const result = response();
|
|
110
|
+
await createStaticHandler(root)()({ method: 'GET', url: '/pipe' }, result.res);
|
|
111
|
+
assert.equal(result.res.status, 404);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('maps filesystem failures to HTTP errors without exposing internal details', async t => {
|
|
115
|
+
const root = temporaryDirectory(t);
|
|
116
|
+
fs.writeFileSync(path.join(root, 'index.html'), 'content');
|
|
117
|
+
for (const method of ['stat', 'open']) {
|
|
118
|
+
for (const [code, status, body] of [
|
|
119
|
+
['ENOTDIR', 404, 'Not Found'],
|
|
120
|
+
['EACCES', 403, 'Forbidden'],
|
|
121
|
+
['EIO', 500, 'Internal Server Error']
|
|
122
|
+
]) {
|
|
123
|
+
await t.test(`${method}: ${code}`, async t => {
|
|
124
|
+
t.mock.method(fs.promises, method, async () => {
|
|
125
|
+
throw Object.assign(new Error('internal filesystem detail'), { code });
|
|
126
|
+
});
|
|
127
|
+
const result = response();
|
|
128
|
+
await createStaticHandler(root)()({ method: 'GET', url: '/' }, result.res);
|
|
129
|
+
assert.equal(result.res.status, status);
|
|
130
|
+
assert.equal(result.body().toString(), body);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('uses opened-file metadata and closes the handle when the file changes before open', async t => {
|
|
137
|
+
const root = temporaryDirectory(t);
|
|
138
|
+
fs.writeFileSync(path.join(root, 'index.html'), 'old');
|
|
139
|
+
const open = fs.promises.open.bind(fs.promises);
|
|
140
|
+
let handle;
|
|
141
|
+
t.mock.method(fs.promises, 'open', async (...args) => {
|
|
142
|
+
fs.writeFileSync(path.join(root, 'index.html'), 'replacement content');
|
|
143
|
+
handle = await open(...args);
|
|
144
|
+
return handle;
|
|
145
|
+
});
|
|
146
|
+
const result = response();
|
|
147
|
+
await createStaticHandler(root)()({ method: 'GET', url: '/' }, result.res);
|
|
148
|
+
assert.equal(result.res.headers['Content-Length'], Buffer.byteLength('replacement content'));
|
|
149
|
+
assert.equal(result.body().toString(), 'replacement content');
|
|
150
|
+
assert.equal(handle.fd, -1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('closes opened files if fstat fails or finds a non-regular replacement', async t => {
|
|
154
|
+
const root = temporaryDirectory(t);
|
|
155
|
+
fs.writeFileSync(path.join(root, 'index.html'), 'content');
|
|
156
|
+
const open = fs.promises.open.bind(fs.promises);
|
|
157
|
+
for (const fail of [false, true]) {
|
|
158
|
+
await t.test(fail ? 'fstat failure' : 'non-regular replacement', async t => {
|
|
159
|
+
let handle;
|
|
160
|
+
t.mock.method(fs.promises, 'open', async (...args) => {
|
|
161
|
+
handle = await open(...args);
|
|
162
|
+
t.mock.method(handle, 'stat', async () => {
|
|
163
|
+
if (fail) throw Object.assign(new Error('stat failed'), { code: 'EIO' });
|
|
164
|
+
return { isFile: () => false };
|
|
165
|
+
});
|
|
166
|
+
return handle;
|
|
167
|
+
});
|
|
168
|
+
const result = response();
|
|
169
|
+
await createStaticHandler(root)()({ method: 'GET', url: '/' }, result.res);
|
|
170
|
+
assert.equal(result.res.status, fail ? 500 : 404);
|
|
171
|
+
assert.equal(handle.fd, -1);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('closes a file opened after its response was already destroyed', async t => {
|
|
177
|
+
const root = temporaryDirectory(t);
|
|
178
|
+
fs.writeFileSync(path.join(root, 'index.html'), 'content');
|
|
179
|
+
const result = response();
|
|
180
|
+
const open = fs.promises.open.bind(fs.promises);
|
|
181
|
+
let handle;
|
|
182
|
+
t.mock.method(fs.promises, 'open', async (...args) => {
|
|
183
|
+
result.res.destroy();
|
|
184
|
+
handle = await open(...args);
|
|
185
|
+
return handle;
|
|
186
|
+
});
|
|
187
|
+
await createStaticHandler(root)()({ method: 'GET', url: '/' }, result.res);
|
|
188
|
+
assert.equal(result.res.headersSent, undefined);
|
|
189
|
+
assert.equal(handle.fd, -1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('reuses TLS contexts, coalesces concurrent loads, and reloads changed certificates', async t => {
|
|
193
|
+
const root = temporaryDirectory(t);
|
|
194
|
+
const directory = path.join(root, 'live', 'example.com');
|
|
195
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
196
|
+
const key = path.join(directory, 'privkey.pem');
|
|
197
|
+
const cert = path.join(directory, 'cert.pem');
|
|
198
|
+
execFileSync('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', key,
|
|
199
|
+
'-out', cert, '-subj', '/CN=example.com', '-days', '1'], { stdio: 'ignore' });
|
|
200
|
+
fs.writeFileSync(path.join(directory, 'chain.pem'), '');
|
|
201
|
+
const roster = new Roster({ local: true, wwwPath: root, greenlockStorePath: root });
|
|
202
|
+
t.after(() => roster.close());
|
|
203
|
+
const read = fs.promises.readFile.bind(fs.promises);
|
|
204
|
+
let reads = 0;
|
|
205
|
+
t.mock.method(fs.promises, 'readFile', (...args) => { reads++; return read(...args); });
|
|
206
|
+
const contexts = await Promise.all(Array.from({ length: 8 }, () => roster._resolveSecureContext('example.com')));
|
|
207
|
+
assert.ok(contexts[0]);
|
|
208
|
+
assert.ok(contexts.every(context => context === contexts[0]));
|
|
209
|
+
assert.equal(reads, 3);
|
|
210
|
+
assert.equal(await roster._resolveSecureContext('example.com'), contexts[0]);
|
|
211
|
+
assert.equal(reads, 3);
|
|
212
|
+
execFileSync('openssl', ['req', '-x509', '-key', key, '-out', cert,
|
|
213
|
+
'-subj', '/CN=example.com', '-days', '1', '-set_serial', '2'], { stdio: 'ignore' });
|
|
214
|
+
assert.notEqual(await roster._resolveSecureContext('example.com'), contexts[0]);
|
|
215
|
+
assert.equal(reads, 6);
|
|
216
|
+
fs.unlinkSync(cert);
|
|
217
|
+
assert.equal(await roster._resolveSecureContext('example.com'), null);
|
|
218
|
+
assert.equal(roster._secureContexts.size, 0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('coalesces certificate checks and prevents new issuance after close', async t => {
|
|
222
|
+
const root = temporaryDirectory(t);
|
|
223
|
+
const roster = new Roster({ local: true, wwwPath: root });
|
|
224
|
+
let calls = 0;
|
|
225
|
+
let release;
|
|
226
|
+
roster._greenlockRuntime = { get: () => { calls++; return new Promise(resolve => { release = resolve; }); } };
|
|
227
|
+
const first = roster._checkCertificate('example.com');
|
|
228
|
+
assert.equal(first, roster._checkCertificate('example.com'));
|
|
229
|
+
await Promise.resolve();
|
|
230
|
+
assert.equal(calls, 1);
|
|
231
|
+
release();
|
|
232
|
+
await first;
|
|
233
|
+
assert.equal(roster._certificateChecks.size, 0);
|
|
234
|
+
await roster.close();
|
|
235
|
+
assert.throws(() => roster._checkCertificate('example.com'), /closing or closed/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('waits for in-flight certificate work before running close hooks', async t => {
|
|
239
|
+
const root = temporaryDirectory(t);
|
|
240
|
+
const roster = new Roster({ local: true, wwwPath: root });
|
|
241
|
+
t.after(() => roster.close());
|
|
242
|
+
let cleaned = false;
|
|
243
|
+
roster.register('example.com', virtual => {
|
|
244
|
+
virtual.onClose(() => { cleaned = true; });
|
|
245
|
+
return () => {};
|
|
246
|
+
});
|
|
247
|
+
await roster.init();
|
|
248
|
+
let release;
|
|
249
|
+
roster._greenlockRuntime = { get: () => new Promise(resolve => { release = resolve; }) };
|
|
250
|
+
const check = roster._checkCertificate('example.com');
|
|
251
|
+
await Promise.resolve();
|
|
252
|
+
const closing = roster.close();
|
|
253
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
254
|
+
assert.equal(cleaned, false);
|
|
255
|
+
release();
|
|
256
|
+
await Promise.all([check, closing]);
|
|
257
|
+
assert.equal(cleaned, true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('does not repopulate TLS caches when close interrupts a context load', async t => {
|
|
261
|
+
const root = temporaryDirectory(t);
|
|
262
|
+
const roster = new Roster({ local: true, wwwPath: root });
|
|
263
|
+
t.after(() => roster.close());
|
|
264
|
+
let release;
|
|
265
|
+
const pendingStat = new Promise(resolve => { release = resolve; });
|
|
266
|
+
t.mock.method(fs.promises, 'stat', () => pendingStat);
|
|
267
|
+
const loading = assert.rejects(roster._resolveSecureContext('example.com'), /closing or closed/);
|
|
268
|
+
const closing = roster.close();
|
|
269
|
+
release({});
|
|
270
|
+
await Promise.all([loading, closing]);
|
|
271
|
+
assert.equal(roster._contextLoads.size, 0);
|
|
272
|
+
assert.equal(roster._secureContexts.size, 0);
|
|
273
|
+
});
|
|
@@ -20,7 +20,7 @@ GLE.init = function(fn) {
|
|
|
20
20
|
// See https://git.coolaj86.com/coolaj86/greenlock-express.js/issues/80
|
|
21
21
|
if (fn && false !== fn.cluster && cluster.isWorker) {
|
|
22
22
|
// ignore the init function and launch the worker
|
|
23
|
-
return require("./worker.js").create();
|
|
23
|
+
return require("./worker.js").create(fn);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
var opts;
|
|
@@ -9,12 +9,13 @@ var sni = require("./sni.js");
|
|
|
9
9
|
var cluster = require("cluster");
|
|
10
10
|
var log = require("lemonlog")("greenlock-servers");
|
|
11
11
|
|
|
12
|
-
Servers.create = function(greenlock) {
|
|
12
|
+
Servers.create = function(greenlock, onError) {
|
|
13
13
|
var servers = {};
|
|
14
14
|
var _httpServer;
|
|
15
15
|
var _httpsServer;
|
|
16
16
|
|
|
17
17
|
function startError(e) {
|
|
18
|
+
if (onError) return onError(e);
|
|
18
19
|
explainError(e);
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
@@ -8,7 +8,7 @@ var Servers = require("./servers.js");
|
|
|
8
8
|
Single.create = function(opts) {
|
|
9
9
|
var greenlock = require("./greenlock-shim.js").create(opts);
|
|
10
10
|
|
|
11
|
-
var servers = Servers.create(greenlock);
|
|
11
|
+
var servers = Servers.create(greenlock, opts.onServerError);
|
|
12
12
|
|
|
13
13
|
var single = {
|
|
14
14
|
ready: function(fn) {
|
|
@@ -5,7 +5,7 @@ var Worker = module.exports;
|
|
|
5
5
|
var messageTimeout = 30 * 1000;
|
|
6
6
|
var msgPrefix = "greenlock:";
|
|
7
7
|
|
|
8
|
-
Worker.create = function() {
|
|
8
|
+
Worker.create = function(opts) {
|
|
9
9
|
var greenlock = {};
|
|
10
10
|
["getAcmeHttp01ChallengeResponse", "get", "notify", "_notify"].forEach(function(k) {
|
|
11
11
|
greenlock[k] = function(args) {
|
|
@@ -15,7 +15,7 @@ Worker.create = function() {
|
|
|
15
15
|
|
|
16
16
|
var worker = {
|
|
17
17
|
ready: function(fn) {
|
|
18
|
-
var servers = require("./servers.js").create(greenlock);
|
|
18
|
+
var servers = require("./servers.js").create(greenlock, opts && opts.onServerError);
|
|
19
19
|
fn(servers);
|
|
20
20
|
return worker;
|
|
21
21
|
},
|