Compare commits

..

8 Commits

Author SHA1 Message Date
wmantly f0b282b679 Merge pull request #162 from theta42/wildcard-edit-and-nginx-fix
Fix wildcard-parent edit greying and deprecated nginx http2 directive
2026-07-17 21:35:28 -04:00
wmantly 4f1fce367e Fix wildcard-parent edit greying and deprecated nginx http2 directive
- The edit form's "Parent Wildcard" option stayed greyed out even when a
  valid wildcard existed, since hostEditOpen() never ran the eligibility
  check (only the host field's keyup handler did, which setting .val()
  programmatically doesn't fire) -- and the check itself, GET
  /host/lookup/:item, had the same self-match bug as the recently-fixed
  Host.prototype.update() case: it resolves an already-existing host to
  its own record instead of a sibling wildcard. Added a dedicated
  /host/wildcard-parent/:item route combining lookUp() (handles a
  brand-new subdomain) with lookUpWildcardParent() (handles an
  already-existing host), and hostEditOpen() now actually runs it.

- Migrated ops/nginx_conf/autossl.conf's deprecated "listen ... http2"
  directive to the standalone "http2 on;" directive (nginx 1.25.1+).

Bumps to v1.1.12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 21:33:26 -04:00
wmantly 9eb3dfa2e6 Merge pull request #161 from theta42/help-icon-relocate
Move help links from the global header onto each relevant card
2026-07-17 20:02:08 -04:00
wmantly a40da55993 Move help links from the global header onto each relevant card
The single header-wide help icon (added last release) pointed at a
per-page doc guess, but a page can have several cards covering different
topics. Removed it and added a small help icon directly to each card
that has real corresponding doc content, linking straight to that doc:
Proxy List + Add/Edit host modal (hosts.ejs), Add DNS Provider + Dynamic
A Records (dns.ejs), Add New User + User List (users.ejs), Add
Permission + Permissions (permissions.ejs), Add Group (groups.ejs).

Bumps to v1.1.11.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 20:00:34 -04:00
wmantly e5df0d3370 Merge pull request #160 from theta42/docs-help-search
Add header help icon and in-app docs search
2026-07-17 19:29:22 -04:00
wmantly fcd73169e0 Add header help icon and in-app docs search
- A ? icon in the top-right header deep-links to the doc most relevant to
  the current page (client-side path mapping, same pattern already used
  for top-nav active-link highlighting -- no server-side "current section"
  local exists to key off of instead). Falls back to the docs index.
- GET /docs/search does a plain line-substring search over the existing
  allowlisted doc set. No new dependency, stays usable with no internet
  access.

Bumps to v1.1.10.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 19:28:07 -04:00
wmantly 876ea6cfd0 Merge pull request #159 from theta42/host-form-ux-polish
Host form/list UX polish: editable hostname, created-by column, mobile tabs, more help text
2026-07-17 19:05:09 -04:00
wmantly 9100e92549 Host form/list UX polish: editable hostname, created-by column, mobile tabs, more help text
- Plain hosts can now be renamed after creation (wildcard/child/cache hosts
  stay locked, since other records reference them by name). Migrates the
  cert cache key on rename.
- Along the way, found and fixed a real bug in the vendored model-redis
  library: its rename path leaves a stray, incomplete hash behind under
  the old key when an `always`-type field (updated_on) is defined earlier
  in the schema than the primary key -- silently blocking that hostname
  from ever being reused. Worked around at the Host model level (can't
  patch node_modules).
- Host list now shows who created each host, and when.
- Host modal's tabs now scroll horizontally on narrow screens instead of
  overflowing awkwardly.
- Added missing inline help text (Target SSL, wildcard matching behavior).

Bumps to v1.1.9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 19:03:26 -04:00
15 changed files with 297 additions and 19 deletions
+34 -2
View File
@@ -6,7 +6,35 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
## [Unreleased]
## [1.1.8] - 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.
Bumps to v1.1.11.
### Added
- A help icon (❓) in the top-right header now deep-links to the doc most relevant to the current page (falls back to the docs index elsewhere).
- The in-app docs viewer (`/docs`) is now searchable — a simple line-substring search over the same local doc set, no new dependency, still works with no internet access.
Bumps to v1.1.10.
### Added
- The host list now shows who created each host, and when.
- Plain (non-wildcard) hosts can now be renamed after creation — the hostname field is no longer permanently locked. Wildcard hosts, wildcard children, and auto-created subdomain cache entries stay locked, since other records reference them by name.
- More inline help text on the host create/edit form (Target SSL, wildcard matching behavior).
### Fixed
- The host create/edit modal's tabs could overflow awkwardly on narrow (mobile) screens — they now scroll horizontally instead.
- Fixed a bug in the vendored `model-redis` library's record-rename path: renaming a record's primary key while another `always`-type field (e.g. `updated_on`) is defined earlier in the schema left a stray, incomplete hash behind under the old key, making that name permanently unavailable for reuse. Worked around in `Host.prototype.update()`.
Bumps to v1.1.9.
### Fixed
- **Couldn't attach an existing host to a parent wildcard.** The host edit form's "Parent Wildcard" option submitted correctly, but `Host.prototype.update()` had no `challengeType` handling at all (only `Host.create()` did) — selecting it and saving silently did nothing. Added the same wildcard-parent lookup to `update()`.
@@ -60,7 +88,11 @@ 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.8...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
[1.1.8]: https://github.com/theta42/proxy/compare/v1.1.7...v1.1.8
[1.1.7]: https://github.com/theta42/proxy/compare/v1.1.6...v1.1.7
[1.1.6]: https://github.com/theta42/proxy/compare/v1.1.5...v1.1.6
+9 -1
View File
@@ -14,6 +14,14 @@ async function getCert(host){
}
}
async function setCert(host, cert){
try{
return await client.SET(`${host}:latest`, JSON.stringify(cert));
}catch(error){
return {}
}
}
async function deleteCert(host){
try{
console.log('looking for', host);
@@ -23,4 +31,4 @@ async function deleteCert(host){
}
}
module.exports = {getCert, deleteCert};
module.exports = {getCert, setCert, deleteCert};
+34 -1
View File
@@ -2,7 +2,7 @@
const Table = require('.');
const {Domain} = require('.').models;
const {deleteCert} = require('./cert');
const {getCert, setCert, deleteCert} = require('./cert');
const ModelPs = require('../utils/model_pubsub');
const tldExtract = require('tld-extract').parse_host;
@@ -343,10 +343,43 @@ class Host extends Table{
}
}
// Real hostname rename. model-redis's own update() (see super.update()
// below) already handles the Redis primary-key RENAME + collision
// check, and Host.buildLookUpObj() below already rebuilds the lookup
// tree afterward -- but the cert cache (models/cert.js, `${host}:latest`)
// is a separate record keyed by hostname string that the generic field
// system doesn't know about, so it doesn't move on its own. Only
// wildcard hosts (createWildcardCert) ever populate this key -- for a
// plain HTTP-01 host this is a no-op (nothing to migrate; auto-ssl
// transparently issues a fresh cert under the new name on first
// access, same as it does for any newly-created host).
let oldHost = this.host;
let renaming = data && typeof data.host === 'string' && data.host !== oldHost;
if(renaming){
let cert = await getCert(oldHost);
if(cert && Object.keys(cert).length) await setCert(data.host, cert);
}
let out = await super.update(data, ...args)
await this.bustCache(this.host);
await Host.buildLookUpObj();
if(renaming){
await deleteCert(oldHost);
// Work around a model-redis bug (as of ^1.5.0): super.update()'s
// field-application loop iterates _keyMap's definition order and
// only reassigns this[_key] (this.host) to the NEW value once it
// reaches the `host` field itself -- but `updated_on` (always:
// true, so always included) is defined BEFORE `host` in _keyMap,
// so it gets HSET while this.host is still the OLD name. Redis's
// HSET on a non-existent key (the old hash, just RENAMEd away)
// silently recreates it -- leaving a stray, incomplete hash under
// the old hostname that makes Host.exists(oldHost) wrongly return
// true forever, blocking that name from ever being reused.
await this.constructor.redisClient.DEL(`${conf.redis.prefix || ''}Host_${oldHost}`);
}
return out;
} catch(error){
throw error;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "proxy-api",
"version": "1.1.8",
"version": "1.1.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "proxy-api",
"version": "1.1.8",
"version": "1.1.12",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "proxy-api",
"version": "1.1.8",
"version": "1.1.12",
"private": true,
"author": [
{
+24
View File
@@ -60,6 +60,30 @@ router.get('/', function(req, res) {
res.render('docs_index', {...values, docs: docList});
});
// Plain, dependency-free line-substring search over the same allowlisted
// doc set -- no separate index to build/maintain, no new dependency, and it
// keeps working with no internet access (same reasoning as the rest of this
// route). Must be registered before the /:slug catch-all below, or "search"
// would be treated as a (nonexistent) doc slug and 404.
router.get('/search', function(req, res) {
const q = (req.query.q || '').trim();
if (!q) return res.json({results: []});
const qLower = q.toLowerCase();
const results = [];
for (const [slug, doc] of Object.entries(DOCS)) {
try {
const content = fs.readFileSync(doc.file, 'utf8');
const matchLine = content.split('\n').find(line => line.toLowerCase().includes(qLower));
if (matchLine) {
results.push({slug, title: doc.title, snippet: matchLine.trim().slice(0, 200)});
}
} catch (error) { /* unreadable doc file -- skip it */ }
}
res.json({results});
});
router.get('/:slug', function(req, res, next) {
const doc = DOCS[req.params.slug];
if (!doc) return next({status: 404, message: 'Doc not found'});
+23
View File
@@ -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{
+51 -1
View File
@@ -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'];
+2
View File
@@ -125,6 +125,7 @@
Add DNS Provider
</span>
<span class="float-end">
<a href="/docs/installation" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
<i class="fa-solid fa-circle-minus"></i>
</span>
</div>
@@ -226,6 +227,7 @@
<div class="card-header d-flex align-items-center">
<span class="card-icon me-2"><i class="fa-solid fa-tower-broadcast"></i></span>
<span class="card-title">Dynamic A Records</span>
<a href="/docs/installation" class="text-reset ms-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
<span class="ms-auto text-muted small">
This server's public IP:
<span class="badge text-bg-primary fs-6"><i class="fa-solid fa-globe me-1"></i><span id="ddns-current-ip">…</span></span>
+42 -1
View File
@@ -11,7 +11,12 @@
A local copy of this project's documentation, readable from the
running app -- no internet access required.
</p>
<ul class="list-group">
<div class="input-group mb-3">
<span class="input-group-text"><i class="fa-solid fa-magnifying-glass"></i></span>
<input type="search" id="docs-search-input" class="form-control" placeholder="Search the docs…" oninput="docsSearch(this.value)">
</div>
<div id="docs-search-results" style="display:none"></div>
<ul id="docs-list" class="list-group">
<% docs.forEach(function(doc){ %>
<li class="list-group-item">
<a href="/docs/<%= doc.slug %>"><%= doc.title %></a>
@@ -22,5 +27,41 @@
</div>
</div>
</div>
<script type="text/javascript">
var docsSearchTimer;
function docsSearch(q){
clearTimeout(docsSearchTimer);
docsSearchTimer = setTimeout(function(){ docsSearchRun(q); }, 200);
}
function docsSearchRun(q){
q = (q || '').trim();
var $results = $('#docs-search-results');
var $list = $('#docs-list');
if(!q){
$results.hide().empty();
$list.show();
return;
}
// Not app.api.get() -- routes/docs.js is mounted at /docs directly,
// not under /api, unlike the rest of this app's endpoints.
$.getJSON('/docs/search', {q: q}, function(data){
$list.hide();
$results.empty().show();
var hits = (data && data.results) || [];
if(!hits.length){
$results.append($('<p class="text-muted"></p>').text('No results for "' + q + '".'));
return;
}
var $ul = $('<ul class="list-group"></ul>');
hits.forEach(function(hit){
var $li = $('<li class="list-group-item"></li>');
$('<a></a>').attr('href', '/docs/' + hit.slug).text(hit.title).appendTo($li);
$('<div class="text-muted small"></div>').text(hit.snippet).appendTo($li);
$ul.append($li);
});
$results.append($ul);
});
}
</script>
<%- include('bottom') %>
+1
View File
@@ -86,6 +86,7 @@
<div class="card-header text-center">
<span class="card-icon float-start"><i class="fa-solid fa-users-gear"></i></span>
<span class="card-title">Add Group</span>
<a href="/docs/architecture" class="text-reset float-end" title="Help"><i class="fa-solid fa-circle-question"></i></a>
</div>
<div class="card-header actionMessage" style="display:none"></div>
<div class="card-body">
+65 -9
View File
@@ -39,6 +39,7 @@
// Parse the JSON object for a host to something the UI wants
function hostParseRow(host) {
host['created_on_text'] = moment(host['created_on'], "x").fromNow();
host['updated_on_text'] = moment(host['updated_on'], "x").fromNow();
host['wildcard_expires_text'] = moment(host['wildcard_expires'], "x").fromNow();
host['targetssl_text'] = host['targetssl'] ? 'https://' : 'http://';
@@ -191,6 +192,7 @@
let $f = $(form);
$f.attr('method', 'POST').attr('action', 'host').attr('evalAJAX', 'hostModalClose()');
$f.find('[name=host]').prop('disabled', false);
$('#host-rename-help').hide();
if($f.validateClear) $f.validateClear();
// A fresh host only qualifies for HTTP-01 until the name says otherwise.
@@ -210,7 +212,7 @@
hostModal().show();
}
function hostEditOpen(host){
async function hostEditOpen(host){
hostFormReset();
let h = $.scope.hosts.getByKey(host);
let $f = $('#hostForm');
@@ -247,11 +249,40 @@
hostAuthModeChanged(authMode);
hostRenderBasicAuthUsers(host, h.basicauth_users);
// The host name is the key; it can't change on edit. Wildcard hosts can
// still toggle their matching mode.
$f.find('[name=host]').prop('disabled', true);
// The host name is the Redis record's key -- renaming it is a real
// migration (see Host.prototype.update() in models/host.js), scoped
// there to plain hosts only: a wildcard's children reference it by
// name (wildcard_parent) and a cache entry's parent likewise, so
// renaming either would orphan those pointers. Keep the field locked
// for those cases; a plain host can be renamed freely.
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();
@@ -298,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;
}
@@ -386,6 +419,7 @@
<span class="card-icon me-2"><i class="fa-solid fa-network-wired"></i></span>
<span class="card-title fw-bold">Proxy List</span>
<span class="ms-auto">
<a href="/docs/installation" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="hostClearCache(this)" title="Clear cached wildcard subdomain lookups">
<i class="fa-solid fa-broom"></i>
Clear cache
@@ -423,6 +457,7 @@
<th>SSL Expire</th>
<th>Host Name</th>
<th>target</th>
<th class="hidden-xs">Created</th>
<th class="hidden-xs">Updated</th>
<th>Actions</th>
</thead>
@@ -454,6 +489,11 @@
<td>
{{{ targetssl_text }}}{{ ip }}:{{ targetPort }}
</td>
<td class="hidden-xs momentFromNow" data-date="{{ created_on }}" title="Created by {{ created_by }}">
{{ created_on_text }}
<br />
<small class="text-muted">{{ created_by }}</small>
</td>
<td class="hidden-xs momentFromNow" data-date="{{ updated_on }}" >
{{ updated_on_text }}
</td>
@@ -513,13 +553,14 @@
<div class="modal-content card border-0">
<div class="modal-header">
<h5 class="modal-title" id="hostModalTitle">Add host</h5>
<a href="/docs/installation" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="card-header actionMessage m-0" style="display:none"></div>
<div class="modal-body">
<ul class="nav nav-tabs" role="tablist">
<ul class="nav nav-tabs flex-nowrap overflow-x-auto" role="tablist">
<li class="nav-item"><button class="nav-link active" id="hostTab-general-btn" data-bs-toggle="tab" data-bs-target="#hostTab-general" type="button" role="tab">General</button></li>
<li class="nav-item"><button class="nav-link" id="hostTab-tls-btn" data-bs-toggle="tab" data-bs-target="#hostTab-tls" type="button" role="tab">TLS &amp; Wildcard</button></li>
<li class="nav-item"><button class="nav-link" id="hostTab-traffic-btn" data-bs-toggle="tab" data-bs-target="#hostTab-traffic" type="button" role="tab">Traffic</button></li>
@@ -542,6 +583,12 @@
for one subdomain level, <code>**.example.com</code> for any depth,
or <code>**</code> as a catch-all.
</small>
<small id="host-rename-help" class="field-help text-muted d-block" style="display:none">
Wildcard hosts, their children, and auto-created subdomain cache
entries can't be renamed here — the name is referenced elsewhere
(the wildcard's own children, or the cache entry's parent). Delete
and recreate instead.
</small>
</div>
<div class="form-group">
@@ -582,6 +629,7 @@
<input type="radio" name="targetssl" id="targetssl-true" value="true">
Proxy to HTTPS
</label></div>
<small class="field-help text-muted d-block">Whether the proxy talks to the target over HTTP or HTTPS. Independent of Incoming SSL above — clients can use HTTPS to reach the proxy while it still talks plain HTTP to the target, or vice versa.</small>
</div>
</div>
</div>
@@ -620,6 +668,14 @@
<input type="radio" name="wildcard_matchAny" id="wildcard_matchAny-true" value="true">
Match any subdomain and proxy to this host
</label></div>
<small class="field-help text-muted d-block">
"Recommended" only routes subdomains you've explicitly registered
as their own host (optionally as a "Parent Wildcard" child of this
one, to reuse this cert). "Match any" auto-creates a temporary
route to this host's target for <i>any</i> undefined subdomain the
first time it's requested — convenient, but it means every subdomain
typo or scan attempt also gets routed here.
</small>
</div>
</div>
+2
View File
@@ -85,6 +85,7 @@
<i class="fa-solid fa-user-shield"></i>
</span>
<span class="card-title">Add Permission</span>
<a href="/docs/architecture" class="text-reset float-end" title="Help"><i class="fa-solid fa-circle-question"></i></a>
</div>
<div class="card-header actionMessage" style="display:none"></div>
@@ -140,6 +141,7 @@
<i class="fa-solid fa-list-check"></i>
</span>
<span class="card-title">Permissions</span>
<a href="/docs/architecture" class="text-reset float-end" title="Help"><i class="fa-solid fa-circle-question"></i></a>
</div>
<div class="card-header actionMessage" style="display:none"></div>
+2
View File
@@ -66,6 +66,7 @@
Add New User
</span>
<span class="float-end">
<a href="/docs/architecture" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
<i class="fa-solid fa-circle-minus"></i>
</span>
</div>
@@ -107,6 +108,7 @@
User List
</span>
<span class="float-end">
<a href="/docs/architecture" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
<i class="fa-solid fa-circle-minus"></i>
</span>
</div>
+5 -1
View File
@@ -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;