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 enable_event_cache: bool,
140 cross_process_lock_config: CrossProcessLockConfig,
141 threading_support: ThreadingSupport,
142 #[cfg(feature = "experimental-search")]
143 search_index_store_kind: SearchIndexStoreKind,
144 #[cfg(feature = "experimental-x509-identity-verification")]
145 x509_signer: Option<Arc<dyn RawX509Signer>>,
146 #[cfg(feature = "experimental-x509-identity-verification")]
147 x509_verifier: Option<Arc<dyn RawX509Verifier>>,
148 dm_room_definition: DmRoomDefinition,
149 media_fetcher: Arc<dyn MediaFetcher>,
150}
151
152impl ClientBuilder {
153 const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
154
155 pub(crate) fn new() -> Self {
156 Self {
157 homeserver_cfg: None,
158 sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
159 http_cfg: None,
160 store_config: BuilderStoreConfig::Custom(StoreConfig::new(
161 CrossProcessLockConfig::multi_process(
162 Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME,
163 ),
164 )),
165 request_config: Default::default(),
166 respect_login_well_known: true,
167 well_known_lookup_disabled: false,
168 server_versions: None,
169 handle_refresh_tokens: false,
170 base_client: None,
171 #[cfg(feature = "e2e-encryption")]
172 encryption_settings: Default::default(),
173 #[cfg(feature = "e2e-encryption")]
174 room_key_recipient_strategy: Default::default(),
175 #[cfg(feature = "e2e-encryption")]
176 decryption_settings: DecryptionSettings {
177 sender_device_trust_requirement: TrustRequirement::Untrusted,
178 },
179 #[cfg(feature = "e2e-encryption")]
180 enable_share_history_on_invite: true,
181 enable_automatic_back_pagination: false,
182 enable_event_cache: true,
183 cross_process_lock_config: CrossProcessLockConfig::MultiProcess {
184 holder_name: Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
185 },
186 threading_support: ThreadingSupport::Disabled,
187 #[cfg(feature = "experimental-search")]
188 search_index_store_kind: SearchIndexStoreKind::InMemory,
189 #[cfg(feature = "experimental-x509-identity-verification")]
190 x509_signer: None,
191 #[cfg(feature = "experimental-x509-identity-verification")]
192 x509_verifier: None,
193 dm_room_definition: DmRoomDefinition::MatrixSpec,
194 media_fetcher: Arc::new(DefaultMediaFetcher),
195 }
196 }
197
198 pub fn media_fetcher(mut self, media_fetcher: Arc<dyn MediaFetcher>) -> Self {
201 self.media_fetcher = media_fetcher.clone();
202 self
203 }
204
205 pub fn dm_room_definition(mut self, dm_room_definition: DmRoomDefinition) -> Self {
209 self.dm_room_definition = dm_room_definition;
210 self
211 }
212
213 pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
224 self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
225 self
226 }
227
228 pub fn server_name(mut self, server_name: &ServerName) -> Self {
242 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
243 server: server_name.to_owned(),
244 protocol: UrlScheme::Https,
246 });
247 self
248 }
249
250 pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
263 self.homeserver_cfg = Some(HomeserverConfig::ServerName {
264 server: server_name.to_owned(),
265 protocol: UrlScheme::Http,
266 });
267 self
268 }
269
270 pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
286 self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
287 server_name_or_url.as_ref().to_owned(),
288 ));
289 self
290 }
291
292 pub fn sliding_sync_version_builder(
294 mut self,
295 version_builder: SlidingSyncVersionBuilder,
296 ) -> Self {
297 self.sliding_sync_version_builder = version_builder;
298 self
299 }
300
301 #[cfg(feature = "sqlite")]
303 pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
304 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
305 self.store_config =
306 BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
307
308 self
309 }
310
311 #[cfg(feature = "sqlite")]
314 pub fn sqlite_store_with_cache_path(
315 mut self,
316 path: impl AsRef<Path>,
317 cache_path: impl AsRef<Path>,
318 passphrase: Option<&str>,
319 ) -> Self {
320 let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
321 self.store_config = BuilderStoreConfig::Sqlite {
322 config: sqlite_store_config,
323 cache_path: Some(cache_path.as_ref().to_owned()),
324 };
325
326 self
327 }
328
329 #[cfg(feature = "sqlite")]
332 pub fn sqlite_store_with_config_and_cache_path(
333 mut self,
334 config: SqliteStoreConfig,
335 cache_path: Option<impl AsRef<Path>>,
336 ) -> Self {
337 self.store_config = BuilderStoreConfig::Sqlite {
338 config,
339 cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
340 };
341
342 self
343 }
344
345 #[cfg(feature = "indexeddb")]
347 pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
348 self.store_config = BuilderStoreConfig::IndexedDb {
349 name: name.to_owned(),
350 passphrase: passphrase.map(ToOwned::to_owned),
351 };
352 self
353 }
354
355 pub fn store_config(mut self, store_config: StoreConfig) -> Self {
379 self.store_config = BuilderStoreConfig::Custom(store_config);
380 self
381 }
382
383 pub fn respect_login_well_known(mut self, value: bool) -> Self {
386 self.respect_login_well_known = value;
387 self
388 }
389
390 pub fn disable_well_known_lookup(mut self, disable: bool) -> Self {
419 self.well_known_lookup_disabled = disable;
420 self
421 }
422
423 pub fn request_config(mut self, request_config: RequestConfig) -> Self {
425 self.request_config = request_config;
426 self
427 }
428
429 #[cfg(not(target_family = "wasm"))]
445 pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
446 self.http_settings().proxy = Some(proxy.as_ref().to_owned());
447 self
448 }
449
450 #[cfg(not(target_family = "wasm"))]
452 pub fn disable_ssl_verification(mut self) -> Self {
453 self.http_settings().disable_ssl_verification = true;
454 self
455 }
456
457 #[cfg(not(target_family = "wasm"))]
459 pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
460 self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
461 self
462 }
463
464 #[cfg(not(target_family = "wasm"))]
473 pub fn add_root_certificates(mut self, certificates: Vec<Certificate>) -> Self {
474 self.http_settings().additional_root_certificates = certificates;
475 self
476 }
477
478 #[cfg(not(target_family = "wasm"))]
482 pub fn disable_built_in_root_certificates(mut self) -> Self {
483 self.http_settings().disable_built_in_root_certificates = true;
484 self
485 }
486
487 pub fn http_client(mut self, client: reqwest::Client) -> Self {
497 self.http_cfg = Some(HttpConfig::Custom(client));
498 self
499 }
500
501 pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
506 self.server_versions = Some(value.into_iter().collect());
507 self
508 }
509
510 #[cfg(not(target_family = "wasm"))]
511 fn http_settings(&mut self) -> &mut HttpSettings {
512 self.http_cfg.get_or_insert_with(Default::default).settings()
513 }
514
515 pub fn handle_refresh_tokens(mut self) -> Self {
537 self.handle_refresh_tokens = true;
538 self
539 }
540
541 #[doc(hidden)]
543 pub fn base_client(mut self, base_client: BaseClient) -> Self {
544 self.base_client = Some(base_client);
545 self
546 }
547
548 #[cfg(feature = "e2e-encryption")]
551 pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
552 self.encryption_settings = settings;
553 self
554 }
555
556 #[cfg(feature = "e2e-encryption")]
559 pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
560 self.room_key_recipient_strategy = strategy;
561 self
562 }
563
564 #[cfg(feature = "e2e-encryption")]
566 pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
567 self.decryption_settings = decryption_settings;
568 self
569 }
570
571 #[cfg(feature = "e2e-encryption")]
579 pub fn with_enable_share_history_on_invite(
580 mut self,
581 enable_share_history_on_invite: bool,
582 ) -> Self {
583 self.enable_share_history_on_invite = enable_share_history_on_invite;
584 self
585 }
586
587 pub fn with_enable_automatic_back_pagination(mut self, enable: bool) -> Self {
593 self.enable_automatic_back_pagination = enable;
594 self
595 }
596
597 pub fn with_event_cache(mut self, enable: bool) -> Self {
610 self.enable_event_cache = enable;
611 self
612 }
613
614 pub fn cross_process_store_config(
624 mut self,
625 cross_process_store_config: CrossProcessLockConfig,
626 ) -> Self {
627 self.cross_process_lock_config = cross_process_store_config;
628 self
629 }
630
631 pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
635 self.threading_support = threading_support;
636 self
637 }
638
639 #[cfg(feature = "experimental-search")]
641 pub fn search_index_store(mut self, kind: SearchIndexStoreKind) -> Self {
642 self.search_index_store_kind = kind;
643 self
644 }
645
646 #[cfg(feature = "experimental-x509-identity-verification")]
649 pub fn with_x509_signer(mut self, x509_signer: Option<Arc<dyn RawX509Signer>>) -> Self {
650 self.x509_signer = x509_signer;
651 self
652 }
653
654 #[cfg(feature = "experimental-x509-identity-verification")]
657 pub fn with_x509_verifier(mut self, x509_verifier: Option<Arc<dyn RawX509Verifier>>) -> Self {
658 self.x509_verifier = x509_verifier;
659 self
660 }
661
662 #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
675 pub async fn build(self) -> Result<Client, ClientBuildError> {
676 debug!("Starting to build the Client");
677
678 let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
679 Span::current().record("homeserver", debug(&homeserver_cfg));
680
681 let base_client = if let Some(base_client) = self.base_client {
682 base_client
683 } else {
684 #[allow(unused_mut)]
685 let mut client = BaseClient::new(
686 build_store_config(self.store_config, &self.cross_process_lock_config).await?,
687 self.threading_support,
688 self.dm_room_definition,
689 );
690
691 #[cfg(feature = "e2e-encryption")]
692 {
693 client.room_key_recipient_strategy = self.room_key_recipient_strategy;
694 client.decryption_settings = self.decryption_settings;
695 }
696
697 #[cfg(feature = "experimental-x509-identity-verification")]
698 client.set_x509_signer(self.x509_signer);
699 #[cfg(feature = "experimental-x509-identity-verification")]
700 client.set_x509_verifier(self.x509_verifier);
701
702 client
703 };
704
705 let http_client = HttpClient::new(
706 #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
707 match self.http_cfg.unwrap_or_default() {
708 #[cfg(not(target_family = "wasm"))]
709 HttpConfig::Settings(mut settings) => {
710 settings.timeout = self.request_config.timeout;
711 settings.make_client()?
712 }
713 HttpConfig::Custom(c) => c,
714 },
715 self.request_config,
716 );
717
718 #[allow(unused_variables)]
719 let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
720 homeserver_cfg.discover(&http_client, self.well_known_lookup_disabled).await?;
721
722 let sliding_sync_version = {
723 let supported_versions = match supported_versions {
724 Some(versions) => Some(versions),
725 None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
726 Some(get_supported_versions(&homeserver, &http_client).await?)
727 }
728 None => None,
729 };
730
731 let version = self.sliding_sync_version_builder.build(
732 supported_versions.map(|response| response.as_supported_versions()).as_ref(),
733 )?;
734
735 tracing::info!(?version, "selected sliding sync version");
736
737 version
738 };
739
740 let allow_insecure_oauth = homeserver.scheme() == "http";
741 let auth_ctx = Arc::new(AuthCtx::new(self.handle_refresh_tokens, allow_insecure_oauth));
742
743 let send_queue = Arc::new(SendQueueData::new(true));
745
746 let supported_versions = match self.server_versions {
747 Some(versions) => Cached(TtlValue::without_expiry(SupportedVersions {
748 versions,
749 features: Default::default(),
750 })),
751 None => NotSet,
752 };
753 let well_known = match well_known {
754 Some(well_known) => Cached(TtlValue::new(Some(well_known.into()))),
755 None => NotSet,
756 };
757
758 let event_cache = OnceCell::new();
759 let latest_events = OnceCell::new();
760 let thread_subscriptions_catchup = OnceCell::new();
761
762 #[cfg(feature = "experimental-search")]
763 let search_index =
764 SearchIndex::new(Arc::new(Mutex::new(HashMap::new())), self.search_index_store_kind);
765
766 let inner = ClientInner::new(
767 auth_ctx,
768 server,
769 homeserver,
770 sliding_sync_version,
771 Arc::new(StdRwLock::new(PresenceState::Online)),
772 http_client,
773 base_client,
774 supported_versions,
775 well_known,
776 self.respect_login_well_known,
777 self.well_known_lookup_disabled,
778 event_cache,
779 self.enable_automatic_back_pagination,
780 self.enable_event_cache,
781 send_queue,
782 latest_events,
783 #[cfg(feature = "e2e-encryption")]
784 self.encryption_settings,
785 #[cfg(feature = "e2e-encryption")]
786 self.enable_share_history_on_invite,
787 self.cross_process_lock_config,
788 #[cfg(feature = "experimental-search")]
789 search_index,
790 thread_subscriptions_catchup,
791 self.media_fetcher.clone(),
792 )
793 .await;
794
795 debug!("Done building the Client");
796
797 Ok(Client { inner })
798 }
799}
800
801pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
805 ServerName::parse(
806 s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
807 )
808}
809
810#[allow(clippy::unused_async, unused)] async fn build_store_config(
812 builder_config: BuilderStoreConfig,
813 cross_process_store_config: &CrossProcessLockConfig,
814) -> Result<StoreConfig, ClientBuildError> {
815 #[allow(clippy::infallible_destructuring_match)]
816 let store_config = match builder_config {
817 #[cfg(feature = "sqlite")]
818 BuilderStoreConfig::Sqlite { config, cache_path } => {
819 let config_with_cache_path = if let Some(ref cache_path) = cache_path {
820 config.clone().path(cache_path)
821 } else {
822 config.clone()
823 };
824
825 #[cfg(feature = "e2e-encryption")]
826 let (state_store, event_cache_store, media_store, crypto_store) = try_join!(
827 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
828 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
829 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config_with_cache_path),
830 matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(&config),
831 )?;
832 #[cfg(not(feature = "e2e-encryption"))]
833 let (state_store, event_cache_store, media_store) = try_join!(
834 matrix_sdk_sqlite::SqliteStateStore::open_with_config(&config),
835 matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(&config_with_cache_path),
836 matrix_sdk_sqlite::SqliteMediaStore::open_with_config(&config),
837 )?;
838 let store_config = StoreConfig::new(cross_process_store_config.clone())
839 .state_store(state_store)
840 .event_cache_store(event_cache_store)
841 .media_store(media_store);
842
843 #[cfg(feature = "e2e-encryption")]
844 let store_config = store_config.crypto_store(crypto_store);
845
846 store_config
847 }
848
849 #[cfg(feature = "indexeddb")]
850 BuilderStoreConfig::IndexedDb { name, passphrase } => {
851 build_indexeddb_store_config(
852 &name,
853 passphrase.as_deref(),
854 cross_process_store_config.clone(),
855 )
856 .await?
857 }
858
859 BuilderStoreConfig::Custom(config) => config,
860 };
861 Ok(store_config)
862}
863
864#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
867async fn build_indexeddb_store_config(
868 name: &str,
869 passphrase: Option<&str>,
870 cross_process_store_config: CrossProcessLockConfig,
871) -> Result<StoreConfig, ClientBuildError> {
872 let stores = matrix_sdk_indexeddb::IndexeddbStores::open(name, passphrase).await?;
873 let store_config = StoreConfig::new(cross_process_store_config)
874 .state_store(stores.state)
875 .event_cache_store(stores.event_cache)
876 .media_store(stores.media);
877
878 #[cfg(feature = "e2e-encryption")]
879 let store_config = store_config.crypto_store(stores.crypto);
880
881 Ok(store_config)
882}
883
884#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
885#[allow(clippy::unused_async)]
886async fn build_indexeddb_store_config(
887 _name: &str,
888 _passphrase: Option<&str>,
889 _cross_process_store_config: CrossProcessLockConfig,
890) -> Result<StoreConfig, ClientBuildError> {
891 panic!("the IndexedDB is only available on the 'wasm32' arch")
892}
893
894#[derive(Clone, Debug)]
895enum HttpConfig {
896 #[cfg(not(target_family = "wasm"))]
897 Settings(HttpSettings),
898 Custom(reqwest::Client),
899}
900
901#[cfg(not(target_family = "wasm"))]
902impl HttpConfig {
903 fn settings(&mut self) -> &mut HttpSettings {
904 match self {
905 Self::Settings(s) => s,
906 Self::Custom(_) => {
907 *self = Self::default();
908 match self {
909 Self::Settings(s) => s,
910 Self::Custom(_) => unreachable!(),
911 }
912 }
913 }
914 }
915}
916
917impl Default for HttpConfig {
918 fn default() -> Self {
919 #[cfg(not(target_family = "wasm"))]
920 return Self::Settings(HttpSettings::default());
921
922 #[cfg(target_family = "wasm")]
923 return Self::Custom(reqwest::Client::new());
924 }
925}
926
927#[derive(Clone)]
928enum BuilderStoreConfig {
929 #[cfg(feature = "sqlite")]
930 Sqlite {
931 config: SqliteStoreConfig,
932 cache_path: Option<PathBuf>,
933 },
934 #[cfg(feature = "indexeddb")]
935 IndexedDb {
936 name: String,
937 passphrase: Option<String>,
938 },
939 Custom(StoreConfig),
940}
941
942#[cfg(not(tarpaulin_include))]
943impl fmt::Debug for BuilderStoreConfig {
944 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
945 #[allow(clippy::infallible_destructuring_match)]
946 match self {
947 #[cfg(feature = "sqlite")]
948 Self::Sqlite { config, cache_path, .. } => f
949 .debug_struct("Sqlite")
950 .field("config", config)
951 .field("cache_path", cache_path)
952 .finish_non_exhaustive(),
953
954 #[cfg(feature = "indexeddb")]
955 Self::IndexedDb { name, .. } => {
956 f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
957 }
958
959 Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
960 }
961 }
962}
963
964#[derive(Debug, Error)]
966pub enum ClientBuildError {
967 #[error("No homeserver or user ID was configured")]
969 MissingHomeserver,
970
971 #[error("The supplied server name is invalid")]
973 InvalidServerName,
974
975 #[error(
979 "Homeserver discovery requires a .well-known lookup, which was disabled; \
980 use `ClientBuilder::homeserver_url` instead"
981 )]
982 WellKnownLookupDisabled,
983
984 #[error("Error looking up the .well-known endpoint on auto-discovery")]
986 AutoDiscovery(Box<FromHttpResponseError<RumaApiError>>),
987
988 #[error(transparent)]
990 SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
991
992 #[error(transparent)]
994 Url(#[from] url::ParseError),
995
996 #[error(transparent)]
998 Http(#[from] HttpError),
999
1000 #[cfg(feature = "indexeddb")]
1002 #[error(transparent)]
1003 IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
1004
1005 #[cfg(feature = "sqlite")]
1007 #[error(transparent)]
1008 SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
1009}
1010
1011#[cfg(all(test, not(target_family = "wasm")))]
1013pub(crate) mod tests {
1014 use std::{future, iter, net::SocketAddr, sync::Mutex as StdMutex};
1015
1016 use assert_matches::assert_matches;
1017 use matrix_sdk_test::{async_test, test_json};
1018 use reqwest::dns::{Addrs, Name, Resolve, Resolving};
1019 use serde_json::{Value as JsonValue, json_internal};
1020 use strass::assert_let;
1021 use url::Url;
1022 use wiremock::{
1023 Mock, MockServer, ResponseTemplate,
1024 matchers::{header, method, path},
1025 };
1026
1027 use super::*;
1028 use crate::sliding_sync::Version as SlidingSyncVersion;
1029
1030 #[test]
1031 fn test_sanitize_server_name() {
1032 assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
1033 assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
1034 assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
1035 assert_eq!(
1036 sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
1037 "matrix.server.org"
1038 );
1039 assert_eq!(
1040 sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
1041 "matrix.server.org"
1042 );
1043 assert_eq!(
1044 sanitize_server_name(" https://matrix.server.org// ").unwrap().as_str(),
1045 "matrix.server.org"
1046 );
1047 assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
1048 }
1049
1050 #[async_test]
1058 async fn test_discovery_invalid_server() {
1059 let mut builder = ClientBuilder::new();
1061
1062 builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
1064 let error = builder.build().await.unwrap_err();
1065
1066 assert_matches!(error, ClientBuildError::InvalidServerName);
1068 }
1069
1070 #[async_test]
1071 async fn test_discovery_no_server() {
1072 let mut builder = ClientBuilder::new();
1074
1075 builder = builder.server_name_or_homeserver_url("localhost:3456");
1077 let error = builder.build().await.unwrap_err();
1078
1079 println!("{error}");
1081 assert_matches!(error, ClientBuildError::Http(_));
1082 }
1083
1084 #[async_test]
1085 async fn test_discovery_web_server() {
1086 let server = MockServer::start().await;
1089 let mut builder = ClientBuilder::new();
1090
1091 builder = builder.server_name_or_homeserver_url(server.uri());
1093 let error = builder.build().await.unwrap_err();
1094
1095 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
1097 assert_matches!(*e, FromHttpResponseError::Server(_));
1098 }
1099
1100 #[async_test]
1101 async fn test_discovery_direct_legacy() {
1102 let homeserver = make_mock_homeserver().await;
1104 let mut builder = ClientBuilder::new();
1105
1106 builder = builder.server_name_or_homeserver_url(homeserver.uri());
1108 let _client = builder.build().await.unwrap();
1109
1110 assert!(_client.sliding_sync_version().is_native());
1112 }
1113
1114 #[async_test]
1115 async fn test_discovery_well_known_parse_error() {
1116 let server = MockServer::start().await;
1118 let homeserver = make_mock_homeserver().await;
1119 let mut builder = ClientBuilder::new();
1120
1121 let well_known = make_well_known_json(&homeserver.uri());
1122 let bad_json = well_known.to_string().replace(',', "");
1123 Mock::given(method("GET"))
1124 .and(path("/.well-known/matrix/client"))
1125 .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
1126 .mount(&server)
1127 .await;
1128
1129 builder = builder.server_name_or_homeserver_url(server.uri());
1131 let error = builder.build().await.unwrap_err();
1132
1133 assert_let!(ClientBuildError::AutoDiscovery(e) = error);
1135 assert_matches!(*e, FromHttpResponseError::Deserialization(_));
1136 }
1137
1138 #[async_test]
1139 async fn test_discovery_well_known_legacy() {
1140 let server = MockServer::start().await;
1143 let homeserver = make_mock_homeserver().await;
1144 let mut builder = ClientBuilder::new();
1145
1146 Mock::given(method("GET"))
1147 .and(path("/.well-known/matrix/client"))
1148 .respond_with(
1149 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1150 )
1151 .mount(&server)
1152 .await;
1153
1154 builder = builder.server_name_or_homeserver_url(server.uri());
1156 let client = builder.build().await.unwrap();
1157
1158 assert!(client.sliding_sync_version().is_native());
1162 }
1163
1164 #[async_test]
1165 async fn test_discovery_server_name_with_well_known_lookup_disabled() {
1166 let builder = ClientBuilder::new()
1168 .server_name(&ServerName::parse("example.org").unwrap())
1169 .disable_well_known_lookup(true);
1170
1171 let error = builder.build().await.unwrap_err();
1174
1175 assert_matches!(error, ClientBuildError::WellKnownLookupDisabled);
1178
1179 let error = ClientBuilder::new()
1181 .insecure_server_name_no_tls(&ServerName::parse("example.org").unwrap())
1182 .disable_well_known_lookup(true)
1183 .build()
1184 .await
1185 .unwrap_err();
1186
1187 assert_matches!(error, ClientBuildError::WellKnownLookupDisabled);
1188 }
1189
1190 #[async_test]
1191 async fn test_discovery_server_name_or_url_with_well_known_lookup_disabled() {
1192 let homeserver = make_mock_homeserver().await;
1196 Mock::given(method("GET"))
1197 .and(path("/.well-known/matrix/client"))
1198 .respond_with(
1199 ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
1200 )
1201 .named("well-known mock")
1202 .expect(0)
1203 .mount(&homeserver)
1204 .await;
1205
1206 let client = ClientBuilder::new()
1208 .server_name_or_homeserver_url(homeserver.uri())
1209 .disable_well_known_lookup(true)
1210 .build()
1211 .await
1212 .unwrap();
1213
1214 assert_eq!(client.homeserver().as_str().trim_end_matches('/'), homeserver.uri());
1217 }
1218
1219 #[async_test]
1220 async fn test_homeserver_url_never_contacts_the_server_name() {
1221 let mock_server = MockServer::start().await;
1225 let address = *mock_server.address();
1226 let port = address.port();
1227 let server_name = format!("servername.com:{port}");
1228 let homeserver_name = format!("matrix.server.com:{port}");
1229
1230 let resolver = Arc::new(RecordingResolver { address, hosts: StdMutex::new(Vec::new()) });
1232 let http_client =
1233 reqwest::Client::builder().dns_resolver(resolver.clone()).build().unwrap();
1234
1235 Mock::given(method("GET"))
1236 .and(path("/.well-known/matrix/client"))
1237 .and(header("host", server_name.as_str()))
1238 .respond_with(
1239 ResponseTemplate::new(200)
1240 .set_body_json(make_well_known_json(&format!("http://{homeserver_name}"))),
1241 )
1242 .mount(&mock_server)
1243 .await;
1244
1245 Mock::given(method("GET"))
1246 .and(path("/_matrix/client/versions"))
1247 .and(header("host", homeserver_name.as_str()))
1248 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1249 .mount(&mock_server)
1250 .await;
1251
1252 let client = ClientBuilder::new()
1255 .http_client(http_client)
1256 .server_name_or_homeserver_url(format!("http://{homeserver_name}"))
1257 .build()
1258 .await
1259 .unwrap();
1260
1261 let resolved_hosts = resolver.hosts.lock().unwrap();
1264 assert!(!resolved_hosts.is_empty(), "The homeserver should have been contacted");
1265 assert!(
1266 resolved_hosts.iter().all(|host| host == "matrix.server.com"),
1267 "A connection was attempted to an unexpected host: {resolved_hosts:?}"
1268 );
1269 assert_eq!(client.homeserver(), Url::parse(&format!("http://{homeserver_name}")).unwrap());
1270 assert_eq!(client.server(), None);
1271 }
1272
1273 #[async_test]
1274 async fn test_sliding_sync_discover_native() {
1275 let homeserver = make_mock_homeserver().await;
1277 let mut builder = ClientBuilder::new();
1278
1279 builder = builder
1282 .server_name_or_homeserver_url(homeserver.uri())
1283 .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
1284
1285 let client = builder.build().await.unwrap();
1286
1287 assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
1289 }
1290
1291 #[async_test]
1292 #[cfg(feature = "e2e-encryption")]
1293 async fn test_set_up_decryption_trust_requirement_cross_signed() {
1294 let homeserver = make_mock_homeserver().await;
1295 let builder = ClientBuilder::new()
1296 .server_name_or_homeserver_url(homeserver.uri())
1297 .with_decryption_settings(DecryptionSettings {
1298 sender_device_trust_requirement: TrustRequirement::CrossSigned,
1299 });
1300
1301 let client = builder.build().await.unwrap();
1302 assert_matches!(
1303 client.base_client().decryption_settings.sender_device_trust_requirement,
1304 TrustRequirement::CrossSigned
1305 );
1306 }
1307
1308 #[async_test]
1309 #[cfg(feature = "e2e-encryption")]
1310 async fn test_set_up_decryption_trust_requirement_untrusted() {
1311 let homeserver = make_mock_homeserver().await;
1312
1313 let builder = ClientBuilder::new()
1314 .server_name_or_homeserver_url(homeserver.uri())
1315 .with_decryption_settings(DecryptionSettings {
1316 sender_device_trust_requirement: TrustRequirement::Untrusted,
1317 });
1318
1319 let client = builder.build().await.unwrap();
1320 assert_matches!(
1321 client.base_client().decryption_settings.sender_device_trust_requirement,
1322 TrustRequirement::Untrusted
1323 );
1324 }
1325
1326 #[async_test]
1327 async fn test_cross_process_store_locks_holder_name() {
1328 {
1329 let homeserver = make_mock_homeserver().await;
1330 let client =
1331 ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1332
1333 assert_let!(
1334 CrossProcessLockConfig::MultiProcess { holder_name } =
1335 client.cross_process_lock_config()
1336 );
1337 assert_eq!(holder_name, "main");
1338 }
1339
1340 {
1341 let homeserver = make_mock_homeserver().await;
1342 let client = ClientBuilder::new()
1343 .homeserver_url(homeserver.uri())
1344 .cross_process_store_config(CrossProcessLockConfig::multi_process("foo"))
1345 .build()
1346 .await
1347 .unwrap();
1348
1349 assert_let!(
1350 CrossProcessLockConfig::MultiProcess { holder_name } =
1351 client.cross_process_lock_config()
1352 );
1353 assert_eq!(holder_name, "foo");
1354 }
1355 }
1356
1357 async fn make_mock_homeserver() -> MockServer {
1360 let homeserver = MockServer::start().await;
1361 Mock::given(method("GET"))
1362 .and(path("/_matrix/client/versions"))
1363 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1364 .mount(&homeserver)
1365 .await;
1366 Mock::given(method("GET"))
1367 .and(path("/_matrix/client/r0/login"))
1368 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1369 .mount(&homeserver)
1370 .await;
1371 homeserver
1372 }
1373
1374 fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1375 ::serde_json::Value::Object({
1376 let mut object = ::serde_json::Map::new();
1377 let _ = object.insert(
1378 "m.homeserver".into(),
1379 json_internal!({
1380 "base_url": homeserver_url
1381 }),
1382 );
1383
1384 object
1385 })
1386 }
1387
1388 #[derive(Debug)]
1391 struct RecordingResolver {
1392 address: SocketAddr,
1393 hosts: StdMutex<Vec<String>>,
1394 }
1395
1396 impl Resolve for RecordingResolver {
1397 fn resolve(&self, name: Name) -> Resolving {
1398 self.hosts.lock().unwrap().push(name.as_str().to_owned());
1399
1400 let addrs: Addrs = Box::new(iter::once(self.address));
1401 Box::pin(future::ready(Ok(addrs)))
1402 }
1403 }
1404}