Skip to main content

matrix_sdk/http_client/
native.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    fmt::Debug,
17    mem,
18    sync::atomic::{AtomicU64, Ordering},
19    time::Duration,
20};
21
22use backon::{ExponentialBuilder, Retryable};
23use bytes::Bytes;
24use bytesize::ByteSize;
25use eyeball::SharedObservable;
26use http::header::CONTENT_LENGTH;
27#[cfg(not(target_family = "wasm"))]
28use reqwest::Certificate;
29use reqwest::tls;
30use ruma::api::{IncomingResponseExt as _, OutgoingRequest, error::FromHttpResponseError};
31use tracing::{debug, info, warn};
32
33use super::{DEFAULT_REQUEST_TIMEOUT, HttpClient, TransmissionProgress, response_to_http_response};
34use crate::{
35    HttpResult,
36    config::RequestConfig,
37    error::{HttpError, RetryKind},
38};
39
40impl HttpClient {
41    pub(super) async fn send_request<R>(
42        &self,
43        request: http::Request<Bytes>,
44        config: RequestConfig,
45        send_progress: SharedObservable<TransmissionProgress>,
46    ) -> HttpResult<R::IncomingResponse>
47    where
48        R: OutgoingRequest + Debug,
49        HttpError: From<FromHttpResponseError<R::EndpointError>>,
50    {
51        // some functions split out so they only get compiled once,
52        // not monomorphized per request type
53        fn make_backoff(config: &RequestConfig) -> ExponentialBuilder {
54            // These values were picked because we used to use the `backoff` crate, those
55            // were defined here: https://docs.rs/backoff/0.4.0/backoff/default/index.html
56            let mut backoff = ExponentialBuilder::new()
57                .with_min_delay(Duration::from_millis(500))
58                .with_max_delay(Duration::from_secs(60))
59                .with_total_delay(Some(Duration::from_secs(15 * 60)))
60                .without_max_times();
61
62            // Let's now apply any override the user or the SDK might have set.
63            if let Some(max_delay) = config.max_retry_time {
64                backoff = backoff.with_max_delay(max_delay)
65            }
66
67            if let Some(max_times) = config.retry_limit {
68                // Backon behaves a bit differently to our own handcrafted max retry logic.
69                // We were counting from one while `backon` counts from zero.
70                backoff = backoff.with_max_times(max_times.saturating_sub(1))
71            }
72
73            backoff
74        }
75        async fn send_request_inner(
76            http_client: &reqwest::Client,
77            request: &http::Request<Bytes>,
78            timeout: Option<Duration>,
79            retry_count: &AtomicU64,
80            send_progress: SharedObservable<TransmissionProgress>,
81        ) -> HttpResult<http::Response<Bytes>> {
82            let num_attempt = retry_count.fetch_add(1, Ordering::SeqCst);
83            debug!(num_attempt, "Sending request");
84            let before = ruma::time::Instant::now();
85
86            let response = execute_request(http_client, request, timeout, send_progress).await?;
87
88            let request_duration = ruma::time::Instant::now().saturating_duration_since(before);
89
90            let status_code = response.status();
91            let response_size = ByteSize(response.body().len().try_into().unwrap_or(u64::MAX));
92            tracing::Span::current()
93                .record("status", status_code.as_u16())
94                .record("response_size", response_size.display().si_short().to_string())
95                .record("request_duration", tracing::field::debug(request_duration));
96
97            // Record interesting headers. If you add more headers, ensure they're not
98            // confidential.
99            for (header_name, header_value) in response.headers() {
100                let header_name = header_name.as_str().to_lowercase();
101
102                // Header added in case of OAuth 2.0 authentication failure, so we can correlate
103                // failures with a Sentry event emitted by the OAuth 2.0 authentication server.
104                if header_name == "x-sentry-event-id" {
105                    tracing::Span::current()
106                        .record("sentry_event_id", header_value.to_str().unwrap_or("<???>"));
107                }
108            }
109
110            Ok(response)
111        }
112        fn adjust_backoff(
113            err: &HttpError,
114            backon_suggested_timeout: Option<Duration>,
115            has_retry_limit: bool,
116        ) -> Option<Duration> {
117            match err.retry_kind() {
118                RetryKind::Transient { retry_after } => {
119                    // This bit is somewhat tricky but it's necessary so we respect the
120                    // `max_times` limit from `backon`.
121                    //
122                    // The exponential backoff in `backon` is implemented as an iterator that
123                    // returns `None` when we hit the `max_times` limit; if it returned `None`,
124                    // that means we ran out of attempts. So it's necessary to only override
125                    // the `backon_suggested_timeout` if it's `Some`.
126                    if backon_suggested_timeout.is_some() {
127                        retry_after.or(backon_suggested_timeout)
128                    } else {
129                        None
130                    }
131                }
132                RetryKind::Permanent => None,
133                RetryKind::NetworkFailure => {
134                    // If we ran into a network failure, only retry if there's some retry limit
135                    // associated to this request's configuration; otherwise, we would end up
136                    // running an infinite loop of network requests in offline mode.
137                    if has_retry_limit { backon_suggested_timeout } else { None }
138                }
139            }
140        }
141
142        let retry_count = AtomicU64::new(1);
143
144        let send_request = || {
145            let send_progress = send_progress.clone();
146            async {
147                let response = send_request_inner(
148                    &self.inner,
149                    &request,
150                    config.timeout,
151                    &retry_count,
152                    send_progress,
153                )
154                .await?;
155                let (parts, body) = response.into_parts();
156                let response: http::Response<&[u8]> = http::Response::from_parts(parts, &body);
157                R::IncomingResponse::try_from_http_response(response).map_err(HttpError::from)
158            }
159        };
160
161        let has_retry_limit = config.retry_limit.is_some();
162
163        send_request
164            .retry(make_backoff(&config))
165            .adjust(|err, backon_suggested_timeout| {
166                adjust_backoff(err, backon_suggested_timeout, has_retry_limit)
167            })
168            .await
169    }
170}
171
172#[cfg(not(target_family = "wasm"))]
173#[derive(Clone, Debug)]
174pub(crate) struct HttpSettings {
175    pub(crate) disable_ssl_verification: bool,
176    pub(crate) proxy: Option<String>,
177    pub(crate) user_agent: Option<String>,
178    pub(crate) timeout: Option<Duration>,
179    pub(crate) read_timeout: Option<Duration>,
180    pub(crate) additional_root_certificates: Vec<Certificate>,
181    pub(crate) disable_built_in_root_certificates: bool,
182}
183
184#[cfg(not(target_family = "wasm"))]
185impl Default for HttpSettings {
186    fn default() -> Self {
187        Self {
188            disable_ssl_verification: false,
189            proxy: None,
190            user_agent: None,
191            timeout: Some(DEFAULT_REQUEST_TIMEOUT),
192            read_timeout: None,
193            additional_root_certificates: Default::default(),
194            disable_built_in_root_certificates: false,
195        }
196    }
197}
198
199#[cfg(not(target_family = "wasm"))]
200impl HttpSettings {
201    /// Build a client with the specified configuration.
202    pub(crate) fn make_client(&self) -> Result<reqwest::Client, HttpError> {
203        let user_agent = self.user_agent.clone().unwrap_or_else(|| "matrix-rust-sdk".to_owned());
204        let mut http_client = reqwest::Client::builder()
205            .user_agent(user_agent)
206            // As recommended by BCP 195.
207            // See: https://datatracker.ietf.org/doc/bcp195/
208            .min_tls_version(tls::Version::TLS_1_2);
209
210        if let Some(timeout) = self.timeout {
211            http_client = http_client.timeout(timeout);
212        }
213
214        if let Some(read_timeout) = self.read_timeout {
215            http_client = http_client.read_timeout(read_timeout);
216        }
217
218        if self.disable_ssl_verification {
219            warn!("SSL verification disabled in the HTTP client!");
220            http_client = http_client.danger_accept_invalid_certs(true);
221        }
222
223        http_client = if self.disable_built_in_root_certificates {
224            info!("Built-in root certificates disabled in the HTTP client.");
225            http_client.tls_certs_only(self.additional_root_certificates.clone())
226        } else {
227            http_client.tls_certs_merge(self.additional_root_certificates.clone())
228        };
229
230        if let Some(p) = &self.proxy {
231            info!(proxy_url = p, "Setting the proxy for the HTTP client");
232            http_client = http_client.proxy(reqwest::Proxy::all(p.as_str())?);
233        }
234
235        Ok(http_client.build()?)
236    }
237}
238
239pub(super) async fn execute_request(
240    client: &reqwest::Client,
241    request: &http::Request<Bytes>,
242    timeout: Option<Duration>,
243    send_progress: SharedObservable<TransmissionProgress>,
244) -> Result<http::Response<Bytes>, HttpError> {
245    use std::convert::Infallible;
246
247    use futures_util::stream;
248
249    let request = request.clone();
250    let request = {
251        let mut request = if send_progress.subscriber_count() != 0 {
252            let content_length = request.body().len();
253            send_progress.update(|p| p.total += content_length);
254
255            // Make sure any concurrent futures in the same task get a chance
256            // to also add to the progress total before the first chunks are
257            // pulled out of the body stream.
258            tokio::task::yield_now().await;
259
260            let mut req = reqwest::Request::try_from(request.map(|body| {
261                let chunks = stream::iter(BytesChunks::new(body, 8192).map(
262                    move |chunk| -> Result<_, Infallible> {
263                        send_progress.update(|p| p.current += chunk.len());
264                        Ok(chunk)
265                    },
266                ));
267                reqwest::Body::wrap_stream(chunks)
268            }))?;
269
270            // When streaming the request, reqwest / hyper doesn't know how
271            // large the body is, so it doesn't set the content-length header
272            // (required by some servers). Set it manually.
273            req.headers_mut().insert(CONTENT_LENGTH, content_length.into());
274
275            req
276        } else {
277            reqwest::Request::try_from(request)?
278        };
279
280        *request.timeout_mut() = timeout;
281        request
282    };
283
284    let response = client.execute(request).await?;
285    Ok(response_to_http_response(response).await?)
286}
287
288struct BytesChunks {
289    bytes: Bytes,
290    size: usize,
291}
292
293impl BytesChunks {
294    fn new(bytes: Bytes, size: usize) -> Self {
295        assert_ne!(size, 0);
296        Self { bytes, size }
297    }
298}
299
300impl Iterator for BytesChunks {
301    type Item = Bytes;
302
303    fn next(&mut self) -> Option<Self::Item> {
304        if self.bytes.is_empty() {
305            None
306        } else if self.bytes.len() < self.size {
307            Some(mem::take(&mut self.bytes))
308        } else {
309            Some(self.bytes.split_to(self.size))
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use bytes::Bytes;
317
318    use super::BytesChunks;
319
320    #[test]
321    fn test_bytes_chunks() {
322        let bytes = Bytes::new();
323        assert!(BytesChunks::new(bytes, 1).collect::<Vec<_>>().is_empty());
324
325        let bytes = Bytes::from_iter([1, 2]);
326        assert_eq!(BytesChunks::new(bytes, 2).collect::<Vec<_>>(), [Bytes::from_iter([1, 2])]);
327
328        let bytes = Bytes::from_iter([1, 2]);
329        assert_eq!(BytesChunks::new(bytes, 3).collect::<Vec<_>>(), [Bytes::from_iter([1, 2])]);
330
331        let bytes = Bytes::from_iter([1, 2, 3]);
332        assert_eq!(
333            BytesChunks::new(bytes, 1).collect::<Vec<_>>(),
334            [Bytes::from_iter([1]), Bytes::from_iter([2]), Bytes::from_iter([3])]
335        );
336
337        let bytes = Bytes::from_iter([1, 2, 3]);
338        assert_eq!(
339            BytesChunks::new(bytes, 2).collect::<Vec<_>>(),
340            [Bytes::from_iter([1, 2]), Bytes::from_iter([3])]
341        );
342
343        let bytes = Bytes::from_iter([1, 2, 3, 4]);
344        assert_eq!(
345            BytesChunks::new(bytes, 2).collect::<Vec<_>>(),
346            [Bytes::from_iter([1, 2]), Bytes::from_iter([3, 4])]
347        );
348    }
349}