roster-server 3.3.0 → 3.4.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 +5 -1
- package/lib/resolve-site-app.js +2 -2
- package/lib/site-worker.js +6 -0
- package/package.json +1 -1
- package/test/isolation.test.js +35 -10
package/README.md
CHANGED
|
@@ -191,6 +191,10 @@ A site directory whose entry exports an object with a `connection(socket, head)`
|
|
|
191
191
|
```javascript
|
|
192
192
|
// www/game.example.com/index.js
|
|
193
193
|
export default {
|
|
194
|
+
async start({ certificate }) {
|
|
195
|
+
// Runs once before the first connection, e.g. to fork workers.
|
|
196
|
+
// certificate(servername) resolves { key, cert } PEM strings; cert includes the chain.
|
|
197
|
+
},
|
|
194
198
|
connection(socket, head) {
|
|
195
199
|
// Terminate TLS here, or pass socket and head to another process.
|
|
196
200
|
},
|
|
@@ -202,7 +206,7 @@ export default {
|
|
|
202
206
|
|
|
203
207
|
In production, `socket` is the client's TCP connection and `head` holds the TLS ClientHello that Roster already read; put it back with `socket.unshift(head)` before terminating TLS, and do not `resume()` the socket after wrapping it. In local mode, the site's assigned port accepts plain TCP connections, `socket` arrives paused, and `head` is empty. `connection()` runs synchronously; if it throws, Roster destroys the socket.
|
|
204
208
|
|
|
205
|
-
Roster
|
|
209
|
+
`certificate(servername)` returns the certificate Roster would serve for that host, with the same wildcard and zone matching. A missing certificate is issued by the main process first, as for any isolated site. Roster also renews it; call `certificate()` again to pick up a renewal, for example on an interval or when a cached copy gets old. It is not available in local mode. Request plugins, `www.` redirects, and `onClose` hooks do not apply: the site owns every connection it receives. A socket site must run isolated; Roster rejects it when it would be served by the main process.
|
|
206
210
|
|
|
207
211
|
## Startup and shutdown
|
|
208
212
|
|
package/lib/resolve-site-app.js
CHANGED
|
@@ -42,8 +42,8 @@ async function resolveSiteApp(domainPath, options = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* A socket site exports `{ connection(socket, head), close?() }` and serves
|
|
46
|
-
* connections itself, including TLS. Only an isolated site process can hand it sockets.
|
|
45
|
+
* A socket site exports `{ start?(context), connection(socket, head), close?() }` and serves
|
|
46
|
+
* raw client connections itself, including TLS. Only an isolated site process can hand it sockets.
|
|
47
47
|
*/
|
|
48
48
|
function isSocketSite(siteApp) {
|
|
49
49
|
return typeof siteApp === 'object' && siteApp !== null && typeof siteApp.connection === 'function';
|
package/lib/site-worker.js
CHANGED
|
@@ -45,6 +45,11 @@ class SiteWorkerRoster extends Roster {
|
|
|
45
45
|
return this._socketSite !== null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// The site terminates TLS with certificates Roster resolves: read from the store, or issued by the main process.
|
|
49
|
+
startSocketSite() {
|
|
50
|
+
return this._socketSite.start?.({ certificate: servername => this.ensureCertificate(servername) });
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
// In local mode a socket site gets plain connections on its assigned ports, with no ClientHello.
|
|
49
54
|
async serveLocalSockets() {
|
|
50
55
|
const head = Buffer.alloc(0);
|
|
@@ -167,6 +172,7 @@ async function start(site) {
|
|
|
167
172
|
}
|
|
168
173
|
await roster.init();
|
|
169
174
|
if (roster.socketSite) {
|
|
175
|
+
await roster.startSocketSite();
|
|
170
176
|
if (roster.local) await roster.serveLocalSockets();
|
|
171
177
|
} else if (roster.local) {
|
|
172
178
|
await roster.start();
|
package/package.json
CHANGED
package/test/isolation.test.js
CHANGED
|
@@ -405,11 +405,11 @@ it('closes isolated sites when the main process dies without closing', { timeout
|
|
|
405
405
|
assert.equal(fs.readFileSync(marker, 'utf8'), 'closed');
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
-
function socketSite(server, marker) {
|
|
408
|
+
function socketSite(server, marker, start = 'async start() {}') {
|
|
409
409
|
return `const fs = require('fs');
|
|
410
|
-
|
|
411
|
-
server.on('request', (req, res) => res.end(JSON.stringify({ pid: process.pid, address: req.socket.remoteAddress })));
|
|
410
|
+
let server = ${server};
|
|
412
411
|
module.exports = {
|
|
412
|
+
${start},
|
|
413
413
|
connection(socket, head) {
|
|
414
414
|
fs.writeFileSync(${JSON.stringify(marker)} + '.head', String(head.length));
|
|
415
415
|
// A ClientHello read by Roster goes back on the stream; a local connection arrives paused.
|
|
@@ -417,6 +417,9 @@ module.exports = {
|
|
|
417
417
|
server.emit('connection', socket);
|
|
418
418
|
if (head.length === 0) socket.resume();
|
|
419
419
|
},
|
|
420
|
+
reply(req, res) {
|
|
421
|
+
res.end(JSON.stringify({ pid: process.pid, address: req.socket.remoteAddress }));
|
|
422
|
+
},
|
|
420
423
|
close() {
|
|
421
424
|
fs.writeFileSync(${JSON.stringify(marker)}, 'closed');
|
|
422
425
|
}
|
|
@@ -426,14 +429,15 @@ module.exports = {
|
|
|
426
429
|
it('hands raw client sockets with their ClientHello to a socket site that terminates TLS', { timeout: 20000 }, async t => {
|
|
427
430
|
const marker = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'roster-socket-site-')), 'closed');
|
|
428
431
|
t.after(() => fs.rmSync(path.dirname(marker), { recursive: true, force: true }));
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
432
|
+
// The site gets its certificate from Roster's store through start(), not from files it reads.
|
|
433
|
+
const start = `async start({ certificate }) {
|
|
434
|
+
const pems = await certificate('raw.example.com');
|
|
435
|
+
server = require('https').createServer(pems, (req, res) => module.exports.reply(req, res));
|
|
436
|
+
}`;
|
|
433
437
|
const { roster, port } = await startProduction(t, {
|
|
434
|
-
'raw.example.com': socketSite(
|
|
438
|
+
'raw.example.com': socketSite('null', marker, start),
|
|
435
439
|
'plain.example.com': REPORT_SITE
|
|
436
|
-
}, { certificates: ['plain.example.com'] });
|
|
440
|
+
}, { certificates: ['plain.example.com', 'raw.example.com'] });
|
|
437
441
|
const raw = JSON.parse((await get(port, 'raw.example.com')).body);
|
|
438
442
|
const plain = JSON.parse((await get(port, 'plain.example.com')).body);
|
|
439
443
|
assert.notEqual(raw.pid, process.pid);
|
|
@@ -445,10 +449,31 @@ it('hands raw client sockets with their ClientHello to a socket site that termin
|
|
|
445
449
|
assert.equal(isRunning(raw.pid), false);
|
|
446
450
|
});
|
|
447
451
|
|
|
452
|
+
it('issues a missing certificate through the main process when a socket site asks for it', { timeout: 20000 }, async t => {
|
|
453
|
+
const marker = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'roster-socket-site-')), 'closed');
|
|
454
|
+
t.after(() => fs.rmSync(path.dirname(marker), { recursive: true, force: true }));
|
|
455
|
+
const store = path.join(path.dirname(marker), 'greenlock.d');
|
|
456
|
+
const issued = [];
|
|
457
|
+
const start = `async start({ certificate }) {
|
|
458
|
+
server = require('https').createServer(await certificate('fresh.example.com'), (req, res) => module.exports.reply(req, res));
|
|
459
|
+
}`;
|
|
460
|
+
const { port } = await startProduction(t, { 'fresh.example.com': socketSite('null', marker, start) }, {
|
|
461
|
+
options: { greenlockStorePath: store },
|
|
462
|
+
issue: async ({ servername }) => {
|
|
463
|
+
issued.push(servername);
|
|
464
|
+
installCertificate(store, servername);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
assert.deepEqual(issued, ['fresh.example.com']);
|
|
468
|
+
assert.notEqual(JSON.parse((await get(port, 'fresh.example.com')).body).pid, process.pid);
|
|
469
|
+
});
|
|
470
|
+
|
|
448
471
|
it('gives a socket site plain connections on its local port', { timeout: 20000 }, async t => {
|
|
449
472
|
const marker = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'roster-socket-site-')), 'closed');
|
|
450
473
|
t.after(() => fs.rmSync(path.dirname(marker), { recursive: true, force: true }));
|
|
451
|
-
const { wwwPath } = createWww(t, {
|
|
474
|
+
const { wwwPath } = createWww(t, {
|
|
475
|
+
'raw.example': socketSite(`require('http').createServer((req, res) => module.exports.reply(req, res))`, marker)
|
|
476
|
+
});
|
|
452
477
|
const roster = new Roster({ local: true, wwwPath, minLocalPort: 19370, maxLocalPort: 19379 });
|
|
453
478
|
t.after(() => roster.close().catch(() => {}));
|
|
454
479
|
await roster.start();
|