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
        }
    })
}