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