roster-server 2.1.1 → 2.1.4

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
@@ -47,6 +47,45 @@ function hostMatchesWildcard(host, pattern) {
47
47
  return h.endsWith('.' + suffix) && h.length > suffix.length;
48
48
  }
49
49
 
50
+ function wildcardSubjectForHost(host) {
51
+ const normalized = (host || '').trim().toLowerCase();
52
+ const labels = normalized.split('.').filter(Boolean);
53
+ if (labels.length < 3) return null;
54
+ return `*.${labels.slice(1).join('.')}`;
55
+ }
56
+
57
+ function certDirCandidatesForSubject(subject) {
58
+ const normalized = (subject || '').trim().toLowerCase();
59
+ if (!normalized) return [];
60
+ if (!normalized.startsWith('*.')) return [normalized];
61
+ const zone = wildcardRoot(normalized);
62
+ if (!zone) return [normalized];
63
+ // greenlock-store-fs may persist wildcard subjects under _wildcard_.<zone>
64
+ return [normalized, `_wildcard_.${zone}`];
65
+ }
66
+
67
+ function buildCertLookupCandidates(servername) {
68
+ const normalized = (servername || '').trim().toLowerCase();
69
+ if (!normalized) return [];
70
+
71
+ const subjects = [normalized];
72
+ const wildcardSubject = wildcardSubjectForHost(normalized);
73
+ if (wildcardSubject) subjects.push(wildcardSubject);
74
+ const zoneSubject = wildcardRoot(normalized) || (wildcardSubject ? wildcardRoot(wildcardSubject) : null);
75
+ if (zoneSubject) subjects.push(zoneSubject);
76
+
77
+ const candidates = [];
78
+ const seen = new Set();
79
+ for (const subject of subjects) {
80
+ for (const certDir of certDirCandidatesForSubject(subject)) {
81
+ if (seen.has(certDir)) continue;
82
+ seen.add(certDir);
83
+ candidates.push(certDir);
84
+ }
85
+ }
86
+ return candidates;
87
+ }
88
+
50
89
  // Virtual Server that completely isolates applications
