Commit Graph

48 Commits

Author SHA1 Message Date
wmantly be666f5b2f Recognize app_super_admin; add-user/add-permission as modal buttons; persist LE key
- app_super_admin is a new cross-app LDAP group (also recognized by
  sso-manager-node and jump-host): added to conf.auth.adminGroups so
  members are always global admins here, same as the existing anti-lockout
  adminUsers/adminGroups mechanism.
- Users and Permissions pages: the always-visible sidebar "Add" forms are
  now an "Add User"/"Add Permission" button in the list header that opens
  an app.modal dialog, matching the hosts.ejs convention.
- The Let's Encrypt ACME account key now defaults to the already-persisted
  /data volume (models/host.js) instead of a CWD-relative path
  (./le_key.cert -> /app/le_key.cert in the container), which was lost on
  every image rebuild. Falls back to the old relative path when /data isn't
  present (e.g. local dev outside docker).
2026-07-30 11:57:44 -04:00
wmantly 4321826dc8 feat: Add multi-target load balancing support
Fixes #47.
- Added lua-resty-balancer to dependencies (Dockerfile & install.sh).
- Added 'targets' field to the Host model to hold additional targets.
- Updated the UI to allow inputting additional targets (IP:port).
- Updated targetinfo.lua to parse the additional targets and load balance between them and the primary target using resty.balancer.round_robin.
2026-07-21 00:48:43 -04:00
wmantly fcd97b12aa Fix attaching an existing sibling subdomain to a parent wildcard
Host.lookUpWildcardParent() walked all labels of the host down to its own
leaf and only inspected that leaf's "*" child, so it found a wildcard
nested under the host (the base-domain case, e.g. *.cool.mysite.com for
cool.mysite.com) but missed the common case where the wildcard is a
SIBLING of the host's leftmost label (e.g. *.nl.wgnode.com covering an
already-existing sso.nl.wgnode.com). The /wildcard-parent route then
returned nothing and the edit form's "Parent Wildcard" option stayed
greyed out, leaving no way to convert an existing auto-SSL host onto a
wildcard issued afterward.

Track the parent node during the walk and check the sibling "*" slot too.
The never-created-subdomain case is unchanged (plain lookUp()'s wildcard
fallback in the route still handles it). Sync the test mock and add
regression coverage for the sibling case (existing host, never-created
host via the route fallback, and a deeper-wildcard negative case).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:13:13 -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
wmantly 17b903e228 Fix two wildcard-cert gaps: attaching an existing host, and the wildcard's own base domain
- Host.prototype.update() had no challengeType handling (only create() did),
  so selecting "Parent Wildcard" on an existing host's edit form silently
  did nothing. Added the same wildcard-parent lookup to update(), using a
  new Host.lookUpWildcardParent() -- the existing lookUp() can't be reused
  here since an already-created host resolves to its own leaf rather than
  falling through to a sibling wildcard.

- A wildcard's issued cert covers both the base domain and *.base domain
  (altNames), but the lookup tree stores the wildcard one level below its
  base -- looking up the bare base domain landed on an empty parent node
  and found nothing. buildLookUpObj() now also stamps that parent node,
  order-independent (a real host explicitly created at that exact name
  always still wins).

Verified both fixes against a real Redis-backed Host model (not just the
mocked lookup-tree tests) -- see PR description.

Bumps to v1.1.8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 18:46:09 -04:00
wmantly ad2cacf094 Make basic auth and SSO mutually exclusive per host; fix silently-broken validation errors
- Auth tab is now a single choice (Off / Basic auth / SSO) instead of two
  independent toggles that could both be on at once, which made it
  ambiguous which gate actually protected a request. Enforced both in the
  UI and server-side (POST/PUT), accounting for partial PUT updates against
  the existing record.
- Add per-user basic-auth management (change password, delete) so an admin
  no longer has to blow away and retype the whole user list to remove or
  rotate one account.
- Fix: `Model.errors.ObjectValidateError(...)` is a constructor and was
  being called without `new` everywhere in this codebase. Without `new`,
  `this` inside it was the module's shared `errors` object (mutated in
  place) and the call evaluated to `undefined` — so every
  `throw Model.errors.ObjectValidateError(...)` actually threw `undefined`,
  which Express's `next(undefined)` treats as "no error" and silently
  falls through to the catch-all 404 handler. Every host/user/group/
  permission/dns-provider validation error (bad hostname, bad IP, etc.) was
  showing a confusing "Page not found" instead of the real message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 00:41:16 -04:00
