rama_http_backend/client/svc.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
use rama_core::{
error::{BoxError, ErrorContext, OpaqueError},
Context, Service,
};
use rama_http_types::{
dep::{http::uri::PathAndQuery, http_body},
header::{CONNECTION, HOST, KEEP_ALIVE, PROXY_CONNECTION, TRANSFER_ENCODING, UPGRADE},
headers::HeaderMapExt,
Method, Request, Response, Version,
};
use rama_net::{address::ProxyAddress, http::RequestContext};
#[derive(Debug)]
pub(super) enum SendRequest<Body> {
Http1(rama_http_core::client::conn::http1::SendRequest<Body>),
Http2(rama_http_core::client::conn::http2::SendRequest<Body>),
}
#[derive(Debug)]
/// Internal http sender used to send the actual requests.
pub struct HttpClientService<Body>(pub(super) SendRequest<Body>);
impl<State, Body> Service<State, Request<Body>> for HttpClientService<Body>
where
State: Clone + Send + Sync + 'static,
Body: http_body::Body<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
{
type Response = Response;
type Error = BoxError;
async fn serve(
&self,
mut ctx: Context<State>,
req: Request<Body>,
) -> Result<Self::Response, Self::Error> {
// sanitize subject line request uri
// because Hyper (http) writes the URI as-is
//
// Originally reported in and fixed for:
// <https://github.com/plabayo/rama/issues/250>
//
// TODO: fix this in hyper fork (embedded in rama http core)
// directly instead of here...
let req = sanitize_client_req_header(&mut ctx, req)?;
let resp = match &self.0 {
SendRequest::Http1(sender) => sender.send_request(req).await,
SendRequest::Http2(sender) => sender.send_request(req).await,
}?;
Ok(resp.map(rama_http_types::Body::new))
}
}
fn sanitize_client_req_header<S, B>(
ctx: &mut Context<S>,
req: Request<B>,
) -> Result<Request<B>, BoxError> {
// logic specific to this method
if req.method() == Method::CONNECT && req.uri().host().is_none() {
return Err(OpaqueError::from_display("missing host in CONNECT request").into());
}
// logic specific to http versions
Ok(match req.version() {
Version::HTTP_09 | Version::HTTP_10 | Version::HTTP_11 => {
// remove authority and scheme for non-connect requests
// cfr: <https://datatracker.ietf.org/doc/html/rfc2616#section-5.1.2>
if !ctx.contains::<ProxyAddress>() && req.uri().host().is_some() {
tracing::trace!(
"remove authority and scheme from non-connect direct http(~1) request"
);
let (mut parts, body) = req.into_parts();
let mut uri_parts = parts.uri.into_parts();
uri_parts.scheme = None;
let authority = uri_parts
.authority
.take()
.expect("to exist due to our host existence test");
// NOTE: in case the requested resource was the root ("/") it is possible
// that the path is now empty. Hyper (currently used) has h1 built-in and
// has a difference between the header encoding and the `as_str` method. The
// encoding will be empty, which is invalid according to
// <https://datatracker.ietf.org/doc/html/rfc2616#section-5.1.2> and will fail.
// As such we force it here to `/` (the path) incase it is empty,
// as there is no way if this required or no... Sad sad sad...
//
// NOTE: once we fork hyper we can just handle it there, as there
// is no valid reason for that encoding every to be empty... *sigh*
if uri_parts.path_and_query.as_ref().map(|pq| pq.as_str()) == Some("/") {
uri_parts.path_and_query = Some(PathAndQuery::from_static("/"));
}
// add required host header if not defined
if !parts.headers.contains_key(HOST) {
parts
.headers
.typed_insert(rama_http_types::headers::Host::from(authority));
}
parts.uri = rama_http_types::Uri::from_parts(uri_parts)?;
Request::from_parts(parts, body)
} else if !req.headers().contains_key(HOST) {
tracing::trace!(uri = %req.uri(), "add authority as HOST header to req (was missing it)");
let authority = req
.uri()
.authority()
.ok_or_else(|| {
OpaqueError::from_display(
"[http1] missing authority in uri and missing host",
)
})?
.clone();
let mut req = req;
req.headers_mut()
.typed_insert(rama_http_types::headers::Host::from(authority));
req
} else {
req
}
}
Version::HTTP_2 => {
// set scheme/host if not defined as otherwise pseudo
// headers won't be possible to be set in the h2 crate
let mut req = if req.uri().host().is_none() {
let request_ctx = ctx.get::<RequestContext>().ok_or_else(|| {
OpaqueError::from_display("[h2+] add scheme/host: missing RequestCtx")
.into_boxed()
})?;
tracing::trace!(
http_version = ?req.version(),
"defining authority and scheme to non-connect direct http request"
);
let (mut parts, body) = req.into_parts();
let mut uri_parts = parts.uri.into_parts();
uri_parts.scheme = Some(
request_ctx
.protocol
.as_str()
.try_into()
.context("use RequestContext.protocol as http scheme")?,
);
// NOTE: in a green future we might not need to stringify
// this entire thing first... maybe something someone at some
// point can take a look at this mess
uri_parts.authority = Some(
request_ctx
.authority
.to_string()
.try_into()
.context("use RequestContext.authority as http authority")?,
);
parts.uri = rama_http_types::Uri::from_parts(uri_parts)
.context("create http uri from parts")?;
Request::from_parts(parts, body)
} else {
req
};
// remove illegal headers
for illegal_h2_header in [
&CONNECTION,
&TRANSFER_ENCODING,
&PROXY_CONNECTION,
&UPGRADE,
&KEEP_ALIVE,
&HOST,
] {
if let Some(header) = req.headers_mut().remove(illegal_h2_header) {
tracing::trace!(?header, "removed illegal (~http1) header from h2 request");
}
}
req
}
Version::HTTP_3 => {
tracing::debug!(
uri = %req.uri(),
"h3 request detected, but sanitize_client_req_header does not yet support this",
);
req
}
_ => {
tracing::warn!(
uri = %req.uri(),
method = ?req.method(),
"request with unknown version detected, sanitize_client_req_header cannot support this",
);
req
}
})
}