matrix_sdk/client/builder/
mod.rs

1// Copyright 2022 The Matrix.org Foundation C.I.C.
2// Copyright 2022 Kévin Commaille
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16mod homeserver_config;
17
18use std::{fmt, sync::Arc};
19
20use homeserver_config::*;
21use matrix_sdk_base::{store::StoreConfig, BaseClient};
22use ruma::{
23    api::{error::FromHttpResponseError, MatrixVersion},
24    OwnedServerName, ServerName,
25};
26use thiserror::Error;
27use tokio::sync::{broadcast, Mutex, OnceCell};
28use tracing::{debug, field::debug, instrument, Span};
29
30use super::{Client, ClientInner};
31#[cfg(feature = "e2e-encryption")]
32use crate::crypto::{CollectStrategy, TrustRequirement};
33#[cfg(feature = "e2e-encryption")]
34use crate::encryption::EncryptionSettings;
35#[cfg(not(target_arch = "wasm32"))]
36use crate::http_client::HttpSettings;
37use crate::{
38    authentication::{oauth::OAuthCtx, AuthCtx},
39    client::ClientServerCapabilities,
40    config::RequestConfig,
41    error::RumaApiError,
42    http_client::HttpClient,
43    send_queue::SendQueueData,
44    sliding_sync::VersionBuilder as SlidingSyncVersionBuilder,
45    HttpError, IdParseError,
46};
47
48/// Builder that allows creating and configuring various parts of a [`Client`].
49///
50/// When setting the `StateStore` it is up to the user to open/connect
51/// the storage backend before client creation.
52///
53/// # Examples
54///
55/// ```
56/// use matrix_sdk::Client;
57/// // To pass all the request through mitmproxy set the proxy and disable SSL
58/// // verification
59///
60/// let client_builder = Client::builder()
61///     .proxy("http://localhost:8080")
62///     .disable_ssl_verification();
63/// ```
64///
65/// # Example for using a custom http client
66///
67/// Note: setting a custom http client will ignore `user_agent`, `proxy`, and
68/// `disable_ssl_verification` - you'd need to set these yourself if you want
69/// them.
70///
71/// ```
72/// use std::sync::Arc;
73///
74/// use matrix_sdk::Client;
75///
76/// // setting up a custom http client
77/// let reqwest_builder = reqwest::ClientBuilder::new()
78///     .https_only(true)
79///     .no_proxy()
80///     .user_agent("MyApp/v3.0");
81///
82/// let client_builder =
83///     Client::builder().http_client(reqwest_builder.build()?);
84/// # anyhow::Ok(())
85/// ```
86#[must_use]
87#[derive(Clone, Debug)]
88pub struct ClientBuilder {
89    homeserver_cfg: Option<HomeserverConfig>,
90    sliding_sync_version_builder: SlidingSyncVersionBuilder,
91    http_cfg: Option<HttpConfig>,
92    store_config: BuilderStoreConfig,
93    request_config: RequestConfig,
94    respect_login_well_known: bool,
95    server_versions: Option<Box<[MatrixVersion]>>,
96    handle_refresh_tokens: bool,
97    base_client: Option<BaseClient>,
98    #[cfg(feature = "e2e-encryption")]
99    encryption_settings: EncryptionSettings,
100    #[cfg(feature = "e2e-encryption")]
101    room_key_recipient_strategy: CollectStrategy,
102    #[cfg(feature = "e2e-encryption")]
103    decryption_trust_requirement: TrustRequirement,
104    cross_process_store_locks_holder_name: String,
105}
106
107impl ClientBuilder {
108    const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
109
110    pub(crate) fn new() -> Self {
111        Self {
112            homeserver_cfg: None,
113            sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
114            http_cfg: None,
115            store_config: BuilderStoreConfig::Custom(StoreConfig::new(
116                Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
117            )),
118            request_config: Default::default(),
119            respect_login_well_known: true,
120            server_versions: None,
121            handle_refresh_tokens: false,
122            base_client: None,
123            #[cfg(feature = "e2e-encryption")]
124            encryption_settings: Default::default(),
125            #[cfg(feature = "e2e-encryption")]
126            room_key_recipient_strategy: Default::default(),
127            #[cfg(feature = "e2e-encryption")]
128            decryption_trust_requirement: TrustRequirement::Untrusted,
129            cross_process_store_locks_holder_name:
130                Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
131        }
132    }
133
134    /// Set the homeserver URL to use.
135    ///
136    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
137    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
138    /// [`Self::server_name_or_homeserver_url`].
139    /// If you set more than one, then whatever was set last will be used.
140    pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
141        self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
142        self
143    }
144
145    /// Set the server name to discover the homeserver from.
146    ///
147    /// We assume we can connect in HTTPS to that server. If that's not the
148    /// case, prefer using [`Self::insecure_server_name_no_tls`].
149    ///
150    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
151    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
152    /// [`Self::server_name_or_homeserver_url`].
153    /// If you set more than one, then whatever was set last will be used.
154    pub fn server_name(mut self, server_name: &ServerName) -> Self {
155        self.homeserver_cfg = Some(HomeserverConfig::ServerName {
156            server: server_name.to_owned(),
157            // Assume HTTPS if not specified.
158            protocol: UrlScheme::Https,
159        });
160        self
161    }
162
163    /// Set the server name to discover the homeserver from, assuming an HTTP
164    /// (not secured) scheme. This also relaxes OAuth 2.0 discovery checks to
165    /// allow HTTP schemes.
166    ///
167    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
168    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
169    /// [`Self::server_name_or_homeserver_url`].
170    /// If you set more than one, then whatever was set last will be used.
171    pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
172        self.homeserver_cfg = Some(HomeserverConfig::ServerName {
173            server: server_name.to_owned(),
174            protocol: UrlScheme::Http,
175        });
176        self
177    }
178
179    /// Set the server name to discover the homeserver from, falling back to
180    /// using it as a homeserver URL if discovery fails. When falling back to a
181    /// homeserver URL, a check is made to ensure that the server exists (unlike
182    /// [`Self::homeserver_url`], so you can guarantee that the client is ready
183    /// to use.
184    ///
185    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
186    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
187    /// [`Self::server_name_or_homeserver_url`].
188    /// If you set more than one, then whatever was set last will be used.
189    pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
190        self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
191            server_name_or_url.as_ref().to_owned(),
192        ));
193        self
194    }
195
196    /// Set sliding sync to a specific version.
197    pub fn sliding_sync_version_builder(
198        mut self,
199        version_builder: SlidingSyncVersionBuilder,
200    ) -> Self {
201        self.sliding_sync_version_builder = version_builder;
202        self
203    }
204
205    /// Set up the store configuration for a SQLite store.
206    #[cfg(feature = "sqlite")]
207    pub fn sqlite_store(
208        mut self,
209        path: impl AsRef<std::path::Path>,
210        passphrase: Option<&str>,
211    ) -> Self {
212        self.store_config = BuilderStoreConfig::Sqlite {
213            path: path.as_ref().to_owned(),
214            cache_path: None,
215            passphrase: passphrase.map(ToOwned::to_owned),
216        };
217        self
218    }
219
220    /// Set up the store configuration for a SQLite store with cached data
221    /// separated out from state/crypto data.
222    #[cfg(feature = "sqlite")]
223    pub fn sqlite_store_with_cache_path(
224        mut self,
225        path: impl AsRef<std::path::Path>,
226        cache_path: impl AsRef<std::path::Path>,
227        passphrase: Option<&str>,
228    ) -> Self {
229        self.store_config = BuilderStoreConfig::Sqlite {
230            path: path.as_ref().to_owned(),
231            cache_path: Some(cache_path.as_ref().to_owned()),
232            passphrase: passphrase.map(ToOwned::to_owned),
233        };
234        self
235    }
236
237    /// Set up the store configuration for a IndexedDB store.
238    #[cfg(feature = "indexeddb")]
239    pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
240        self.store_config = BuilderStoreConfig::IndexedDb {
241            name: name.to_owned(),
242            passphrase: passphrase.map(ToOwned::to_owned),
243        };
244        self
245    }
246
247    /// Set up the store configuration.
248    ///
249    /// The easiest way to get a [`StoreConfig`] is to use the
250    /// `make_store_config` method from one of the store crates.
251    ///
252    /// # Arguments
253    ///
254    /// * `store_config` - The configuration of the store.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// # use matrix_sdk_base::store::MemoryStore;
260    /// # let custom_state_store = MemoryStore::new();
261    /// use matrix_sdk::{config::StoreConfig, Client};
262    ///
263    /// let store_config =
264    ///     StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
265    ///         .state_store(custom_state_store);
266    /// let client_builder = Client::builder().store_config(store_config);
267    /// ```
268    pub fn store_config(mut self, store_config: StoreConfig) -> Self {
269        self.store_config = BuilderStoreConfig::Custom(store_config);
270        self
271    }
272
273    /// Update the client's homeserver URL with the discovery information
274    /// present in the login response, if any.
275    pub fn respect_login_well_known(mut self, value: bool) -> Self {
276        self.respect_login_well_known = value;
277        self
278    }
279
280    /// Set the default timeout, fail and retry behavior for all HTTP requests.
281    pub fn request_config(mut self, request_config: RequestConfig) -> Self {
282        self.request_config = request_config;
283        self
284    }
285
286    /// Set the proxy through which all the HTTP requests should go.
287    ///
288    /// Note, only HTTP proxies are supported.
289    ///
290    /// # Arguments
291    ///
292    /// * `proxy` - The HTTP URL of the proxy.
293    ///
294    /// # Examples
295    ///
296    /// ```no_run
297    /// use matrix_sdk::Client;
298    ///
299    /// let client_config = Client::builder().proxy("http://localhost:8080");
300    /// ```
301    #[cfg(not(target_arch = "wasm32"))]
302    pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
303        self.http_settings().proxy = Some(proxy.as_ref().to_owned());
304        self
305    }
306
307    /// Disable SSL verification for the HTTP requests.
308    #[cfg(not(target_arch = "wasm32"))]
309    pub fn disable_ssl_verification(mut self) -> Self {
310        self.http_settings().disable_ssl_verification = true;
311        self
312    }
313
314    /// Set a custom HTTP user agent for the client.
315    #[cfg(not(target_arch = "wasm32"))]
316    pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
317        self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
318        self
319    }
320
321    /// Add the given list of certificates to the certificate store of the HTTP
322    /// client.
323    ///
324    /// These additional certificates will be trusted and considered when
325    /// establishing a HTTP request.
326    ///
327    /// Internally this will call the
328    /// [`reqwest::ClientBuilder::add_root_certificate()`] method.
329    #[cfg(not(target_arch = "wasm32"))]
330    pub fn add_root_certificates(mut self, certificates: Vec<reqwest::Certificate>) -> Self {
331        self.http_settings().additional_root_certificates = certificates;
332        self
333    }
334
335    /// Don't trust any system root certificates, only trust the certificates
336    /// provided through
337    /// [`add_root_certificates`][ClientBuilder::add_root_certificates].
338    #[cfg(not(target_arch = "wasm32"))]
339    pub fn disable_built_in_root_certificates(mut self) -> Self {
340        self.http_settings().disable_built_in_root_certificates = true;
341        self
342    }
343
344    /// Specify a [`reqwest::Client`] instance to handle sending requests and
345    /// receiving responses.
346    ///
347    /// This method is mutually exclusive with
348    /// [`proxy()`][ClientBuilder::proxy],
349    /// [`disable_ssl_verification`][ClientBuilder::disable_ssl_verification],
350    /// [`add_root_certificates`][ClientBuilder::add_root_certificates],
351    /// [`disable_built_in_root_certificates`][ClientBuilder::disable_built_in_root_certificates],
352    /// and [`user_agent()`][ClientBuilder::user_agent].
353    pub fn http_client(mut self, client: reqwest::Client) -> Self {
354        self.http_cfg = Some(HttpConfig::Custom(client));
355        self
356    }
357
358    /// Specify the Matrix versions supported by the homeserver manually, rather
359    /// than `build()` doing it using a `get_supported_versions` request.
360    ///
361    /// This is helpful for test code that doesn't care to mock that endpoint.
362    pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
363        self.server_versions = Some(value.into_iter().collect());
364        self
365    }
366
367    #[cfg(not(target_arch = "wasm32"))]
368    fn http_settings(&mut self) -> &mut HttpSettings {
369        self.http_cfg.get_or_insert_with(Default::default).settings()
370    }
371
372    /// Handle [refreshing access tokens] automatically.
373    ///
374    /// By default, the `Client` forwards any error and doesn't handle errors
375    /// with the access token, which means that
376    /// [`Client::refresh_access_token()`] needs to be called manually to
377    /// refresh access tokens.
378    ///
379    /// Enabling this setting means that the `Client` will try to refresh the
380    /// token automatically, which means that:
381    ///
382    /// * If refreshing the token fails, the error is forwarded, so any endpoint
383    ///   can return [`HttpError::RefreshToken`]. If an [`UnknownToken`] error
384    ///   is encountered, it means that the user needs to be logged in again.
385    ///
386    /// * The access token and refresh token need to be watched for changes,
387    ///   using the authentication API's `session_tokens_stream()` for example,
388    ///   to be able to [restore the session] later.
389    ///
390    /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
391    /// [`UnknownToken`]: ruma::api::client::error::ErrorKind::UnknownToken
392    /// [restore the session]: Client::restore_session
393    pub fn handle_refresh_tokens(mut self) -> Self {
394        self.handle_refresh_tokens = true;
395        self
396    }
397
398    /// Public for test only
399    #[doc(hidden)]
400    pub fn base_client(mut self, base_client: BaseClient) -> Self {
401        self.base_client = Some(base_client);
402        self
403    }
404
405    /// Enables specific encryption settings that will persist throughout the
406    /// entire lifetime of the `Client`.
407    #[cfg(feature = "e2e-encryption")]
408    pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
409        self.encryption_settings = settings;
410        self
411    }
412
413    /// Set the strategy to be used for picking recipient devices, when sending
414    /// an encrypted message.
415    #[cfg(feature = "e2e-encryption")]
416    pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
417        self.room_key_recipient_strategy = strategy;
418        self
419    }
420
421    /// Set the trust requirement to be used when decrypting events.
422    #[cfg(feature = "e2e-encryption")]
423    pub fn with_decryption_trust_requirement(
424        mut self,
425        trust_requirement: TrustRequirement,
426    ) -> Self {
427        self.decryption_trust_requirement = trust_requirement;
428        self
429    }
430
431    /// Set the cross-process store locks holder name.
432    ///
433    /// The SDK provides cross-process store locks (see
434    /// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
435    /// `holder_name` will be the value used for all cross-process store locks
436    /// used by the `Client` being built.
437    ///
438    /// If 2 concurrent `Client`s are running in 2 different process, this
439    /// method must be called with different `hold_name` values.
440    pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self {
441        self.cross_process_store_locks_holder_name = holder_name;
442        self
443    }
444
445    /// Create a [`Client`] with the options set on this builder.
446    ///
447    /// # Errors
448    ///
449    /// This method can fail for two general reasons:
450    ///
451    /// * Invalid input: a missing or invalid homeserver URL or invalid proxy
452    ///   URL
453    /// * HTTP error: If you supplied a user ID instead of a homeserver URL, a
454    ///   server discovery request is made which can fail; if you didn't set
455    ///   [`server_versions(false)`][Self::server_versions], that amounts to
456    ///   another request that can fail
457    #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
458    pub async fn build(self) -> Result<Client, ClientBuildError> {
459        debug!("Starting to build the Client");
460
461        let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
462        Span::current().record("homeserver", debug(&homeserver_cfg));
463
464        #[cfg_attr(target_arch = "wasm32", allow(clippy::infallible_destructuring_match))]
465        let inner_http_client = match self.http_cfg.unwrap_or_default() {
466            #[cfg(not(target_arch = "wasm32"))]
467            HttpConfig::Settings(mut settings) => {
468                settings.timeout = self.request_config.timeout;
469                settings.make_client()?
470            }
471            HttpConfig::Custom(c) => c,
472        };
473
474        let base_client = if let Some(base_client) = self.base_client {
475            base_client
476        } else {
477            #[allow(unused_mut)]
478            let mut client = BaseClient::new(
479                build_store_config(self.store_config, &self.cross_process_store_locks_holder_name)
480                    .await?,
481            );
482
483            #[cfg(feature = "e2e-encryption")]
484            {
485                client.room_key_recipient_strategy = self.room_key_recipient_strategy;
486                client.decryption_trust_requirement = self.decryption_trust_requirement;
487            }
488
489            client
490        };
491
492        let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
493
494        #[allow(unused_variables)]
495        let HomeserverDiscoveryResult { server, homeserver, supported_versions } =
496            homeserver_cfg.discover(&http_client).await?;
497
498        let sliding_sync_version = {
499            let supported_versions = match supported_versions {
500                Some(versions) => Some(versions),
501                None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
502                    Some(get_supported_versions(&homeserver, &http_client).await?)
503                }
504                None => None,
505            };
506
507            let version = self.sliding_sync_version_builder.build(supported_versions.as_ref())?;
508
509            tracing::info!(?version, "selected sliding sync version");
510
511            version
512        };
513
514        let allow_insecure_oauth = homeserver.scheme() == "http";
515
516        let auth_ctx = Arc::new(AuthCtx {
517            handle_refresh_tokens: self.handle_refresh_tokens,
518            refresh_token_lock: Arc::new(Mutex::new(Ok(()))),
519            session_change_sender: broadcast::Sender::new(1),
520            auth_data: OnceCell::default(),
521            tokens: OnceCell::default(),
522            reload_session_callback: OnceCell::default(),
523            save_session_callback: OnceCell::default(),
524            oauth: OAuthCtx::new(allow_insecure_oauth),
525        });
526
527        // Enable the send queue by default.
528        let send_queue = Arc::new(SendQueueData::new(true));
529
530        let server_capabilities = ClientServerCapabilities {
531            server_versions: self.server_versions,
532            unstable_features: None,
533        };
534
535        let event_cache = OnceCell::new();
536        let inner = ClientInner::new(
537            auth_ctx,
538            server,
539            homeserver,
540            sliding_sync_version,
541            http_client,
542            base_client,
543            server_capabilities,
544            self.respect_login_well_known,
545            event_cache,
546            send_queue,
547            #[cfg(feature = "e2e-encryption")]
548            self.encryption_settings,
549            self.cross_process_store_locks_holder_name,
550        )
551        .await;
552
553        debug!("Done building the Client");
554
555        Ok(Client { inner })
556    }
557}
558
559/// Creates a server name from a user supplied string. The string is first
560/// sanitized by removing whitespace, the http(s) scheme and any trailing
561/// slashes before being parsed.
562pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
563    ServerName::parse(
564        s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
565    )
566}
567
568#[allow(clippy::unused_async, unused)] // False positive when building with !sqlite & !indexeddb
569async fn build_store_config(
570    builder_config: BuilderStoreConfig,
571    cross_process_store_locks_holder_name: &str,
572) -> Result<StoreConfig, ClientBuildError> {
573    #[allow(clippy::infallible_destructuring_match)]
574    let store_config = match builder_config {
575        #[cfg(feature = "sqlite")]
576        BuilderStoreConfig::Sqlite { path, cache_path, passphrase } => {
577            let store_config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
578                .state_store(
579                    matrix_sdk_sqlite::SqliteStateStore::open(&path, passphrase.as_deref()).await?,
580                )
581                .event_cache_store(
582                    matrix_sdk_sqlite::SqliteEventCacheStore::open(
583                        cache_path.as_ref().unwrap_or(&path),
584                        passphrase.as_deref(),
585                    )
586                    .await?,
587                );
588
589            #[cfg(feature = "e2e-encryption")]
590            let store_config = store_config.crypto_store(
591                matrix_sdk_sqlite::SqliteCryptoStore::open(&path, passphrase.as_deref()).await?,
592            );
593
594            store_config
595        }
596
597        #[cfg(feature = "indexeddb")]
598        BuilderStoreConfig::IndexedDb { name, passphrase } => {
599            build_indexeddb_store_config(
600                &name,
601                passphrase.as_deref(),
602                cross_process_store_locks_holder_name,
603            )
604            .await?
605        }
606
607        BuilderStoreConfig::Custom(config) => config,
608    };
609    Ok(store_config)
610}
611
612// The indexeddb stores only implement `IntoStateStore` and `IntoCryptoStore` on
613// wasm32, so this only compiles there.
614#[cfg(all(target_arch = "wasm32", feature = "indexeddb"))]
615async fn build_indexeddb_store_config(
616    name: &str,
617    passphrase: Option<&str>,
618    cross_process_store_locks_holder_name: &str,
619) -> Result<StoreConfig, ClientBuildError> {
620    let cross_process_store_locks_holder_name = cross_process_store_locks_holder_name.to_owned();
621
622    #[cfg(feature = "e2e-encryption")]
623    let store_config = {
624        let (state_store, crypto_store) =
625            matrix_sdk_indexeddb::open_stores_with_name(name, passphrase).await?;
626        StoreConfig::new(cross_process_store_locks_holder_name)
627            .state_store(state_store)
628            .crypto_store(crypto_store)
629    };
630
631    #[cfg(not(feature = "e2e-encryption"))]
632    let store_config = {
633        let state_store = matrix_sdk_indexeddb::open_state_store(name, passphrase).await?;
634        StoreConfig::new(cross_process_store_locks_holder_name).state_store(state_store)
635    };
636
637    let store_config = {
638        tracing::warn!("The IndexedDB backend does not implement an event cache store, falling back to the in-memory event cache store…");
639        store_config.event_cache_store(matrix_sdk_base::event_cache::store::MemoryStore::new())
640    };
641
642    Ok(store_config)
643}
644
645#[cfg(all(not(target_arch = "wasm32"), feature = "indexeddb"))]
646#[allow(clippy::unused_async)]
647async fn build_indexeddb_store_config(
648    _name: &str,
649    _passphrase: Option<&str>,
650    _event_cache_store_lock_holder_name: &str,
651) -> Result<StoreConfig, ClientBuildError> {
652    panic!("the IndexedDB is only available on the 'wasm32' arch")
653}
654
655#[derive(Clone, Debug)]
656enum HttpConfig {
657    #[cfg(not(target_arch = "wasm32"))]
658    Settings(HttpSettings),
659    Custom(reqwest::Client),
660}
661
662#[cfg(not(target_arch = "wasm32"))]
663impl HttpConfig {
664    fn settings(&mut self) -> &mut HttpSettings {
665        match self {
666            Self::Settings(s) => s,
667            Self::Custom(_) => {
668                *self = Self::default();
669                match self {
670                    Self::Settings(s) => s,
671                    Self::Custom(_) => unreachable!(),
672                }
673            }
674        }
675    }
676}
677
678impl Default for HttpConfig {
679    fn default() -> Self {
680        #[cfg(not(target_arch = "wasm32"))]
681        return Self::Settings(HttpSettings::default());
682
683        #[cfg(target_arch = "wasm32")]
684        return Self::Custom(reqwest::Client::new());
685    }
686}
687
688#[derive(Clone)]
689enum BuilderStoreConfig {
690    #[cfg(feature = "sqlite")]
691    Sqlite {
692        path: std::path::PathBuf,
693        cache_path: Option<std::path::PathBuf>,
694        passphrase: Option<String>,
695    },
696    #[cfg(feature = "indexeddb")]
697    IndexedDb {
698        name: String,
699        passphrase: Option<String>,
700    },
701    Custom(StoreConfig),
702}
703
704#[cfg(not(tarpaulin_include))]
705impl fmt::Debug for BuilderStoreConfig {
706    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707        #[allow(clippy::infallible_destructuring_match)]
708        match self {
709            #[cfg(feature = "sqlite")]
710            Self::Sqlite { path, .. } => {
711                f.debug_struct("Sqlite").field("path", path).finish_non_exhaustive()
712            }
713            #[cfg(feature = "indexeddb")]
714            Self::IndexedDb { name, .. } => {
715                f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
716            }
717            Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
718        }
719    }
720}
721
722/// Errors that can happen in [`ClientBuilder::build`].
723#[derive(Debug, Error)]
724pub enum ClientBuildError {
725    /// No homeserver or user ID was configured
726    #[error("no homeserver or user ID was configured")]
727    MissingHomeserver,
728
729    /// The supplied server name was invalid.
730    #[error("The supplied server name is invalid")]
731    InvalidServerName,
732
733    /// Error looking up the .well-known endpoint on auto-discovery
734    #[error("Error looking up the .well-known endpoint on auto-discovery")]
735    AutoDiscovery(FromHttpResponseError<RumaApiError>),
736
737    /// Error when building the sliding sync version.
738    #[error(transparent)]
739    SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
740
741    /// An error encountered when trying to parse the homeserver url.
742    #[error(transparent)]
743    Url(#[from] url::ParseError),
744
745    /// Error doing an HTTP request.
746    #[error(transparent)]
747    Http(#[from] HttpError),
748
749    /// Error opening the indexeddb store.
750    #[cfg(feature = "indexeddb")]
751    #[error(transparent)]
752    IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
753
754    /// Error opening the sqlite store.
755    #[cfg(feature = "sqlite")]
756    #[error(transparent)]
757    SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
758}
759
760// The http mocking library is not supported for wasm32
761#[cfg(all(test, not(target_arch = "wasm32")))]
762pub(crate) mod tests {
763    use assert_matches::assert_matches;
764    use matrix_sdk_test::{async_test, test_json};
765    use serde_json::{json_internal, Value as JsonValue};
766    use wiremock::{
767        matchers::{method, path},
768        Mock, MockServer, ResponseTemplate,
769    };
770
771    use super::*;
772    use crate::sliding_sync::Version as SlidingSyncVersion;
773
774    #[test]
775    fn test_sanitize_server_name() {
776        assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
777        assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
778        assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
779        assert_eq!(
780            sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
781            "matrix.server.org"
782        );
783        assert_eq!(
784            sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
785            "matrix.server.org"
786        );
787        assert_eq!(
788            sanitize_server_name("  https://matrix.server.org// ").unwrap().as_str(),
789            "matrix.server.org"
790        );
791        assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
792    }
793
794    // Note: Due to a limitation of the http mocking library the following tests all
795    // supply an http:// url, to `server_name_or_homeserver_url` rather than the plain server name,
796    // otherwise  the builder will prepend https:// and the request will fail. In practice, this
797    // isn't a problem as the builder first strips the scheme and then checks if the
798    // name is a valid server name, so it is a close enough approximation.
799
800    #[async_test]
801    async fn test_discovery_invalid_server() {
802        // Given a new client builder.
803        let mut builder = ClientBuilder::new();
804
805        // When building a client with an invalid server name.
806        builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
807        let error = builder.build().await.unwrap_err();
808
809        // Then the operation should fail due to the invalid server name.
810        assert_matches!(error, ClientBuildError::InvalidServerName);
811    }
812
813    #[async_test]
814    async fn test_discovery_no_server() {
815        // Given a new client builder.
816        let mut builder = ClientBuilder::new();
817
818        // When building a client with a valid server name that doesn't exist.
819        builder = builder.server_name_or_homeserver_url("localhost:3456");
820        let error = builder.build().await.unwrap_err();
821
822        // Then the operation should fail with an HTTP error.
823        println!("{error}");
824        assert_matches!(error, ClientBuildError::Http(_));
825    }
826
827    #[async_test]
828    async fn test_discovery_web_server() {
829        // Given a random web server that isn't a Matrix homeserver or hosting the
830        // well-known file for one.
831        let server = MockServer::start().await;
832        let mut builder = ClientBuilder::new();
833
834        // When building a client with the server's URL.
835        builder = builder.server_name_or_homeserver_url(server.uri());
836        let error = builder.build().await.unwrap_err();
837
838        // Then the operation should fail with a server discovery error.
839        assert_matches!(error, ClientBuildError::AutoDiscovery(FromHttpResponseError::Server(_)));
840    }
841
842    #[async_test]
843    async fn test_discovery_direct_legacy() {
844        // Given a homeserver without a well-known file.
845        let homeserver = make_mock_homeserver().await;
846        let mut builder = ClientBuilder::new();
847
848        // When building a client with the server's URL.
849        builder = builder.server_name_or_homeserver_url(homeserver.uri());
850        let _client = builder.build().await.unwrap();
851
852        // Then a client should be built with native support for sliding sync.
853        assert!(_client.sliding_sync_version().is_native());
854    }
855
856    #[async_test]
857    async fn test_discovery_well_known_parse_error() {
858        // Given a base server with a well-known file that has errors.
859        let server = MockServer::start().await;
860        let homeserver = make_mock_homeserver().await;
861        let mut builder = ClientBuilder::new();
862
863        let well_known = make_well_known_json(&homeserver.uri());
864        let bad_json = well_known.to_string().replace(',', "");
865        Mock::given(method("GET"))
866            .and(path("/.well-known/matrix/client"))
867            .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
868            .mount(&server)
869            .await;
870
871        // When building a client with the base server.
872        builder = builder.server_name_or_homeserver_url(server.uri());
873        let error = builder.build().await.unwrap_err();
874
875        // Then the operation should fail due to the well-known file's contents.
876        assert_matches!(
877            error,
878            ClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(_))
879        );
880    }
881
882    #[async_test]
883    async fn test_discovery_well_known_legacy() {
884        // Given a base server with a well-known file that points to a homeserver that
885        // doesn't support sliding sync.
886        let server = MockServer::start().await;
887        let homeserver = make_mock_homeserver().await;
888        let mut builder = ClientBuilder::new();
889
890        Mock::given(method("GET"))
891            .and(path("/.well-known/matrix/client"))
892            .respond_with(
893                ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
894            )
895            .mount(&server)
896            .await;
897
898        // When building a client with the base server.
899        builder = builder.server_name_or_homeserver_url(server.uri());
900        let client = builder.build().await.unwrap();
901
902        // Then a client should be built with native support for sliding sync.
903        // It's native support because it's the default. Nothing is checked here.
904        assert!(client.sliding_sync_version().is_native());
905    }
906
907    #[async_test]
908    async fn test_sliding_sync_discover_native() {
909        // Given a homeserver with a `/versions` file.
910        let homeserver = make_mock_homeserver().await;
911        let mut builder = ClientBuilder::new();
912
913        // When building the client with sliding sync to auto-discover the
914        // native version.
915        builder = builder
916            .server_name_or_homeserver_url(homeserver.uri())
917            .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
918
919        let client = builder.build().await.unwrap();
920
921        // Then, sliding sync has the correct native version.
922        assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
923    }
924
925    #[async_test]
926    #[cfg(feature = "e2e-encryption")]
927    async fn test_set_up_decryption_trust_requirement_cross_signed() {
928        let homeserver = make_mock_homeserver().await;
929        let builder = ClientBuilder::new()
930            .server_name_or_homeserver_url(homeserver.uri())
931            .with_decryption_trust_requirement(TrustRequirement::CrossSigned);
932
933        let client = builder.build().await.unwrap();
934        assert_matches!(
935            client.base_client().decryption_trust_requirement,
936            TrustRequirement::CrossSigned
937        );
938    }
939
940    #[async_test]
941    #[cfg(feature = "e2e-encryption")]
942    async fn test_set_up_decryption_trust_requirement_untrusted() {
943        let homeserver = make_mock_homeserver().await;
944
945        let builder = ClientBuilder::new()
946            .server_name_or_homeserver_url(homeserver.uri())
947            .with_decryption_trust_requirement(TrustRequirement::Untrusted);
948
949        let client = builder.build().await.unwrap();
950        assert_matches!(
951            client.base_client().decryption_trust_requirement,
952            TrustRequirement::Untrusted
953        );
954    }
955
956    /* Helper functions */
957
958    async fn make_mock_homeserver() -> MockServer {
959        let homeserver = MockServer::start().await;
960        Mock::given(method("GET"))
961            .and(path("/_matrix/client/versions"))
962            .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
963            .mount(&homeserver)
964            .await;
965        Mock::given(method("GET"))
966            .and(path("/_matrix/client/r0/login"))
967            .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
968            .mount(&homeserver)
969            .await;
970        homeserver
971    }
972
973    fn make_well_known_json(homeserver_url: &str) -> JsonValue {
974        ::serde_json::Value::Object({
975            let mut object = ::serde_json::Map::new();
976            let _ = object.insert(
977                "m.homeserver".into(),
978                json_internal!({
979                    "base_url": homeserver_url
980                }),
981            );
982
983            object
984        })
985    }
986
987    #[async_test]
988    async fn test_cross_process_store_locks_holder_name() {
989        {
990            let homeserver = make_mock_homeserver().await;
991            let client =
992                ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
993
994            assert_eq!(client.cross_process_store_locks_holder_name(), "main");
995        }
996
997        {
998            let homeserver = make_mock_homeserver().await;
999            let client = ClientBuilder::new()
1000                .homeserver_url(homeserver.uri())
1001                .cross_process_store_locks_holder_name("foo".to_owned())
1002                .build()
1003                .await
1004                .unwrap();
1005
1006            assert_eq!(client.cross_process_store_locks_holder_name(), "foo");
1007        }
1008    }
1009}