Skip to main content

matrix_sdk/client/builder/
homeserver_config.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use ruma::{
16    OwnedServerName, ServerName,
17    api::client::discovery::{discover_homeserver, get_supported_versions},
18};
19use tracing::debug;
20use url::Url;
21
22use crate::{
23    ClientBuildError, HttpError, config::RequestConfig, http_client::HttpClient,
24    sanitize_server_name,
25};
26
27/// Configuration for the homeserver.
28#[derive(Clone, Debug)]
29pub(super) enum HomeserverConfig {
30    /// A homeserver name URL, including the protocol.
31    HomeserverUrl(String),
32
33    /// A server name, with the protocol put apart.
34    ServerName { server: OwnedServerName, protocol: UrlScheme },
35
36    /// A server name with or without the protocol (it will fallback to `https`
37    /// if absent), or a homeserver URL.
38    ServerNameOrHomeserverUrl(String),
39}
40
41/// A simple helper to represent `http` or `https` in a URL.
42#[derive(Clone, Copy, Debug)]
43pub(super) enum UrlScheme {
44    Http,
45    Https,
46}
47
48/// The `Ok` result for `HomeserverConfig::discover`.
49pub(super) struct HomeserverDiscoveryResult {
50    pub server: Option<Url>,
51    pub homeserver: Url,
52    pub supported_versions: Option<get_supported_versions::Response>,
53    pub well_known: Option<discover_homeserver::Response>,
54}
55
56impl HomeserverConfig {
57    /// Resolve this configuration into a homeserver URL.
58    ///
59    /// If `well_known_lookup_disabled` is set, no request is ever made to the
60    /// `.well-known/matrix/client` URI. [`Self::ServerName`] can then not be
61    /// resolved at all, and fails with
62    /// [`ClientBuildError::WellKnownLookupDisabled`].
63    pub async fn discover(
64        &self,
65        http_client: &HttpClient,
66        well_known_lookup_disabled: bool,
67    ) -> Result<HomeserverDiscoveryResult, ClientBuildError> {
68        Ok(match self {
69            Self::HomeserverUrl(url) => {
70                let homeserver = Url::parse(url)?;
71
72                HomeserverDiscoveryResult {
73                    server: None, // We can't know the `server` if we only have a `homeserver`.
74                    homeserver,
75                    supported_versions: None,
76                    well_known: None,
77                }
78            }
79
80            Self::ServerName { server, protocol } => {
81                // The well-known is the only source of the homeserver URL here, so there is
82                // nothing we could fall back to. Assuming the server name *is* the homeserver
83                // would silently talk to the wrong host for any delegating deployment.
84                if well_known_lookup_disabled {
85                    return Err(ClientBuildError::WellKnownLookupDisabled);
86                }
87
88                let (server, well_known) =
89                    discover_homeserver(server, protocol, http_client).await?;
90
91                HomeserverDiscoveryResult {
92                    server: Some(server),
93                    homeserver: Url::parse(&well_known.homeserver.base_url)?,
94                    supported_versions: None,
95                    well_known: Some(well_known),
96                }
97            }
98
99            Self::ServerNameOrHomeserverUrl(server_name_or_url) => {
100                let (server, homeserver, supported_versions, well_known) =
101                    discover_homeserver_from_server_name_or_url(
102                        server_name_or_url.to_owned(),
103                        http_client,
104                        well_known_lookup_disabled,
105                    )
106                    .await?;
107
108                HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known }
109            }
110        })
111    }
112}
113
114/// Discovers a homeserver from a server name or a URL.
115///
116/// Tries well-known discovery and checking if the URL points to a homeserver.
117///
118/// If `well_known_lookup_disabled` is set, the well-known discovery step is
119/// skipped entirely and only the homeserver URL check is performed.
120async fn discover_homeserver_from_server_name_or_url(
121    mut server_name_or_url: String,
122    http_client: &HttpClient,
123    well_known_lookup_disabled: bool,
124) -> Result<
125    (
126        Option<Url>,
127        Url,
128        Option<get_supported_versions::Response>,
129        Option<discover_homeserver::Response>,
130    ),
131    ClientBuildError,
132> {
133    let mut discovery_error: Option<ClientBuildError> = None;
134
135    // Attempt discovery as a server name first.
136    let sanitize_result = sanitize_server_name(&server_name_or_url);
137
138    if let Ok(server_name) = sanitize_result.as_ref() {
139        let protocol = if server_name_or_url.starts_with("http://") {
140            UrlScheme::Http
141        } else {
142            UrlScheme::Https
143        };
144
145        let server_name_as_url = match protocol {
146            UrlScheme::Http => format!("http://{server_name}"),
147            UrlScheme::Https => format!("https://{server_name}"),
148        };
149
150        if well_known_lookup_disabled {
151            debug!("Well-known discovery is disabled, checking for a homeserver URL directly.");
152            server_name_or_url = server_name_as_url;
153        } else {
154            match discover_homeserver(server_name, &protocol, http_client).await {
155                Ok((server, well_known)) => {
156                    return Ok((
157                        Some(server),
158                        Url::parse(&well_known.homeserver.base_url)?,
159                        None,
160                        Some(well_known),
161                    ));
162                }
163                Err(e) => {
164                    debug!(error = %e, "Well-known discovery failed.");
165                    discovery_error = Some(e);
166
167                    // Check if the server name points to a homeserver.
168                    server_name_or_url = server_name_as_url;
169                }
170            }
171        }
172    }
173
174    // When discovery fails, or the input isn't a valid server name, fallback to
175    // trying a homeserver URL.
176    if let Ok(homeserver_url) = Url::parse(&server_name_or_url) {
177        // Make sure the URL is definitely for a homeserver.
178        match get_supported_versions(&homeserver_url, http_client).await {
179            Ok(response) => {
180                return Ok((None, homeserver_url, Some(response), None));
181            }
182            Err(e) => {
183                debug!(error = %e, "Checking supported versions failed.");
184            }
185        }
186    }
187
188    Err(discovery_error.unwrap_or(ClientBuildError::InvalidServerName))
189}
190
191/// Discovers a homeserver by looking up the well-known at the supplied server
192/// name.
193async fn discover_homeserver(
194    server_name: &ServerName,
195    protocol: &UrlScheme,
196    http_client: &HttpClient,
197) -> Result<(Url, discover_homeserver::Response), ClientBuildError> {
198    debug!("Trying to discover the homeserver");
199
200    let server = Url::parse(&match protocol {
201        UrlScheme::Http => format!("http://{server_name}"),
202        UrlScheme::Https => format!("https://{server_name}"),
203    })?;
204
205    let well_known = http_client
206        .send(
207            discover_homeserver::Request::new(),
208            Some(RequestConfig::short_retry()),
209            server.to_string(),
210            None,
211            (),
212            Default::default(),
213        )
214        .await
215        .map_err(|e| match e {
216            HttpError::Api(err) => ClientBuildError::AutoDiscovery(err),
217            err => ClientBuildError::Http(err),
218        })?;
219
220    debug!(homeserver_url = well_known.homeserver.base_url, "Discovered the homeserver");
221
222    Ok((server, well_known))
223}
224
225pub(super) async fn get_supported_versions(
226    homeserver_url: &Url,
227    http_client: &HttpClient,
228) -> Result<get_supported_versions::Response, HttpError> {
229    http_client
230        .send(
231            get_supported_versions::Request::new(),
232            Some(RequestConfig::short_retry()),
233            homeserver_url.to_string(),
234            None,
235            (),
236            Default::default(),
237        )
238        .await
239}
240
241#[cfg(all(test, not(target_family = "wasm")))]
242mod tests {
243    use assert_matches::assert_matches;
244    use matrix_sdk_test::async_test;
245    use ruma::OwnedServerName;
246    use serde_json::json;
247    use wiremock::{
248        Mock, MockServer, ResponseTemplate,
249        matchers::{method, path},
250    };
251
252    use super::*;
253    use crate::http_client::HttpSettings;
254
255    #[async_test]
256    async fn test_url() {
257        let http_client =
258            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
259
260        let result = HomeserverConfig::HomeserverUrl("https://matrix-client.matrix.org".to_owned())
261            .discover(&http_client, false)
262            .await
263            .unwrap();
264
265        assert_eq!(result.server, None);
266        assert_eq!(result.homeserver, Url::parse("https://matrix-client.matrix.org").unwrap());
267        assert!(result.supported_versions.is_none());
268    }
269
270    #[async_test]
271    async fn test_server_name() {
272        let http_client =
273            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
274
275        let server = MockServer::start().await;
276        let homeserver = MockServer::start().await;
277
278        Mock::given(method("GET"))
279            .and(path("/.well-known/matrix/client"))
280            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
281                "m.homeserver": {
282                    "base_url": homeserver.uri(),
283                },
284            })))
285            .mount(&server)
286            .await;
287
288        let result = HomeserverConfig::ServerName {
289            server: OwnedServerName::try_from(server.address().to_string()).unwrap(),
290            protocol: UrlScheme::Http,
291        }
292        .discover(&http_client, false)
293        .await
294        .unwrap();
295
296        assert_eq!(result.server, Some(Url::parse(&server.uri()).unwrap()));
297        assert_eq!(result.homeserver, Url::parse(&homeserver.uri()).unwrap());
298        assert!(result.supported_versions.is_none());
299    }
300
301    #[async_test]
302    async fn test_server_name_or_url_with_name() {
303        let http_client =
304            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
305
306        let server = MockServer::start().await;
307        let homeserver = MockServer::start().await;
308
309        Mock::given(method("GET"))
310            .and(path("/.well-known/matrix/client"))
311            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
312                "m.homeserver": {
313                    "base_url": homeserver.uri(),
314                },
315            })))
316            .mount(&server)
317            .await;
318
319        let result = HomeserverConfig::ServerNameOrHomeserverUrl(server.uri().to_string())
320            .discover(&http_client, false)
321            .await
322            .unwrap();
323
324        assert_eq!(result.server, Some(Url::parse(&server.uri()).unwrap()));
325        assert_eq!(result.homeserver, Url::parse(&homeserver.uri()).unwrap());
326        assert!(result.supported_versions.is_none());
327    }
328
329    #[async_test]
330    async fn test_server_name_or_url_with_url() {
331        let http_client =
332            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
333
334        let homeserver = MockServer::start().await;
335
336        Mock::given(method("GET"))
337            .and(path("/_matrix/client/versions"))
338            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
339                "versions": [],
340            })))
341            .mount(&homeserver)
342            .await;
343
344        let result = HomeserverConfig::ServerNameOrHomeserverUrl(homeserver.uri().to_string())
345            .discover(&http_client, false)
346            .await
347            .unwrap();
348
349        assert!(result.server.is_none());
350        assert_eq!(result.homeserver, Url::parse(&homeserver.uri()).unwrap());
351        assert!(result.supported_versions.is_some());
352    }
353
354    /// Mounts a well-known mock that must never be hit. `MockServer` verifies
355    /// the expectation when it is dropped, at the end of the test.
356    async fn mock_well_known_never_called(server: &MockServer, homeserver: &MockServer) {
357        Mock::given(method("GET"))
358            .and(path("/.well-known/matrix/client"))
359            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
360                "m.homeserver": {
361                    "base_url": homeserver.uri(),
362                },
363            })))
364            .named("well-known mock")
365            .expect(0)
366            .mount(server)
367            .await;
368    }
369
370    #[async_test]
371    async fn test_url_with_well_known_lookup_disabled() {
372        let http_client =
373            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
374
375        // A homeserver URL never needs a lookup, so the flag changes nothing.
376        let result = HomeserverConfig::HomeserverUrl("https://matrix-client.matrix.org".to_owned())
377            .discover(&http_client, true)
378            .await
379            .unwrap();
380
381        assert_eq!(result.server, None);
382        assert_eq!(result.homeserver, Url::parse("https://matrix-client.matrix.org").unwrap());
383        assert!(result.supported_versions.is_none());
384    }
385
386    #[async_test]
387    async fn test_server_name_with_well_known_lookup_disabled() {
388        let http_client =
389            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
390
391        let server = MockServer::start().await;
392        let homeserver = MockServer::start().await;
393
394        mock_well_known_never_called(&server, &homeserver).await;
395
396        // A server name can only be resolved through the well-known, so this must fail
397        // rather than guess a homeserver.
398        let error = HomeserverConfig::ServerName {
399            server: OwnedServerName::try_from(server.address().to_string()).unwrap(),
400            protocol: UrlScheme::Http,
401        }
402        .discover(&http_client, true)
403        .await
404        .err()
405        .unwrap();
406
407        assert_matches!(error, ClientBuildError::WellKnownLookupDisabled);
408    }
409
410    #[async_test]
411    async fn test_server_name_or_url_with_name_and_well_known_lookup_disabled() {
412        let http_client =
413            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
414
415        let server = MockServer::start().await;
416        let homeserver = MockServer::start().await;
417
418        mock_well_known_never_called(&server, &homeserver).await;
419
420        // The value points at a delegating server, not at a homeserver: with the
421        // well-known step skipped, the homeserver check is all that's left, and it
422        // fails since `server` doesn't answer `/_matrix/client/versions`.
423        let error = HomeserverConfig::ServerNameOrHomeserverUrl(server.uri().to_string())
424            .discover(&http_client, true)
425            .await
426            .err()
427            .unwrap();
428
429        assert_matches!(error, ClientBuildError::InvalidServerName);
430    }
431
432    #[async_test]
433    async fn test_server_name_or_url_with_url_and_well_known_lookup_disabled() {
434        let http_client =
435            HttpClient::new(HttpSettings::default().make_client().unwrap(), Default::default());
436
437        let homeserver = MockServer::start().await;
438
439        mock_well_known_never_called(&homeserver, &homeserver).await;
440
441        Mock::given(method("GET"))
442            .and(path("/_matrix/client/versions"))
443            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
444                "versions": [],
445            })))
446            .mount(&homeserver)
447            .await;
448
449        // The value points at a homeserver, which the `/_matrix/client/versions` check
450        // proves, so this resolves without ever touching the well-known.
451        let result = HomeserverConfig::ServerNameOrHomeserverUrl(homeserver.uri().to_string())
452            .discover(&http_client, true)
453            .await
454            .unwrap();
455
456        assert!(result.server.is_none());
457        assert_eq!(result.homeserver, Url::parse(&homeserver.uri()).unwrap());
458        assert!(result.supported_versions.is_some());
459        assert!(result.well_known.is_none());
460    }
461}