diff --git a/CHANGELOG.md b/CHANGELOG.md index e1e284a..8b9a292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,13 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`. ## [Unreleased] -## [1.1.11] - 2026-07-17 +## [1.1.12] - 2026-07-17 + +### Fixed +- The host edit form's "Parent Wildcard" option stayed greyed out even when a valid wildcard actually existed for that host, so an already-created host could never be switched onto one from the edit modal (only brand-new hosts, via the field's `keyup` handler, ever saw it become available). The underlying `/host/lookup/:item` check also had the same self-match issue as the recently-fixed backend bug: it resolved an already-existing host to its own record instead of a sibling wildcard. Added a dedicated `/host/wildcard-parent/:item` endpoint that checks both directions, and the edit form now actually runs the check when it opens. +- Fixed an nginx startup warning: `the "listen ... http2" directive is deprecated, use the "http2" directive instead`. Migrated to the standalone `http2 on;` directive (nginx 1.25.1+). + +Bumps to v1.1.12. ### Changed - Moved the help (❓) link out of the global header and onto each relevant card individually (Proxy List, Add/Edit host, Add DNS Provider, Dynamic A Records, Add New User, User List, Add Permission, Permissions, Add Group) — each now deep-links straight to the doc that actually covers it, instead of one generic header icon. @@ -82,7 +88,8 @@ First tagged release. Establishes the `vX.Y.Z` tag convention that the in-app up - Standalone backup script (`ops/backup.sh`) for deployments not using theta-env's orchestrator — snapshots Redis and `./config`, with retention. - Admin-only in-app banner that checks GitHub releases every 24h and surfaces available updates. -[Unreleased]: https://github.com/theta42/proxy/compare/v1.1.11...HEAD +[Unreleased]: https://github.com/theta42/proxy/compare/v1.1.12...HEAD +[1.1.12]: https://github.com/theta42/proxy/compare/v1.1.11...v1.1.12 [1.1.11]: https://github.com/theta42/proxy/compare/v1.1.10...v1.1.11 [1.1.10]: https://github.com/theta42/proxy/compare/v1.1.9...v1.1.10 [1.1.9]: https://github.com/theta42/proxy/compare/v1.1.8...v1.1.9 diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 91af64c..c7d7352 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxy-api", - "version": "1.1.11", + "version": "1.1.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxy-api", - "version": "1.1.11", + "version": "1.1.12", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^7.3.0", diff --git a/nodejs/package.json b/nodejs/package.json index 5781942..ca5b5bf 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "proxy-api", - "version": "1.1.11", + "version": "1.1.12", "private": true, "author": [ { diff --git a/nodejs/routes/host.js b/nodejs/routes/host.js index 775d3d4..9132890 100755 --- a/nodejs/routes/host.js +++ b/nodejs/routes/host.js @@ -128,6 +128,29 @@ router.get('/lookup/:item', authz.requireDomainRole('viewer', authz.resolve.host } }); +// Is there a wildcard host that could serve as :item's parent (i.e. an +// already-issued cert :item could reuse instead of getting its own)? Two +// cases, covered by two different lookups: a brand-new subdomain that has +// never been created (lookUp()'s normal wildcard fallback finds it, since +// the name has no leaf of its own yet), and an ALREADY-EXISTING host or the +// wildcard's own base domain (lookUp() would just resolve to that host's +// own leaf -- lookUpWildcardParent() checks the sibling "*" slot instead; +// see its comment in models/host.js). Used by the host create/edit form to +// decide whether to offer "Parent Wildcard" as a challenge type. +router.get('/wildcard-parent/:item', authz.requireDomainRole('viewer', authz.resolve.hostParam), async function(req, res, next){ + try{ + let match = Model.lookUp(req.params.item); + if(!match || !match.is_wildcard){ + match = Model.lookUpWildcardParent(req.params.item); + } + return res.json({ + results: (match && match.is_wildcard) ? match : null, + }); + }catch(error){ + return next(error); + } +}); + // The full lookup tree exposes every host, so restrict it to admins. router.get('/lookupobj', authz.requireAdmin, async function(req, res, next){ try{ diff --git a/nodejs/test/unit/host_lookup.test.js b/nodejs/test/unit/host_lookup.test.js index 9b7b097..71f85f2 100644 --- a/nodejs/test/unit/host_lookup.test.js +++ b/nodejs/test/unit/host_lookup.test.js @@ -187,6 +187,52 @@ describe('Host wildcard base-domain lookup', () => { }); }); +/** + * Tests for the exact fallback combination used by + * routes/host.js's GET /wildcard-parent/:item (and, via hostMatchWildcard(), + * the host create/edit form's "Parent Wildcard" option) -- lookUp() first + * (handles a brand-new subdomain that has no leaf of its own yet), falling + * back to lookUpWildcardParent() only when lookUp() didn't resolve to a + * wildcard (handles an ALREADY-EXISTING host, which lookUp() would resolve + * to its own record). Regression coverage for the edit-form bug where the + * "Parent Wildcard" option stayed permanently greyed out for an existing + * host, because the route only ever tried lookUp(). + */ +describe('Host wildcard-parent route fallback (lookUp then lookUpWildcardParent)', () => { + + let Host; + + before(async () => { + Host = createMockHostClassWithWildcardParentFix(); + }); + + function findWildcardParent(host){ + let match = Host.lookUp(host); + if(!match || !match.is_wildcard) match = Host.lookUpWildcardParent(host); + return (match && match.is_wildcard) ? match : null; + } + + test('finds the wildcard for a brand-new subdomain that was never created', async () => { + await populateTree(Host, ['*.cool.mysite.com']); + const result = findWildcardParent('newthing.cool.mysite.com'); + assert.ok(result); + assert.strictEqual(result.host, '*.cool.mysite.com'); + }); + + test('finds the wildcard for the wildcard\'s own base domain, whether or not it is already a plain host', async () => { + await populateTree(Host, ['*.cool.mysite.com']); + assert.strictEqual(findWildcardParent('cool.mysite.com').host, '*.cool.mysite.com'); + + await populateTree(Host, ['*.cool.mysite.com', 'cool.mysite.com']); + assert.strictEqual(findWildcardParent('cool.mysite.com').host, '*.cool.mysite.com'); + }); + + test('returns null when the host has no wildcard sibling at all', async () => { + await populateTree(Host, ['cool.mysite.com']); + assert.strictEqual(findWildcardParent('cool.mysite.com'), null); + }); +}); + /** * Same mock shape as createMockHostClass() above, plus the parent-record * stamp in the tree-population loop and the lookUpWildcardParent() method -- @@ -243,7 +289,11 @@ async function populateTree(Host, hosts) { } if(fragments.length === 0){ - pointer[fragment]['#record'] = {host}; + // is_wildcard mirrors the real Host model's own field (set + // whenever a host is DNS-01 wildcard-issued, i.e. starts with + // "*."), needed by tests that check it the same way the real + // /wildcard-parent/:item route does. + pointer[fragment]['#record'] = {host, is_wildcard: host.startsWith('*.')}; if(fragment === '*' && !pointer['#record']){ pointer['#record'] = pointer[fragment]['#record']; diff --git a/nodejs/views/hosts.ejs b/nodejs/views/hosts.ejs index 0eb7996..fe52662 100755 --- a/nodejs/views/hosts.ejs +++ b/nodejs/views/hosts.ejs @@ -212,7 +212,7 @@ hostModal().show(); } - function hostEditOpen(host){ + async function hostEditOpen(host){ hostFormReset(); let h = $.scope.hosts.getByKey(host); let $f = $('#hostForm'); @@ -258,8 +258,31 @@ let hostRenameable = !h.is_wildcard && !h.wildcard_parent && !h.is_cache; $f.find('[name=host]').prop('disabled', !hostRenameable); $('#host-rename-help').toggle(!hostRenameable); + + // Reflect + enable the challenge-type options actually available for + // this host. Setting the host field's .val() above does not fire a + // 'keyup' event, so without this the "Parent Wildcard" option stayed + // permanently greyed out on edit even when a valid parent wildcard + // existed -- it only ever got un-greyed by the user re-typing the + // hostname (the keyup handler further down). + $('#challengeType-child-container, #challengeType-DNS-01-wildcard-container, #wildcard_matchAny-container') + .addClass('challengeType-container'); + if(h.is_wildcard){ + $('#challengeType-DNS-01-wildcard-container').removeClass('challengeType-container'); + $('#challengeType-DNS-01-wildcard').prop('checked', true); $('#wildcard_matchAny-container').removeClass('challengeType-container'); + }else{ + let wildcardParent = await hostMatchWildcard(h.host); + if(wildcardParent){ + $('#challengeType-child-container').removeClass('challengeType-container'); + $('#challengeType-child-relatedHost').text(wildcardParent.host); + } + if(h.wildcard_parent){ + $('#challengeType-wildcardChild').prop('checked', true); + }else{ + $('#challengeType-HTTP-01').prop('checked', true); + } } hostModal().show(); @@ -306,10 +329,12 @@ async function hostMatchWildcard(host){ try{ - let res = await app.api.get(`host/lookup/${host}`); - if(res.results && res.results.is_wildcard){ - return res.results; - } + // Not /host/lookup/ -- that resolves an ALREADY-EXISTING host to its + // own record, not a sibling wildcard (see the route's comment). This + // dedicated endpoint correctly finds a usable wildcard parent whether + // @host is brand new or already exists as its own host. + let res = await app.api.get(`host/wildcard-parent/${host}`); + return res.results || false; }catch(error){ return false; } diff --git a/ops/nginx_conf/autossl.conf b/ops/nginx_conf/autossl.conf index 2598c80..bb698c3 100644 --- a/ops/nginx_conf/autossl.conf +++ b/ops/nginx_conf/autossl.conf @@ -1,5 +1,9 @@ -listen 443 ssl http2; +listen 443 ssl; listen 4443 ssl; +# The "http2" listen parameter is deprecated since nginx 1.25.1 in favor of +# this standalone directive, which applies to every "listen ... ssl" in the +# server block (both 443 and 4443 here). +http2 on; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on;