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