roster-server 2.1.20 → 2.1.23

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.1.20",
3
+ "version": "2.1.23",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const legacyCli = require('acme-dns-01-cli');
4
4
  const log = require('lemonlog')('acme-dns-01');
5
+ const dns = require('node:dns').promises;
5
6
 
6
7
  function toPromise(fn, context) {
7
8
  if (typeof fn !== 'function') {
@@ -60,16 +61,72 @@ module.exports.create = function create(config = {}) {
60
61
  : Number.isFinite(Number(process.env.ROSTER_DNS_DRYRUN_DELAY_MS))
61
62
  ? Number(process.env.ROSTER_DNS_DRYRUN_DELAY_MS)
62
63
  : propagationDelay;
64
+ const parseBool = (value, fallback) => {
65
+ if (value === undefined || value === null || value === '') return fallback;
66
+ const normalized = String(value).trim().toLowerCase();
67
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
68
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
69
+ return fallback;
70
+ };
71
+ const verifyDnsBeforeContinue = config.verifyDnsBeforeContinue !== undefined
72
+ ? parseBool(config.verifyDnsBeforeContinue, true)
73
+ : parseBool(process.env.ROSTER_DNS_VERIFY_BEFORE_CONTINUE, true);
74
+ const dnsPollIntervalMs = Number.isFinite(config.dnsPollIntervalMs)
75
+ ? config.dnsPollIntervalMs
76
+ : Number.isFinite(Number(process.env.ROSTER_DNS_POLL_INTERVAL_MS))
77
+ ? Number(process.env.ROSTER_DNS_POLL_INTERVAL_MS)
78
+ : 5000;
79
+ const dnsPollTimeoutMs = Number.isFinite(config.dnsPollTimeoutMs)
80
+ ? config.dnsPollTimeoutMs
81
+ : Number.isFinite(Number(process.env.ROSTER_DNS_POLL_TIMEOUT_MS))
82
+ ? Number(process.env.ROSTER_DNS_POLL_TIMEOUT_MS)
83
+ : null;
63
84
 
64
85
  function sleep(ms) {
65
86
  return new Promise((resolve) => setTimeout(resolve, ms));
66
87
  }
67
88
 
89
+ function normalizeTxtChunk(value) {
90
+ return String(value || '').replace(/^"+|"+$/g, '');
91
+ }
92
+
93
+ function resolveExpectedToken(opts, ch) {
94
+ const candidate = ch?.dnsAuthorization || opts?.dnsAuthorization;
95
+ return candidate ? String(candidate).trim() : '';
96
+ }
97
+
98
+ async function hasDnsTxtToken(dnsHost, expectedToken) {
99
+ if (!dnsHost || !expectedToken) return false;
100
+ try {
101
+ const records = await dns.resolveTxt(dnsHost);
102
+ for (const recordParts of records || []) {
103
+ const joined = (recordParts || []).map(normalizeTxtChunk).join('').trim();
104
+ if (joined === expectedToken) return true;
105
+ }
106
+ return false;
107
+ } catch (_) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ async function waitForDnsTxtPropagation(dnsHost, expectedToken, timeoutMs) {
113
+ const started = Date.now();
114
+ const maxWait = Math.max(0, Number.isFinite(timeoutMs) ? timeoutMs : 0);
115
+ if (maxWait === 0) return hasDnsTxtToken(dnsHost, expectedToken);
116
+
117
+ while ((Date.now() - started) <= maxWait) {
118
+ if (await hasDnsTxtToken(dnsHost, expectedToken)) return true;
119
+ await sleep(Math.max(1000, dnsPollIntervalMs));
120
+ }
121
+ return false;
122
+ }
123
+
68
124
  const presentedByHost = new Map();
125
+ const presentedByAltname = new Map();
69
126
 
70
127
  async function setChallenge(opts) {
71
128
  const isInteractive = Boolean(process.stdin && process.stdin.isTTY);
72
- if (isInteractive && !autoContinue) {
129
+ if (isInteractive && !autoContinue && !verifyDnsBeforeContinue) {
73
130
  return toPromise(challenger.set, challenger)(opts);
74
131
  }
75
132
 
@@ -83,10 +140,16 @@ module.exports.create = function create(config = {}) {
83
140
  if (dnsHost && dnsAuth) {
84
141
  presentedByHost.set(dnsHost, dnsAuth);
85
142
  }
143
+ const altname = String(ch.altname || opts?.altname || '');
144
+ if (altname && dnsAuth) {
145
+ presentedByAltname.set(altname, { dnsHost, dnsAuthorization: dnsAuth });
146
+ }
86
147
  const isDryRunChallenge = dnsHost.includes('_greenlock-dryrun-');
87
148
  const effectiveDelay = isDryRunChallenge
88
149
  ? Math.max(0, dryRunDelay)
89
150
  : propagationDelay;
151
+ const effectiveTimeoutMs = dnsPollTimeoutMs === null ? effectiveDelay : dnsPollTimeoutMs;
152
+ const expectedToken = resolveExpectedToken(opts, ch);
90
153
 
91
154
  log.info(
92
155
  '\tTXT\t' +
@@ -96,6 +159,28 @@ module.exports.create = function create(config = {}) {
96
159
  '\tTTL 60'
97
160
  );
98
161
  log.info('');
162
+ if (verifyDnsBeforeContinue && dnsHost && expectedToken) {
163
+ log.info(
164
+ 'DNS verification enabled. Continuing automatically when TXT appears at ' +
165
+ dnsHost +
166
+ ' (timeout ' +
167
+ effectiveTimeoutMs +
168
+ 'ms, poll ' +
169
+ dnsPollIntervalMs +
170
+ 'ms).'
171
+ );
172
+ const propagated = await waitForDnsTxtPropagation(dnsHost, expectedToken, effectiveTimeoutMs);
173
+ if (propagated) {
174
+ log.info(`DNS TXT detected for ${dnsHost}; continuing ACME flow.`);
175
+ return null;
176
+ }
177
+ log.warn(
178
+ `DNS TXT not detected for ${dnsHost} within ${effectiveTimeoutMs}ms; ` +
179
+ 'continuing anyway (ACME preflight may still fail).'
180
+ );
181
+ return null;
182
+ }
183
+
99
184
  log.info(
100
185
  'Non-interactive mode (or autoContinue) detected. ' +
101
186
  'Set the TXT record now. Continuing automatically in ' +
@@ -108,11 +193,18 @@ module.exports.create = function create(config = {}) {
108
193
 
109
194
  async function getChallenge(opts) {
110
195
  const ch = opts?.challenge || {};
111
- const dnsHost = String(ch.dnsHost || opts?.dnsHost || '');
196
+ const altname = String(ch.altname || opts?.altname || '');
197
+ const wildcardZone = altname.startsWith('*.') ? altname.slice(2) : '';
198
+ const dnsHostFromAltname = wildcardZone ? `_acme-challenge.${wildcardZone}` : '';
199
+ const dnsHost = String(ch.dnsHost || opts?.dnsHost || dnsHostFromAltname || '');
200
+
201
+ const byAltname = altname ? presentedByAltname.get(altname) : null;
112
202
  const dnsAuthorization =
113
203
  ch.dnsAuthorization ||
114
204
  opts?.dnsAuthorization ||
115
- (dnsHost ? presentedByHost.get(dnsHost) : null);
205
+ (dnsHost ? presentedByHost.get(dnsHost) : null) ||
206
+ byAltname?.dnsAuthorization ||
207
+ null;
116
208
 
117
209
  if (!dnsAuthorization) {
118
210
  return null;
@@ -120,6 +212,8 @@ module.exports.create = function create(config = {}) {
120
212
 
121
213
  return {
122
214
  ...(typeof ch === 'object' ? ch : {}),
215
+ ...(altname ? { altname } : {}),
216
+ ...(dnsHost ? { dnsHost } : {}),
123
217
  dnsAuthorization
124
218
  };
125
219
  }