šŸŽ© You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP →
Sign In

roster-server

Package Overview
Dependencies
Maintainers
1
Versions
84
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

roster-server - npm Package Compare versions

Comparing version
2.4.4
to
2.4.6
+30
-19
demo/https-cluster-configurable.js
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();

@@ -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 = {};

{
"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",

@@ -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);
});