SuperAPI AI Directory

roster-server

Security & auditing

View repository

Updated 10-16 05:02

About this skill

Quick Setup

Production

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

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

module.exports = (httpsServer) => {
    return (req, res) => {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Hello World');
    };
};

Pattern 2: Express App

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

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

// 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)

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)

// 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);

Pattern 8: Optional Scanner Blocking

const Roster = require('roster-server');
const { createScannerBlocker } = require('roster-server/plugins/scanner-blocker.js');

const roster = new Roster({ local: true, wwwPath: './www' });
roster.use(createScannerBlocker());
roster.start();

The plugin blocks common PHP, WordPress, repository, and sensitive-file probes before site handlers. Defaults are a 60-second window, 3 strikes, a 15-minute ban, 10,000 tracked clients, and trustProxy: false; pass only the values to override. Set trustProxy: true only behind a trusted reverse proxy that overwrites X-Forwarded-For. Ban state is per process and in memory; use onBlock to integrate a shared firewall or Fail2ban.

Key Configuration Options

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.use(plugin)

Registers a synchronous request plugin. It receives (req, res, { host, domain }) and stops dispatch when it returns true.

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

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

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

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