From c562d91533ca7898dacd1cca64171fd6d23c8a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charlotte=20=F0=9F=A6=9D=20Delenk?= Date: Tue, 31 May 2022 09:48:12 +0100 Subject: [PATCH] feat(appservice): Use appservice logins for e2ee This commit also adds a virtual user builder, which allows you to control the device id, change the client builder, perform an appservice login, and even restore a previously created session. https://spec.matrix.org/v1.2/client-server-api/#appservice-login --- crates/matrix-sdk-appservice/src/lib.rs | 183 ++++++++++++++++++------ 1 file changed, 139 insertions(+), 44 deletions(-) diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index cadc08157..f3e730608 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -90,6 +90,7 @@ pub use matrix_sdk; pub use matrix_sdk::ruma; use matrix_sdk::{ bytes::Bytes, + config::RequestConfig, event_handler::{EventHandler, EventHandlerResult, SyncEvent}, reqwest::Url, Client, ClientBuildError, ClientBuilder, Session, @@ -101,9 +102,9 @@ use ruma::{ query::{query_room_alias::v1 as query_room, query_user_id::v1 as query_user}, Registration, }, - client::account::register, + client::{account::register, session::login, uiaa::UserIdentifier}, }, - assign, DeviceId, IdParseError, OwnedServerName, UserId, + assign, DeviceId, IdParseError, OwnedDeviceId, OwnedServerName, UserId, }; use serde::de::DeserializeOwned; use tracing::info; @@ -118,6 +119,111 @@ pub type Port = u16; const USER_KEY: &[u8] = b"appservice.users."; +/// Builder for a virtual user +#[derive(Debug)] +pub struct VirtualUserBuilder<'a> { + appservice: &'a AppService, + localpart: &'a str, + device_id: Option, + client_builder: ClientBuilder, + log_in: bool, + restored_session: Option, +} + +impl<'a> VirtualUserBuilder<'a> { + /// Create a new virtual user builder + /// # Arguments + /// + /// * `localpart` - The localpart of the virtual user + pub fn new(appservice: &'a AppService, localpart: &'a str) -> Self { + Self { + appservice, + localpart, + device_id: None, + client_builder: Client::builder(), + log_in: false, + restored_session: None, + } + } + + /// Set the device id of the virtual user + pub fn device_id(mut self, device_id: Option) -> Self { + self.device_id = device_id; + self + } + + /// Sets the client builder to use for the virtual user + pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self { + self.client_builder = client_builder; + self + } + + /// Log in as the virtual user + /// + /// In some cases it is necessary to log in as the virtual user, such as to + /// upload device keys + pub fn login(mut self) -> Self { + self.log_in = true; + self + } + + /// Restore a persisted session + /// + /// This is primarily useful if you enable + /// [`VirtualUserBuilder::login()`] and want to restore a session + /// from a previous run. + pub fn restored_session(mut self, session: Session) -> Self { + self.restored_session = Some(session); + self + } + + /// Build the virtual user + /// + /// # Errors + /// This function returns an error if an invalid localpart is provided. + pub async fn build(self) -> Result { + if let Some(client) = self.appservice.clients.get(self.localpart) { + return Ok(client.clone()); + } + + let user_id = UserId::parse_with_server_name(self.localpart, &self.appservice.server_name)?; + + let mut builder = self.client_builder; + + if !self.log_in && self.localpart != self.appservice.registration.sender_localpart { + builder = builder.assert_identity(); + } + + let client = builder + .homeserver_url(self.appservice.homeserver_url.clone()) + .appservice_mode() + .build() + .await + .map_err(ClientBuildError::assert_valid_builder_args)?; + + let session = if let Some(session) = self.restored_session { + session + } else if self.log_in && self.localpart != self.appservice.registration.sender_localpart { + self.appservice + .create_session(self.localpart, self.device_id.as_ref().map(|v| v.as_ref()), None) + .await? + } else { + // Don’t log in + Session { + access_token: self.appservice.registration.as_token.clone(), + user_id: user_id.clone(), + device_id: self.device_id.unwrap_or_else(DeviceId::new), + } + }; + + client.restore_login(session).await?; + + self.appservice.clients.insert(self.localpart.to_owned(), client.clone()); + + Ok(client) + } +} + /// AppService Registration /// /// Wrapper around [`Registration`] @@ -252,7 +358,7 @@ impl AppService { AppService { homeserver_url, server_name, registration, clients, event_handler }; // we create and cache the [`MainUser`] by default - appservice.create_and_cache_client(&sender_localpart, builder).await?; + appservice.virtual_user_builder(&sender_localpart).client_builder(builder).build().await?; Ok(appservice) } @@ -280,10 +386,7 @@ impl AppService { /// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration /// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion pub async fn virtual_user_client(&self, localpart: impl AsRef) -> Result { - let client = - self.virtual_user_client_with_client_builder(localpart, Client::builder()).await?; - - Ok(client) + self.virtual_user_builder(localpart.as_ref()).build().await } /// Same as [`virtual_user_client()`][Self::virtual_user_client] but with @@ -296,50 +399,42 @@ impl AppService { localpart: impl AsRef, builder: ClientBuilder, ) -> Result { - // TODO: check if localpart is covered by namespace? - let localpart = localpart.as_ref(); - - let client = if let Some(client) = self.clients.get(localpart) { - client.clone() - } else { - self.create_and_cache_client(localpart, builder).await? - }; - - Ok(client) + self.virtual_user_builder(localpart.as_ref()).client_builder(builder).build().await } - async fn create_and_cache_client( + pub fn virtual_user_builder<'a>(&'a self, localpart: &'a str) -> VirtualUserBuilder<'a> { + VirtualUserBuilder::new(self, localpart) + } + + /// Create a session using appservice login for a virtual user. + async fn create_session( &self, - localpart: &str, - mut builder: ClientBuilder, - ) -> Result { - let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?; + user: impl AsRef, + device_id: Option<&str>, + initial_device_display_name: Option<&str>, + ) -> Result { + let homeserver = self.homeserver_url.clone(); + info!(homeserver = homeserver.as_str(), user = user.as_ref(), "Logging in as virtual user"); - // The `as_token` in the `Session` maps to the [`MainUser`] - // (`sender_localpart`) by default, so we don't need to assert identity - // in that case - if localpart != self.registration.sender_localpart { - builder = builder.assert_identity(); - } + let login_info = login::v3::LoginInfo::ApplicationService( + login::v3::ApplicationService::new(UserIdentifier::UserIdOrLocalpart(user.as_ref())), + ); - let client = builder - .homeserver_url(self.homeserver_url.clone()) - .appservice_mode() - .build() - .await - .map_err(ClientBuildError::assert_valid_builder_args)?; + let request = assign!(login::v3::Request::new(login_info), { + device_id: device_id.map(|d| d.into()), + initial_device_display_name + }); - let session = Session { - access_token: self.registration.as_token.clone(), - user_id: user_id.clone(), - // TODO: expose & proper E2EE - device_id: DeviceId::new(), - }; + let response = self + .get_cached_client(None)? + .send(request, Some(RequestConfig::short_retry().force_auth())) + .await?; - client.restore_login(session).await?; - self.clients.insert(localpart.to_owned(), client.clone()); - - Ok(client) + Ok(Session { + access_token: response.access_token, + user_id: response.user_id, + device_id: response.device_id, + }) } /// Get cached [`Client`]