roster-server 2.4.0 → 2.4.2

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
@@ -109,19 +109,6 @@ function parseBooleanFlag(value, fallback = false) {
109
109
  return fallback;
110
110
  }
111
111
 
112
- function normalizeHostInput(value) {
113
- if (typeof value === 'string') return value;
114
- if (!value || typeof value !== 'object') return '';
115
- if (typeof value.servername === 'string') return value.servername;
116
- if (typeof value.hostname === 'string') return value.hostname;
117
- if (typeof value.subject === 'string') return value.subject;
118
- return '';
119
- }
120
-
121
- function hostWithoutPort(value) {
122
- return normalizeHostInput(value).split(':')[0].trim().toLowerCase();
123
- }
124
-
125
112
  function normalizeDomainForLocalHost(domain) {
126
113
  return (domain || '').trim().toLowerCase().replace(/^www\./, '');
127
114
  }
@@ -265,6 +252,9 @@ class Roster {
265
252
  this.portServers = {}; // Store servers by port
266
253
  this.domainPorts = {}; // Store domain → port mapping for local mode
267
254
  this.assignedPorts = new Set(); // Track ports assigned to domains (not OS availability)
255
+ this._sitesByPort = {};
256
+ this._initialized = false;
257
+ this._sniCallback = null;
268
258
  this.hostname = options.hostname ?? '::';
269
259
  this.filename = options.filename || 'index';
270
260
  this.minLocalPort = options.minLocalPort || 4000;
@@ -562,21 +552,13 @@ class Roster {
562
552
  }
563
553
  }
564
554
 
