Merge branch 'feat/appservice-client-config'

master
Damir Jelić 2021-05-31 13:28:31 +02:00
commit ee40d917d1
11 changed files with 129 additions and 69 deletions

View File

@ -26,7 +26,7 @@ rustls-tls = ["reqwest/rustls-tls"]
socks = ["reqwest/socks"] socks = ["reqwest/socks"]
sso_login = ["warp", "rand", "tokio-stream"] sso_login = ["warp", "rand", "tokio-stream"]
require_auth_for_profile_requests = [] require_auth_for_profile_requests = []
appservice = ["matrix-sdk-common/appservice", "serde_yaml"] appservice = ["matrix-sdk-common/appservice"]
docs = ["encryption", "sled_cryptostore", "sled_state_store", "sso_login"] docs = ["encryption", "sled_cryptostore", "sled_state_store", "sso_login"]
@ -41,7 +41,6 @@ url = "2.2.0"
zeroize = "1.2.0" zeroize = "1.2.0"
mime = "0.3.16" mime = "0.3.16"
rand = { version = "0.8.2", optional = true } rand = { version = "0.8.2", optional = true }
serde_yaml = { version = "0.8", optional = true }
bytes = "1.0.1" bytes = "1.0.1"
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" } matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }

View File

@ -304,6 +304,11 @@ impl ClientConfig {
self self
} }
/// Get the [`RequestConfig`]
pub fn get_request_config(&self) -> &RequestConfig {
&self.request_config
}
/// Specify a client to handle sending requests and receiving responses. /// Specify a client to handle sending requests and receiving responses.
/// ///
/// Any type that implements the `HttpSend` trait can be used to /// Any type that implements the `HttpSend` trait can be used to

View File

@ -133,7 +133,7 @@ impl HttpClient {
let request = if !self.request_config.assert_identity { let request = if !self.request_config.assert_identity {
self.try_into_http_request(request, session, config).await? self.try_into_http_request(request, session, config).await?
} else { } else {
self.try_into_http_request_with_identy_assertion(request, session, config).await? self.try_into_http_request_with_identity_assertion(request, session, config).await?
}; };
self.inner.send_request(request, config).await self.inner.send_request(request, config).await
@ -180,7 +180,7 @@ impl HttpClient {
} }
#[cfg(feature = "appservice")] #[cfg(feature = "appservice")]
async fn try_into_http_request_with_identy_assertion<Request: OutgoingRequest>( async fn try_into_http_request_with_identity_assertion<Request: OutgoingRequest>(
&self, &self,
request: Request, request: Request,
session: Arc<RwLock<Option<Session>>>, session: Arc<RwLock<Option<Session>>>,

View File

@ -52,8 +52,7 @@
//! synapse configuration `require_auth_for_profile_requests`. Enabled by //! synapse configuration `require_auth_for_profile_requests`. Enabled by
//! default. //! default.
//! * `appservice`: Enables low-level appservice functionality. For an //! * `appservice`: Enables low-level appservice functionality. For an
//! high-level API there's the //! high-level API there's the `matrix-sdk-appservice` crate
//! `matrix-sdk-appservice` crate
#![deny( #![deny(
missing_debug_implementations, missing_debug_implementations,

View File

@ -16,6 +16,7 @@ docs = []
[dependencies] [dependencies]
actix-rt = { version = "2", optional = true } actix-rt = { version = "2", optional = true }
actix-web = { version = "4.0.0-beta.6", optional = true } actix-web = { version = "4.0.0-beta.6", optional = true }
dashmap = "4"
futures = "0.3" futures = "0.3"
futures-util = "0.3" futures-util = "0.3"
http = "0.2" http = "0.2"

View File

@ -34,9 +34,10 @@ impl EventHandler for AppserviceEventHandler {
if let MembershipState::Invite = event.content.membership { if let MembershipState::Invite = event.content.membership {
let user_id = UserId::try_from(event.state_key.clone()).unwrap(); let user_id = UserId::try_from(event.state_key.clone()).unwrap();
self.appservice.register(user_id.localpart()).await.unwrap(); let mut appservice = self.appservice.clone();
appservice.register(user_id.localpart()).await.unwrap();
let client = self.appservice.client(Some(user_id.localpart())).await.unwrap(); let client = appservice.client(Some(user_id.localpart())).await.unwrap();
client.join_room_by_id(room.room_id()).await.unwrap(); client.join_room_by_id(room.room_id()).await.unwrap();
} }
@ -53,7 +54,7 @@ pub async fn main() -> std::io::Result<()> {
let registration = let registration =
AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml").unwrap(); AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml").unwrap();
let appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap(); let mut appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap();
let event_handler = AppserviceEventHandler::new(appservice.clone()); let event_handler = AppserviceEventHandler::new(appservice.clone());

View File

@ -65,7 +65,7 @@ async fn push_transactions(
return Ok(HttpResponse::Unauthorized().finish()); return Ok(HttpResponse::Unauthorized().finish());
} }
appservice.client(None).await?.receive_transaction(request.incoming).await?; appservice.get_cached_client(None)?.receive_transaction(request.incoming).await?;
Ok(HttpResponse::Ok().json("{}")) Ok(HttpResponse::Ok().json("{}"))
} }

