Merge branch 'tmp/sticky-cookie' into feat/sticky-cookie-feature

This commit is contained in:
Jun Kurihara 2025-06-03 14:50:00 +09:00 committed by GitHub
commit d8cadf06af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 4870 additions and 867 deletions

View file

@ -44,10 +44,7 @@ mod tests {
}
#[test]
fn ipv6_to_canonical() {
let socket = SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0xdead, 0xbeef)),
8080,
);
let socket = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0xdead, 0xbeef)), 8080);
assert_eq!(socket.to_canonical(), socket);
}
#[test]

View file

@ -71,7 +71,7 @@ where
Ok(v)
}
Err(e) => {
error!("{e}");
error!("{e}: {log_data}");
let code = StatusCode::from(e);
log_data.status_code(&code).output();
synthetic_error_response(code)
@ -107,9 +107,11 @@ where
let backend_app = match self.app_manager.apps.get(&server_name) {
Some(backend_app) => backend_app,
None => {
let Some(default_server_name) = &self.app_manager.default_server_name else {
return Err(HttpError::NoMatchingBackendApp);
};
let default_server_name = self
.app_manager
.default_server_name
.as_ref()
.ok_or(HttpError::NoMatchingBackendApp)?;
debug!("Serving by default app");
self.app_manager.apps.get(default_server_name).unwrap()
}
@ -131,9 +133,7 @@ where
// Find reverse proxy for given path and choose one of upstream host
// Longest prefix match
let path = req.uri().path();
let Some(upstream_candidates) = backend_app.path_manager.get(path) else {
return Err(HttpError::NoUpstreamCandidates);
};
let upstream_candidates = backend_app.path_manager.get(path).ok_or(HttpError::NoUpstreamCandidates)?;
// Upgrade in request header
let upgrade_in_request = extract_upgrade(req.headers());
@ -147,19 +147,17 @@ where
let req_on_upgrade = hyper::upgrade::on(&mut req);
// Build request from destination information
let _context = match self.generate_request_forwarded(
&client_addr,
&listen_addr,
&mut req,
&upgrade_in_request,
upstream_candidates,
tls_enabled,
) {
Err(e) => {
return Err(HttpError::FailedToGenerateUpstreamRequest(e.to_string()));
}
Ok(v) => v,
};
let _context = self
.generate_request_forwarded(
&client_addr,
&listen_addr,
&mut req,
&upgrade_in_request,
upstream_candidates,
tls_enabled,
)
.map_err(|e| HttpError::FailedToGenerateUpstreamRequest(e.to_string()))?;
debug!(
"Request to be forwarded: [uri {}, method: {}, version {:?}, headers {:?}]",
req.uri(),
@ -173,12 +171,12 @@ where
//////////////
// Forward request to a chosen backend
let mut res_backend = match self.forwarder.request(req).await {
Ok(v) => v,
Err(e) => {
return Err(HttpError::FailedToGetResponseFromBackend(e.to_string()));
}
};
let mut res_backend = self
.forwarder
.request(req)
.await
.map_err(|e| HttpError::FailedToGetResponseFromBackend(e.to_string()))?;
//////////////
// Process reverse proxy context generated during the forwarding request generation.
#[cfg(feature = "sticky-cookie")]
@ -191,16 +189,16 @@ where
if res_backend.status() != StatusCode::SWITCHING_PROTOCOLS {
// Generate response to client
if let Err(e) = self.generate_response_forwarded(&mut res_backend, backend_app) {
return Err(HttpError::FailedToGenerateDownstreamResponse(e.to_string()));
}
self
.generate_response_forwarded(&mut res_backend, backend_app)
.map_err(|e| HttpError::FailedToGenerateDownstreamResponse(e.to_string()))?;
return Ok(res_backend);
}
// Handle StatusCode::SWITCHING_PROTOCOLS in response
let upgrade_in_response = extract_upgrade(res_backend.headers());
let should_upgrade = match (upgrade_in_request.as_ref(), upgrade_in_response.as_ref()) {
(Some(u_req), Some(u_res)) => u_req.to_ascii_lowercase() == u_res.to_ascii_lowercase(),
(Some(u_req), Some(u_res)) => u_req.eq_ignore_ascii_case(u_res),
_ => false,
};

View file

@ -1,11 +1,11 @@
use super::{handler_main::HandlerContext, utils_headers::*, utils_request::update_request_line, HttpMessageHandler};
use super::{HttpMessageHandler, handler_main::HandlerContext, utils_headers::*, utils_request::update_request_line};
use crate::{
backend::{BackendApp, UpstreamCandidates},
constants::RESPONSE_HEADER_SERVER,
log::*,
};
use anyhow::{anyhow, ensure, Result};
use http::{header, HeaderValue, Request, Response, Uri};
use anyhow::{Result, anyhow, ensure};
use http::{HeaderValue, Request, Response, Uri, header};
use hyper_util::client::legacy::connect::Connect;
use std::net::SocketAddr;
@ -66,17 +66,19 @@ where
upstream_candidates: &UpstreamCandidates,
tls_enabled: bool,
) -> Result<HandlerContext> {
debug!("Generate request to be forwarded");
trace!("Generate request to be forwarded");
// Add te: trailer if contained in original request
let contains_te_trailers = {
if let Some(te) = req.headers().get(header::TE) {
te.as_bytes()
.split(|v| v == &b',' || v == &b' ')
.any(|x| x == "trailers".as_bytes())
} else {
false
}
req
.headers()
.get(header::TE)
.map(|te| {
te.as_bytes()
.split(|v| v == &b',' || v == &b' ')
.any(|x| x == "trailers".as_bytes())
})
.unwrap_or(false)
};
let original_uri = req.uri().to_string();
@ -136,11 +138,7 @@ where
let new_uri = Uri::builder()
.scheme(upstream_chosen.uri.scheme().unwrap().as_str())
.authority(upstream_chosen.uri.authority().unwrap().as_str());
let org_pq = match req.uri().path_and_query() {
Some(pq) => pq.to_string(),
None => "/".to_string(),
}
.into_bytes();
let org_pq = req.uri().path_and_query().map(|pq| pq.as_str()).unwrap_or("/").as_bytes();
// replace some parts of path if opt_replace_path is enabled for chosen upstream
let new_pq = match &upstream_candidates.replace_path {
@ -155,7 +153,7 @@ where
new_pq.extend_from_slice(&org_pq[matched_path.len()..]);
new_pq
}
None => org_pq,
None => org_pq.to_vec(),
};
*req.uri_mut() = new_uri.path_and_query(new_pq).build()?;

View file

@ -34,11 +34,7 @@ impl<T> From<&http::Request<T>> for HttpMessageLog {
client_addr: "".to_string(),
method: req.method().to_string(),
host: header_mapper(header::HOST),
p_and_q: req
.uri()
.path_and_query()
.map_or_else(|| "", |v| v.as_str())
.to_string(),
p_and_q: req.uri().path_and_query().map_or_else(|| "", |v| v.as_str()).to_string(),
version: req.version(),
uri_scheme: req.uri().scheme_str().unwrap_or("").to_string(),
uri_host: req.uri().host().unwrap_or("").to_string(),
@ -50,6 +46,33 @@ impl<T> From<&http::Request<T>> for HttpMessageLog {
}
}
impl std::fmt::Display for HttpMessageLog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} <- {} -- {} {} {:?} -- {} -- {} \"{}\", \"{}\" \"{}\"",
if !self.host.is_empty() {
self.host.as_str()
} else {
self.uri_host.as_str()
},
self.client_addr,
self.method,
self.p_and_q,
self.version,
self.status,
if !self.uri_scheme.is_empty() && !self.uri_host.is_empty() {
format!("{}://{}", self.uri_scheme, self.uri_host)
} else {
"".to_string()
},
self.ua,
self.xff,
self.upstream
)
}
}
impl HttpMessageLog {
pub fn client_addr(&mut self, client_addr: &SocketAddr) -> &mut Self {
self.client_addr = client_addr.to_canonical().to_string();
@ -74,26 +97,8 @@ impl HttpMessageLog {
pub fn output(&self) {
info!(
"{} <- {} -- {} {} {:?} -- {} -- {} \"{}\", \"{}\" \"{}\"",
if !self.host.is_empty() {
self.host.as_str()
} else {
self.uri_host.as_str()
},
self.client_addr,
self.method,
self.p_and_q,
self.version,
self.status,
if !self.uri_scheme.is_empty() && !self.uri_host.is_empty() {
format!("{}://{}", self.uri_scheme, self.uri_host)
} else {
"".to_string()
},
self.ua,
self.xff,
self.upstream,
// self.tls_server_name
name: crate::constants::log_event_names::ACCESS_LOG,
"{}", self
);
}
}

View file

@ -53,6 +53,7 @@ impl From<HttpError> for StatusCode {
HttpError::FailedToAddSetCookeInResponse(_) => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::FailedToGenerateDownstreamResponse(_) => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::FailedToUpgrade(_) => StatusCode::INTERNAL_SERVER_ERROR,
HttpError::FailedToGetResponseFromBackend(_) => StatusCode::BAD_GATEWAY,
// HttpError::NoUpgradeExtensionInRequest => StatusCode::BAD_REQUEST,
// HttpError::NoUpgradeExtensionInResponse => StatusCode::BAD_GATEWAY,
_ => StatusCode::INTERNAL_SERVER_ERROR,

View file

@ -1,7 +1,7 @@
use super::http_result::{HttpError, HttpResult};
use crate::{
error::*,
hyper_ext::body::{empty, ResponseBody},
hyper_ext::body::{ResponseBody, empty},
name_exp::ServerName,
};
use http::{Request, Response, StatusCode, Uri};

View file

@ -3,9 +3,9 @@ use crate::{
backend::{UpstreamCandidates, UpstreamOption},
log::*,
};
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow, ensure};
use bytes::BufMut;
use http::{header, HeaderMap, HeaderName, HeaderValue, Uri};
use http::{HeaderMap, HeaderName, HeaderValue, Uri, header};
use std::{borrow::Cow, net::SocketAddr};
#[cfg(feature = "sticky-cookie")]
@ -238,10 +238,9 @@ pub(super) fn add_forwarding_header(
pub(super) fn remove_connection_header(headers: &mut HeaderMap) {
if let Some(values) = headers.get(header::CONNECTION) {
if let Ok(v) = values.clone().to_str() {
for m in v.split(',') {
if !m.is_empty() {
headers.remove(m.trim());
}
let keys = v.split(',').map(|m| m.trim()).filter(|m| !m.is_empty());
for m in keys {
headers.remove(m);
}
}
}
@ -274,13 +273,11 @@ pub(super) fn extract_upgrade(headers: &HeaderMap) -> Option<String> {
.to_str()
.unwrap_or("")
.split(',')
.any(|w| w.trim().to_ascii_lowercase() == header::UPGRADE.as_str().to_ascii_lowercase())
.any(|w| w.trim().eq_ignore_ascii_case(header::UPGRADE.as_str()))
{
if let Some(u) = headers.get(header::UPGRADE) {
if let Ok(m) = u.to_str() {
debug!("Upgrade in request header: {}", m);
return Some(m.to_owned());
}
if let Some(Ok(m)) = headers.get(header::UPGRADE).map(|u| u.to_str()) {
debug!("Upgrade in request header: {}", m);
return Some(m.to_owned());
}
}
}

View file

@ -2,8 +2,8 @@ use crate::{
backend::{Upstream, UpstreamCandidates, UpstreamOption},
log::*,
};
use anyhow::{anyhow, ensure, Result};
use http::{header, uri::Scheme, Request, Version};
use anyhow::{Result, anyhow, ensure};
use http::{Request, Version, header, uri::Scheme};
/// Trait defining parser of hostname
/// Inspect and extract hostname from either the request HOST header or request line
@ -59,6 +59,18 @@ pub(super) fn update_request_line<B>(
upstream_chosen: &Upstream,
upstream_candidates: &UpstreamCandidates,
) -> anyhow::Result<()> {
// If request is grpc, HTTP/2 is required
if req
.headers()
.get(header::CONTENT_TYPE)
.map(|v| v.as_bytes().starts_with(b"application/grpc"))
== Some(true)
{
debug!("Must be http/2 for gRPC request.");
*req.version_mut() = Version::HTTP_2;
return Ok(());
}
// If not specified (force_httpXX_upstream) and https, version is preserved except for http/3
if upstream_chosen.uri.scheme() == Some(&Scheme::HTTP) {
// Change version to http/1.1 when destination scheme is http