Skip to main content

matrix_sdk/http_client/
mod.rs

1// Copyright 2020 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    any::type_name,
17    borrow::Cow,
18    fmt::Debug,
19    num::NonZeroUsize,
20    sync::{
21        Arc,
22        atomic::{AtomicU64, Ordering},
23    },
24    time::Duration,
25};
26
27use bytes::{Bytes, BytesMut};
28use bytesize::ByteSize;
29use eyeball::SharedObservable;
30use http::Method;
31use matrix_sdk_base::SendOutsideWasm;
32use ruma::api::{
33    OutgoingRequest, OutgoingRequestExt, SupportedVersions,
34    auth_scheme::{self, AuthScheme, SendAccessToken},
35    error::{FromHttpResponseError, IntoHttpError},
36    path_builder,
37};
38use tokio::sync::{Semaphore, SemaphorePermit};
39use tracing::{Instrument, debug, error, field::debug, trace};
40
41use crate::{HttpResult, config::RequestConfig, error::HttpError};
42
43#[cfg(not(target_family = "wasm"))]
44mod native;
45#[cfg(target_family = "wasm")]
46mod wasm;
47
48#[cfg(not(target_family = "wasm"))]
49pub(crate) use native::HttpSettings;
50
51pub(crate) const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
52
53#[derive(Clone, Debug)]
54struct MaybeSemaphore(Arc<Option<Semaphore>>);
55
56#[allow(dead_code)] // false-positive lint: we never use it but only hold it for the drop
57struct MaybeSemaphorePermit<'a>(Option<SemaphorePermit<'a>>);
58
59impl MaybeSemaphore {
60    fn new(max: Option<NonZeroUsize>) -> Self {
61        let inner = max.map(|i| Semaphore::new(i.into()));
62        MaybeSemaphore(Arc::new(inner))
63    }
64
65    async fn acquire(&self) -> MaybeSemaphorePermit<'_> {
66        match self.0.as_ref() {
67            Some(inner) => {
68                // This can only ever error if the semaphore was closed, which
69                // we never do, so we can safely ignore any error case
70                MaybeSemaphorePermit(inner.acquire().await.ok())
71            }
72            None => MaybeSemaphorePermit(None),
73        }
74    }
75}
76
77#[derive(Clone, Debug)]
78pub(crate) struct HttpClient {
79    pub(crate) inner: reqwest::Client,
80    pub(crate) request_config: RequestConfig,
81    concurrent_request_semaphore: MaybeSemaphore,
82    next_request_id: Arc<AtomicU64>,
83}
84
85impl HttpClient {
86    pub(crate) fn new(inner: reqwest::Client, request_config: RequestConfig) -> Self {
87        HttpClient {
88            inner,
89            request_config,
90            concurrent_request_semaphore: MaybeSemaphore::new(
91                request_config.max_concurrent_requests,
92            ),
93            next_request_id: AtomicU64::new(0).into(),
94        }
95    }
96
97    fn get_request_id(&self) -> String {
98        let request_id = self.next_request_id.fetch_add(1, Ordering::SeqCst);
99        format!("REQ-{request_id}")
100    }
101
102    fn serialize_request<R>(
103        &self,
104        request: R,
105        config: RequestConfig,
106        homeserver: String,
107        access_token: Option<&str>,
108        path_builder_input: <R::PathBuilder as path_builder::PathBuilder>::Input<'_>,
109    ) -> Result<http::Request<Bytes>, IntoHttpError>
110    where
111        R: OutgoingRequest + Debug,
112        R::Authentication: SupportedAuthScheme,
113    {
114        trace!(request_type = type_name::<R>(), "Serializing request");
115
116        let send_access_token = match access_token {
117            Some(access_token) => match (config.force_auth, config.skip_auth) {
118                (true, true) | (true, false) => SendAccessToken::Always(access_token),
119                (false, true) => SendAccessToken::None,
120                (false, false) => SendAccessToken::IfRequired(access_token),
121            },
122            None => SendAccessToken::None,
123        };
124        let authentication_input = R::Authentication::authentication_input(send_access_token);
125
126        let request = request
127            .try_into_http_request::<BytesMut>(
128                &homeserver,
129                authentication_input,
130                path_builder_input,
131            )?
132            .map(|body| body.freeze());
133
134        Ok(request)
135    }
136
137    pub fn send<R>(
138        &self,
139        request: R,
140        config: Option<RequestConfig>,
141        homeserver: String,
142        access_token: Option<&str>,
143        path_builder_input: <R::PathBuilder as path_builder::PathBuilder>::Input<'_>,
144        send_progress: SharedObservable<TransmissionProgress>,
145    ) -> impl Future<Output = Result<R::IncomingResponse, HttpError>>
146    where
147        R: OutgoingRequest + Debug,
148        R::Authentication: SupportedAuthScheme,
149        HttpError: From<FromHttpResponseError<R::EndpointError>>,
150    {
151        // some functions split out so they only get compiled once, not
152        // monomorphized per request type
153        fn make_span(client: &HttpClient, config: &RequestConfig) -> tracing::Span {
154            tracing::info_span!(
155                "send",
156                uri = tracing::field::Empty,
157                ?config,
158                method = tracing::field::Empty,
159                request_id = client.get_request_id(),
160                request_size = tracing::field::Empty,
161                request_duration = tracing::field::Empty,
162                status = tracing::field::Empty,
163                response_size = tracing::field::Empty,
164                sentry_event_id = tracing::field::Empty
165            )
166        }
167        fn record_request_uri_and_size(request: &http::Request<Bytes>) {
168            let method = request.method();
169
170            let mut uri_parts = request.uri().clone().into_parts();
171
172            // Erase the query parameters for the sake of secrecy (in case a
173            // token is present).
174            if let Some(path_and_query) = &mut uri_parts.path_and_query {
175                *path_and_query =
176                    path_and_query.path().try_into().expect("path is valid PathAndQuery");
177            }
178
179            let uri = http::Uri::from_parts(uri_parts).expect("created from valid URI");
180
181            let span = tracing::Span::current();
182            span.record("method", debug(method)).record("uri", uri.to_string());
183
184            // POST, PUT, PATCH are the only methods that are reasonably used in
185            // conjunction with request bodies
186            if [Method::POST, Method::PUT, Method::PATCH].contains(method) {
187                let request_size = request.body().len().try_into().unwrap_or(u64::MAX);
188                span.record(
189                    "request_size",
190                    ByteSize(request_size).display().si_short().to_string(),
191                );
192            }
193        }
194        // these macros expand to a lot of code, also want to skip
195        // monomorphization for them even though they might look super simple
196        fn log_got_response() {
197            debug!("Got response");
198        }
199        fn log_error(e: &HttpError) {
200            error!("Error while sending request: {e:?}");
201        }
202
203        let config = match config {
204            Some(config) => config,
205            None => self.request_config,
206        };
207
208        async move {
209            let request = self
210                .serialize_request(request, config, homeserver, access_token, path_builder_input)
211                .map_err(HttpError::IntoHttp)?;
212            record_request_uri_and_size(&request);
213
214            // will be automatically dropped at the end of this function
215            let _handle = self.concurrent_request_semaphore.acquire().await;
216
217            // There's a bunch of state in send_request, factor out a pinned
218            // inner future to reduce the size of futures that await this
219            // function.
220            match Box::pin(self.send_request::<R>(request, config, send_progress)).await {
221                Ok(response) => {
222                    log_got_response();
223                    Ok(response)
224                }
225                Err(e) => {
226                    log_error(&e);
227                    Err(e)
228                }
229            }
230        }
231        .instrument(make_span(self, &config))
232    }
233}
234
235/// Progress of sending or receiving a payload.
236#[derive(Clone, Copy, Debug, Default)]
237pub struct TransmissionProgress {
238    /// How many bytes were already transferred.
239    pub current: usize,
240    /// How many bytes there are in total.
241    pub total: usize,
242}
243
244async fn response_to_http_response(
245    mut response: reqwest::Response,
246) -> Result<http::Response<Bytes>, reqwest::Error> {
247    let status = response.status();
248
249    let mut http_builder = http::Response::builder().status(status);
250    let headers = http_builder.headers_mut().expect("Can't get the response builder headers");
251
252    for (k, v) in response.headers_mut().drain() {
253        if let Some(key) = k {
254            headers.insert(key, v);
255        }
256    }
257
258    let body = response.bytes().await?;
259
260    Ok(http_builder.body(body).expect("Can't construct a response using the given body"))
261}
262
263/// Marker trait to identify the authentication schemes that the
264/// [`Client`](crate::Client) supports.
265///
266/// This trait can also be implemented for custom [`AuthScheme`]s if necessary.
267pub trait SupportedAuthScheme: AuthScheme {
268    /// Get the [`AuthScheme::Input`] from the access token.
269    fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_>;
270}
271
272impl SupportedAuthScheme for auth_scheme::NoAccessToken {
273    fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
274        access_token
275    }
276}
277
278impl SupportedAuthScheme for auth_scheme::AccessToken {
279    fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
280        access_token
281    }
282}
283
284impl SupportedAuthScheme for auth_scheme::AccessTokenOptional {
285    fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
286        access_token
287    }
288}
289
290impl SupportedAuthScheme for auth_scheme::AppserviceToken {
291    fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
292        access_token
293    }
294}
295
296impl SupportedAuthScheme for auth_scheme::AppserviceTokenOptional {
297    fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
298        access_token
299    }
300}
301
302impl SupportedAuthScheme for auth_scheme::NoAuthentication {
303    fn authentication_input(_access_token: SendAccessToken<'_>) -> Self::Input<'_> {}
304}
305
306/// Marker trait to identify the path builders that the
307/// [`Client`](crate::Client) supports.
308///
309/// This trait can also be implemented for custom
310/// [`PathBuilder`](path_builder::PathBuilder)s if necessary.
311pub trait SupportedPathBuilder: path_builder::PathBuilder {
312    /// Get the [`PathBuilder::Input`](path_builder::PathBuilder::Input) from
313    /// the [`Client`](crate::Client).
314    fn get_path_builder_input(
315        client: &crate::Client,
316        skip_auth: bool,
317    ) -> impl Future<Output = HttpResult<Self::Input<'static>>> + SendOutsideWasm;
318}
319
320impl SupportedPathBuilder for path_builder::VersionHistory {
321    async fn get_path_builder_input(
322        client: &crate::Client,
323        skip_auth: bool,
324    ) -> HttpResult<Cow<'static, SupportedVersions>> {
325        // We always enable "failsafe" mode for the GET /versions requests in
326        // this function. It disables trying to refresh the access token for
327        // those requests, to avoid possible deadlocks.
328
329        if !client.auth_ctx().has_valid_access_token() {
330            // Try to get the value in the cache.
331            if let Ok(Some(versions)) = client.supported_versions_cached().await {
332                return Ok(Cow::Owned(versions));
333            }
334
335            // The request will skip auth so we might not get all the supported
336            // features, so just fetch the supported versions and don't cache
337            // them.
338            let response = client.fetch_server_versions_inner(true, None).await?;
339
340            Ok(Cow::Owned(response.as_supported_versions()))
341        } else if skip_auth {
342            let cached_versions = client.supported_versions_cached().await;
343
344            let versions = if let Ok(Some(versions)) = cached_versions {
345                versions
346            } else {
347                // If we're skipping auth we might not get all the supported
348                // features, so just fetch the versions and don't cache them.
349                let request_config = RequestConfig::default().retry_limit(5).skip_auth();
350                let response =
351                    client.fetch_server_versions_inner(true, Some(request_config)).await?;
352
353                response.as_supported_versions()
354            };
355
356            Ok(Cow::Owned(versions))
357        } else {
358            client.supported_versions_inner(true).await.map(Cow::Owned)
359        }
360    }
361}
362
363impl SupportedPathBuilder for path_builder::SinglePath {
364    async fn get_path_builder_input(_client: &crate::Client, _skip_auth: bool) -> HttpResult<()> {
365        Ok(())
366    }
367}
368
369#[cfg(all(test, not(target_family = "wasm")))]
370mod tests {
371    use std::{
372        num::NonZeroUsize,
373        sync::{
374            Arc,
375            atomic::{AtomicU8, Ordering},
376        },
377        time::Duration,
378    };
379
380    use matrix_sdk_common::executor::spawn;
381    use matrix_sdk_test::{async_test, test_json};
382    use wiremock::{
383        Mock, Request, ResponseTemplate,
384        matchers::{method, path},
385    };
386
387    use crate::{
388        http_client::RequestConfig,
389        test_utils::{set_client_session, test_client_builder_with_server},
390    };
391
392    #[async_test]
393    async fn test_ensure_concurrent_request_limit_is_observed() {
394        let (client_builder, server) = test_client_builder_with_server().await;
395        let client = client_builder
396            .request_config(RequestConfig::default().max_concurrent_requests(NonZeroUsize::new(5)))
397            .build()
398            .await
399            .unwrap();
400
401        set_client_session(&client).await;
402
403        let counter = Arc::new(AtomicU8::new(0));
404        let inner_counter = counter.clone();
405
406        Mock::given(method("GET"))
407            .and(path("/_matrix/client/versions"))
408            .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
409            .mount(&server)
410            .await;
411
412        Mock::given(method("GET"))
413            .and(path("_matrix/client/r0/account/whoami"))
414            .respond_with(move |_req: &Request| {
415                inner_counter.fetch_add(1, Ordering::SeqCst);
416                // we stall the requests
417                ResponseTemplate::new(200).set_delay(Duration::from_secs(60))
418            })
419            .mount(&server)
420            .await;
421
422        let bg_task = spawn(async move {
423            futures_util::future::join_all((0..10).map(|_| client.whoami())).await
424        });
425
426        // give it some time to issue the requests
427        tokio::time::sleep(Duration::from_millis(300)).await;
428
429        assert_eq!(
430            counter.load(Ordering::SeqCst),
431            5,
432            "More requests passed than the limit we configured"
433        );
434        bg_task.abort();
435    }
436
437    #[async_test]
438    async fn test_ensure_no_max_concurrent_request_does_not_limit() {
439        let (client_builder, server) = test_client_builder_with_server().await;
440        let client = client_builder
441            .request_config(RequestConfig::default().max_concurrent_requests(None))
442            .build()
443            .await
444            .unwrap();
445
446        set_client_session(&client).await;
447
448        let counter = Arc::new(AtomicU8::new(0));
449        let inner_counter = counter.clone();
450
451        Mock::given(method("GET"))
452            .and(path("/_matrix/client/versions"))
453            .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
454            .mount(&server)
455            .await;
456
457        Mock::given(method("GET"))
458            .and(path("_matrix/client/r0/account/whoami"))
459            .respond_with(move |_req: &Request| {
460                inner_counter.fetch_add(1, Ordering::SeqCst);
461                ResponseTemplate::new(200).set_delay(Duration::from_secs(60))
462            })
463            .mount(&server)
464            .await;
465
466        let bg_task = spawn(async move {
467            futures_util::future::join_all((0..254).map(|_| client.whoami())).await
468        });
469
470        // give it some time to issue the requests
471        tokio::time::sleep(Duration::from_secs(1)).await;
472
473        assert_eq!(counter.load(Ordering::SeqCst), 254, "Not all requests passed through");
474        bg_task.abort();
475    }
476}