565
- /**
566
- * Register a domain handler.
567
- * @param {string} domainString Domain, optionally with :port.
568
- * @param {(virtualServer: VirtualServer) => Function} requestHandler Site app factory.
569
- * @param {{ silent?: boolean, skipDomainBookkeeping?: boolean }} [options]
570
- * @returns {Roster}
571
- */
572
- register(domainString, requestHandler, options = {}) {
555
+ register(domainString, requestHandler) {
573
556
  if (!domainString) {
574
557
  throw new Error('Domain is required');
575
558
  }
576
559
  if (typeof requestHandler !== 'function') {
577
560
  throw new Error('requestHandler must be a function');
578
561
  }
579
- const { silent = false, skipDomainBookkeeping = false } = options || {};
580
562
 
581
563
  const { domain, port } = this.parseDomainWithPort(domainString);
582
564
 
@@ -586,13 +568,11 @@ class Roster {
586
568
  return this;
587
569
  }
588
570
  const domainKey = port === this.defaultPort ? domain : `${domain}:${port}`;
589
- if (!skipDomainBookkeeping) this.domains.push(domain);
571
+ this.domains.push(domain);
590
572
  this.sites[domainKey] = requestHandler;
591
573
  const root = wildcardRoot(domain);
592
574
  if (root) this.wildcardZones.add(root);
593
- if (!silent) {
594
- log.info(`(✔) Registered wildcard site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
595
- }
575
+ log.info(`(✔) Registered wildcard site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
596
576
  return this;
597
577
  }
598
578
 
@@ -601,17 +581,13 @@ class Roster {
601
581
  domainEntries.push(`www.${domain}`);
602
582
  }
603
583
 
604
- if (!skipDomainBookkeeping) {
605
- this.domains.push(...domainEntries);
606
- }
584
+ this.domains.push(...domainEntries);
607
585
  domainEntries.forEach(d => {
608
586
  const domainKey = port === this.defaultPort ? d : `${d}:${port}`;
609
587
  this.sites[domainKey] = requestHandler;
610
588
  });
611
589
 
612
- if (!silent) {
613
- log.info(`(✔) Registered site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
614
- }
590
+ log.info(`(✔) Registered site: ${domain}${port !== this.defaultPort ? ':' + port : ''}`);
615
591
  return this;
616
592
  }
617
593
 
@@ -655,49 +631,114 @@ class Roster {
655
631
  return new VirtualServer(domain);
656
632
  }
657
633
 
658
- resolveRoutedHost(rawHost, hostAliases) {
659
- const normalizedHost = hostWithoutPort(rawHost);
660
- if (!normalizedHost) {
634
+ // Assign port to domain, detecting collisions with already assigned ports
635
+ assignPortToDomain(domain) {
636
+ let port = domainToPort(domain, this.minLocalPort, this.maxLocalPort);
637
+
638
+ // If port is already assigned to another domain, increment until we find a free one
639
+ while (this.assignedPorts.has(port)) {
640
+ port++;
641
+ if (port > this.maxLocalPort) {
642
+ port = this.minLocalPort; // Wrap around if we exceed max port
643
+ }
644
+ }
645
+
646
+ this.assignedPorts.add(port);
647
+ return port;
648
+ }
649
+
650
+ // Get SSL context from Greenlock for custom ports
651
+ async getSSLContext(domain, greenlock) {
652
+ try {
653
+ // Try to get existing certificate for the domain
654
+ const site = await greenlock.get({ servername: domain });
655
+ if (site && site.pems) {
656
+ return {
657
+ key: site.pems.privkey,
658
+ cert: site.pems.cert + site.pems.chain
659
+ };
660
+ }
661
+ } catch (error) {
662
+ }
663
+
664
+ // Return undefined to let HTTPS server handle SNI callback
665
+ return null;
666
+ }
667
+
668
+ _normalizeHostInput(value) {
669
+ if (typeof value === 'string') return value;
670
+ if (!value || typeof value !== 'object') return '';
671
+ if (typeof value.servername === 'string') return value.servername;
672
+ if (typeof value.hostname === 'string') return value.hostname;
673
+ if (typeof value.subject === 'string') return value.subject;
674
+ return '';
675
+ }
676
+
677
+ _loadCert(subjectDir) {
678
+ const normalizedSubject = this._normalizeHostInput(subjectDir).trim().toLowerCase();
679
+ if (!normalizedSubject) return null;
680
+ const certPath = path.join(this.greenlockStorePath, 'live', normalizedSubject);
681
+ const keyPath = path.join(certPath, 'privkey.pem');
682
+ const certFilePath = path.join(certPath, 'cert.pem');
683
+ const chainPath = path.join(certPath, 'chain.pem');
684
+ if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
661
685
  return {
662
- normalizedHost: '',
663
- routedHost: '',
664
- isWww: false
686
+ key: fs.readFileSync(keyPath, 'utf8'),
687
+ cert: fs.readFileSync(certFilePath, 'utf8') + fs.readFileSync(chainPath, 'utf8')
665
688
  };
666
689
  }
690
+ return null;
691
+ }
667
692
 
668
- let aliasedHost = normalizedHost;
669
- if (typeof hostAliases === 'function') {
670
- const mapped = hostAliases(normalizedHost);
671
- if (mapped) aliasedHost = hostWithoutPort(mapped);
672
- } else if (hostAliases && typeof hostAliases === 'object') {
673
- const mapped = hostAliases[normalizedHost];
674
- if (mapped) aliasedHost = hostWithoutPort(mapped);
693
+ _resolvePemsForServername(servername) {
694
+ const host = this._normalizeHostInput(servername).trim().toLowerCase();
695
+ if (!host) return null;
696
+ const candidates = buildCertLookupCandidates(host);
697
+ for (const candidate of candidates) {
698
+ const pems = this._loadCert(candidate);
699
+ if (pems) return pems;
675
700
  }
676
-
677
- const isWww = aliasedHost.startsWith('www.');
678
- return {
679
- normalizedHost: aliasedHost,
680
- routedHost: isWww ? aliasedHost.slice(4) : aliasedHost,
681
- isWww
682
- };
701
+ return null;
683
702
  }
684
703
 
685
- createPortRequestDispatcher(portData, options = {}) {
686
- const {
687
- hostAliases,
688
- allowWwwRedirect = true
689
- } = options;
704
+ _initSiteHandlers() {
705
+ this._sitesByPort = {};
706
+ for (const [hostKey, siteApp] of Object.entries(this.sites)) {
707
+ if (hostKey.startsWith('www.')) continue;
708
+ const { domain, port } = this.parseDomainWithPort(hostKey);
709
+ if (!this._sitesByPort[port]) {
710
+ this._sitesByPort[port] = {
711
+ virtualServers: {},
712
+ appHandlers: {}
713
+ };
714
+ }
715
+
716
+ const virtualServer = this.createVirtualServer(domain);
717
+ this._sitesByPort[port].virtualServers[domain] = virtualServer;
718
+ this.domainServers[domain] = virtualServer;
690
719
 
720
+ const appHandler = siteApp(virtualServer);
721
+ this._sitesByPort[port].appHandlers[domain] = appHandler;
722
+ if (!domain.startsWith('*.')) {
723
+ this._sitesByPort[port].appHandlers[`www.${domain}`] = appHandler;
724
+ }
725
+ }
726
+ }
727
+
728
+ _createDispatcher(portData) {
691
729
  return (req, res) => {
692
- const { normalizedHost, routedHost, isWww } = this.resolveRoutedHost(req.headers.host || '', hostAliases);
730
+ const host = req.headers.host || '';
731
+ const hostWithoutPort = host.split(':')[0].toLowerCase();
732
+ const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
693
733
 
694
- if (allowWwwRedirect && isWww) {
695
- res.writeHead(301, { Location: `https://${routedHost}${req.url}` });
734
+ if (hostWithoutPort.startsWith('www.')) {
735
+ const protocol = this.local ? 'http' : 'https';
736
+ res.writeHead(301, { Location: `${protocol}://${domain}${req.url}` });
696
737
  res.end();
697
738
  return;
698
739
  }
699
740
 
700
- const resolved = this.getHandlerForPortData(routedHost, portData);
741
+ const resolved = this.getHandlerForPortData(domain, portData);
701
742
  if (!resolved) {
702
743
  res.writeHead(404);
703
744
  res.end('Site not found');
@@ -717,11 +758,13 @@ class Roster {
717
758
  };
718
759
  }
719
760
 
720
- createPortUpgradeDispatcher(portData, options = {}) {
721
- const { hostAliases } = options;
761
+ _createUpgradeHandler(portData) {
722
762
  return (req, socket, head) => {
723
- const { routedHost } = this.resolveRoutedHost(req.headers.host || '', hostAliases);
724
- const resolved = this.getHandlerForPortData(routedHost, portData);
763
+ const host = req.headers.host || '';
764
+ const hostWithoutPort = host.split(':')[0].toLowerCase();
765
+ const domain = hostWithoutPort.startsWith('www.') ? hostWithoutPort.slice(4) : hostWithoutPort;
766
+
767
+ const resolved = this.getHandlerForPortData(domain, portData);
725
768
  if (resolved && resolved.virtualServer) {
726
769
  resolved.virtualServer.processUpgrade(req, socket, head);
727
770
  } else {
@@ -730,169 +773,111 @@ class Roster {
730
773
  };
731
774
  }
732
775
 
733
- /**
734
- * Prepare port-based virtual server/app handler data without listening.
735
- * @param {{ targetPort?: number }} [options]
736
- * @returns {{ sitesByPort: Record<number, { virtualServers: Record<string, VirtualServer>, appHandlers: Record<string, Function> }> }}
737
- */
738
- prepareSites(options = {}) {
739
- const { targetPort } = options;
740
- const sitesByPort = {};
741
-
742
- for (const [hostKey, siteApp] of Object.entries(this.sites)) {
743
- if (hostKey.startsWith('www.')) continue;
744
-
745
- const { domain, port } = this.parseDomainWithPort(hostKey);
746
- if (targetPort !== undefined && port !== targetPort) continue;
747
-
748
- if (!sitesByPort[port]) {
749
- sitesByPort[port] = {
750
- virtualServers: {},
751
- appHandlers: {}
752
- };
776
+ _initSniResolver() {
777
+ this._sniCallback = (servername, callback) => {
778
+ try {
779
+ const pems = this._resolvePemsForServername(servername);
780
+ if (pems) {
781
+ callback(null, tls.createSecureContext({ key: pems.key, cert: pems.cert }));
782
+ } else {
783
+ callback(new Error(`No certificate files available for ${servername}`));
784
+ }
785
+ } catch (error) {
786
+ callback(error);
753
787
  }
788
+ };
789
+ }
754
790
 
755
- const virtualServer = this.createVirtualServer(domain);
756
- sitesByPort[port].virtualServers[domain] = virtualServer;
757
- this.domainServers[domain] = virtualServer;
758
-
759
- const appHandler = siteApp(virtualServer);
760
- sitesByPort[port].appHandlers[domain] = appHandler;
761
- if (!domain.startsWith('*.')) {
762
- sitesByPort[port].appHandlers[`www.${domain}`] = appHandler;
763
- }
791
+ async init() {
792
+ if (this._initialized) return this;
793
+ await this.loadSites();
794
+ if (!this.local) {
795
+ this.generateConfigJson();
764
796
  }
765
-
766
- return { sitesByPort };
797
+ this._initSiteHandlers();
798
+ if (!this.local) {
799
+ this._initSniResolver();
800
+ }
801
+ this._initialized = true;
802
+ return this;
767
803
  }
768
804
 
769
- /**
770
- * Build a router that can be attached to externally managed servers (e.g. sticky-session workers).
771
- * This does not call listen() and does not boot Greenlock.
772
- * @param {{ targetPort?: number, hostAliases?: Record<string, string>|((host: string) => string|undefined), allowWwwRedirect?: boolean }} [options]
773
- * @returns {{ attach: (server: import('http').Server) => import('http').Server, dispatchRequest: Function, dispatchUpgrade: Function, portData: { virtualServers: Record<string, VirtualServer>, appHandlers: Record<string, Function> }, diagnostics: { targetPort: number, hosts: string[], virtualServers: string[] } }}
774
- */
775
- buildRuntimeRouter(options = {}) {
776
- const {
777
- targetPort = this.defaultPort,
778
- hostAliases,
779
- allowWwwRedirect = true
780
- } = options;
781
- const { sitesByPort } = this.prepareSites({ targetPort });
782
- const portData = sitesByPort[targetPort] || { virtualServers: {}, appHandlers: {} };
783
- const dispatchRequest = this.createPortRequestDispatcher(portData, { hostAliases, allowWwwRedirect });
784
- const dispatchUpgrade = this.createPortUpgradeDispatcher(portData, { hostAliases });
785
-
786
- return {
787
- attach: (server) => {
788
- server.on('request', dispatchRequest);
789
- server.on('upgrade', dispatchUpgrade);
790
- return server;
791
- },
792
- dispatchRequest,
793
- dispatchUpgrade,
794
- portData,
795
- diagnostics: {
796
- targetPort,
797
- hosts: Object.keys(portData.appHandlers),
798
- virtualServers: Object.keys(portData.virtualServers)
799
- }
800
- };
805
+ requestHandler(port) {
806
+ if (!this._initialized) throw new Error('Call init() before requestHandler()');
807
+ const targetPort = port || this.defaultPort;
808
+ const portData = this._sitesByPort[targetPort];
809
+ if (!portData) {
810
+ return (req, res) => {
811
+ res.writeHead(404);
812
+ res.end('Site not found');
813
+ };
814
+ }
815
+ return this._createDispatcher(portData);
801
816
  }
802
817
 
803
- // Assign port to domain, detecting collisions with already assigned ports
804
- assignPortToDomain(domain) {
805
- let port = domainToPort(domain, this.minLocalPort, this.maxLocalPort);
806
-
807
- // If port is already assigned to another domain, increment until we find a free one
808
- while (this.assignedPorts.has(port)) {
809
- port++;
810
- if (port > this.maxLocalPort) {
811
- port = this.minLocalPort; // Wrap around if we exceed max port
812
- }
818
+ upgradeHandler(port) {
819
+ if (!this._initialized) throw new Error('Call init() before upgradeHandler()');
820
+ const targetPort = port || this.defaultPort;
821
+ const portData = this._sitesByPort[targetPort];
822
+ if (!portData) {
823
+ return (req, socket, head) => { socket.destroy(); };
813
824
  }
814
-
815
- this.assignedPorts.add(port);
816
- return port;
825
+ return this._createUpgradeHandler(portData);
817
826
  }
818
827
 
819
- // Get SSL context from Greenlock for custom ports
820
- async getSSLContext(domain, greenlock) {
821
- try {
822
- // Try to get existing certificate for the domain
823
- const site = await greenlock.get({ servername: domain });
824
- if (site && site.pems) {
825
- return {
826
- key: site.pems.privkey,
827
- cert: site.pems.cert + site.pems.chain
828
- };
829
- }
830
- } catch (error) {
831
- }
828
+ sniCallback() {
829
+ if (!this._initialized) throw new Error('Call init() before sniCallback()');
830
+ if (!this._sniCallback) throw new Error('SNI callback not available in local mode');
831
+ return this._sniCallback;
832
+ }
832
833
 
833
- // Return undefined to let HTTPS server handle SNI callback
834
- return null;
834
+ attach(server, { port } = {}) {
835
+ if (!this._initialized) throw new Error('Call init() before attach()');
836
+ server.on('request', this.requestHandler(port));
837
+ server.on('upgrade', this.upgradeHandler(port));
838
+ return this;
835
839
  }
836
840
 
837
- // Start server in local mode with HTTP - simplified version
838
841
  startLocalMode() {
839
- // Store mapping of domain to port for later retrieval
840
842
  this.domainPorts = {};
841
843
 
842
- // Create a simple HTTP server for each domain with CRC32-based ports
843
- for (const [hostKey, siteApp] of Object.entries(this.sites)) {
844
- const domain = hostKey.split(':')[0]; // Remove port if present
845
-
846
- // Skip www domains in local mode
847
- if (domain.startsWith('www.')) {
848
- continue;
849
- }
850
-
851
- // Calculate deterministic port based on domain CRC32, with collision detection
852
- const port = this.assignPortToDomain(domain);
853
-
854
- // Store domain → port mapping
855
- this.domainPorts[domain] = port;
856
-
857
- // Create virtual server for the domain
858
- const virtualServer = this.createVirtualServer(domain);
859
- this.domainServers[domain] = virtualServer;
844
+ for (const portData of Object.values(this._sitesByPort)) {
845
+ for (const [domain, virtualServer] of Object.entries(portData.virtualServers)) {
846
+ if (domain.startsWith('www.')) continue;
860
847
 
861
- // Initialize app with virtual server
862
- const appHandler = siteApp(virtualServer);
848
+ const port = this.assignPortToDomain(domain);
849
+ this.domainPorts[domain] = port;
863
850
 
864
- // Create simple dispatcher for this domain
865
- const dispatcher = (req, res) => {
866
- // Set fallback handler on virtual server for non-Socket.IO requests
867
- virtualServer.fallbackHandler = appHandler;
851
+ const appHandler = portData.appHandlers[domain];
868
852
 
869
- if (virtualServer.requestListeners.length > 0) {
870
- virtualServer.processRequest(req, res);
871
- } else if (appHandler) {
872
- appHandler(req, res);
873
- } else {
874
- res.writeHead(404);
875
- res.end('Site not found');
876
- }
877
- };
853
+ const dispatcher = (req, res) => {
854
+ virtualServer.fallbackHandler = appHandler;
855
+ if (virtualServer.requestListeners.length > 0) {
856
+ virtualServer.processRequest(req, res);
857
+ } else if (appHandler) {
858
+ appHandler(req, res);
859
+ } else {
860
+ res.writeHead(404);
861
+ res.end('Site not found');
862
+ }
863
+ };
878
864
 
879
- // Create HTTP server for this domain
880
- const httpServer = http.createServer(dispatcher);
881
- this.portServers[port] = httpServer;
865
+ const httpServer = http.createServer(dispatcher);
866
+ this.portServers[port] = httpServer;
882
867
 
883
- // Handle WebSocket upgrade events
884
- httpServer.on('upgrade', (req, socket, head) => {
885
- virtualServer.processUpgrade(req, socket, head);
886
- });
868
+ httpServer.on('upgrade', (req, socket, head) => {
869
+ virtualServer.processUpgrade(req, socket, head);
870
+ });
887
871
 
888
- httpServer.listen(port, 'localhost', () => {
889
- const cleanDomain = normalizeDomainForLocalHost(domain);
890
- log.info(`🌐 ${domain} → http://${localHostForDomain(cleanDomain)}:${port}`);
891
- });
872
+ httpServer.listen(port, 'localhost', () => {
873
+ const cleanDomain = normalizeDomainForLocalHost(domain);
874
+ log.info(`🌐 ${domain} → http://${localHostForDomain(cleanDomain)}:${port}`);
875
+ });
892
876
 
893
- httpServer.on('error', (error) => {
894
- log.error(`❌ Error on port ${port} for ${domain}:`, error.message);
895
- });
877
+ httpServer.on('error', (error) => {
878
+ log.error(`❌ Error on port ${port} for ${domain}:`, error.message);
879
+ });
880
+ }
896
881
  }
897
882
 
898
883
  log.info(`(✔) Started ${Object.keys(this.portServers).length} sites in local mode`);
@@ -900,14 +885,8 @@ class Roster {
900
885
  }
901
886
 
902
887
  async start() {
903
- await this.loadSites();
888
+ await this.init();
904
889
 
905
- // Skip Greenlock configuration generation in local mode
906
- if (!this.local) {
907
- this.generateConfigJson();
908
- }
909
-
910
- // Handle local mode with simple HTTP server
911
890
  if (this.local) {
912
891
  return this.startLocalMode();
913
892
  }
@@ -966,7 +945,6 @@ class Roster {
966
945
  if (eventDomain && !msg.includes(`[${eventDomain}]`)) {
967
946
  msg = `[${eventDomain}] ${msg}`;
968
947
  }
969
- // Suppress known benign warnings from ACME when using acme-dns-01-cli
970
948
  if (event === 'warning' && typeof msg === 'string') {
971
949
  if (/acme-dns-01-cli.*(incorrect function signatures|deprecated use of callbacks)/i.test(msg)) return;
972
950
  if (/dns-01 challenge plugin should have zones/i.test(msg)) return;
@@ -976,8 +954,6 @@ class Roster {
976
954
  else log.info(msg);
977
955
  }
978
956
  };
979
- // Keep a direct greenlock runtime handle so we can call get() explicitly under Bun
980
- // before binding :443, avoiding invalid non-TLS responses on startup.
981
957
  const greenlockRuntime = GreenlockShim.create(greenlockOptions);
982
958
  const greenlock = Greenlock.init({
983
959
  ...greenlockOptions,
@@ -986,50 +962,22 @@ class Roster {
986
962
 
987
963
  return greenlock.ready(async glx => {
988
964
  const httpServer = glx.httpServer();
989
- const { sitesByPort } = this.prepareSites();
990
-
991
965
  const bunTlsHotReloadHandlers = [];
992
966
 
993
967
  httpServer.listen(80, this.hostname, () => {
994
968
  log.info('HTTP server listening on port 80');
995
969
  });
996
970
 
997
- // Handle different port types
998
- for (const [port, portData] of Object.entries(sitesByPort)) {
971
+ for (const [port, portData] of Object.entries(this._sitesByPort)) {
999
972
  const portNum = parseInt(port);
1000
- const dispatcher = this.createPortRequestDispatcher(portData);
1001
- const upgradeHandler = this.createPortUpgradeDispatcher(portData);
1002
- const greenlockStorePath = this.greenlockStorePath;
1003
- const loadCert = (subjectDir) => {
1004
- const normalizedSubject = normalizeHostInput(subjectDir).trim().toLowerCase();
1005
- if (!normalizedSubject) return null;
1006
- const certPath = path.join(greenlockStorePath, 'live', normalizedSubject);
1007
- const keyPath = path.join(certPath, 'privkey.pem');
1008
- const certFilePath = path.join(certPath, 'cert.pem');
1009
- const chainPath = path.join(certPath, 'chain.pem');
1010
- if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
1011
- return {
1012
- key: fs.readFileSync(keyPath, 'utf8'),
1013
- cert: fs.readFileSync(certFilePath, 'utf8') + fs.readFileSync(chainPath, 'utf8')
1014
- };
1015
- }
1016
- return null;
1017
- };
1018
- const resolvePemsForServername = (servername) => {
1019
- const host = normalizeHostInput(servername).trim().toLowerCase();
1020
- if (!host) return null;
1021
- const candidates = buildCertLookupCandidates(host);
1022
- for (const candidate of candidates) {
1023
- const pems = loadCert(candidate);
1024
- if (pems) return pems;
1025
- }
1026
- return null;
1027
- };
973
+ const dispatcher = this._createDispatcher(portData);
974
+ const upgradeHandler = this._createUpgradeHandler(portData);
975
+
1028
976
  const issueAndReloadPemsForServername = async (servername) => {
1029
- const host = normalizeHostInput(servername).trim().toLowerCase();
977
+ const host = this._normalizeHostInput(servername).trim().toLowerCase();
1030
978
  if (!host) return null;
1031
979
 
1032
- let pems = resolvePemsForServername(host);
980
+ let pems = this._resolvePemsForServername(host);
1033
981
  if (pems) return pems;
1034
982
 
1035
983
  try {
@@ -1038,11 +986,9 @@ class Roster {
1038
986
  log.warn(`⚠️ Greenlock issuance failed for ${host}: ${error?.message || error}`);
1039
987
  }
1040
988
 
1041
- pems = resolvePemsForServername(host);
989
+ pems = this._resolvePemsForServername(host);
1042
990
  if (pems) return pems;
1043
991
 
1044
- // For wildcard zones, try a valid subdomain bootstrap host so Greenlock can
1045
- // resolve the wildcard site without relying on invalid "*.domain" servername input.
1046
992
  const wildcardSubject = wildcardSubjectForHost(host);
1047
993
  const zone = wildcardSubject ? wildcardRoot(wildcardSubject) : null;
1048
994
  if (zone) {
@@ -1052,11 +998,12 @@ class Roster {
1052
998
  } catch (error) {
1053
999
  log.warn(`⚠️ Greenlock wildcard bootstrap failed for ${bootstrapHost}: ${error?.message || error}`);
1054
1000
  }
1055
- pems = resolvePemsForServername(host);
1001
+ pems = this._resolvePemsForServername(host);
1056
1002
  }
1057
1003
 
1058
1004
  return pems;
1059
1005
  };
1006
+
1060
1007
  const ensureBunDefaultPems = async (primaryDomain) => {
1061
1008
  let pems = await issueAndReloadPemsForServername(primaryDomain);
1062
1009
 
@@ -1066,7 +1013,7 @@ class Roster {
1066
1013
 
1067
1014
  if (pems && needsWildcard && !certCoversName(pems.cert, `*.${primaryDomain}`)) {
1068
1015
  log.warn(`⚠️ Existing cert for ${primaryDomain} lacks *.${primaryDomain} SAN — clearing stale cert for combined re-issuance`);
1069
- const certDir = path.join(greenlockStorePath, 'live', primaryDomain);
1016
+ const certDir = path.join(this.greenlockStorePath, 'live', primaryDomain);
1070
1017
  try { fs.rmSync(certDir, { recursive: true, force: true }); } catch {}
1071
1018
  pems = null;
1072
1019
  }
@@ -1081,7 +1028,7 @@ class Roster {
1081
1028
  log.error(`❌ Failed to obtain certificate for ${certSubject} under Bun:`, error?.message || error);
1082
1029
  }
1083
1030
 
1084
- pems = resolvePemsForServername(primaryDomain);
1031
+ pems = this._resolvePemsForServername(primaryDomain);
1085
1032
  if (pems) return pems;
1086
1033
 
1087
1034
  throw new Error(
@@ -1091,15 +1038,11 @@ class Roster {
1091
1038
  };
1092
1039
 
1093
1040
  if (portNum === this.defaultPort) {
1094
- // Bun has known gaps around SNICallback compatibility.
1095
- // Fallback to static cert loading for the primary domain on default HTTPS port.
1096
1041
  const tlsOpts = { minVersion: this.tlsMinVersion, maxVersion: this.tlsMaxVersion };
1097
1042
  let httpsServer;
1098
1043
 
1099
1044
  if (isBunRuntime) {
1100
1045
  const primaryDomain = Object.keys(portData.virtualServers)[0];
1101
- // Under Bun, avoid glx.httpsServer fallback (may serve invalid TLS on :443).
1102
- // Require concrete PEM files and create native https server directly.
1103
1046
  let defaultPems = await ensureBunDefaultPems(primaryDomain);
1104
1047
  httpsServer = https.createServer({
1105
1048
  ...tlsOpts,
@@ -1135,21 +1078,18 @@ class Roster {
1135
1078
  }
1136
1079
 
1137
1080
  this.portServers[portNum] = httpsServer;
1138
-
1139
- // Handle WebSocket upgrade events
1140
1081
  httpsServer.on('upgrade', upgradeHandler);
1141
1082
 
1142
1083
  httpsServer.listen(portNum, this.hostname, () => {
1143
1084
  log.info(`HTTPS server listening on port ${portNum}`);
1144
1085
  });
1145
1086
  } else {
1146
- // Create HTTPS server for custom ports using Greenlock certificates
1147
1087
  const httpsOptions = {
1148
1088
  minVersion: this.tlsMinVersion,
1149
1089
  maxVersion: this.tlsMaxVersion,
1150
1090
  SNICallback: (servername, callback) => {
1151
1091
  try {
1152
- const pems = resolvePemsForServername(servername);
1092
+ const pems = this._resolvePemsForServername(servername);
1153
1093
  if (pems) {
1154
1094
  callback(null, tls.createSecureContext({ key: pems.key, cert: pems.cert }));
1155
1095
  } else {
@@ -1162,8 +1102,6 @@ class Roster {
1162
1102
  };
1163
1103
 
1164
1104
  const httpsServer = https.createServer(httpsOptions, dispatcher);
1165
-
1166
- // Handle WebSocket upgrade events
1167
1105
  httpsServer.on('upgrade', upgradeHandler);
1168
1106
 
1169
1107
  httpsServer.on('error', (error) => {
@@ -1171,7 +1109,6 @@ class Roster {
1171
1109
  });
1172
1110
 
1173
1111
  httpsServer.on('tlsClientError', (error) => {
1174
- // Suppress HTTP request errors to avoid log spam
1175
1112
  if (!error.message.includes('http request')) {
1176
1113
  log.error(`TLS error on port ${portNum}:`, error.message);
1177
1114
  }
@@ -1195,7 +1132,7 @@ class Roster {
1195
1132
  : 30000;
1196
1133
  const maxAttempts = Number.isFinite(Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
1197
1134
  ? Math.max(0, Number(process.env.ROSTER_BUN_WILDCARD_PREWARM_MAX_ATTEMPTS))
1198
- : 0; // 0 = retry forever
1135
+ : 0;
1199
1136
 
1200
1137
  for (const zone of this.wildcardZones) {
1201
1138
  const bootstrapHost = `bun-bootstrap.${zone}`;
@@ -1204,7 +1141,6 @@ class Roster {
1204
1141
  log.warn(`⚠️ Bun runtime detected: prewarming wildcard certificate via ${bootstrapHost} (attempt ${attempt})`);
1205
1142
  let reloaded = false;
1206
1143
  for (const reloadTls of bunTlsHotReloadHandlers) {
1207
- // Trigger issuance + immediately hot-reload default TLS context when ready.
1208
1144
  reloaded = (await reloadTls(bootstrapHost, `prewarm ${bootstrapHost} attempt ${attempt}`)) || reloaded;
1209
1145
  }
1210
1146
  if (!reloaded) {
@@ -1223,7 +1159,6 @@ class Roster {
1223
1159
  }
1224
1160
  };
1225
1161
 
1226
- // Background prewarm + retries so HTTPS startup is not blocked by DNS propagation timing.
1227
1162
  attemptPrewarm().catch(() => {});
1228
1163
  }
1229
1164
  }