From e091c15d95f5afce18cc43c5cea552948b41f5a5 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 14 Jul 2026 12:29:06 -0400 Subject: [PATCH] Fix DuckDNS double-suffixing a subdomain that already includes .duckdns.org (#128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported error when adding a DuckDNS provider with subdomains="nl-theta42.duckdns.org" (the full name, as DuckDNS's own site displays it): {"name": "EntryNotFound", "message": "Domain:duckdns.org does not exists"} listDomains() blindly appended ".duckdns.org" to whatever was entered, turning "nl-theta42.duckdns.org" into "nl-theta42.duckdns.org.duckdns.org". tld-extract doesn't know duckdns.org is a shared suffix, so it parsed that malformed string down to domain "duckdns.org" — surfacing as a confusing EntryNotFound two layers away from the actual cause (Domain.create's internal lookup). Add __normalizeLabel() to strip a trailing ".duckdns.org" (and lowercase) before use, so both "myhost" and "myhost.duckdns.org" work identically. Also make __label()'s existing suffix-strip case-insensitive to match. --- nodejs/models/dns_provider/duckdns.js | 16 ++++++++++++++-- nodejs/test/integration/dns_provider.test.js | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/nodejs/models/dns_provider/duckdns.js b/nodejs/models/dns_provider/duckdns.js index f93d8b0..4a0f422 100644 --- a/nodejs/models/dns_provider/duckdns.js +++ b/nodejs/models/dns_provider/duckdns.js @@ -54,6 +54,18 @@ class DuckDns extends DnsApi{ if(status !== 'OK') throw this.errors.unauthorized(); } + // Accepts either the bare label ("myhost") or the full duckdns.org name + // ("myhost.duckdns.org", how DuckDNS's own site displays it, and what + // operators naturally paste in) — strip a trailing ".duckdns.org" so + // both forms end up as the same label. Without this, "myhost.duckdns.org" + // would get double-suffixed to "myhost.duckdns.org.duckdns.org", which + // tld-extract (not aware duckdns.org is a shared suffix) then misparses + // as domain "duckdns.org" — surfacing as a confusing "Domain:duckdns.org + // does not exists" error two layers away from the actual cause. + __normalizeLabel(value){ + return value.replace(/\.duckdns\.org$/i, '').toLowerCase(); + } + // No API to enumerate owned subdomains, so the operator supplies them. // This call both validates the token and, as a side effect, syncs each // domain's A/AAAA record to this host's current public IP if the token @@ -61,14 +73,14 @@ class DuckDns extends DnsApi{ // — the same thing an operator would need to do anyway when pointing a // fresh DuckDNS domain at this proxy. async listDomains(){ - let labels = this.subdomains.split(',').map(d => d.trim()).filter(Boolean); + let labels = this.subdomains.split(',').map(d => this.__normalizeLabel(d.trim())).filter(Boolean); await this.update(labels.join(','), {}); return labels.map(label => ({domain: `${label}.duckdns.org`})); } __label(domain){ - return domain.domain.replace(/\.duckdns\.org$/, ''); + return domain.domain.replace(/\.duckdns\.org$/i, ''); } // No read API exists; public DNS is the only source of truth available. diff --git a/nodejs/test/integration/dns_provider.test.js b/nodejs/test/integration/dns_provider.test.js index 3196ab0..c2e2b51 100644 --- a/nodejs/test/integration/dns_provider.test.js +++ b/nodejs/test/integration/dns_provider.test.js @@ -200,6 +200,25 @@ describe('DNS Provider Contract Compliance', () => { const instance = new DuckDns({token: 'mock-token', subdomains: 'mockhost'}); assert.strictEqual(instance.__label({domain: 'mockhost.duckdns.org'}), 'mockhost'); }); + + test('__normalizeLabel accepts both the bare label and the full duckdns.org name', () => { + const instance = new DuckDns({token: 'mock-token', subdomains: 'mockhost'}); + assert.strictEqual(instance.__normalizeLabel('mockhost'), 'mockhost'); + assert.strictEqual(instance.__normalizeLabel('mockhost.duckdns.org'), 'mockhost'); + assert.strictEqual(instance.__normalizeLabel('MockHost.DuckDNS.org'), 'mockhost'); + }); + + test('listDomains does not double-suffix a subdomains value that already includes .duckdns.org', async () => { + const instance = new DuckDns({token: 'mock-token', subdomains: 'nl-theta42.duckdns.org,other'}); + // Stub out the network call — this test is only about what domain + // name(s) listDomains() builds from `subdomains`, not the live API. + instance.update = async () => {}; + const domains = await instance.listDomains(); + assert.deepStrictEqual(domains, [ + {domain: 'nl-theta42.duckdns.org'}, + {domain: 'other.duckdns.org'}, + ]); + }); }); });