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,345 @@
|
||||
async fn forward(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(peer): Extension<PeerAddr>,
|
||||
Extension(local): Extension<LocalAddr>,
|
||||
mut request: Request,
|
||||
) -> Response {
|
||||
if request.method() == Method::CONNECT
|
||||
&& !matches!(request.version(), Version::HTTP_2 | Version::HTTP_3)
|
||||
{
|
||||
return error(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"RustProxy: CONNECT is not supported\n",
|
||||
);
|
||||
}
|
||||
let tls = request
|
||||
.extensions()
|
||||
.get::<Transport>()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.tls;
|
||||
let Ok(original_host) = effective_host(&request, tls) else {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid or conflicting request authority\n",
|
||||
);
|
||||
};
|
||||
let Ok(host) = original_host
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|v| request_host(v).ok())
|
||||
.ok_or(())
|
||||
else {
|
||||
return error(StatusCode::BAD_REQUEST, "RustProxy: invalid hostname\n");
|
||||
};
|
||||
let handshake = if request.method() == Method::CONNECT {
|
||||
let protocol = match request.version() {
|
||||
Version::HTTP_2 => websocket::h2_protocol(&request),
|
||||
Version::HTTP_3 => websocket::h3_protocol(&request),
|
||||
_ => None,
|
||||
};
|
||||
match (request.version(), protocol) {
|
||||
(Version::HTTP_2, Some("websocket")) => match websocket::validate_h2(&request) {
|
||||
Ok(handshake) => Some(handshake),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid HTTP/2 WebSocket Extended CONNECT\n",
|
||||
)
|
||||
}
|
||||
},
|
||||
(Version::HTTP_3, Some("websocket")) => match websocket::validate_h3(&request) {
|
||||
Ok(handshake) => Some(handshake),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid HTTP/3 WebSocket Extended CONNECT\n",
|
||||
)
|
||||
}
|
||||
},
|
||||
(_, Some(_)) => {
|
||||
return error(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"RustProxy: unsupported Extended CONNECT protocol\n",
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
return error(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"RustProxy: CONNECT is not supported\n",
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if headers::requests_upgrade(request.headers()) {
|
||||
match websocket::validate(&request) {
|
||||
Ok(handshake) => Some(handshake),
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid or unsupported WebSocket upgrade\n",
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let snapshot = request
|
||||
.extensions()
|
||||
.get::<Arc<crate::config::Snapshot>>()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| state.config.load_full());
|
||||
let Ok(path) = crate::acl::routing_path(request.uri().path()) else {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: ambiguous request path\n",
|
||||
);
|
||||
};
|
||||
let frontend = snapshot.frontend(&host, local.0, tls);
|
||||
let websocket_legacy_support = handshake.is_some()
|
||||
&& frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
|
||||
let redirect = snapshot.redirect_on(&host, &path, local.0, tls);
|
||||
let local_host = snapshot.not_found_host_on(&host, local.0, tls);
|
||||
// TLS ALPN normally prevents HTTP/2 from reaching a frontend that disabled it.
|
||||
// Keep this request-level guard for coalesced/reused connections and direct tests.
|
||||
if request.version() == Version::HTTP_2
|
||||
&& (frontend.is_some_and(|frontend| !frontend.http2_enabled)
|
||||
|| redirect.is_some_and(|(redirect, _)| !redirect.http2_enabled)
|
||||
|| local_host.is_some_and(|item| !item.http2_enabled))
|
||||
{
|
||||
return error(
|
||||
StatusCode::MISDIRECTED_REQUEST,
|
||||
"RustProxy: HTTP/2 is disabled for this vhost\n",
|
||||
);
|
||||
}
|
||||
if request.version() == Version::HTTP_3
|
||||
&& (frontend.is_some_and(|frontend| !frontend.http3_enabled)
|
||||
|| redirect.is_some_and(|(redirect, _)| !redirect.http3_enabled)
|
||||
|| local_host.is_some_and(|item| !item.http3_enabled))
|
||||
{
|
||||
return error(
|
||||
StatusCode::MISDIRECTED_REQUEST,
|
||||
"RustProxy: HTTP/3 is disabled for this vhost\n",
|
||||
);
|
||||
}
|
||||
if request.version() == Version::HTTP_2
|
||||
&& request.method() == Method::CONNECT
|
||||
&& websocket::h2_protocol(&request) == Some("websocket")
|
||||
&& frontend.is_none_or(|frontend| !frontend.websocket_rfc8441)
|
||||
{
|
||||
return error(
|
||||
StatusCode::MISDIRECTED_REQUEST,
|
||||
"RustProxy: RFC 8441 WebSocket transport is not enabled for this vhost\n",
|
||||
);
|
||||
}
|
||||
if request.version() == Version::HTTP_3
|
||||
&& request.method() == Method::CONNECT
|
||||
&& websocket::h3_protocol(&request) == Some("websocket")
|
||||
&& frontend.is_none_or(|frontend| !frontend.websocket_rfc9220)
|
||||
{
|
||||
return error(
|
||||
StatusCode::MISDIRECTED_REQUEST,
|
||||
"RustProxy: RFC 9220 WebSocket transport is not enabled for this vhost\n",
|
||||
);
|
||||
}
|
||||
if !tls {
|
||||
if let Some(response) = frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri())) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
let mut context = match snapshot.forwarding.context(
|
||||
request.headers(),
|
||||
peer.0,
|
||||
local.0,
|
||||
tls,
|
||||
&original_host,
|
||||
request.uri(),
|
||||
) {
|
||||
Ok(context) => context,
|
||||
Err(_) => {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid forwarding context\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
if frontend.is_some_and(|frontend| frontend.cloudflare_client_ip) {
|
||||
if let Some(client) = crate::forwarding::cloudflare_client_ip(request.headers(), peer.0) {
|
||||
if context.override_client_ip(client).is_err() {
|
||||
return error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Cloudflare client address\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = Some(context.client_ip.to_string());
|
||||
}
|
||||
if let Some(frontend) = frontend {
|
||||
// GeoIP is deliberately off the default request path. Resolve only when
|
||||
// a country policy/rate-limit requires it or the operator explicitly
|
||||
// enables country capture for access logs + traffic analytics.
|
||||
let capture_country = state.logs.capture_country();
|
||||
let country = (capture_country
|
||||
|| frontend.profile_country_access.needs_geoip()
|
||||
|| frontend.rate_limit.needs_geoip())
|
||||
.then(|| state.geoip.country(context.client_ip))
|
||||
.flatten();
|
||||
if capture_country {
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
|
||||
}
|
||||
}
|
||||
if !frontend.profile_country_access.allows(country.as_deref())
|
||||
|| !frontend.rate_limit.country_allowed(country.as_deref())
|
||||
{
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: country access policy denied this request\n",
|
||||
);
|
||||
}
|
||||
if let Err(limited) = state.rate_limiter.check(
|
||||
snapshot.envelope.revision,
|
||||
&frontend.id,
|
||||
context.client_ip,
|
||||
country.as_deref(),
|
||||
&frontend.rate_limit,
|
||||
) {
|
||||
let mut response = error(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
"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);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
// Enforce host ACLs even for maintenance/fallback/direct-IP responses.
|
||||
if !snapshot.host_allowed(&host, context.client_ip)
|
||||
|| !frontend
|
||||
.and_then(|f| f.profile_acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
|| !frontend
|
||||
.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",
|
||||
);
|
||||
}
|
||||
if let Some((_redirect, rule)) = redirect {
|
||||
let authority = original_host.to_str().unwrap_or(&host);
|
||||
let Some(mut response) = rule.response(authority, tls, request.uri()) else {
|
||||
return error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: redirect target could not be rendered\n",
|
||||
);
|
||||
};
|
||||
response.extensions_mut().insert(RedirectResponse { http3_enabled: _redirect.http3_enabled });
|
||||
return response;
|
||||
}
|
||||
if let Some(item) = local_host {
|
||||
let mut response = item.page.serve(request.method(), StatusCode::NOT_FOUND);
|
||||
response.extensions_mut().insert(LocalHostResponse { http3_enabled: item.http3_enabled });
|
||||
return response;
|
||||
}
|
||||
let selected = snapshot.select_on(&host, &path, local.0, tls);
|
||||
if !selected
|
||||
.and_then(|r| r.acl.as_ref())
|
||||
.is_none_or(|acl| acl.allows(context.client_ip))
|
||||
{
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: denied by access policy\n",
|
||||
);
|
||||
}
|
||||
// The admin cookie is never ordinary application state. Strip it BEFORE
|
||||
// auth/cache and keep only the removed fragment locally. A verified self-admin
|
||||
// backend may restore it after the actual upstream has been selected.
|
||||
let admin_cookie = headers::strip_admin_cookie(request.headers_mut());
|
||||
// This is internal authorization, not a public /tinyauth location. It runs
|
||||
// before origin selection, static responses, upgrades and every cache lookup.
|
||||
let basic_identity = if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
|
||||
match auth
|
||||
.authorize(
|
||||
request.headers_mut(),
|
||||
tls,
|
||||
context.client_ip,
|
||||
state.basic_workers.clone(),
|
||||
&state.basic_limits,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(identity) => identity,
|
||||
Err(response) => return response,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if handshake.is_some() && frontend.is_some_and(|f| !f.websockets) {
|
||||
return error(
|
||||
StatusCode::FORBIDDEN,
|
||||
"RustProxy: WebSocket access is disabled for this host\n",
|
||||
);
|
||||
}
|
||||
let identity = if let Some(auth) = frontend.and_then(|f| f.tinyauth.as_ref()) {
|
||||
auth.strip_identity(request.headers_mut());
|
||||
match auth
|
||||
.authorize(
|
||||
request.headers(),
|
||||
request.method(),
|
||||
&context,
|
||||
&state.shutdown,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(identity) => Some(identity),
|
||||
Err(response) => return response,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if crate::pages::literal_ip_authority(original_host.to_str().unwrap_or("")) {
|
||||
if let Some(page) = &snapshot.envelope.config.pages.direct_ip {
|
||||
return page.serve(request.method(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
if let Some(page) = frontend.and_then(|f| f.pages.response.as_ref()) {
|
||||
return page.serve(request.method(), StatusCode::OK);
|
||||
}
|
||||
let Some(route) = selected else {
|
||||
let page = match frontend {
|
||||
Some(frontend) => frontend.pages.fallback.as_ref(),
|
||||
None => snapshot.envelope.config.pages.unknown_host.as_ref(),
|
||||
};
|
||||
return page
|
||||
.map(|page| page.serve(request.method(), StatusCode::NOT_FOUND))
|
||||
.unwrap_or_else(|| {
|
||||
error(
|
||||
StatusCode::NOT_FOUND,
|
||||
"RustProxy: no route for this host/path\n",
|
||||
)
|
||||
});
|
||||
};
|
||||
let (
|
||||
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, mut h2_downstream,
|
||||
range_selection, if_range, range_requested, effective_reason, revalidation_reason,
|
||||
cache_ticket, client_conditionals, cached_revalidation, freshness_revalidation_reason,
|
||||
pending_revalidation, mut miss_recorded,
|
||||
) = include!("forward/route_cache.rs");
|
||||
|
||||
let (serve_stale, mut fill_leader) = include!("forward/cache_lookup.rs");
|
||||
|
||||
let (
|
||||
websocket_upstream_h2, revalidation_sent, mut h2_direct_session, request_body_empty,
|
||||
proactive_base_headers, upstream_started, active_backend,
|
||||
active_lease, active_affinity_cookie, active_uri, retries, upstream_result,
|
||||
final_attempt_elapsed,
|
||||
) = include!("forward/upstream.rs");
|
||||
|
||||
include!("forward/response.rs")
|
||||
}
|
||||
Reference in new issue
Block a user