roster-server 2.4.11 → 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 +420 -128
- 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 +59 -14
- 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,12 +501,63 @@ 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
|
});
|
|
514
508
|
|
|
515
509
|
describe('Roster loadSites', () => {
|
|
510
|
+
it('keeps discovered sites on 443 when the same domain is registered on other ports', async () => {
|
|
511
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
|
|
512
|
+
const wwwPath = path.join(tmpDir, 'www');
|
|
513
|
+
const siteDir = path.join(wwwPath, 'persist.example.com');
|
|
514
|
+
fs.mkdirSync(siteDir, { recursive: true });
|
|
515
|
+
fs.writeFileSync(path.join(siteDir, 'index.html'), '<html>static-443</html>', 'utf8');
|
|
516
|
+
|
|
517
|
+
const roster = new Roster({ wwwPath, local: true, port: 8880 });
|
|
518
|
+
roster.register('persist.example.com', () => (req, res) => {
|
|
519
|
+
res.writeHead(200);
|
|
520
|
+
res.end('app-8880');
|
|
521
|
+
});
|
|
522
|
+
roster.register('persist.example.com:8882', () => (req, res) => {
|
|
523
|
+
res.writeHead(200);
|
|
524
|
+
res.end('socket-8882');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
await roster.init();
|
|
529
|
+
|
|
530
|
+
const { Writable } = require('stream');
|
|
531
|
+
const dispatch = async (port) => {
|
|
532
|
+
let statusCode;
|
|
533
|
+
let body = '';
|
|
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
|
+
});
|
|
542
|
+
roster.requestHandler(port)(
|
|
543
|
+
{ headers: { host: 'persist.example.com' }, method: 'GET', url: '/' },
|
|
544
|
+
res
|
|
545
|
+
);
|
|
546
|
+
await finished;
|
|
547
|
+
return { statusCode, body };
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
assert.deepStrictEqual(await dispatch(443), {
|
|
551
|
+
statusCode: 200,
|
|
552
|
+
body: '<html>static-443</html>'
|
|
553
|
+
});
|
|
554
|
+
assert.deepStrictEqual(await dispatch(8880), { statusCode: 200, body: 'app-8880' });
|
|
555
|
+
assert.deepStrictEqual(await dispatch(8882), { statusCode: 200, body: 'socket-8882' });
|
|
556
|
+
} finally {
|
|
557
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
516
561
|
it('loads site from www directory and registers domain + www', async () => {
|
|
517
562
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
|
|
518
563
|
const wwwPath = path.join(tmpDir, 'www');
|
|
@@ -646,7 +691,7 @@ describe('Roster loadSites', () => {
|
|
|
646
691
|
assert.strictEqual(result.statusCode, 200);
|
|
647
692
|
assert.ok(result.body.includes('static ok'));
|
|
648
693
|
} finally {
|
|
649
|
-
closePortServers(roster);
|
|
694
|
+
await closePortServers(roster);
|
|
650
695
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
651
696
|
}
|
|
652
697
|
});
|
|
@@ -666,7 +711,7 @@ describe('Roster loadSites', () => {
|
|
|
666
711
|
const result = await httpGet('localhost', port, '/nonexistent.html');
|
|
667
712
|
assert.strictEqual(result.statusCode, 404);
|
|
668
713
|
} finally {
|
|
669
|
-
closePortServers(roster);
|
|
714
|
+
await closePortServers(roster);
|
|
670
715
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
671
716
|
}
|
|
672
717
|
});
|
|
@@ -693,7 +738,7 @@ describe('Roster loadSites', () => {
|
|
|
693
738
|
assert.strictEqual(resultNoSlash.statusCode, 200);
|
|
694
739
|
assert.ok(resultNoSlash.body.includes('en page'));
|
|
695
740
|
} finally {
|
|
696
|
-
closePortServers(roster);
|
|
741
|
+
await closePortServers(roster);
|
|
697
742
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
698
743
|
}
|
|
699
744
|
});
|
|
@@ -815,7 +860,7 @@ describe('Roster init() (cluster-friendly API)', () => {
|
|
|
815
860
|
assert.strictEqual(result.statusCode, 200);
|
|
816
861
|
assert.strictEqual(result.body, 'after-init');
|
|
817
862
|
} finally {
|
|
818
|
-
closePortServers(roster);
|
|
863
|
+
await closePortServers(roster);
|
|
819
864
|
}
|
|
820
865
|
});
|
|
821
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
|
},
|