UNPKG

roster-server

Version:

πŸ‘Ύ RosterServer - A domain host router to host multiple HTTPS.

353 lines (279 loc) β€’ 12.7 kB
--- name: roster-server description: Virtual hosting for multiple HTTPS sites with Let's Encrypt SSL automation. Each domain gets isolated VirtualServer instance, supports Express/Socket.IO/custom handlers, static sites (index.html only, no Node), local HTTP dev mode with CRC32-based ports, automatic www redirects, and SNI certificate management. Static site logic is modular (lib/static-site-handler.js, lib/resolve-site-app.js). --- ## Quick Setup ### Production ```javascript const Roster = require('roster-server'); const roster = new Roster({ email: 'admin@example.com', wwwPath: '/srv/www', greenlockStorePath: '/srv/greenlock.d', local: true }); roster.start(); ``` ### Local Development ```javascript const roster = new Roster({ local: true, // HTTP mode, no SSL wwwPath: './www' }); roster.start().then(() => { console.log('example.com:', roster.getUrl('example.com')); // β†’ http://localhost:9465 (deterministic CRC32-based port) }); ``` ## Directory Structure ``` project/ β”œβ”€β”€ greenlock.d/ # SSL certificates (auto-generated) β”œβ”€β”€ www/ β”‚ β”œβ”€β”€ example.com/ β”‚ β”‚ └── index.js # Handler for example.com β”‚ β”œβ”€β”€ api.example.com/ β”‚ β”‚ └── index.js # Handler for subdomain β”‚ β”œβ”€β”€ static-site.com/ # Static site (no index.js) β”‚ β”‚ β”œβ”€β”€ index.html β”‚ β”‚ β”œβ”€β”€ css/ β”‚ β”‚ └── images/ β”‚ └── *.example.com/ β”‚ └── index.js # Wildcard: one handler for all subdomains └── server.js # Your setup ``` **Site resolution**: For each domain folder, RosterServer looks for `index.js` / `index.mjs` / `index.cjs` first. If none exist but `index.html` exists, it serves the folder as a static site (modular handler in `lib/static-site-handler.js`). Node app takes precedence when both exist. ## Handler Patterns **Node app**: Each `www/{domain}/index.js` (or `.mjs`/`.cjs`) must export a function that receives `httpsServer` and returns a request handler. **Static site**: If the domain folder has no index script but has `index.html`, RosterServer serves the folder as static files (`GET /` β†’ `index.html`, other paths β†’ file or 404, path-traversal protected). No code required. ### Pattern 1: Basic HTTP Handler ```javascript module.exports = (httpsServer) => { return (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello World'); }; }; ``` ### Pattern 2: Express App ```javascript const express = require('express'); module.exports = (httpsServer) => { const app = express(); app.get('/', (req, res) => res.send('Hello')); app.post('/api/data', (req, res) => res.json({ ok: true })); return app; }; ``` ### Pattern 3: Socket.IO ```javascript const { Server } = require('socket.io'); module.exports = (httpsServer) => { const io = new Server(httpsServer); io.on('connection', (socket) => { socket.on('message', (data) => io.emit('message', data)); }); return (req, res) => { if (req.url && req.url.startsWith(io.opts.path)) return; res.writeHead(200); res.end('Socket.IO running'); }; }; ``` ### Pattern 4: Manual Registration ```javascript // In server.js, before roster.start() roster.register('example.com', (httpsServer) => { return (req, res) => { res.writeHead(200); res.end('Manual handler'); }; }); // With custom port roster.register('api.example.com:8443', handler); // Wildcard: one handler for all subdomains (default port or custom) roster.register('*.example.com', handler); roster.register('*.example.com:8080', handler); ``` ### Pattern 5: Static Site (no code) Place only `index.html` (and assets) in `www/example.com/`. No `index.js` needed. RosterServer serves files with path-traversal protection; `/` β†’ `index.html`, other paths β†’ file or 404. Implemented in `lib/static-site-handler.js` and `lib/resolve-site-app.js`. ### Pattern 6: Cluster-Friendly (external server) ```javascript const https = require('https'); const Roster = require('roster-server'); const roster = new Roster({ email: 'admin@example.com', wwwPath: '/srv/www', greenlockStorePath: '/srv/greenlock.d' }); await roster.init(); const server = https.createServer({ SNICallback: roster.sniCallback() }); roster.attach(server); // Master passes connections β€” worker never calls listen() process.on('message', (msg, connection) => { if (msg === 'sticky-session:connection') { server.emit('connection', connection); } }); ``` ### 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 ```javascript new Roster({ email: 'admin@example.com', // Required for SSL wwwPath: '/srv/www', // Site handlers directory greenlockStorePath: '/srv/greenlock.d', // SSL storage dnsChallenge: { ... }, // Optional override. Default is local/manual DNS-01 (acme-dns-01-cli) // Environment local: false, // true = HTTP, false = HTTPS staging: false, // true = Let's Encrypt staging // Server hostname: '::', port: 443, // Default HTTPS port (NOT 80!) // Local mode minLocalPort: 4000, maxLocalPort: 9999, // Advanced filename: 'index', // Handler filename (no extension) basePath: '/srv' // Base for relative paths }) ``` ## Core API ### `roster.start()` Loads sites, generates SSL config, starts servers. Returns `Promise<void>`. Calls `init()` internally. ### `roster.init()` Loads sites, creates VirtualServers, prepares dispatchers β€” but creates **no servers** and calls **no `.listen()`**. Returns `Promise<Roster>`. Idempotent. Use this for cluster-friendly integration where an external manager owns the socket. ### `roster.requestHandler(port?)` Returns `(req, res) => void` dispatcher for a port (defaults to `defaultPort`). Requires `init()` first. Handles Host-header routing, wwwβ†’non-www redirects, wildcard matching. ### `roster.upgradeHandler(port?)` Returns `(req, socket, head) => void` for WebSocket upgrade routing. Requires `init()` first. ### `roster.sniCallback()` Returns `(servername, callback) => void` TLS SNI callback that resolves certs from `greenlockStorePath`. With `autoCertificates` enabled (default), it can issue missing certs automatically. Production mode only. Requires `init()` first. ### `roster.ensureCertificate(servername)` Forces certificate availability for a domain and returns `{ key, cert }`. With `autoCertificates` enabled (default), it issues certs automatically when missing. ### `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 }?)` Convenience: wires `requestHandler` + `upgradeHandler` onto an external `http.Server` or `https.Server`. Returns `this`. Requires `init()` first. ### `roster.register(domain, handler)` Manually register a domain handler. Domain can include port: `'api.com:8443'`. For wildcards use `'*.example.com'` or `'*.example.com:8080'`. ### `roster.getUrl(domain)` Get environment-aware URL: - Local mode: `http://localhost:{port}` - Production: `https://{domain}` or `https://{domain}:{port}` - Returns `null` if domain not registered. Supports wildcard-matched hosts (e.g. `getUrl('api.example.com')` when `*.example.com` is registered). ## How It Works ### Request Flow 1. Request arrives β†’ Dispatcher extracts `Host` header 2. Strips `www.` prefix (301 redirect if present) 3. Looks up domain β†’ Gets `VirtualServer` instance 4. Routes to handler via `virtualServer.processRequest(req, res)` ### VirtualServer Architecture Each domain gets isolated server instance that simulates `http.Server`: - Captures `request` and `upgrade` event listeners - Complete separation between domains - No configuration conflicts between apps ### Port Assignment **Production**: Default 443, custom via `domain:port` syntax **Local**: CRC32 hash of domain β†’ deterministic port in range 4000-9999 **Reserved**: Port 80 for ACME challenges only ### SSL Management - Automatic Let's Encrypt certificate generation - Auto-renewal 45 days before expiration - SNI support for multiple domains - Custom ports reuse certificates via SNI callback - **Wildcard** (`*.example.com`): use folder `www/*.example.com/` or `roster.register('*.example.com', handler)`. Default DNS-01 plugin is local/manual `acme-dns-01-cli`; set `dnsChallenge` only when overriding provider integration. ## Common Issues & Solutions **Port 443 in use**: Use different port `{ port: 8443 }` **Certificate failed**: Check firewall (ports 80, 443), verify DNS, try `staging: true` **Site not found**: Verify directory name matches domain. For Node: check `index.js` exports function. For static: ensure `index.html` exists (no index script). **Local port conflict**: Adjust `minLocalPort`/`maxLocalPort` range **Socket.IO not working**: Ensure handler checks `io.opts.path` and returns properly ## Best Practices 1. **Test with staging first**: `staging: true` to avoid Let's Encrypt rate limits 2. **Use local mode for dev**: `local: true` for faster iteration 3. **Environment variables**: Configure via `process.env` for portability 4. **Error handling**: Wrap handlers with try/catch, don't expose internals 5. **Socket.IO paths**: Always check `req.url.startsWith(io.opts.path)` in returned handler 6. **Port 80**: Never use as HTTPS port (reserved for ACME) ## Quick Examples ### Full Production Setup ```javascript const Roster = require('roster-server'); const roster = new Roster({ email: process.env.ADMIN_EMAIL, wwwPath: '/srv/www', greenlockStorePath: '/srv/greenlock.d', staging: process.env.NODE_ENV !== 'production' }); roster.start().then(() => { console.log('RosterServer running'); }).catch(err => { console.error('Startup failed:', err); process.exit(1); }); ``` ### Local Dev with Manual Registration ```javascript const roster = new Roster({ local: true, wwwPath: './www' }); roster.register('test.local', (server) => { return (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', url: roster.getUrl('test.local') })); }; }); roster.start(); ``` ### Environment-Aware Configuration ```javascript const isProduction = process.env.NODE_ENV === 'production'; const roster = new Roster({ email: process.env.ADMIN_EMAIL || 'admin@example.com', wwwPath: process.env.WWW_PATH || './www', greenlockStorePath: process.env.SSL_PATH || './greenlock.d', local: !isProduction, staging: !isProduction, minLocalPort: parseInt(process.env.MIN_PORT) || 4000, maxLocalPort: parseInt(process.env.MAX_PORT) || 9999 }); roster.start(); ``` ## Implementation Checklist When implementing RosterServer: - [ ] Create `www/` directory structure with domain folders - [ ] Each domain has either `index.js` (or `.mjs`/`.cjs`) exporting `(httpsServer) => handler`, or `index.html` (and assets) for a static site - [ ] Configure email for Let's Encrypt notifications - [ ] Test with `local: true` first - [ ] Test with `staging: true` before production - [ ] Ensure ports 80 and 443 are open (production) - [ ] Verify DNS points to server - [ ] Never use port 80 as HTTPS port - [ ] Use `roster.getUrl(domain)` for environment-aware URLs - [ ] Handle Socket.IO paths correctly in returned handler - [ ] Implement error handling in handlers