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 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 keeps issuing and renewing the site's certificates under `greenlockStorePath/live/<domain>`, but the site loads them and must pick up renewed files itself. 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.
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
 
@@ -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 raw client
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';
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- const server = ${server};
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
- const tlsServer = `require('https').createServer({
430
- key: fs.readFileSync(${JSON.stringify(path.join(pki, 'key.pem'))}),
431
- cert: fs.readFileSync(${JSON.stringify(path.join(pki, 'cert.pem'))})
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(tlsServer, marker),
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, { 'raw.example': socketSite(`require('http').createServer()`, marker) });
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();