v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,357 @@
|
||||
{
|
||||
// Both scopes run before DNS, TCP, TLS or reuse of an existing upstream connection.
|
||||
if !snapshot.host_allowed(&host, context.client_ip)
|
||||
|| !route
|
||||
.acl
|
||||
.as_ref()
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !route
|
||||
.frontend
|
||||
.as_ref()
|
||||
.and_then(|f| f.profile_acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !route
|
||||
.frontend
|
||||
.as_ref()
|
||||
.and_then(|f| f.acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
{
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: denied by access policy\n",
|
||||
);
|
||||
}
|
||||
// Local location responses intentionally still pass host/profile/location ACLs
|
||||
// and the outer response-header/security pipeline, but never select/open an upstream.
|
||||
if let Some(spec) = &route.response {
|
||||
return local_location_response(spec, request.method());
|
||||
}
|
||||
if let crate::config::CompiledSource::WebServer(service) = &route.source {
|
||||
if handshake.is_some() {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: Web Server source does not support WebSocket upgrades\n",
|
||||
);
|
||||
}
|
||||
let service_path = match route.rewritten_path(&path) {
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => path.clone(),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server rewrite path\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
|
||||
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) = Some(false);
|
||||
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) = Some(false);
|
||||
}
|
||||
// Do not hold a shared reference to Request<Body> across await. Axum handlers
|
||||
// must return Send futures, while Body is not Sync. Clone only static-request
|
||||
// metadata needed by the filesystem source before entering the async server.
|
||||
let static_method = request.method().clone();
|
||||
let static_headers = request.headers().clone();
|
||||
let static_uri = request.uri().clone();
|
||||
let static_version = request.version();
|
||||
let static_body = std::mem::replace(request.body_mut(), Body::empty());
|
||||
let mut response = crate::web_server::serve(
|
||||
service,
|
||||
static_method,
|
||||
static_headers,
|
||||
static_uri,
|
||||
&service_path,
|
||||
static_body,
|
||||
crate::web_server::RequestContext {
|
||||
client_ip: context.client_ip,
|
||||
client_port: Some(peer.0.port()),
|
||||
server_name: host.clone(),
|
||||
server_port: local.0.port(),
|
||||
tls,
|
||||
version: static_version,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
response.extensions_mut().insert(StaticSourceResponse);
|
||||
return response;
|
||||
}
|
||||
let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool {
|
||||
let selection = match &route.backend_id {
|
||||
Some(id) => pool.select_member_wait(id).await,
|
||||
None => pool
|
||||
.select_wait(context.client_ip, request.headers(), tls)
|
||||
.await,
|
||||
};
|
||||
let selection = match selection {
|
||||
Ok(selection) => selection,
|
||||
Err(crate::balance::SelectionFailure::Unavailable) => {
|
||||
let mut response = error(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"RustProxy: no healthy backend in this vhost pool\n",
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("retry-after", HeaderValue::from_static("1"));
|
||||
return response;
|
||||
}
|
||||
Err(crate::balance::SelectionFailure::Capacity) => {
|
||||
let mut response = error(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"RustProxy: backend concurrency limit reached\n",
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("retry-after", HeaderValue::from_static("1"));
|
||||
return response;
|
||||
}
|
||||
};
|
||||
(
|
||||
selection.lease.member.backend.clone(),
|
||||
Some(selection.lease),
|
||||
selection.cookie,
|
||||
)
|
||||
} else {
|
||||
let Some(backend) = route.backend.clone() else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: Reverse Proxy route has no backend\n",
|
||||
);
|
||||
};
|
||||
(backend, None, None)
|
||||
};
|
||||
let admin_control_plane = admin_session_passthrough(&snapshot, &host, tls, &backend);
|
||||
if admin_control_plane {
|
||||
headers::restore_admin_cookie(request.headers_mut(), &admin_cookie);
|
||||
}
|
||||
let cache_settings = route
|
||||
.frontend
|
||||
.as_ref()
|
||||
.map(|f| &f.cache)
|
||||
.unwrap_or(&snapshot.envelope.config.cache);
|
||||
let transform_settings = route.frontend.as_ref().map(|f| &f.transform);
|
||||
let scope = route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or("");
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
|
||||
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(cache_settings.active_for(route.cache));
|
||||
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(transform_settings.is_some_and(|settings| settings.enabled));
|
||||
}
|
||||
let policy_reason = if admin_control_plane {
|
||||
Some("admin_control_plane")
|
||||
} else if frontend.is_some_and(|f| f.basic_auth.is_some()) {
|
||||
Some("basic_auth")
|
||||
} else if frontend.is_some_and(|f| f.tinyauth.is_some()) {
|
||||
Some("tinyauth")
|
||||
} else if route
|
||||
.pool
|
||||
.as_ref()
|
||||
.is_some_and(|pool| pool.cookie_affinity())
|
||||
{
|
||||
Some("sticky_cookie")
|
||||
} else if !cache_settings.enabled {
|
||||
Some("cache_disabled")
|
||||
} else if route.cache == Some(false) {
|
||||
Some("route_disabled")
|
||||
} else if handshake.is_some() {
|
||||
Some("request_upgrade")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cache_enabled = policy_reason.is_none();
|
||||
let body_empty = if cache_enabled && handshake.is_none() && request.method() == Method::GET {
|
||||
match empty_for_cache(&mut request).await {
|
||||
Ok(empty) => empty,
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid or incomplete request body\n",
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
request.body().is_end_stream()
|
||||
};
|
||||
let ignore_static_cookie = cache_settings.ignore_cookie_for_path(&path);
|
||||
let original_policy = policy_reason
|
||||
.map(crate::cache::RequestPolicy::Bypass)
|
||||
.unwrap_or_else(|| {
|
||||
crate::cache::request_policy(
|
||||
request.method(),
|
||||
request.headers(),
|
||||
body_empty,
|
||||
&path,
|
||||
request.uri().query().is_some(),
|
||||
cache_settings,
|
||||
)
|
||||
});
|
||||
|
||||
let only_cached =
|
||||
cache_settings.active_for(route.cache) && crate::cache::only_if_cached(request.headers());
|
||||
let public_suffix = request
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or("/")
|
||||
.to_owned();
|
||||
let origin_suffix = match route.rewritten_path(&path) {
|
||||
Ok(None) => public_suffix.clone(),
|
||||
Ok(Some(rewritten)) => {
|
||||
let Some(mut encoded) = encoded_origin_path(&rewritten) else {
|
||||
return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n");
|
||||
};
|
||||
if let Some(query) = request.uri().query() {
|
||||
encoded.push('?');
|
||||
encoded.push_str(query);
|
||||
}
|
||||
encoded
|
||||
}
|
||||
Err(_) => return error(StatusCode::BAD_REQUEST, "RustProxy: invalid rewritten request target\n"),
|
||||
};
|
||||
let Some(uri) = upstream_uri(&backend, &origin_suffix) else {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid request target\n",
|
||||
);
|
||||
};
|
||||
let exchange = request.extensions().get::<Arc<Exchange>>().cloned();
|
||||
let lease = request.extensions().get::<Arc<ConnectionLease>>().cloned();
|
||||
let stop = request.extensions().get::<ServerShutdown>().cloned();
|
||||
if handshake.is_some() {
|
||||
if let Some(exchange) = &exchange {
|
||||
exchange.websocket_phase("upstream_handshake");
|
||||
}
|
||||
}
|
||||
let downstream = if handshake.as_ref().is_some_and(|handshake| handshake.h1()) {
|
||||
Some(hyper::upgrade::on(&mut request))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let h2_downstream = if handshake.as_ref().is_some_and(|handshake| handshake.h2())
|
||||
&& (backend.protocol != crate::config::UpstreamProtocol::Http2
|
||||
|| websocket_legacy_support)
|
||||
{
|
||||
Some(std::mem::replace(request.body_mut(), Body::empty()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
headers::strip_hop_by_hop(request.headers_mut());
|
||||
request.headers_mut().insert(
|
||||
HOST,
|
||||
if websocket_legacy_support {
|
||||
backend.host_header.clone()
|
||||
} else if backend.preserve_host {
|
||||
original_host.clone()
|
||||
} else {
|
||||
backend.host_header.clone()
|
||||
},
|
||||
);
|
||||
crate::headers::apply(request.headers_mut(), &snapshot.headers.request);
|
||||
if let Some(frontend) = &route.frontend {
|
||||
crate::headers::apply(request.headers_mut(), &frontend.headers.request);
|
||||
}
|
||||
crate::headers::apply(request.headers_mut(), &route.headers.request);
|
||||
// Canonical trusted metadata cannot be overwritten by custom request rules.
|
||||
// A vhost may intentionally withhold only the visitor IP from the application;
|
||||
// ACL/rate-limit/Tinyauth decisions above still use the authenticated client address.
|
||||
context.apply_upstream(
|
||||
request.headers_mut(),
|
||||
route
|
||||
.frontend
|
||||
.as_ref()
|
||||
.is_none_or(|frontend| frontend.forward_client_ip),
|
||||
);
|
||||
if let Some(auth) = frontend.and_then(|f| f.tinyauth.as_ref()) {
|
||||
auth.strip_identity(request.headers_mut());
|
||||
}
|
||||
if let Some(identity) = identity {
|
||||
identity.apply(request.headers_mut());
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
let range_selection = crate::cache::request_range(request.method(), request.headers());
|
||||
let if_range = request.headers().get("if-range").cloned();
|
||||
let range_requested = !matches!(range_selection, crate::cache::RangeSelection::None);
|
||||
let post_policy = crate::cache::request_policy(
|
||||
request.method(),
|
||||
request.headers(),
|
||||
body_empty,
|
||||
&path,
|
||||
request.uri().query().is_some(),
|
||||
cache_settings,
|
||||
);
|
||||
let effective_policy = match (original_policy, post_policy) {
|
||||
(crate::cache::RequestPolicy::Bypass(reason), _) => {
|
||||
crate::cache::RequestPolicy::Bypass(reason)
|
||||
}
|
||||
(_, crate::cache::RequestPolicy::Bypass(reason)) => {
|
||||
crate::cache::RequestPolicy::Bypass(reason)
|
||||
}
|
||||
(crate::cache::RequestPolicy::Revalidate(reason), _) => {
|
||||
crate::cache::RequestPolicy::Revalidate(reason)
|
||||
}
|
||||
(_, crate::cache::RequestPolicy::Revalidate(reason)) => {
|
||||
crate::cache::RequestPolicy::Revalidate(reason)
|
||||
}
|
||||
_ => crate::cache::RequestPolicy::Use,
|
||||
};
|
||||
let effective_reason = effective_policy.bypass_reason();
|
||||
let revalidation_reason = effective_policy.revalidation_reason();
|
||||
let effective_cacheable = effective_reason.is_none();
|
||||
|
||||
// Match Varnish-style cache identity: a load-balanced backend member must not
|
||||
// fragment the object cache. The vhost scope + canonical host + request target
|
||||
// already identify the public resource; Vary handles representation variants.
|
||||
let cache_route = route.id.as_str();
|
||||
let mut cache_key_headers = None;
|
||||
if ignore_static_cookie && request.headers().contains_key("cookie") {
|
||||
let mut headers = request.headers().clone();
|
||||
headers.remove("cookie");
|
||||
cache_key_headers = Some(headers);
|
||||
}
|
||||
let key_headers = cache_key_headers
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| request.headers());
|
||||
let cache_ticket = if effective_cacheable {
|
||||
state.cache.ticket_scoped(
|
||||
state.cache.revision(),
|
||||
route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or(""),
|
||||
context.scheme,
|
||||
original_host.to_str().unwrap_or(""),
|
||||
cache_route,
|
||||
&public_suffix,
|
||||
key_headers,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let client_conditionals = request.headers().clone();
|
||||
let cached_revalidation = if let Some(ticket) = cache_ticket.as_ref() {
|
||||
state.cache.revalidation_tiered(ticket).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let freshness_revalidation_reason = cached_revalidation
|
||||
.as_ref()
|
||||
.and_then(|cached| cached.request_freshness_revalidation_reason(&client_conditionals));
|
||||
let pending_revalidation = revalidation_reason.is_some()
|
||||
|| freshness_revalidation_reason.is_some()
|
||||
|| cached_revalidation
|
||||
.as_ref()
|
||||
.is_some_and(|cached| !cached.reusable_for_request(&client_conditionals));
|
||||
let miss_recorded = false;
|
||||
(
|
||||
backend, backend_lease, affinity_cookie, admin_control_plane, cache_settings,
|
||||
transform_settings, scope, ignore_static_cookie, only_cached, public_suffix,
|
||||
origin_suffix, uri, exchange, lease, stop, downstream, h2_downstream,
|
||||
range_selection, if_range, range_requested, effective_reason, revalidation_reason,
|
||||
cache_ticket, client_conditionals, cached_revalidation, freshness_revalidation_reason,
|
||||
pending_revalidation, miss_recorded,
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user