roster-server 2.1.34 → 2.2.1

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 CHANGED
@@ -3,11 +3,14 @@ const path = require('path');
3
3
  const http = require('http');
4
4
  const https = require('https');
5
5
  const tls = require('tls');
6
+ const crypto = require('crypto');
6
7
  const { EventEmitter } = require('events');
7
8
  const Greenlock = require('./vendor/greenlock-express/greenlock-express.js');
8
9
  const GreenlockShim = require('./vendor/greenlock-express/greenlock-shim.js');
9
10
  const log = require('lemonlog')('roster');
10
11
 
12
+ const isBunRuntime = typeof Bun !== 'undefined' || (typeof process !== 'undefined' && process.release?.name === 'bun');
13
+
11
14
  // CRC32 implementation for deterministic port assignment
12
15
  function crc32(str) {
13
16
  const crcTable = [];
@@ -87,6 +90,16 @@ function buildCertLookupCandidates(servername) {
87
90
  return candidates;
88
91
  }
89
92
 
93
+ function certCoversName(certPem, name) {
94
+ try {
95
+ const x509 = new crypto.X509Certificate(certPem);
96
+ const san = (x509.subjectAltName || '').toLowerCase();
97
+ return san.split(',').some(entry => entry.trim() === `dns:${name.toLowerCase()}`);
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
90
103
  function parseBooleanFlag(value, fallback = false) {
91
104
  if (value === undefined || value === null || value === '') return fallback;
92
105
  const normalized = String(value).trim().toLowerCase();
@@ -234,9 +247,13 @@ class Roster {
234
247
  this.disableWildcard = options.disableWildcard !== undefined
235
248
  ? parseBooleanFlag(options.disableWildcard, false)
236
249
  : parseBooleanFlag(process.env.ROSTER_DISABLE_WILDCARD, false);
250
+ const combineDefault = isBunRuntime;
237
251
  this.combineWildcardCerts = options.combineWildcardCerts !== undefined
238
- ? parseBooleanFlag(options.combineWildcardCerts, false)
239
- : parseBooleanFlag(process.env.ROSTER_COMBINE_WILDCARD_CERTS, false);
252
+ ? parseBooleanFlag(options.combineWildcardCerts, combineDefault)
253
+ : parseBooleanFlag(process.env.ROSTER_COMBINE_WILDCARD_CERTS, combineDefault);
254
+ if (isBunRuntime && this.combineWildcardCerts) {
255
+ log.info('Bun runtime detected: combined wildcard certificates enabled by default (SNI bypass)');
256
+ }
240
257
 
241
258
  const port = options.port === undefined ? 443 : options.port;
242
259
  if (port === 80 && !this.local) {
@@ -775,38 +792,7 @@ class Roster {
775
792
  }
776
793
  }
777
794
 
778
- const isBunRuntime = typeof Bun !== 'undefined' || process.release?.name === 'bun';
779
- if (isBunRuntime && this.wildcardZones.size > 0) {
780
- const retryDelayMs = Number.isFinite(Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_RETRY_MS))
781
- ? Math.max(1000, Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_RETRY_MS))
782
- : 30000;
783
- const maxAttempts = Number.isFinite(Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
784
- ? Math.max(0, Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
785
- : 0; // 0 = retry forever
786
-
787
- for (const zone of this.wildcardZones) {
788
- const bootstrapHost = `bun-bootstrap.${zone}`;
789
- const attemptPrewarm = async (attempt = 1) => {
790
- try {
791
- log.warn(`⚠️ Bun runtime detected: prewarming wildcard certificate via ${bootstrapHost} (attempt ${attempt})`);
792
- await greenlockRuntime.get({ servername: bootstrapHost });
793
- log.info(`✅ Bun wildcard prewarm succeeded for ${zone} on attempt ${attempt}`);
794
- } catch (error) {
795
- log.warn(`⚠️ Bun wildcard prewarm failed for ${zone} (attempt ${attempt}): ${error?.message || error}`);
796
- if (maxAttempts > 0 && attempt >= maxAttempts) {
797
- log.warn(`⚠️ Bun wildcard prewarm stopped for ${zone} after ${attempt} attempts`);
798
- return;
799
- }
800
- setTimeout(() => {
801
- attemptPrewarm(attempt + 1).catch(() => {});
802
- }, retryDelayMs);
803
- }
804
- };
805
-
806
- // Background prewarm + retries so HTTPS startup is not blocked by DNS propagation timing.
807
- attemptPrewarm().catch(() => {});
808
- }
809
- }
795
+ const bunTlsHotReloadHandlers = [];
810
796
 
811
797
  // Create dispatcher for each port
812
798
  const createDispatcher = (portData) => {
@@ -934,10 +920,22 @@ class Roster {
934
920
  };
935
921
  const ensureBunDefaultPems = async (primaryDomain) => {
936
922
  let pems = await issueAndReloadPemsForServername(primaryDomain);
923
+
924
+ const needsWildcard = this.combineWildcardCerts
925
+ && this.wildcardZones.has(primaryDomain)
926
+ && this.dnsChallenge;
927
+
928
+ if (pems && needsWildcard && !certCoversName(pems.cert, `*.${primaryDomain}`)) {
929
+ log.warn(`⚠️ Existing cert for ${primaryDomain} lacks *.${primaryDomain} SAN — clearing stale cert for combined re-issuance`);
930
+ const certDir = path.join(greenlockStorePath, 'live', primaryDomain);
931
+ try { fs.rmSync(certDir, { recursive: true, force: true }); } catch {}
932
+ pems = null;
933
+ }
934
+
937
935
  if (pems) return pems;
938
936
 
939
937
  const certSubject = primaryDomain.startsWith('*.') ? wildcardRoot(primaryDomain) : primaryDomain;
940
- log.warn(`⚠️ Bun runtime detected and cert files missing for ${primaryDomain}; requesting certificate via Greenlock before HTTPS bind`);
938
+ log.warn(`⚠️ Bun: requesting ${needsWildcard ? 'combined wildcard' : ''} certificate for ${certSubject} via Greenlock before HTTPS bind`);
941
939
  try {
942
940
  await greenlockRuntime.get({ servername: certSubject });
943
941
  } catch (error) {
@@ -963,7 +961,7 @@ class Roster {
963
961
  const primaryDomain = Object.keys(portData.virtualServers)[0];
964
962
  // Under Bun, avoid glx.httpsServer fallback (may serve invalid TLS on :443).
965
963
  // Require concrete PEM files and create native https server directly.
966
- const defaultPems = await ensureBunDefaultPems(primaryDomain);
964
+ let defaultPems = await ensureBunDefaultPems(primaryDomain);
967
965
  httpsServer = https.createServer({
968
966
  ...tlsOpts,
969
967
  key: defaultPems.key,
@@ -977,6 +975,21 @@ class Roster {
977
975
  .catch(callback);
978
976
  }
979
977
  }, dispatcher);
978
+ const reloadBunDefaultTls = async (servername, reason) => {
979
+ const nextPems = await issueAndReloadPemsForServername(servername);
980
+ if (!nextPems) return false;
981
+ defaultPems = nextPems;
982
+ if (typeof httpsServer.setSecureContext === 'function') {
983
+ try {
984
+ httpsServer.setSecureContext({ key: defaultPems.key, cert: defaultPems.cert });
985
+ log.info(`🔄 Bun TLS default certificate reloaded on port ${portNum} (${reason})`);
986
+ } catch (error) {
987
+ log.warn(`⚠️ Failed to hot-reload Bun TLS context on port ${portNum}: ${error?.message || error}`);
988
+ }
989
+ }
990
+ return true;
991
+ };
992
+ bunTlsHotReloadHandlers.push(reloadBunDefaultTls);
980
993
  log.warn(`⚠️ Bun runtime detected: using file-based TLS with SNI for ${primaryDomain} on port ${portNum}`);
981
994
  } else {
982
995
  httpsServer = glx.httpsServer(tlsOpts, dispatcher);
@@ -1036,12 +1049,53 @@ class Roster {
1036
1049
  });
1037
1050
  }
1038
1051
  }
1052
+
1053
+ if (isBunRuntime && !this.combineWildcardCerts && this.wildcardZones.size > 0 && bunTlsHotReloadHandlers.length > 0) {
1054
+ const retryDelayMs = Number.isFinite(Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_RETRY_MS))
1055
+ ? Math.max(1000, Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_RETRY_MS))
1056
+ : 30000;
1057
+ const maxAttempts = Number.isFinite(Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
1058
+ ? Math.max(0, Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
1059
+ : 0; // 0 = retry forever
1060
+
1061
+ for (const zone of this.wildcardZones) {
1062
+ const bootstrapHost = `bun-bootstrap.${zone}`;
1063
+ const attemptPrewarm = async (attempt = 1) => {
1064
+ try {
1065
+ log.warn(`⚠️ Bun runtime detected: prewarming wildcard certificate via ${bootstrapHost} (attempt ${attempt})`);
1066
+ let reloaded = false;
1067
+ for (const reloadTls of bunTlsHotReloadHandlers) {
1068
+ // Trigger issuance + immediately hot-reload default TLS context when ready.
1069
+ reloaded = (await reloadTls(bootstrapHost, `prewarm ${bootstrapHost} attempt ${attempt}`)) || reloaded;
1070
+ }
1071
+ if (!reloaded) {
1072
+ throw new Error(`No certificate could be loaded for ${bootstrapHost}`);
1073
+ }
1074
+ log.info(`✅ Bun wildcard prewarm succeeded for ${zone} on attempt ${attempt}`);
1075
+ } catch (error) {
1076
+ log.warn(`⚠️ Bun wildcard prewarm failed for ${zone} (attempt ${attempt}): ${error?.message || error}`);
1077
+ if (maxAttempts > 0 && attempt >= maxAttempts) {
1078
+ log.warn(`⚠️ Bun wildcard prewarm stopped for ${zone} after ${attempt} attempts`);
1079
+ return;
1080
+ }
1081
+ setTimeout(() => {
1082
+ attemptPrewarm(attempt + 1).catch(() => {});
1083
+ }, retryDelayMs);
1084
+ }
1085
+ };
1086
+
1087
+ // Background prewarm + retries so HTTPS startup is not blocked by DNS propagation timing.
1088
+ attemptPrewarm().catch(() => {});
1089
+ }
1090
+ }
1039
1091
  });
1040
1092
  }
1041
1093
  }
1042
1094
 
1043
1095
  module.exports = Roster;
1096
+ module.exports.isBunRuntime = isBunRuntime;
1044
1097
  module.exports.wildcardRoot = wildcardRoot;
1045
1098
  module.exports.hostMatchesWildcard = hostMatchesWildcard;
1046
1099
  module.exports.wildcardSubjectForHost = wildcardSubjectForHost;
1047
- module.exports.buildCertLookupCandidates = buildCertLookupCandidates;
1100
+ module.exports.buildCertLookupCandidates = buildCertLookupCandidates;
1101
+ module.exports.certCoversName = certCoversName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.1.34",
3
+ "version": "2.2.1",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,6 +8,7 @@ const http = require('http');
8
8
  const os = require('os');
9
9
  const Roster = require('../index.js');
10
10
  const {
11
+ isBunRuntime,
11
12
  wildcardRoot,
12
13
  hostMatchesWildcard,
13
14
  wildcardSubjectForHost,
@@ -324,6 +325,14 @@ describe('Roster', () => {
324
325
  else process.env.ROSTER_COMBINE_WILDCARD_CERTS = previous;
325
326
  }
326
327
  });
328
+ it('defaults combineWildcardCerts based on isBunRuntime', () => {
329
+ const roster = new Roster({ local: false });
330
+ assert.strictEqual(roster.combineWildcardCerts, isBunRuntime);
331
+ });
332
+ it('explicit combineWildcardCerts=false overrides Bun default', () => {
333
+ const roster = new Roster({ local: false, combineWildcardCerts: false });
334
+ assert.strictEqual(roster.combineWildcardCerts, false);
335
+ });
327
336
  });
328
337
 
329
338
  describe('register (normal domain)', () => {