wmantly 653c79f099 Per-host SSO: data model + normalization (#57)
Add Host fields sso_enabled / sso_allow_users / sso_allow_groups (empty
allow-lists = any authenticated user) and normalize them (parseAllowList). SSO
reuses conf.oidc and is OR'd with basic auth. Enforcement (session cookie + Lua
+ nginx auth location) lands separately. Unit tests included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:01:54 -04:00
wmantly 3e5590288a Per-host HTTP basic auth (#57)
Adds opt-in basic auth per Host, following the existing per-host controls
pattern:
- Host fields basicauth_enabled / basicauth_realm / basicauth_users
  ({user: base64(sha1(pw))}). Credentials are parsed to plaintext by the pure
  host_features normalizer and hashed at the route layer (utils/basicauth.js),
  so plaintext never reaches Redis.
- ops/nginx_conf/hostfeatures.lua enforces it in access phase: verifies the
  Authorization header against base64(sha1(password)), fails closed with a 401
  WWW-Authenticate challenge.
- hosts.ejs gains an enable toggle, realm, and a username:password textarea
  (passwords never echoed back; blank keeps the current set).

Unit tests cover hashing (matches the htpasswd {SHA} vector), credential
parsing, and normalization. Note: the Lua path needs verification on a live
OpenResty box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:47:08 -04:00
wmantly 6d8dc45209 Validate host/target fields (hostname or IP; host allows */** wildcards)
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>
2026-07-11 11:18:47 -04:00
wmantly 6092468901 Add per-host reverse-proxy controls (rate limit, cache, headers, IP ACL)
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>
2026-07-10 22:10:55 -04:00
wmantly 7cc7f95019 Add TTL-based expiry for wildcard subdomain cache entries
Adopt model-redis v1.5.0 and give the on-demand is_cache Host records
(and their Cached tracking records) created by Host.addCache a TTL, so
they auto-expire instead of accumulating forever. Only the record hash
carries the TTL, so OpenResty's direct HGETALL sees a miss once it
expires and re-resolves through the lookup path.

The lifetime is configurable via conf.cacheTTL (seconds, default 3600;
0 disables expiry). This also mitigates the matchAny=false "wrong host"
bug: stale leftover cache entries now expire (and are still busted on
parent update), so undefined subdomains stop being served by old caches.

Add test/unit/wildcard_matchany.test.js covering the matchAny routing
decision (defined vs undefined subdomains, mixed-policy sibling
wildcards, and cache-entry behavior) and wire it into the test scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:51:06 -04:00
wmantly 626a4189fa Fixed clearcahce 2026-07-10 00:39:25 -04:00
wmantly 99cdbe6586 Fixed issue with lookup 2026-07-10 00:35:50 -04:00
wmantly 1295d4f5ce Fixed issue with matching wild card 2026-07-10 00:24:16 -04:00
wmantly 56c2fb1a5c Fixed issue with matching wild card 2026-07-10 00:13:40 -04:00
wmantly 2665e7d466 Fix cert deletion key and harden nginx SSL/redis handling
- host.remove() now deletes the cert under the host key instead of the
  Domain relation object, so certs are actually removed from redis
- targetinfo.lua returns the redis connection to the pool via
  set_keepalive instead of leaking one connection per request
- autossl.conf drops TLSv1/1.1 and 3DES, adds TLSv1.3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:29:43 -04:00
wmantly 59bba8273a Fixed issue with cert renew 2026-02-25 22:59:04 -05:00
wmantly bcc777ccdf Delete certs from redis 2026-02-25 22:21:16 -05:00
wmantly 1bacc17a12 Fixed host put on frontend 2026-02-25 19:45:09 -05:00
wmantly 585aa14c0b services 2026-02-25 19:17:25 -05:00
wmantly 0a63e05028 Import path fix 2026-02-25 12:35:21 -05:00
wmantly 21ff1b0bb5 DNS route 2026-02-25 12:11:31 -05:00
wmantly 6cb2880131 Using the simpleworks conf now 2025-12-31 19:28:26 -05:00
wmantly ef3fffdef2 Updated DNS-01 validation 2025-12-31 16:00:21 -05:00
wmantly 17f8bec34d Polished off DNS api models 2024-08-15 18:51:36 -04:00
wmantly b27e1b1617 Added relations to redis ORM 2024-08-13 15:18:47 -04:00
noot c38d2c6d0c cf change wip 2024-08-13 01:29:57 +08:00
wmantly 3df3341235 DNS stuff 2024-08-10 23:07:32 -04:00
wmantly f44284685f Fixed nginx conf issue 2024-08-07 21:34:03 -04:00
wmantly 41919db8f8 wild card UI done 2024-08-06 17:52:00 -04:00
wmantly 84db245f4a fixed issue with redis models 2024-08-06 13:11:55 -04:00
wmantly 0eb6af1723 syntax cleanups 2024-07-31 20:19:14 -04:00
wmantly fb9a19d5f0 Changed .add to .create 2024-07-31 20:02:51 -04:00
wmantly 441e372987 Added socket.io 2024-07-31 19:01:55 -04:00
wmantly fc66225cf5 new front end #33 2024-07-30 20:47:46 -04:00
wmantly 99ab768f45 fixed prefix issue 2023-08-17 13:26:47 -04:00
wmantly 41325f7a55 updated files to ES6 2023-08-17 12:28:32 -04:00
wmantly e34af091bc fixed typo 2021-01-01 16:50:16 -05:00
wmantly 6ae2a081e9 removed tests from hosts models 2020-12-29 13:20:22 -05:00
wmantly 1ff7d3cb4f added caching for complex lookups 2020-12-29 13:15:27 -05:00
wmantly bce23ab9fe update lookup object on edit 2020-12-22 21:27:17 -05:00
wmantly 7053536353 service for complex look up 2020-12-22 20:20:15 -05:00
wmantly 7d1f4f800f removed some simple tests 2020-12-21 21:32:38 -05:00
wmantly 9c3d29bb7b wild card socket lookup 2020-12-21 21:28:59 -05:00
wmantly ea319109a2 small fixes 2020-12-21 00:17:30 -05:00
wmantly 6e1b987c34 better wild cards 2020-12-21 00:14:12 -05:00
wmantly 1bf358565a migrations 2020-04-11 14:48:45 -04:00
wmantly 255d6568fd new model model 2020-04-09 22:22:54 -04:00