Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a83fb8252 | |||
| 17b903e228 | |||
| 11f44176c0 | |||
| b4d971b508 | |||
| 28f1c53d06 | |||
| 8c3a263937 | |||
| e249b4e168 | |||
| 34b1413c96 | |||
| eded87b6f9 | |||
| a57f3f03f6 | |||
| 5b08eecca9 | |||
| 7c0cb5eabd | |||
| 3f0d6fb438 | |||
| 6cd3a5bc58 |
+26
-1
@@ -6,6 +6,27 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.1.8] - 2026-07-17
|
||||
|
||||
### 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()`.
|
||||
- **Couldn't register a wildcard's own base domain as a host.** A wildcard cert's `altNames` already cover both the base domain and `*.base domain`, but the lookup tree stores the wildcard one level below its base domain, and a lookup for the bare base domain landed on that empty parent node and found nothing — even though the already-issued cert covers it. `buildLookUpObj()` now also stamps the parent node so this resolves correctly, without re-issuing or duplicating the cert.
|
||||
|
||||
Both required a corrected lookup: attaching an *existing* host (which already has its own tree leaf) needed a new `Host.lookUpWildcardParent()` that checks the sibling wildcard slot instead of resolving to the host's own record.
|
||||
|
||||
### Changed
|
||||
- Redesigned the GitHub Pages docs site to match the app's own look (dark navbar/footer, Bootstrap 5, Font Awesome) instead of the generic `jekyll-theme-cayman` theme, added a real cross-page nav, SEO (`jekyll-seo-tag` + `jekyll-sitemap`, per-page descriptions, OG/Twitter tags, sitemap.xml, robots.txt), and mobile-responsive layout.
|
||||
|
||||
## [1.1.6] - 2026-07-16
|
||||
|
||||
### Fixed
|
||||
- Hosts admin UI's Authentication tab radios (Off / Basic / SSO) had no shared `name`, so clicking one didn't uncheck the others -- multiple options could appear selected at once. Added `name="auth_mode"` to restore standard exclusive radio-group behavior.
|
||||
|
||||
## [1.1.5] - 2026-07-16
|
||||
|
||||
### Fixed
|
||||
- Bumped `jq-repeat` 2.0.1 -> 2.1.0. Fixed real breakage: `users.ejs`/`groups.ejs`/`permissions.ejs` called the removed `$.scope.X.__setPut(fn)`/`__setTake(fn)` setter-method API; insert/remove row hooks are now set via direct property assignment (`$.scope.X.__put = fn`), matching 2.1.0's API.
|
||||
|
||||
## [1.1.4] - 2026-07-16
|
||||
|
||||
### Added
|
||||
@@ -39,7 +60,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.4...HEAD
|
||||
[Unreleased]: https://github.com/theta42/proxy/compare/v1.1.8...HEAD
|
||||
[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
|
||||
[1.1.5]: https://github.com/theta42/proxy/compare/v1.1.4...v1.1.5
|
||||
[1.1.4]: https://github.com/theta42/proxy/compare/v1.1.3...v1.1.4
|
||||
[1.1.3]: https://github.com/theta42/proxy/compare/v1.1.2...v1.1.3
|
||||
[1.1.2]: https://github.com/theta42/proxy/compare/v1.1.1...v1.1.2
|
||||
|
||||
+41
-3
@@ -1,9 +1,47 @@
|
||||
title: Proxy
|
||||
description: A reverse proxy and HTTPS termination service using OpenResty/nginx with a management API and web GUI
|
||||
theme: jekyll-theme-cayman
|
||||
show_downloads: false
|
||||
description: A reverse proxy and HTTPS termination service built on OpenResty/nginx, with an OIDC + LDAP-aware management API and web GUI.
|
||||
url: "https://theta42.github.io"
|
||||
baseurl: "/proxy"
|
||||
logo: /assets/img/theta42.svg
|
||||
lang: en_US
|
||||
|
||||
plugins:
|
||||
- jekyll-seo-tag
|
||||
- jekyll-sitemap
|
||||
|
||||
github:
|
||||
repository_url: https://github.com/theta42/proxy
|
||||
zip_url: https://github.com/theta42/proxy/archive/refs/heads/master.zip
|
||||
tar_url: https://github.com/theta42/proxy/archive/refs/heads/master.tar.gz
|
||||
repository_name: theta42/proxy
|
||||
|
||||
nav:
|
||||
- title: Home
|
||||
page: /
|
||||
icon: fa-house
|
||||
- title: Installation
|
||||
page: /installation.html
|
||||
icon: fa-download
|
||||
- title: Architecture
|
||||
page: /architecture.html
|
||||
icon: fa-sitemap
|
||||
- title: API
|
||||
page: /api.html
|
||||
icon: fa-code
|
||||
- title: Docker
|
||||
page: /docker.html
|
||||
icon: fa-box
|
||||
- title: Contributing
|
||||
page: /contributing.html
|
||||
icon: fa-code-branch
|
||||
- title: Changelog
|
||||
url: https://github.com/theta42/proxy/blob/master/CHANGELOG.md
|
||||
icon: fa-list
|
||||
|
||||
defaults:
|
||||
- scope:
|
||||
path: ""
|
||||
type: "pages"
|
||||
values:
|
||||
layout: default
|
||||
image: /assets/img/theta42.svg
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ '/assets/img/favicon.svg' | relative_url }}">
|
||||
|
||||
{% seo title=false %}
|
||||
<title>{% if page.title %}{{ page.title }} · {% endif %}{{ site.title }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||
<link rel="stylesheet" href="{{ '/assets/css/style.css' | relative_url }}">
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
|
||||
<div class="container-fluid px-3">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{{ '/' | relative_url }}">
|
||||
<img src="{{ '/assets/img/theta42.svg' | relative_url }}" height="28" class="me-2" alt="">
|
||||
{{ site.title }}
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain" aria-controls="navMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navMain">
|
||||
<ul class="navbar-nav">
|
||||
{% for item in site.nav %}
|
||||
<li class="nav-item">
|
||||
{% if item.page %}
|
||||
<a class="nav-link{% if page.url == item.page %} active{% endif %}" href="{{ item.page | relative_url }}">
|
||||
{% if item.icon %}<i class="fa-solid {{ item.icon }}"></i>{% endif %} {{ item.title }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{{ item.url }}" target="_blank" rel="noopener">
|
||||
{% if item.icon %}<i class="fa-solid {{ item.icon }}"></i>{% endif %} {{ item.title }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="flex-grow-1" style="margin-top: 4.5rem;">
|
||||
<div class="container-fluid py-4 py-md-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-8">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-body p-4 p-md-5 site-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="py-3 bg-dark text-light mt-auto">
|
||||
<div class="container-fluid d-flex flex-wrap justify-content-between align-items-center small gap-2 px-3">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<a href="https://theta42.com" target="_blank" rel="noopener">
|
||||
<img width="40" src="{{ '/assets/img/theta42.svg' | relative_url }}" alt="theta42">
|
||||
</a>
|
||||
© {{ 'now' | date: '%Y' }} theta42 ·
|
||||
<a href="{{ site.github.repository_url }}/blob/master/LICENSE" target="_blank" rel="noopener" class="text-light">MIT License</a>
|
||||
</span>
|
||||
<span class="d-flex align-items-center gap-3">
|
||||
<a href="{{ site.github.repository_url }}" target="_blank" rel="noopener" class="text-light text-decoration-none">
|
||||
<i class="fa-brands fa-github"></i> GitHub
|
||||
</a>
|
||||
<a href="{{ site.github.repository_url }}/blob/master/CHANGELOG.md" target="_blank" rel="noopener" class="text-light text-decoration-none">
|
||||
<i class="fa-solid fa-list"></i> Changelog
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: API Reference
|
||||
description: The proxy's management REST API — hosts, DNS providers, users, groups, and permissions.
|
||||
---
|
||||
|
||||
# API Documentation
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Architecture
|
||||
description: How the proxy's OIDC client, LDAP client, and OpenResty routing fit together.
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/* theta42 docs site — shares the in-app dark navbar/footer + card look
|
||||
(Bootstrap 5 + Font Awesome, same as the running apps) rather than a
|
||||
generic Jekyll theme. */
|
||||
|
||||
body {
|
||||
background-color: #f4f5f6;
|
||||
}
|
||||
|
||||
.navbar-brand img {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .4));
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link.active {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Markdown content typography, scoped to the card body so it doesn't leak
|
||||
into the nav/footer. */
|
||||
.site-content h1:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.site-content h1,
|
||||
.site-content h2,
|
||||
.site-content h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.site-content h2 {
|
||||
margin-top: 2.5rem;
|
||||
padding-bottom: .4rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.site-content h3 {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.site-content a {
|
||||
color: #a3671f;
|
||||
text-decoration-color: rgba(163, 103, 31, .35);
|
||||
}
|
||||
|
||||
.site-content a:hover {
|
||||
color: #8a5a16;
|
||||
}
|
||||
|
||||
.site-content pre {
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: .375rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.site-content code {
|
||||
color: #a3671f;
|
||||
background-color: #f4f0e8;
|
||||
padding: .15em .4em;
|
||||
border-radius: .25rem;
|
||||
font-size: .875em;
|
||||
}
|
||||
|
||||
.site-content pre code {
|
||||
color: inherit;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.site-content table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
.site-content table th,
|
||||
.site-content table td {
|
||||
border: 1px solid #dee2e6;
|
||||
padding: .5rem .75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.site-content table th {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.site-content blockquote {
|
||||
border-left: 4px solid #C59341;
|
||||
padding: .5rem 1rem;
|
||||
margin: 1.25rem 0;
|
||||
background-color: #f8f6f1;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.site-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Screenshot grids in the markdown use width="49%" inline attrs for a
|
||||
two-up desktop layout -- stack them on narrow screens instead of
|
||||
squeezing to illegibility. */
|
||||
@media (max-width: 576px) {
|
||||
.site-content img[width] {
|
||||
width: 100% !important;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.site-content hr {
|
||||
margin: 2rem 0;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<!-- Background circle -->
|
||||
<circle cx="50" cy="50" r="48" fill="#1a1a1a" stroke="#4a9eff" stroke-width="3"/>
|
||||
|
||||
<!-- Network nodes -->
|
||||
<circle cx="30" cy="30" r="8" fill="#4a9eff"/>
|
||||
<circle cx="70" cy="30" r="8" fill="#4a9eff"/>
|
||||
<circle cx="50" cy="50" r="10" fill="#66b3ff"/>
|
||||
<circle cx="30" cy="70" r="8" fill="#4a9eff"/>
|
||||
<circle cx="70" cy="70" r="8" fill="#4a9eff"/>
|
||||
|
||||
<!-- Connection lines -->
|
||||
<line x1="30" y1="30" x2="50" y2="50" stroke="#4a9eff" stroke-width="2"/>
|
||||
<line x1="70" y1="30" x2="50" y2="50" stroke="#4a9eff" stroke-width="2"/>
|
||||
<line x1="30" y1="70" x2="50" y2="50" stroke="#4a9eff" stroke-width="2"/>
|
||||
<line x1="70" y1="70" x2="50" y2="50" stroke="#4a9eff" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 788 B |
@@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="gold-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#C59341" />
|
||||
<stop offset="20%" stop-color="#E4B869" />
|
||||
<stop offset="40%" stop-color="#FBF0B9" />
|
||||
<stop offset="60%" stop-color="#DFB260" />
|
||||
<stop offset="80%" stop-color="#BC8837" />
|
||||
<stop offset="100%" stop-color="#A36F28" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="text-grad" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#FFFFFF" />
|
||||
<stop offset="40%" stop-color="#F5E3B5" />
|
||||
<stop offset="70%" stop-color="#D4A343" />
|
||||
<stop offset="100%" stop-color="#8A5A16" />
|
||||
</linearGradient>
|
||||
|
||||
<filter id="drop-shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="8" stdDeviation="6" flood-color="#000000" flood-opacity="0.4"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<g filter="url(#drop-shadow)">
|
||||
<g fill="url(#gold-grad)">
|
||||
<path d="M 200,40
|
||||
C 290,40 350,110 350,200
|
||||
C 350,290 290,360 200,360
|
||||
C 110,360 50,290 50,200
|
||||
C 50,110 110,40 200,40 Z
|
||||
M 200,75
|
||||
C 130,75 88,130 88,200
|
||||
C 88,270 130,325 200,325
|
||||
C 270,325 312,270 312,200
|
||||
C 312,130 270,75 200,75 Z"
|
||||
fill-rule="evenodd" />
|
||||
|
||||
<path d="M 88,190 L 140,190 C 140,190 142,210 140,210 L 88,210 Z" />
|
||||
|
||||
<path d="M 260,190 L 312,190 C 312,190 310,210 260,210 Z" />
|
||||
</g>
|
||||
|
||||
<text x="200" y="222"
|
||||
font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
||||
font-size="78"
|
||||
font-weight="900"
|
||||
fill="url(#text-grad)"
|
||||
text-anchor="middle"
|
||||
letter-spacing="-2">42</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Contributing
|
||||
description: How to contribute to the proxy — dev setup, tests, and code conventions.
|
||||
---
|
||||
|
||||
# Contributing Guide
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Docker
|
||||
description: Running the proxy's all-in-one Docker image — OpenResty, the management app, and Redis in one container.
|
||||
---
|
||||
|
||||
# Docker Deployment
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Home
|
||||
description: A reverse proxy and HTTPS termination service built on OpenResty/nginx, with automatic Let's Encrypt certs, OIDC login, and direct LDAP access control per host.
|
||||
---
|
||||
|
||||
# Proxy
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Installation
|
||||
description: Installing the proxy — Docker, bare metal, or as part of the unified theta-env stack.
|
||||
---
|
||||
|
||||
# Installation Guide
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://theta42.github.io/proxy/sitemap.xml
|
||||
+62
-2
@@ -320,9 +320,30 @@ class Host extends Table{
|
||||
}
|
||||
}
|
||||
|
||||
async update(...args){
|
||||
async update(data, ...args){
|
||||
try{
|
||||
let out = await super.update(...args)
|
||||
// Mirror Host.create()'s challengeType handling (lines above) so an
|
||||
// existing HTTP-01 host can be attached to a parent wildcard's cert
|
||||
// after creation -- previously this was silently dropped since only
|
||||
// create() understood challengeType, leaving no way to convert an
|
||||
// existing host onto a wildcard once one was issued.
|
||||
if(data && data.challengeType === 'wildcardChild'){
|
||||
// Not Host.lookUp() -- this.host already has its own leaf in the
|
||||
// tree (it already exists), so a plain lookUp() would just find
|
||||
// itself. lookUpWildcardParent() checks the sibling "*" slot
|
||||
// instead. See its comment for why create()'s own wildcardChild
|
||||
// branch doesn't need this (a host being newly created hasn't
|
||||
// claimed its own leaf yet, so plain lookUp() already falls
|
||||
// through to the wildcard correctly there).
|
||||
let parentHost = Host.lookUpWildcardParent(this.host);
|
||||
if(parentHost && parentHost.is_wildcard){
|
||||
data.wildcard_parent = parentHost.host;
|
||||
}else{
|
||||
throw new Error(`No parent wild card for ${this.host}`);
|
||||
}
|
||||
}
|
||||
|
||||
let out = await super.update(data, ...args)
|
||||
await this.bustCache(this.host);
|
||||
await Host.buildLookUpObj();
|
||||
|
||||
@@ -385,6 +406,25 @@ class Host extends Table{
|
||||
// #record denotes a leaf node on this tree.
|
||||
if(fragments.length === 0){
|
||||
pointer[fragment]['#record'] = await this.get(host)
|
||||
|
||||
// A single-level wildcard's issued cert also covers its own
|
||||
// base domain (createWildcardCert requests altNames:
|
||||
// [domain, *.domain] -- see utils/letsencrypt.js), but the
|
||||
// base domain sits one level ABOVE the wildcard's own leaf
|
||||
// in this tree (e.g. "*.cool.mysite.com" is a child of the
|
||||
// node for "cool.mysite.com"). Without this, looking up the
|
||||
// bare base domain when it has no host of its own falls
|
||||
// through to nothing, even though the already-issued cert
|
||||
// covers it. `pointer` here is still that parent node
|
||||
// (reassigned to the child only below) -- stamp it too, but
|
||||
// only if a real, explicitly-created host at that exact
|
||||
// name hasn't already claimed this leaf (order-independent:
|
||||
// this only ever fills a gap -- a real host's own pass
|
||||
// through this loop always overwrites #record
|
||||
// unconditionally when it's finalized, see above).
|
||||
if(fragment === '*' && !pointer['#record']){
|
||||
pointer['#record'] = pointer[fragment]['#record'];
|
||||
}
|
||||
}
|
||||
|
||||
// Advance the pointer to the next level of the tree.
|
||||
@@ -445,6 +485,26 @@ class Host extends Table{
|
||||
if(parent && parent['*'] && parent['*']['#record']) return parent['*']['#record'];
|
||||
}
|
||||
|
||||
// Find the wildcard covering @host as its own base domain (e.g.
|
||||
// "*.cool.mysite.com" for host="cool.mysite.com"), regardless of whether
|
||||
// @host is already registered as its own host. Unlike lookUp(), which
|
||||
// walks to and returns @host's own exact-match leaf when one exists, this
|
||||
// walks to that exact position and looks one level deeper at its "*"
|
||||
// child -- the sibling wildcard slot -- so it still finds the parent
|
||||
// wildcard even when @host already has its own (non-wildcard) record.
|
||||
// Used when attaching an already-created host to a wildcard after the
|
||||
// fact (see update() below); Host.create()'s own wildcardChild handling
|
||||
// can keep using plain lookUp() since a host being newly created hasn't
|
||||
// claimed its own leaf yet.
|
||||
static lookUpWildcardParent(host){
|
||||
let place = this.lookUpObj;
|
||||
for(let fragment of host.split('.').reverse()){
|
||||
if(!place[fragment]) return undefined;
|
||||
place = place[fragment];
|
||||
}
|
||||
if(place['*'] && place['*']['#record']) return place['*']['#record'];
|
||||
}
|
||||
|
||||
static async lookUpReady(){
|
||||
/*
|
||||
Wait for the lookup tree to be built.
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "proxy-api",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "proxy-api",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||
@@ -21,7 +21,7 @@
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"extend": "^3.0.2",
|
||||
"jq-repeat": "^2.0.1",
|
||||
"jq-repeat": "^2.1.0",
|
||||
"jquery": "^4.0.0",
|
||||
"ldapts": "^8.1.8",
|
||||
"linux-sys-user": "^1.2.0",
|
||||
@@ -1375,9 +1375,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jq-repeat": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.0.1.tgz",
|
||||
"integrity": "sha512-ATI25tKQG3uHW8f8XPqBe85JsH4PNGHA/YLy1KgMVeYDoUSf9cqGNBum+4A+Pg1WKh9PA6bYyWfYNsgktwIbSg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.1.0.tgz",
|
||||
"integrity": "sha512-e1OmSWeBEHEtyOhNVysx0bnT5wd6HlZ37JZgPcGPmACJ0K9bXDPq0xOwrM1slQMSTw7FOSNDX+MD6VwvPeeZyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proxy-api",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.8",
|
||||
"private": true,
|
||||
"author": [
|
||||
{
|
||||
@@ -32,7 +32,7 @@
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"extend": "^3.0.2",
|
||||
"jq-repeat": "^2.0.1",
|
||||
"jq-repeat": "^2.1.0",
|
||||
"jquery": "^4.0.0",
|
||||
"ldapts": "^8.1.8",
|
||||
"linux-sys-user": "^1.2.0",
|
||||
|
||||
@@ -136,6 +136,125 @@ describe('Host Lookup Algorithm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests for the wildcard's-own-base-domain fix: a single-level wildcard's
|
||||
* issued cert also covers its own base domain (altNames: [domain, *.domain],
|
||||
* see utils/letsencrypt.js), but that base domain sits one tree level ABOVE
|
||||
* the wildcard's own leaf. buildLookUpObj() now also stamps that parent
|
||||
* node's #record, and lookUpWildcardParent() finds it even when the base
|
||||
* domain is ALSO separately registered as its own plain host (the "attach an
|
||||
* existing host to a parent wildcard" case, unlike lookUp() which would just
|
||||
* resolve to that host's own record).
|
||||
*/
|
||||
describe('Host wildcard base-domain lookup', () => {
|
||||
|
||||
let Host;
|
||||
|
||||
before(async () => {
|
||||
Host = createMockHostClassWithWildcardParentFix();
|
||||
});
|
||||
|
||||
test('lookUp finds the wildcard record for its own bare base domain when no plain host exists', async () => {
|
||||
await populateTree(Host, ['*.cool.mysite.com']);
|
||||
const result = Host.lookUp('cool.mysite.com');
|
||||
assert.ok(result, 'Should find a match');
|
||||
assert.strictEqual(result.host, '*.cool.mysite.com');
|
||||
});
|
||||
|
||||
test('lookUp still prefers an explicitly-created plain host over the wildcard, regardless of population order', async () => {
|
||||
await populateTree(Host, ['*.cool.mysite.com', 'cool.mysite.com']);
|
||||
assert.strictEqual(Host.lookUp('cool.mysite.com').host, 'cool.mysite.com');
|
||||
|
||||
await populateTree(Host, ['cool.mysite.com', '*.cool.mysite.com']);
|
||||
assert.strictEqual(Host.lookUp('cool.mysite.com').host, 'cool.mysite.com');
|
||||
});
|
||||
|
||||
test('lookUpWildcardParent finds the wildcard even when the base domain already has its own plain host', async () => {
|
||||
await populateTree(Host, ['*.cool.mysite.com', 'cool.mysite.com']);
|
||||
const result = Host.lookUpWildcardParent('cool.mysite.com');
|
||||
assert.ok(result, 'Should find the sibling wildcard');
|
||||
assert.strictEqual(result.host, '*.cool.mysite.com');
|
||||
});
|
||||
|
||||
test('lookUpWildcardParent returns undefined when there is no wildcard sibling', async () => {
|
||||
await populateTree(Host, ['cool.mysite.com']);
|
||||
assert.strictEqual(Host.lookUpWildcardParent('cool.mysite.com'), undefined);
|
||||
});
|
||||
|
||||
test('lookUpWildcardParent returns undefined for an unrelated host', async () => {
|
||||
await populateTree(Host, ['*.cool.mysite.com']);
|
||||
assert.strictEqual(Host.lookUpWildcardParent('other.example.com'), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Same mock shape as createMockHostClass() above, plus the parent-record
|
||||
* stamp in the tree-population loop and the lookUpWildcardParent() method --
|
||||
* both copied from the real implementation in models/host.js.
|
||||
*/
|
||||
function createMockHostClassWithWildcardParentFix() {
|
||||
return class MockHost {
|
||||
static lookUpObj = {};
|
||||
|
||||
static lookUp(host) {
|
||||
let place = this.lookUpObj;
|
||||
let last_resort = {};
|
||||
let parent = undefined;
|
||||
|
||||
for(let fragment of host.split('.').reverse()){
|
||||
parent = place;
|
||||
if(place['**']) last_resort = place['**'];
|
||||
if({...last_resort, ...place}[fragment]){
|
||||
place = {...last_resort, ...place}[fragment];
|
||||
}else if(place['*']){
|
||||
place = place['*']
|
||||
}else if(last_resort){
|
||||
place = last_resort;
|
||||
}
|
||||
}
|
||||
|
||||
if(place && place['#record']) return place['#record'];
|
||||
if(parent && parent['*'] && parent['*']['#record']) return parent['*']['#record'];
|
||||
}
|
||||
|
||||
static lookUpWildcardParent(host) {
|
||||
let place = this.lookUpObj;
|
||||
for(let fragment of host.split('.').reverse()){
|
||||
if(!place[fragment]) return undefined;
|
||||
place = place[fragment];
|
||||
}
|
||||
if(place['*'] && place['*']['#record']) return place['*']['#record'];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function populateTree(Host, hosts) {
|
||||
Host.lookUpObj = {};
|
||||
|
||||
for(let host of hosts){
|
||||
let fragments = host.split('.');
|
||||
let pointer = Host.lookUpObj;
|
||||
|
||||
while(fragments.length){
|
||||
let fragment = fragments.pop();
|
||||
|
||||
if(!pointer[fragment]){
|
||||
pointer[fragment] = {};
|
||||
}
|
||||
|
||||
if(fragments.length === 0){
|
||||
pointer[fragment]['#record'] = {host};
|
||||
|
||||
if(fragment === '*' && !pointer['#record']){
|
||||
pointer['#record'] = pointer[fragment]['#record'];
|
||||
}
|
||||
}
|
||||
|
||||
pointer = pointer[fragment];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock Host class with just the lookUp functionality
|
||||
* This allows us to test the algorithm without Redis dependencies
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
|
||||
loadUserSuggestions();
|
||||
|
||||
$.scope.LocalGroup.__setTake(function($el){
|
||||
$.scope.LocalGroup.__take = function($el){
|
||||
$el.addClass('bg-danger');
|
||||
$el.fadeOut(600, function(){ $el.remove(); });
|
||||
});
|
||||
};
|
||||
|
||||
app.subscribe(/^model:LocalGroup:create/, function(data){
|
||||
$.scope.LocalGroup.remove(data.name);
|
||||
|
||||
+10
-7
@@ -115,10 +115,13 @@
|
||||
// attach users to).
|
||||
let hostFormCurrentHost = null;
|
||||
|
||||
// The auth_mode radios aren't real form fields (no [name]); this keeps the
|
||||
// two hidden basicauth_enabled/sso_enabled inputs — the ones actually
|
||||
// submitted — in sync so only one can ever be true, and shows/hides the
|
||||
// matching field group.
|
||||
// The auth_mode radios share a name so the browser enforces mutual
|
||||
// exclusivity, but auth_mode itself isn't in Host's _keyMap -- the model
|
||||
// layer strips unrecognized fields on save (see model-redis's
|
||||
// processKeys), so it's never actually persisted. This keeps the two
|
||||
// hidden basicauth_enabled/sso_enabled inputs -- the real, submitted
|
||||
// fields -- in sync with whichever radio is selected, and shows/hides
|
||||
// the matching field group.
|
||||
function hostAuthModeChanged(mode){
|
||||
$('#basicauth_enabled-hidden').val(mode === 'basic' ? 'true' : 'false');
|
||||
$('#sso_enabled-hidden').val(mode === 'sso' ? 'true' : 'false');
|
||||
@@ -713,15 +716,15 @@
|
||||
|
||||
<div class="form-group">
|
||||
<div class="radio"><label>
|
||||
<input type="radio" id="auth_mode-none" value="none" checked onchange="hostAuthModeChanged('none')">
|
||||
<input type="radio" name="auth_mode" id="auth_mode-none" value="none" checked onchange="hostAuthModeChanged('none')">
|
||||
Off (public)
|
||||
</label></div>
|
||||
<div class="radio"><label>
|
||||
<input type="radio" id="auth_mode-basic" value="basic" onchange="hostAuthModeChanged('basic')">
|
||||
<input type="radio" name="auth_mode" id="auth_mode-basic" value="basic" onchange="hostAuthModeChanged('basic')">
|
||||
Basic authentication
|
||||
</label></div>
|
||||
<div class="radio"><label>
|
||||
<input type="radio" id="auth_mode-sso" value="sso" onchange="hostAuthModeChanged('sso')">
|
||||
<input type="radio" name="auth_mode" id="auth_mode-sso" value="sso" onchange="hostAuthModeChanged('sso')">
|
||||
Single sign-on (SSO)
|
||||
</label></div>
|
||||
</div>
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
|
||||
loadSubjectSuggestions();
|
||||
|
||||
$.scope.Permission.__setTake(function($el, item, list){
|
||||
$.scope.Permission.__take = function($el, item, list){
|
||||
$el.addClass('bg-danger');
|
||||
$el.fadeOut(600, function(){ $el.remove(); });
|
||||
});
|
||||
};
|
||||
|
||||
// Live updates (model:Permission:*), so adds/removes reflect for everyone.
|
||||
app.subscribe(/^model:Permission:create/, function(data){
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
for(let user of data.results){
|
||||
$.scope.users.push(user);
|
||||
}
|
||||
$.scope.users.__setPut(function($el, item, list){
|
||||
$.scope.users.__put = function($el, item, list){
|
||||
$el.addClass('bg-success');
|
||||
$el.fadeIn(3000, function(){
|
||||
$el.removeClass('bg-success');
|
||||
});
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
$(document).ready(function(){
|
||||
populateUsers(); //populate the table
|
||||
|
||||
$.scope.users.__setTake(function($el, item, list){
|
||||
$.scope.users.__take = function($el, item, list){
|
||||
$el.addClass('bg-danger');
|
||||
$el.fadeOut(1000, function(){
|
||||
$el.remove()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user