roster-server
Advanced tools
| const cluster = require('cluster'); | ||
| const os = require('os'); | ||
| const path = require('path'); | ||
| const https = require('https'); | ||
| const Roster = require('../index.js'); | ||
@@ -9,13 +8,13 @@ | ||
| const CONFIG = { | ||
| domain: 'chinchon.blyts.com', | ||
| domain: 'example.com', | ||
| httpsPort: 4336, | ||
| workers: Math.max(1, Math.min(2, os.cpus().length)), | ||
| certificateManagerHttpsPort: 0, // 0 = ephemeral, manager does not serve public traffic | ||
| wwwPath: path.join(__dirname, 'www'), | ||
| greenlockStorePath: path.join(__dirname, '..', 'greenlock.d'), | ||
| email: 'mclasen@blyts.com', | ||
| staging: false, | ||
| autoCertificates: true | ||
| email: 'mclasen@example.com', | ||
| staging: false | ||
| }; | ||
| async function startWorker() { | ||
| function createRoster({ isCertificateManager }) { | ||
| const roster = new Roster({ | ||
@@ -27,5 +26,12 @@ local: false, | ||
| greenlockStorePath: CONFIG.greenlockStorePath, | ||
| autoCertificates: CONFIG.autoCertificates | ||
| autoCertificates: isCertificateManager, | ||
| // Certificate manager can bind an ephemeral HTTPS port; workers serve real traffic. | ||
| port: isCertificateManager ? CONFIG.certificateManagerHttpsPort : 443 | ||
| }); | ||
| return roster; | ||
| } | ||
| async function startWorker() { | ||
| const roster = createRoster({ isCertificateManager: false }); | ||
| // Domain is configured from CONFIG (single source of truth). | ||
@@ -40,15 +46,6 @@ roster.register(CONFIG.domain, () => { | ||
| await roster.init(); | ||
| // Automatic cert issuance by roster-server (and renewal loop) in cluster-friendly mode. | ||
| const defaultPems = await roster.ensureCertificate(CONFIG.domain); | ||
| const server = https.createServer({ | ||
| key: defaultPems.key, | ||
| cert: defaultPems.cert, | ||
| SNICallback: roster.sniCallback(), | ||
| minVersion: 'TLSv1.2', | ||
| maxVersion: 'TLSv1.3' | ||
| const server = await roster.createServingHttpsServer({ | ||
| servername: CONFIG.domain | ||
| }); | ||
| roster.attach(server); | ||
| server.listen(CONFIG.httpsPort, () => { | ||
@@ -64,4 +61,18 @@ console.log(`[worker ${process.pid}] listening on https://0.0.0.0:${CONFIG.httpsPort} for domain ${CONFIG.domain}`); | ||
| console.log(`greenlockStorePath=${CONFIG.greenlockStorePath}\n`); | ||
| console.log(`[primary] cert lifecycle managed by roster-server (autoCertificates=${CONFIG.autoCertificates})\n`); | ||
| console.log('[primary] cert lifecycle managed by roster-server\n'); | ||
| // Primary is the single certificate manager to avoid ACME race conditions. | ||
| // It starts Roster standalone lifecycle so ACME http-01 challenge server (:80) is active. | ||
| const certificateManager = createRoster({ isCertificateManager: true }); | ||
| certificateManager.register(CONFIG.domain, () => { | ||
| return (req, res) => { | ||
| res.writeHead(200); | ||
| res.end('certificate-manager'); | ||
| }; | ||
| }); | ||
| await certificateManager.start(); | ||
| await certificateManager.ensureCertificate(CONFIG.domain); | ||
| const subject = CONFIG.domain; | ||
| console.log(`[primary] certificate ready for ${CONFIG.domain} (subject=${subject})\n`); | ||
| for (let i = 0; i < CONFIG.workers; i++) { | ||
@@ -68,0 +79,0 @@ cluster.fork(); |
+58
-0
@@ -938,2 +938,20 @@ const fs = require('fs'); | ||
| loadCertificate(servername) { | ||
| if (this.local) { | ||
| throw new Error('loadCertificate() is not available in local mode'); | ||
| } | ||
| if (!this._initialized) { | ||
| throw new Error('Call init() before loadCertificate()'); | ||
| } | ||
| const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase(); | ||
| if (!normalizedServername) { | ||
| throw new Error('servername is required'); | ||
| } | ||
| const pems = this._resolvePemsForServername(normalizedServername); | ||
| if (!pems) { | ||
| throw new Error(`No certificate files available for ${normalizedServername}`); | ||
| } | ||
| return pems; | ||
| } | ||
| async init() { | ||
@@ -995,2 +1013,42 @@ if (this._initialized) return this; | ||
| async createManagedHttpsServer(options = {}) { | ||
| if (this.local) throw new Error('createManagedHttpsServer() is not available in local mode'); | ||
| if (!this._initialized) throw new Error('Call init() before createManagedHttpsServer()'); | ||
| const { | ||
| servername, | ||
| port, | ||
| ensureCertificate = true, | ||
| tlsOptions = {} | ||
| } = options; | ||
| const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase(); | ||
| if (!normalizedServername) { | ||
| throw new Error('servername is required'); | ||
| } | ||
| const pems = ensureCertificate | ||
| ? await this.ensureCertificate(normalizedServername) | ||
| : this.loadCertificate(normalizedServername); | ||
| const server = https.createServer({ | ||
| minVersion: this.tlsMinVersion, | ||
| maxVersion: this.tlsMaxVersion, | ||
| ...tlsOptions, | ||
| key: pems.key, | ||
| cert: pems.cert, | ||
| SNICallback: this.sniCallback() | ||
| }); | ||
| this.attach(server, { port }); | ||
| return server; | ||
| } | ||
| async createServingHttpsServer(options = {}) { | ||
| return this.createManagedHttpsServer({ | ||
| ...options, | ||
| ensureCertificate: false | ||
| }); | ||
| } | ||
| startLocalMode() { | ||
@@ -997,0 +1055,0 @@ this.domainPorts = {}; |
+1
-1
| { | ||
| "name": "roster-server", | ||
| "version": "2.4.4", | ||
| "version": "2.4.6", | ||
| "description": "š¾ RosterServer - A domain host router to host multiple HTTPS.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
+46
-0
@@ -389,2 +389,32 @@ # š¾ RosterServer | ||
| ### Production Pattern: Single Certificate Manager + Workers | ||
| For robust ACME behavior with cluster runtimes, run a single certificate manager process (primary) and keep workers in serving-only mode. This avoids challenge race conditions while keeping certificate lifecycle automatic. | ||
| ```javascript | ||
| // primary | ||
| const certManager = new Roster({ | ||
| email: 'admin@example.com', | ||
| greenlockStorePath: '/srv/greenlock.d', | ||
| wwwPath: '/srv/www' | ||
| }); | ||
| certManager.register('example.com', () => (req, res) => res.end('manager')); | ||
| await certManager.start(); // enables ACME challenge lifecycle | ||
| await certManager.ensureCertificate('example.com'); | ||
| // worker | ||
| const workerRoster = new Roster({ | ||
| email: 'admin@example.com', | ||
| greenlockStorePath: '/srv/greenlock.d', | ||
| wwwPath: '/srv/www', | ||
| autoCertificates: false | ||
| }); | ||
| workerRoster.register('example.com', () => (req, res) => res.end('worker')); | ||
| await workerRoster.init(); | ||
| const server = await workerRoster.createServingHttpsServer({ servername: 'example.com' }); | ||
| server.listen(4336); | ||
| ``` | ||
| Reference implementation: `demo/https-cluster-configurable.js`. | ||
| ### API Reference | ||
@@ -408,2 +438,18 @@ | ||
| #### `roster.ensureCertificate(servername)` ā `Promise<{ key, cert }>` | ||
| Ensures a certificate exists for `servername`. With `autoCertificates` enabled (default), it issues missing certificates automatically and returns PEMs. | ||
| #### `roster.loadCertificate(servername)` ā `{ key, cert }` | ||
| Loads an existing certificate from `greenlockStorePath` without issuing new certificates. Useful for serving-only workers. | ||
| #### `roster.createManagedHttpsServer({ servername, port?, ensureCertificate?, tlsOptions? })` ā `Promise<https.Server>` | ||
| Creates an HTTPS server prewired with default cert, SNI callback, and request/upgrade routing. By default it ensures certificate issuance before returning. | ||
| #### `roster.createServingHttpsServer({ servername, port?, tlsOptions? })` ā `Promise<https.Server>` | ||
| Convenience alias for serving-only workers. Equivalent to `createManagedHttpsServer(..., ensureCertificate: false)`. | ||
| #### `roster.attach(server, { port }?)` ā `Roster` | ||
@@ -410,0 +456,0 @@ |
@@ -150,2 +150,27 @@ --- | ||
| ### Pattern 7: Cluster Production (single cert manager + workers) | ||
| ```javascript | ||
| // PRIMARY: certificate manager (single process) | ||
| const manager = new Roster({ | ||
| email: 'admin@example.com', | ||
| greenlockStorePath: '/srv/greenlock.d', | ||
| wwwPath: '/srv/www' | ||
| }); | ||
| manager.register('example.com', () => (req, res) => res.end('manager')); | ||
| await manager.start(); | ||
| await manager.ensureCertificate('example.com'); | ||
| // WORKER: serving-only process | ||
| const worker = new Roster({ | ||
| email: 'admin@example.com', | ||
| greenlockStorePath: '/srv/greenlock.d', | ||
| wwwPath: '/srv/www', | ||
| autoCertificates: false | ||
| }); | ||
| worker.register('example.com', () => (req, res) => res.end('worker')); | ||
| await worker.init(); | ||
| const httpsServer = await worker.createServingHttpsServer({ servername: 'example.com' }); | ||
| httpsServer.listen(4336); | ||
| ``` | ||
| ## Key Configuration Options | ||
@@ -198,2 +223,11 @@ | ||
| ### `roster.loadCertificate(servername)` | ||
| Loads existing `{ key, cert }` from `greenlockStorePath` without issuing new certificates. | ||
| ### `roster.createManagedHttpsServer(options)` | ||
| Creates a pre-wired `https.Server` with default cert, SNI callback, and attached request/upgrade handlers. | ||
| ### `roster.createServingHttpsServer(options)` | ||
| Serving-only helper for worker processes. Same as `createManagedHttpsServer(..., ensureCertificate: false)`. | ||
| ### `roster.attach(server, { port }?)` | ||
@@ -200,0 +234,0 @@ Convenience: wires `requestHandler` + `upgradeHandler` onto an external `http.Server` or `https.Server`. Returns `this`. Requires `init()` first. |
@@ -936,2 +936,56 @@ 'use strict'; | ||
| describe('Roster loadCertificate()', () => { | ||
| it('throws if called before init()', () => { | ||
| const roster = new Roster({ local: false }); | ||
| assert.throws(() => roster.loadCertificate('example.com'), /Call init\(\) before loadCertificate/); | ||
| }); | ||
| it('throws in local mode', async () => { | ||
| const roster = new Roster({ local: true }); | ||
| roster.register('local-load.example', () => () => {}); | ||
| await roster.init(); | ||
| assert.throws(() => roster.loadCertificate('local-load.example'), /not available in local mode/); | ||
| }); | ||
| }); | ||
| describe('Roster createManagedHttpsServer()', () => { | ||
| it('throws if called before init()', async () => { | ||
| const roster = new Roster({ local: false }); | ||
| await assert.rejects( | ||
| () => roster.createManagedHttpsServer({ servername: 'example.com' }), | ||
| /Call init\(\) before createManagedHttpsServer/ | ||
| ); | ||
| }); | ||
| it('throws in local mode', async () => { | ||
| const roster = new Roster({ local: true }); | ||
| roster.register('local-managed.example', () => () => {}); | ||
| await roster.init(); | ||
| await assert.rejects( | ||
| () => roster.createManagedHttpsServer({ servername: 'local-managed.example' }), | ||
| /not available in local mode/ | ||
| ); | ||
| }); | ||
| }); | ||
| describe('Roster createServingHttpsServer()', () => { | ||
| it('throws if called before init()', async () => { | ||
| const roster = new Roster({ local: false }); | ||
| await assert.rejects( | ||
| () => roster.createServingHttpsServer({ servername: 'example.com' }), | ||
| /Call init\(\) before createManagedHttpsServer/ | ||
| ); | ||
| }); | ||
| it('throws in local mode', async () => { | ||
| const roster = new Roster({ local: true }); | ||
| roster.register('local-serving.example', () => () => {}); | ||
| await roster.init(); | ||
| await assert.rejects( | ||
| () => roster.createServingHttpsServer({ servername: 'local-serving.example' }), | ||
| /not available in local mode/ | ||
| ); | ||
| }); | ||
| }); | ||
| describe('Roster attach()', () => { | ||
@@ -938,0 +992,0 @@ it('throws if called before init()', () => { |
| const http = require('http'); | ||
| const Roster = require('../index.js'); | ||
| async function main() { | ||
| const roster = new Roster({ | ||
| local: true, | ||
| minLocalPort: 19500, | ||
| maxLocalPort: 19550 | ||
| }); | ||
| roster.register('example.com', () => { | ||
| return (req, res) => { | ||
| res.writeHead(200, { 'Content-Type': 'text/plain' }); | ||
| res.end('[example.com] hello from external worker wiring'); | ||
| }; | ||
| }); | ||
| roster.register('api.example.com:9000', () => { | ||
| return (req, res) => { | ||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify({ ok: true, source: 'api.example.com:9000' })); | ||
| }; | ||
| }); | ||
| // init() prepares virtual-host routing, but does not create/listen sockets. | ||
| await roster.init(); | ||
| // This server represents your external runtime-owned worker server. | ||
| const server443 = http.createServer(); | ||
| roster.attach(server443); // defaultPort routes (443) | ||
| const server9000 = http.createServer(); | ||
| roster.attach(server9000, { port: 9000 }); // custom port routes | ||
| await new Promise((resolve, reject) => { | ||
| server443.listen(19501, 'localhost', resolve); | ||
| server443.on('error', reject); | ||
| }); | ||
| await new Promise((resolve, reject) => { | ||
| server9000.listen(19502, 'localhost', resolve); | ||
| server9000.on('error', reject); | ||
| }); | ||
| console.log('\nā Cluster-friendly worker demo running'); | ||
| console.log('Roster did not bind ports itself. External servers own listen().\n'); | ||
| console.log('Try requests with Host headers:'); | ||
| console.log('curl -H "Host: example.com" http://localhost:19501/'); | ||
| console.log('curl -H "Host: api.example.com" http://localhost:19502/\n'); | ||
| // Sticky-session runtimes normally pass accepted sockets into the worker: | ||
| // process.on('message', (msg, socket) => { | ||
| // if (msg === 'sticky-session:connection') server443.emit('connection', socket); | ||
| // }); | ||
| } | ||
| main().catch((err) => { | ||
| console.error('Demo failed:', err); | ||
| process.exit(1); | ||
| }); |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
604040
0.98%10258
0.59%496
10.22%26
-7.14%110
-0.9%