v0.36.5
This commit is contained in:
1 parent
34490fd5fc
commit
6ef01212ec
21 files changed
+204
-77
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.36.4",
|
||||
"version": "0.36.5",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
|
||||
@@ -727,3 +727,56 @@ const rustProxyAutoId = (() => {
|
||||
return { slugify, unique, attach };
|
||||
})();
|
||||
window.rustProxyAutoId = rustProxyAutoId;
|
||||
|
||||
// Automatic certificate choice: for a new entry, pick the stored certificate that covers every host name (exact names beat wildcards); the user can still choose another one.
|
||||
const rustProxyAutoCertificate = (() => {
|
||||
const states = new WeakMap();
|
||||
const covers = (pattern, host) => {
|
||||
pattern = String(pattern).toLowerCase(); host = String(host).toLowerCase();
|
||||
if (pattern === host) return true;
|
||||
if (host.startsWith("*.") || !pattern.startsWith("*.")) return false;
|
||||
const suffix = pattern.slice(1);
|
||||
return host.length > suffix.length && host.endsWith(suffix) && !host.slice(0, -suffix.length).includes(".");
|
||||
};
|
||||
function suggest(hosts, certificates) {
|
||||
const names = [...new Set((hosts || []).map(host => String(host).trim().toLowerCase()).filter(Boolean))];
|
||||
if (!names.length) return "";
|
||||
let best = "", bestScore = -Infinity;
|
||||
for (const cert of certificates || []) {
|
||||
const coverage = certificateInventory.get(cert.id)?.covered_hosts?.length ? certificateInventory.get(cert.id).covered_hosts : (cert.covered_hosts || []);
|
||||
let score = 0, all = true;
|
||||
for (const host of names) {
|
||||
if (coverage.some(pattern => String(pattern).toLowerCase() === host)) score += 2;
|
||||
else if (coverage.some(pattern => covers(pattern, host))) score += 1;
|
||||
else { all = false; break; }
|
||||
}
|
||||
if (!all) continue;
|
||||
score -= coverage.length / 1000;
|
||||
if (score > bestScore) { best = cert.id; bestScore = score; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
// options: { hosts: () => host names, certificates: () => certificate objects, auto: boolean, anchor: element after which the hint is placed }
|
||||
function attach(select, options) {
|
||||
let state = states.get(select);
|
||||
if (!state) {
|
||||
state = { hint: document.createElement("small") };
|
||||
state.hint.className = "fine auto-cert-hint"; state.hint.hidden = true;
|
||||
(options.anchor || select).after(state.hint);
|
||||
select.addEventListener("change", event => { if (!event.isTrusted) return; const current = states.get(select); current.auto = false; current.hint.hidden = true; });
|
||||
states.set(select, state);
|
||||
}
|
||||
state.options = options; state.auto = options.auto !== false && !select.value; state.hint.hidden = true;
|
||||
const refresh = hosts => {
|
||||
const current = states.get(select);
|
||||
if (!current.auto) return;
|
||||
const list = hosts || current.options.hosts();
|
||||
const best = suggest(list, current.options.certificates());
|
||||
if (select.value !== best) { select.value = best; select.dispatchEvent(new Event("change", { bubbles: true })); }
|
||||
current.hint.hidden = !best; current.hint.textContent = best ? t("autoCertHint").replace("{id}", best) : "";
|
||||
};
|
||||
return { refresh };
|
||||
}
|
||||
return { attach, suggest, covers };
|
||||
})();
|
||||
window.rustProxyAutoCertificate = rustProxyAutoCertificate;
|
||||
@@ -1767,9 +1767,9 @@
|
||||
.vh-setting-section>h3 { letter-spacing: .01em; }
|
||||
.vh-tabs { flex-wrap: wrap; overflow: visible; row-gap: 0; }
|
||||
@media (max-width:700px) { .vh-tabs button { font-size: 11px; padding: 7px 8px; } }
|
||||
.auto-id-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; }
|
||||
.auto-id-hint, .auto-cert-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; }
|
||||
.auto-id-reset { justify-self: start; margin-top: 3px; padding: 2px 8px; font-size: 11px; }
|
||||
.auto-id-hint[hidden], .auto-id-reset[hidden] { display: none; }
|
||||
.auto-id-hint[hidden], .auto-cert-hint[hidden], .auto-id-reset[hidden] { display: none; }
|
||||
.auto-id-field>span:first-child { color: var(--muted); }
|
||||
.auto-id-field.is-compact>input { display: none; }
|
||||
.auto-id-row { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 2px; }
|
||||
@@ -1779,6 +1779,3 @@
|
||||
.auto-id-change { padding: 2px 8px; font-size: 11px; margin-left: auto; }
|
||||
.auto-id-row[hidden], .auto-id-change[hidden], .auto-id-badge[hidden] { display: none; }
|
||||
.auto-id-field input { font-family: var(--mono, monospace); font-size: 12px; }
|
||||
.auto-id-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; }
|
||||
.auto-id-reset { justify-self: start; margin-top: 3px; padding: 2px 8px; font-size: 11px; }
|
||||
.auto-id-hint[hidden], .auto-id-reset[hidden] { display: none; }
|
||||
@@ -3,7 +3,7 @@
|
||||
const form = $("notfound-form"), dialog = $("notfound-dialog"), list = $("notfound-list");
|
||||
if (!form || !dialog || !list) return;
|
||||
const sortColumns = ["id", "name", "hostname", "status", "mode", "listeners", "certificate"];
|
||||
let editing = null, revision = null, dirty = false, readBindings = () => [], readHosts = () => [], listPrefs = { sort: "id", descending: false };
|
||||
let notFoundAutoCertificate = null, editing = null, revision = null, dirty = false, readBindings = () => [], readHosts = () => [], listPrefs = { sort: "id", descending: false };
|
||||
function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.notfound_list_sort) ? preferences.notfound_list_sort : "id", descending: preferences?.notfound_list_descending === true }; }
|
||||
function syncSortDirection() { const control = $("notfound-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); }
|
||||
function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("notfound-sort")) $("notfound-sort").value = listPrefs.sort; syncSortDirection(); render(); }
|
||||
@@ -46,8 +46,9 @@
|
||||
$("notfound-id").value = item.id || ""; $("notfound-id").disabled = Boolean(editing); $("notfound-name").value = item.name || "";
|
||||
window.rustProxyAutoId.attach($("notfound-id"), { elements: [$("notfound-name")], taken: () => (envelope?.config.not_found_hosts || []).map(value => value.id) });
|
||||
$("notfound-enabled").checked = item.enabled !== false; syncEnabledControl(); $("notfound-http2").checked = item.http2_enabled !== false; $("notfound-http3").checked = item.http3_enabled !== false;
|
||||
const hostHolder = $("notfound-hosts-editor"); hostHolder.replaceChildren(); readHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "notfoundHosts", helpKey: "vhHostsHelp", requiredKey: "notfoundHostsRequired", onChange: changed });
|
||||
const hostHolder = $("notfound-hosts-editor"); hostHolder.replaceChildren(); readHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "notfoundHosts", helpKey: "vhHostsHelp", requiredKey: "notfoundHostsRequired", onChange: () => { changed(); notFoundAutoCertificate?.refresh(); } });
|
||||
listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id);
|
||||
notFoundAutoCertificate = window.rustProxyAutoCertificate.attach($("notfound-certificate"), { auto: !editing, hosts: () => { try { return readHosts(); } catch { return []; } }, certificates: () => envelope?.config.tls?.certificates || [] }); notFoundAutoCertificate.refresh();
|
||||
$("notfound-mode").value = item.page?.kind || "welcome"; populateStatuses(item.page?.status ?? 404); $("notfound-html").value = item.page?.html || ""; syncMode(); dialog.showModal();
|
||||
}
|
||||
function suspendDraft() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const form = $("redirect-form"), dialog = $("redirect-dialog"), list = $("redirect-list"), rules = $("redirect-rules");
|
||||
if (!form || !dialog || !list || !rules) return;
|
||||
const sortColumns = ["id", "name", "hostname", "status", "target", "rules", "listeners", "certificate"];
|
||||
let editing = null, revision = null, dirty = false, readRedirectBindings = () => [], readRedirectHosts = () => [], listPrefs = { sort: "id", descending: false };
|
||||
let redirectAutoCertificate = null, editing = null, revision = null, dirty = false, readRedirectBindings = () => [], readRedirectHosts = () => [], listPrefs = { sort: "id", descending: false };
|
||||
function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.redirect_list_sort) ? preferences.redirect_list_sort : "id", descending: preferences?.redirect_list_descending === true }; }
|
||||
function syncSortDirection() { const control = $("redirect-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); }
|
||||
function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("redirect-sort")) $("redirect-sort").value = listPrefs.sort; syncSortDirection(); render(); }
|
||||
@@ -71,9 +71,10 @@
|
||||
$("redirect-id").value = item.id || ""; $("redirect-id").disabled = Boolean(editing);
|
||||
$("redirect-name").value = item.name || "";
|
||||
window.rustProxyAutoId.attach($("redirect-id"), { elements: [$("redirect-name")], taken: () => (envelope?.config.redirects || []).map(value => value.id) });
|
||||
const hostHolder = $("redirect-hosts-editor"); hostHolder.replaceChildren(); readRedirectHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "redirectHosts", helpKey: "vhHostsHelp", requiredKey: "redirectHostsRequired", onChange: changed });
|
||||
const hostHolder = $("redirect-hosts-editor"); hostHolder.replaceChildren(); readRedirectHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "redirectHosts", helpKey: "vhHostsHelp", requiredKey: "redirectHostsRequired", onChange: () => { changed(); redirectAutoCertificate?.refresh(); } });
|
||||
$("redirect-enabled").checked = item.enabled !== false; syncEnabledControl(); $("redirect-http2").checked = item.http2_enabled !== false; $("redirect-http3").checked = item.http3_enabled !== false;
|
||||
listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id);
|
||||
redirectAutoCertificate = window.rustProxyAutoCertificate.attach($("redirect-certificate"), { auto: !editing, hosts: () => { try { return readRedirectHosts(); } catch { return []; } }, certificates: () => envelope?.config.tls?.certificates || [] }); redirectAutoCertificate.refresh();
|
||||
rules.replaceChildren(); for (const [i, rule] of (item.rules?.length ? item.rules : defaults().rules).entries()) addRule(rule, i);
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
|
||||
let g = group("vhEditor"); const basicFields = subsection(g, "vhGeneralDetailsSection");
|
||||
const vhostIdInput = field(basicFields, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(basicFields, "vhIdRenameHelp");
|
||||
let refreshCanonical = () => { }, vhostAutoId = null;
|
||||
let refreshCanonical = () => { }, vhostAutoId = null, vhostAutoCertificate = null;
|
||||
vhostAutoId = window.rustProxyAutoId.attach(vhostIdInput, { text: () => lines(read("hosts")?.value || "")[0] || "", taken: () => (envelope?.config.vhosts || []).map(item => item.id), resettable: !editing });
|
||||
const enabledControl = field(basicFields, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
|
||||
window.rustProxyEnableControl(enabledControl, "vhEnableHint");
|
||||
@@ -89,7 +89,7 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
const websocketLegacy = field(websocketFields, "websocket-legacy-support", "vhWebsocketLegacySupport", value.websocket_legacy_support === true, "checkbox");
|
||||
note(websocketFields, "vhWebsocketLegacySupportHelp");
|
||||
g = group("vhFrontends");
|
||||
readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => { refreshCanonical(hosts); vhostAutoId?.refresh(); });
|
||||
readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => { refreshCanonical(hosts); vhostAutoId?.refresh(); vhostAutoCertificate?.refresh(hosts); });
|
||||
if (!editing) g.lastElementChild?.after(vhostIdInput.closest("label"));
|
||||
const bindingControl = window.rustProxyIntegrations.bindings(g, value.frontend?.listener_ids || [], !editing); readBindings = bindingControl.read;
|
||||
note(g, "listenerDefaultPortsHint");
|
||||
@@ -350,6 +350,7 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
const certificateRow = el("div", undefined, "vh-wide vh-certificate-select-row"); g.append(certificateRow);
|
||||
const selector = field(certificateRow, "certificate-id", "vhCertificate", value.certificate_id || "", "select");
|
||||
const fillCertificates = () => { const selected = selector.value || value.certificate_id || ""; const inherit = text("option", "vhCertificateDefault"); inherit.value = ""; selector.replaceChildren(inherit, ...[...(envelope?.config.tls?.certificates || []), ...stagedCertificates].map(c => { const o = el("option", c.id); o.value = c.id; return o; })); selector.value = selected; }; fillCertificates();
|
||||
vhostAutoCertificate = window.rustProxyAutoCertificate.attach(selector, { auto: !editing, anchor: certificateRow, hosts: () => { try { return readHosts(); } catch { return []; } }, certificates: () => [...(envelope?.config.tls?.certificates || []), ...stagedCertificates] }); vhostAutoCertificate.refresh();
|
||||
certificateRow.append(button("certManage", async () => { if (!(await closeEditor())) return; setPanel("tls"); }));
|
||||
note(g, "certSelectHint");
|
||||
const mismatch = field(g, "allow-certificate-mismatch", "vhCertificateMismatchForce", value.allow_certificate_mismatch === true, "checkbox");
|
||||
|
||||
+23
-10
@@ -150,28 +150,35 @@ pub fn compile_reusing(
|
||||
type LocalHostBuild = (
|
||||
HashMap<String, Vec<Arc<crate::redirect::CompiledHost>>>,
|
||||
HashMap<String, Vec<Arc<crate::not_found::CompiledHost>>>,
|
||||
HashMap<String, Vec<crate::vhost::Listener>>,
|
||||
HashMap<String, Vec<(String, crate::vhost::Listener)>>,
|
||||
);
|
||||
|
||||
fn compile_local_hosts(config: &Config) -> Result<LocalHostBuild> {
|
||||
let mut redirects: HashMap<String, Vec<Arc<crate::redirect::CompiledHost>>> = HashMap::new();
|
||||
let mut not_found_hosts: HashMap<String, Vec<Arc<crate::not_found::CompiledHost>>> =
|
||||
HashMap::new();
|
||||
let mut claimed_hosts: HashMap<String, Vec<crate::vhost::Listener>> = HashMap::new();
|
||||
let mut claimed_hosts: HashMap<String, Vec<(String, crate::vhost::Listener)>> =
|
||||
HashMap::new();
|
||||
for redirect in &config.redirects {
|
||||
let bindings = redirect.frontend.resolved(&config.listeners)?;
|
||||
for name in &redirect.frontend.hosts {
|
||||
let claimed = claimed_hosts.entry(name.clone()).or_default();
|
||||
for a in claimed.iter() {
|
||||
for (owner, a) in claimed.iter() {
|
||||
for b in &bindings {
|
||||
ensure!(
|
||||
!crate::vhost::socket_overlap(a.address, b.address),
|
||||
"overlapping frontend bindings for hostname {}",
|
||||
name
|
||||
"overlapping frontend bindings for hostname {} (already used by {} on {})",
|
||||
name,
|
||||
owner,
|
||||
a.address
|
||||
);
|
||||
}
|
||||
}
|
||||
claimed.extend(bindings.iter().copied());
|
||||
claimed.extend(
|
||||
bindings
|
||||
.iter()
|
||||
.map(|binding| (format!("redirect {}", redirect.id), *binding)),
|
||||
);
|
||||
}
|
||||
if redirect.enabled {
|
||||
let compiled = Arc::new(crate::redirect::compile(redirect, &config.listeners)?);
|
||||
@@ -187,16 +194,22 @@ fn compile_local_hosts(config: &Config) -> Result<LocalHostBuild> {
|
||||
let bindings = host.frontend.resolved(&config.listeners)?;
|
||||
for name in &host.frontend.hosts {
|
||||
let claimed = claimed_hosts.entry(name.clone()).or_default();
|
||||
for a in claimed.iter() {
|
||||
for (owner, a) in claimed.iter() {
|
||||
for b in &bindings {
|
||||
ensure!(
|
||||
!crate::vhost::socket_overlap(a.address, b.address),
|
||||
"overlapping frontend bindings for hostname {}",
|
||||
name
|
||||
"overlapping frontend bindings for hostname {} (already used by {} on {})",
|
||||
name,
|
||||
owner,
|
||||
a.address
|
||||
);
|
||||
}
|
||||
}
|
||||
claimed.extend(bindings.iter().copied());
|
||||
claimed.extend(
|
||||
bindings
|
||||
.iter()
|
||||
.map(|binding| (format!("404 host {}", host.id), *binding)),
|
||||
);
|
||||
}
|
||||
if host.enabled {
|
||||
let compiled = Arc::new(crate::not_found::compile(host, &config.listeners)?);
|
||||
|
||||
+21
-8
@@ -65,7 +65,8 @@ pub fn compile_with_active(
|
||||
php_profiles
|
||||
.insert(profile.id.clone(), profile.php.clone())
|
||||
.is_none(),
|
||||
"duplicate PHP-FPM profile id"
|
||||
"duplicate PHP-FPM profile id {:?}: every profile needs a unique ID",
|
||||
profile.id
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
@@ -120,7 +121,8 @@ pub fn compile_with_active(
|
||||
);
|
||||
ensure!(
|
||||
web_services.insert(service.id.clone(), compiled).is_none(),
|
||||
"duplicate Web Service id"
|
||||
"duplicate Web Service id {:?}: every Web Service needs a unique ID",
|
||||
service.id
|
||||
);
|
||||
}
|
||||
ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)");
|
||||
@@ -145,7 +147,11 @@ pub fn compile_with_active(
|
||||
validate_source(&route.source)?;
|
||||
}
|
||||
}
|
||||
ensure!(vhost_ids.insert(host.id.clone()), "duplicate vhost id");
|
||||
ensure!(
|
||||
vhost_ids.insert(host.id.clone()),
|
||||
"duplicate vhost id {:?}: every vhost needs a unique ID",
|
||||
host.id
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
config.redirects.len() <= 64,
|
||||
@@ -158,7 +164,8 @@ pub fn compile_with_active(
|
||||
.with_context(|| format!("redirect {}", redirect.id))?;
|
||||
ensure!(
|
||||
redirect_ids.insert(redirect.id.clone()),
|
||||
"duplicate redirect id"
|
||||
"duplicate redirect id {:?}: every redirect needs a unique ID",
|
||||
redirect.id
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
@@ -171,7 +178,8 @@ pub fn compile_with_active(
|
||||
.with_context(|| format!("404 host {}", host.id))?;
|
||||
ensure!(
|
||||
not_found_ids.insert(host.id.clone()),
|
||||
"duplicate 404 host id"
|
||||
"duplicate 404 host id {:?}: every 404 host needs a unique ID",
|
||||
host.id
|
||||
);
|
||||
}
|
||||
let effective_tls = crate::tls::for_all_frontends(
|
||||
@@ -279,7 +287,11 @@ pub fn compile_with_active(
|
||||
let mut acl_profiles: HashMap<String, (Option<CompiledAcl>, crate::rate_limit::CountryAccess)> =
|
||||
HashMap::new();
|
||||
for profile in &mut config.acl_profiles {
|
||||
ensure!(valid_id(&profile.id), "invalid ACL profile id");
|
||||
ensure!(
|
||||
valid_id(&profile.id),
|
||||
"invalid ACL profile id {:?}: use 1-64 letters, digits, '-' or '_'",
|
||||
profile.id
|
||||
);
|
||||
ensure!(
|
||||
!profile.name.trim().is_empty()
|
||||
&& profile.name.len() <= 96
|
||||
@@ -293,7 +305,8 @@ pub fn compile_with_active(
|
||||
acl_profiles
|
||||
.insert(profile.id.clone(), (acl, profile.country_access.clone()))
|
||||
.is_none(),
|
||||
"duplicate ACL profile id"
|
||||
"duplicate ACL profile id {:?}: every ACL profile needs a unique ID",
|
||||
profile.id
|
||||
);
|
||||
}
|
||||
crate::acl::dynamic::configured_hostnames(&config)?;
|
||||
@@ -380,7 +393,7 @@ fn normalize_prefix(prefix: &mut String) -> Result<()> {
|
||||
&& !prefix.contains('?')
|
||||
&& !prefix.contains('#')
|
||||
&& !prefix.bytes().any(|b| b <= 32 || b == 127),
|
||||
"invalid path prefix"
|
||||
"invalid path prefix: it must start with '/', be at most 512 bytes and contain no '?', '#', spaces or control characters"
|
||||
);
|
||||
if prefix.len() > 1 {
|
||||
*prefix = prefix.trim_end_matches('/').to_owned();
|
||||
|
||||
+25
-10
@@ -79,16 +79,22 @@ fn compile_routing(
|
||||
);
|
||||
for name in &routing_hosts {
|
||||
let claimed = claimed_hosts.entry(name.clone()).or_default();
|
||||
for a in claimed.iter() {
|
||||
for (owner, a) in claimed.iter() {
|
||||
for b in &host_bindings {
|
||||
ensure!(
|
||||
!crate::vhost::socket_overlap(a.address, b.address),
|
||||
"overlapping frontend bindings for hostname {}",
|
||||
name
|
||||
"overlapping frontend bindings for hostname {} (already used by {} on {})",
|
||||
name,
|
||||
owner,
|
||||
a.address
|
||||
);
|
||||
}
|
||||
}
|
||||
claimed.extend(host_bindings.iter().copied());
|
||||
claimed.extend(
|
||||
host_bindings
|
||||
.iter()
|
||||
.map(|binding| (format!("vhost {}", host.id), *binding)),
|
||||
);
|
||||
}
|
||||
let needs_proxy_pool = host.source.is_reverse_proxy()
|
||||
|| host.routes.iter().any(|route| {
|
||||
@@ -323,7 +329,8 @@ fn compile_routing(
|
||||
for route in &mut host.routes {
|
||||
ensure!(
|
||||
valid_id(&route.id) && path_ids.insert(route.id.clone()),
|
||||
"invalid/duplicate vhost path id"
|
||||
"location ID {:?} is invalid or used twice in this vhost (use 1-64 letters, digits, '-' or '_', unique per vhost)",
|
||||
route.id
|
||||
);
|
||||
let path_regex = match route.match_type {
|
||||
crate::vhost::PathMatch::Regex | crate::vhost::PathMatch::RegexInsensitive => {
|
||||
@@ -331,7 +338,8 @@ fn compile_routing(
|
||||
!route.path_prefix.is_empty()
|
||||
&& route.path_prefix.len() <= 1024
|
||||
&& !route.path_prefix.chars().any(char::is_control),
|
||||
"invalid vhost location regex"
|
||||
"location {}: the regular expression must be 1-1024 characters without control characters",
|
||||
route.id
|
||||
);
|
||||
let mut builder = regex::RegexBuilder::new(&route.path_prefix);
|
||||
builder
|
||||
@@ -340,17 +348,23 @@ fn compile_routing(
|
||||
)
|
||||
.size_limit(2 * 1024 * 1024)
|
||||
.dfa_size_limit(2 * 1024 * 1024);
|
||||
Some(builder.build().context("invalid vhost location regex")?)
|
||||
Some(builder.build().with_context(|| {
|
||||
format!(
|
||||
"location {}: invalid or too complex regular expression",
|
||||
route.id
|
||||
)
|
||||
})?)
|
||||
}
|
||||
crate::vhost::PathMatch::Exact => {
|
||||
ensure!(
|
||||
!route.path_prefix.contains('?')
|
||||
&& !route.path_prefix.contains('#')
|
||||
&& !route.path_prefix.bytes().any(|b| b <= 32 || b == 127),
|
||||
"invalid exact location path"
|
||||
"location {}: an exact path must not contain '?', '#', spaces or control characters",
|
||||
route.id
|
||||
);
|
||||
route.path_prefix = crate::acl::routing_path(&route.path_prefix)
|
||||
.context("invalid exact location path")?;
|
||||
.with_context(|| format!("location {}: invalid exact path", route.id))?;
|
||||
None
|
||||
}
|
||||
crate::vhost::PathMatch::Prefix | crate::vhost::PathMatch::PrefixPriority => {
|
||||
@@ -361,7 +375,8 @@ fn compile_routing(
|
||||
if !route.internal {
|
||||
ensure!(
|
||||
path_bindings.insert((route.match_type, route.path_prefix.clone())),
|
||||
"duplicate vhost location matcher"
|
||||
"location {}: another location already uses the same path and match type",
|
||||
route.id
|
||||
);
|
||||
}
|
||||
validate_route_rewrite(route, path_regex.as_ref())?;
|
||||
|
||||
+10
-2
@@ -37,7 +37,11 @@ fn default_page() -> crate::pages::Page {
|
||||
|
||||
impl Host {
|
||||
pub fn validate(&mut self) -> Result<()> {
|
||||
ensure!(crate::config::valid_id(&self.id), "invalid 404 host id");
|
||||
ensure!(
|
||||
crate::config::valid_id(&self.id),
|
||||
"invalid 404 host id {:?}: use 1-64 letters, digits, '-' or '_'",
|
||||
self.id
|
||||
);
|
||||
if self.name.trim().is_empty() {
|
||||
self.name = self.id.clone();
|
||||
}
|
||||
@@ -55,7 +59,11 @@ impl Host {
|
||||
let mut hosts = std::collections::HashSet::new();
|
||||
for host in &mut self.frontend.hosts {
|
||||
*host = crate::config::normalize_host_pattern(host)?;
|
||||
ensure!(hosts.insert(host.clone()), "duplicate 404 hostname");
|
||||
ensure!(
|
||||
hosts.insert(host.clone()),
|
||||
"hostname {} is listed twice in this 404 host",
|
||||
host
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
!self.frontend.listener_ids.is_empty() && self.frontend.listener_ids.len() <= 16,
|
||||
|
||||
+12
-4
@@ -127,7 +127,11 @@ pub struct Host {
|
||||
}
|
||||
impl Host {
|
||||
pub fn validate(&mut self) -> Result<()> {
|
||||
ensure!(crate::config::valid_id(&self.id), "invalid redirect id");
|
||||
ensure!(
|
||||
crate::config::valid_id(&self.id),
|
||||
"invalid redirect id {:?}: use 1-64 letters, digits, '-' or '_'",
|
||||
self.id
|
||||
);
|
||||
if self.name.is_empty() {
|
||||
self.name = self.id.clone();
|
||||
}
|
||||
@@ -140,16 +144,20 @@ impl Host {
|
||||
);
|
||||
ensure!(
|
||||
!self.frontend.hosts.is_empty(),
|
||||
"redirect requires at least one hostname"
|
||||
"redirect requires at least one hostname (for example old.example.com)"
|
||||
);
|
||||
let mut hosts = std::collections::HashSet::new();
|
||||
for host in &mut self.frontend.hosts {
|
||||
*host = crate::config::normalize_host_pattern(host)?;
|
||||
ensure!(hosts.insert(host.clone()), "duplicate redirect hostname");
|
||||
ensure!(
|
||||
hosts.insert(host.clone()),
|
||||
"hostname {} is listed twice in this redirect",
|
||||
host
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
!self.frontend.listener_ids.is_empty() && self.frontend.listener_ids.len() <= 16,
|
||||
"redirect requires 1..16 named listeners"
|
||||
"select between 1 and 16 listeners for this redirect"
|
||||
);
|
||||
if self.rules.is_empty() {
|
||||
self.rules.push(Rule::default());
|
||||
|
||||
+14
-5
@@ -46,7 +46,11 @@ impl PathRoute {
|
||||
}
|
||||
impl Vhost {
|
||||
pub fn validate(&mut self) -> Result<()> {
|
||||
ensure!(crate::config::valid_id(&self.id), "invalid vhost id");
|
||||
ensure!(
|
||||
crate::config::valid_id(&self.id),
|
||||
"invalid vhost id {:?}: use 1-64 letters, digits, '-' or '_'",
|
||||
self.id
|
||||
);
|
||||
ensure!(
|
||||
!self.websocket_legacy_support || self.websockets,
|
||||
"legacy WebSocket compatibility requires WebSocket support to be enabled"
|
||||
@@ -69,12 +73,16 @@ impl Vhost {
|
||||
);
|
||||
ensure!(
|
||||
!self.frontend.hosts.is_empty(),
|
||||
"vhost requires at least one hostname"
|
||||
"vhost requires at least one hostname (for example app.example.com) in the Frontend section"
|
||||
);
|
||||
let mut hosts = std::collections::HashSet::new();
|
||||
for host in &mut self.frontend.hosts {
|
||||
*host = crate::config::normalize_host_pattern(host)?;
|
||||
ensure!(hosts.insert(host.clone()), "duplicate vhost hostname");
|
||||
ensure!(
|
||||
hosts.insert(host.clone()),
|
||||
"hostname {} is listed twice in this vhost",
|
||||
host
|
||||
);
|
||||
}
|
||||
if let Some(target) = &mut self.www_redirect_target {
|
||||
*target = crate::config::normalize_host_pattern(target)?;
|
||||
@@ -101,7 +109,7 @@ impl Vhost {
|
||||
self.content_type_policy.validate()?;
|
||||
ensure!(
|
||||
!self.frontend.listener_ids.is_empty(),
|
||||
"vhost requires at least one named listener"
|
||||
"select at least one listener for this vhost in the Frontend section"
|
||||
);
|
||||
ensure!(
|
||||
self.frontend.listener_ids.len() <= 16,
|
||||
@@ -150,7 +158,8 @@ impl Vhost {
|
||||
for route in &mut self.routes {
|
||||
ensure!(
|
||||
crate::config::valid_id(&route.id) && route_ids.insert(route.id.clone()),
|
||||
"invalid/duplicate vhost path id"
|
||||
"location ID {:?} is invalid or used twice in this vhost (use 1-64 letters, digits, '-' or '_', unique per vhost)",
|
||||
route.id
|
||||
);
|
||||
route.source.validate()?;
|
||||
ensure!(
|
||||
|
||||
Reference in new issue
Block a user