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