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