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 cross_process_lock_config: CrossProcessLockConfig,
139 threading_support: ThreadingSupport,
140 #[cfg(feature = "experimental-search")]
141 search_index_store_kind: SearchIndexStoreKind,
142 #[cfg(feature = "experimental-x509-identity-verification")]
143 x509_signer: Option<Arc<dyn RawX509Signer>>,
144 #[cfg(feature = "experimental-x509-identity-verification")]
145 x509_verifier: Option<Arc<dyn RawX509Verifier>>,
146 dm_room_definition: DmRoomDefinition,
147 media_fetcher: Arc<dyn MediaFetcher>,
148}
149
150impl ClientBuilder {
151 const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
152
153 pub(crate) fn new() -> Self {
154 Self {
155 homeserver_cfg: None,
156 sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
157 http_cfg: None,
158 store_config: BuilderStoreConfig::Custom(StoreConfig::new(
159 CrossProcessLockConfig::multi_process(
160 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME,
161 ),
162 )),
163 request_config: Default::default(),
164 respect_login_well_known: true,
165 well_known_lookup_disabled: false,
166 server_versions: None,
167 handle_refresh_tokens: false,
168 base_client: None,
169 #[cfg(feature = "e2e-encryption")]
170 encryption_settings: Default::default(),
171 #[cfg(feature = "e2e-encryption")]
172 room_key_recipient_strategy: Default::default(),
173 #[cfg(feature = "e2e-encryption")]
174 decryption_settings: DecryptionSettings {
175 sender_device_trust_requirement: TrustRequirement::Untrusted,
176 },
177 #[cfg(feature = "e2e-encryption")]
178 enable_share_history_on_invite: true,
179 cross_process_lock_config: CrossProcessLockConfig::MultiProcess {
180 holder_name: Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
181 },
182 threading_support: ThreadingSupport::Disabled,
183 #[cfg(feature = "experimental-search")]
184 search_index_store_kind: SearchIndexStoreKind::InMemory,
185 #[cfg(feature = "experimental-x509-identity-verification")]
186 x509_signer: None,
187 #[cfg(feature = "experimental-x509-identity-verification")]
188 x509_verifier: None,
189 dm_room_definition: DmRoomDefinition::MatrixSpec,
190 media_fetcher: Arc::new(DefaultMediaFetcher),
191 }
192 }
193
194 pub fn media_fetcher(mut self, media_fetcher: Arc<dyn MediaFetcher>) -> Self {
197 self.media_fetcher = media_fetcher.clone();
198 self
199 }
200
201 pub fn dm_room_definition(mut self, dm_room_definition: DmRoomDefinition) -> Self {
205 self.dm_room_definition = dm_room_definition;
206 self
207 }
208
209 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
220 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
221 self
222 }
223
224 pub fn server_name(mut self, server_name: &ServerName) -> Self {
238 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
239 server: server_name.to_owned(),
240 protocol: UrlScheme::Https,
242 });
243 self
244 }
245
246 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
259 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
260 server: server_name.to_owned(),
261 protocol: UrlScheme::Http,
262 });
263 self
264 }
265
266 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
282 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
283 server_name_or_url.as_ref().to_owned(),
284 ));
285 self
286 }
287
288 pub fn sliding_sync_version_builder(
290 mut self,
291 version_builder: SlidingSyncVersionBuilder,
292 ) -> Self {
293 self.sliding_sync_version_builder = version_builder;
294 self
295 }
296
297 #[cfg(feature = "sqlite")]
299 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
300 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
301 self.store_config =
302 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
303
304 self
305 }
306
307 #[cfg(feature = "sqlite")]
310 pub fn sqlite_store_with_cache_path(
311 mut self,
312 path: impl AsRef<Path>,
313 cache_path: impl AsRef<Path>,
314 passphrase: Option<&str>,
315 ) -> Self {
316 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
317 self.store_config = BuilderStoreConfig::Sqlite {
318 config: sqlite_store_config,
319 cache_path: Some(cache_path.as_ref().to_owned()),
320 };
321
322 self
323 }
324
325 #[cfg(feature = "sqlite")]
328 pub fn sqlite_store_with_config_and_cache_path(
329 mut self,
330 config: SqliteStoreConfig,
331 cache_path: Option<impl AsRef<Path>>,
332 ) -> Self {
333 self.store_config = BuilderStoreConfig::Sqlite {
334 config,
335 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
336 };
337
338 self
339 }
340
341 #[cfg(feature = "indexeddb")]
343 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
344 self.store_config = BuilderStoreConfig::IndexedDb {
345 name: name.to_owned(),
346 passphrase: passphrase.map(ToOwned::to_owned),
347 };
348 self
349 }
350
351 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
375 self.store_config = BuilderStoreConfig::Custom(store_config);
376 self
377 }
378
379 pub fn respect_login_well_known(mut self, value: bool) -> Self {
382 self.respect_login_well_known = value;
383 self
384 }
385
386 pub fn disable_well_known_lookup(mut self, disable: bool) -> Self {
415 self.well_known_lookup_disabled = disable;
416 self
417 }
418
419 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
421 self.request_config = request_config;
422 self
423 }
424
425 #[cfg(not(target_family = "wasm"))]
441 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
442 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
443 self
444 }
445
446 #[cfg(not(target_family = "wasm"))]
448 pub fn disable_ssl_verification(mut self) -> Self {
449 self.http_settings().disable_ssl_verification = true;
450 self
451 }
452
453 #[cfg(not(target_family = "wasm"))]
455 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
456 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
457 self
458 }
459
460 #[cfg(not(target_family = "wasm"))]
469 pub fn add_root_certificates(mut self, certificates: Vec<Certificate>) -> Self {
470 self.http_settings().additional_root_certificates = certificates;
471 self
472 }
473
474 #[cfg(not(target_family = "wasm"))]
478 pub fn disable_built_in_root_certificates(mut self) -> Self {
479 self.http_settings().disable_built_in_root_certificates = true;
480 self
481 }
482
483 pub fn http_client(mut self, client: reqwest::Client) -> Self {
493 self.http_cfg = Some(HttpConfig::Custom(client));
494 self
495 }
496
497 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
502 self.server_versions = Some(value.into_iter().collect());
503 self
504 }
505
506 #[cfg(not(target_family = "wasm"))]
507 fn http_settings(&mut self) -> &mut HttpSettings {
508 self.http_cfg.get_or_insert_with(Default::default).settings()
509 }
510
511 pub fn handle_refresh_tokens(mut self) -> Self {
533 self.handle_refresh_tokens = true;
534 self
535 }
536
537 #[doc(hidden)]
539 pub fn base_client(mut self, base_client: BaseClient) -> Self {
540 self.base_client = Some(base_client);
541 self
542 }
543
544 #[cfg(feature = "e2e-encryption")]
547 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
548 self.encryption_settings = settings;
549 self
550 }
551
552 #[cfg(feature = "e2e-encryption")]
555 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
556 self.room_key_recipient_strategy = strategy;
557 self
558 }
559
560 #[cfg(feature = "e2e-encryption")]
562 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
563 self.decryption_settings = decryption_settings;
564 self
565 }
566
567 #[cfg(feature = "e2e-encryption")]
575 pub fn with_enable_share_history_on_invite(
576 mut self,
577 enable_share_history_on_invite: bool,
578 ) -> Self {
579 self.enable_share_history_on_invite = enable_share_history_on_invite;
580 self
581 }
582
583 pub fn cross_process_store_config(
593 mut self,
594 cross_process_store_config: CrossProcessLockConfig,
595 ) -> Self {
596 self.cross_process_lock_config = cross_process_store_config;
597 self
598 }
599
600 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
604 self.threading_support = threading_support;
605 self
606 }
607
608 #[cfg(feature = "experimental-search")]
610 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
611 self.search_index_store_kind = kind;
612 self
613 }
614
615 #[cfg(feature = "experimental-x509-identity-verification")]
618 pub fn with_x509_signer(mut self, x509_signer: Option<Arc<dyn RawX509Signer>>) -> Self {
619 self.x509_signer = x509_signer;
620 self
621 }
622
623 #[cfg(feature = "experimental-x509-identity-verification")]
626 pub fn with_x509_verifier(mut self, x509_verifier: Option<Arc<dyn RawX509Verifier>>) -> Self {
627 self.x509_verifier = x509_verifier;
628 self
629 }
630
631 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
644 pub async fn build(self) -> Result<Client, ClientBuildError> {
645 debug!("Starting to build the Client");
646
647 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
648 Span::current().record("homeserver", debug(&homeserver_cfg));
649
650 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
651 let inner_http_client = match self.http_cfg.unwrap_or_default() {
652 #[cfg(not(target_family = "wasm"))]
653 HttpConfig::Settings(mut settings) => {
654 settings.timeout = self.request_config.timeout;
655 settings.make_client()?
656 }
657 HttpConfig::Custom(c) => c,
658 };
659
660 let base_client = if let Some(base_client) = self.base_client {
661 base_client
662 } else {
663 #[allow(unused_mut)]
664 let mut client = BaseClient::new(
665 build_store_config(self.store_config, &self.cross_process_lock_config).await?,
666 self.threading_support,
667 self.dm_room_definition,
668 );
669
670 #[cfg(feature = "e2e-encryption")]
671 {
672 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
673 client.decryption_settings = self.decryption_settings;
674 }
675
676 #[cfg(feature = "experimental-x509-identity-verification")]
677 client.set_x509_signer(self.x509_signer);
678 #[cfg(feature = "experimental-x509-identity-verification")]
679 client.set_x509_verifier(self.x509_verifier);
680
681 client
682 };
683
684 let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
685
686 #[allow(unused_variables)]
687 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
688 homeserver_cfg.discover(&http_client, self.well_known_lookup_disabled).await?;
689
690 let sliding_sync_version = {
691 let supported_versions = match supported_versions {
692 Some(versions) => Some(versions),
693 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
694 Some(get_supported_versions(&homeserver, &http_client).await?)
695 }
696 None => None,
697 };
698
699 let version = self.sliding_sync_version_builder.build(
700 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
701 )?;
702
703 tracing::info!(?version, "selected sliding sync version");
704
705 version
706 };
707
708 let allow_insecure_oauth = homeserver.scheme() == "http";
709 let auth_ctx = Arc::new(AuthCtx::new(self.handle_refresh_tokens, allow_insecure_oauth));
710
711 let send_queue = Arc::new(SendQueueData::new(true));
713
714 let supported_versions = match self.server_versions {
715 Some(versions) => Cached(TtlValue::without_expiry(SupportedVersions {
716 versions,
717 features: Default::default(),
718 })),
719 None => NotSet,
720 };
721 let well_known = match well_known {
722 Some(well_known) => Cached(TtlValue::new(Some(well_known.into()))),
723 None => NotSet,
724 };
725
726 let event_cache = OnceCell::new();
727 let latest_events = OnceCell::new();
728 let thread_subscriptions_catchup = OnceCell::new();
729
730 #[cfg(feature = "experimental-search")]
731 let search_index =
732 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
733
734 let inner = ClientInner::new(
735 auth_ctx,
736 server,
737 homeserver,
738 sliding_sync_version,
739 Arc::new(StdRwLock::new(PresenceState::Online)),
740 http_client,
741 base_client,
742 supported_versions,
743 well_known,
744 self.respect_login_well_known,
745 self.well_known_lookup_disabled,
746 event_cache,
747 send_queue,
748 latest_events,
749 #[cfg(feature = "e2e-encryption")]
750 self.encryption_settings,
751 #[cfg(feature = "e2e-encryption")]
752 self.enable_share_history_on_invite,
753 self.cross_process_lock_config,
754 #[cfg(feature = "experimental-search")]
755 search_index,
756 thread_subscriptions_catchup,
757 self.media_fetcher.clone(),
758 )
759 .await;
760
761 debug!("Done building the Client");
762
763 Ok(Client { inner })
764 }
765}
766
767pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
771 ServerName::parse(
772 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
773 )
774}
775
776#[allow(clippy::unused_async, unused)] async fn build_store_config(
778 builder_config: BuilderStoreConfig,
779 cross_process_store_config: &CrossProcessLockConfig,
780) -> Result<StoreConfig, ClientBuildError> {
781 #[allow(clippy::infallible_destructuring_match)]
782 let store_config = match builder_config {
783 #[cfg(feature = "sqlite")]
784 BuilderStoreConfig::Sqlite { config, cache_path } => {
785 let config_with_cache_path = if let Some(ref cache_path) = cache_path {
786 config.clone().path(cache_path)
787 } else {
788 config.clone()
789 };
790
791 #[cfg(feature = "e2e-encryption")]
792 let (state_store, event_cache_store, media_store, crypto_store) = try_join!(
793 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
794 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
795 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config_with_cache_path),
796 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(&config),
797 )?;
798 #[cfg(not(feature = "e2e-encryption"))]
799 let (state_store, event_cache_store, media_store) = try_join!(
800 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
801 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
802 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config),
803 )?;
804 let store_config = StoreConfig::new(cross_process_store_config.clone())
805 .state_store(state_store)
806 .event_cache_store(event_cache_store)
807 .media_store(media_store);
808
809 #[cfg(feature = "e2e-encryption")]
810 let store_config = store_config.crypto_store(crypto_store);
811
812 store_config
813 }
814
815 #[cfg(feature = "indexeddb")]
816 BuilderStoreConfig::IndexedDb { name, passphrase } => {
817 build_indexeddb_store_config(
818 &name,
819 passphrase.as_deref(),
820 cross_process_store_config.clone(),
821 )
822 .await?
823 }
824
825 BuilderStoreConfig::Custom(config) => config,
826 };
827 Ok(store_config)
828}
829
830#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
833async fn build_indexeddb_store_config(
834 name: &str,
835 passphrase: Option<&str>,
836 cross_process_store_config: CrossProcessLockConfig,
837) -> Result<StoreConfig, ClientBuildError> {
838 let stores = matrix_sdk_indexeddb::IndexeddbStores::open(name, passphrase).await?;
839 let store_config = StoreConfig::new(cross_process_store_config)
840 .state_store(stores.state)
841 .event_cache_store(stores.event_cache)
842 .media_store(stores.media);
843
844 #[cfg(feature = "e2e-encryption")]
845 let store_config = store_config.crypto_store(stores.crypto);
846
847 Ok(store_config)
848}
849
850#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
851#[allow(clippy::unused_async)]
852async fn build_indexeddb_store_config(
853 _name: &str,
854 _passphrase: Option<&str>,
855 _cross_process_store_config: CrossProcessLockConfig,
856) -> Result<StoreConfig, ClientBuildError> {
857 panic!("the IndexedDB is only available on the 'wasm32' arch")
858}
859
860#[derive(Clone, Debug)]
861enum HttpConfig {
862 #[cfg(not(target_family = "wasm"))]
863 Settings(HttpSettings),
864 Custom(reqwest::Client),
865}
866
867#[cfg(not(target_family = "wasm"))]
868impl HttpConfig {
869 fn settings(&mut self) -> &mut HttpSettings {
870 match self {
871 Self::Settings(s) => s,
872 Self::Custom(_) => {
873 *self = Self::default();
874 match self {
875 Self::Settings(s) => s,
876 Self::Custom(_) => unreachable!(),
877 }
878 }
879 }
880 }
881}
882
883impl Default for HttpConfig {
884 fn default() -> Self {
885 #[cfg(not(target_family = "wasm"))]
886 return Self::Settings(HttpSettings::default());
887
888 #[cfg(target_family = "wasm")]
889 return Self::Custom(reqwest::Client::new());
890 }
891}
892
893#[derive(Clone)]
894enum BuilderStoreConfig {
895 #[cfg(feature = "sqlite")]
896 Sqlite {
897 config: SqliteStoreConfig,
898 cache_path: Option<PathBuf>,
899 },
900 #[cfg(feature = "indexeddb")]
901 IndexedDb {
902 name: String,
903 passphrase: Option<String>,
904 },
905 Custom(StoreConfig),
906}
907
908#[cfg(not(tarpaulin_include))]
909impl fmt::Debug for BuilderStoreConfig {
910 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911 #[allow(clippy::infallible_destructuring_match)]
912 match self {
913 #[cfg(feature = "sqlite")]
914 Self::Sqlite { config, cache_path, .. } => f
915 .debug_struct("Sqlite")
916 .field("config", config)
917 .field("cache_path", cache_path)
918 .finish_non_exhaustive(),
919
920 #[cfg(feature = "indexeddb")]
921 Self::IndexedDb { name, .. } => {
922 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
923 }
924
925 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
926 }
927 }
928}
929
930#[derive(Debug, Error)]
932pub enum ClientBuildError {
933 #[error("No homeserver or user ID was configured")]
935 MissingHomeserver,
936
937 #[error("The supplied server name is invalid")]
939 InvalidServerName,
940
941 #[error(
945 "Homeserver discovery requires a .well-known lookup, which was disabled; \
946 use `ClientBuilder::homeserver_url` instead"
947 )]
948 WellKnownLookupDisabled,
949
950 #[error("Error looking up the .well-known endpoint on auto-discovery")]
952 AutoDiscovery(Box<FromHttpResponseError<RumaApiError>>),
953
954 #[error(transparent)]
956 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
957
958 #[error(transparent)]
960 Url(#[from] url::ParseError),
961
962 #[error(transparent)]
964 Http(#[from] HttpError),
965
966 #[cfg(feature = "indexeddb")]
968 #[error(transparent)]
969 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
970
971 #[cfg(feature = "sqlite")]
973 #[error(transparent)]
974 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
975}
976
977#[cfg(all(test, not(target_family = "wasm")))]
979pub(crate) mod tests {
980 use std::{future, iter, net::SocketAddr, sync::Mutex as StdMutex};
981
982 use assert_matches::assert_matches;
983 use assert_matches2::assert_let;
984 use matrix_sdk_test::{async_test, test_json};
985 use reqwest::dns::{Addrs, Name, Resolve, Resolving};
986 use serde_json::{Value as JsonValue, json_internal};
987 use url::Url;
988 use wiremock::{
989 Mock, MockServer, ResponseTemplate,
990 matchers::{header, method, path},
991 };
992
993 use super::*;
994 use crate::sliding_sync::Version as SlidingSyncVersion;
995
996 #[test]
997 fn test_sanitize_server_name() {
998 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
999 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
1000 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
1001 assert_eq!(
1002 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
1003 "matrix.server.org"
1004 );
1005 assert_eq!(
1006 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
1007 "matrix.server.org"
1008 );
1009 assert_eq!(
1010 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
1011 "matrix.server.org"
1012 );
1013 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
1014 }
1015
1016 #[async_test]
1023 async fn test_discovery_invalid_server() {
1024 let mut builder = ClientBuilder::new();
1026
1027 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
1029 let error = builder.build().await.unwrap_err();
1030
1031 assert_matches!(error, ClientBuildError::InvalidServerName);
1033 }
1034
1035 #[async_test]
1036 async fn test_discovery_no_server() {
1037 let mut builder = ClientBuilder::new();
1039
1040 builder = builder.server_name_or_homeserver_url("localhost:3456");
1042 let error = builder.build().await.unwrap_err();
1043
1044 println!("{error}");
1046 assert_matches!(error, ClientBuildError::Http(_));
1047 }
1048
1049 #[async_test]
1050 async fn test_discovery_web_server() {
1051 let server = MockServer::start().await;
1054 let mut builder = ClientBuilder::new();
1055
1056 builder = builder.server_name_or_homeserver_url(server.uri());
1058 let error = builder.build().await.unwrap_err();
1059
1060 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
1062 assert_matches!(*e, FromHttpResponseError::Server(_));
1063 }
1064
1065 #[async_test]
1066 async fn test_discovery_direct_legacy() {
1067 let homeserver = make_mock_homeserver().await;
1069 let mut builder = ClientBuilder::new();
1070
1071 builder = builder.server_name_or_homeserver_url(homeserver.uri());
1073 let _client = builder.build().await.unwrap();
1074
1075 assert!(_client.sliding_sync_version().is_native());
1077 }
1078
1079 #[async_test]
1080 async fn test_discovery_well_known_parse_error() {
1081 let server = MockServer::start().await;
1083 let homeserver = make_mock_homeserver().await;
1084 let mut builder = ClientBuilder::new();
1085
1086 let well_known = make_well_known_json(&homeserver.uri());
1087 let bad_json = well_known.to_string().replace(',', "");
1088 Mock::given(method("GET"))
1089 .and(path("/.well-known/matrix/client"))
1090 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
1091 .mount(&server)
1092 .await;
1093
1094 builder = builder.server_name_or_homeserver_url(server.uri());
1096 let error = builder.build().await.unwrap_err();
1097
1098 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
1100 assert_matches!(*e, FromHttpResponseError::Deserialization(_));
1101 }
1102
1103 #[async_test]
1104 async fn test_discovery_well_known_legacy() {
1105 let server = MockServer::start().await;
1108 let homeserver = make_mock_homeserver().await;
1109 let mut builder = ClientBuilder::new();
1110
1111 Mock::given(method("GET"))
1112 .and(path("/.well-known/matrix/client"))
1113 .respond_with(
1114 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1115 )
1116 .mount(&server)
1117 .await;
1118
1119 builder = builder.server_name_or_homeserver_url(server.uri());
1121 let client = builder.build().await.unwrap();
1122
1123 assert!(client.sliding_sync_version().is_native());
1126 }
1127
1128 #[async_test]
1129 async fn test_discovery_server_name_with_well_known_lookup_disabled() {
1130 let builder = ClientBuilder::new()
1132 .server_name(&ServerName::parse("example.org").unwrap())
1133 .disable_well_known_lookup(true);
1134
1135 let error = builder.build().await.unwrap_err();
1138
1139 assert_matches!(error, ClientBuildError::WellKnownLookupDisabled);
1142
1143 let error = ClientBuilder::new()
1145 .insecure_server_name_no_tls(&ServerName::parse("example.org").unwrap())
1146 .disable_well_known_lookup(true)
1147 .build()
1148 .await
1149 .unwrap_err();
1150
1151 assert_matches!(error, ClientBuildError::WellKnownLookupDisabled);
1152 }
1153
1154 #[async_test]
1155 async fn test_discovery_server_name_or_url_with_well_known_lookup_disabled() {
1156 let homeserver = make_mock_homeserver().await;
1159 Mock::given(method("GET"))
1160 .and(path("/.well-known/matrix/client"))
1161 .respond_with(
1162 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1163 )
1164 .named("well-known mock")
1165 .expect(0)
1166 .mount(&homeserver)
1167 .await;
1168
1169 let client = ClientBuilder::new()
1171 .server_name_or_homeserver_url(homeserver.uri())
1172 .disable_well_known_lookup(true)
1173 .build()
1174 .await
1175 .unwrap();
1176
1177 assert_eq!(client.homeserver().as_str().trim_end_matches('/'), homeserver.uri());
1180 }
1181
1182 #[async_test]
1183 async fn test_homeserver_url_never_contacts_the_server_name() {
1184 let mock_server = MockServer::start().await;
1187 let address = *mock_server.address();
1188 let port = address.port();
1189 let server_name = format!("servername.com:{port}");
1190 let homeserver_name = format!("matrix.server.com:{port}");
1191
1192 let resolver = Arc::new(RecordingResolver { address, hosts: StdMutex::new(Vec::new()) });
1194 let http_client =
1195 reqwest::Client::builder().dns_resolver(resolver.clone()).build().unwrap();
1196
1197 Mock::given(method("GET"))
1198 .and(path("/.well-known/matrix/client"))
1199 .and(header("host", server_name.as_str()))
1200 .respond_with(
1201 ResponseTemplate::new(200)
1202 .set_body_json(make_well_known_json(&format!("http://{homeserver_name}"))),
1203 )
1204 .mount(&mock_server)
1205 .await;
1206
1207 Mock::given(method("GET"))
1208 .and(path("/_matrix/client/versions"))
1209 .and(header("host", homeserver_name.as_str()))
1210 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1211 .mount(&mock_server)
1212 .await;
1213
1214 let client = ClientBuilder::new()
1217 .http_client(http_client)
1218 .server_name_or_homeserver_url(format!("http://{homeserver_name}"))
1219 .build()
1220 .await
1221 .unwrap();
1222
1223 let resolved_hosts = resolver.hosts.lock().unwrap();
1225 assert!(!resolved_hosts.is_empty(), "The homeserver should have been contacted");
1226 assert!(
1227 resolved_hosts.iter().all(|host| host == "matrix.server.com"),
1228 "A connection was attempted to an unexpected host: {resolved_hosts:?}"
1229 );
1230 assert_eq!(client.homeserver(), Url::parse(&format!("http://{homeserver_name}")).unwrap());
1231 assert_eq!(client.server(), None);
1232 }
1233
1234 #[async_test]
1235 async fn test_sliding_sync_discover_native() {
1236 let homeserver = make_mock_homeserver().await;
1238 let mut builder = ClientBuilder::new();
1239
1240 builder = builder
1243 .server_name_or_homeserver_url(homeserver.uri())
1244 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1245
1246 let client = builder.build().await.unwrap();
1247
1248 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1250 }
1251
1252 #[async_test]
1253 #[cfg(feature = "e2e-encryption")]
1254 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1255 let homeserver = make_mock_homeserver().await;
1256 let builder = ClientBuilder::new()
1257 .server_name_or_homeserver_url(homeserver.uri())
1258 .with_decryption_settings(DecryptionSettings {
1259 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1260 });
1261
1262 let client = builder.build().await.unwrap();
1263 assert_matches!(
1264 client.base_client().decryption_settings.sender_device_trust_requirement,
1265 TrustRequirement::CrossSigned
1266 );
1267 }
1268
1269 #[async_test]
1270 #[cfg(feature = "e2e-encryption")]
1271 async fn test_set_up_decryption_trust_requirement_untrusted() {
1272 let homeserver = make_mock_homeserver().await;
1273
1274 let builder = ClientBuilder::new()
1275 .server_name_or_homeserver_url(homeserver.uri())
1276 .with_decryption_settings(DecryptionSettings {
1277 sender_device_trust_requirement: TrustRequirement::Untrusted,
1278 });
1279
1280 let client = builder.build().await.unwrap();
1281 assert_matches!(
1282 client.base_client().decryption_settings.sender_device_trust_requirement,
1283 TrustRequirement::Untrusted
1284 );
1285 }
1286
1287 #[async_test]
1288 async fn test_cross_process_store_locks_holder_name() {
1289 {
1290 let homeserver = make_mock_homeserver().await;
1291 let client =
1292 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1293
1294 assert_let!(
1295 CrossProcessLockConfig::MultiProcess { holder_name } =
1296 client.cross_process_lock_config()
1297 );
1298 assert_eq!(holder_name, "main");
1299 }
1300
1301 {
1302 let homeserver = make_mock_homeserver().await;
1303 let client = ClientBuilder::new()
1304 .homeserver_url(homeserver.uri())
1305 .cross_process_store_config(CrossProcessLockConfig::multi_process("foo"))
1306 .build()
1307 .await
1308 .unwrap();
1309
1310 assert_let!(
1311 CrossProcessLockConfig::MultiProcess { holder_name } =
1312 client.cross_process_lock_config()
1313 );
1314 assert_eq!(holder_name, "foo");
1315 }
1316 }
1317
1318 async fn make_mock_homeserver() -> MockServer {
1321 let homeserver = MockServer::start().await;
1322 Mock::given(method("GET"))
1323 .and(path("/_matrix/client/versions"))
1324 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1325 .mount(&homeserver)
1326 .await;
1327 Mock::given(method("GET"))
1328 .and(path("/_matrix/client/r0/login"))
1329 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1330 .mount(&homeserver)
1331 .await;
1332 homeserver
1333 }
1334
1335 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1336 ::serde_json::Value::Object({
1337 let mut object = ::serde_json::Map::new();
1338 let _ = object.insert(
1339 "m.homeserver".into(),
1340 json_internal!({
1341 "base_url": homeserver_url
1342 }),
1343 );
1344
1345 object
1346 })
1347 }
1348
1349 #[derive(Debug)]
1352 struct RecordingResolver {
1353 address: SocketAddr,
1354 hosts: StdMutex<Vec<String>>,
1355 }
1356
1357 impl Resolve for RecordingResolver {
1358 fn resolve(&self, name: Name) -> Resolving {
1359 self.hosts.lock().unwrap().push(name.as_str().to_owned());
1360
1361 let addrs: Addrs = Box::new(iter::once(self.address));
1362 Box::pin(future::ready(Ok(addrs)))
1363 }
1364 }
1365}