roster-server 2.4.11 → 2.4.13
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/README.md +192 -404
- package/index.js +420 -128
- package/lib/static-site-handler.js +36 -27
- package/package.json +1 -1
- package/skills/roster-server/SKILL.md +50 -338
- package/test/https-integration.test.js +149 -0
- package/test/lifecycle.test.js +469 -0
- package/test/roster-server.test.js +59 -14
- package/test/scanner-blocker.test.js +3 -6
- package/test/static-and-tls.test.js +273 -0
- package/vendor/greenlock-express/greenlock-express.js +1 -1
- package/vendor/greenlock-express/servers.js +2 -1
- package/vendor/greenlock-express/single.js +1 -1
- package/vendor/greenlock-express/worker.js +2 -2
package/index.js
CHANGED
|
@@ -10,6 +10,29 @@ const GreenlockShim = require('./vendor/greenlock-express/greenlock-shim.js');
|
|
|
10
10
|
const { resolveSiteApp } = require('./lib/resolve-site-app.js');
|
|
11
11
|
const log = require('lemonlog')('roster');
|
|
12
12
|
|
|
13
|
+
function requestError(error, res) {
|
|
14
|
+
log.error('Request handler failed:', error?.message || error);
|
|
15
|
+
if (res.destroyed || res.writableEnded) return;
|
|
16
|
+
if (res.headersSent) {
|
|
17
|
+
res.destroy(error);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
21
|
+
res.end('Internal Server Error');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function invokeRequest(handler, req, res) {
|
|
25
|
+
try {
|
|
26
|
+
const result = handler(req, res);
|
|
27
|
+
if (result && typeof result.then === 'function') {
|
|
28
|
+
return Promise.resolve(result).catch(error => requestError(error, res));
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
requestError(error, res);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
const isBunRuntime = typeof Bun !== 'undefined' || (typeof process !== 'undefined' && process.release?.name === 'bun');
|
|
14
37
|
|
|
15
38
|
// CRC32 implementation for deterministic port assignment
|
|
@@ -129,6 +152,8 @@ class VirtualServer extends EventEmitter {
|
|
|
129
152
|
this.domain = domain;
|
|
130
153
|
this.requestListeners = [];
|
|
131
154
|
this.upgradeListeners = [];
|
|
155
|
+
this._closeHooks = [];
|
|
156
|
+
this._closed = false;
|
|
132
157
|
|
|
133
158
|
// Simulate http.Server properties
|
|
134
159
|
this.listening = false;
|
|
@@ -156,6 +181,7 @@ class VirtualServer extends EventEmitter {
|
|
|
156
181
|
// Socket.IO compatibility methods
|
|
157
182
|
listeners(event) {
|
|
158
183
|
if (event === 'request') {
|
|
184
|
+
if (this.requestListeners.length === 0 && this.fallbackHandler) return [this.fallbackHandler];
|
|
159
185
|
return this.requestListeners.slice();
|
|
160
186
|
} else if (event === 'upgrade') {
|
|
161
187
|
return this.upgradeListeners.slice();
|
|
@@ -179,9 +205,10 @@ class VirtualServer extends EventEmitter {
|
|
|
179
205
|
}
|
|
180
206
|
|
|
181
207
|
removeAllListeners(event) {
|
|
182
|
-
if (event === 'request') {
|
|
208
|
+
if (event === undefined || event === 'request') {
|
|
183
209
|
this.requestListeners = [];
|
|
184
|
-
}
|
|
210
|
+
}
|
|
211
|
+
if (event === undefined || event === 'upgrade') {
|
|
185
212
|
this.upgradeListeners = [];
|
|
186
213
|
}
|
|
187
214
|
return super.removeAllListeners(event);
|
|
@@ -189,34 +216,34 @@ class VirtualServer extends EventEmitter {
|
|
|
189
216
|
|
|
190
217
|
// Simulate other http.Server methods
|
|
191
218
|
listen() { this.listening = true; return this; }
|
|
192
|
-
close() {
|
|
219
|
+
close(callback) {
|
|
220
|
+
this.listening = false;
|
|
221
|
+
if (!this._closed) {
|
|
222
|
+
this._closed = true;
|
|
223
|
+
this.emit('close');
|
|
224
|
+
}
|
|
225
|
+
if (callback) process.nextTick(callback);
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
onClose(handler) {
|
|
229
|
+
if (typeof handler !== 'function') throw new TypeError('Close hook must be a function');
|
|
230
|
+
if (this._closed) throw new Error('Virtual server is closed');
|
|
231
|
+
this._closeHooks.push(handler);
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
193
234
|
setTimeout() { return this; }
|
|
194
235
|
|
|
195
236
|
// Process request with this virtual server's listeners
|
|
196
237
|
processRequest(req, res) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
handled = true;
|
|
203
|
-
return originalEnd.apply(this, args);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// Try all listeners
|
|
207
|
-
for (const listener of this.requestListeners) {
|
|
208
|
-
if (!handled) {
|
|
209
|
-
listener(req, res);
|
|
238
|
+
const listeners = this.requestListeners.slice();
|
|
239
|
+
if (listeners.length > 0) {
|
|
240
|
+
for (const listener of listeners) {
|
|
241
|
+
if (res.writableEnded || res.destroyed) break;
|
|
242
|
+
invokeRequest(listener.bind(this), req, res);
|
|
210
243
|
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
res.end = originalEnd;
|
|
215
|
-
|
|
216
|
-
// If no listener handled the request, try fallback handler
|
|
217
|
-
if (!handled && this.fallbackHandler) {
|
|
218
|
-
this.fallbackHandler(req, res);
|
|
219
|
-
} else if (!handled) {
|
|
244
|
+
} else if (this.fallbackHandler) {
|
|
245
|
+
return invokeRequest(this.fallbackHandler, req, res);
|
|
246
|
+
} else {
|
|
220
247
|
res.writeHead(404);
|
|
221
248
|
res.end('No handler found');
|
|
222
249
|
}
|
|
@@ -255,6 +282,25 @@ class Roster {
|
|
|
255
282
|
this.assignedPorts = new Set(); // Track ports assigned to domains (not OS availability)
|
|
256
283
|
this._sitesByPort = {};
|
|
257
284
|
this._initialized = false;
|
|
285
|
+
this._closing = false;
|
|
286
|
+
this._initTask = null;
|
|
287
|
+
this._initPromise = null;
|
|
288
|
+
this._startPromise = null;
|
|
289
|
+
this._closePromise = null;
|
|
290
|
+
this._ownedServers = new Set();
|
|
291
|
+
this._sockets = new Set();
|
|
292
|
+
this._upgradedSockets = new Set();
|
|
293
|
+
this._responses = new Set();
|
|
294
|
+
this._attachments = new Map();
|
|
295
|
+
this._retryTimers = new Set();
|
|
296
|
+
this._pendingListens = new Set();
|
|
297
|
+
this._certificateChecks = new Map();
|
|
298
|
+
this._secureContexts = new Map();
|
|
299
|
+
this._contextLoads = new Map();
|
|
300
|
+
this.closeTimeoutMs = options.closeTimeoutMs ?? 30000;
|
|
301
|
+
if (!Number.isFinite(this.closeTimeoutMs) || this.closeTimeoutMs <= 0) {
|
|
302
|
+
throw new TypeError('closeTimeoutMs must be a positive finite number');
|
|
303
|
+
}
|
|
258
304
|
this._sniCallback = null;
|
|
259
305
|
this.hostname = options.hostname ?? '::';
|
|
260
306
|
this.filename = options.filename || 'index';
|
|
@@ -322,10 +368,12 @@ class Roster {
|
|
|
322
368
|
return;
|
|
323
369
|
}
|
|
324
370
|
|
|
371
|
+
const siteKeyForDomain = (domain) => this.defaultPort === 443 ? domain : `${domain}:443`;
|
|
325
372
|
const sites = fs.readdirSync(this.wwwPath, { withFileTypes: true })
|
|
326
373
|
.filter(dirent => dirent.isDirectory());
|
|
327
374
|
|
|
328
375
|
for (const dirent of sites) {
|
|
376
|
+
this._assertOpen();
|
|
329
377
|
const domain = dirent.name;
|
|
330
378
|
const domainPath = path.join(this.wwwPath, domain);
|
|
331
379
|
|
|
@@ -342,6 +390,8 @@ class Roster {
|
|
|
342
390
|
continue;
|
|
343
391
|
}
|
|
344
392
|
|
|
393
|
+
this._assertOpen();
|
|
394
|
+
|
|
345
395
|
const { siteApp, type } = resolved;
|
|
346
396
|
|
|
347
397
|
if (domain.startsWith('*.')) {
|
|
@@ -350,7 +400,7 @@ class Roster {
|
|
|
350
400
|
continue;
|
|
351
401
|
}
|
|
352
402
|
this.domains.push(domain);
|
|
353
|
-
this.sites[domain] = siteApp;
|
|
403
|
+
this.sites[siteKeyForDomain(domain)] = siteApp;
|
|
354
404
|
const root = wildcardRoot(domain);
|
|
355
405
|
if (root) this.wildcardZones.add(root);
|
|
356
406
|
log.info(`(✔) Loaded wildcard site: https://${domain}${type === 'static' ? ' (static)' : ''}`);
|
|
@@ -358,7 +408,7 @@ class Roster {
|
|
|
358
408
|
const domainEntries = [domain, `www.${domain}`];
|
|
359
409
|
this.domains.push(...domainEntries);
|
|
360
410
|
domainEntries.forEach(d => {
|
|
361
|
-
this.sites[d] = siteApp;
|
|
411
|
+
this.sites[siteKeyForDomain(d)] = siteApp;
|
|
362
412
|
});
|
|
363
413
|
log.info(`(✔) Loaded site: https://${domain}${type === 'static' ? ' (static)' : ''}`);
|
|
364
414
|
}
|
|
@@ -527,7 +577,7 @@ class Roster {
|
|
|
527
577
|
getHandlerForPortData(host, portData) {
|
|
528
578
|
const virtualServer = portData.virtualServers[host];
|
|
529
579
|
const appHandler = portData.appHandlers[host];
|
|
530
|
-
if (
|
|
580
|
+
if (Object.hasOwn(portData.virtualServers, host)) return { virtualServer, appHandler };
|
|
531
581
|
for (const key of Object.keys(portData.appHandlers)) {
|
|
532
582
|
if (key.startsWith('*.') && hostMatchesWildcard(host, key)) {
|
|
533
583
|
return {
|
|
@@ -540,6 +590,7 @@ class Roster {
|
|
|
540
590
|
}
|
|
541
591
|
|
|
542
592
|
handleRequest(req, res) {
|
|
593
|
+
if (!this._beginRequest(res)) return;
|
|
543
594
|
const host = req.headers.host || '';
|
|
544
595
|
const hostWithoutPort = host.split(':')[0];
|
|
545
596
|
const normalizedHost = hostWithoutPort.toLowerCase();
|
|
@@ -556,7 +607,7 @@ class Roster {
|
|
|
556
607
|
|
|
557
608
|
const siteApp = this.getHandlerForHost(hostWithoutPort);
|
|
558
609
|
if (siteApp) {
|
|
559
|
-
siteApp
|
|
610
|
+
return invokeRequest(siteApp, req, res);
|
|
560
611
|
} else {
|
|
561
612
|
res.writeHead(404);
|
|
562
613
|
res.end('Site not found');
|
|
@@ -564,6 +615,7 @@ class Roster {
|
|
|
564
615
|
}
|
|
565
616
|
|
|
566
617
|
register(domainString, requestHandler) {
|
|
618
|
+
this._assertOpen();
|
|
567
619
|
if (!domainString) {
|
|
568
620
|
throw new Error('Domain is required');
|
|
569
621
|
}
|
|
@@ -603,6 +655,7 @@ class Roster {
|
|
|
603
655
|
}
|
|
604
656
|
|
|
605
657
|
use(plugin) {
|
|
658
|
+
this._assertOpen();
|
|
606
659
|
if (typeof plugin !== 'function') {
|
|
607
660
|
throw new Error('plugin must be a function');
|
|
608
661
|
}
|
|
@@ -611,14 +664,20 @@ class Roster {
|
|
|
611
664
|
}
|
|
612
665
|
|
|
613
666
|
_runRequestPlugins(req, res, context) {
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
667
|
+
try {
|
|
668
|
+
for (const plugin of this.plugins) {
|
|
669
|
+
const handled = plugin(req, res, context);
|
|
670
|
+
if (handled && typeof handled.then === 'function') {
|
|
671
|
+
Promise.resolve(handled).catch(() => {});
|
|
672
|
+
throw new Error('Request plugins must be synchronous');
|
|
673
|
+
}
|
|
674
|
+
if (handled === true) return true;
|
|
618
675
|
}
|
|
619
|
-
|
|
676
|
+
return false;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
requestError(error, res);
|
|
679
|
+
return true;
|
|
620
680
|
}
|
|
621
|
-
return false;
|
|
622
681
|
}
|
|
623
682
|
|
|
624
683
|
parseDomainWithPort(domainString) {
|
|
@@ -663,6 +722,9 @@ class Roster {
|
|
|
663
722
|
|
|
664
723
|
// Assign port to domain, detecting collisions with already assigned ports
|
|
665
724
|
assignPortToDomain(domain) {
|
|
725
|
+
if (this.assignedPorts.size >= this.maxLocalPort - this.minLocalPort + 1) {
|
|
726
|
+
throw new Error('Local port range is exhausted');
|
|
727
|
+
}
|
|
666
728
|
let port = domainToPort(domain, this.minLocalPort, this.maxLocalPort);
|
|
667
729
|
|
|
668
730
|
// If port is already assigned to another domain, increment until we find a free one
|
|
@@ -734,6 +796,7 @@ class Roster {
|
|
|
734
796
|
_initSiteHandlers() {
|
|
735
797
|
this._sitesByPort = {};
|
|
736
798
|
for (const [hostKey, siteApp] of Object.entries(this.sites)) {
|
|
799
|
+
this._assertOpen();
|
|
737
800
|
if (hostKey.startsWith('www.')) continue;
|
|
738
801
|
const { domain, port } = this.parseDomainWithPort(hostKey);
|
|
739
802
|
if (!this._sitesByPort[port]) {
|
|
@@ -747,7 +810,13 @@ class Roster {
|
|
|
747
810
|
this._sitesByPort[port].virtualServers[domain] = virtualServer;
|
|
748
811
|
this.domainServers[domain] = virtualServer;
|
|
749
812
|
|
|
750
|
-
|
|
813
|
+
let appHandler;
|
|
814
|
+
virtualServer.fallbackHandler = (req, res) => {
|
|
815
|
+
if (appHandler) return appHandler(req, res);
|
|
816
|
+
res.writeHead(404);
|
|
817
|
+
res.end('Site not found');
|
|
818
|
+
};
|
|
819
|
+
appHandler = siteApp(virtualServer);
|
|
751
820
|
this._sitesByPort[port].appHandlers[domain] = appHandler;
|
|
752
821
|
if (!domain.startsWith('*.')) {
|
|
753
822
|
this._sitesByPort[port].appHandlers[`www.${domain}`] = appHandler;
|
|
@@ -757,6 +826,7 @@ class Roster {
|
|
|
757
826
|
|
|
758
827
|
_createDispatcher(portData) {
|
|
759
828
|
return (req, res) => {
|
|
829
|
+
if (!this._beginRequest(res)) return;
|
|
760
830
|
const host = req.headers.host || '';
|
|
761
831
|
const hostWithoutPort = host.split(':')[0].toLowerCase();
|
|
762
832
|
const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
|
|
@@ -765,7 +835,7 @@ class Roster {
|
|
|
765
835
|
|
|
766
836
|
if (hostWithoutPort.startsWith('www.')) {
|
|
767
837
|
const protocol = this.local ? 'http' : 'https';
|
|
768
|
-
res.writeHead(301, { Location: `${protocol}://${
|
|
838
|
+
res.writeHead(301, { Location: `${protocol}://${host.toLowerCase().slice(4)}${req.url}` });
|
|
769
839
|
res.end();
|
|
770
840
|
return;
|
|
771
841
|
}
|
|
@@ -779,10 +849,9 @@ class Roster {
|
|
|
779
849
|
const { virtualServer, appHandler } = resolved;
|
|
780
850
|
|
|
781
851
|
if (virtualServer && virtualServer.requestListeners.length > 0) {
|
|
782
|
-
virtualServer.fallbackHandler = appHandler;
|
|
783
852
|
virtualServer.processRequest(req, res);
|
|
784
853
|
} else if (appHandler) {
|
|
785
|
-
appHandler
|
|
854
|
+
return invokeRequest(appHandler, req, res);
|
|
786
855
|
} else {
|
|
787
856
|
res.writeHead(404);
|
|
788
857
|
res.end('Site not found');
|
|
@@ -790,8 +859,16 @@ class Roster {
|
|
|
790
859
|
};
|
|
791
860
|
}
|
|
792
861
|
|
|
862
|
+
_trackUpgrade(socket) {
|
|
863
|
+
if (typeof socket.once !== 'function') return;
|
|
864
|
+
this._upgradedSockets.add(socket);
|
|
865
|
+
socket.once('close', () => this._upgradedSockets.delete(socket));
|
|
866
|
+
}
|
|
867
|
+
|
|
793
868
|
_createUpgradeHandler(portData) {
|
|
794
869
|
return (req, socket, head) => {
|
|
870
|
+
if (this._closing) { socket.destroy(); return; }
|
|
871
|
+
this._trackUpgrade(socket);
|
|
795
872
|
const host = req.headers.host || '';
|
|
796
873
|
const hostWithoutPort = host.split(':')[0].toLowerCase();
|
|
797
874
|
const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
|
|
@@ -805,38 +882,69 @@ class Roster {
|
|
|
805
882
|
};
|
|
806
883
|
}
|
|
807
884
|
|
|
808
|
-
|
|
809
|
-
this.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
} catch (error) {
|
|
818
|
-
callback(error);
|
|
819
|
-
return;
|
|
885
|
+
async _resolveSecureContext(servername) {
|
|
886
|
+
this._assertOpen();
|
|
887
|
+
const host = this._normalizeHostInput(servername).trim().toLowerCase();
|
|
888
|
+
for (const subject of buildCertLookupCandidates(host)) {
|
|
889
|
+
let pending = this._contextLoads.get(subject);
|
|
890
|
+
if (!pending) {
|
|
891
|
+
pending = this._loadSecureContext(subject).finally(() => this._contextLoads.delete(subject));
|
|
892
|
+
this._contextLoads.set(subject, pending);
|
|
820
893
|
}
|
|
894
|
+
const context = await pending;
|
|
895
|
+
if (context) return context;
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
821
899
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
900
|
+
async _loadSecureContext(subject) {
|
|
901
|
+
const directory = path.join(this.greenlockStorePath, 'live', subject);
|
|
902
|
+
const files = ['privkey.pem', 'cert.pem', 'chain.pem'].map(name => path.join(directory, name));
|
|
903
|
+
let stats;
|
|
904
|
+
try {
|
|
905
|
+
stats = await Promise.all(files.map(file => fs.promises.stat(file, { bigint: true })));
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error;
|
|
908
|
+
this._secureContexts.delete(subject);
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
this._assertOpen();
|
|
912
|
+
const version = stats.map(stat => `${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`).join('|');
|
|
913
|
+
const cached = this._secureContexts.get(subject);
|
|
914
|
+
if (cached && cached.version === version) return cached.context;
|
|
915
|
+
const [key, cert, chain] = await Promise.all(files.map(file => fs.promises.readFile(file, 'utf8')));
|
|
916
|
+
this._assertOpen();
|
|
917
|
+
const context = tls.createSecureContext({ key, cert: cert + chain });
|
|
918
|
+
this._secureContexts.set(subject, { version, context });
|
|
919
|
+
return context;
|
|
920
|
+
}
|
|
827
921
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
922
|
+
_checkCertificate(servername) {
|
|
923
|
+
this._assertOpen();
|
|
924
|
+
if (!this._certificateChecks.has(servername)) {
|
|
925
|
+
const pending = Promise.resolve().then(() => {
|
|
926
|
+
this._assertOpen();
|
|
927
|
+
return this._greenlockRuntime.get({ servername });
|
|
928
|
+
}).finally(() => this._certificateChecks.delete(servername));
|
|
929
|
+
this._certificateChecks.set(servername, pending);
|
|
930
|
+
}
|
|
931
|
+
return this._certificateChecks.get(servername);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async _getSecureContext(servername, issue = true) {
|
|
935
|
+
const host = this._normalizeHostInput(servername).trim().toLowerCase();
|
|
936
|
+
let context = await this._resolveSecureContext(host);
|
|
937
|
+
if (!context && issue && this._greenlockRuntime && host) {
|
|
938
|
+
await this._checkCertificate(host);
|
|
939
|
+
context = await this._resolveSecureContext(host);
|
|
940
|
+
}
|
|
941
|
+
if (!context) throw new Error(`No certificate files available for ${servername}`);
|
|
942
|
+
return context;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
_initSniResolver() {
|
|
946
|
+
this._sniCallback = (servername, callback) => {
|
|
947
|
+
this._getSecureContext(servername).then(context => callback(null, context), callback);
|
|
840
948
|
};
|
|
841
949
|
}
|
|
842
950
|
|
|
@@ -849,6 +957,7 @@ class Roster {
|
|
|
849
957
|
staging: this.staging,
|
|
850
958
|
skipDryRun: this.skipLocalCheck,
|
|
851
959
|
skipChallengeTest: this.skipLocalCheck,
|
|
960
|
+
renew: false,
|
|
852
961
|
notify: (event, details) => {
|
|
853
962
|
const eventDomain = (() => {
|
|
854
963
|
if (!details || typeof details !== 'object') return null;
|
|
@@ -922,12 +1031,12 @@ class Roster {
|
|
|
922
1031
|
}
|
|
923
1032
|
|
|
924
1033
|
_startCertificateRenewLoop() {
|
|
925
|
-
if (!this._greenlockRuntime || this._certificateRenewTimer) return;
|
|
1034
|
+
if (this._closing || !this._greenlockRuntime || this._certificateRenewTimer) return;
|
|
926
1035
|
const subjects = this._getManagedCertificateSubjects();
|
|
927
1036
|
if (subjects.length === 0) return;
|
|
928
1037
|
this._certificateRenewTimer = setInterval(() => {
|
|
929
1038
|
subjects.forEach((subject) => {
|
|
930
|
-
this.
|
|
1039
|
+
this._checkCertificate(subject).catch((error) => {
|
|
931
1040
|
log.warn(`⚠️ Certificate renew check failed for ${subject}: ${error?.message || error}`);
|
|
932
1041
|
});
|
|
933
1042
|
});
|
|
@@ -938,6 +1047,7 @@ class Roster {
|
|
|
938
1047
|
}
|
|
939
1048
|
|
|
940
1049
|
async ensureCertificate(servername) {
|
|
1050
|
+
this._assertOpen();
|
|
941
1051
|
if (this.local) {
|
|
942
1052
|
throw new Error('ensureCertificate() is not available in local mode');
|
|
943
1053
|
}
|
|
@@ -953,7 +1063,7 @@ class Roster {
|
|
|
953
1063
|
if (!this._greenlockRuntime) {
|
|
954
1064
|
throw new Error('autoCertificates is disabled; enable { autoCertificates: true } to issue certificates automatically');
|
|
955
1065
|
}
|
|
956
|
-
await this.
|
|
1066
|
+
await this._checkCertificate(normalizedServername);
|
|
957
1067
|
pems = this._resolvePemsForServername(normalizedServername);
|
|
958
1068
|
if (!pems) {
|
|
959
1069
|
throw new Error(`Certificate issuance completed but no PEM files were found for ${normalizedServername}`);
|
|
@@ -962,6 +1072,7 @@ class Roster {
|
|
|
962
1072
|
}
|
|
963
1073
|
|
|
964
1074
|
loadCertificate(servername) {
|
|
1075
|
+
this._assertOpen();
|
|
965
1076
|
if (this.local) {
|
|
966
1077
|
throw new Error('loadCertificate() is not available in local mode');
|
|
967
1078
|
}
|
|
@@ -979,9 +1090,28 @@ class Roster {
|
|
|
979
1090
|
return pems;
|
|
980
1091
|
}
|
|
981
1092
|
|
|
982
|
-
|
|
983
|
-
if (this.
|
|
1093
|
+
_assertOpen() {
|
|
1094
|
+
if (this._closing) throw new Error('Roster is closing or closed');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
init() {
|
|
1098
|
+
if (this._closing) return Promise.reject(new Error('Roster is closing or closed'));
|
|
1099
|
+
if (this._initPromise) return this._initPromise;
|
|
1100
|
+
this._initTask = this._initialize();
|
|
1101
|
+
this._initPromise = this._initTask.catch(async error => {
|
|
1102
|
+
if (!this._closing) {
|
|
1103
|
+
try { await this.close(); } catch (cleanupError) {
|
|
1104
|
+
throw new AggregateError([error, cleanupError], 'Initialization and cleanup failed');
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
throw error;
|
|
1108
|
+
});
|
|
1109
|
+
return this._initPromise;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async _initialize() {
|
|
984
1113
|
await this.loadSites();
|
|
1114
|
+
this._assertOpen();
|
|
985
1115
|
if (!this.local) {
|
|
986
1116
|
this.generateConfigJson();
|
|
987
1117
|
if (this.autoCertificates) {
|
|
@@ -989,6 +1119,7 @@ class Roster {
|
|
|
989
1119
|
}
|
|
990
1120
|
}
|
|
991
1121
|
this._initSiteHandlers();
|
|
1122
|
+
this._assertOpen();
|
|
992
1123
|
if (!this.local) {
|
|
993
1124
|
this._initSniResolver();
|
|
994
1125
|
if (this.autoCertificates) {
|
|
@@ -999,6 +1130,147 @@ class Roster {
|
|
|
999
1130
|
return this;
|
|
1000
1131
|
}
|
|
1001
1132
|
|
|
1133
|
+
_beginRequest(res) {
|
|
1134
|
+
if (this._closing) {
|
|
1135
|
+
res.writeHead(503, { Connection: 'close' });
|
|
1136
|
+
res.end('Server is closing');
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
if (typeof res.once === 'function') {
|
|
1140
|
+
this._responses.add(res);
|
|
1141
|
+
const done = () => {
|
|
1142
|
+
this._responses.delete(res);
|
|
1143
|
+
res.removeListener('finish', done);
|
|
1144
|
+
res.removeListener('close', done);
|
|
1145
|
+
};
|
|
1146
|
+
res.once('finish', done);
|
|
1147
|
+
res.once('close', done);
|
|
1148
|
+
}
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
_listen(server, port, hostname) {
|
|
1153
|
+
this._assertOpen();
|
|
1154
|
+
this._ownedServers.add(server);
|
|
1155
|
+
server.on('connection', socket => {
|
|
1156
|
+
this._sockets.add(socket);
|
|
1157
|
+
socket.once('close', () => this._sockets.delete(socket));
|
|
1158
|
+
if (this._closing) socket.destroy();
|
|
1159
|
+
});
|
|
1160
|
+
return new Promise((resolve, reject) => {
|
|
1161
|
+
const done = error => {
|
|
1162
|
+
server.removeListener('error', failed);
|
|
1163
|
+
server.removeListener('listening', listening);
|
|
1164
|
+
this._pendingListens.delete(cancel);
|
|
1165
|
+
if (error) reject(error);
|
|
1166
|
+
else resolve();
|
|
1167
|
+
};
|
|
1168
|
+
const failed = error => done(error);
|
|
1169
|
+
const listening = () => done();
|
|
1170
|
+
const cancel = () => {
|
|
1171
|
+
// A pending DNS lookup may complete after close().
|
|
1172
|
+
server.once('listening', () => server.close());
|
|
1173
|
+
done(new Error('Roster is closing or closed'));
|
|
1174
|
+
};
|
|
1175
|
+
this._pendingListens.add(cancel);
|
|
1176
|
+
server.once('error', failed);
|
|
1177
|
+
server.once('listening', listening);
|
|
1178
|
+
server.on('error', error => log.error(`Server error on port ${port}:`, error.message));
|
|
1179
|
+
try { server.listen(port, hostname); } catch (error) { done(error); }
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
close() {
|
|
1184
|
+
if (this._closePromise) return this._closePromise;
|
|
1185
|
+
this._closing = true;
|
|
1186
|
+
this._closePromise = this._close();
|
|
1187
|
+
return this._closePromise;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
async _close() {
|
|
1191
|
+
const errors = [];
|
|
1192
|
+
let timer;
|
|
1193
|
+
const timeoutError = new Error('Roster close timed out');
|
|
1194
|
+
const timeout = new Promise((resolve, reject) => {
|
|
1195
|
+
timer = setTimeout(() => {
|
|
1196
|
+
for (const res of this._responses) res.destroy();
|
|
1197
|
+
for (const socket of this._sockets) socket.destroy();
|
|
1198
|
+
for (const server of this._ownedServers) server.closeAllConnections();
|
|
1199
|
+
reject(timeoutError);
|
|
1200
|
+
}, this.closeTimeoutMs);
|
|
1201
|
+
});
|
|
1202
|
+
const wait = async promise => {
|
|
1203
|
+
try { await Promise.race([promise, timeout]); } catch (error) {
|
|
1204
|
+
if (!errors.includes(error)) errors.push(error);
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
try {
|
|
1208
|
+
clearInterval(this._certificateRenewTimer);
|
|
1209
|
+
this._certificateRenewTimer = null;
|
|
1210
|
+
for (const timer of this._retryTimers) clearTimeout(timer);
|
|
1211
|
+
this._retryTimers.clear();
|
|
1212
|
+
for (const cancel of this._pendingListens) cancel();
|
|
1213
|
+
if (this._initTask) await wait(this._initTask.catch(() => {}));
|
|
1214
|
+
|
|
1215
|
+
const draining = [...this._responses].map(res => new Promise(resolve => {
|
|
1216
|
+
const done = () => {
|
|
1217
|
+
res.removeListener('finish', done);
|
|
1218
|
+
res.removeListener('close', done);
|
|
1219
|
+
resolve();
|
|
1220
|
+
};
|
|
1221
|
+
res.once('finish', done);
|
|
1222
|
+
res.once('close', done);
|
|
1223
|
+
}));
|
|
1224
|
+
const closeServer = server => new Promise(resolve => {
|
|
1225
|
+
try {
|
|
1226
|
+
server.close(error => {
|
|
1227
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') errors.push(error);
|
|
1228
|
+
resolve();
|
|
1229
|
+
});
|
|
1230
|
+
server.closeIdleConnections();
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
errors.push(error);
|
|
1233
|
+
resolve();
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
// Bun's close() releases its native server handle before upgraded
|
|
1237
|
+
// connections finish, preventing a later closeAllConnections().
|
|
1238
|
+
const closing = isBunRuntime ? [] : [...this._ownedServers].map(closeServer);
|
|
1239
|
+
const virtualServers = Object.values(this._sitesByPort).flatMap(portData => Object.values(portData.virtualServers));
|
|
1240
|
+
for (const server of virtualServers) {
|
|
1241
|
+
try { server.close(); } catch (error) { errors.push(error); }
|
|
1242
|
+
}
|
|
1243
|
+
for (const socket of this._upgradedSockets) socket.destroy();
|
|
1244
|
+
await wait(Promise.all([
|
|
1245
|
+
...closing, ...draining,
|
|
1246
|
+
Promise.allSettled([...this._certificateChecks.values(), ...this._contextLoads.values()])
|
|
1247
|
+
]));
|
|
1248
|
+
if (isBunRuntime) {
|
|
1249
|
+
for (const server of this._ownedServers) server.closeAllConnections();
|
|
1250
|
+
await wait(Promise.all([...this._ownedServers].map(closeServer)));
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const hooks = virtualServers.flatMap(server => server._closeHooks.splice(0).map(hook =>
|
|
1254
|
+
Promise.resolve().then(() => hook())
|
|
1255
|
+
));
|
|
1256
|
+
await wait(Promise.allSettled(hooks).then(results => {
|
|
1257
|
+
for (const result of results) {
|
|
1258
|
+
if (result.status === 'rejected') errors.push(result.reason);
|
|
1259
|
+
}
|
|
1260
|
+
}));
|
|
1261
|
+
} finally {
|
|
1262
|
+
clearTimeout(timer);
|
|
1263
|
+
for (const [server, handlers] of this._attachments) {
|
|
1264
|
+
server.removeListener('request', handlers.request);
|
|
1265
|
+
server.removeListener('upgrade', handlers.upgrade);
|
|
1266
|
+
}
|
|
1267
|
+
this._attachments.clear();
|
|
1268
|
+
this._secureContexts.clear();
|
|
1269
|
+
this._ownedServers.clear();
|
|
1270
|
+
}
|
|
1271
|
+
if (errors.length) throw new AggregateError(errors, 'Roster close failed');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1002
1274
|
requestHandler(port) {
|
|
1003
1275
|
if (!this._initialized) throw new Error('Call init() before requestHandler()');
|
|
1004
1276
|
const targetPort = port || this.defaultPort;
|
|
@@ -1029,9 +1301,18 @@ class Roster {
|
|
|
1029
1301
|
}
|
|
1030
1302
|
|
|
1031
1303
|
attach(server, { port } = {}) {
|
|
1304
|
+
this._assertOpen();
|
|
1032
1305
|
if (!this._initialized) throw new Error('Call init() before attach()');
|
|
1033
|
-
|
|
1034
|
-
|
|
1306
|
+
const targetPort = port || this.defaultPort;
|
|
1307
|
+
const existing = this._attachments.get(server);
|
|
1308
|
+
if (existing) {
|
|
1309
|
+
if (existing.port !== targetPort) throw new Error('Server is already attached to another port');
|
|
1310
|
+
return this;
|
|
1311
|
+
}
|
|
1312
|
+
const handlers = { request: this.requestHandler(port), upgrade: this.upgradeHandler(port), port: targetPort };
|
|
1313
|
+
server.on('request', handlers.request);
|
|
1314
|
+
server.on('upgrade', handlers.upgrade);
|
|
1315
|
+
this._attachments.set(server, handlers);
|
|
1035
1316
|
return this;
|
|
1036
1317
|
}
|
|
1037
1318
|
|
|
@@ -1075,7 +1356,7 @@ class Roster {
|
|
|
1075
1356
|
});
|
|
1076
1357
|
}
|
|
1077
1358
|
|
|
1078
|
-
startLocalMode() {
|
|
1359
|
+
async startLocalMode() {
|
|
1079
1360
|
this.domainPorts = {};
|
|
1080
1361
|
|
|
1081
1362
|
for (const portData of Object.values(this._sitesByPort)) {
|
|
@@ -1088,13 +1369,13 @@ class Roster {
|
|
|
1088
1369
|
const appHandler = portData.appHandlers[domain];
|
|
1089
1370
|
|
|
1090
1371
|
const dispatcher = (req, res) => {
|
|
1372
|
+
if (!this._beginRequest(res)) return;
|
|
1091
1373
|
const host = (req.headers.host || '').split(':')[0].toLowerCase();
|
|
1092
1374
|
if (this._runRequestPlugins(req, res, { host, domain })) return;
|
|
1093
|
-
virtualServer.fallbackHandler = appHandler;
|
|
1094
1375
|
if (virtualServer.requestListeners.length > 0) {
|
|
1095
1376
|
virtualServer.processRequest(req, res);
|
|
1096
1377
|
} else if (appHandler) {
|
|
1097
|
-
appHandler
|
|
1378
|
+
return invokeRequest(appHandler, req, res);
|
|
1098
1379
|
} else {
|
|
1099
1380
|
res.writeHead(404);
|
|
1100
1381
|
res.end('Site not found');
|
|
@@ -1105,17 +1386,14 @@ class Roster {
|
|
|
1105
1386
|
this.portServers[port] = httpServer;
|
|
1106
1387
|
|
|
1107
1388
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
1389
|
+
if (this._closing) { socket.destroy(); return; }
|
|
1390
|
+
this._trackUpgrade(socket);
|
|
1108
1391
|
virtualServer.processUpgrade(req, socket, head);
|
|
1109
1392
|
});
|
|
1110
1393
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
httpServer.on('error', (error) => {
|
|
1117
|
-
log.error(`❌ Error on port ${port} for ${domain}:`, error.message);
|
|
1118
|
-
});
|
|
1394
|
+
await this._listen(httpServer, port, 'localhost');
|
|
1395
|
+
const cleanDomain = normalizeDomainForLocalHost(domain);
|
|
1396
|
+
log.info(`🌐 ${domain} → http://${localHostForDomain(cleanDomain)}:${port}`);
|
|
1119
1397
|
}
|
|
1120
1398
|
}
|
|
1121
1399
|
|
|
@@ -1123,27 +1401,50 @@ class Roster {
|
|
|
1123
1401
|
return Promise.resolve();
|
|
1124
1402
|
}
|
|
1125
1403
|
|
|
1126
|
-
|
|
1404
|
+
start() {
|
|
1405
|
+
if (this._closing) return Promise.reject(new Error('Roster is closing or closed'));
|
|
1406
|
+
if (!this._startPromise) {
|
|
1407
|
+
this._startPromise = this._start().catch(async error => {
|
|
1408
|
+
if (!this._closing) {
|
|
1409
|
+
try { await this.close(); } catch (cleanupError) {
|
|
1410
|
+
throw new AggregateError([error, cleanupError], 'Startup and cleanup failed');
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
throw error;
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
return this._startPromise;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async _start() {
|
|
1127
1420
|
await this.init();
|
|
1421
|
+
this._assertOpen();
|
|
1128
1422
|
|
|
1129
1423
|
if (this.local) {
|
|
1130
1424
|
return this.startLocalMode();
|
|
1131
1425
|
}
|
|
1132
1426
|
|
|
1133
1427
|
const greenlockOptions = this._buildGreenlockOptions();
|
|
1134
|
-
const greenlockRuntime = GreenlockShim.create(greenlockOptions);
|
|
1428
|
+
const greenlockRuntime = this._greenlockRuntime || GreenlockShim.create(greenlockOptions);
|
|
1429
|
+
this._greenlockRuntime = greenlockRuntime;
|
|
1430
|
+
this._startCertificateRenewLoop();
|
|
1135
1431
|
const greenlock = Greenlock.init({
|
|
1136
1432
|
...greenlockOptions,
|
|
1137
|
-
greenlock: greenlockRuntime
|
|
1433
|
+
greenlock: greenlockRuntime,
|
|
1434
|
+
onServerError: error => log.error('Server error:', error.message)
|
|
1138
1435
|
});
|
|
1139
1436
|
|
|
1140
|
-
|
|
1437
|
+
if (this.cluster && require('cluster').isPrimary) {
|
|
1438
|
+
return greenlock.ready();
|
|
1439
|
+
}
|
|
1440
|
+
const glx = await new Promise(resolve => greenlock.ready(resolve));
|
|
1441
|
+
this._assertOpen();
|
|
1442
|
+
{
|
|
1141
1443
|
const httpServer = glx.httpServer();
|
|
1142
1444
|
const bunTlsHotReloadHandlers = [];
|
|
1143
1445
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
});
|
|
1446
|
+
await this._listen(httpServer, 80, this.hostname);
|
|
1447
|
+
log.info('HTTP server listening on port 80');
|
|
1147
1448
|
|
|
1148
1449
|
for (const [port, portData] of Object.entries(this._sitesByPort)) {
|
|
1149
1450
|
const portNum = parseInt(port);
|
|
@@ -1158,7 +1459,7 @@ class Roster {
|
|
|
1158
1459
|
if (pems) return pems;
|
|
1159
1460
|
|
|
1160
1461
|
try {
|
|
1161
|
-
await
|
|
1462
|
+
await this._checkCertificate(host);
|
|
1162
1463
|
} catch (error) {
|
|
1163
1464
|
log.warn(`⚠️ Greenlock issuance failed for ${host}: ${error?.message || error}`);
|
|
1164
1465
|
}
|
|
@@ -1171,7 +1472,7 @@ class Roster {
|
|
|
1171
1472
|
if (zone) {
|
|
1172
1473
|
const bootstrapHost = `bun-bootstrap.${zone}`;
|
|
1173
1474
|
try {
|
|
1174
|
-
await
|
|
1475
|
+
await this._checkCertificate(bootstrapHost);
|
|
1175
1476
|
} catch (error) {
|
|
1176
1477
|
log.warn(`⚠️ Greenlock wildcard bootstrap failed for ${bootstrapHost}: ${error?.message || error}`);
|
|
1177
1478
|
}
|
|
@@ -1200,7 +1501,7 @@ class Roster {
|
|
|
1200
1501
|
const certSubject = primaryDomain.startsWith('*.') ? wildcardRoot(primaryDomain) : primaryDomain;
|
|
1201
1502
|
log.warn(`⚠️ Bun: requesting ${needsWildcard ? 'combined wildcard' : ''} certificate for ${certSubject} via Greenlock before HTTPS bind`);
|
|
1202
1503
|
try {
|
|
1203
|
-
await
|
|
1504
|
+
await this._checkCertificate(certSubject);
|
|
1204
1505
|
} catch (error) {
|
|
1205
1506
|
log.error(`❌ Failed to obtain certificate for ${certSubject} under Bun:`, error?.message || error);
|
|
1206
1507
|
}
|
|
@@ -1221,23 +1522,28 @@ class Roster {
|
|
|
1221
1522
|
if (isBunRuntime) {
|
|
1222
1523
|
const primaryDomain = Object.keys(portData.virtualServers)[0];
|
|
1223
1524
|
let defaultPems = await ensureBunDefaultPems(primaryDomain);
|
|
1525
|
+
let defaultContext = tls.createSecureContext(defaultPems);
|
|
1224
1526
|
httpsServer = https.createServer({
|
|
1225
1527
|
...tlsOpts,
|
|
1226
1528
|
key: defaultPems.key,
|
|
1227
1529
|
cert: defaultPems.cert,
|
|
1228
1530
|
SNICallback: (servername, callback) => {
|
|
1229
|
-
|
|
1230
|
-
.
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
.
|
|
1531
|
+
const resolveContext = async () => {
|
|
1532
|
+
const cached = await this._resolveSecureContext(servername);
|
|
1533
|
+
if (cached) return cached;
|
|
1534
|
+
const pems = await issueAndReloadPemsForServername(servername);
|
|
1535
|
+
this._assertOpen();
|
|
1536
|
+
return pems ? this._getSecureContext(servername, false) : defaultContext;
|
|
1537
|
+
};
|
|
1538
|
+
resolveContext().then(context => callback(null, context), callback);
|
|
1235
1539
|
}
|
|
1236
1540
|
}, dispatcher);
|
|
1237
1541
|
const reloadBunDefaultTls = async (servername, reason) => {
|
|
1238
1542
|
const nextPems = await issueAndReloadPemsForServername(servername);
|
|
1239
1543
|
if (!nextPems) return false;
|
|
1544
|
+
this._assertOpen();
|
|
1240
1545
|
defaultPems = nextPems;
|
|
1546
|
+
defaultContext = tls.createSecureContext(defaultPems);
|
|
1241
1547
|
if (typeof httpsServer.setSecureContext === 'function') {
|
|
1242
1548
|
try {
|
|
1243
1549
|
httpsServer.setSecureContext({ key: defaultPems.key, cert: defaultPems.cert });
|
|
@@ -1257,34 +1563,20 @@ class Roster {
|
|
|
1257
1563
|
this.portServers[portNum] = httpsServer;
|
|
1258
1564
|
httpsServer.on('upgrade', upgradeHandler);
|
|
1259
1565
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
});
|
|
1566
|
+
await this._listen(httpsServer, portNum, this.hostname);
|
|
1567
|
+
log.info(`HTTPS server listening on port ${portNum}`);
|
|
1263
1568
|
} else {
|
|
1264
1569
|
const httpsOptions = {
|
|
1265
1570
|
minVersion: this.tlsMinVersion,
|
|
1266
1571
|
maxVersion: this.tlsMaxVersion,
|
|
1267
1572
|
SNICallback: (servername, callback) => {
|
|
1268
|
-
|
|
1269
|
-
const pems = this._resolvePemsForServername(servername);
|
|
1270
|
-
if (pems) {
|
|
1271
|
-
callback(null, tls.createSecureContext({ key: pems.key, cert: pems.cert }));
|
|
1272
|
-
} else {
|
|
1273
|
-
callback(new Error(`No certificate files available for ${servername}`));
|
|
1274
|
-
}
|
|
1275
|
-
} catch (error) {
|
|
1276
|
-
callback(error);
|
|
1277
|
-
}
|
|
1573
|
+
this._getSecureContext(servername, false).then(context => callback(null, context), callback);
|
|
1278
1574
|
}
|
|
1279
1575
|
};
|
|
1280
1576
|
|
|
1281
1577
|
const httpsServer = https.createServer(httpsOptions, dispatcher);
|
|
1282
1578
|
httpsServer.on('upgrade', upgradeHandler);
|
|
1283
1579
|
|
|
1284
|
-
httpsServer.on('error', (error) => {
|
|
1285
|
-
log.error(`HTTPS server error on port ${portNum}:`, error.message);
|
|
1286
|
-
});
|
|
1287
|
-
|
|
1288
1580
|
httpsServer.on('tlsClientError', (error) => {
|
|
1289
1581
|
if (!error.message.includes('http request')) {
|
|
1290
1582
|
log.error(`TLS error on port ${portNum}:`, error.message);
|
|
@@ -1293,13 +1585,8 @@ class Roster {
|
|
|
1293
1585
|
|
|
1294
1586
|
this.portServers[portNum] = httpsServer;
|
|
1295
1587
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
|
|
1299
|
-
} else {
|
|
1300
|
-
log.info(`HTTPS server listening on port ${portNum}`);
|
|
1301
|
-
}
|
|
1302
|
-
});
|
|
1588
|
+
await this._listen(httpsServer, portNum, this.hostname);
|
|
1589
|
+
log.info(`HTTPS server listening on port ${portNum}`);
|
|
1303
1590
|
}
|
|
1304
1591
|
}
|
|
1305
1592
|
|
|
@@ -1314,6 +1601,7 @@ class Roster {
|
|
|
1314
1601
|
for (const zone of this.wildcardZones) {
|
|
1315
1602
|
const bootstrapHost = `bun-bootstrap.${zone}`;
|
|
1316
1603
|
const attemptPrewarm = async (attempt = 1) => {
|
|
1604
|
+
if (this._closing) return;
|
|
1317
1605
|
try {
|
|
1318
1606
|
log.warn(`⚠️ Bun runtime detected: prewarming wildcard certificate via ${bootstrapHost} (attempt ${attempt})`);
|
|
1319
1607
|
let reloaded = false;
|
|
@@ -1330,16 +1618,20 @@ class Roster {
|
|
|
1330
1618
|
log.warn(`⚠️ Bun wildcard prewarm stopped for ${zone} after ${attempt} attempts`);
|
|
1331
1619
|
return;
|
|
1332
1620
|
}
|
|
1333
|
-
|
|
1621
|
+
if (this._closing) return;
|
|
1622
|
+
const timer = setTimeout(() => {
|
|
1623
|
+
this._retryTimers.delete(timer);
|
|
1334
1624
|
attemptPrewarm(attempt + 1).catch(() => {});
|
|
1335
1625
|
}, retryDelayMs);
|
|
1626
|
+
this._retryTimers.add(timer);
|
|
1336
1627
|
}
|
|
1337
1628
|
};
|
|
1338
1629
|
|
|
1339
1630
|
attemptPrewarm().catch(() => {});
|
|
1340
1631
|
}
|
|
1341
1632
|
}
|
|
1342
|
-
}
|
|
1633
|
+
}
|
|
1634
|
+
return this;
|
|
1343
1635
|
}
|
|
1344
1636
|
}
|
|
1345
1637
|
|