v0.35.1: performance, AGPL licensing and documentation overhaul
- Web Server reads static files inline; proxy middleware chain flattened - About window with license summary and project links; AGPL-3.0-or-later - Rewritten documentation: feature list, screenshots, performance guide - Fix false spikes in per-Vhost request-rate charts
This commit is contained in:
1 parent
d3b492874b
commit
eba9f89283
135 files changed
+1425
-1064
No files matched your search
@@ -35,17 +35,20 @@ async fn execute(
|
||||
.map_err(|_| "acme.validation_worker_failed".to_owned())?
|
||||
}
|
||||
|
||||
pub async fn challenge(
|
||||
axum::extract::State(state): axum::extract::State<Arc<App>>,
|
||||
request: axum::extract::Request,
|
||||
next: axum::middleware::Next,
|
||||
) -> axum::response::Response {
|
||||
const CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
|
||||
|
||||
pub fn is_challenge_path(path: &str) -> bool {
|
||||
path.starts_with(CHALLENGE_PREFIX)
|
||||
}
|
||||
|
||||
pub async fn challenge(state: Arc<App>, request: axum::extract::Request) -> axum::response::Response {
|
||||
use axum::{body::Body, response::IntoResponse};
|
||||
use tokio::io::AsyncReadExt;
|
||||
const PREFIX: &str = "/.well-known/acme-challenge/";
|
||||
let Some(token) = request.uri().path().strip_prefix(PREFIX) else {
|
||||
return next.run(request).await;
|
||||
};
|
||||
let token = request
|
||||
.uri()
|
||||
.path()
|
||||
.strip_prefix(CHALLENGE_PREFIX)
|
||||
.unwrap_or_default();
|
||||
let denied = || {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
|
||||
@@ -11,10 +11,11 @@ macro_rules! reader_doc {
|
||||
pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("README.md", "project"),
|
||||
reader_doc!("CHANGELOG.md", "project"),
|
||||
reader_doc!("ROADMAP.md", "project"),
|
||||
reader_doc!("LICENSE", "project"),
|
||||
reader_doc!("docs/README.md", "guides"),
|
||||
reader_doc!("docs/FEATURES.md", "guides"),
|
||||
reader_doc!("docs/SCREENSHOTS.md", "guides"),
|
||||
reader_doc!("docs/INSTALL.md", "guides"),
|
||||
reader_doc!("docs/CONTAINER.md", "guides"),
|
||||
reader_doc!("docs/CONFIGURATION.md", "guides"),
|
||||
reader_doc!("docs/VHOSTS.md", "guides"),
|
||||
reader_doc!("docs/CUSTOM_LOCATIONS.md", "guides"),
|
||||
@@ -24,11 +25,9 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("docs/CACHE.md", "guides"),
|
||||
reader_doc!("docs/RESPONSE_DELIVERY.md", "guides"),
|
||||
reader_doc!("docs/TRANSPORT.md", "guides"),
|
||||
reader_doc!("docs/TLS_POST_QUANTUM.md", "guides"),
|
||||
reader_doc!("docs/STREAMS.md", "guides"),
|
||||
reader_doc!("docs/ACME_PROVIDERS.md", "guides"),
|
||||
reader_doc!("docs/CERTIFICATES.md", "guides"),
|
||||
reader_doc!("docs/DOCKER_DISCOVERY.md", "guides"),
|
||||
reader_doc!("docs/INTEGRATIONS.md", "guides"),
|
||||
reader_doc!("docs/DASHBOARD.md", "guides"),
|
||||
reader_doc!("docs/MONITORING.md", "guides"),
|
||||
reader_doc!("docs/LOGS.md", "guides"),
|
||||
@@ -36,6 +35,6 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("docs/MIGRATION.md", "guides"),
|
||||
reader_doc!("docs/SECURITY.md", "guides"),
|
||||
reader_doc!("docs/API.md", "guides"),
|
||||
reader_doc!("docs/API_ACCESS.md", "guides"),
|
||||
reader_doc!("docs/PERFORMANCE.md", "guides"),
|
||||
reader_doc!("docs/TROUBLESHOOTING.md", "guides"),
|
||||
];
|
||||
@@ -12,7 +12,7 @@ mod tests {
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/PHP_FPM.md"));
|
||||
assert!(documents
|
||||
.iter()
|
||||
.any(|doc| doc["path"] == "docs/TLS_POST_QUANTUM.md"));
|
||||
.any(|doc| doc["path"] == "docs/FEATURES.md"));
|
||||
assert!(documents.iter().all(|doc| {
|
||||
doc["path"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["title"].as_str().is_some_and(|value| !value.is_empty())
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.35.0",
|
||||
"version": "0.35.1",
|
||||
"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": [
|
||||
@@ -9521,7 +9521,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Maximum body 16 KiB. Staging by default. Production requires explicit consent. Provider fields depend on the selected provider; see docs/ACME_PROVIDERS.md. Poll GET /api/acme.",
|
||||
"description": "Maximum body 16 KiB. Staging by default. Production requires explicit consent. Provider fields depend on the selected provider; see docs/CERTIFICATES.md. Poll GET /api/acme.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Origin",
|
||||
|
||||
+1
-2
@@ -1043,8 +1043,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// GET returns the same standalone section shape accepted by PUT, at the
|
||||
// matching revision and without unrequested configuration fields.
|
||||
// GET returns the same section shape that PUT accepts.
|
||||
let read = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
|
||||
@@ -21,8 +21,7 @@ function syncConfigHistoryRestoreButton() {
|
||||
button.hidden = configHistorySelectedRevision === null;
|
||||
button.disabled = busy || !envelope || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision);
|
||||
}
|
||||
// Compare leaf fields, not serialized object/array blobs. Object IDs are used
|
||||
// for stable array paths so reordering vhosts does not become a huge fake diff.
|
||||
// Leaf-level diff keyed by object IDs, so reordering is not a change.
|
||||
function configDiff(before, after, path = "$", output = [], depth = 0) {
|
||||
if (output.length >= 200 || Object.is(before, after)) return output;
|
||||
if (depth >= 24) {
|
||||
|
||||
@@ -144,9 +144,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
widen.addEventListener("click", () => resize(id, 1));
|
||||
const widthValue = el("span", `${width}/12`, "overview-layout-width-value");
|
||||
widthValue.title = t("overviewLayoutWidth");
|
||||
// The visual editor intentionally keeps uniformly sized previews.
|
||||
// Group sizing separately from ordering/hiding so both sets wrap
|
||||
// inside a narrow dialog rather than spilling into adjacent tiles.
|
||||
// Size and order controls are grouped separately so they wrap in narrow dialogs.
|
||||
const sizeActions = el("span", undefined, "overview-layout-size-actions");
|
||||
sizeActions.append(narrow, widthValue, widen);
|
||||
const orderActions = el("span", undefined, "overview-layout-order-actions");
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// One continuity policy for all browser-side counter-derived rates.
|
||||
// Background tabs cannot supply a valid instantaneous rate for an interval
|
||||
// spanning the pause. Keep the plotted history and rebase on the next sample.
|
||||
// Shared rate sampling: rebase after background-tab pauses instead of plotting a false spike.
|
||||
const rustProxyRateSampling = (() => {
|
||||
let generation = 0;
|
||||
const pause = () => { generation += 1; };
|
||||
@@ -19,6 +17,10 @@ const rustProxyRateSampling = (() => {
|
||||
at - previous.at <= Math.max(10000, seconds * 3000));
|
||||
}
|
||||
function difference(previous, values, at, intervalSeconds = 3) {
|
||||
if (previous && previous.generation === generation && Number.isFinite(at) &&
|
||||
Number.isFinite(previous.at) && at >= previous.at && at - previous.at < 250) {
|
||||
return { next: previous, deltas: null, seconds: null, rapid: true };
|
||||
}
|
||||
const next = { ...stamp(at), values: [...values] };
|
||||
if (!continuous(previous, at, intervalSeconds) ||
|
||||
values.some((value, i) => !Number.isFinite(value) || value < 0 ||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Theme palettes and appearance tokens. Runtime preference variables remain writable by JavaScript.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Theme palettes and appearance tokens */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Shared reset, typography, controls, forms, tables, cards and overlay primitives.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Shared reset, typography, controls, forms, tables, cards and overlay primitives */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -1304,8 +1302,9 @@ body[data-density=compact] .page-footer {
|
||||
}
|
||||
|
||||
.about-dialog {
|
||||
width: calc(min(560px, 100vw - 24px));
|
||||
overflow: hidden;
|
||||
width: calc(min(640px, 100vw - 24px));
|
||||
max-height: calc(100vh - 24px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.about-dialog .window-heading button {
|
||||
@@ -1370,6 +1369,38 @@ body[data-density=compact] .page-footer {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.about-license {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.about-license h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.about-license p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.about-license ul {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding-inline-start: 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.about-links {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.about-repository {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
@@ -1381,7 +1412,9 @@ body[data-density=compact] .page-footer {
|
||||
.about-repository span:last-child {
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
html[data-table-density=compact] :is(table th, table td) {
|
||||
@@ -1479,7 +1512,6 @@ html[data-table-density=ultra] :is(table th, table td) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Keep generated ACL instructions in the original metrics-example code style. */
|
||||
.path-permissions {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -1506,7 +1538,6 @@ html[data-table-density=ultra] :is(table th, table td) {
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
/* Permission guidance belongs to its generated command block, not the path inputs. */
|
||||
#certificate-external-acl-example.path-permissions {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Application startup, authentication, navigation, page chrome and draft dock.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Application startup, authentication, navigation, page chrome and draft dock */
|
||||
|
||||
html.app-loading .shell {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Overview metrics, charts, dashboard density, inline layout controls and presets.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Overview metrics, charts, dashboard density, inline layout controls and presets */
|
||||
|
||||
html[data-theme=ultra_dark] :is(.card, .stat, dialog) {
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
|
||||
@@ -76,8 +74,7 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
stroke-dasharray: var(--chart-grid-dash, 2 5);
|
||||
}
|
||||
|
||||
/* Optional X/Y scale readouts do not change SVG coordinates or chart ranges.
|
||||
* Labels are HTML, not distorted SVG text under preserveAspectRatio="none". */
|
||||
/* Axis labels are HTML overlays, not stretched SVG text. */
|
||||
.rp-chart-axis-host {
|
||||
position: relative;
|
||||
display: flow-root;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Shared service editors: Vhosts, backends, Web Server, Streams, routing and ACL pickers.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Shared service editors: Vhosts, backends, Web Server, Streams, routing and ACL pickers */
|
||||
|
||||
.host-chip-editor.field-invalid,
|
||||
.listener-choices.field-invalid {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Settings, certificates, integrations and Docker agent/service management.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Settings, certificates, integrations and Docker agent/service management */
|
||||
|
||||
.settings-tabs button.has-unsaved:after {
|
||||
content: "";
|
||||
@@ -1205,7 +1203,7 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
/* 0.34.13: global security policy forms, grouped with clear control boundaries. */
|
||||
/* Global security policy forms */
|
||||
.global-policy-form .global-policy-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Traffic, cache, transformations, live/stored logs and configuration history.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Traffic, cache, transformations, live/stored logs and configuration history */
|
||||
|
||||
.certificate-card .pill,
|
||||
.cache-miss-key,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Responsive rules. Each viewport, container and reduced-motion condition is declared once.
|
||||
* Historical overrides were resolved before consolidation; preserve the documented order.
|
||||
*/
|
||||
/* Responsive rules */
|
||||
|
||||
@media (min-width:1201px) {
|
||||
#panel-overview .stats-grid {
|
||||
@@ -1052,7 +1050,8 @@
|
||||
}
|
||||
|
||||
.named-listener-row .vh-grid,
|
||||
.about-meta {
|
||||
.about-meta,
|
||||
.about-links {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -1388,7 +1387,6 @@
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
/* The standalone auth heading keeps its later, unconditional font size. */
|
||||
.login-section h1:where(:not(.auth-main h1)) {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
@@ -91,9 +91,3 @@ When replacing a previously built checkout with a source ZIP, include the root
|
||||
the [generated-CSS recovery steps](../../../../scripts/README.md#missing-generated-css-during-rust-compilation).
|
||||
Source preflight does not generate Cargo outputs. The build tracks the generated
|
||||
bundle as well as its inputs, and rejects a missing or empty output.
|
||||
|
||||
The optional [dashboard browser regression runner](../../../../tools/dev/README-dashboard-css.md)
|
||||
assembles the actual HTML and JavaScript against synthetic API fixtures and
|
||||
compares a supplied baseline stylesheet with the built candidate. It includes
|
||||
breakpoint/container boundaries and dynamic editor states. Its dependencies are
|
||||
development-only; they are not part of the application build or deployment.
|
||||
+34
-3
@@ -1,4 +1,4 @@
|
||||
/* A deliberately self-contained documentation layout. */
|
||||
/* Documentation Reader layout. */
|
||||
* {
|
||||
box-sizing: border-box
|
||||
}
|
||||
@@ -325,6 +325,38 @@ main {
|
||||
color: #52606d
|
||||
}
|
||||
|
||||
.reader-table {
|
||||
overflow-x: auto;
|
||||
margin: 14px 0
|
||||
}
|
||||
|
||||
.reader-table table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
.reader-table :is(th, td) {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #e0e5ea;
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.reader-table th {
|
||||
background: #f4f6f8
|
||||
}
|
||||
|
||||
.reader-image {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px dashed #c9d1d9;
|
||||
color: #52606d;
|
||||
font-size: 13px
|
||||
}
|
||||
|
||||
.reader-empty {
|
||||
color: #687581;
|
||||
padding: 10px 8px
|
||||
@@ -344,8 +376,7 @@ main {
|
||||
padding: 18px
|
||||
}
|
||||
}
|
||||
/* Authenticated documentation uses the same stored corner preference as the
|
||||
main panel, while retaining its original flat shape for "Original". */
|
||||
/* Corner style follows the panel preference. */
|
||||
html[data-corner-style=square] {
|
||||
--rp-docs-radius-surface: 0px;
|
||||
--rp-docs-radius-control: 0px;
|
||||
|
||||
+23
-2
@@ -10,6 +10,7 @@
|
||||
let documents = [], docsByPath = new Map(), currentDocument = null, activeTab = "reader";
|
||||
let channel = null; try { channel = new BroadcastChannel("rustproxy-admin-session"); } catch { }
|
||||
const t = key => translations[key] ?? fallback[key] ?? key;
|
||||
const REPOSITORY_RAW = "https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master";
|
||||
|
||||
function docsConfirm(message) {
|
||||
return new Promise(resolve => {
|
||||
@@ -78,12 +79,22 @@
|
||||
}
|
||||
|
||||
function appendInline(parent, text, basePath) {
|
||||
const pattern = /(`[^`]+`|\[[^\]]+\]\([^)]+\))/g; let offset = 0;
|
||||
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|!?\[[^\]]*\]\([^)]+\))/g; let offset = 0;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
if (match.index > offset) parent.append(document.createTextNode(text.slice(offset, match.index)));
|
||||
const token = match[0];
|
||||
if (token.startsWith("`")) {
|
||||
const code = document.createElement("code"); code.textContent = token.slice(1, -1); parent.append(code);
|
||||
} else if (token.startsWith("**")) {
|
||||
const strong = document.createElement("strong"); appendInline(strong, token.slice(2, -2), basePath); parent.append(strong);
|
||||
} else if (token.startsWith("!")) {
|
||||
const parsed = /^!\[([^\]]*)\]\(([^)]+)\)$/.exec(token);
|
||||
const caption = document.createElement("a"); caption.className = "reader-image";
|
||||
caption.textContent = `${t("docsImage")}: ${parsed[1] || parsed[2]}`;
|
||||
const target = parsed[2].trim();
|
||||
caption.href = /^https?:\/\//i.test(target) ? target : `${REPOSITORY_RAW}/${normalizeDocPath(basePath, target)}`;
|
||||
caption.target = "_blank"; caption.rel = "noreferrer";
|
||||
parent.append(caption);
|
||||
} else {
|
||||
const parsed = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token); const anchor = document.createElement("a");
|
||||
anchor.textContent = parsed[1]; const target = parsed[2].trim();
|
||||
@@ -106,7 +117,7 @@
|
||||
function renderMarkdown(documentInfo) {
|
||||
const target = element("docs-reader-content"); target.replaceChildren();
|
||||
const lines = documentInfo.content.replaceAll("\r\n", "\n").split("\n");
|
||||
let code = null, list = null, paragraph = [];
|
||||
let code = null, list = null, table = null, paragraph = [];
|
||||
const flushParagraph = () => { if (!paragraph.length) return; const p = document.createElement("p"); appendInline(p, paragraph.join(" ").trim(), documentInfo.path); target.append(p); paragraph = []; };
|
||||
const closeList = () => { list = null; };
|
||||
for (const line of lines) {
|
||||
@@ -117,6 +128,16 @@
|
||||
continue;
|
||||
}
|
||||
if (code) { code.lines.push(line); code.node.textContent = code.lines.join("\n"); continue; }
|
||||
if (/^\s*\|.*\|\s*$/.test(line)) {
|
||||
flushParagraph(); closeList();
|
||||
const cells = line.trim().slice(1, -1).split("|").map(cell => cell.trim());
|
||||
if (cells.every(cell => /^:?-{2,}:?$/.test(cell))) continue;
|
||||
if (!table) { table = document.createElement("table"); const wrap = document.createElement("div"); wrap.className = "reader-table"; wrap.append(table); target.append(wrap); }
|
||||
const row = document.createElement("tr");
|
||||
for (const cell of cells) { const node = document.createElement(table.rows.length ? "td" : "th"); appendInline(node, cell, documentInfo.path); row.append(node); }
|
||||
table.append(row); continue;
|
||||
}
|
||||
table = null;
|
||||
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
|
||||
if (heading) { flushParagraph(); closeList(); const h = document.createElement(`h${heading[1].length}`); appendInline(h, heading[2], documentInfo.path); target.append(h); continue; }
|
||||
const bullet = /^\s*[-*]\s+(.+)$/.exec(line), ordered = /^\s*\d+\.\s+(.+)$/.exec(line);
|
||||
|
||||
+39
-11
@@ -3143,7 +3143,7 @@
|
||||
<p class="fine" data-i18n="tlsPqcHelp">TLS 1.3 only: prioritize hybrid
|
||||
X25519MLKEM768 with classic key exchanges as fallback. This is a key exchange
|
||||
group, not a cipher suite. Disabled by default.</p>
|
||||
<a href="/docs#doc=docs%2FTLS_POST_QUANTUM.md" target="_blank"
|
||||
<a href="/docs#doc=docs%2FTRANSPORT.md" target="_blank"
|
||||
rel="noopener noreferrer" data-i18n="tlsPqcDocs">Learn about PQC, compatibility
|
||||
and testing (documentation)</a>
|
||||
</div>
|
||||
@@ -4300,24 +4300,52 @@ log_errors = On</code></pre>
|
||||
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
||||
<div><span class="small-label">RustProxy</span><strong>__RUSTPROXY_VERSION__</strong></div>
|
||||
</div>
|
||||
<p data-i18n="aboutTagline">Reverse proxy, web server, certificate manager and traffic monitor in a single
|
||||
service with a built-in administration panel.</p>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
<dt data-i18n="aboutAuthor">Author</dt>
|
||||
<dd>Mateusz Gruszczynski @linuxiarz.pl</dd>
|
||||
<dd>Mateusz Gruszczyński · linuxiarz.pl</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="aboutLicense">License</dt>
|
||||
<dd data-i18n="aboutLicenseValue">GNU AGPL v3</dd>
|
||||
<dd data-i18n="aboutLicenseValue">GNU AGPL v3.0 or later</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="aboutCopyright">Copyright</dt>
|
||||
<dd>© 2026 Mateusz Gruszczyński</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="aboutSpdx">SPDX identifier</dt>
|
||||
<dd><code>AGPL-3.0-or-later</code></dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p data-i18n="aboutOpenSourceText">RustProxy is an open-source project. Its source code is publicly
|
||||
available for inspection, development and community contribution.</p>
|
||||
<a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutRepository">Official source
|
||||
repository</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
|
||||
<a class="outline about-repository" href="https://rustproxy.linuxiarz.pl/" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutPackagesRepository">Official package
|
||||
repository</span><span>rustproxy.linuxiarz.pl</span></a>
|
||||
<section class="about-license" aria-labelledby="about-license-title">
|
||||
<h3 class="small-label" data-i18n="aboutLicenseTitle" id="about-license-title">License summary</h3>
|
||||
<p data-i18n="aboutOpenSourceText">RustProxy is free and open-source software. You may use, study,
|
||||
modify and redistribute it under the terms of the GNU Affero General Public License, version 3
|
||||
or any later version.</p>
|
||||
<ul>
|
||||
<li data-i18n="aboutLicensePointUse">Use it for any purpose, including commercial deployments.</li>
|
||||
<li data-i18n="aboutLicensePointShare">Distributed copies and modified versions must stay under the
|
||||
same license and include their source code.</li>
|
||||
<li data-i18n="aboutLicensePointNetwork">If you run a modified version as a network service, its
|
||||
users must be able to obtain the modified source code.</li>
|
||||
<li data-i18n="aboutLicensePointWarranty">The software is provided without any warranty.</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="about-links">
|
||||
<a class="outline about-repository" href="https://rustproxy.linuxiarz.pl/" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutPackagesRepository">Website and packages</span><span>rustproxy.linuxiarz.pl</span></a>
|
||||
<a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy"
|
||||
rel="noopener noreferrer" target="_blank"><span data-i18n="aboutRepository">Source
|
||||
code</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
|
||||
<a class="outline about-repository" href="/docs#doc=docs%2FREADME.md" rel="noopener"
|
||||
target="_blank"><span data-i18n="aboutDocumentation">Documentation and API
|
||||
reference</span><span>/docs</span></a>
|
||||
<a class="outline about-repository" href="/docs#doc=LICENSE" rel="noopener" target="_blank"><span
|
||||
data-i18n="aboutLicenseText">Full license text</span><span>LICENSE</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</dialog>
|
||||
<div aria-live="polite" class="toast" hidden="" id="toast" popover="manual" role="status"></div>
|
||||
|
||||
@@ -24,7 +24,6 @@ const group = (key, advanced = false) => {
|
||||
const body = el("div", undefined, "vh-grid"); box.append(body); container.append(box); return body;
|
||||
};
|
||||
const note = (parent, key) => { const node = text("p", key, "fine vh-wide"); parent.append(node); return node; };
|
||||
// Presentation-only subgroup: all existing inputs remain inside their original form.
|
||||
const subsection = (parent, key) => {
|
||||
const box = el("section", undefined, "vh-wide vh-setting-section");
|
||||
box.append(text("h3", key));
|
||||
@@ -215,8 +214,7 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
const connection = el("div", undefined, "vh-wide vh-backend-switches vh-backend-connection"); main.append(connection);
|
||||
controls["member-private"] = field(connection, "member-private", "vhPrivate", value.allow_private, "checkbox");
|
||||
const preserve = field(connection, "member-host", "vhPreserveHost", value.preserve_host === true, "checkbox");
|
||||
// Preserve Host is a request-authority choice, not an HTTP/1.1-only option.
|
||||
// The Rust forwarder keeps HTTP/2 :authority consistent with this value.
|
||||
// Preserve Host also applies to HTTP/2 :authority.
|
||||
|
||||
const advanced = el("details", undefined, "vh-wide vh-backend-advanced"), extra = el("div", undefined, "vh-grid"); advanced.append(text("summary", "advanced"), extra);
|
||||
field(extra, "member-allowlist", "vhAddressAllowlist", (value.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
||||
@@ -230,8 +228,7 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
field(extra, "member-max-concurrent", "vhMaxConcurrentRequests", value.max_concurrent_requests ?? 0, "number", { required: true, min: 0, max: 1000000 });
|
||||
field(extra, "member-queue-timeout", "vhBackendQueueTimeout", value.queue_timeout_ms ?? 0, "number", { required: true, min: 0, max: 60000 });
|
||||
note(extra, "vhBackendCapacityHelp");
|
||||
// The per-member settings live in the Health tab. Keep one persistent form
|
||||
// per backend, so switching the selection never discards unsaved edits.
|
||||
// One persistent form per backend keeps unsaved edits when switching.
|
||||
const healthPanel = el("section", undefined, "vh-grid vh-backend-health-panel");
|
||||
const customHealth = field(healthPanel, "member-health-custom", "backendHealthOverride", value.health_check != null, "checkbox");
|
||||
const health = value.health_check || {};
|
||||
@@ -305,9 +302,7 @@ function debugOriginIssues(members) {
|
||||
let parsed; try { parsed = new URL(member.url); } catch { return false; }
|
||||
const host = parsed.hostname.replace(/^\[|\]$/g, ""), ipHost = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
||||
if (parsed.protocol !== "https:" || !ipHost) return false;
|
||||
// The troubleshooting preset is OPTIONAL, not a protocol compatibility rule.
|
||||
// Warn only when IP-host TLS verification may need an explicit identity;
|
||||
// HTTP/1.1, Auto and H2 plus preserve_host are all valid configurations.
|
||||
// Warn only when TLS verification of an IP host needs an explicit server name.
|
||||
return member.tls_verify === true && !String(member.tls_server_name || "").trim();
|
||||
});
|
||||
}
|
||||
@@ -498,9 +493,7 @@ function locationsEditor(parent, values) {
|
||||
row.addEventListener("input", () => { syncUi(); sync(); }); row.addEventListener("change", () => { syncUi(); sync(); });
|
||||
rows.append(row); syncUi();
|
||||
}
|
||||
// The main vhost backend is an internal fallback, not a Custom location.
|
||||
// An empty location list must stay empty so the editor cannot accidentally
|
||||
// delete or recreate the main route as user configuration.
|
||||
// The main backend is an internal fallback, never an editable location.
|
||||
for (const value of values) add(value);
|
||||
const bottom = el("div", undefined, "vh-wide location-toolbar location-toolbar-bottom"); bottom.append(button("locationAdd", () => { add(); sync(); }, "primary")); parent.append(bottom);
|
||||
form.querySelector(".vh-members").addEventListener("input", () => { for (const select of rows.querySelectorAll('[name="location-backend"]')) memberOptions(select); });
|
||||
|
||||
@@ -516,6 +516,7 @@ function renderStatus(status) {
|
||||
errors: metrics ? Number(metrics.errors) || 0 : existing.errors ?? 0,
|
||||
traffic: metrics ? vhostTrafficRate(id, metrics, trafficSnapshot, trafficNow, seenVhostTraffic) : existing.traffic ?? 0,
|
||||
latency: metrics ? Number(metrics.ewma_latency_ms) || 0 : existing.latency ?? 0,
|
||||
measured: Boolean(metrics) || Boolean(existing.measured),
|
||||
});
|
||||
}
|
||||
for (const key of vhostTrafficHistory.keys()) if (!seenVhostTraffic.has(key)) vhostTrafficHistory.delete(key);
|
||||
@@ -533,8 +534,10 @@ function renderStatus(status) {
|
||||
fill("vhostLatency", d => d.latency > 0 ? `${fmt(d.latency, 1)} ms` : "-"); fill("vhostTraffic", d => `${rateBytes(d.traffic)}/s`);
|
||||
const now = performance.now();
|
||||
for (const [id, data] of aggregate) {
|
||||
if (!data.measured) continue;
|
||||
const history = chartHistory.get(id) || { last: null, points: [], bucketSize: 1, pendingSum: 0, pendingCount: 0 };
|
||||
const sample = rustProxyRateSampling.difference(history.last, [data.requests], now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
if (sample.rapid) continue;
|
||||
const pausedRate = history.last !== null && sample.deltas === null;
|
||||
history.last = sample.next;
|
||||
const rate = sample.deltas ? sample.deltas[0] / sample.seconds : 0;
|
||||
|
||||
@@ -112,7 +112,7 @@ impl Pool {
|
||||
}
|
||||
fn affinity<'a>(&self, headers: &'a HeaderMap) -> Option<&'a str> {
|
||||
let mut values = headers
|
||||
.get_all("cookie")
|
||||
.get_all(::http::header::COOKIE)
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.flat_map(|v| v.split(';'))
|
||||
@@ -131,7 +131,7 @@ impl Pool {
|
||||
return;
|
||||
}
|
||||
let retained: Vec<_> = headers
|
||||
.get_all("set-cookie")
|
||||
.get_all(::http::header::SET_COOKIE)
|
||||
.iter()
|
||||
.filter(|value| {
|
||||
value
|
||||
@@ -142,9 +142,9 @@ impl Pool {
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
headers.remove("set-cookie");
|
||||
headers.remove(::http::header::SET_COOKIE);
|
||||
for value in retained {
|
||||
headers.append("set-cookie", value);
|
||||
headers.append(::http::header::SET_COOKIE, value);
|
||||
}
|
||||
}
|
||||
pub fn strip_affinity(&self, headers: &mut HeaderMap) {
|
||||
@@ -152,7 +152,7 @@ impl Pool {
|
||||
return;
|
||||
}
|
||||
let values: Vec<HeaderValue> = headers
|
||||
.get_all("cookie")
|
||||
.get_all(::http::header::COOKIE)
|
||||
.iter()
|
||||
.filter_map(|value| {
|
||||
let Ok(text) = value.to_str() else {
|
||||
@@ -175,9 +175,9 @@ impl Pool {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
headers.remove("cookie");
|
||||
headers.remove(::http::header::COOKIE);
|
||||
for value in values {
|
||||
headers.append("cookie", value);
|
||||
headers.append(::http::header::COOKIE, value);
|
||||
}
|
||||
}
|
||||
pub fn select_member(&self, id: &str) -> Option<Selection> {
|
||||
|
||||
Vendored
+1
-2
@@ -6,8 +6,7 @@ mod tests {
|
||||
h.insert("cache-control", value.parse().unwrap());
|
||||
h
|
||||
}
|
||||
// Legacy-policy regression tests explicitly opt out of the new 0.34.7
|
||||
// default; varnish-compatible behavior has its own test module.
|
||||
// Legacy policy; varnish-compatible behavior is tested separately.
|
||||
fn settings() -> Settings {
|
||||
Settings {
|
||||
enabled: true,
|
||||
|
||||
Vendored
+44
-1
@@ -1,3 +1,6 @@
|
||||
const UNCACHEABLE_MEMO_TTL: Duration = Duration::from_secs(30);
|
||||
const UNCACHEABLE_MEMO_CAPACITY: usize = 8192;
|
||||
|
||||
struct FillFlight {
|
||||
id: u64,
|
||||
scope: String,
|
||||
@@ -50,7 +53,7 @@ impl Drop for FillLeaderGuard {
|
||||
|
||||
impl Cache {
|
||||
pub async fn claim_fill(self: &Arc<Self>, ticket: &Ticket, settings: &Settings) -> FillClaim {
|
||||
if !settings.collapse_enabled {
|
||||
if !settings.collapse_enabled || self.recently_uncacheable(&ticket.key) {
|
||||
return FillClaim::Disabled;
|
||||
}
|
||||
let existing = {
|
||||
@@ -116,7 +119,46 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a key as uncacheable so requests skip coalescing.
|
||||
pub fn remember_uncacheable(&self, ticket: &Ticket) {
|
||||
let now = std::time::Instant::now();
|
||||
let mut memo = self
|
||||
.uncacheable_until
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if memo.len() >= UNCACHEABLE_MEMO_CAPACITY {
|
||||
memo.retain(|_, until| *until > now);
|
||||
if memo.len() >= UNCACHEABLE_MEMO_CAPACITY {
|
||||
return;
|
||||
}
|
||||
}
|
||||
memo.insert(ticket.key, now + UNCACHEABLE_MEMO_TTL);
|
||||
}
|
||||
|
||||
fn recently_uncacheable(&self, key: &Key) -> bool {
|
||||
let mut memo = self
|
||||
.uncacheable_until
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
match memo.get(key) {
|
||||
Some(until) if *until > std::time::Instant::now() => true,
|
||||
Some(_) => {
|
||||
memo.remove(key);
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn forget_uncacheable(&self) {
|
||||
self.uncacheable_until
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn cancel_all_fill_flights(&self) {
|
||||
self.forget_uncacheable();
|
||||
let flights = {
|
||||
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
flights
|
||||
@@ -130,6 +172,7 @@ impl Cache {
|
||||
}
|
||||
|
||||
fn cancel_scope_fill_flights(&self, scope: &str) {
|
||||
self.forget_uncacheable();
|
||||
let removed = {
|
||||
let mut flights = self.fill_flights.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let keys = flights
|
||||
|
||||
Vendored
+2
-2
@@ -92,9 +92,9 @@ impl Candidate {
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
if !self.headers.contains_key("date") {
|
||||
if !self.headers.contains_key(::http::header::DATE) {
|
||||
if let Ok(value) = httpdate::fmt_http_date(SystemTime::now()).parse() {
|
||||
self.headers.insert("date", value);
|
||||
self.headers.insert(::http::header::DATE, value);
|
||||
}
|
||||
}
|
||||
self.headers.remove("x-request-id");
|
||||
|
||||
Vendored
+2
@@ -23,6 +23,7 @@ impl Cache {
|
||||
cold_read_bytes_active: AtomicU64::new(0),
|
||||
revalidating: Mutex::new(HashSet::new()),
|
||||
fill_flights: Mutex::new(HashMap::new()),
|
||||
uncacheable_until: Mutex::new(HashMap::new()),
|
||||
fill_flight_sequence: AtomicU64::new(0),
|
||||
collapse_waiters: AtomicU64::new(0),
|
||||
bans: Mutex::new(Vec::new()),
|
||||
@@ -75,6 +76,7 @@ impl Cache {
|
||||
cold_read_bytes_active: AtomicU64::new(0),
|
||||
revalidating: Mutex::new(HashSet::new()),
|
||||
fill_flights: Mutex::new(HashMap::new()),
|
||||
uncacheable_until: Mutex::new(HashMap::new()),
|
||||
fill_flight_sequence: AtomicU64::new(0),
|
||||
collapse_waiters: AtomicU64::new(0),
|
||||
bans: Mutex::new(Vec::new()),
|
||||
|
||||
Vendored
+2
-4
@@ -87,8 +87,7 @@ impl Cache {
|
||||
if skip_compression {
|
||||
response.extensions_mut().insert(crate::compression::Skip);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
"age",
|
||||
response.headers_mut().insert(::http::header::AGE,
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
self.domain_stats
|
||||
@@ -285,8 +284,7 @@ impl Cache {
|
||||
if entry.skip_compression {
|
||||
response.extensions_mut().insert(crate::compression::Skip);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
"age",
|
||||
response.headers_mut().insert(::http::header::AGE,
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
self.domain_stats
|
||||
|
||||
Vendored
+7
-8
@@ -22,9 +22,9 @@ impl Cache {
|
||||
if cached.body.len() > settings.max_object_bytes {
|
||||
return None;
|
||||
}
|
||||
if !headers.contains_key("date") {
|
||||
if !headers.contains_key(::http::header::DATE) {
|
||||
if let Ok(value) = httpdate::fmt_http_date(SystemTime::now()).parse() {
|
||||
headers.insert("date", value);
|
||||
headers.insert(::http::header::DATE, value);
|
||||
}
|
||||
}
|
||||
headers.remove("x-request-id");
|
||||
@@ -34,11 +34,11 @@ impl Cache {
|
||||
let vary = plan.vary.clone();
|
||||
let tags = cache_tags(&headers);
|
||||
let origin_etag = validation_headers
|
||||
.get("etag")
|
||||
.get(::http::header::ETAG)
|
||||
.cloned()
|
||||
.or_else(|| cached.origin_etag.clone());
|
||||
let origin_last_modified = validation_headers
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.cloned()
|
||||
.or_else(|| cached.origin_last_modified.clone());
|
||||
let cost = hot_entry_cost(
|
||||
@@ -98,8 +98,7 @@ impl Cache {
|
||||
}
|
||||
self.counters.hits.fetch_add(1, Ordering::Relaxed);
|
||||
let mut response = cached.response(headers);
|
||||
response.headers_mut().insert(
|
||||
"age",
|
||||
response.headers_mut().insert(::http::header::AGE,
|
||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||
);
|
||||
Some(Hit {
|
||||
@@ -156,11 +155,11 @@ impl Cache {
|
||||
.cloned();
|
||||
let origin_etag = match origin_validators.as_ref() {
|
||||
Some(validators) => validators.etag.clone(),
|
||||
None => headers.get("etag").cloned(),
|
||||
None => headers.get(::http::header::ETAG).cloned(),
|
||||
};
|
||||
let origin_last_modified = match origin_validators.as_ref() {
|
||||
Some(validators) => validators.last_modified.clone(),
|
||||
None => headers.get("last-modified").cloned(),
|
||||
None => headers.get(::http::header::LAST_MODIFIED).cloned(),
|
||||
};
|
||||
let transformed = parts
|
||||
.extensions
|
||||
|
||||
Vendored
+1
-2
@@ -333,8 +333,7 @@ impl Settings {
|
||||
if transformed {
|
||||
value.push_str("; transformed");
|
||||
}
|
||||
headers.append(
|
||||
"cache-status",
|
||||
headers.append(::http::header::CACHE_STATUS,
|
||||
HeaderValue::from_str(&value).expect("generated Cache-Status member"),
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+18
-17
@@ -68,6 +68,7 @@ pub struct Cache {
|
||||
cold_read_bytes_active: AtomicU64,
|
||||
revalidating: Mutex<HashSet<Key>>,
|
||||
fill_flights: Mutex<HashMap<Key, Arc<FillFlight>>>,
|
||||
uncacheable_until: Mutex<HashMap<Key, std::time::Instant>>,
|
||||
fill_flight_sequence: AtomicU64,
|
||||
collapse_waiters: AtomicU64,
|
||||
bans: Mutex<Vec<BanRule>>,
|
||||
@@ -183,35 +184,35 @@ impl Revalidation {
|
||||
&self.headers
|
||||
}
|
||||
pub fn apply_validators(&self, headers: &mut HeaderMap) -> bool {
|
||||
if headers.contains_key("if-none-match") {
|
||||
let Some(public_etag) = self.headers.get("etag").and_then(|v| v.to_str().ok()) else {
|
||||
if headers.contains_key(::http::header::IF_NONE_MATCH) {
|
||||
let Some(public_etag) = self.headers.get(::http::header::ETAG).and_then(|v| v.to_str().ok()) else {
|
||||
return false;
|
||||
};
|
||||
if !single_if_none_match_matches(headers, public_etag) {
|
||||
return false;
|
||||
}
|
||||
if let Some(origin) = self.origin_etag.clone() {
|
||||
headers.insert("if-none-match", origin);
|
||||
headers.remove("if-modified-since");
|
||||
headers.insert(::http::header::IF_NONE_MATCH, origin);
|
||||
headers.remove(::http::header::IF_MODIFIED_SINCE);
|
||||
return true;
|
||||
}
|
||||
if let Some(modified) = self.origin_last_modified.clone() {
|
||||
headers.remove("if-none-match");
|
||||
headers.insert("if-modified-since", modified);
|
||||
headers.remove(::http::header::IF_NONE_MATCH);
|
||||
headers.insert(::http::header::IF_MODIFIED_SINCE, modified);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if headers.contains_key("if-modified-since") {
|
||||
if headers.contains_key(::http::header::IF_MODIFIED_SINCE) {
|
||||
let Some(cached) = self
|
||||
.headers
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(client) = headers
|
||||
.get("if-modified-since")
|
||||
.get(::http::header::IF_MODIFIED_SINCE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
else {
|
||||
return false;
|
||||
@@ -226,16 +227,16 @@ impl Revalidation {
|
||||
let Some(origin) = self.origin_last_modified.clone() else {
|
||||
return false;
|
||||
};
|
||||
headers.insert("if-modified-since", origin);
|
||||
headers.insert(::http::header::IF_MODIFIED_SINCE, origin);
|
||||
return true;
|
||||
}
|
||||
if let Some(etag) = self.origin_etag.clone() {
|
||||
headers.insert("if-none-match", etag);
|
||||
headers.remove("if-modified-since");
|
||||
headers.insert(::http::header::IF_NONE_MATCH, etag);
|
||||
headers.remove(::http::header::IF_MODIFIED_SINCE);
|
||||
true
|
||||
} else if let Some(modified) = self.origin_last_modified.clone() {
|
||||
headers.insert("if-modified-since", modified);
|
||||
headers.remove("if-none-match");
|
||||
headers.insert(::http::header::IF_MODIFIED_SINCE, modified);
|
||||
headers.remove(::http::header::IF_NONE_MATCH);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -281,7 +282,7 @@ impl Revalidation {
|
||||
}
|
||||
pub fn merge_validation_headers(&self, validation: &HeaderMap) -> HeaderMap {
|
||||
let mut merged = merge_revalidated_headers(&self.headers, validation);
|
||||
let public_etag = self.headers.get("etag").cloned();
|
||||
let public_etag = self.headers.get(::http::header::ETAG).cloned();
|
||||
let representation_etag_differs = public_etag.as_ref().is_some_and(|public| {
|
||||
self.origin_etag
|
||||
.as_ref()
|
||||
@@ -289,7 +290,7 @@ impl Revalidation {
|
||||
});
|
||||
if representation_etag_differs {
|
||||
if let Some(public) = public_etag {
|
||||
merged.insert("etag", public);
|
||||
merged.insert(::http::header::ETAG, public);
|
||||
}
|
||||
}
|
||||
merged
|
||||
@@ -316,7 +317,7 @@ impl Revalidation {
|
||||
}
|
||||
fn vary_names(headers: &HeaderMap) -> Option<Vec<String>> {
|
||||
let mut names = Vec::new();
|
||||
for value in headers.get_all("vary") {
|
||||
for value in headers.get_all(::http::header::VARY) {
|
||||
for raw in value.to_str().ok()?.split(',') {
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() || raw == "*" {
|
||||
|
||||
Vendored
+1
-1
@@ -77,7 +77,7 @@ fn retention_deadline(
|
||||
}
|
||||
|
||||
fn has_validator(headers: &HeaderMap) -> bool {
|
||||
headers.contains_key("etag") || headers.contains_key("last-modified")
|
||||
headers.contains_key(::http::header::ETAG) || headers.contains_key(::http::header::LAST_MODIFIED)
|
||||
}
|
||||
|
||||
fn stale_forbidden(must_revalidate: bool, proxy_revalidate: bool) -> bool {
|
||||
|
||||
Vendored
+29
-29
@@ -1,6 +1,6 @@
|
||||
fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
|
||||
let mut result = HashMap::new();
|
||||
for header in headers.get_all("cache-control") {
|
||||
for header in headers.get_all(::http::header::CACHE_CONTROL) {
|
||||
for raw in header.to_str().ok()?.split(',') {
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() {
|
||||
@@ -49,7 +49,7 @@ fn compatible_freshness_headers<'a>(
|
||||
headers: &'a HeaderMap,
|
||||
compatible: bool,
|
||||
) -> std::borrow::Cow<'a, HeaderMap> {
|
||||
if !compatible || (!headers.contains_key("cache-control") && !headers.contains_key("pragma")) {
|
||||
if !compatible || (!headers.contains_key(::http::header::CACHE_CONTROL) && !headers.contains_key(::http::header::PRAGMA)) {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ fn compatible_freshness_headers<'a>(
|
||||
let mut changed = false;
|
||||
|
||||
if pragma_no_cache(headers) {
|
||||
freshness.remove("pragma");
|
||||
for header in headers.get_all("pragma") {
|
||||
freshness.remove(::http::header::PRAGMA);
|
||||
for header in headers.get_all(::http::header::PRAGMA) {
|
||||
let Ok(text) = header.to_str() else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
@@ -84,14 +84,14 @@ fn compatible_freshness_headers<'a>(
|
||||
let Ok(value) = HeaderValue::from_str(&retained.join(", ")) else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
freshness.append("pragma", value);
|
||||
freshness.append(::http::header::PRAGMA, value);
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if headers.contains_key("cache-control") {
|
||||
if headers.contains_key(::http::header::CACHE_CONTROL) {
|
||||
let mut retained_values = Vec::new();
|
||||
for header in headers.get_all("cache-control") {
|
||||
for header in headers.get_all(::http::header::CACHE_CONTROL) {
|
||||
let Ok(text) = header.to_str() else {
|
||||
return std::borrow::Cow::Borrowed(headers);
|
||||
};
|
||||
@@ -118,9 +118,9 @@ fn compatible_freshness_headers<'a>(
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
freshness.remove("cache-control");
|
||||
freshness.remove(::http::header::CACHE_CONTROL);
|
||||
for value in retained_values {
|
||||
freshness.append("cache-control", value);
|
||||
freshness.append(::http::header::CACHE_CONTROL, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ pub fn request_freshness_headers<'a>(
|
||||
|
||||
pub fn only_if_cached(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get_all("cache-control")
|
||||
.get_all(::http::header::CACHE_CONTROL)
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.any(|v| {
|
||||
@@ -170,7 +170,7 @@ impl RequestPolicy {
|
||||
}
|
||||
fn pragma_no_cache(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get_all("pragma")
|
||||
.get_all(::http::header::PRAGMA)
|
||||
.iter()
|
||||
.filter_map(|value| value.to_str().ok())
|
||||
.any(|value| {
|
||||
@@ -193,7 +193,7 @@ pub fn forced_request_policy(
|
||||
if matches!(request_range(method, headers), RangeSelection::Unsupported) {
|
||||
return RequestPolicy::Bypass("request_range_unsupported");
|
||||
}
|
||||
if headers.contains_key("if-range") && !headers.contains_key("range") {
|
||||
if headers.contains_key(::http::header::IF_RANGE) && !headers.contains_key(::http::header::RANGE) {
|
||||
return RequestPolicy::Bypass("request_precondition");
|
||||
}
|
||||
for key in ["if-match", "if-unmodified-since", "upgrade"] {
|
||||
@@ -225,7 +225,7 @@ pub fn request_policy(
|
||||
if matches!(request_range(method, headers), RangeSelection::Unsupported) {
|
||||
return RequestPolicy::Bypass("request_range_unsupported");
|
||||
}
|
||||
if headers.contains_key("if-range") && !headers.contains_key("range") {
|
||||
if headers.contains_key(::http::header::IF_RANGE) && !headers.contains_key(::http::header::RANGE) {
|
||||
return RequestPolicy::Bypass("request_precondition");
|
||||
}
|
||||
if settings.bypass_query && has_query {
|
||||
@@ -285,7 +285,7 @@ pub fn request_policy(
|
||||
}
|
||||
}
|
||||
if !settings.varnish_compatible
|
||||
&& !headers.contains_key("cache-control")
|
||||
&& !headers.contains_key(::http::header::CACHE_CONTROL)
|
||||
&& pragma_no_cache(headers)
|
||||
{
|
||||
return RequestPolicy::Revalidate("request_pragma_no_cache");
|
||||
@@ -327,7 +327,7 @@ fn weak_etag(value: &str) -> Option<&str> {
|
||||
}
|
||||
fn if_none_match_matches(headers: &HeaderMap, etag: &str) -> Option<bool> {
|
||||
let target = weak_etag(etag)?;
|
||||
let values: Vec<_> = headers.get_all("if-none-match").iter().collect();
|
||||
let values: Vec<_> = headers.get_all(::http::header::IF_NONE_MATCH).iter().collect();
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -362,7 +362,7 @@ fn single_if_none_match_matches(headers: &HeaderMap, etag: &str) -> bool {
|
||||
let Some(target) = weak_etag(etag) else {
|
||||
return false;
|
||||
};
|
||||
let values: Vec<_> = headers.get_all("if-none-match").iter().collect();
|
||||
let values: Vec<_> = headers.get_all(::http::header::IF_NONE_MATCH).iter().collect();
|
||||
if values.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
@@ -381,22 +381,22 @@ fn single_if_none_match_matches(headers: &HeaderMap, etag: &str) -> bool {
|
||||
&& weak_etag(token).is_some_and(|candidate| candidate == target)
|
||||
}
|
||||
pub fn conditional_not_modified(request: &HeaderMap, response: &HeaderMap) -> bool {
|
||||
if request.contains_key("if-none-match") {
|
||||
if request.contains_key(::http::header::IF_NONE_MATCH) {
|
||||
return response
|
||||
.get("etag")
|
||||
.get(::http::header::ETAG)
|
||||
.and_then(|etag| etag.to_str().ok())
|
||||
.and_then(|etag| if_none_match_matches(request, etag))
|
||||
.unwrap_or(false);
|
||||
}
|
||||
let Some(since) = request
|
||||
.get("if-modified-since")
|
||||
.get(::http::header::IF_MODIFIED_SINCE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| httpdate::parse_http_date(value).ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
response
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| httpdate::parse_http_date(value).ok())
|
||||
.is_some_and(|modified| modified <= since)
|
||||
@@ -411,8 +411,8 @@ pub fn make_not_modified(mut response: Response) -> Response {
|
||||
}
|
||||
pub fn merge_revalidated_headers(cached: &HeaderMap, validation: &HeaderMap) -> HeaderMap {
|
||||
let mut merged = cached.clone();
|
||||
if !validation.contains_key("age") {
|
||||
merged.remove("age");
|
||||
if !validation.contains_key(::http::header::AGE) {
|
||||
merged.remove(::http::header::AGE);
|
||||
}
|
||||
for name in validation.keys() {
|
||||
if matches!(
|
||||
@@ -426,16 +426,16 @@ pub fn merge_revalidated_headers(cached: &HeaderMap, validation: &HeaderMap) ->
|
||||
merged.append(name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
if !validation.contains_key("date") {
|
||||
if !validation.contains_key(::http::header::DATE) {
|
||||
if let Ok(value) = httpdate::fmt_http_date(SystemTime::now()).parse() {
|
||||
merged.insert("date", value);
|
||||
merged.insert(::http::header::DATE, value);
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
pub fn varies_on(headers: &HeaderMap, wanted: &str) -> bool {
|
||||
headers.get_all("vary").iter().any(|value| {
|
||||
headers.get_all(::http::header::VARY).iter().any(|value| {
|
||||
value.to_str().ok().is_some_and(|value| {
|
||||
value
|
||||
.split(',')
|
||||
@@ -465,13 +465,13 @@ pub fn response_reason(
|
||||
if status != StatusCode::OK {
|
||||
return "response_status";
|
||||
}
|
||||
if headers.contains_key("set-cookie") {
|
||||
if headers.contains_key(::http::header::SET_COOKIE) {
|
||||
return "response_set_cookie";
|
||||
}
|
||||
if headers.contains_key("content-range") {
|
||||
if headers.contains_key(::http::header::CONTENT_RANGE) {
|
||||
return "response_range";
|
||||
}
|
||||
if headers.contains_key("trailer") {
|
||||
if headers.contains_key(::http::header::TRAILER) {
|
||||
return "response_trailers";
|
||||
}
|
||||
let Some(control) = directives(headers) else {
|
||||
@@ -485,7 +485,7 @@ pub fn response_reason(
|
||||
if settings.varnish_compatible && surrogate_no_store(headers) {
|
||||
return "response_surrogate_control";
|
||||
}
|
||||
for value in headers.get_all("vary") {
|
||||
for value in headers.get_all(::http::header::VARY) {
|
||||
if value.to_str().ok().is_none_or(|value| {
|
||||
value.split(',').any(|name| {
|
||||
let name = name.trim();
|
||||
|
||||
Vendored
+13
-20
@@ -61,9 +61,9 @@ pub fn plan(
|
||||
return None;
|
||||
}
|
||||
if status != StatusCode::OK
|
||||
|| headers.contains_key("set-cookie")
|
||||
|| headers.contains_key("content-range")
|
||||
|| headers.contains_key("trailer")
|
||||
|| headers.contains_key(::http::header::SET_COOKIE)
|
||||
|| headers.contains_key(::http::header::CONTENT_RANGE)
|
||||
|| headers.contains_key(::http::header::TRAILER)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ pub fn plan(
|
||||
return None;
|
||||
}
|
||||
if headers
|
||||
.get("content-type")
|
||||
.get(::http::header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.is_some_and(|v| {
|
||||
let v = v.trim_start().to_ascii_lowercase();
|
||||
@@ -92,14 +92,11 @@ pub fn plan(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Surrogate-Control: no-store is a shared-cache veto in compatibility
|
||||
// mode, regardless of public/fallback TTL or any other directive.
|
||||
// Surrogate-Control: no-store always vetoes shared caching.
|
||||
if settings.varnish_compatible && surrogate_no_store(headers) {
|
||||
return None;
|
||||
}
|
||||
// Explicitly configured fallback TTL is an opt-in origin no-cache
|
||||
// override *only* for public responses. With fallback disabled, keep
|
||||
// the pre-0.34.7 validator-based revalidation and never grant a HIT.
|
||||
// Fallback TTL overrides origin no-cache only for public responses.
|
||||
let override_public_no_cache = settings.varnish_compatible
|
||||
&& control.contains_key("no-cache")
|
||||
&& control.contains_key("public")
|
||||
@@ -117,13 +114,13 @@ pub fn plan(
|
||||
let mut stale_while_revalidate = parse_window("stale-while-revalidate")?;
|
||||
let stale_if_error = parse_window("stale-if-error")?;
|
||||
if requires_revalidation
|
||||
&& !headers.contains_key("etag")
|
||||
&& !headers.contains_key("last-modified")
|
||||
&& !headers.contains_key(::http::header::ETAG)
|
||||
&& !headers.contains_key(::http::header::LAST_MODIFIED)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let now = SystemTime::now();
|
||||
let date = match headers.get("date") {
|
||||
let date = match headers.get(::http::header::DATE) {
|
||||
Some(v) => httpdate::parse_http_date(v.to_str().ok()?).ok()?,
|
||||
None => now,
|
||||
};
|
||||
@@ -131,7 +128,7 @@ pub fn plan(
|
||||
Some(settings.default_ttl_seconds)
|
||||
} else if let Some(value) = control.get("s-maxage").or_else(|| control.get("max-age")) {
|
||||
Some(value.as_ref()?.parse::<u64>().ok()?)
|
||||
} else if let Some(expires) = headers.get("expires") {
|
||||
} else if let Some(expires) = headers.get(::http::header::EXPIRES) {
|
||||
Some(
|
||||
httpdate::parse_http_date(expires.to_str().ok()?)
|
||||
.ok()?
|
||||
@@ -144,7 +141,7 @@ pub fn plan(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let age = match headers.get("age") {
|
||||
let age = match headers.get(::http::header::AGE) {
|
||||
Some(v) => v.to_str().ok()?.parse::<u64>().ok()?,
|
||||
None => 0,
|
||||
};
|
||||
@@ -161,11 +158,7 @@ pub fn plan(
|
||||
None if requires_revalidation => 0,
|
||||
None => return None,
|
||||
};
|
||||
// Retain zero-TTL and already-expired representations for conditional
|
||||
// revalidation, client max-stale or stale-if-error. They must not be
|
||||
// served as fresh HITs. Varnish-style grace is only implicit when the
|
||||
// origin did not explicitly set stale-while-revalidate (including =0),
|
||||
// the object arrived fresh and no origin no-cache rule is in effect.
|
||||
// Expired entries are kept for revalidation only; implicit grace needs no explicit SWR/no-cache.
|
||||
if settings.varnish_compatible
|
||||
&& !control.contains_key("stale-while-revalidate")
|
||||
&& !control.contains_key("no-cache")
|
||||
@@ -184,7 +177,7 @@ pub fn plan(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let expected_length = match headers.get("content-length") {
|
||||
let expected_length = match headers.get(::http::header::CONTENT_LENGTH) {
|
||||
Some(v) => Some(v.to_str().ok()?.parse::<usize>().ok()?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Vendored
+9
-12
@@ -12,11 +12,11 @@ pub enum RangeSelection {
|
||||
}
|
||||
|
||||
pub fn request_range(method: &Method, headers: &HeaderMap) -> RangeSelection {
|
||||
let mut values = headers.get_all("range").iter();
|
||||
let mut values = headers.get_all(::http::header::RANGE).iter();
|
||||
let Some(value) = values.next() else {
|
||||
return RangeSelection::None;
|
||||
};
|
||||
if values.next().is_some() || headers.get_all("if-range").iter().count() > 1 {
|
||||
if values.next().is_some() || headers.get_all(::http::header::IF_RANGE).iter().count() > 1 {
|
||||
return RangeSelection::Unsupported;
|
||||
}
|
||||
if method != Method::GET {
|
||||
@@ -74,7 +74,7 @@ fn if_range_matches(headers: &HeaderMap, if_range: Option<&HeaderValue>) -> bool
|
||||
return false;
|
||||
}
|
||||
return headers
|
||||
.get("etag")
|
||||
.get(::http::header::ETAG)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|etag| !etag.trim().starts_with("W/") && etag.trim() == candidate);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ fn if_range_matches(headers: &HeaderMap, if_range: Option<&HeaderValue>) -> bool
|
||||
return false;
|
||||
};
|
||||
headers
|
||||
.get("last-modified")
|
||||
.get(::http::header::LAST_MODIFIED)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| httpdate::parse_http_date(value).ok())
|
||||
.is_some_and(|modified| modified <= candidate_date)
|
||||
@@ -127,32 +127,29 @@ impl Hit {
|
||||
let full_len = self.body.len();
|
||||
self.response
|
||||
.headers_mut()
|
||||
.insert("accept-ranges", HeaderValue::from_static("bytes"));
|
||||
.insert(::http::header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
|
||||
self.response
|
||||
.extensions_mut()
|
||||
.insert(crate::compression::Skip);
|
||||
let Some((start, end)) = resolve_byte_range(range, full_len) else {
|
||||
*self.response.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
|
||||
*self.response.body_mut() = Body::empty();
|
||||
self.response.headers_mut().insert(
|
||||
"content-range",
|
||||
self.response.headers_mut().insert(::http::header::CONTENT_RANGE,
|
||||
HeaderValue::from_str(&format!("bytes */{full_len}")).expect("generated range"),
|
||||
);
|
||||
self.response
|
||||
.headers_mut()
|
||||
.insert("content-length", HeaderValue::from_static("0"));
|
||||
.insert(::http::header::CONTENT_LENGTH, HeaderValue::from_static("0"));
|
||||
return self;
|
||||
};
|
||||
let slice = self.body.slice(start..=end);
|
||||
*self.response.status_mut() = StatusCode::PARTIAL_CONTENT;
|
||||
*self.response.body_mut() = Body::from(slice.clone());
|
||||
self.response.headers_mut().insert(
|
||||
"content-range",
|
||||
self.response.headers_mut().insert(::http::header::CONTENT_RANGE,
|
||||
HeaderValue::from_str(&format!("bytes {start}-{end}/{full_len}"))
|
||||
.expect("generated range"),
|
||||
);
|
||||
self.response.headers_mut().insert(
|
||||
"content-length",
|
||||
self.response.headers_mut().insert(::http::header::CONTENT_LENGTH,
|
||||
HeaderValue::from_str(&slice.len().to_string()).expect("generated length"),
|
||||
);
|
||||
self.body = slice;
|
||||
|
||||
Vendored
+3
-5
@@ -294,9 +294,8 @@ impl Cache {
|
||||
}
|
||||
let hits = cached.hits.saturating_add(1);
|
||||
let mut headers = cached.headers.clone();
|
||||
headers.insert("age", HeaderValue::from_str(&cached.age.to_string()).ok()?);
|
||||
headers.append(
|
||||
"warning",
|
||||
headers.insert(::http::header::AGE, HeaderValue::from_str(&cached.age.to_string()).ok()?);
|
||||
headers.append(::http::header::WARNING,
|
||||
HeaderValue::from_static("110 RustProxy \"Response is stale\""),
|
||||
);
|
||||
let response = cached.response(headers);
|
||||
@@ -396,8 +395,7 @@ impl Cache {
|
||||
}
|
||||
|
||||
impl Ticket {
|
||||
/// Ignore only browser forced reload for freshness; keep min-fresh,
|
||||
/// max-stale and other client constraints, plus original Vary headers.
|
||||
/// Ignores forced-reload directives only; other client constraints apply.
|
||||
pub fn with_varnish_compatible(mut self, compatible: bool) -> Self {
|
||||
if let std::borrow::Cow::Owned(freshness) =
|
||||
compatible_freshness_headers(&self.request_headers, compatible)
|
||||
|
||||
+28
-17
@@ -15,7 +15,7 @@ impl Predicate for MinimumSize {
|
||||
fn should_compress<B: http_body::Body>(&self, response: &http::Response<B>) -> bool {
|
||||
let size = response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_LENGTH)
|
||||
.get(::http::header::CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.or_else(|| response.body().size_hint().exact());
|
||||
@@ -147,7 +147,7 @@ impl AppliedEncoding {
|
||||
|
||||
pub fn no_transform(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get_all("cache-control")
|
||||
.get_all(::http::header::CACHE_CONTROL)
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.any(|v| {
|
||||
@@ -165,9 +165,13 @@ pub fn no_transform(headers: &HeaderMap) -> bool {
|
||||
|
||||
pub(crate) fn request_headers(headers: &HeaderMap) -> HeaderMap {
|
||||
let mut selected = HeaderMap::new();
|
||||
for name in ["accept-encoding", "range", "cache-control"] {
|
||||
for value in headers.get_all(name) {
|
||||
selected.append(name, value.clone());
|
||||
for name in [
|
||||
::http::header::ACCEPT_ENCODING,
|
||||
::http::header::RANGE,
|
||||
::http::header::CACHE_CONTROL,
|
||||
] {
|
||||
for value in headers.get_all(&name) {
|
||||
selected.append(name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
selected
|
||||
@@ -185,10 +189,14 @@ fn eligible(
|
||||
|| response.status().is_informational()
|
||||
|| response.status().as_u16() == 204
|
||||
|| response.status().as_u16() == 304
|
||||
|| response.headers().contains_key("content-encoding")
|
||||
|| response.headers().contains_key("content-range")
|
||||
|| response.headers().contains_key("upgrade")
|
||||
|| request.contains_key("range")
|
||||
|| response
|
||||
.headers()
|
||||
.contains_key(::http::header::CONTENT_ENCODING)
|
||||
|| response
|
||||
.headers()
|
||||
.contains_key(::http::header::CONTENT_RANGE)
|
||||
|| response.headers().contains_key(::http::header::UPGRADE)
|
||||
|| request.contains_key(::http::header::RANGE)
|
||||
|| no_transform(request)
|
||||
|| no_transform(response.headers())
|
||||
{
|
||||
@@ -196,7 +204,7 @@ fn eligible(
|
||||
}
|
||||
!response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.get(::http::header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.is_some_and(|v| {
|
||||
v.split(';')
|
||||
@@ -221,7 +229,7 @@ pub(crate) fn preferred_encoding(
|
||||
) -> Option<AppliedEncoding> {
|
||||
let mut explicit = [None; 4];
|
||||
let mut wildcard = None;
|
||||
for line in headers.get_all("accept-encoding").iter() {
|
||||
for line in headers.get_all(::http::header::ACCEPT_ENCODING).iter() {
|
||||
let Ok(line) = line.to_str() else { continue };
|
||||
for item in line.split(',') {
|
||||
let mut parts = item.split(';');
|
||||
@@ -304,17 +312,20 @@ pub async fn encode(
|
||||
let mut request = Request::new(Body::empty());
|
||||
*request.method_mut() = method.clone();
|
||||
request.headers_mut().insert(
|
||||
"accept-encoding",
|
||||
::http::header::ACCEPT_ENCODING,
|
||||
HeaderValue::from_static(selected.as_str()),
|
||||
);
|
||||
let mut response = match compression.oneshot(request).await {
|
||||
Ok(response) => response.map(Body::new),
|
||||
Err(never) => match never {},
|
||||
};
|
||||
if response.headers().contains_key("content-encoding") {
|
||||
if response
|
||||
.headers()
|
||||
.contains_key(::http::header::CONTENT_ENCODING)
|
||||
{
|
||||
if let Some(applied) = response
|
||||
.headers()
|
||||
.get("content-encoding")
|
||||
.get(::http::header::CONTENT_ENCODING)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(AppliedEncoding::from_header)
|
||||
{
|
||||
@@ -322,15 +333,15 @@ pub async fn encode(
|
||||
}
|
||||
if let Some(value) = response
|
||||
.headers()
|
||||
.get("etag")
|
||||
.get(::http::header::ETAG)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::to_owned)
|
||||
{
|
||||
if !value.starts_with("W/") {
|
||||
if let Ok(weak) = HeaderValue::from_str(&format!("W/{value}")) {
|
||||
response.headers_mut().insert("etag", weak);
|
||||
response.headers_mut().insert(::http::header::ETAG, weak);
|
||||
} else {
|
||||
response.headers_mut().remove("etag");
|
||||
response.headers_mut().remove(::http::header::ETAG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -293,7 +293,7 @@ impl Compiled {
|
||||
"for={peer_node};by={by_node};proto={scheme};host=\"{authority_text}\""
|
||||
);
|
||||
let mut values = HeaderMap::with_capacity(7);
|
||||
values.insert("forwarded", forwarded.parse()?);
|
||||
values.insert(::http::header::FORWARDED, forwarded.parse()?);
|
||||
values.insert("x-forwarded-for", peer_header.clone());
|
||||
values.insert("x-real-ip", peer_header);
|
||||
values.insert("x-forwarded-host", authority.clone());
|
||||
@@ -384,7 +384,7 @@ impl Compiled {
|
||||
}
|
||||
write!(&mut forwarded, ";proto={scheme};host=\"{authority_text}\"")?;
|
||||
let mut values = HeaderMap::with_capacity(7);
|
||||
values.insert("forwarded", forwarded.parse()?);
|
||||
values.insert(::http::header::FORWARDED, forwarded.parse()?);
|
||||
values.insert("x-forwarded-for", xff.parse()?);
|
||||
values.insert("x-real-ip", client_ip.to_string().parse()?);
|
||||
values.insert("x-forwarded-host", authority.clone());
|
||||
@@ -436,7 +436,7 @@ impl Context {
|
||||
self.values.insert("x-forwarded-for", xff.parse()?);
|
||||
let existing = self
|
||||
.values
|
||||
.get("forwarded")
|
||||
.get(::http::header::FORWARDED)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("");
|
||||
let forwarded = if existing.is_empty() {
|
||||
@@ -444,7 +444,8 @@ impl Context {
|
||||
} else {
|
||||
format!("for={}, {existing}", node(client))
|
||||
};
|
||||
self.values.insert("forwarded", forwarded.parse()?);
|
||||
self.values
|
||||
.insert(::http::header::FORWARDED, forwarded.parse()?);
|
||||
}
|
||||
self.values.insert("x-real-ip", client_text.parse()?);
|
||||
self.client_ip = client;
|
||||
|
||||
@@ -35,6 +35,12 @@ impl PeerFormatting {
|
||||
untrusted_headers: std::sync::Mutex::new(None),
|
||||
}
|
||||
}
|
||||
/// Text form of the peer address when it needed no canonicalization.
|
||||
pub(crate) fn raw_peer_text(&self) -> Option<&str> {
|
||||
(self.peer.ip() == self.peer_ip)
|
||||
.then(|| self.peer_header.to_str().ok())
|
||||
.flatten()
|
||||
}
|
||||
fn cached_untrusted(&self, authority: &HeaderValue, tls: bool) -> Option<HeaderMap> {
|
||||
self.untrusted_headers
|
||||
.lock()
|
||||
|
||||
+5
-5
@@ -184,7 +184,7 @@ pub fn withdraw_advertisement(
|
||||
tls: bool,
|
||||
active: &[Listener],
|
||||
) {
|
||||
headers.remove("alt-svc");
|
||||
headers.remove(::http::header::ALT_SVC);
|
||||
if tls
|
||||
&& local.is_some_and(|local| {
|
||||
active
|
||||
@@ -192,7 +192,7 @@ pub fn withdraw_advertisement(
|
||||
.any(|binding| binding.matches(local, true) && binding.http3.is_some())
|
||||
})
|
||||
{
|
||||
headers.insert("alt-svc", HeaderValue::from_static("clear"));
|
||||
headers.insert(::http::header::ALT_SVC, HeaderValue::from_static("clear"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ pub fn advertise(
|
||||
desired: &[Listener],
|
||||
host_http3_enabled: bool,
|
||||
) {
|
||||
headers.remove("alt-svc");
|
||||
headers.remove(::http::header::ALT_SVC);
|
||||
if !tls {
|
||||
return;
|
||||
}
|
||||
@@ -225,7 +225,7 @@ pub fn advertise(
|
||||
if let Some(settings) = settings {
|
||||
let value = format!("h3=\":{}\"; ma={}", local.port(), settings.alt_svc_max_age);
|
||||
if let Ok(value) = HeaderValue::from_str(&value) {
|
||||
headers.insert("alt-svc", value);
|
||||
headers.insert(::http::header::ALT_SVC, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,7 +436,7 @@ async fn connection_loop(
|
||||
);
|
||||
send.stop_stream(Code::H3_MESSAGE_ERROR);return;
|
||||
}
|
||||
let expected=request.headers().get("content-length").and_then(|v|v.to_str().ok()).and_then(|v|v.parse::<u64>().ok());
|
||||
let expected=request.headers().get(::http::header::CONTENT_LENGTH).and_then(|v|v.to_str().ok()).and_then(|v|v.parse::<u64>().ok());
|
||||
let mut tunnel_receive=None;
|
||||
let mut request=if websocket {
|
||||
tunnel_receive=Some(receive);
|
||||
|
||||
+3
-3
@@ -9,11 +9,11 @@ fn invalid_headers(headers: &HeaderMap) -> bool {
|
||||
.iter()
|
||||
.any(|name| headers.contains_key(*name))
|
||||
|| headers
|
||||
.get_all("te")
|
||||
.get_all(::http::header::TE)
|
||||
.iter()
|
||||
.any(|value| value.as_bytes() != b"trailers")
|
||||
|| headers.get_all("content-length").iter().count() > 1
|
||||
|| headers.get("content-length").is_some_and(|value| {
|
||||
|| headers.get_all(::http::header::CONTENT_LENGTH).iter().count() > 1
|
||||
|| headers.get(::http::header::CONTENT_LENGTH).is_some_and(|value| {
|
||||
value
|
||||
.to_str()
|
||||
.ok()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Boundary/runtime helpers intentionally keep explicit typed parameters; Clippy's
|
||||
// argument-count threshold is stylistic and does not improve these call sites.
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
#![cfg_attr(test, allow(clippy::field_reassign_with_default))]
|
||||
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
// Boundary/runtime helpers intentionally keep explicit typed parameters; Clippy's
|
||||
// argument-count threshold is stylistic and does not improve these call sites.
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
#![cfg_attr(test, allow(clippy::field_reassign_with_default))]
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
include!("main/bootstrap.rs");
|
||||
include!("main/admin_runtime.rs");
|
||||
include!("main/proxy_runtime.rs");
|
||||
|
||||
+12
-8
@@ -183,7 +183,7 @@ impl Page {
|
||||
*response.status_mut() = status;
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
"content-type",
|
||||
::http::header::CONTENT_TYPE,
|
||||
HeaderValue::from_static(if matches!(self.kind, Kind::Empty) {
|
||||
"text/plain; charset=utf-8"
|
||||
} else {
|
||||
@@ -192,15 +192,18 @@ impl Page {
|
||||
);
|
||||
if !bodyless {
|
||||
if let Ok(value) = HeaderValue::from_str(&length.to_string()) {
|
||||
headers.insert("content-length", value);
|
||||
headers.insert(::http::header::CONTENT_LENGTH, value);
|
||||
}
|
||||
}
|
||||
headers.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
headers.insert(
|
||||
::http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-store"),
|
||||
);
|
||||
if matches!(self.kind, Kind::Welcome) {
|
||||
response.extensions_mut().insert(MinimalWelcome);
|
||||
} else {
|
||||
headers.insert(
|
||||
"x-content-type-options",
|
||||
::http::header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
}
|
||||
@@ -213,10 +216,11 @@ impl Page {
|
||||
*response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("allow", HeaderValue::from_static("GET, HEAD"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
.insert(::http::header::ALLOW, HeaderValue::from_static("GET, HEAD"));
|
||||
response.headers_mut().insert(
|
||||
::http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-store"),
|
||||
);
|
||||
response.extensions_mut().insert(PageServed);
|
||||
return response;
|
||||
}
|
||||
|
||||
+1
-2
@@ -97,8 +97,7 @@ mod tests {
|
||||
assert!(gateway_502.contains("RustProxy"));
|
||||
assert!(gateway_502.contains("#131615"));
|
||||
assert!(!gateway_502.contains("Tinyauth"));
|
||||
// The standard HTTP error registry and its stock pages predate the
|
||||
// Tinyauth-specific diagnostic pages and must remain unchanged.
|
||||
// Stock error pages must stay unchanged.
|
||||
assert_eq!(
|
||||
ERROR_CODES,
|
||||
[400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504]
|
||||
|
||||
+22
-21
@@ -97,7 +97,7 @@ fn enforce_local_page_no_cache(response: &mut Response) {
|
||||
}
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
}
|
||||
|
||||
fn error(status: StatusCode, detail: &'static str) -> Response {
|
||||
@@ -108,12 +108,10 @@ fn error(status: StatusCode, detail: &'static str) -> Response {
|
||||
);
|
||||
let mut response = if let Some(html) = crate::pages::builtin_gateway_error(status) {
|
||||
let mut response = (status, html).into_response();
|
||||
response.headers_mut().insert(
|
||||
"content-type",
|
||||
response.headers_mut().insert(::http::header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"x-content-type-options",
|
||||
response.headers_mut().insert(::http::header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
response
|
||||
@@ -127,9 +125,8 @@ fn error(status: StatusCode, detail: &'static str) -> Response {
|
||||
};
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
response.headers_mut().insert(
|
||||
"x-content-type-options",
|
||||
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
response.headers_mut().insert(::http::header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
response.extensions_mut().insert(LocalGeneratedResponse);
|
||||
@@ -145,7 +142,7 @@ fn method_policy_denial(policy: &crate::vhost::MethodPolicy, method: &Method) ->
|
||||
"RustProxy: HTTP method denied by access policy\n",
|
||||
);
|
||||
if let Some(value) = policy.allow_header() {
|
||||
response.headers_mut().insert(http::header::ALLOW, value);
|
||||
response.headers_mut().insert(::http::header::ALLOW, value);
|
||||
}
|
||||
Some(response)
|
||||
}
|
||||
@@ -162,7 +159,7 @@ fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Metho
|
||||
})
|
||||
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, Body::empty()).into_response());
|
||||
if let Ok(value) = HeaderValue::from_str(&spec.content_type) {
|
||||
response.headers_mut().insert("content-type", value);
|
||||
response.headers_mut().insert(::http::header::CONTENT_TYPE, value);
|
||||
}
|
||||
response
|
||||
}
|
||||
@@ -283,8 +280,7 @@ fn finish_headers(
|
||||
.unwrap_or(&snapshot.envelope.config.diagnostic_headers)
|
||||
.apply(response.headers_mut(), request_id);
|
||||
if frontend.is_some_and(|f| f.auth_provider.is_some() || f.basic_auth.is_some()) {
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
response.headers_mut().insert(::http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("private, no-store"),
|
||||
);
|
||||
}
|
||||
@@ -330,14 +326,21 @@ fn apply_backend_diagnostic_values(
|
||||
backend_header: bool,
|
||||
backend_timing_header: bool,
|
||||
) {
|
||||
response.headers_mut().remove("x-rustproxy-backend");
|
||||
response.headers_mut().remove("x-rust-proxy-backend");
|
||||
response.headers_mut().remove("x-rust-proxy-backend-time");
|
||||
response.headers_mut().remove("x-rustproxy-backend-time");
|
||||
const BACKEND: ::http::HeaderName = ::http::HeaderName::from_static("x-rustproxy-backend");
|
||||
const LEGACY_BACKEND: ::http::HeaderName = ::http::HeaderName::from_static("x-rust-proxy-backend");
|
||||
const LEGACY_BACKEND_TIME: ::http::HeaderName =
|
||||
::http::HeaderName::from_static("x-rust-proxy-backend-time");
|
||||
const BACKEND_TIME: ::http::HeaderName = ::http::HeaderName::from_static("x-rustproxy-backend-time");
|
||||
let headers = response.headers_mut();
|
||||
for name in [&BACKEND, &LEGACY_BACKEND, &LEGACY_BACKEND_TIME, &BACKEND_TIME] {
|
||||
if headers.contains_key(name) {
|
||||
headers.remove(name);
|
||||
}
|
||||
}
|
||||
if backend_header {
|
||||
if let Some(origin) = response.extensions().get::<OriginBackend>() {
|
||||
if let Ok(value) = HeaderValue::from_str(&origin.0) {
|
||||
response.headers_mut().insert("x-rust-proxy-backend", value);
|
||||
response.headers_mut().insert(LEGACY_BACKEND, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,9 +348,7 @@ fn apply_backend_diagnostic_values(
|
||||
if let Some(origin) = response.extensions().get::<OriginTiming>() {
|
||||
let value = format!("{:.2}ms", origin.0.as_secs_f64() * 1000.0);
|
||||
if let Ok(value) = HeaderValue::from_str(&value) {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("x-rustproxy-backend-time", value);
|
||||
response.headers_mut().insert(BACKEND_TIME, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,7 +417,7 @@ async fn normalize_h3_h1_unknown_body(
|
||||
if request.version() != Version::HTTP_3
|
||||
|| upstream_version != Version::HTTP_11
|
||||
|| method == Method::CONNECT
|
||||
|| request.headers().contains_key(http::header::CONTENT_LENGTH)
|
||||
|| request.headers().contains_key(::http::header::CONTENT_LENGTH)
|
||||
|| request.body().size_hint().exact().is_some()
|
||||
{
|
||||
return Ok(());
|
||||
|
||||
@@ -11,17 +11,17 @@ fn cache_http_invalidation_request(
|
||||
) -> Result<crate::cache::InvalidationRequest, &'static str> {
|
||||
let mut lengths = request
|
||||
.headers()
|
||||
.get_all(http::header::CONTENT_LENGTH)
|
||||
.get_all(::http::header::CONTENT_LENGTH)
|
||||
.iter();
|
||||
if let Some(length) = lengths.next() {
|
||||
if lengths.next().is_some() || length.as_bytes() != b"0" {
|
||||
return Err("BAN/PURGE must not contain a request body");
|
||||
}
|
||||
}
|
||||
if request.headers().contains_key("upgrade")
|
||||
if request.headers().contains_key(::http::header::UPGRADE)
|
||||
|| request
|
||||
.headers()
|
||||
.contains_key(http::header::TRANSFER_ENCODING)
|
||||
.contains_key(::http::header::TRANSFER_ENCODING)
|
||||
|| !request.body().is_end_stream()
|
||||
{
|
||||
return Err("BAN/PURGE must not contain a request body or upgrade");
|
||||
@@ -207,6 +207,6 @@ async fn cache_http_invalidation(
|
||||
let mut response = (code, axum::Json(serde_json::json!({"result": result}))).into_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
response
|
||||
}
|
||||
@@ -90,7 +90,7 @@ pub(super) fn range_selection(
|
||||
) -> crate::cache::RangeSelection {
|
||||
if cache_eligible {
|
||||
crate::cache::request_range(method, headers)
|
||||
} else if headers.contains_key("range") {
|
||||
} else if headers.contains_key(::http::header::RANGE) {
|
||||
crate::cache::RangeSelection::Unsupported
|
||||
} else {
|
||||
crate::cache::RangeSelection::None
|
||||
|
||||
@@ -207,7 +207,7 @@ if only_cached {
|
||||
"RustProxy: no reusable cached response\n",
|
||||
);
|
||||
cache_settings.annotate(response.headers_mut(), "MISS", 0, 0);
|
||||
response.headers_mut().remove("cache-status");
|
||||
response.headers_mut().remove(::http::header::CACHE_STATUS);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,11 +85,11 @@
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
if status == StatusCode::SERVICE_UNAVAILABLE {
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("retry-after", HeaderValue::from_static("1"));
|
||||
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ if let Some(spec) = &route.redirect {
|
||||
};
|
||||
let status = StatusCode::from_u16(spec.status).unwrap_or(StatusCode::FOUND);
|
||||
let mut response = status.into_response();
|
||||
response.headers_mut().insert(http::header::LOCATION, location);
|
||||
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
response.headers_mut().insert(::http::header::LOCATION, location);
|
||||
response.headers_mut().insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
return response;
|
||||
}
|
||||
if route.unavailable {
|
||||
@@ -33,7 +33,7 @@ if route.unavailable {
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("retry-after", HeaderValue::from_static("1"));
|
||||
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
|
||||
if let Some(fallback) = execute_error_fallback(
|
||||
state.clone(), peer, local, &snapshot, &host, tls, route,
|
||||
response.status(), &fallback_request, &fallback_body,
|
||||
@@ -61,7 +61,7 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("retry-after", HeaderValue::from_static("1"));
|
||||
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
|
||||
if let Some(fallback) = execute_error_fallback(
|
||||
state.clone(), peer, local, &snapshot, &host, tls, route,
|
||||
response.status(), &fallback_request, &fallback_body,
|
||||
@@ -77,7 +77,7 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("retry-after", HeaderValue::from_static("1"));
|
||||
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
|
||||
if let Some(fallback) = execute_error_fallback(
|
||||
state.clone(), peer, local, &snapshot, &host, tls, route,
|
||||
response.status(), &fallback_request, &fallback_body,
|
||||
@@ -253,7 +253,7 @@ if let Some(identity) = identity {
|
||||
if websocket_legacy_support {
|
||||
if let Some(upstream_host) = request.headers().get(HOST).cloned() {
|
||||
if let Some(origin) = legacy_websocket_origin(&backend.request_origin, &upstream_host) {
|
||||
request.headers_mut().insert("origin", origin);
|
||||
request.headers_mut().insert(::http::header::ORIGIN, origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,8 +263,8 @@ let effective_policy = cache_plan::combine_policy(original_policy, || {
|
||||
request.uri().query().is_some(), cache_settings,
|
||||
)
|
||||
});
|
||||
let force_vary_cookie = force_cache && request.headers().contains_key("cookie");
|
||||
let force_vary_authorization = force_cache && request.headers().contains_key("authorization");
|
||||
let force_vary_cookie = force_cache && request.headers().contains_key(::http::header::COOKIE);
|
||||
let force_vary_authorization = force_cache && request.headers().contains_key(::http::header::AUTHORIZATION);
|
||||
let effective_reason = effective_policy.bypass_reason();
|
||||
let revalidation_reason = effective_policy.revalidation_reason();
|
||||
let effective_cacheable = effective_reason.is_none();
|
||||
@@ -272,15 +272,15 @@ let range_selection = cache_plan::range_selection(
|
||||
effective_cacheable, request.method(), request.headers(),
|
||||
);
|
||||
let if_range = if effective_cacheable {
|
||||
request.headers().get("if-range").cloned()
|
||||
request.headers().get(::http::header::IF_RANGE).cloned()
|
||||
} else { None };
|
||||
let range_requested = !matches!(range_selection, crate::cache::RangeSelection::None);
|
||||
|
||||
let cache_route = route.id.as_str();
|
||||
let mut cache_key_headers = None;
|
||||
if effective_cacheable && ignore_static_cookie && request.headers().contains_key("cookie") {
|
||||
if effective_cacheable && ignore_static_cookie && request.headers().contains_key(::http::header::COOKIE) {
|
||||
let mut headers = request.headers().clone();
|
||||
headers.remove("cookie");
|
||||
headers.remove(::http::header::COOKIE);
|
||||
cache_key_headers = Some(headers);
|
||||
}
|
||||
let key_headers = cache_key_headers
|
||||
|
||||
@@ -135,6 +135,11 @@ if let Some(exchange) = &exchange {
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = reason;
|
||||
}
|
||||
if fill_leader.is_some() && (origin_plan.is_none() || transformed_plan.is_none()) {
|
||||
if let Some(ticket) = cache_ticket.as_ref() {
|
||||
state.cache.remember_uncacheable(ticket);
|
||||
}
|
||||
}
|
||||
if let (Some(ticket), Some(plan), Some(transformed)) =
|
||||
(cache_ticket, origin_plan, transformed_plan)
|
||||
{
|
||||
|
||||
@@ -137,8 +137,8 @@ let origin_reason = (cache_ticket.is_some() && origin_plan.is_none()).then(|| {
|
||||
let origin_no_transform = crate::transform::no_transform(response.headers());
|
||||
let skip_compression = crate::compression::no_transform(response.headers());
|
||||
let origin_validators = crate::transform::OriginValidators {
|
||||
etag: response.headers().get("etag").cloned(),
|
||||
last_modified: response.headers().get("last-modified").cloned(),
|
||||
etag: response.headers().get(::http::header::ETAG).cloned(),
|
||||
last_modified: response.headers().get(::http::header::LAST_MODIFIED).cloned(),
|
||||
};
|
||||
let (mut parts, body) = response.into_parts();
|
||||
parts.extensions.insert(origin_validators);
|
||||
@@ -147,8 +147,8 @@ if skip_compression || range_requested {
|
||||
}
|
||||
headers::strip_hop_by_hop(&mut parts.headers);
|
||||
if websocket_legacy_support && handshake.is_some() {
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove("transfer-encoding");
|
||||
parts.headers.remove(::http::header::CONTENT_LENGTH);
|
||||
parts.headers.remove(::http::header::TRANSFER_ENCODING);
|
||||
}
|
||||
apply_response_rules(
|
||||
&mut parts.headers,
|
||||
@@ -161,7 +161,7 @@ if let Some(pool) = &route.pool {
|
||||
pool.strip_affinity_response(&mut parts.headers);
|
||||
}
|
||||
if let Some(cookie) = active_affinity_cookie {
|
||||
parts.headers.append("set-cookie", cookie);
|
||||
parts.headers.append(::http::header::SET_COOKIE, cookie);
|
||||
}
|
||||
let h3_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h3())
|
||||
&& upgraded.is_some();
|
||||
@@ -208,9 +208,9 @@ let mut response = if h3_tunnel {
|
||||
};
|
||||
exchange.websocket_phase("tunnel_ready");
|
||||
parts.status = StatusCode::OK;
|
||||
parts.headers.remove("sec-websocket-accept");
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove("transfer-encoding");
|
||||
parts.headers.remove(::http::header::SEC_WEBSOCKET_ACCEPT);
|
||||
parts.headers.remove(::http::header::CONTENT_LENGTH);
|
||||
parts.headers.remove(::http::header::TRANSFER_ENCODING);
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
parts
|
||||
.extensions
|
||||
@@ -301,8 +301,8 @@ let mut response = if h3_tunnel {
|
||||
"RustProxy: HTTP/2 WebSocket tunnel state unavailable\n",
|
||||
);
|
||||
};
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove("transfer-encoding");
|
||||
parts.headers.remove(::http::header::CONTENT_LENGTH);
|
||||
parts.headers.remove(::http::header::TRANSFER_ENCODING);
|
||||
parts.extensions.insert(crate::compression::Skip);
|
||||
Response::from_parts(
|
||||
parts,
|
||||
@@ -314,10 +314,10 @@ let mut response = if h3_tunnel {
|
||||
{
|
||||
parts
|
||||
.headers
|
||||
.insert("connection", HeaderValue::from_static("Upgrade"));
|
||||
.insert(::http::header::CONNECTION, HeaderValue::from_static("Upgrade"));
|
||||
parts
|
||||
.headers
|
||||
.insert("upgrade", HeaderValue::from_static("websocket"));
|
||||
.insert(::http::header::UPGRADE, HeaderValue::from_static("websocket"));
|
||||
websocket::spawn(
|
||||
state.clone(),
|
||||
downstream,
|
||||
@@ -342,8 +342,7 @@ if intercept_origin_error {
|
||||
Some(route),
|
||||
admin_control_plane,
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
response.headers_mut().insert(::http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-store"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,27 +4,27 @@ let websocket_upstream_h2 = !websocket_legacy_support
|
||||
&& matches!(backend.protocol,
|
||||
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2);
|
||||
if let Some(handshake) = &handshake {
|
||||
request.headers_mut().remove("content-length");
|
||||
request.headers_mut().remove("transfer-encoding");
|
||||
request.headers_mut().remove(::http::header::CONTENT_LENGTH);
|
||||
request.headers_mut().remove(::http::header::TRANSFER_ENCODING);
|
||||
if websocket_upstream_h2 {
|
||||
request.headers_mut().remove("connection");
|
||||
request.headers_mut().remove("upgrade");
|
||||
request.headers_mut().remove("sec-websocket-key");
|
||||
request.headers_mut().remove("sec-websocket-accept");
|
||||
request.headers_mut().remove(::http::header::CONNECTION);
|
||||
request.headers_mut().remove(::http::header::UPGRADE);
|
||||
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_KEY);
|
||||
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_ACCEPT);
|
||||
} else {
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("connection", HeaderValue::from_static("Upgrade"));
|
||||
.insert(::http::header::CONNECTION, HeaderValue::from_static("Upgrade"));
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("upgrade", HeaderValue::from_static("websocket"));
|
||||
.insert(::http::header::UPGRADE, HeaderValue::from_static("websocket"));
|
||||
if !handshake.h1() {
|
||||
*request.method_mut() = Method::GET;
|
||||
request.headers_mut().remove("sec-websocket-key");
|
||||
request.headers_mut().remove("sec-websocket-accept");
|
||||
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_KEY);
|
||||
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_ACCEPT);
|
||||
if let Some(key) = handshake.upstream_key() {
|
||||
if let Ok(key) = HeaderValue::from_str(key) {
|
||||
request.headers_mut().insert("sec-websocket-key", key);
|
||||
request.headers_mut().insert(::http::header::SEC_WEBSOCKET_KEY, key);
|
||||
} else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -39,7 +39,7 @@ if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
|
||||
auth.apply_upstream(request.headers_mut(), basic_identity);
|
||||
}
|
||||
if range_requested || transform_settings.is_some_and(|settings| settings.enabled) {
|
||||
request.headers_mut().remove("accept-encoding");
|
||||
request.headers_mut().remove(::http::header::ACCEPT_ENCODING);
|
||||
}
|
||||
let mut revalidation_sent = false;
|
||||
if pending_revalidation {
|
||||
@@ -61,10 +61,11 @@ let pool_retry_possible = route.backend_id.is_none()
|
||||
let h2_replay_possible = !admin_control_plane
|
||||
&& handshake.is_none()
|
||||
&& retry_safe_method(&retry_method)
|
||||
&& matches!(backend.protocol,
|
||||
crate::config::UpstreamProtocol::Http2
|
||||
| crate::config::UpstreamProtocol::H2
|
||||
| crate::config::UpstreamProtocol::Auto);
|
||||
&& match backend.protocol {
|
||||
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2 => true,
|
||||
crate::config::UpstreamProtocol::Auto => !backend.origin.starts_with("http://"),
|
||||
_ => false,
|
||||
};
|
||||
let replay_headers = ((pool_retry_possible || h2_replay_possible)
|
||||
&& (request.body().size_hint().exact() == Some(0)
|
||||
|| request.version() == Version::HTTP_3))
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
.get::<crate::server::sendfile::SendfileSlots>().cloned();
|
||||
#[cfg(target_os = "linux")]
|
||||
let identity_request = request.version() == http::Version::HTTP_11
|
||||
&& !request.headers().contains_key("accept-encoding")
|
||||
&& !request.headers().contains_key("range")
|
||||
&& !request.headers().contains_key("if-range");
|
||||
&& !request.headers().contains_key(::http::header::ACCEPT_ENCODING)
|
||||
&& !request.headers().contains_key(::http::header::RANGE)
|
||||
&& !request.headers().contains_key(::http::header::IF_RANGE);
|
||||
let cache_settings = route.frontend.as_ref()
|
||||
.map(|frontend| &frontend.cache)
|
||||
.unwrap_or(&snapshot.envelope.config.cache);
|
||||
@@ -54,9 +54,9 @@
|
||||
request_uri.query().is_some(), cache_settings,
|
||||
);
|
||||
let eligible = enabled && policy.bypass_reason().is_none();
|
||||
let key_headers = if eligible && ignore_cookie && request_headers.contains_key("cookie") {
|
||||
let key_headers = if eligible && ignore_cookie && request_headers.contains_key(::http::header::COOKIE) {
|
||||
let mut headers = request_headers.clone();
|
||||
headers.remove("cookie");
|
||||
headers.remove(::http::header::COOKIE);
|
||||
Some(headers)
|
||||
} else { None };
|
||||
let cache_ticket = if eligible {
|
||||
@@ -67,7 +67,7 @@
|
||||
.map(|ticket| ticket.with_varnish_compatible(cache_settings.varnish_compatible))
|
||||
} else { None };
|
||||
let range = cache_plan::range_selection(eligible, &request_method, &request_headers);
|
||||
let if_range = if eligible { request_headers.get("if-range").cloned() } else { None };
|
||||
let if_range = if eligible { request_headers.get(::http::header::IF_RANGE).cloned() } else { None };
|
||||
let range_requested = !matches!(range, crate::cache::RangeSelection::None);
|
||||
let cached_revalidation = if let Some(ticket) = cache_ticket.as_ref() {
|
||||
state.cache.revalidation_tiered(ticket).await
|
||||
@@ -181,18 +181,18 @@
|
||||
}
|
||||
let origin_headers = response.headers();
|
||||
let origin_safe = cache_ticket.is_some() && {
|
||||
let explicit_origin_policy = origin_headers.contains_key("cache-control")
|
||||
|| origin_headers.contains_key("expires")
|
||||
|| origin_headers.contains_key("set-cookie")
|
||||
|| origin_headers.contains_key("vary");
|
||||
let explicit_origin_policy = origin_headers.contains_key(::http::header::CACHE_CONTROL)
|
||||
|| origin_headers.contains_key(::http::header::EXPIRES)
|
||||
|| origin_headers.contains_key(::http::header::SET_COOKIE)
|
||||
|| origin_headers.contains_key(::http::header::VARY);
|
||||
!explicit_origin_policy || crate::cache::plan(response.status(), origin_headers,
|
||||
cache_settings, started.elapsed()).is_some()
|
||||
};
|
||||
let no_transform = crate::transform::no_transform(origin_headers);
|
||||
let skip_compression = crate::compression::no_transform(origin_headers);
|
||||
let origin_validators = crate::transform::OriginValidators {
|
||||
etag: origin_headers.get("etag").cloned(),
|
||||
last_modified: origin_headers.get("last-modified").cloned(),
|
||||
etag: origin_headers.get(::http::header::ETAG).cloned(),
|
||||
last_modified: origin_headers.get(::http::header::LAST_MODIFIED).cloned(),
|
||||
};
|
||||
if skip_compression || range_requested {
|
||||
response.extensions_mut().insert(crate::compression::Skip);
|
||||
@@ -201,7 +201,7 @@
|
||||
apply_response_rules(response.headers_mut(), &snapshot, route.frontend.as_deref(),
|
||||
Some(route), false);
|
||||
if response.extensions().get::<crate::pages::PageServed>().is_some() {
|
||||
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
response.headers_mut().insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
}
|
||||
response.extensions_mut().insert(ResponseRulesApplied);
|
||||
response.extensions_mut().insert(StaticSourceResponse);
|
||||
@@ -250,8 +250,8 @@
|
||||
let mut plan = if canonical { origin.without_vary("accept-encoding") }
|
||||
else { origin };
|
||||
if force_cache {
|
||||
if request_headers.contains_key("cookie") { plan = plan.with_vary("cookie"); }
|
||||
if request_headers.contains_key("authorization") { plan = plan.with_vary("authorization"); }
|
||||
if request_headers.contains_key(::http::header::COOKIE) { plan = plan.with_vary("cookie"); }
|
||||
if request_headers.contains_key(::http::header::AUTHORIZATION) { plan = plan.with_vary("authorization"); }
|
||||
}
|
||||
response = state.cache.wrap_with_fill(response, ticket,
|
||||
plan.restrict(transformed), cache_settings, None);
|
||||
@@ -278,15 +278,15 @@
|
||||
if !enabled && identity_request && request_method == Method::GET
|
||||
&& !transform_settings.is_some_and(|settings| settings.enabled)
|
||||
&& response.status() == StatusCode::OK
|
||||
&& !response.headers().contains_key("content-encoding")
|
||||
&& !response.headers().contains_key("transfer-encoding")
|
||||
&& !response.headers().contains_key("content-range")
|
||||
&& !response.headers().contains_key(::http::header::CONTENT_ENCODING)
|
||||
&& !response.headers().contains_key(::http::header::TRANSFER_ENCODING)
|
||||
&& !response.headers().contains_key(::http::header::CONTENT_RANGE)
|
||||
{
|
||||
if let (Some(slots), Some(candidate)) = (
|
||||
kernel_sendfile,
|
||||
response.extensions().get::<crate::server::sendfile::SendfileCandidate>().cloned(),
|
||||
) {
|
||||
let correct_length = response.headers().get("content-length")
|
||||
let correct_length = response.headers().get(::http::header::CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok()) == Some(candidate.length);
|
||||
if correct_length {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
async fn forward(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(peer): Extension<PeerAddr>,
|
||||
Extension(local): Extension<LocalAddr>,
|
||||
state: Arc<App>,
|
||||
peer: PeerAddr,
|
||||
local: LocalAddr,
|
||||
mut request: Request,
|
||||
) -> Response {
|
||||
let cookies = crate::auth_provider::CookieSink::default();
|
||||
@@ -279,7 +279,7 @@ async fn forward_request(
|
||||
country.as_deref(),
|
||||
request
|
||||
.headers()
|
||||
.get(http::header::USER_AGENT)
|
||||
.get(::http::header::USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
&snapshot.global_security.rate_limit,
|
||||
) {
|
||||
@@ -288,7 +288,7 @@ async fn forward_request(
|
||||
"RustProxy: global request rate limit exceeded\n",
|
||||
);
|
||||
if let Ok(value) = HeaderValue::from_str(&limited.retry_after_seconds.to_string()) {
|
||||
response.headers_mut().insert("retry-after", value);
|
||||
response.headers_mut().insert(::http::header::RETRY_AFTER, value);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -335,7 +335,7 @@ async fn forward_request(
|
||||
country.as_deref(),
|
||||
request
|
||||
.headers()
|
||||
.get(http::header::USER_AGENT)
|
||||
.get(::http::header::USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
&frontend.rate_limit,
|
||||
) {
|
||||
@@ -344,7 +344,7 @@ async fn forward_request(
|
||||
"RustProxy: request rate limit exceeded\n",
|
||||
);
|
||||
if let Ok(value) = HeaderValue::from_str(&limited.retry_after_seconds.to_string()) {
|
||||
response.headers_mut().insert("retry-after", value);
|
||||
response.headers_mut().insert(::http::header::RETRY_AFTER, value);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
+23
-21
@@ -27,22 +27,24 @@ pub fn strip_hop_by_hop(headers: &mut HeaderMap) {
|
||||
headers.remove(name);
|
||||
}
|
||||
}
|
||||
const KEEP_ALIVE: HeaderName = HeaderName::from_static("keep-alive");
|
||||
const PROXY_CONNECTION: HeaderName = HeaderName::from_static("proxy-connection");
|
||||
for name in [
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-connection",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"trailer",
|
||||
"te",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
CONNECTION,
|
||||
KEEP_ALIVE,
|
||||
PROXY_CONNECTION,
|
||||
http::header::TRANSFER_ENCODING,
|
||||
http::header::UPGRADE,
|
||||
http::header::TRAILER,
|
||||
http::header::TE,
|
||||
http::header::PROXY_AUTHENTICATE,
|
||||
http::header::PROXY_AUTHORIZATION,
|
||||
] {
|
||||
headers.remove(name);
|
||||
}
|
||||
}
|
||||
pub fn requests_upgrade(headers: &HeaderMap) -> bool {
|
||||
if headers.contains_key("upgrade") {
|
||||
if headers.contains_key(::http::header::UPGRADE) {
|
||||
return true;
|
||||
}
|
||||
let mut connection = headers.get_all(CONNECTION).iter();
|
||||
@@ -67,13 +69,13 @@ pub fn requests_upgrade(headers: &HeaderMap) -> bool {
|
||||
include!("tests.rs");
|
||||
|
||||
pub fn strip_admin_cookie(headers: &mut HeaderMap) -> Vec<String> {
|
||||
if !headers.contains_key(http::header::COOKIE) {
|
||||
if !headers.contains_key(::http::header::COOKIE) {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut retained = Vec::new();
|
||||
let mut admin = Vec::new();
|
||||
for part in headers
|
||||
.get_all(http::header::COOKIE)
|
||||
.get_all(::http::header::COOKIE)
|
||||
.iter()
|
||||
.filter_map(|h| h.to_str().ok())
|
||||
.flat_map(|h| h.split(';'))
|
||||
@@ -88,10 +90,10 @@ pub fn strip_admin_cookie(headers: &mut HeaderMap) -> Vec<String> {
|
||||
retained.push(part.to_owned());
|
||||
}
|
||||
}
|
||||
headers.remove(http::header::COOKIE);
|
||||
headers.remove(::http::header::COOKIE);
|
||||
if !retained.is_empty() {
|
||||
if let Ok(value) = retained.join("; ").parse() {
|
||||
headers.insert(http::header::COOKIE, value);
|
||||
headers.insert(::http::header::COOKIE, value);
|
||||
}
|
||||
}
|
||||
admin
|
||||
@@ -102,22 +104,22 @@ pub fn restore_admin_cookie(headers: &mut HeaderMap, admin: &[String]) {
|
||||
return;
|
||||
}
|
||||
let mut values: Vec<String> = headers
|
||||
.get_all(http::header::COOKIE)
|
||||
.get_all(::http::header::COOKIE)
|
||||
.iter()
|
||||
.filter_map(|h| h.to_str().ok().map(str::to_owned))
|
||||
.collect();
|
||||
values.extend(admin.iter().cloned());
|
||||
headers.remove(http::header::COOKIE);
|
||||
headers.remove(::http::header::COOKIE);
|
||||
if let Ok(value) = values.join("; ").parse() {
|
||||
headers.insert(http::header::COOKIE, value);
|
||||
headers.insert(::http::header::COOKIE, value);
|
||||
}
|
||||
}
|
||||
pub fn strip_admin_set_cookie(headers: &mut HeaderMap) {
|
||||
if !headers.contains_key(http::header::SET_COOKIE) {
|
||||
if !headers.contains_key(::http::header::SET_COOKIE) {
|
||||
return;
|
||||
}
|
||||
let retained: Vec<_> = headers
|
||||
.get_all(http::header::SET_COOKIE)
|
||||
.get_all(::http::header::SET_COOKIE)
|
||||
.iter()
|
||||
.filter(|h| {
|
||||
h.to_str()
|
||||
@@ -127,8 +129,8 @@ pub fn strip_admin_set_cookie(headers: &mut HeaderMap) {
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
headers.remove(http::header::SET_COOKIE);
|
||||
headers.remove(::http::header::SET_COOKIE);
|
||||
for value in retained {
|
||||
headers.append(http::header::SET_COOKIE, value);
|
||||
headers.append(::http::header::SET_COOKIE, value);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ fn canonical_host_redirect(
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
Some(response)
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ fn force_ssl_redirect(
|
||||
let mut response = StatusCode::PERMANENT_REDIRECT.into_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(http::header::LOCATION, location);
|
||||
.insert(::http::header::LOCATION, location);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
Some(response)
|
||||
}
|
||||
+21
-13
@@ -9,9 +9,8 @@ use crate::{
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Request, State},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Router,
|
||||
Router,
|
||||
};
|
||||
use body::{Direction, Exchange, ExchangeSource, MeteredBody};
|
||||
use bytes::Bytes;
|
||||
@@ -49,14 +48,23 @@ use std::{
|
||||
};
|
||||
|
||||
pub fn router(state: Arc<App>) -> Router {
|
||||
Router::new()
|
||||
.fallback(forward)
|
||||
.layer(middleware::from_fn_with_state(state.clone(), observe))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
crate::acme::challenge,
|
||||
))
|
||||
.with_state(state)
|
||||
Router::new().fallback(entry).with_state(state)
|
||||
}
|
||||
|
||||
async fn entry(State(state): State<Arc<App>>, request: Request) -> Response {
|
||||
if crate::acme::is_challenge_path(request.uri().path()) {
|
||||
return crate::acme::challenge(state, request).await;
|
||||
}
|
||||
observe(state, request).await
|
||||
}
|
||||
|
||||
async fn dispatch(state: Arc<App>, request: Request) -> Response {
|
||||
let peer = request.extensions().get::<PeerAddr>().copied();
|
||||
let local = request.extensions().get::<LocalAddr>().copied();
|
||||
let (Some(peer), Some(local)) = (peer, local) else {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Missing request extension").into_response();
|
||||
};
|
||||
forward(state, peer, local, request).await
|
||||
}
|
||||
const MAX_UPSTREAM_ERROR_DETAIL: usize = 2048;
|
||||
|
||||
@@ -320,7 +328,7 @@ async fn refresh_cache_once(
|
||||
.as_ref()
|
||||
.is_some_and(|settings| settings.enabled)
|
||||
{
|
||||
refresh_headers.remove("accept-encoding");
|
||||
refresh_headers.remove(::http::header::ACCEPT_ENCODING);
|
||||
}
|
||||
let mut refresh = Request::new(Body::empty());
|
||||
*refresh.method_mut() = Method::GET;
|
||||
@@ -382,8 +390,8 @@ async fn refresh_cache_once(
|
||||
let origin_no_transform = crate::transform::no_transform(origin.headers());
|
||||
let skip_compression = crate::compression::no_transform(origin.headers());
|
||||
let origin_validators = crate::transform::OriginValidators {
|
||||
etag: origin.headers().get("etag").cloned(),
|
||||
last_modified: origin.headers().get("last-modified").cloned(),
|
||||
etag: origin.headers().get(::http::header::ETAG).cloned(),
|
||||
last_modified: origin.headers().get(::http::header::LAST_MODIFIED).cloned(),
|
||||
};
|
||||
let (mut parts, body) = origin.into_parts();
|
||||
parts.extensions.insert(origin_validators);
|
||||
|
||||
@@ -41,7 +41,7 @@ fn should_replace_error_page(response: &Response, minimal_welcome: bool) -> bool
|
||||
.is_none()
|
||||
}
|
||||
|
||||
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
|
||||
async fn observe(state: Arc<App>, mut request: Request) -> Response {
|
||||
let snapshot = state.config.load_full();
|
||||
let selected = response_context(&snapshot, &request);
|
||||
let frontend_context = selected.frontend.clone();
|
||||
@@ -65,7 +65,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
let is_head = method == Method::HEAD;
|
||||
let ws_attempt = request
|
||||
.headers()
|
||||
.get("upgrade")
|
||||
.get(::http::header::UPGRADE)
|
||||
.is_some_and(|v| v.as_bytes().eq_ignore_ascii_case(b"websocket"))
|
||||
|| (request.version() == Version::HTTP_2
|
||||
&& request.method() == Method::CONNECT
|
||||
@@ -148,10 +148,14 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.unwrap_or_else(|| request.uri().authority().map(|v| v.as_str()).unwrap_or("")),
|
||||
253,
|
||||
);
|
||||
let initial_client_ip = request
|
||||
.extensions()
|
||||
.get::<PeerAddr>()
|
||||
.map(|peer| peer.0.ip().to_string());
|
||||
let initial_client_ip = request.extensions().get::<PeerAddr>().map(|peer| {
|
||||
request
|
||||
.extensions()
|
||||
.get::<Arc<crate::forwarding::PeerFormatting>>()
|
||||
.and_then(|formatting| formatting.raw_peer_text())
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| peer.0.ip().to_string())
|
||||
});
|
||||
let user_agent = state
|
||||
.logs
|
||||
.capture_user_agent()
|
||||
@@ -221,7 +225,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.and_then(|frontend| frontend.proxy_timeouts.send_timeout())
|
||||
.unwrap_or(state.body_idle_timeout);
|
||||
let request = request.map(|body| wrap_observed_request(body, &exchange, request_send_timeout));
|
||||
let mut response = next.run(request).await;
|
||||
let mut response = dispatch(state.clone(), request).await;
|
||||
let final_route_context = response
|
||||
.extensions()
|
||||
.get::<FinalRouteContext>()
|
||||
@@ -277,7 +281,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
}
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
} else {
|
||||
if response
|
||||
.extensions()
|
||||
|
||||
@@ -17,8 +17,8 @@ pub(super) fn web_origin_request_headers(
|
||||
let Some(cached) = cached else {
|
||||
return (headers, false);
|
||||
};
|
||||
headers.remove("if-none-match");
|
||||
headers.remove("if-modified-since");
|
||||
headers.remove(::http::header::IF_NONE_MATCH);
|
||||
headers.remove(::http::header::IF_MODIFIED_SINCE);
|
||||
let sent = cached.apply_validators(&mut headers);
|
||||
(headers, sent)
|
||||
}
|
||||
@@ -62,17 +62,20 @@ pub(super) fn finish_web_revalidation(
|
||||
let mut merged = origin_merged;
|
||||
apply_rules(&mut merged);
|
||||
if force_cache {
|
||||
if client.contains_key("cookie") {
|
||||
merged.append("vary", HeaderValue::from_static("Cookie"));
|
||||
if client.contains_key(::http::header::COOKIE) {
|
||||
merged.append(::http::header::VARY, HeaderValue::from_static("Cookie"));
|
||||
}
|
||||
if client.contains_key("authorization") {
|
||||
merged.append("vary", HeaderValue::from_static("Authorization"));
|
||||
if client.contains_key(::http::header::AUTHORIZATION) {
|
||||
merged.append(
|
||||
::http::header::VARY,
|
||||
HeaderValue::from_static("Authorization"),
|
||||
);
|
||||
}
|
||||
}
|
||||
let can_store = origin_safe && !(ignore_cookie && crate::cache::varies_on(&merged, "cookie"));
|
||||
let client_not_modified = crate::cache::conditional_not_modified(client, &merged);
|
||||
let range = crate::cache::request_range(method, client);
|
||||
let if_range = client.get("if-range");
|
||||
let if_range = client.get(::http::header::IF_RANGE);
|
||||
let published = if can_store {
|
||||
cache.publish_revalidated(
|
||||
ticket.clone(),
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Handshake {
|
||||
|
||||
fn protocols(headers: &HeaderMap) -> Result<Vec<String>> {
|
||||
let protocols = headers
|
||||
.get_all("sec-websocket-protocol")
|
||||
.get_all(::http::header::SEC_WEBSOCKET_PROTOCOL)
|
||||
.iter()
|
||||
.map(|v| v.to_str())
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?
|
||||
@@ -123,18 +123,18 @@ pub fn validate(request: &Request) -> Result<Handshake> {
|
||||
"invalid WebSocket key"
|
||||
);
|
||||
ensure!(
|
||||
!request.headers().contains_key("transfer-encoding"),
|
||||
!request.headers().contains_key(::http::header::TRANSFER_ENCODING),
|
||||
"WebSocket handshake cannot contain a transfer body"
|
||||
);
|
||||
ensure!(
|
||||
request.headers().get_all("content-length").iter().count() <= 1
|
||||
request.headers().get_all(::http::header::CONTENT_LENGTH).iter().count() <= 1
|
||||
&& request
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.get(::http::header::CONTENT_LENGTH)
|
||||
.is_none_or(|v| v == "0"),
|
||||
"WebSocket handshake cannot contain a body"
|
||||
);
|
||||
for value in request.headers().get_all("connection").iter() {
|
||||
for value in request.headers().get_all(::http::header::CONNECTION).iter() {
|
||||
for field in value.to_str()?.split(',') {
|
||||
ensure!(
|
||||
!field
|
||||
@@ -256,8 +256,8 @@ pub fn validate_response(
|
||||
has_token(headers, "upgrade", "websocket"),
|
||||
"invalid legacy upstream upgrade"
|
||||
);
|
||||
if headers.contains_key("content-length") {
|
||||
for value in headers.get_all("content-length").iter() {
|
||||
if headers.contains_key(::http::header::CONTENT_LENGTH) {
|
||||
for value in headers.get_all(::http::header::CONTENT_LENGTH).iter() {
|
||||
ensure!(
|
||||
value.to_str()?.trim() == "0",
|
||||
"legacy 101 response declared a non-zero body"
|
||||
@@ -271,17 +271,17 @@ pub fn validate_response(
|
||||
"invalid upstream upgrade"
|
||||
);
|
||||
ensure!(
|
||||
!headers.contains_key("transfer-encoding"),
|
||||
!headers.contains_key(::http::header::TRANSFER_ENCODING),
|
||||
"101 response must not use Transfer-Encoding"
|
||||
);
|
||||
ensure!(
|
||||
headers.get_all("content-length").iter().count() <= 1
|
||||
headers.get_all(::http::header::CONTENT_LENGTH).iter().count() <= 1
|
||||
&& headers
|
||||
.get("content-length")
|
||||
.get(::http::header::CONTENT_LENGTH)
|
||||
.is_none_or(|value| value.to_str().is_ok_and(|value| value.trim() == "0")),
|
||||
"101 response Content-Length must be 0 when present"
|
||||
);
|
||||
for value in headers.get_all("connection").iter() {
|
||||
for value in headers.get_all(::http::header::CONNECTION).iter() {
|
||||
for field in value.to_str()?.split(',') {
|
||||
ensure!(
|
||||
!field
|
||||
@@ -297,7 +297,7 @@ pub fn validate_response(
|
||||
single(headers, "sec-websocket-accept")? == expected.accept,
|
||||
"upstream WebSocket accept mismatch"
|
||||
);
|
||||
if headers.contains_key("sec-websocket-protocol") {
|
||||
if headers.contains_key(::http::header::SEC_WEBSOCKET_PROTOCOL) {
|
||||
let selected = single(headers, "sec-websocket-protocol")?;
|
||||
ensure!(
|
||||
expected.protocols.iter().any(|v| v == selected),
|
||||
@@ -321,7 +321,7 @@ pub fn validate_h2_response(headers: &HeaderMap, expected: &Handshake) -> Result
|
||||
"HTTP/1.1-only WebSocket field on HTTP/2 response"
|
||||
);
|
||||
}
|
||||
if headers.contains_key("sec-websocket-protocol") {
|
||||
if headers.contains_key(::http::header::SEC_WEBSOCKET_PROTOCOL) {
|
||||
let selected = single(headers, "sec-websocket-protocol")?;
|
||||
ensure!(
|
||||
expected.protocols.iter().any(|value| value == selected),
|
||||
|
||||
+2
-2
@@ -105,9 +105,9 @@ impl CompiledSecurity {
|
||||
headers.insert(*name, value.clone());
|
||||
}
|
||||
if !tls {
|
||||
headers.remove("strict-transport-security");
|
||||
headers.remove(::http::header::STRICT_TRANSPORT_SECURITY);
|
||||
} else if let Some(value) = &self.hsts {
|
||||
headers.insert("strict-transport-security", value.clone());
|
||||
headers.insert(::http::header::STRICT_TRANSPORT_SECURITY, value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,9 @@ fn h2c_attempt<B>(request: &Request<B>) -> bool {
|
||||
.filter_map(|value| value.to_str().ok())
|
||||
.flat_map(|value| value.split(','))
|
||||
.any(|value| value.trim().eq_ignore_ascii_case("h2c"))
|
||||
|| request.headers().contains_key("http2-settings")
|
||||
|| request
|
||||
.headers()
|
||||
.contains_key(::http::HeaderName::from_static("http2-settings"))
|
||||
}
|
||||
|
||||
fn hpack_integer(out: &mut Vec<u8>, value: usize, prefix_bits: u8, first: u8) {
|
||||
|
||||
+2
-2
@@ -451,11 +451,11 @@ impl ServerHeader {
|
||||
match self.mode {
|
||||
ServerMode::Preserve => {}
|
||||
ServerMode::Hide => {
|
||||
headers.remove("server");
|
||||
headers.remove(::http::header::SERVER);
|
||||
}
|
||||
ServerMode::Custom => {
|
||||
if let Ok(value) = self.value.parse() {
|
||||
headers.insert("server", value);
|
||||
headers.insert(::http::header::SERVER, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,7 @@ fn read_pem_with_policy(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
// Reject special files below without blocking while opening a FIFO.
|
||||
// O_NONBLOCK has no effect on ordinary certificate/key files.
|
||||
// O_NONBLOCK avoids hanging on a FIFO; special files are rejected below.
|
||||
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
|
||||
}
|
||||
let file = options
|
||||
|
||||
+35
-13
@@ -9,7 +9,8 @@ use std::{
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const QUEUE_CAPACITY: usize = 2048;
|
||||
const QUEUE_CAPACITY: usize = 8192;
|
||||
const DRAIN_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
|
||||
const TOP_CAPACITY: usize = 32;
|
||||
const MAX_VHOSTS: usize = 64;
|
||||
const MAX_LABEL_BYTES: usize = 256;
|
||||
@@ -228,22 +229,43 @@ impl Analytics {
|
||||
let (tx, mut rx) = mpsc::channel::<Sample>(QUEUE_CAPACITY);
|
||||
let worker_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut batch = Vec::with_capacity(64);
|
||||
// Drain on a short interval instead of waking once per request.
|
||||
let mut tick = tokio::time::interval(DRAIN_INTERVAL);
|
||||
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
let mut batch = Vec::with_capacity(256);
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.cancelled() => break,
|
||||
count = rx.recv_many(&mut batch, 64) => {
|
||||
if count == 0 { break; }
|
||||
let mut state = worker_state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for sample in batch.drain(..) {
|
||||
if let Some(counters) = state.vhosts.get_mut(sample.vhost.as_str()) {
|
||||
counters.record(sample);
|
||||
} else if state.vhosts.len() < MAX_VHOSTS {
|
||||
let name = sample.vhost.clone();
|
||||
let mut counters = Counters::default();
|
||||
counters.record(sample);
|
||||
state.vhosts.insert(name, counters);
|
||||
_ = tick.tick() => {
|
||||
let mut closed = false;
|
||||
loop {
|
||||
while batch.len() < 256 {
|
||||
match rx.try_recv() {
|
||||
Ok(sample) => batch.push(sample),
|
||||
Err(mpsc::error::TryRecvError::Empty) => break,
|
||||
Err(mpsc::error::TryRecvError::Disconnected) => {
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if batch.is_empty() {
|
||||
break;
|
||||
}
|
||||
let mut state = worker_state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for sample in batch.drain(..) {
|
||||
if let Some(counters) = state.vhosts.get_mut(sample.vhost.as_str()) {
|
||||
counters.record(sample);
|
||||
} else if state.vhosts.len() < MAX_VHOSTS {
|
||||
let name = sample.vhost.clone();
|
||||
let mut counters = Counters::default();
|
||||
counters.record(sample);
|
||||
state.vhosts.insert(name, counters);
|
||||
}
|
||||
}
|
||||
}
|
||||
if closed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-10
@@ -23,7 +23,7 @@ pub async fn apply(
|
||||
{
|
||||
return Ok(skip(response, None, "status_ineligible"));
|
||||
}
|
||||
if response.headers().contains_key("content-range") {
|
||||
if response.headers().contains_key(::http::header::CONTENT_RANGE) {
|
||||
return Ok(skip(response, None, "content_range"));
|
||||
}
|
||||
let Some(kind) =
|
||||
@@ -39,7 +39,7 @@ pub async fn apply(
|
||||
};
|
||||
let Some(content_length) = response
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.get(::http::header::CONTENT_LENGTH)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
else {
|
||||
@@ -121,18 +121,17 @@ pub async fn apply(
|
||||
.get::<OriginValidators>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| OriginValidators {
|
||||
etag: parts.headers.get("etag").cloned(),
|
||||
last_modified: parts.headers.get("last-modified").cloned(),
|
||||
etag: parts.headers.get(::http::header::ETAG).cloned(),
|
||||
last_modified: parts.headers.get(::http::header::LAST_MODIFIED).cloned(),
|
||||
});
|
||||
parts.headers.remove("content-encoding");
|
||||
parts.headers.remove("content-length");
|
||||
parts.headers.remove(::http::header::CONTENT_ENCODING);
|
||||
parts.headers.remove(::http::header::CONTENT_LENGTH);
|
||||
parts.headers.remove("content-md5");
|
||||
parts.headers.remove("digest");
|
||||
parts.headers.remove("content-digest");
|
||||
parts.headers.remove("accept-ranges");
|
||||
parts.headers.insert("etag", transformed_etag(&transformed));
|
||||
parts.headers.insert(
|
||||
"content-length",
|
||||
parts.headers.remove(::http::header::ACCEPT_RANGES);
|
||||
parts.headers.insert(::http::header::ETAG, transformed_etag(&transformed));
|
||||
parts.headers.insert(::http::header::CONTENT_LENGTH,
|
||||
HeaderValue::from_str(&transformed.len().to_string()).expect("integer header"),
|
||||
);
|
||||
drop_vary_accept_encoding(&mut parts.headers);
|
||||
|
||||
@@ -50,7 +50,7 @@ fn transformer(kind: Kind) -> &'static dyn Transformer {
|
||||
|
||||
pub fn no_transform(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get_all("cache-control")
|
||||
.get_all(::http::header::CACHE_CONTROL)
|
||||
.iter()
|
||||
.filter_map(|value| value.to_str().ok())
|
||||
.any(|value| {
|
||||
@@ -65,7 +65,7 @@ pub fn no_transform(headers: &HeaderMap) -> bool {
|
||||
}
|
||||
|
||||
fn content_type_kind(headers: &HeaderMap) -> Option<Kind> {
|
||||
let value = headers.get("content-type")?.to_str().ok()?;
|
||||
let value = headers.get(::http::header::CONTENT_TYPE)?.to_str().ok()?;
|
||||
let mime = value.split(';').next()?.trim().to_ascii_lowercase();
|
||||
match mime.as_str() {
|
||||
"application/javascript"
|
||||
@@ -118,7 +118,7 @@ fn kind_enabled(kind: Kind, settings: &Settings) -> bool {
|
||||
}
|
||||
|
||||
fn supported_encoding(headers: &HeaderMap) -> Option<Option<&'static str>> {
|
||||
let Some(value) = headers.get("content-encoding") else {
|
||||
let Some(value) = headers.get(::http::header::CONTENT_ENCODING) else {
|
||||
return Some(None);
|
||||
};
|
||||
let value = value.to_str().ok()?.trim();
|
||||
@@ -188,7 +188,7 @@ fn decode<'a>(body: &'a [u8], encoding: Option<&str>, limit: usize) -> Result<Co
|
||||
}
|
||||
|
||||
fn drop_vary_accept_encoding(headers: &mut HeaderMap) {
|
||||
let values: Vec<_> = headers.get_all("vary").iter().cloned().collect();
|
||||
let values: Vec<_> = headers.get_all(::http::header::VARY).iter().cloned().collect();
|
||||
if values.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -216,10 +216,10 @@ fn drop_vary_accept_encoding(headers: &mut HeaderMap) {
|
||||
}
|
||||
}
|
||||
}
|
||||
headers.remove("vary");
|
||||
headers.remove(::http::header::VARY);
|
||||
if !retained.is_empty() {
|
||||
if let Ok(value) = HeaderValue::from_str(&retained.join(", ")) {
|
||||
headers.insert("vary", value);
|
||||
headers.insert(::http::header::VARY, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ async fn php_response_inner(
|
||||
let mut response = builtin_error_response(StatusCode::SERVICE_UNAVAILABLE, &method);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("retry-after", HeaderValue::from_static("1"));
|
||||
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,6 +177,16 @@ async fn file_response_inner(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(unix)]
|
||||
if boundary.is_none() {
|
||||
let stream = file_body::InlineFileStream::new(opened.file, start, body_length, stream_capacity);
|
||||
*response.body_mut() = Body::new(file_body::FileBody::new(stream, body_length));
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(candidate) = sendfile_candidate {
|
||||
response.extensions_mut().insert(candidate);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
let mut file = tokio::fs::File::from_std(opened.file);
|
||||
if let Some(boundary) = boundary {
|
||||
let ranges = ranges.expect("multipart ranges");
|
||||
|
||||
@@ -80,3 +80,62 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Reads a regular file in bounded chunks on the calling task.
|
||||
#[cfg(unix)]
|
||||
pub(super) struct InlineFileStream {
|
||||
file: std::fs::File,
|
||||
offset: u64,
|
||||
remaining: u64,
|
||||
chunk: usize,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl InlineFileStream {
|
||||
pub(super) fn new(file: std::fs::File, offset: u64, length: u64, chunk: usize) -> Self {
|
||||
Self {
|
||||
file,
|
||||
offset,
|
||||
remaining: length,
|
||||
chunk: chunk.max(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Stream for InlineFileStream {
|
||||
type Item = Result<Bytes, io::Error>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
use std::os::unix::fs::FileExt;
|
||||
let this = self.get_mut();
|
||||
if this.remaining == 0 {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
let want = this.remaining.min(this.chunk as u64) as usize;
|
||||
let mut buffer = bytes::BytesMut::zeroed(want);
|
||||
let mut filled = 0;
|
||||
while filled < want {
|
||||
match this
|
||||
.file
|
||||
.read_at(&mut buffer[filled..], this.offset + filled as u64)
|
||||
{
|
||||
Ok(0) => break,
|
||||
Ok(read) => filled += read,
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => {
|
||||
this.remaining = 0;
|
||||
return Poll::Ready(Some(Err(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if filled == 0 {
|
||||
this.remaining = 0;
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
buffer.truncate(filled);
|
||||
this.offset += filled as u64;
|
||||
this.remaining -= filled as u64;
|
||||
Poll::Ready(Some(Ok(buffer.freeze())))
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,7 @@ pub(super) fn builtin_error_response(status: StatusCode, method: &Method) -> Res
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
response.headers_mut().insert(
|
||||
"x-content-type-options",
|
||||
response.headers_mut().insert(::http::header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
response
|
||||
@@ -249,7 +248,7 @@ fn add_download_header(response: &mut Response, service: &CompiledService, path:
|
||||
|
||||
pub(super) fn accepts_content_encoding(headers: &HeaderMap, wanted: &str) -> bool {
|
||||
let Some(value) = headers
|
||||
.get(http::header::ACCEPT_ENCODING)
|
||||
.get(::http::header::ACCEPT_ENCODING)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
else {
|
||||
return false;
|
||||
|
||||
@@ -13,9 +13,7 @@ pub(super) async fn open_components(
|
||||
components: Vec<String>,
|
||||
) -> io::Result<Opened> {
|
||||
let root = FilesystemRoot::for_service(service);
|
||||
tokio::task::spawn_blocking(move || secure_open_sync(&root, &components))
|
||||
.await
|
||||
.map_err(|_| io::Error::other("Web Server filesystem task failed"))?
|
||||
secure_open_sync(&root, &components)
|
||||
}
|
||||
|
||||
fn target_path_and_query(
|
||||
@@ -129,7 +127,7 @@ pub(super) async fn select_precompressed(
|
||||
return None;
|
||||
}
|
||||
if headers
|
||||
.get("range")
|
||||
.get(::http::header::RANGE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value.contains(','))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user