51
90
  class VirtualServer extends EventEmitter {
52
91
  constructor(domain) {
@@ -754,8 +793,18 @@ class Roster {
754
793
  const dispatcher = createDispatcher(portData);
755
794
  const upgradeHandler = createUpgradeHandler(portData);
756
795
  const greenlockStorePath = this.greenlockStorePath;
796
+ const normalizeHostInput = (value) => {
797
+ if (typeof value === 'string') return value;
798
+ if (!value || typeof value !== 'object') return '';
799
+ if (typeof value.servername === 'string') return value.servername;
800
+ if (typeof value.hostname === 'string') return value.hostname;
801
+ if (typeof value.subject === 'string') return value.subject;
802
+ return '';
803
+ };
757
804
  const loadCert = (subjectDir) => {
758
- const certPath = path.join(greenlockStorePath, 'live', subjectDir);
805
+ const normalizedSubject = normalizeHostInput(subjectDir).trim().toLowerCase();
806
+ if (!normalizedSubject) return null;
807
+ const certPath = path.join(greenlockStorePath, 'live', normalizedSubject);
759
808
  const keyPath = path.join(certPath, 'privkey.pem');
760
809
  const certFilePath = path.join(certPath, 'cert.pem');
761
810
  const chainPath = path.join(certPath, 'chain.pem');
@@ -767,14 +816,15 @@ class Roster {
767
816
  }
768
817
  return null;
769
818
  };
770
- const zoneSubjectForHost = (servername) => {
771
- const labels = String(servername || '').split('.').filter(Boolean);
772
- if (labels.length < 3) return null;
773
- return labels.slice(1).join('.');
774
- };
775
819
  const resolvePemsForServername = (servername) => {
776
- if (!servername) return null;
777
- return loadCert(servername) || loadCert(zoneSubjectForHost(servername));
820
+ const host = normalizeHostInput(servername).trim().toLowerCase();
821
+ if (!host) return null;
822
+ const candidates = buildCertLookupCandidates(host);
823
+ for (const candidate of candidates) {
824
+ const pems = loadCert(candidate);
825
+ if (pems) return pems;
826
+ }
827
+ return null;
778
828
  };
779
829
 
780
830
  if (portNum === this.defaultPort) {
@@ -787,8 +837,7 @@ class Roster {
787
837
  if (isBunRuntime) {
788
838
  const primaryDomain = Object.keys(portData.virtualServers)[0];
789
839
  // Greenlock stores certs by subject (e.g. tagnu.com), not by wildcard (*.tagnu.com)
790
- const certSubject = primaryDomain.startsWith('*.') ? wildcardRoot(primaryDomain) : primaryDomain;
791
- const defaultPems = resolvePemsForServername(certSubject);
840
+ const defaultPems = resolvePemsForServername(primaryDomain);
792
841
 
793
842
  if (defaultPems) {
794
843
  httpsServer = https.createServer({
@@ -806,7 +855,7 @@ class Roster {
806
855
  }, dispatcher);
807
856
  log.warn(`⚠️ Bun runtime detected: using file-based TLS with SNI for ${primaryDomain} on port ${portNum}`);
808
857
  } else {
809
- log.warn(`⚠️ Bun runtime detected but cert files missing for ${certSubject} (${primaryDomain}); falling back to Greenlock HTTPS server`);
858
+ log.warn(`⚠️ Bun runtime detected but cert files missing for ${primaryDomain}; falling back to Greenlock HTTPS server`);
810
859
  httpsServer = glx.httpsServer(tlsOpts, dispatcher);
811
860
  }
812
861
  } else {
@@ -873,4 +922,6 @@ class Roster {
873
922
 
874
923
  module.exports = Roster;
875
924
  module.exports.wildcardRoot = wildcardRoot;
876
- module.exports.hostMatchesWildcard = hostMatchesWildcard;
925
+ module.exports.hostMatchesWildcard = hostMatchesWildcard;
926
+ module.exports.wildcardSubjectForHost = wildcardSubjectForHost;
927
+ module.exports.buildCertLookupCandidates = buildCertLookupCandidates;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.1.1",
3
+ "version": "2.1.4",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -7,7 +7,12 @@ const fs = require('fs');
7
7
  const http = require('http');
8
8
  const os = require('os');
9
9
  const Roster = require('../index.js');
10
- const { wildcardRoot, hostMatchesWildcard } = require('../index.js');
10
+ const {
11
+ wildcardRoot,
12
+ hostMatchesWildcard,
13
+ wildcardSubjectForHost,
14
+ buildCertLookupCandidates
15
+ } = require('../index.js');
11
16
 
12
17
  function closePortServers(roster) {
13
18
  if (roster.portServers && typeof roster.portServers === 'object') {
@@ -75,6 +80,35 @@ describe('hostMatchesWildcard', () => {
75
80
  });
76
81
  });
77
82
 
83
+ describe('wildcardSubjectForHost', () => {
84
+ it('returns wildcard subject for subdomain hosts', () => {
85
+ assert.strictEqual(wildcardSubjectForHost('admin.tagnu.com'), '*.tagnu.com');
86
+ assert.strictEqual(wildcardSubjectForHost('api.eu.example.com'), '*.eu.example.com');
87
+ });
88
+ it('returns null for apex hosts', () => {
89
+ assert.strictEqual(wildcardSubjectForHost('tagnu.com'), null);
90
+ assert.strictEqual(wildcardSubjectForHost('localhost'), null);
91
+ });
92
+ });
93
+
94
+ describe('buildCertLookupCandidates', () => {
95
+ it('prioritizes wildcard cert for subdomains and includes apex fallback', () => {
96
+ assert.deepStrictEqual(
97
+ buildCertLookupCandidates('admin.tagnu.com'),
98
+ ['admin.tagnu.com', '*.tagnu.com', '_wildcard_.tagnu.com', 'tagnu.com']
99
+ );
100
+ });
101
+ it('includes wildcard storage path candidates when wildcard subject is provided', () => {
102
+ assert.deepStrictEqual(
103
+ buildCertLookupCandidates('*.tagnu.com'),
104
+ ['*.tagnu.com', '_wildcard_.tagnu.com', 'tagnu.com']
105
+ );
106
+ });
107
+ it('returns only apex subject for apex hosts', () => {
108
+ assert.deepStrictEqual(buildCertLookupCandidates('tagnu.com'), ['tagnu.com']);
109
+ });
110
+ });
111
+
78
112
  describe('Roster', () => {
79
113
  describe('parseDomainWithPort', () => {
80
114
  it('parses *.example.com with default port', () => {