View File

@ -16,9 +16,6 @@ use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("tried to run without webserver configured")]
RunWithoutServer,
#[error("missing access token")] #[error("missing access token")]
MissingAccessToken, MissingAccessToken,
@ -31,6 +28,9 @@ pub enum Error {
#[error("no port found")] #[error("no port found")]
MissingRegistrationPort, MissingRegistrationPort,
#[error("no client for localpart found")]
NoClientForLocalpart,
#[error(transparent)] #[error(transparent)]
HttpRequest(#[from] matrix_sdk::FromHttpRequestError), HttpRequest(#[from] matrix_sdk::FromHttpRequestError),

View File

@ -14,8 +14,9 @@
//! Matrix [Application Service] library //! Matrix [Application Service] library
//! //!
//! The appservice crate aims to provide a batteries-included experience. That //! The appservice crate aims to provide a batteries-included experience by
//! means that we //! being a thin wrapper around the [`matrix_sdk`]. That means that we
//!
//! * ship with functionality to configure your webserver crate or simply run //! * ship with functionality to configure your webserver crate or simply run
//! the webserver for you //! the webserver for you
//! * receive and validate requests from the homeserver correctly //! * receive and validate requests from the homeserver correctly
@ -57,7 +58,7 @@
//! regex: '@_appservice_.*' //! regex: '@_appservice_.*'
//! ")?; //! ")?;
//! //!
//! let appservice = Appservice::new(homeserver_url, server_name, registration).await?; //! let mut appservice = Appservice::new(homeserver_url, server_name, registration).await?;
//! appservice.set_event_handler(Box::new(AppserviceEventHandler)).await?; //! appservice.set_event_handler(Box::new(AppserviceEventHandler)).await?;
//! //!
//! let (host, port) = appservice.registration().get_host_and_port()?; //! let (host, port) = appservice.registration().get_host_and_port()?;
@ -81,8 +82,10 @@ use std::{
fs::File, fs::File,
ops::Deref, ops::Deref,
path::PathBuf, path::PathBuf,
sync::Arc,
}; };
use dashmap::DashMap;
use http::Uri; use http::Uri;
#[doc(inline)] #[doc(inline)]
pub use matrix_sdk::api_appservice as api; pub use matrix_sdk::api_appservice as api;
@ -98,8 +101,7 @@ use matrix_sdk::{
assign, assign,
identifiers::{self, DeviceId, ServerNameBox, UserId}, identifiers::{self, DeviceId, ServerNameBox, UserId},
reqwest::Url, reqwest::Url,
Client, ClientConfig, EventHandler, FromHttpResponseError, HttpError, RequestConfig, Client, ClientConfig, EventHandler, FromHttpResponseError, HttpError, ServerError, Session,
ServerError, Session,
}; };
use regex::Regex; use regex::Regex;
use tracing::warn; use tracing::warn;
@ -173,34 +175,31 @@ impl Deref for AppserviceRegistration {
} }
} }
async fn client_session_with_login_restore( type Localpart = String;
client: &Client,
registration: &AppserviceRegistration,
localpart: impl AsRef<str> + Into<Box<str>>,
server_name: &ServerNameBox,
) -> Result<()> {
let session = Session {
access_token: registration.as_token.clone(),
user_id: UserId::parse_with_server_name(localpart, server_name)?,
device_id: DeviceId::new(),
};
client.restore_login(session).await?;
Ok(()) /// The main appservice user is the `sender_localpart` from the given
} /// [`AppserviceRegistration`]
///
/// Dummy type for shared documentation
#[allow(dead_code)]
pub type MainAppserviceUser = ();
/// Appservice /// Appservice
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Appservice { pub struct Appservice {
homeserver_url: Url, homeserver_url: Url,
server_name: ServerNameBox, server_name: ServerNameBox,
registration: AppserviceRegistration, registration: Arc<AppserviceRegistration>,
client_sender_localpart: Client, clients: Arc<DashMap<Localpart, Client>>,
} }
impl Appservice { impl Appservice {
/// Create new Appservice /// Create new Appservice
/// ///
/// Also creates and caches a [`Client`] with the [`MainAppserviceUser`].
/// The default [`ClientConfig`] is used, if you want to customize it
/// use [`Self::new_with_client_config()`] instead.
///
/// # Arguments /// # Arguments
/// ///
/// * `homeserver_url` - The homeserver that the client should connect to. /// * `homeserver_url` - The homeserver that the client should connect to.
@ -215,28 +214,46 @@ impl Appservice {
server_name: impl TryInto<ServerNameBox, Error = identifiers::Error>, server_name: impl TryInto<ServerNameBox, Error = identifiers::Error>,
registration: AppserviceRegistration, registration: AppserviceRegistration,
) -> Result<Self> { ) -> Result<Self> {
let homeserver_url = homeserver_url.try_into()?; let appservice = Self::new_with_client_config(
let server_name = server_name.try_into()?; homeserver_url,
server_name,
let client_sender_localpart = Client::new(homeserver_url.clone())?; registration,
ClientConfig::default(),
client_session_with_login_restore(
&client_sender_localpart,
&registration,
registration.sender_localpart.as_ref(),
&server_name,
) )
.await?; .await?;
Ok(Appservice { homeserver_url, server_name, registration, client_sender_localpart }) Ok(appservice)
} }
/// Get a [`Client`] /// Same as [`Self::new()`] but lets you provide a [`ClientConfig`] for the
/// [`Client`]
pub async fn new_with_client_config(
homeserver_url: impl TryInto<Url, Error = url::ParseError>,
server_name: impl TryInto<ServerNameBox, Error = identifiers::Error>,
registration: AppserviceRegistration,
client_config: ClientConfig,
) -> Result<Self> {
let homeserver_url = homeserver_url.try_into()?;
let server_name = server_name.try_into()?;
let registration = Arc::new(registration);
let clients = Arc::new(DashMap::new());
let appservice = Appservice { homeserver_url, server_name, registration, clients };
// we cache the [`MainAppserviceUser`] by default
appservice.client_with_config(None, client_config).await?;
Ok(appservice)
}
/// Create a [`Client`]
/// ///
/// Will return a `Client` that's configured to [assert the identity] on all /// Will create and return a [`Client`] that's configured to [assert the
/// outgoing homeserver requests if `localpart` is given. If not given /// identity] on all outgoing homeserver requests if `localpart` is
/// the `Client` will use the main user associated with this appservice, /// given. If not given the [`Client`] will use the [`MainAppserviceUser`].
/// that is the `sender_localpart` in the [`AppserviceRegistration`] ///
/// This method is a singleton that saves the client internally for re-use
/// based on the `localpart`.
/// ///
/// # Arguments /// # Arguments
/// ///
@ -245,25 +262,46 @@ impl Appservice {
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration /// [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 /// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion
pub async fn client(&self, localpart: Option<&str>) -> Result<Client> { pub async fn client(&self, localpart: Option<&str>) -> Result<Client> {
let client = self.client_with_config(localpart, ClientConfig::default()).await?;
Ok(client)
}
/// Same as [`Self::client`] but with the ability to pass in a
/// [`ClientConfig`]
///
/// Since this method is a singleton follow-up calls with different
/// [`ClientConfig`]s will be ignored.
pub async fn client_with_config(
&self,
localpart: Option<&str>,
config: ClientConfig,
) -> Result<Client> {
let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref()); let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref());
// The `as_token` in the `Session` maps to the main appservice user let client = if let Some(client) = self.clients.get(localpart) {
client.clone()
} else {
let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?;
// The `as_token` in the `Session` maps to the [`MainAppserviceUser`]
// (`sender_localpart`) by default, so we don't need to assert identity // (`sender_localpart`) by default, so we don't need to assert identity
// in that case // in that case
let client = if localpart == self.registration.sender_localpart { if localpart != self.registration.sender_localpart {
self.client_sender_localpart.clone() config.get_request_config().assert_identity();
} else { }
let request_config = RequestConfig::default().assert_identity();
let config = ClientConfig::default().request_config(request_config);
let client = Client::new_with_config(self.homeserver_url.clone(), config)?; let client = Client::new_with_config(self.homeserver_url.clone(), config)?;
client_session_with_login_restore( let session = Session {
&client, access_token: self.registration.as_token.clone(),
&self.registration, user_id: user_id.clone(),
localpart, // TODO: expose & proper E2EE
&self.server_name, device_id: DeviceId::new(),
) };
.await?;
client.restore_login(session).await?;
self.clients.insert(localpart.to_owned(), client.clone());
client client
}; };
@ -271,9 +309,26 @@ impl Appservice {
Ok(client) Ok(client)
} }
/// Get cached [`Client`]
///
/// Will return the client for the given `localpart` if previously
/// constructed with [`Self::client()`] or [`Self::client_with_config()`].
/// If no client for the `localpart` is found it will return an Error.
pub fn get_cached_client(&self, localpart: Option<&str>) -> Result<Client> {
let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref());
let entry = self.clients.get(localpart).ok_or(Error::NoClientForLocalpart)?;
Ok(entry.value().clone())
}
/// Convenience wrapper around [`Client::set_event_handler()`] /// Convenience wrapper around [`Client::set_event_handler()`]
pub async fn set_event_handler(&self, handler: Box<dyn EventHandler>) -> Result<()> { ///
/// Attaches the event handler to [`Self::client()`] with `None` as
/// `localpart`
pub async fn set_event_handler(&mut self, handler: Box<dyn EventHandler>) -> Result<()> {
let client = self.client(None).await?; let client = self.client(None).await?;
client.set_event_handler(handler).await; client.set_event_handler(handler).await;
Ok(()) Ok(())
@ -286,7 +341,7 @@ impl Appservice {
/// ///
/// * `localpart` - The localpart of the user to register. Must be covered /// * `localpart` - The localpart of the user to register. Must be covered
/// by the namespaces in the [`Registration`] in order to succeed. /// by the namespaces in the [`Registration`] in order to succeed.
pub async fn register(&self, localpart: impl AsRef<str>) -> Result<()> { pub async fn register(&mut self, localpart: impl AsRef<str>) -> Result<()> {
let request = assign!(RegistrationRequest::new(), { let request = assign!(RegistrationRequest::new(), {
username: Some(localpart.as_ref()), username: Some(localpart.as_ref()),
login_type: Some(&LoginType::ApplicationService), login_type: Some(&LoginType::ApplicationService),

View File

@ -12,7 +12,7 @@ mod actix {
Appservice::new( Appservice::new(
mockito::server_url().as_ref(), mockito::server_url().as_ref(),
"test.local", "test.local",
AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml").unwrap(), AppserviceRegistration::try_from_yaml_str(include_str!("./registration.yaml")).unwrap(),
) )
.await .await
.unwrap() .unwrap()

View File

@ -59,7 +59,7 @@ fn member_json() -> serde_json::Value {
#[async_test] #[async_test]
async fn test_event_handler() -> Result<()> { async fn test_event_handler() -> Result<()> {
let appservice = appservice(None).await?; let mut appservice = appservice(None).await?;
struct Example {} struct Example {}
@ -94,7 +94,7 @@ async fn test_event_handler() -> Result<()> {
#[async_test] #[async_test]
async fn test_transaction() -> Result<()> { async fn test_transaction() -> Result<()> {
let appservice = appservice(None).await?; let mut appservice = appservice(None).await?;
let event = serde_json::from_value::<AnyStateEvent>(member_json()).unwrap(); let event = serde_json::from_value::<AnyStateEvent>(member_json()).unwrap();
let event: Raw<AnyRoomEvent> = AnyRoomEvent::State(event).into(); let event: Raw<AnyRoomEvent> = AnyRoomEvent::State(event).into();