Make client use .well-known redirects

Was supposed to fix #219, but apparently that was about something else.
master
timorl 2021-05-13 13:13:58 +02:00
parent fe17dce813
commit ded5830deb
4 changed files with 232 additions and 48 deletions

View File

@ -75,29 +75,6 @@ pub enum LoopCtrl {
Break, Break,
} }
use matrix_sdk_common::{
api::r0::{
account::register,
device::{delete_devices, get_devices},
directory::{get_public_rooms, get_public_rooms_filtered},
filter::{create_filter::Request as FilterUploadRequest, FilterDefinition},
media::{create_content, get_content, get_content_thumbnail},
membership::{join_room_by_id, join_room_by_id_or_alias},
message::send_message_event,
profile::{get_avatar_url, get_display_name, set_avatar_url, set_display_name},
room::create_room,
session::{get_login_types, login, sso_login},
sync::sync_events,
uiaa::AuthData,
},
assign,
identifiers::{DeviceIdBox, RoomId, RoomIdOrAliasId, ServerName, UserId},
instant::{Duration, Instant},
locks::{Mutex, RwLock},
presence::PresenceState,
uuid::Uuid,
FromHttpResponseError, UInt,
};
#[cfg(feature = "encryption")] #[cfg(feature = "encryption")]
use matrix_sdk_common::{ use matrix_sdk_common::{
api::r0::{ api::r0::{
@ -108,6 +85,32 @@ use matrix_sdk_common::{
}, },
identifiers::EventId, identifiers::EventId,
}; };
use matrix_sdk_common::{
api::{
r0::{
account::register,
device::{delete_devices, get_devices},
directory::{get_public_rooms, get_public_rooms_filtered},
filter::{create_filter::Request as FilterUploadRequest, FilterDefinition},
media::{create_content, get_content, get_content_thumbnail},
membership::{join_room_by_id, join_room_by_id_or_alias},
message::send_message_event,
profile::{get_avatar_url, get_display_name, set_avatar_url, set_display_name},
room::create_room,
session::{get_login_types, login, sso_login},
sync::sync_events,
uiaa::AuthData,
},
unversioned::{discover_homeserver, get_supported_versions},
},
assign,
identifiers::{DeviceIdBox, RoomId, RoomIdOrAliasId, ServerName, UserId},
instant::{Duration, Instant},
locks::{Mutex, RwLock},
presence::PresenceState,
uuid::Uuid,
FromHttpResponseError, UInt,
};
#[cfg(feature = "encryption")] #[cfg(feature = "encryption")]
use crate::{ use crate::{
@ -142,7 +145,7 @@ const SSO_SERVER_BIND_TRIES: u8 = 10;
#[derive(Clone)] #[derive(Clone)]
pub struct Client { pub struct Client {
/// The URL of the homeserver to connect to. /// The URL of the homeserver to connect to.
homeserver: Arc<Url>, homeserver: Arc<RwLock<Url>>,
/// The underlying HTTP client. /// The underlying HTTP client.
http_client: HttpClient, http_client: HttpClient,
/// User session data. /// User session data.
@ -164,7 +167,7 @@ pub struct Client {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
impl Debug for Client { impl Debug for Client {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
write!(fmt, "Client {{ homeserver: {} }}", self.homeserver) write!(fmt, "Client")
} }
} }
@ -502,7 +505,7 @@ impl Client {
/// ///
/// * `config` - Configuration for the client. /// * `config` - Configuration for the client.
pub fn new_with_config(homeserver_url: Url, config: ClientConfig) -> Result<Self> { pub fn new_with_config(homeserver_url: Url, config: ClientConfig) -> Result<Self> {
let homeserver = Arc::new(homeserver_url); let homeserver = Arc::new(RwLock::new(homeserver_url));
let client = if let Some(client) = config.client { let client = if let Some(client) = config.client {
client client
@ -513,12 +516,8 @@ impl Client {
let base_client = BaseClient::new_with_config(config.base_config)?; let base_client = BaseClient::new_with_config(config.base_config)?;
let session = base_client.session().clone(); let session = base_client.session().clone();
let http_client = HttpClient { let http_client =
homeserver: homeserver.clone(), HttpClient::new(client, homeserver.clone(), session, config.request_config);
inner: client,
session,
request_config: config.request_config,
};
Ok(Self { Ok(Self {
homeserver, homeserver,
@ -534,6 +533,89 @@ impl Client {
}) })
} }
/// Creates a new client for making HTTP requests to the homeserver of the
/// given user. Follows homeserver discovery directions described
/// [here](https://spec.matrix.org/unstable/client-server-api/#well-known-uri).
///
/// # Arguments
///
/// * `user_id` - The id of the user whose homeserver the client should
/// connect to.
///
/// # Example
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, identifiers::UserId};
/// # use futures::executor::block_on;
/// let alice = UserId::try_from("@alice:example.org").unwrap();
/// # block_on(async {
/// let client = Client::new_from_user_id(alice.clone()).await.unwrap();
/// client.login(alice.localpart(), "password", None, None).await.unwrap();
/// # });
/// ```
pub async fn new_from_user_id(user_id: UserId) -> Result<Self> {
let config = ClientConfig::new();
Client::new_from_user_id_with_config(user_id, config).await
}
/// Creates a new client for making HTTP requests to the homeserver of the
/// given user and configuration. Follows homeserver discovery directions
/// described [here](https://spec.matrix.org/unstable/client-server-api/#well-known-uri).
///
/// # Arguments
///
/// * `user_id` - The id of the user whose homeserver the client should
/// connect to.
///
/// * `config` - Configuration for the client.
pub async fn new_from_user_id_with_config(
user_id: UserId,
config: ClientConfig,
) -> Result<Self> {
let homeserver = Client::homeserver_from_user_id(user_id)?;
let mut client = Client::new_with_config(homeserver, config)?;
let well_known = client.discover_homeserver().await?;
let well_known = Url::parse(well_known.homeserver.base_url.as_ref())?;
client.set_homeserver(well_known).await;
client.get_supported_versions().await?;
Ok(client)
}
fn homeserver_from_user_id(user_id: UserId) -> Result<Url> {
let homeserver = format!("https://{}", user_id.server_name());
#[allow(unused_mut)]
let mut result = Url::parse(homeserver.as_str())?;
// Mockito only knows how to test http endpoints:
// https://github.com/lipanski/mockito/issues/127
#[cfg(test)]
let _ = result.set_scheme("http");
Ok(result)
}
async fn discover_homeserver(&self) -> Result<discover_homeserver::Response> {
self.send(discover_homeserver::Request::new(), Some(RequestConfig::new().disable_retry()))
.await
}
/// Change the homeserver URL used by this client.
///
/// # Arguments
///
/// * `homeserver_url` - The new URL to use.
pub async fn set_homeserver(&mut self, homeserver_url: Url) {
let mut homeserver = self.homeserver.write().await;
*homeserver = homeserver_url;
}
async fn get_supported_versions(&self) -> Result<get_supported_versions::Response> {
self.send(
get_supported_versions::Request::new(),
Some(RequestConfig::new().disable_retry()),
)
.await
}
/// Process a [transaction] received from the homeserver /// Process a [transaction] received from the homeserver
/// ///
/// # Arguments /// # Arguments
@ -566,8 +648,8 @@ impl Client {
} }
/// The Homeserver of the client. /// The Homeserver of the client.
pub fn homeserver(&self) -> &Url { pub async fn homeserver(&self) -> Url {
&self.homeserver self.homeserver.read().await.clone()
} }
/// Get the user id of the current owner of the client. /// Get the user id of the current owner of the client.
@ -866,8 +948,8 @@ impl Client {
/// successful SSO login. /// successful SSO login.
/// ///
/// [`login_with_token`]: #method.login_with_token /// [`login_with_token`]: #method.login_with_token
pub fn get_sso_login_url(&self, redirect_url: &str) -> Result<String> { pub async fn get_sso_login_url(&self, redirect_url: &str) -> Result<String> {
let homeserver = self.homeserver(); let homeserver = self.homeserver().await;
let request = sso_login::Request::new(redirect_url) let request = sso_login::Request::new(redirect_url)
.try_into_http_request::<Vec<u8>>(homeserver.as_str(), SendAccessToken::None); .try_into_http_request::<Vec<u8>>(homeserver.as_str(), SendAccessToken::None);
match request { match request {
@ -928,7 +1010,7 @@ impl Client {
device_id: Option<&str>, device_id: Option<&str>,
initial_device_display_name: Option<&str>, initial_device_display_name: Option<&str>,
) -> Result<login::Response> { ) -> Result<login::Response> {
info!("Logging in to {} as {:?}", self.homeserver, user); info!("Logging in to {} as {:?}", self.homeserver().await, user);
let request = assign!( let request = assign!(
login::Request::new( login::Request::new(
@ -1037,7 +1119,7 @@ impl Client {
where where
C: Future<Output = Result<()>>, C: Future<Output = Result<()>>,
{ {
info!("Logging in to {}", self.homeserver); info!("Logging in to {}", self.homeserver().await);
let (signal_tx, signal_rx) = oneshot::channel(); let (signal_tx, signal_rx) = oneshot::channel();
let (data_tx, data_rx) = oneshot::channel(); let (data_tx, data_rx) = oneshot::channel();
let data_tx_mutex = Arc::new(std::sync::Mutex::new(Some(data_tx))); let data_tx_mutex = Arc::new(std::sync::Mutex::new(Some(data_tx)));
@ -1109,7 +1191,7 @@ impl Client {
tokio::spawn(server); tokio::spawn(server);
let sso_url = self.get_sso_login_url(redirect_url.as_str()).unwrap(); let sso_url = self.get_sso_login_url(redirect_url.as_str()).await.unwrap();
match use_sso_login_url(sso_url).await { match use_sso_login_url(sso_url).await {
Ok(t) => t, Ok(t) => t,
@ -1193,7 +1275,7 @@ impl Client {
device_id: Option<&str>, device_id: Option<&str>,
initial_device_display_name: Option<&str>, initial_device_display_name: Option<&str>,
) -> Result<login::Response> { ) -> Result<login::Response> {
info!("Logging in to {}", self.homeserver); info!("Logging in to {}", self.homeserver().await);
let request = assign!( let request = assign!(
login::Request::new( login::Request::new(
@ -1264,7 +1346,7 @@ impl Client {
&self, &self,
registration: impl Into<register::Request<'_>>, registration: impl Into<register::Request<'_>>,
) -> Result<register::Response> { ) -> Result<register::Response> {
info!("Registering to {}", self.homeserver); info!("Registering to {}", self.homeserver().await);
let request = registration.into(); let request = registration.into();
self.send(request, None).await self.send(request, None).await
@ -2387,7 +2469,13 @@ impl Client {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{collections::BTreeMap, convert::TryInto, io::Cursor, str::FromStr, time::Duration}; use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
io::Cursor,
str::FromStr,
time::Duration,
};
use matrix_sdk_base::identifiers::mxc_uri; use matrix_sdk_base::identifiers::mxc_uri;
use matrix_sdk_common::{ use matrix_sdk_common::{
@ -2399,7 +2487,7 @@ mod test {
assign, assign,
directory::Filter, directory::Filter,
events::{room::message::MessageEventContent, AnyMessageEventContent}, events::{room::message::MessageEventContent, AnyMessageEventContent},
identifiers::{event_id, room_id, user_id}, identifiers::{event_id, room_id, user_id, UserId},
thirdparty, thirdparty,
}; };
use matrix_sdk_test::{test_json, EventBuilder, EventsJson}; use matrix_sdk_test::{test_json, EventBuilder, EventsJson};
@ -2425,6 +2513,59 @@ mod test {
client client
} }
#[tokio::test]
async fn set_homeserver() {
let homeserver = Url::from_str("http://example.com/").unwrap();
let mut client = Client::new(homeserver).unwrap();
let homeserver = Url::from_str(&mockito::server_url()).unwrap();
client.set_homeserver(homeserver.clone()).await;
assert_eq!(client.homeserver().await, homeserver);
}
#[tokio::test]
async fn successful_discovery() {
let server_url = mockito::server_url();
let domain = server_url.strip_prefix("http://").unwrap();
let alice = UserId::try_from("@alice:".to_string() + domain).unwrap();
let _m_well_known = mock("GET", "/.well-known/matrix/client")
.with_status(200)
.with_body(
test_json::WELL_KNOWN.to_string().replace("HOMESERVER_URL", server_url.as_ref()),
)
.create();
let _m_versions = mock("GET", "/_matrix/client/versions")
.with_status(200)
.with_body(test_json::VERSIONS.to_string())
.create();
let client = Client::new_from_user_id(alice).await.unwrap();
assert_eq!(client.homeserver().await, Url::parse(server_url.as_ref()).unwrap());
}
#[tokio::test]
async fn discovery_broken_server() {
let server_url = mockito::server_url();
let domain = server_url.strip_prefix("http://").unwrap();
let alice = UserId::try_from("@alice:".to_string() + domain).unwrap();
let _m = mock("GET", "/.well-known/matrix/client")
.with_status(200)
.with_body(
test_json::WELL_KNOWN.to_string().replace("HOMESERVER_URL", server_url.as_ref()),
)
.create();
if Client::new_from_user_id(alice).await.is_ok() {
panic!("Creating a client from a user ID should fail when the .well-known server returns no version infromation.");
}
}
#[tokio::test] #[tokio::test]
async fn login() { async fn login() {
let homeserver = Url::from_str(&mockito::server_url()).unwrap(); let homeserver = Url::from_str(&mockito::server_url()).unwrap();
@ -2514,7 +2655,7 @@ mod test {
.any(|flow| matches!(flow, LoginType::Sso(_))); .any(|flow| matches!(flow, LoginType::Sso(_)));
assert!(can_sso); assert!(can_sso);
let sso_url = client.get_sso_login_url("http://127.0.0.1:3030"); let sso_url = client.get_sso_login_url("http://127.0.0.1:3030").await;
assert!(sso_url.is_ok()); assert!(sso_url.is_ok());
let _m = mock("POST", "/_matrix/client/r0/login") let _m = mock("POST", "/_matrix/client/r0/login")
@ -2626,7 +2767,7 @@ mod test {
client.base_client.receive_sync_response(response).await.unwrap(); client.base_client.receive_sync_response(response).await.unwrap();
let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
assert_eq!(client.homeserver(), &Url::parse(&mockito::server_url()).unwrap()); assert_eq!(client.homeserver().await, Url::parse(&mockito::server_url()).unwrap());
let room = client.get_joined_room(&room_id); let room = client.get_joined_room(&room_id);
assert!(room.is_some()); assert!(room.is_some());

View File

@ -31,6 +31,7 @@ use matrix_sdk_common::{
use reqwest::Error as ReqwestError; use reqwest::Error as ReqwestError;
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
use thiserror::Error; use thiserror::Error;
use url::ParseError as UrlParseError;
/// Result type of the rust-sdk. /// Result type of the rust-sdk.
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -128,6 +129,10 @@ pub enum Error {
/// An error encountered when trying to parse an identifier. /// An error encountered when trying to parse an identifier.
#[error(transparent)] #[error(transparent)]
Identifier(#[from] IdentifierError), Identifier(#[from] IdentifierError),
/// An error encountered when trying to parse a url.
#[error(transparent)]
Url(#[from] UrlParseError),
} }
impl Error { impl Error {

View File

@ -97,7 +97,7 @@ pub trait HttpSend: AsyncTraitDeps {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct HttpClient { pub(crate) struct HttpClient {
pub(crate) inner: Arc<dyn HttpSend>, pub(crate) inner: Arc<dyn HttpSend>,
pub(crate) homeserver: Arc<Url>, pub(crate) homeserver: Arc<RwLock<Url>>,
pub(crate) session: Arc<RwLock<Option<Session>>>, pub(crate) session: Arc<RwLock<Option<Session>>>,
pub(crate) request_config: RequestConfig, pub(crate) request_config: RequestConfig,
} }
@ -106,6 +106,15 @@ pub(crate) struct HttpClient {
use crate::OutgoingRequestAppserviceExt; use crate::OutgoingRequestAppserviceExt;
impl HttpClient { impl HttpClient {
pub(crate) fn new(
inner: Arc<dyn HttpSend>,
homeserver: Arc<RwLock<Url>>,
session: Arc<RwLock<Option<Session>>>,
request_config: RequestConfig,
) -> Self {
HttpClient { inner, homeserver, session, request_config }
}
async fn send_request<Request: OutgoingRequest>( async fn send_request<Request: OutgoingRequest>(
&self, &self,
request: Request, request: Request,
@ -161,7 +170,10 @@ impl HttpClient {
}; };
let http_request = request let http_request = request
.try_into_http_request::<BytesMut>(&self.homeserver.to_string(), access_token)? .try_into_http_request::<BytesMut>(
&self.homeserver.read().await.to_string(),
access_token,
)?
.map(|body| body.freeze()); .map(|body| body.freeze());
Ok(http_request) Ok(http_request)
@ -189,7 +201,7 @@ impl HttpClient {
let http_request = request let http_request = request
.try_into_http_request_with_user_id::<BytesMut>( .try_into_http_request_with_user_id::<BytesMut>(
&self.homeserver.to_string(), &self.homeserver.read().await.to_string(),
access_token, access_token,
user_id, user_id,
)? )?

View File

@ -42,3 +42,29 @@ lazy_static! {
] ]
}); });
} }
lazy_static! {
pub static ref WELL_KNOWN: JsonValue = json!({
"m.homeserver": {
"base_url": "HOMESERVER_URL"
}
});
}
lazy_static! {
pub static ref VERSIONS: JsonValue = json!({
"versions": [
"r0.0.1",
"r0.1.0",
"r0.2.0",
"r0.3.0",
"r0.4.0",
"r0.5.0",
"r0.6.0"
],
"unstable_features": {
"org.matrix.label_based_filtering":true,
"org.matrix.e2e_cross_signing":true
}
});
}