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 +91 -37
- package/package.json +1 -1
- package/test/roster-server.test.js +9 -0
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,
|
|
239
|
-
: parseBooleanFlag(process.env.ROSTER_COMBINE_WILDCARD_CERTS,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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)', () => {
|