roster-server 2.2.5 → 2.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.2.5",
3
+ "version": "2.2.7",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -130,6 +130,51 @@ describe('acme-dns-01-cli-wrapper automatic Linode DNS', () => {
130
130
  assert.strictEqual(payload.name, '_acme-challenge.sub');
131
131
  });
132
132
 
133
+ it('prefers apex zone over www zone for www challenges', async () => {
134
+ const calls = [];
135
+ global.fetch = async (url, options = {}) => {
136
+ calls.push({ url, options });
137
+ if (url.endsWith('/domains?page_size=500')) {
138
+ return {
139
+ ok: true,
140
+ status: 200,
141
+ json: async () => ({
142
+ data: [
143
+ { id: 777, domain: 'www.tagnu.com' },
144
+ { id: 888, domain: 'tagnu.com' }
145
+ ]
146
+ })
147
+ };
148
+ }
149
+ if (url.endsWith('/domains/888/records?page_size=500')) {
150
+ return { ok: true, status: 200, json: async () => ({ data: [] }) };
151
+ }
152
+ if (url.endsWith('/domains/888/records') && options.method === 'POST') {
153
+ return { ok: true, status: 200, json: async () => ({ id: 333 }) };
154
+ }
155
+ return { ok: true, status: 204, json: async () => ({}) };
156
+ };
157
+
158
+ const challenger = wrapper.create({
159
+ provider: 'linode',
160
+ linodeApiKey: 'fake-token',
161
+ verifyDnsBeforeContinue: false,
162
+ propagationDelay: 0,
163
+ dryRunDelay: 0
164
+ });
165
+
166
+ await challenger.set({
167
+ challenge: {
168
+ altname: 'www.tagnu.com',
169
+ dnsHost: '_greenlock-dryrun-abc.www.tagnu.com',
170
+ dnsAuthorization: 'www-token'
171
+ }
172
+ });
173
+
174
+ assert.ok(calls.some((c) => c.url.endsWith('/domains/888/records') && c.options.method === 'POST'));
175
+ assert.ok(!calls.some((c) => c.url.endsWith('/domains/777/records') && c.options.method === 'POST'));
176
+ });
177
+
133
178
  it('falls back to manual when provider mode has no API key (default)', async () => {
134
179
  const prevLinode = process.env.LINODE_API_KEY;
135
180
  delete process.env.LINODE_API_KEY;
@@ -115,11 +115,12 @@ module.exports.create = function create(config = {}) {
115
115
  ? config.dnsResolvers.map((s) => String(s).trim()).filter(Boolean)
116
116
  : parseResolvers(process.env.ROSTER_DNS_RESOLVERS);
117
117
  const effectiveResolvers = configuredResolvers.length > 0 ? configuredResolvers : ['1.1.1.1', '8.8.8.8'];
118
- const resolverClients = effectiveResolvers.map((server) => {
118
+ const staticResolverClients = effectiveResolvers.map((server) => {
119
119
  const resolver = new dns.Resolver();
120
120
  resolver.setServers([server]);
121
121
  return { server, resolver };
122
122
  });
123
+ const authoritativeResolverClientsByHost = new Map();
123
124
  const normalizeProvider = (value) => String(value || '').trim().toLowerCase();
124
125
  const configuredProvider = normalizeProvider(
125
126
  config.provider
@@ -149,11 +150,78 @@ module.exports.create = function create(config = {}) {
149
150
  return candidate ? String(candidate).trim() : '';
150
151
  }
151
152
 
153
+ function zoneCandidatesFromDnsHost(dnsHost) {
154
+ const normalized = String(dnsHost || '')
155
+ .replace(/^_acme-challenge\./, '')
156
+ .replace(/^_greenlock-[^.]+\./, '')
157
+ .replace(/\.$/, '')
158
+ .toLowerCase();
159
+ if (!normalized) return [];
160
+
161
+ const labels = normalized.split('.').filter(Boolean);
162
+ const candidates = [];
163
+ for (let i = 0; i <= labels.length - 2; i += 1) {
164
+ candidates.push(labels.slice(i).join('.'));
165
+ }
166
+ return candidates;
167
+ }
168
+
169
+ async function resolveAuthoritativeResolverClients(dnsHost) {
170
+ const cached = authoritativeResolverClientsByHost.get(dnsHost);
171
+ if (cached) return cached;
172
+
173
+ const candidates = zoneCandidatesFromDnsHost(dnsHost);
174
+ for (const zone of candidates) {
175
+ let nsRecords = [];
176
+ try {
177
+ nsRecords = await dns.resolveNs(zone);
178
+ } catch {
179
+ continue;
180
+ }
181
+ if (!Array.isArray(nsRecords) || nsRecords.length === 0) continue;
182
+
183
+ const clients = [];
184
+ for (const nsNameRaw of nsRecords) {
185
+ const nsName = String(nsNameRaw || '').replace(/\.$/, '');
186
+ if (!nsName) continue;
187
+ let ips = [];
188
+ try {
189
+ ips = await dns.resolve4(nsName);
190
+ } catch {}
191
+ if (ips.length === 0) {
192
+ try {
193
+ ips = await dns.resolve6(nsName);
194
+ } catch {}
195
+ }
196
+ for (const ip of ips) {
197
+ try {
198
+ const resolver = new dns.Resolver();
199
+ resolver.setServers([ip]);
200
+ clients.push({ server: `${nsName}/${ip}`, resolver });
201
+ } catch {}
202
+ }
203
+ }
204
+
205
+ if (clients.length > 0) {
206
+ authoritativeResolverClientsByHost.set(dnsHost, clients);
207
+ return clients;
208
+ }
209
+ }
210
+
211
+ authoritativeResolverClientsByHost.set(dnsHost, []);
212
+ return [];
213
+ }
214
+
152
215
  async function resolveTxtRecords(dnsHost) {
153
216
  const records = [];
154
217
  const errors = [];
218
+ const authClients = await resolveAuthoritativeResolverClients(dnsHost);
219
+ const resolverClients = [...staticResolverClients, ...authClients];
220
+ const seenServers = new Set();
155
221
 
156
222
  for (const { server, resolver } of resolverClients) {
223
+ if (seenServers.has(server)) continue;
224
+ seenServers.add(server);
157
225
  try {
158
226
  const result = await resolver.resolveTxt(dnsHost);
159
227
  if (Array.isArray(result)) {
@@ -212,6 +280,14 @@ module.exports.create = function create(config = {}) {
212
280
  }
213
281
  };
214
282
 
283
+ // Prefer apex zone when validating www.<domain> challenges so records are
284
+ // created in the commonly delegated parent zone (e.g. tagnu.com).
285
+ if (altname) {
286
+ const normalizedAltname = String(altname).replace(/^\*\./, '').replace(/\.$/, '').toLowerCase();
287
+ if (normalizedAltname.startsWith('www.')) {
288
+ add(normalizedAltname.slice(4));
289
+ }
290
+ }
215
291
  if (dnsHost) {
216
292
  const normalizedDnsHost = String(dnsHost).replace(/^_acme-challenge\./, '').replace(/^_greenlock-[^.]+\./, '');
217
293
  add(normalizedDnsHost);