roster-server 2.1.6 → 2.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +56 -22
- package/package.json +1 -1
- package/test/roster-server.test.js +43 -0
package/index.js
CHANGED
|
@@ -86,6 +86,14 @@ function buildCertLookupCandidates(servername) {
|
|
|
86
86
|
return candidates;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function parseBooleanFlag(value, fallback = false) {
|
|
90
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
91
|
+
const normalized = String(value).trim().toLowerCase();
|
|
92
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
93
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
// Virtual Server that completely isolates applications
|
|
90
98
|
class VirtualServer extends EventEmitter {
|
|
91
99
|
constructor(domain) {
|
|
@@ -222,6 +230,9 @@ class Roster {
|
|
|
222
230
|
this.maxLocalPort = options.maxLocalPort || 9999;
|
|
223
231
|
this.tlsMinVersion = options.tlsMinVersion ?? 'TLSv1.2';
|
|
224
232
|
this.tlsMaxVersion = options.tlsMaxVersion ?? 'TLSv1.3';
|
|
233
|
+
this.disableWildcard = options.disableWildcard !== undefined
|
|
234
|
+
? parseBooleanFlag(options.disableWildcard, false)
|
|
235
|
+
: parseBooleanFlag(process.env.ROSTER_DISABLE_WILDCARD, false);
|
|
225
236
|
|
|
226
237
|
const port = options.port === undefined ? 443 : options.port;
|
|
227
238
|
if (port === 80 && !this.local) {
|
|
@@ -299,6 +310,10 @@ class Roster {
|
|
|
299
310
|
|
|
300
311
|
if (siteApp) {
|
|
301
312
|
if (domain.startsWith('*.')) {
|
|
313
|
+
if (this.disableWildcard) {
|
|
314
|
+
log.warn(`⚠️ Wildcard site skipped (disableWildcard enabled): ${domain}`);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
302
317
|
// Wildcard site: one handler for all subdomains (e.g. *.example.com)
|
|
303
318
|
this.domains.push(domain);
|
|
304
319
|
this.sites[domain] = siteApp;
|
|
@@ -513,6 +528,10 @@ class Roster {
|
|
|
513
528
|
const { domain, port } = this.parseDomainWithPort(domainString);
|
|
514
529
|
|
|
515
530
|
if (domain.startsWith('*.')) {
|
|
531
|
+
if (this.disableWildcard) {
|
|
532
|
+
log.warn(`⚠️ Wildcard registration ignored (disableWildcard enabled): ${domain}`);
|
|
533
|
+
return this;
|
|
534
|
+
}
|
|
516
535
|
const domainKey = port === this.defaultPort ? domain : `${domain}:${port}`;
|
|
517
536
|
this.domains.push(domain);
|
|
518
537
|
this.sites[domainKey] = requestHandler;
|
|
@@ -707,7 +726,7 @@ class Roster {
|
|
|
707
726
|
}
|
|
708
727
|
});
|
|
709
728
|
|
|
710
|
-
return greenlock.ready(glx => {
|
|
729
|
+
return greenlock.ready(async glx => {
|
|
711
730
|
const httpServer = glx.httpServer();
|
|
712
731
|
|
|
713
732
|
// Group sites by port
|
|
@@ -826,6 +845,26 @@ class Roster {
|
|
|
826
845
|
}
|
|
827
846
|
return null;
|
|
828
847
|
};
|
|
848
|
+
const ensureBunDefaultPems = async (primaryDomain) => {
|
|
849
|
+
let pems = resolvePemsForServername(primaryDomain);
|
|
850
|
+
if (pems) return pems;
|
|
851
|
+
|
|
852
|
+
const certSubject = primaryDomain.startsWith('*.') ? wildcardRoot(primaryDomain) : primaryDomain;
|
|
853
|
+
log.warn(`⚠️ Bun runtime detected and cert files missing for ${primaryDomain}; requesting certificate via Greenlock before HTTPS bind`);
|
|
854
|
+
try {
|
|
855
|
+
await greenlock.get({ servername: certSubject });
|
|
856
|
+
} catch (error) {
|
|
857
|
+
log.error(`❌ Failed to obtain certificate for ${certSubject} under Bun:`, error?.message || error);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
pems = resolvePemsForServername(primaryDomain);
|
|
861
|
+
if (pems) return pems;
|
|
862
|
+
|
|
863
|
+
throw new Error(
|
|
864
|
+
`Bun runtime could not load TLS certificate files for ${primaryDomain}. ` +
|
|
865
|
+
`Refusing to start HTTPS on port ${portNum} to avoid serving invalid TLS.`
|
|
866
|
+
);
|
|
867
|
+
};
|
|
829
868
|
|
|
830
869
|
if (portNum === this.defaultPort) {
|
|
831
870
|
// Bun has known gaps around SNICallback compatibility.
|
|
@@ -836,28 +875,23 @@ class Roster {
|
|
|
836
875
|
|
|
837
876
|
if (isBunRuntime) {
|
|
838
877
|
const primaryDomain = Object.keys(portData.virtualServers)[0];
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
callback(error);
|
|
853
|
-
}
|
|
878
|
+
// Under Bun, avoid glx.httpsServer fallback (may serve invalid TLS on :443).
|
|
879
|
+
// Require concrete PEM files and create native https server directly.
|
|
880
|
+
const defaultPems = await ensureBunDefaultPems(primaryDomain);
|
|
881
|
+
httpsServer = https.createServer({
|
|
882
|
+
...tlsOpts,
|
|
883
|
+
key: defaultPems.key,
|
|
884
|
+
cert: defaultPems.cert,
|
|
885
|
+
SNICallback: (servername, callback) => {
|
|
886
|
+
try {
|
|
887
|
+
const pems = resolvePemsForServername(servername) || defaultPems;
|
|
888
|
+
callback(null, tls.createSecureContext({ key: pems.key, cert: pems.cert }));
|
|
889
|
+
} catch (error) {
|
|
890
|
+
callback(error);
|
|
854
891
|
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
}
|
|
858
|
-
log.warn(`⚠️ Bun runtime detected but cert files missing for ${primaryDomain}; falling back to Greenlock HTTPS server`);
|
|
859
|
-
httpsServer = glx.httpsServer(tlsOpts, dispatcher);
|
|
860
|
-
}
|
|
892
|
+
}
|
|
893
|
+
}, dispatcher);
|
|
894
|
+
log.warn(`⚠️ Bun runtime detected: using file-based TLS with SNI for ${primaryDomain} on port ${portNum}`);
|
|
861
895
|
} else {
|
|
862
896
|
httpsServer = glx.httpsServer(tlsOpts, dispatcher);
|
|
863
897
|
}
|
package/package.json
CHANGED
|
@@ -168,6 +168,14 @@ describe('Roster', () => {
|
|
|
168
168
|
roster.register('*.example.com', wildcardHandler);
|
|
169
169
|
assert.strictEqual(roster.getHandlerForHost('api.example.com'), exactHandler);
|
|
170
170
|
});
|
|
171
|
+
it('ignores wildcard registration when disableWildcard is true', () => {
|
|
172
|
+
const roster = new Roster({ local: true, disableWildcard: true });
|
|
173
|
+
const handler = () => {};
|
|
174
|
+
roster.register('*.example.com', handler);
|
|
175
|
+
assert.strictEqual(roster.sites['*.example.com'], undefined);
|
|
176
|
+
assert.strictEqual(roster.getHandlerForHost('api.example.com'), null);
|
|
177
|
+
assert.strictEqual(roster.wildcardZones.has('example.com'), false);
|
|
178
|
+
});
|
|
171
179
|
});
|
|
172
180
|
|
|
173
181
|
describe('getHandlerForPortData', () => {
|
|
@@ -290,6 +298,21 @@ describe('Roster', () => {
|
|
|
290
298
|
const roster = new Roster({ local: false, dnsChallenge: false });
|
|
291
299
|
assert.strictEqual(roster.dnsChallenge, null);
|
|
292
300
|
});
|
|
301
|
+
it('enables disableWildcard from constructor option', () => {
|
|
302
|
+
const roster = new Roster({ local: true, disableWildcard: true });
|
|
303
|
+
assert.strictEqual(roster.disableWildcard, true);
|
|
304
|
+
});
|
|
305
|
+
it('reads disableWildcard from env var', () => {
|
|
306
|
+
const previous = process.env.ROSTER_DISABLE_WILDCARD;
|
|
307
|
+
process.env.ROSTER_DISABLE_WILDCARD = '1';
|
|
308
|
+
try {
|
|
309
|
+
const roster = new Roster({ local: true });
|
|
310
|
+
assert.strictEqual(roster.disableWildcard, true);
|
|
311
|
+
} finally {
|
|
312
|
+
if (previous === undefined) delete process.env.ROSTER_DISABLE_WILDCARD;
|
|
313
|
+
else process.env.ROSTER_DISABLE_WILDCARD = previous;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
293
316
|
});
|
|
294
317
|
|
|
295
318
|
describe('register (normal domain)', () => {
|
|
@@ -470,6 +493,26 @@ describe('Roster loadSites', () => {
|
|
|
470
493
|
}
|
|
471
494
|
});
|
|
472
495
|
|
|
496
|
+
it('skips wildcard site from www when disableWildcard is true', async () => {
|
|
497
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'roster-test-'));
|
|
498
|
+
const wwwPath = path.join(tmpDir, 'www');
|
|
499
|
+
const siteDir = path.join(wwwPath, '*.wildcard.example');
|
|
500
|
+
fs.mkdirSync(siteDir, { recursive: true });
|
|
501
|
+
fs.writeFileSync(
|
|
502
|
+
path.join(siteDir, 'index.js'),
|
|
503
|
+
'module.exports = () => (req, res) => { res.writeHead(200); res.end("wildcard"); };',
|
|
504
|
+
'utf8'
|
|
505
|
+
);
|
|
506
|
+
try {
|
|
507
|
+
const roster = new Roster({ wwwPath, local: true, disableWildcard: true });
|
|
508
|
+
await roster.loadSites();
|
|
509
|
+
assert.strictEqual(roster.sites['*.wildcard.example'], undefined);
|
|
510
|
+
assert.strictEqual(roster.wildcardZones.has('wildcard.example'), false);
|
|
511
|
+
} finally {
|
|
512
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
473
516
|
it('does not throw when www path does not exist', async () => {
|
|
474
517
|
const roster = new Roster({
|
|
475
518
|
wwwPath: path.join(os.tmpdir(), 'roster-nonexistent-' + Date.now()),
|