roster-server
Version:
👾 RosterServer - A domain host router to host multiple HTTPS.
1,080 lines (1,007 loc) • 47.5 kB
JavaScript
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert');
const path = require('path');
const fs = require('fs');
const http = require('http');
const os = require('os');
const Roster = require('../index.js');
const {
wildcardRoot,
hostMatchesWildcard,
wildcardSubjectForHost,
buildCertLookupCandidates
} = require('../index.js');
function closePortServers(roster) {
if (roster.portServers && typeof roster.portServers === 'object') {
for (const server of Object.values(roster.portServers)) {
try {
server.close();
} catch (_) {}
}
}
}
function httpGet(host, port, pathname = '/') {
return new Promise((resolve, reject) => {
const req = http.get(
{ host, port, path: pathname, headers: { host: host + (port === 80 ? '' : ':' + port) } },
(res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => resolve({ statusCode: res.statusCode, headers: res.headers, body }));
}
);
req.on('error', reject);
req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
});
}
describe('wildcardRoot', () => {
it('returns root domain for *.example.com', () => {
assert.strictEqual(wildcardRoot('*.example.com'), 'example.com');
});
it('returns root for *.sub.example.com', () => {
assert.strictEqual(wildcardRoot('*.sub.example.com'), 'sub.example.com');
});
it('returns null for non-wildcard', () => {
assert.strictEqual(wildcardRoot('example.com'), null);
assert.strictEqual(wildcardRoot('api.example.com'), null);
});
it('returns null for empty or null', () => {
assert.strictEqual(wildcardRoot(''), null);
assert.strictEqual(wildcardRoot(null), null);
});
});
describe('hostMatchesWildcard', () => {
it('matches subdomain to pattern', () => {
assert.strictEqual(hostMatchesWildcard('api.example.com', '*.example.com'), true);
assert.strictEqual(hostMatchesWildcard('app.example.com', '*.example.com'), true);
assert.strictEqual(hostMatchesWildcard('a.example.com', '*.example.com'), true);
});
it('does not match apex domain', () => {
assert.strictEqual(hostMatchesWildcard('example.com', '*.example.com'), false);
});
it('does not match other zones', () => {
assert.strictEqual(hostMatchesWildcard('api.other.com', '*.example.com'), false);
assert.strictEqual(hostMatchesWildcard('example.com.evil.com', '*.example.com'), false);
});
it('returns false for invalid pattern', () => {
assert.strictEqual(hostMatchesWildcard('api.example.com', 'example.com'), false);
assert.strictEqual(hostMatchesWildcard('api.example.com', ''), false);
assert.strictEqual(hostMatchesWildcard('api.example.com', null), false);
});
it('matches case-insensitively (Host header may be any case)', () => {
assert.strictEqual(hostMatchesWildcard('Admin.Tagnu.com', '*.tagnu.com'), true);
assert.strictEqual(hostMatchesWildcard('API.EXAMPLE.COM', '*.example.com'), true);
});
});
describe('wildcardSubjectForHost', () => {
it('returns wildcard subject for subdomain hosts', () => {
assert.strictEqual(wildcardSubjectForHost('admin.tagnu.com'), '*.tagnu.com');
assert.strictEqual(wildcardSubjectForHost('api.eu.example.com'), '*.eu.example.com');
});
it('returns null for apex hosts', () => {
assert.strictEqual(wildcardSubjectForHost('tagnu.com'), null);
assert.strictEqual(wildcardSubjectForHost('localhost'), null);
});
});
describe('buildCertLookupCandidates', () => {
it('prioritizes wildcard cert for subdomains and includes apex fallback', () => {
assert.deepStrictEqual(
buildCertLookupCandidates('admin.tagnu.com'),
['admin.tagnu.com', '*.tagnu.com', '_wildcard_.tagnu.com', 'tagnu.com']
);
});
it('includes wildcard storage path candidates when wildcard subject is provided', () => {
assert.deepStrictEqual(
buildCertLookupCandidates('*.tagnu.com'),
['*.tagnu.com', '_wildcard_.tagnu.com', 'tagnu.com']
);
});
it('returns only apex subject for apex hosts', () => {
assert.deepStrictEqual(buildCertLookupCandidates('tagnu.com'), ['tagnu.com']);
});
});
describe('Roster', () => {
describe('parseDomainWithPort', () => {
it('parses *.example.com with default port', () => {
const roster = new Roster({ local: true });
assert.deepStrictEqual(roster.parseDomainWithPort('*.example.com'), {
domain: '*.example.com',
port: 443
});
});
it('parses *.example.com:8080', () => {
const roster = new Roster({ local: true });
assert.deepStrictEqual(roster.parseDomainWithPort('*.example.com:8080'), {
domain: '*.example.com',
port: 8080
});
});
it('parses normal domain with port', () => {
const roster = new Roster({ local: true });
assert.deepStrictEqual(roster.parseDomainWithPort('example.com:8443'), {
domain: 'example.com',
port: 8443
});
});
});
describe('register (wildcard)', () => {
it('registers *.example.com and resolves handler for subdomain', () => {
const roster = new Roster({ local: true });
const handler = () => {};
roster.register('*.example.com', handler);
assert.strictEqual(roster.getHandlerForHost('api.example.com'), handler);
assert.strictEqual(roster.getHandlerForHost('app.example.com'), handler);
assert.strictEqual(roster.getHandlerForHost('example.com'), null);
assert.ok(roster.wildcardZones.has('example.com'));
});
it('registers *.example.com:8080 with custom port', () => {
const roster = new Roster({ local: true });
const handler = () => {};
roster.register('*.example.com:8080', handler);
assert.strictEqual(roster.sites['*.example.com:8080'], handler);
assert.ok(roster.wildcardZones.has('example.com'));
});
it('getHandlerAndKeyForHost returns handler and siteKey for wildcard match', () => {
const roster = new Roster({ local: true });
const handler = () => {};
roster.register('*.example.com', handler);
const resolved = roster.getHandlerAndKeyForHost('api.example.com');
assert.ok(resolved);
assert.strictEqual(resolved.handler, handler);
assert.strictEqual(resolved.siteKey, '*.example.com');
});
it('exact match takes precedence over wildcard', () => {
const roster = new Roster({ local: true });
const exactHandler = () => {};
const wildcardHandler = () => {};
roster.register('api.example.com', exactHandler);
roster.register('*.example.com', wildcardHandler);
assert.strictEqual(roster.getHandlerForHost('api.example.com'), exactHandler);
});
it('ignores wildcard registration when disableWildcard is true', () => {
const roster = new Roster({ local: true, disableWildcard: true });
const handler = () => {};
roster.register('*.example.com', handler);
assert.strictEqual(roster.sites['*.example.com'], undefined);
assert.strictEqual(roster.getHandlerForHost('api.example.com'), null);
assert.strictEqual(roster.wildcardZones.has('example.com'), false);
});
});
describe('getHandlerForPortData', () => {
it('returns exact match when present', () => {
const roster = new Roster({ local: true });
const vs = roster.createVirtualServer('example.com');
const handler = () => {};
const portData = {
virtualServers: { 'example.com': vs },
appHandlers: { 'example.com': handler }
};
const resolved = roster.getHandlerForPortData('example.com', portData);
assert.ok(resolved);
assert.strictEqual(resolved.virtualServer, vs);
assert.strictEqual(resolved.appHandler, handler);
});
it('returns wildcard match for subdomain', () => {
const roster = new Roster({ local: true });
const vs = roster.createVirtualServer('*.example.com');
const handler = () => {};
const portData = {
virtualServers: { '*.example.com': vs },
appHandlers: { '*.example.com': handler }
};
const resolved = roster.getHandlerForPortData('api.example.com', portData);
assert.ok(resolved);
assert.strictEqual(resolved.virtualServer, vs);
assert.strictEqual(resolved.appHandler, handler);
});
it('returns null when no match', () => {
const roster = new Roster({ local: true });
const portData = { virtualServers: {}, appHandlers: {} };
assert.strictEqual(roster.getHandlerForPortData('unknown.com', portData), null);
});
});
describe('getUrl (wildcard)', () => {
it('returns URL for wildcard-matched host in local mode', () => {
const roster = new Roster({ local: true });
roster.register('*.example.com', () => {});
roster.domainPorts = { '*.example.com': 9999 };
roster.local = true;
assert.strictEqual(roster.getUrl('api.example.com'), 'http://api.localhost:9999');
});
it('returns https URL for wildcard-matched host in production', () => {
const roster = new Roster({ local: false });
roster.register('*.example.com', () => {});
roster.local = false;
assert.strictEqual(roster.getUrl('api.example.com'), 'https://api.example.com');
});
it('returns null for host that matches no site', () => {
const roster = new Roster({ local: true });
assert.strictEqual(roster.getUrl('unknown.com'), null);
});
});
describe('register validation', () => {
it('throws when domain is missing', () => {
const roster = new Roster({ local: true });
assert.throws(() => roster.register('', () => {}), /Domain is required/);
assert.throws(() => roster.register(null, () => {}), /Domain is required/);
});
it('throws when handler is not a function', () => {
const roster = new Roster({ local: true });
assert.throws(() => roster.register('*.example.com', {}), /requestHandler must be a function/);
});
});
describe('constructor', () => {
it('throws when port is 80 and not local', () => {
assert.throws(() => new Roster({ port: 80, local: false }), /Port 80 is reserved/);
});
it('allows port 80 when local is true', () => {
const roster = new Roster({ port: 80, local: true });
assert.strictEqual(roster.defaultPort, 80);
});
it('sets defaultPort 443 when port not given', () => {
const roster = new Roster({ local: true });
assert.strictEqual(roster.defaultPort, 443);
});
it('uses acme-dns-01-cli by default (resolved to absolute path for Greenlock)', () => {
const roster = new Roster({ local: false });
assert.ok(roster.dnsChallenge);
assert.strictEqual(typeof roster.dnsChallenge.module, 'string');
assert.ok(require('path').isAbsolute(roster.dnsChallenge.module));
assert.ok(roster.dnsChallenge.module.includes('acme-dns-01-cli-wrapper'));
assert.strictEqual(roster.dnsChallenge.propagationDelay, 120000);
assert.strictEqual(roster.dnsChallenge.autoContinue, false);
assert.strictEqual(roster.dnsChallenge.dryRunDelay, 120000);
});
it('normalizes explicit acme-dns-01-cli module to wrapper and sets default propagationDelay', () => {
const roster = new Roster({ local: false, dnsChallenge: { module: 'acme-dns-01-cli' } });
assert.ok(require('path').isAbsolute(roster.dnsChallenge.module));
assert.ok(roster.dnsChallenge.module.includes('acme-dns-01-cli-wrapper'));
assert.strictEqual(roster.dnsChallenge.propagationDelay, 120000);
assert.strictEqual(roster.dnsChallenge.autoContinue, false);
assert.strictEqual(roster.dnsChallenge.dryRunDelay, 120000);
});
it('keeps explicit non-cli dnsChallenge module as-is', () => {
const roster = new Roster({ local: false, dnsChallenge: { module: 'acme-dns-01-route53', token: 'x' } });
assert.strictEqual(roster.dnsChallenge.module, 'acme-dns-01-route53');
assert.strictEqual(roster.dnsChallenge.token, 'x');
assert.strictEqual(roster.dnsChallenge.propagationDelay, 120000);
assert.strictEqual(roster.dnsChallenge.autoContinue, false);
assert.strictEqual(roster.dnsChallenge.dryRunDelay, 120000);
});
it('normalizes explicit acme-dns-01-cli absolute path to wrapper', () => {
const path = require('path');
const roster = new Roster({
local: false,
dnsChallenge: { module: path.join('/srv/roster/node_modules/acme-dns-01-cli', 'index.js') }
});
assert.ok(require('path').isAbsolute(roster.dnsChallenge.module));
assert.ok(roster.dnsChallenge.module.includes('acme-dns-01-cli-wrapper'));
assert.strictEqual(roster.dnsChallenge.propagationDelay, 120000);
assert.strictEqual(roster.dnsChallenge.autoContinue, false);
assert.strictEqual(roster.dnsChallenge.dryRunDelay, 120000);
});
it('allows disabling DNS challenge with dnsChallenge: false', () => {
const roster = new Roster({ local: false, dnsChallenge: false });
assert.strictEqual(roster.dnsChallenge, null);
});
it('enables disableWildcard from constructor option', () => {
const roster = new Roster({ local: true, disableWildcard: true });
assert.strictEqual(roster.disableWildcard, true);
});
it('enables disableWildcard from constructor option (truthy string)', () => {
const roster = new Roster({ local: true, disableWildcard: '1' });
assert.strictEqual(roster.disableWildcard, true);
});
it('enables combined wildcard certs from constructor option', () => {
const roster = new Roster({ local: false, combineWildcardCerts: true });
assert.strictEqual(roster.combineWildcardCerts, true);
});
it('defaults combineWildcardCerts to false', () => {
const roster = new Roster({ local: false });
assert.strictEqual(roster.combineWildcardCerts, false);
});
it('explicit combineWildcardCerts=true enables combined cert mode', () => {
const roster = new Roster({ local: false, combineWildcardCerts: true });
assert.strictEqual(roster.combineWildcardCerts, true);
});
it('defaults autoCertificates to true', () => {
const roster = new Roster({ local: false });
assert.strictEqual(roster.autoCertificates, true);
});
it('allows disabling autoCertificates explicitly', () => {
const roster = new Roster({ local: false, autoCertificates: false });
assert.strictEqual(roster.autoCertificates, false);
});
});
describe('register (normal domain)', () => {
it('adds domain and www when domain has fewer than 2 dots', () => {
const roster = new Roster({ local: true });
const handler = () => {};
roster.register('example.com', handler);
assert.strictEqual(roster.sites['example.com'], handler);
assert.strictEqual(roster.sites['www.example.com'], handler);
});
it('does not add www for multi-label domain', () => {
const roster = new Roster({ local: true });
const handler = () => {};
roster.register('api.example.com', handler);
assert.strictEqual(roster.sites['api.example.com'], handler);
assert.strictEqual(roster.sites['www.api.example.com'], undefined);
});
});
describe('getUrl (exact domain)', () => {
it('returns http://localhost:PORT in local mode for registered domain', () => {
const roster = new Roster({ local: true });
roster.register('exact.local', () => {});
roster.domainPorts = { 'exact.local': 4567 };
roster.local = true;
assert.strictEqual(roster.getUrl('exact.local'), 'http://localhost:4567');
});
it('returns http://subdomain.localhost:PORT in local mode for exact subdomain', () => {
const roster = new Roster({ local: true });
roster.register('api.example.com', () => {});
roster.domainPorts = { 'api.example.com': 5678 };
roster.local = true;
assert.strictEqual(roster.getUrl('api.example.com'), 'http://api.localhost:5678');
});
it('returns https URL in production for registered domain', () => {
const roster = new Roster({ local: false });
roster.register('example.com', () => {});
roster.local = false;
assert.strictEqual(roster.getUrl('example.com'), 'https://example.com');
});
it('strips www and returns canonical URL (same as non-www)', () => {
const roster = new Roster({ local: false });
roster.register('example.com', () => {});
assert.strictEqual(roster.getUrl('www.example.com'), 'https://example.com');
assert.strictEqual(roster.getUrl('example.com'), 'https://example.com');
});
});
describe('handleRequest', () => {
it('redirects www to non-www with 301', () => {
const roster = new Roster({ local: true });
const res = {
writeHead: (status, headers) => {
assert.strictEqual(status, 301);
assert.strictEqual(headers.Location, 'https://example.com/');
},
end: () => {}
};
roster.handleRequest(
{ headers: { host: 'www.example.com' }, url: '/' },
res
);
});
it('returns 404 when host has no handler', () => {
const roster = new Roster({ local: true });
let status;
const res = {
writeHead: (s) => { status = s; },
end: () => {}
};
roster.handleRequest(
{ headers: { host: 'unknown.example.com' }, url: '/' },
res
);
assert.strictEqual(status, 404);
});
it('invokes handler for registered host', () => {
const roster = new Roster({ local: true });
let called = false;
roster.register('example.com', (req, res) => {
called = true;
res.writeHead(200);
res.end('ok');
});
const res = {
writeHead: () => {},
end: () => {}
};
roster.handleRequest(
{ headers: { host: 'example.com' }, url: '/' },
res
);
assert.strictEqual(called, true);
});
});
});
describe('Roster local mode (local: true)', () => {
it('starts HTTP server and responds for registered domain', async () => {
const roster = new Roster({
local: true,
minLocalPort: 19090,
maxLocalPort: 19099,
hostname: 'localhost'
});
const body = 'local-mode-ok';
roster.register('testlocal.example', (server) => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(body);
};
});
await roster.start();
try {
const port = roster.domainPorts['testlocal.example'];
assert.ok(typeof port === 'number' && port >= 19090 && port <= 19099);
await new Promise((r) => setTimeout(r, 50));
const result = await httpGet('localhost', port, '/');
assert.strictEqual(result.statusCode, 200);
assert.strictEqual(result.body, body);
} finally {
closePortServers(roster);
}
});
it('getUrl returns localhost URL after start', async () => {
const roster = new Roster({
local: true,
minLocalPort: 19100,
maxLocalPort: 19109
});
roster.register('geturltest.example', () => () => {});
await roster.start();
try {
const url = roster.getUrl('geturltest.example');
assert.ok(url && url.startsWith('http://localhost:'));
assert.ok(roster.domainPorts['geturltest.example'] !== undefined);
} finally {
closePortServers(roster);
}
});
});
describe('Roster loadSites', () => {
it('loads site from www directory and registers domain + www', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, 'loaded.example');
fs.mkdirSync(siteDir, { recursive: true });
fs.writeFileSync(
path.join(siteDir, 'index.js'),
'module.exports = () => (req, res) => { res.writeHead(200); res.end("loaded"); };',
'utf8'
);
try {
const roster = new Roster({ wwwPath, local: true });
await roster.loadSites();
assert.ok(roster.sites['loaded.example']);
assert.ok(roster.sites['www.loaded.example']);
const handler = roster.sites['loaded.example'];
assert.strictEqual(typeof handler, 'function');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('loads wildcard site from www/*.example.com directory', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, '*.wildcard.example');
fs.mkdirSync(siteDir, { recursive: true });
fs.writeFileSync(
path.join(siteDir, 'index.js'),
'module.exports = () => (req, res) => { res.writeHead(200); res.end("wildcard"); };',
'utf8'
);
try {
const roster = new Roster({ wwwPath, local: true });
await roster.loadSites();
assert.ok(roster.sites['*.wildcard.example']);
assert.ok(roster.wildcardZones.has('wildcard.example'));
assert.strictEqual(roster.getHandlerForHost('api.wildcard.example'), roster.sites['*.wildcard.example']);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('skips wildcard site from www when disableWildcard is true', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, '*.wildcard.example');
fs.mkdirSync(siteDir, { recursive: true });
fs.writeFileSync(
path.join(siteDir, 'index.js'),
'module.exports = () => (req, res) => { res.writeHead(200); res.end("wildcard"); };',
'utf8'
);
try {
const roster = new Roster({ wwwPath, local: true, disableWildcard: true });
await roster.loadSites();
assert.strictEqual(roster.sites['*.wildcard.example'], undefined);
assert.strictEqual(roster.wildcardZones.has('wildcard.example'), false);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('does not throw when www path does not exist', async () => {
const roster = new Roster({
wwwPath: path.join(os.tmpdir(), 'roster-nonexistent-' + Date.now()),
local: true
});
await assert.doesNotReject(roster.loadSites());
});
it('loads static site from www/domain when index.html exists and no index.js', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, 'static.example');
fs.mkdirSync(siteDir, { recursive: true });
fs.writeFileSync(path.join(siteDir, 'index.html'), '<html>hello</html>', 'utf8');
try {
const roster = new Roster({ wwwPath, local: true });
await roster.loadSites();
assert.ok(roster.sites['static.example']);
assert.ok(roster.sites['www.static.example']);
const handler = roster.sites['static.example'];
assert.strictEqual(typeof handler, 'function');
const appHandler = handler(roster.createVirtualServer('static.example'));
assert.strictEqual(typeof appHandler, 'function');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('loads index.js over index.html when both exist', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, 'both.example');
fs.mkdirSync(siteDir, { recursive: true });
fs.writeFileSync(path.join(siteDir, 'index.html'), '<html>static</html>', 'utf8');
fs.writeFileSync(
path.join(siteDir, 'index.js'),
'module.exports = () => (req, res) => { res.writeHead(200); res.end("js"); };',
'utf8'
);
try {
const roster = new Roster({ wwwPath, local: true });
await roster.loadSites();
const handler = roster.sites['both.example'];
const appHandler = handler(roster.createVirtualServer('both.example'));
let body = '';
const res = { writeHead: () => {}, end: (b) => { body = (b || '').toString(); } };
appHandler({ url: '/', method: 'GET' }, res);
assert.strictEqual(body, 'js');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('static site serves index.html for / in local mode', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, 'staticlocal.example');
fs.mkdirSync(siteDir, { recursive: true });
const html = '<html><body>static ok</body></html>';
fs.writeFileSync(path.join(siteDir, 'index.html'), html, 'utf8');
const roster = new Roster({ wwwPath, local: true, minLocalPort: 19200, maxLocalPort: 19209 });
try {
await roster.start();
const port = roster.domainPorts['staticlocal.example'];
assert.ok(typeof port === 'number');
await new Promise((r) => setTimeout(r, 50));
const result = await httpGet('localhost', port, '/');
assert.strictEqual(result.statusCode, 200);
assert.ok(result.body.includes('static ok'));
} finally {
closePortServers(roster);
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('static site returns 404 for non-existent path', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, 'static404.example');
fs.mkdirSync(siteDir, { recursive: true });
fs.writeFileSync(path.join(siteDir, 'index.html'), '<html>ok</html>', 'utf8');
const roster = new Roster({ wwwPath, local: true, minLocalPort: 19210, maxLocalPort: 19219 });
try {
await roster.start();
const port = roster.domainPorts['static404.example'];
assert.ok(typeof port === 'number');
await new Promise((r) => setTimeout(r, 50));
const result = await httpGet('localhost', port, '/nonexistent.html');
assert.strictEqual(result.statusCode, 404);
} finally {
closePortServers(roster);
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('static site serves index.html for subpath directory (/en/)', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
const wwwPath = path.join(tmpDir, 'www');
const siteDir = path.join(wwwPath, 'subpath.example');
fs.mkdirSync(siteDir, { recursive: true });
fs.writeFileSync(path.join(siteDir, 'index.html'), '<html>root</html>', 'utf8');
const enDir = path.join(siteDir, 'en');
fs.mkdirSync(enDir, { recursive: true });
fs.writeFileSync(path.join(enDir, 'index.html'), '<html><body>en page</body></html>', 'utf8');
const roster = new Roster({ wwwPath, local: true, minLocalPort: 19220, maxLocalPort: 19229 });
try {
await roster.start();
const port = roster.domainPorts['subpath.example'];
assert.ok(typeof port === 'number');
await new Promise((r) => setTimeout(r, 50));
const resultSlash = await httpGet('localhost', port, '/en/');
assert.strictEqual(resultSlash.statusCode, 200);
assert.ok(resultSlash.body.includes('en page'));
const resultNoSlash = await httpGet('localhost', port, '/en');
assert.strictEqual(resultNoSlash.statusCode, 200);
assert.ok(resultNoSlash.body.includes('en page'));
} finally {
closePortServers(roster);
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
});
describe('Roster generateConfigJson', () => {
it('uses http-01 for apex/www and dns-01 only for wildcard cert', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-config-'));
try {
const roster = new Roster({
local: false,
greenlockStorePath: tmpDir,
dnsChallenge: {
module: 'acme-dns-01-cli',
propagationDelay: 120000,
autoContinue: false,
dryRunDelay: 120000
}
});
roster.domains = ['tagnu.com', 'www.tagnu.com', '*.tagnu.com'];
roster.wildcardZones.add('tagnu.com');
roster.generateConfigJson();
const configPath = path.join(tmpDir, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const apexSite = config.sites.find((site) => site.subject === 'tagnu.com');
const wildcardSite = config.sites.find((site) => site.subject === '*.tagnu.com');
assert.ok(apexSite);
assert.deepStrictEqual(apexSite.altnames.sort(), ['tagnu.com', 'www.tagnu.com'].sort());
assert.strictEqual(apexSite.challenges, undefined);
assert.ok(wildcardSite);
assert.deepStrictEqual(wildcardSite.altnames, ['*.tagnu.com']);
assert.ok(wildcardSite.challenges && wildcardSite.challenges['dns-01']);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('can combine apex+www+wildcard in one cert with dns-01', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-config-'));
try {
const roster = new Roster({
local: false,
greenlockStorePath: tmpDir,
combineWildcardCerts: true,
dnsChallenge: {
module: 'acme-dns-01-cli',
propagationDelay: 120000,
autoContinue: false,
dryRunDelay: 120000
}
});
roster.domains = ['tagnu.com', 'www.tagnu.com', '*.tagnu.com'];
roster.wildcardZones.add('tagnu.com');
roster.generateConfigJson();
const configPath = path.join(tmpDir, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const apexSite = config.sites.find((site) => site.subject === 'tagnu.com');
const wildcardSite = config.sites.find((site) => site.subject === '*.tagnu.com');
assert.ok(apexSite);
assert.deepStrictEqual(
apexSite.altnames.sort(),
['tagnu.com', 'www.tagnu.com', '*.tagnu.com'].sort()
);
assert.ok(apexSite.challenges && apexSite.challenges['dns-01']);
assert.strictEqual(wildcardSite, undefined);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
});
describe('Roster init() (cluster-friendly API)', () => {
it('initializes without creating or listening on any server', async () => {
const roster = new Roster({
local: true,
minLocalPort: 19300,
maxLocalPort: 19309
});
roster.register('init-test.example', (server) => {
return (req, res) => { res.writeHead(200); res.end('ok'); };
});
await roster.init();
assert.strictEqual(roster._initialized, true);
assert.strictEqual(Object.keys(roster.portServers).length, 0);
assert.ok(roster._sitesByPort[443]);
assert.ok(roster.domainServers['init-test.example']);
});
it('is idempotent (calling init twice does not reinitialize)', async () => {
const roster = new Roster({ local: true });
roster.register('idem.example', () => () => {});
await roster.init();
const firstSitesByPort = roster._sitesByPort;
await roster.init();
assert.strictEqual(roster._sitesByPort, firstSitesByPort);
});
it('start() still works after manual init()', async () => {
const roster = new Roster({
local: true,
minLocalPort: 19310,
maxLocalPort: 19319
});
roster.register('after-init.example', (server) => {
return (req, res) => { res.writeHead(200); res.end('after-init'); };
});
await roster.init();
await roster.start();
try {
const port = roster.domainPorts['after-init.example'];
assert.ok(typeof port === 'number');
await new Promise((r) => setTimeout(r, 50));
const result = await httpGet('localhost', port, '/');
assert.strictEqual(result.statusCode, 200);
assert.strictEqual(result.body, 'after-init');
} finally {
closePortServers(roster);
}
});
});
describe('Roster requestHandler() / upgradeHandler()', () => {
it('throws if called before init()', () => {
const roster = new Roster({ local: true });
assert.throws(() => roster.requestHandler(), /Call init\(\) before/);
assert.throws(() => roster.upgradeHandler(), /Call init\(\) before/);
});
it('returns a working request dispatcher after init()', async () => {
const roster = new Roster({ local: true });
roster.register('handler-test.example', (server) => {
return (req, res) => { res.writeHead(200); res.end('dispatched'); };
});
await roster.init();
const handler = roster.requestHandler();
assert.strictEqual(typeof handler, 'function');
let statusCode, body;
const fakeRes = {
writeHead: (s) => { statusCode = s; },
end: (b) => { body = b; }
};
handler({ headers: { host: 'handler-test.example' }, url: '/' }, fakeRes);
assert.strictEqual(statusCode, 200);
assert.strictEqual(body, 'dispatched');
});
it('returns 404 dispatcher for unregistered port', async () => {
const roster = new Roster({ local: true });
roster.register('port-test.example', () => () => {});
await roster.init();
const handler = roster.requestHandler(9999);
let statusCode;
const fakeRes = {
writeHead: (s) => { statusCode = s; },
end: () => {}
};
handler({ headers: { host: 'port-test.example' }, url: '/' }, fakeRes);
assert.strictEqual(statusCode, 404);
});
it('upgrade handler destroys socket for unknown host', async () => {
const roster = new Roster({ local: true });
roster.register('upgrade-test.example', () => () => {});
await roster.init();
const handler = roster.upgradeHandler();
let destroyed = false;
const fakeSocket = { destroy: () => { destroyed = true; } };
handler({ headers: { host: 'unknown.example' }, url: '/' }, fakeSocket, Buffer.alloc(0));
assert.strictEqual(destroyed, true);
});
it('www redirect uses http:// protocol in local mode', async () => {
const roster = new Roster({ local: true });
roster.register('redirect.example', (server) => {
return (req, res) => { res.writeHead(200); res.end('ok'); };
});
await roster.init();
const handler = roster.requestHandler();
let location;
const fakeRes = {
writeHead: (s, headers) => { location = headers?.Location; },
end: () => {}
};
handler({ headers: { host: 'www.redirect.example' }, url: '/path' }, fakeRes);
assert.ok(location);
assert.ok(location.startsWith('http://'), `Expected http:// redirect, got: ${location}`);
});
it('dispatches correctly for custom registered port via requestHandler(port)', async () => {
const roster = new Roster({ local: true });
roster.register('api.ported.example:8443', () => {
return (req, res) => { res.writeHead(200); res.end('port-8443'); };
});
await roster.init();
const handler = roster.requestHandler(8443);
let statusCode;
let body;
const fakeRes = {
writeHead: (s) => { statusCode = s; },
end: (b) => { body = b; }
};
handler({ headers: { host: 'api.ported.example' }, url: '/' }, fakeRes);
assert.strictEqual(statusCode, 200);
assert.strictEqual(body, 'port-8443');
});
it('www redirect uses https:// protocol in production mode', async () => {
const roster = new Roster({ local: false });
roster.register('redirect-prod.example', () => {
return (req, res) => { res.writeHead(200); res.end('ok'); };
});
await roster.init();
const handler = roster.requestHandler();
let location;
const fakeRes = {
writeHead: (s, headers) => { location = headers?.Location; },
end: () => {}
};
handler({ headers: { host: 'www.redirect-prod.example' }, url: '/secure' }, fakeRes);
assert.ok(location);
assert.ok(location.startsWith('https://'), `Expected https:// redirect, got: ${location}`);
});
});
describe('Roster sniCallback()', () => {
it('throws if called before init()', () => {
const roster = new Roster({ local: false });
assert.throws(() => roster.sniCallback(), /Call init\(\) before/);
});
it('throws in local mode (no SNI in HTTP)', async () => {
const roster = new Roster({ local: true });
roster.register('sni-local.example', () => () => {});
await roster.init();
assert.throws(() => roster.sniCallback(), /not available in local mode/);
});
it('returns a function after init() in production mode', async () => {
const roster = new Roster({ local: false });
roster.register('sni-prod.example', () => () => {});
await roster.init();
const cb = roster.sniCallback();
assert.strictEqual(typeof cb, 'function');
});
});
describe('Roster ensureCertificate()', () => {
it('throws if called before init()', async () => {
const roster = new Roster({ local: false, autoCertificates: true });
await assert.rejects(() => roster.ensureCertificate('example.com'), /Call init\(\) before ensureCertificate/);
});
it('throws in local mode', async () => {
const roster = new Roster({ local: true, autoCertificates: true });
roster.register('local-cert.example', () => () => {});
await roster.init();
await assert.rejects(() => roster.ensureCertificate('local-cert.example'), /not available in local mode/);
});
it('throws when autoCertificates is disabled and cert is missing', async () => {
const roster = new Roster({ local: false, autoCertificates: false });
roster.register('missing-cert.example', () => () => {});
await roster.init();
await assert.rejects(
() => roster.ensureCertificate('missing-cert.example'),
/autoCertificates is disabled/
);
});
});
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()', () => {
it('throws if called before init()', () => {
const roster = new Roster({ local: true });
const fakeServer = { on: () => {} };
assert.throws(() => roster.attach(fakeServer), /Call init\(\) before/);
});
it('wires request and upgrade listeners onto external server', async () => {
const roster = new Roster({ local: true });
roster.register('attach-test.example', (server) => {
return (req, res) => { res.writeHead(200); res.end('attached'); };
});
await roster.init();
const listeners = {};
const fakeServer = {
on: (event, fn) => { listeners[event] = fn; }
};
const result = roster.attach(fakeServer);
assert.strictEqual(result, roster);
assert.strictEqual(typeof listeners['request'], 'function');
assert.strictEqual(typeof listeners['upgrade'], 'function');
});
it('uses provided port option when attaching', async () => {
const roster = new Roster({ local: true });
roster.register('attach-443.example', () => (req, res) => { res.writeHead(200); res.end('on-443'); });
roster.register('attach-9443.example:9443', () => (req, res) => { res.writeHead(200); res.end('on-9443'); });
await roster.init();
const listeners = {};
const fakeServer = {
on: (event, fn) => { listeners[event] = fn; }
};
roster.attach(fakeServer, { port: 9443 });
let statusCode;
let body;
const fakeRes = {
writeHead: (s) => { statusCode = s; },
end: (b) => { body = b; }
};
listeners.request({ headers: { host: 'attach-9443.example' }, url: '/' }, fakeRes);
assert.strictEqual(statusCode, 200);
assert.strictEqual(body, 'on-9443');
});
it('attached handler dispatches requests correctly', async () => {
const roster = new Roster({
local: true,
minLocalPort: 19400,
maxLocalPort: 19409
});
roster.register('attach-http.example', (server) => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('from-attach');
};
});
await roster.init();
const server = http.createServer();
roster.attach(server);
const port = 19400;
await new Promise((resolve, reject) => {
server.listen(port, 'localhost', resolve);
server.on('error', reject);
});
try {
await new Promise((r) => setTimeout(r, 50));
const result = await new Promise((resolve, reject) => {
const req = http.get(
{ host: 'localhost', port, path: '/', headers: { host: 'attach-http.example' } },
(res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => resolve({ statusCode: res.statusCode, body }));
}
);
req.on('error', reject);
req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
});
assert.strictEqual(result.statusCode, 200);
assert.strictEqual(result.body, 'from-attach');
} finally {
server.close();
}
});
});