1mod 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::{
25 collections::BTreeSet,
26 fmt,
27 sync::{Arc, RwLock as StdRwLock},
28};
29
30#[cfg(feature = "sqlite")]
31use futures_util::try_join;
32use homeserver_config::*;
33#[cfg(feature = "e2e-encryption")]
34use matrix_sdk_base::crypto::DecryptionSettings;
35#[cfg(feature = "experimental-x509-identity-verification")]
36use matrix_sdk_base::crypto::x509::{RawX509Signer, RawX509Verifier};
37#[cfg(feature = "e2e-encryption")]
38use matrix_sdk_base::crypto::{CollectStrategy, TrustRequirement};
39use matrix_sdk_base::{
40 BaseClient, DmRoomDefinition, ThreadingSupport, store::StoreConfig, ttl::TtlValue,
41};
42use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
43#[cfg(feature = "sqlite")]
44use matrix_sdk_sqlite::SqliteStoreConfig;
45#[cfg(not(target_family = "wasm"))]
46use reqwest::Certificate;
47use ruma::{
48 OwnedServerName, ServerName,
49 api::{MatrixVersion, SupportedVersions, error::FromHttpResponseError},
50 presence::PresenceState,
51};
52use thiserror::Error;
53#[cfg(feature = "experimental-search")]
54use tokio::sync::Mutex;
55use tokio::sync::OnceCell;
56use tracing::{Span, debug, field::debug, instrument};
57
58use super::{Client, ClientInner};
59#[cfg(feature = "e2e-encryption")]
60use crate::encryption::EncryptionSettings;
61#[cfg(not(target_family = "wasm"))]
62use crate::http_client::HttpSettings;
63#[cfg(feature = "experimental-search")]
64use crate::search_index::SearchIndex;
65#[cfg(feature = "experimental-search")]
66use crate::search_index::SearchIndexStoreKind;
67use crate::{
68 HttpError, IdParseError,
69 authentication::AuthCtx,
70 client::caches::CachedValue::{Cached, NotSet},
71 config::RequestConfig,
72 error::RumaApiError,
73 http_client::HttpClient,
74 media::{DefaultMediaFetcher, MediaFetcher},
75 send_queue::SendQueueData,
76 sliding_sync::VersionBuilder as SlidingSyncVersionBuilder,
77};
78
79#[must_use]
118#[derive(Clone, Debug)]
119pub struct ClientBuilder {
120 homeserver_cfg: Option<HomeserverConfig>,
121 sliding_sync_version_builder: SlidingSyncVersionBuilder,
122 http_cfg: Option<HttpConfig>,
123 store_config: BuilderStoreConfig,
124 request_config: RequestConfig,
125 respect_login_well_known: bool,
126 well_known_lookup_disabled: bool,
127 server_versions: Option<BTreeSet<MatrixVersion>>,
128 handle_refresh_tokens: bool,
129 base_client: Option<BaseClient>,
130 #[cfg(feature = "e2e-encryption")]
131 encryption_settings: EncryptionSettings,
132 #[cfg(feature = "e2e-encryption")]
133 room_key_recipient_strategy: CollectStrategy,
134 #[cfg(feature = "e2e-encryption")]
135 decryption_settings: DecryptionSettings,
136 #[cfg(feature = "e2e-encryption")]
137 enable_share_history_on_invite: bool,
138 enable_automatic_back_pagination: bool,
139 cross_process_lock_config: CrossProcessLockConfig,
140 threading_support: ThreadingSupport,
141 #[cfg(feature = "experimental-search")]
142 search_index_store_kind: SearchIndexStoreKind,
143 #[cfg(feature = "experimental-x509-identity-verification")]
144 x509_signer: Option<Arc<dyn RawX509Signer>>,
145 #[cfg(feature = "experimental-x509-identity-verification")]
146 x509_verifier: Option<Arc<dyn RawX509Verifier>>,
147 dm_room_definition: DmRoomDefinition,
148 media_fetcher: Arc<dyn MediaFetcher>,
149}
150
151impl ClientBuilder {
152 const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
153
154 pub(crate) fn new() -> Self {
155 Self {
156 homeserver_cfg: None,
157 sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
158 http_cfg: None,
159 store_config: BuilderStoreConfig::Custom(StoreConfig::new(
160 CrossProcessLockConfig::multi_process(
161 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME,
162 ),
163 )),
164 request_config: Default::default(),
165 respect_login_well_known: true,
166 well_known_lookup_disabled: false,
167 server_versions: None,
168 handle_refresh_tokens: false,
169 base_client: None,
170 #[cfg(feature = "e2e-encryption")]
171 encryption_settings: Default::default(),
172 #[cfg(feature = "e2e-encryption")]
173 room_key_recipient_strategy: Default::default(),
174 #[cfg(feature = "e2e-encryption")]
175 decryption_settings: DecryptionSettings {
176 sender_device_trust_requirement: TrustRequirement::Untrusted,
177 },
178 #[cfg(feature = "e2e-encryption")]
179 enable_share_history_on_invite: true,
180 enable_automatic_back_pagination: false,
181 cross_process_lock_config: CrossProcessLockConfig::MultiProcess {
182 holder_name: Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
183 },
184 threading_support: ThreadingSupport::Disabled,
185 #[cfg(feature = "experimental-search")]
186 search_index_store_kind: SearchIndexStoreKind::InMemory,
187 #[cfg(feature = "experimental-x509-identity-verification")]
188 x509_signer: None,
189 #[cfg(feature = "experimental-x509-identity-verification")]
190 x509_verifier: None,
191 dm_room_definition: DmRoomDefinition::MatrixSpec,
192 media_fetcher: Arc::new(DefaultMediaFetcher),
193 }
194 }
195
196 pub fn media_fetcher(mut self, media_fetcher: Arc<dyn MediaFetcher>) -> Self {
199 self.media_fetcher = media_fetcher.clone();
200 self
201 }
202
203 pub fn dm_room_definition(mut self, dm_room_definition: DmRoomDefinition) -> Self {
207 self.dm_room_definition = dm_room_definition;
208 self
209 }
210
211 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
222 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
223 self
224 }
225
226 pub fn server_name(mut self, server_name: &ServerName) -> Self {
240 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
241 server: server_name.to_owned(),
242 protocol: UrlScheme::Https,
244 });
245 self
246 }
247
248 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
261 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
262 server: server_name.to_owned(),
263 protocol: UrlScheme::Http,
264 });
265 self
266 }
267
268 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
284 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
285 server_name_or_url.as_ref().to_owned(),
286 ));
287 self
288 }
289
290 pub fn sliding_sync_version_builder(
292 mut self,
293 version_builder: SlidingSyncVersionBuilder,
294 ) -> Self {
295 self.sliding_sync_version_builder = version_builder;
296 self
297 }
298
299 #[cfg(feature = "sqlite")]
301 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
302 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
303 self.store_config =
304 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
305
306 self
307 }
308
309 #[cfg(feature = "sqlite")]
312 pub fn sqlite_store_with_cache_path(
313 mut self,
314 path: impl AsRef<Path>,
315 cache_path: impl AsRef<Path>,
316 passphrase: Option<&str>,
317 ) -> Self {
318 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
319 self.store_config = BuilderStoreConfig::Sqlite {
320 config: sqlite_store_config,
321 cache_path: Some(cache_path.as_ref().to_owned()),
322 };
323
324 self
325 }
326
327 #[cfg(feature = "sqlite")]
330 pub fn sqlite_store_with_config_and_cache_path(
331 mut self,
332 config: SqliteStoreConfig,
333 cache_path: Option<impl AsRef<Path>>,
334 ) -> Self {
335 self.store_config = BuilderStoreConfig::Sqlite {
336 config,
337 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
338 };
339
340 self
341 }
342
343 #[cfg(feature = "indexeddb")]
345 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
346 self.store_config = BuilderStoreConfig::IndexedDb {
347 name: name.to_owned(),
348 passphrase: passphrase.map(ToOwned::to_owned),
349 };
350 self
351 }
352
353 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
377 self.store_config = BuilderStoreConfig::Custom(store_config);
378 self
379 }
380
381 pub fn respect_login_well_known(mut self, value: bool) -> Self {
384 self.respect_login_well_known = value;
385 self
386 }
387
388 pub fn disable_well_known_lookup(mut self, disable: bool) -> Self {
417 self.well_known_lookup_disabled = disable;
418 self
419 }
420
421 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
423 self.request_config = request_config;
424 self
425 }
426
427 #[cfg(not(target_family = "wasm"))]
443 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
444 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
445 self
446 }
447
448 #[cfg(not(target_family = "wasm"))]
450 pub fn disable_ssl_verification(mut self) -> Self {
451 self.http_settings().disable_ssl_verification = true;
452 self
453 }
454
455 #[cfg(not(target_family = "wasm"))]
457 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
458 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
459 self
460 }
461
462 #[cfg(not(target_family = "wasm"))]
471 pub fn add_root_certificates(mut self, certificates: Vec<Certificate>) -> Self {
472 self.http_settings().additional_root_certificates = certificates;
473 self
474 }
475
476 #[cfg(not(target_family = "wasm"))]
480 pub fn disable_built_in_root_certificates(mut self) -> Self {
481 self.http_settings().disable_built_in_root_certificates = true;
482 self
483 }
484
485 pub fn http_client(mut self, client: reqwest::Client) -> Self {
495 self.http_cfg = Some(HttpConfig::Custom(client));
496 self
497 }
498
499 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
504 self.server_versions = Some(value.into_iter().collect());
505 self
506 }
507
508 #[cfg(not(target_family = "wasm"))]
509 fn http_settings(&mut self) -> &mut HttpSettings {
510 self.http_cfg.get_or_insert_with(Default::default).settings()
511 }
512
513 pub fn handle_refresh_tokens(mut self) -> Self {
535 self.handle_refresh_tokens = true;
536 self
537 }
538
539 #[doc(hidden)]
541 pub fn base_client(mut self, base_client: BaseClient) -> Self {
542 self.base_client = Some(base_client);
543 self
544 }
545
546 #[cfg(feature = "e2e-encryption")]
549 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
550 self.encryption_settings = settings;
551 self
552 }
553
554 #[cfg(feature = "e2e-encryption")]
557 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
558 self.room_key_recipient_strategy = strategy;
559 self
560 }
561
562 #[cfg(feature = "e2e-encryption")]
564 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
565 self.decryption_settings = decryption_settings;
566 self
567 }
568
569 #[cfg(feature = "e2e-encryption")]
577 pub fn with_enable_share_history_on_invite(
578 mut self,
579 enable_share_history_on_invite: bool,
580 ) -> Self {
581 self.enable_share_history_on_invite = enable_share_history_on_invite;
582 self
583 }
584
585 pub fn with_enable_automatic_back_pagination(mut self, enable: bool) -> Self {
591 self.enable_automatic_back_pagination = enable;
592 self
593 }
594
595 pub fn cross_process_store_config(
605 mut self,
606 cross_process_store_config: CrossProcessLockConfig,
607 ) -> Self {
608 self.cross_process_lock_config = cross_process_store_config;
609 self
610 }
611
612 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
616 self.threading_support = threading_support;
617 self
618 }
619
620 #[cfg(feature = "experimental-search")]
622 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
623 self.search_index_store_kind = kind;
624 self
625 }
626
627 #[cfg(feature = "experimental-x509-identity-verification")]
630 pub fn with_x509_signer(mut self, x509_signer: Option<Arc<dyn RawX509Signer>>) -> Self {
631 self.x509_signer = x509_signer;
632 self
633 }
634
635 #[cfg(feature = "experimental-x509-identity-verification")]
638 pub fn with_x509_verifier(mut self, x509_verifier: Option<Arc<dyn RawX509Verifier>>) -> Self {
639 self.x509_verifier = x509_verifier;
640 self
641 }
642
643 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
656 pub async fn build(self) -> Result<Client, ClientBuildError> {
657 debug!("Starting to build the Client");
658
659 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
660 Span::current().record("homeserver", debug(&homeserver_cfg));
661
662 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
663 let inner_http_client = match self.http_cfg.unwrap_or_default() {
664 #[cfg(not(target_family = "wasm"))]
665 HttpConfig::Settings(mut settings) => {
666 settings.timeout = self.request_config.timeout;
667 settings.make_client()?
668 }
669 HttpConfig::Custom(c) => c,
670 };
671
672 let base_client = if let Some(base_client) = self.base_client {
673 base_client
674 } else {
675 #[allow(unused_mut)]
676 let mut client = BaseClient::new(
677 build_store_config(self.store_config, &self.cross_process_lock_config).await?,
678 self.threading_support,
679 self.dm_room_definition,
680 );
681
682 #[cfg(feature = "e2e-encryption")]
683 {
684 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
685 client.decryption_settings = self.decryption_settings;
686 }
687
688 #[cfg(feature = "experimental-x509-identity-verification")]
689 client.set_x509_signer(self.x509_signer);
690 #[cfg(feature = "experimental-x509-identity-verification")]
691 client.set_x509_verifier(self.x509_verifier);
692
693 client
694 };
695
696 let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
697
698 #[allow(unused_variables)]
699 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
700 homeserver_cfg.discover(&http_client, self.well_known_lookup_disabled).await?;
701
702 let sliding_sync_version = {
703 let supported_versions = match supported_versions {
704 Some(versions) => Some(versions),
705 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
706 Some(get_supported_versions(&homeserver, &http_client).await?)
707 }
708 None => None,
709 };
710
711 let version = self.sliding_sync_version_builder.build(
712 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
713 )?;
714
715 tracing::info!(?version, "selected sliding sync version");
716
717 version
718 };
719
720 let allow_insecure_oauth = homeserver.scheme() == "http";
721 let auth_ctx = Arc::new(AuthCtx::new(self.handle_refresh_tokens, allow_insecure_oauth));
722
723 let send_queue = Arc::new(SendQueueData::new(true));
725
726 let supported_versions = match self.server_versions {
727 Some(versions) => Cached(TtlValue::without_expiry(SupportedVersions {
728 versions,
729 features: Default::default(),
730 })),
731 None => NotSet,
732 };
733 let well_known = match well_known {
734 Some(well_known) => Cached(TtlValue::new(Some(well_known.into()))),
735 None => NotSet,
736 };
737
738 let event_cache = OnceCell::new();
739 let latest_events = OnceCell::new();
740 let thread_subscriptions_catchup = OnceCell::new();
741
742 #[cfg(feature = "experimental-search")]
743 let search_index =
744 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
745
746 let inner = ClientInner::new(
747 auth_ctx,
748 server,
749 homeserver,
750 sliding_sync_version,
751 Arc::new(StdRwLock::new(PresenceState::Online)),
752 http_client,
753 base_client,
754 supported_versions,
755 well_known,
756 self.respect_login_well_known,
757 self.well_known_lookup_disabled,
758 event_cache,
759 self.enable_automatic_back_pagination,
760 send_queue,
761 latest_events,
762 #[cfg(feature = "e2e-encryption")]
763 self.encryption_settings,
764 #[cfg(feature = "e2e-encryption")]
765 self.enable_share_history_on_invite,
766 self.cross_process_lock_config,
767 #[cfg(feature = "experimental-search")]
768 search_index,
769 thread_subscriptions_catchup,
770 self.media_fetcher.clone(),
771 )
772 .await;
773
774 debug!("Done building the Client");
775
776 Ok(Client { inner })
777 }
778}
779
780pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
784 ServerName::parse(
785 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
786 )
787}
788
789#[allow(clippy::unused_async, unused)] async fn build_store_config(
791 builder_config: BuilderStoreConfig,
792 cross_process_store_config: &CrossProcessLockConfig,
793) -> Result<StoreConfig, ClientBuildError> {
794 #[allow(clippy::infallible_destructuring_match)]
795 let store_config = match builder_config {
796 #[cfg(feature = "sqlite")]
797 BuilderStoreConfig::Sqlite { config, cache_path } => {
798 let config_with_cache_path = if let Some(ref cache_path) = cache_path {
799 config.clone().path(cache_path)
800 } else {
801 config.clone()
802 };
803
804 #[cfg(feature = "e2e-encryption")]
805 let (state_store, event_cache_store, media_store, crypto_store) = try_join!(
806 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
807 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
808 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config_with_cache_path),
809 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(&config),
810 )?;
811 #[cfg(not(feature = "e2e-encryption"))]
812 let (state_store, event_cache_store, media_store) = try_join!(
813 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
814 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
815 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config),
816 )?;
817 let store_config = StoreConfig::new(cross_process_store_config.clone())
818 .state_store(state_store)
819 .event_cache_store(event_cache_store)
820 .media_store(media_store);
821
822 #[cfg(feature = "e2e-encryption")]
823 let store_config = store_config.crypto_store(crypto_store);
824
825 store_config
826 }
827
828 #[cfg(feature = "indexeddb")]
829 BuilderStoreConfig::IndexedDb { name, passphrase } => {
830 build_indexeddb_store_config(
831 &name,
832 passphrase.as_deref(),
833 cross_process_store_config.clone(),
834 )
835 .await?
836 }
837
838 BuilderStoreConfig::Custom(config) => config,
839 };
840 Ok(store_config)
841}
842
843#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
846async fn build_indexeddb_store_config(
847 name: &str,
848 passphrase: Option<&str>,
849 cross_process_store_config: CrossProcessLockConfig,
850) -> Result<StoreConfig, ClientBuildError> {
851 let stores = matrix_sdk_indexeddb::IndexeddbStores::open(name, passphrase).await?;
852 let store_config = StoreConfig::new(cross_process_store_config)
853 .state_store(stores.state)
854 .event_cache_store(stores.event_cache)
855 .media_store(stores.media);
856
857 #[cfg(feature = "e2e-encryption")]
858 let store_config = store_config.crypto_store(stores.crypto);
859
860 Ok(store_config)
861}
862
863#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
864#[allow(clippy::unused_async)]
865async fn build_indexeddb_store_config(
866 _name: &str,
867 _passphrase: Option<&str>,
868 _cross_process_store_config: CrossProcessLockConfig,
869) -> Result<StoreConfig, ClientBuildError> {
870 panic!("the IndexedDB is only available on the 'wasm32' arch")
871}
872
873#[derive(Clone, Debug)]
874enum HttpConfig {
875 #[cfg(not(target_family = "wasm"))]
876 Settings(HttpSettings),
877 Custom(reqwest::Client),
878}
879
880#[cfg(not(target_family = "wasm"))]
881impl HttpConfig {
882 fn settings(&mut self) -> &mut HttpSettings {
883 match self {
884 Self::Settings(s) => s,
885 Self::Custom(_) => {
886 *self = Self::default();
887 match self {
888 Self::Settings(s) => s,
889 Self::Custom(_) => unreachable!(),
890 }
891 }
892 }
893 }
894}
895
896impl Default for HttpConfig {
897 fn default() -> Self {
898 #[cfg(not(target_family = "wasm"))]
899 return Self::Settings(HttpSettings::default());
900
901 #[cfg(target_family = "wasm")]
902 return Self::Custom(reqwest::Client::new());
903 }
904}
905
906#[derive(Clone)]
907enum BuilderStoreConfig {
908 #[cfg(feature = "sqlite")]
909 Sqlite {
910 config: SqliteStoreConfig,
911 cache_path: Option<PathBuf>,
912 },
913 #[cfg(feature = "indexeddb")]
914 IndexedDb {
915 name: String,
916 passphrase: Option<String>,
917 },
918 Custom(StoreConfig),
919}
920
921#[cfg(not(tarpaulin_include))]
922impl fmt::Debug for BuilderStoreConfig {
923 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
924 #[allow(clippy::infallible_destructuring_match)]
925 match self {
926 #[cfg(feature = "sqlite")]
927 Self::Sqlite { config, cache_path, .. } => f
928 .debug_struct("Sqlite")
929 .field("config", config)
930 .field("cache_path", cache_path)
931 .finish_non_exhaustive(),
932
933 #[cfg(feature = "indexeddb")]
934 Self::IndexedDb { name, .. } => {
935 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
936 }
937
938 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
939 }
940 }
941}
942
943#[derive(Debug, Error)]
945pub enum ClientBuildError {
946 #[error("No homeserver or user ID was configured")]
948 MissingHomeserver,
949
950 #[error("The supplied server name is invalid")]
952 InvalidServerName,
953
954 #[error(
958 "Homeserver discovery requires a .well-known lookup, which was disabled; \
959 use `ClientBuilder::homeserver_url` instead"
960 )]
961 WellKnownLookupDisabled,
962
963 #[error("Error looking up the .well-known endpoint on auto-discovery")]
965 AutoDiscovery(Box<FromHttpResponseError<RumaApiError>>),
966
967 #[error(transparent)]
969 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
970
971 #[error(transparent)]
973 Url(#[from] url::ParseError),
974
975 #[error(transparent)]
977 Http(#[from] HttpError),
978
979 #[cfg(feature = "indexeddb")]
981 #[error(transparent)]
982 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
983
984 #[cfg(feature = "sqlite")]
986 #[error(transparent)]
987 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
988}
989
990#[cfg(all(test, not(target_family = "wasm")))]
992pub(crate) mod tests {
993 use std::{future, iter, net::SocketAddr, sync::Mutex as StdMutex};
994
995 use assert_matches::assert_matches;
996 use assert_matches2::assert_let;
997 use matrix_sdk_test::{async_test, test_json};
998 use reqwest::dns::{Addrs, Name, Resolve, Resolving};
999 use serde_json::{Value as JsonValue, json_internal};
1000 use url::Url;
1001 use wiremock::{
1002 Mock, MockServer, ResponseTemplate,
1003 matchers::{header, method, path},
1004 };
1005
1006 use super::*;
1007 use crate::sliding_sync::Version as SlidingSyncVersion;
1008
1009 #[test]
1010 fn test_sanitize_server_name() {
1011 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
1012 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
1013 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
1014 assert_eq!(
1015 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
1016 "matrix.server.org"
1017 );
1018 assert_eq!(
1019 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
1020 "matrix.server.org"
1021 );
1022 assert_eq!(
1023 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
1024 "matrix.server.org"
1025 );
1026 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
1027 }
1028
1029 #[async_test]
1036 async fn test_discovery_invalid_server() {
1037 let mut builder = ClientBuilder::new();
1039
1040 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
1042 let error = builder.build().await.unwrap_err();
1043
1044 assert_matches!(error, ClientBuildError::InvalidServerName);
1046 }
1047
1048 #[async_test]
1049 async fn test_discovery_no_server() {
1050 let mut builder = ClientBuilder::new();
1052
1053 builder = builder.server_name_or_homeserver_url("localhost:3456");
1055 let error = builder.build().await.unwrap_err();
1056
1057 println!("{error}");
1059 assert_matches!(error, ClientBuildError::Http(_));
1060 }
1061
1062 #[async_test]
1063 async fn test_discovery_web_server() {
1064 let server = MockServer::start().await;
1067 let mut builder = ClientBuilder::new();
1068
1069 builder = builder.server_name_or_homeserver_url(server.uri());
1071 let error = builder.build().await.unwrap_err();
1072
1073 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
1075 assert_matches!(*e, FromHttpResponseError::Server(_));
1076 }
1077
1078 #[async_test]
1079 async fn test_discovery_direct_legacy() {
1080 let homeserver = make_mock_homeserver().await;
1082 let mut builder = ClientBuilder::new();
1083
1084 builder = builder.server_name_or_homeserver_url(homeserver.uri());
1086 let _client = builder.build().await.unwrap();
1087
1088 assert!(_client.sliding_sync_version().is_native());
1090 }
1091
1092 #[async_test]
1093 async fn test_discovery_well_known_parse_error() {
1094 let server = MockServer::start().await;
1096 let homeserver = make_mock_homeserver().await;
1097 let mut builder = ClientBuilder::new();
1098
1099 let well_known = make_well_known_json(&homeserver.uri());
1100 let bad_json = well_known.to_string().replace(',', "");
1101 Mock::given(method("GET"))
1102 .and(path("/.well-known/matrix/client"))
1103 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
1104 .mount(&server)
1105 .await;
1106
1107 builder = builder.server_name_or_homeserver_url(server.uri());
1109 let error = builder.build().await.unwrap_err();
1110
1111 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
1113 assert_matches!(*e, FromHttpResponseError::Deserialization(_));
1114 }
1115
1116 #[async_test]
1117 async fn test_discovery_well_known_legacy() {
1118 let server = MockServer::start().await;
1121 let homeserver = make_mock_homeserver().await;
1122 let mut builder = ClientBuilder::new();
1123
1124 Mock::given(method("GET"))
1125 .and(path("/.well-known/matrix/client"))
1126 .respond_with(
1127 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1128 )
1129 .mount(&server)
1130 .await;
1131
1132 builder = builder.server_name_or_homeserver_url(server.uri());
1134 let client = builder.build().await.unwrap();
1135
1136 assert!(client.sliding_sync_version().is_native());
1139 }
1140
1141 #[async_test]
1142 async fn test_discovery_server_name_with_well_known_lookup_disabled() {
1143 let builder = ClientBuilder::new()
1145 .server_name(&ServerName::parse("example.org").unwrap())
1146 .disable_well_known_lookup(true);
1147
1148 let error = builder.build().await.unwrap_err();
1151
1152 assert_matches!(error, ClientBuildError::WellKnownLookupDisabled);
1155
1156 let error = ClientBuilder::new()
1158 .insecure_server_name_no_tls(&ServerName::parse("example.org").unwrap())
1159 .disable_well_known_lookup(true)
1160 .build()
1161 .await
1162 .unwrap_err();
1163
1164 assert_matches!(error, ClientBuildError::WellKnownLookupDisabled);
1165 }
1166
1167 #[async_test]
1168 async fn test_discovery_server_name_or_url_with_well_known_lookup_disabled() {
1169 let homeserver = make_mock_homeserver().await;
1172 Mock::given(method("GET"))
1173 .and(path("/.well-known/matrix/client"))
1174 .respond_with(
1175 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1176 )
1177 .named("well-known mock")
1178 .expect(0)
1179 .mount(&homeserver)
1180 .await;
1181
1182 let client = ClientBuilder::new()
1184 .server_name_or_homeserver_url(homeserver.uri())
1185 .disable_well_known_lookup(true)
1186 .build()
1187 .await
1188 .unwrap();
1189
1190 assert_eq!(client.homeserver().as_str().trim_end_matches('/'), homeserver.uri());
1193 }
1194
1195 #[async_test]
1196 async fn test_homeserver_url_never_contacts_the_server_name() {
1197 let mock_server = MockServer::start().await;
1200 let address = *mock_server.address();
1201 let port = address.port();
1202 let server_name = format!("servername.com:{port}");
1203 let homeserver_name = format!("matrix.server.com:{port}");
1204
1205 let resolver = Arc::new(RecordingResolver { address, hosts: StdMutex::new(Vec::new()) });
1207 let http_client =
1208 reqwest::Client::builder().dns_resolver(resolver.clone()).build().unwrap();
1209
1210 Mock::given(method("GET"))
1211 .and(path("/.well-known/matrix/client"))
1212 .and(header("host", server_name.as_str()))
1213 .respond_with(
1214 ResponseTemplate::new(200)
1215 .set_body_json(make_well_known_json(&format!("http://{homeserver_name}"))),
1216 )
1217 .mount(&mock_server)
1218 .await;
1219
1220 Mock::given(method("GET"))
1221 .and(path("/_matrix/client/versions"))
1222 .and(header("host", homeserver_name.as_str()))
1223 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1224 .mount(&mock_server)
1225 .await;
1226
1227 let client = ClientBuilder::new()
1230 .http_client(http_client)
1231 .server_name_or_homeserver_url(format!("http://{homeserver_name}"))
1232 .build()
1233 .await
1234 .unwrap();
1235
1236 let resolved_hosts = resolver.hosts.lock().unwrap();
1238 assert!(!resolved_hosts.is_empty(), "The homeserver should have been contacted");
1239 assert!(
1240 resolved_hosts.iter().all(|host| host == "matrix.server.com"),
1241 "A connection was attempted to an unexpected host: {resolved_hosts:?}"
1242 );
1243 assert_eq!(client.homeserver(), Url::parse(&format!("http://{homeserver_name}")).unwrap());
1244 assert_eq!(client.server(), None);
1245 }
1246
1247 #[async_test]
1248 async fn test_sliding_sync_discover_native() {
1249 let homeserver = make_mock_homeserver().await;
1251 let mut builder = ClientBuilder::new();
1252
1253 builder = builder
1256 .server_name_or_homeserver_url(homeserver.uri())
1257 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1258
1259 let client = builder.build().await.unwrap();
1260
1261 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1263 }
1264
1265 #[async_test]
1266 #[cfg(feature = "e2e-encryption")]
1267 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1268 let homeserver = make_mock_homeserver().await;
1269 let builder = ClientBuilder::new()
1270 .server_name_or_homeserver_url(homeserver.uri())
1271 .with_decryption_settings(DecryptionSettings {
1272 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1273 });
1274
1275 let client = builder.build().await.unwrap();
1276 assert_matches!(
1277 client.base_client().decryption_settings.sender_device_trust_requirement,
1278 TrustRequirement::CrossSigned
1279 );
1280 }
1281
1282 #[async_test]
1283 #[cfg(feature = "e2e-encryption")]
1284 async fn test_set_up_decryption_trust_requirement_untrusted() {
1285 let homeserver = make_mock_homeserver().await;
1286
1287 let builder = ClientBuilder::new()
1288 .server_name_or_homeserver_url(homeserver.uri())
1289 .with_decryption_settings(DecryptionSettings {
1290 sender_device_trust_requirement: TrustRequirement::Untrusted,
1291 });
1292
1293 let client = builder.build().await.unwrap();
1294 assert_matches!(
1295 client.base_client().decryption_settings.sender_device_trust_requirement,
1296 TrustRequirement::Untrusted
1297 );
1298 }
1299
1300 #[async_test]
1301 async fn test_cross_process_store_locks_holder_name() {
1302 {
1303 let homeserver = make_mock_homeserver().await;
1304 let client =
1305 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1306
1307 assert_let!(
1308 CrossProcessLockConfig::MultiProcess { holder_name } =
1309 client.cross_process_lock_config()
1310 );
1311 assert_eq!(holder_name, "main");
1312 }
1313
1314 {
1315 let homeserver = make_mock_homeserver().await;
1316 let client = ClientBuilder::new()
1317 .homeserver_url(homeserver.uri())
1318 .cross_process_store_config(CrossProcessLockConfig::multi_process("foo"))
1319 .build()
1320 .await
1321 .unwrap();
1322
1323 assert_let!(
1324 CrossProcessLockConfig::MultiProcess { holder_name } =
1325 client.cross_process_lock_config()
1326 );
1327 assert_eq!(holder_name, "foo");
1328 }
1329 }
1330
1331 async fn make_mock_homeserver() -> MockServer {
1334 let homeserver = MockServer::start().await;
1335 Mock::given(method("GET"))
1336 .and(path("/_matrix/client/versions"))
1337 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1338 .mount(&homeserver)
1339 .await;
1340 Mock::given(method("GET"))
1341 .and(path("/_matrix/client/r0/login"))
1342 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1343 .mount(&homeserver)
1344 .await;
1345 homeserver
1346 }
1347
1348 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1349 ::serde_json::Value::Object({
1350 let mut object = ::serde_json::Map::new();
1351 let _ = object.insert(
1352 "m.homeserver".into(),
1353 json_internal!({
1354 "base_url": homeserver_url
1355 }),
1356 );
1357
1358 object
1359 })
1360 }
1361
1362 #[derive(Debug)]
1365 struct RecordingResolver {
1366 address: SocketAddr,
1367 hosts: StdMutex<Vec<String>>,
1368 }
1369
1370 impl Resolve for RecordingResolver {
1371 fn resolve(&self, name: Name) -> Resolving {
1372 self.hosts.lock().unwrap().push(name.as_str().to_owned());
1373
1374 let addrs: Addrs = Box::new(iter::once(self.address));
1375 Box::pin(future::ready(Ok(addrs)))
1376 }
1377 }
1378}