Backend (utils/hostname_validate.js, enforced in routes/host.js on create/update):
- host: IPv4 or a wildcard pattern whose labels may be normal, "*" (one
fragment) or "**" (any depth, incl. a bare "**" catch-all) — matching
Host.lookUp. Lowered Host.host min length to 1 so "**"/"*" pass the model.
- target (ip): IPv4 or a strict hostname, no wildcards.
- Both reject scheme, "/", ":" and whitespace; 422 with per-field keys.
Frontend (val.js) mirrors the rules: host/target validators + hosts.ejs fields
point at them. Unit tests in test/unit/hostname_validate.test.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every proxied request flows through one shared OpenResty location whose
behavior is chosen at request time from the host's Redis hash. Add per-host
controls as new Host fields enforced in Lua rather than static nginx config
(which can't key off a per-request variable):
- Rate limiting: per-client-IP token bucket via resty.limit.req
(ratelimit_enabled/rate/burst), backed by a new `ratelimit` shared dict.
- Response caching: opt-in per host via a global proxy_cache zone gated by
$skip_cache (respcache_enabled). Off by default; upstream Cache-Control
still honored.
- Custom/security headers: req_headers (upstream) + resp_headers (client) and
hsts_enabled, applied in access/header_filter phases.
- IP allow/deny CIDR lists via resty.ipmatcher (deny wins; non-empty allow is
default-deny).
New ops/nginx_conf/hostfeatures.lua holds the enforcement; proxy.conf's
access_by_lua string becomes a block that calls it, plus a header_filter block.
nodejs/utils/host_features.js is the pure, unit-tested normalize/validate layer
(header/CIDR parsing, range clamping, injection-safe values) applied in
routes/host.js and mirrored by the hosts.ejs edit form. install.sh gains the
ipmatcher rock, the cache dir, and the hostfeatures.lua symlink.
Per-host cache TTL is intentionally deferred (global default only) — see the
plan's limitations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A *.example.com wildcard host now chooses between two routing modes:
- wildcard_matchAny=false (default): only subdomains explicitly defined
in redis route; undefined subdomains get no match (406)
- wildcard_matchAny=true: any subdomain catches-all to the wildcard
parent host, preserving the previous behavior
The gate lives in the host_lookup socket service, which is only reached
for domains missing a direct redis entry, so defined children and
**-style hosts are unaffected. Adds the matching-mode selector to the
host add/edit form, shown for wildcard hosts.
Note: existing wildcard hosts have no wildcard_matchAny field and so
default to the stricter "only defined" mode until re-saved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>