From eece9209534865d2871c240552ad82291dda6d68 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Thu, 29 Apr 2021 13:28:08 +0200 Subject: [PATCH 1/7] appservice: Initial version --- .github/workflows/ci.yml | 4 + Cargo.toml | 1 + matrix_sdk/Cargo.toml | 3 + matrix_sdk/src/client.rs | 38 +++ matrix_sdk/src/error.rs | 9 + matrix_sdk/src/http_client.rs | 34 +- matrix_sdk_appservice/Cargo.toml | 41 +++ .../examples/actix_autojoin.rs | 80 +++++ matrix_sdk_appservice/src/actix.rs | 182 ++++++++++ matrix_sdk_appservice/src/error.rs | 74 +++++ matrix_sdk_appservice/src/lib.rs | 314 ++++++++++++++++++ matrix_sdk_appservice/tests/actix.rs | 115 +++++++ matrix_sdk_appservice/tests/registration.yaml | 13 + matrix_sdk_appservice/tests/tests.rs | 157 +++++++++ matrix_sdk_common/Cargo.toml | 1 + matrix_sdk_common/src/lib.rs | 5 + 16 files changed, 1070 insertions(+), 1 deletion(-) create mode 100644 matrix_sdk_appservice/Cargo.toml create mode 100644 matrix_sdk_appservice/examples/actix_autojoin.rs create mode 100644 matrix_sdk_appservice/src/actix.rs create mode 100644 matrix_sdk_appservice/src/error.rs create mode 100644 matrix_sdk_appservice/src/lib.rs create mode 100644 matrix_sdk_appservice/tests/actix.rs create mode 100644 matrix_sdk_appservice/tests/registration.yaml create mode 100644 matrix_sdk_appservice/tests/tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 630cf331..f8236d07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,6 +103,7 @@ jobs: - linux / features-socks - linux / features-sso_login - linux / features-require_auth_for_profile_requests + - linux / features-appservice include: - name: linux / features-no-encryption @@ -132,6 +133,9 @@ jobs: - name: linux / features-sso_login cargo_args: --features sso_login + - name: linux / features-appservice + cargo_args: --no-default-features --features "appservice, native-tls" + steps: - name: Checkout uses: actions/checkout@v1 diff --git a/Cargo.toml b/Cargo.toml index 57e18193..050a6c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "matrix_sdk_test_macros", "matrix_sdk_crypto", "matrix_sdk_common", + "matrix_sdk_appservice" ] diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index 0ab3b50f..abee49cc 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -26,6 +26,7 @@ rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] sso_login = ["warp", "rand", "tokio-stream"] require_auth_for_profile_requests = [] +appservice = ["matrix-sdk-common/appservice", "serde_yaml"] docs = ["encryption", "sled_cryptostore", "sled_state_store", "sso_login"] @@ -40,6 +41,7 @@ url = "2.2.0" zeroize = "1.2.0" mime = "0.3.16" rand = { version = "0.8.2", optional = true } +serde_yaml = { version = "0.8", optional = true } bytes = "1.0.1" matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" } @@ -93,6 +95,7 @@ tracing-subscriber = "0.2.15" tempfile = "3.2.0" mockito = "0.29.0" lazy_static = "1.4.0" +matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" } [[example]] name = "emoji_verification" diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index a6d9b651..a5154fe2 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -405,6 +405,7 @@ pub struct RequestConfig { pub(crate) retry_limit: Option, pub(crate) retry_timeout: Option, pub(crate) force_auth: bool, + pub(crate) assert_identity: bool, } #[cfg(not(tarpaulin_include))] @@ -426,6 +427,7 @@ impl Default for RequestConfig { retry_limit: Default::default(), retry_timeout: Default::default(), force_auth: false, + assert_identity: false, } } } @@ -464,10 +466,21 @@ impl RequestConfig { /// Force sending authorization even if the endpoint does not require it. Default is only /// sending authorization if it is required #[cfg(feature = "require_auth_for_profile_requests")] + #[cfg_attr(feature = "docs", doc(cfg(require_auth_for_profile_requests)))] pub(crate) fn force_auth(mut self) -> Self { self.force_auth = true; self } + + /// [Assert the identity][identity-assertion] of requests to the `user_id` in the `Session` + /// + /// [identity-assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion + #[cfg(feature = "appservice")] + #[cfg_attr(feature = "docs", doc(cfg(appservice)))] + pub fn assert_identity(mut self) -> Self { + self.assert_identity = true; + self + } } impl Client { @@ -521,6 +534,31 @@ impl Client { }) } + /// Process a [transaction] received from the homeserver + /// + /// # Arguments + /// + /// * `incoming_transaction` - The incoming transaction received from the homeserver. + /// + /// [transaction]: https://matrix.org/docs/spec/application_service/r0.1.2#put-matrix-app-v1-transactions-txnid + #[cfg(feature = "appservice")] + #[cfg_attr(feature = "docs", doc(cfg(appservice)))] + pub async fn receive_transaction( + &self, + incoming_transaction: crate::api_appservice::event::push_events::v1::IncomingRequest, + ) -> Result<()> { + let txn_id = incoming_transaction.txn_id.clone(); + let response = incoming_transaction.try_into_sync_response(txn_id)?; + let base_client = self.base_client.clone(); + let sync_response = base_client.receive_sync_response(response).await?; + + if let Some(handler) = self.event_handler.read().await.as_ref() { + handler.handle_sync(&sync_response).await; + } + + Ok(()) + } + /// Is the client logged in. pub async fn logged_in(&self) -> bool { self.base_client.logged_in().await diff --git a/matrix_sdk/src/error.rs b/matrix_sdk/src/error.rs index 3fd66c24..fc284640 100644 --- a/matrix_sdk/src/error.rs +++ b/matrix_sdk/src/error.rs @@ -78,6 +78,15 @@ pub enum HttpError { /// The given request can't be cloned and thus can't be retried. #[error("The request cannot be cloned")] UnableToCloneRequest, + + /// Tried to send a request without `user_id` in the `Session` + #[error("missing user_id in session")] + #[cfg(feature = "appservice")] + UserIdRequired, + + /// Tried to assert identity without appservice feature enabled + #[error("appservice feature not enabled")] + NeedsAppserviceFeature, } /// Internal representation of errors. diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index 88ccb7aa..63c3142a 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -100,6 +100,9 @@ pub(crate) struct HttpClient { pub(crate) request_config: RequestConfig, } +#[cfg(feature = "appservice")] +use crate::OutgoingRequestAppserviceExt; + impl HttpClient { async fn send_request( &self, @@ -112,7 +115,7 @@ impl HttpClient { None => self.request_config, }; - let request = { + let request = if !self.request_config.assert_identity { let read_guard; let access_token = if config.force_auth { read_guard = session.read().await; @@ -140,6 +143,35 @@ impl HttpClient { request .try_into_http_request::(&self.homeserver.to_string(), access_token)? .map(|body| body.freeze()) + } else { + // this should be unreachable since assert_identity on request_config can only be set + // with the appservice feature active + #[cfg(not(feature = "appservice"))] + return Err(HttpError::NeedsAppserviceFeature); + + #[cfg(feature = "appservice")] + { + let read_guard = session.read().await; + let access_token = if let Some(session) = read_guard.as_ref() { + SendAccessToken::Always(session.access_token.as_str()) + } else { + return Err(HttpError::AuthenticationRequired); + }; + + let user_id = if let Some(session) = read_guard.as_ref() { + session.user_id.clone() + } else { + return Err(HttpError::UserIdRequired); + }; + + request + .try_into_http_request_with_user_id::( + &self.homeserver.to_string(), + access_token, + user_id, + )? + .map(|body| body.freeze()) + } }; self.inner.send_request(request, config).await diff --git a/matrix_sdk_appservice/Cargo.toml b/matrix_sdk_appservice/Cargo.toml new file mode 100644 index 00000000..766fefac --- /dev/null +++ b/matrix_sdk_appservice/Cargo.toml @@ -0,0 +1,41 @@ +[package] +authors = ["Johannes Becker "] +edition = "2018" +homepage = "https://github.com/matrix-org/matrix-rust-sdk" +keywords = ["matrix", "chat", "messaging", "ruma", "nio", "appservice"] +license = "Apache-2.0" +name = "matrix-sdk-appservice" +version = "0.1.0" + +[features] +default = ["actix"] +actix = ["actix-rt", "actix-web"] + +docs = [] + +[dependencies] +actix-rt = { version = "2", optional = true } +actix-web = { version = "4.0.0-beta.6", optional = true } +futures = "0.3" +futures-util = "0.3" +http = "0.2" +regex = "1" +serde_yaml = "0.8" +thiserror = "1.0" +tracing = "0.1" +url = "2" + +matrix-sdk = { version = "0.2", path = "../matrix_sdk", default-features = false, features = ["appservice", "native-tls"] } + +[dev-dependencies] +env_logger = "0.8" +mockito = "0.30" +serde_json = "1" +tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] } +tracing-subscriber = "0.2" + +matrix-sdk-test = { version = "0.2", path = "../matrix_sdk_test" } + +[[example]] +name = "actix_autojoin" +required-features = ["actix"] diff --git a/matrix_sdk_appservice/examples/actix_autojoin.rs b/matrix_sdk_appservice/examples/actix_autojoin.rs new file mode 100644 index 00000000..1f380cca --- /dev/null +++ b/matrix_sdk_appservice/examples/actix_autojoin.rs @@ -0,0 +1,80 @@ +use std::{convert::TryFrom, env}; + +use actix_web::{App, HttpServer}; +use matrix_sdk::{ + async_trait, + events::{ + room::member::{MemberEventContent, MembershipState}, + SyncStateEvent, + }, + identifiers::UserId, + room::Room, + EventHandler, +}; +use matrix_sdk_appservice::{Appservice, AppserviceRegistration}; + +struct AppserviceEventHandler { + appservice: Appservice, +} + +impl AppserviceEventHandler { + pub fn new(appservice: Appservice) -> Self { + Self { appservice } + } +} + +#[async_trait] +impl EventHandler for AppserviceEventHandler { + async fn on_room_member(&self, room: Room, event: &SyncStateEvent) { + if !self + .appservice + .user_id_is_in_namespace(&event.state_key) + .unwrap() + { + dbg!("not an appservice user"); + return; + } + + if let MembershipState::Invite = event.content.membership { + let user_id = UserId::try_from(event.state_key.clone()).unwrap(); + + let client = self + .appservice + .client_with_localpart(user_id.localpart()) + .await + .unwrap(); + + client.join_room_by_id(room.room_id()).await.unwrap(); + } + } +} + +#[actix_web::main] +pub async fn main() -> std::io::Result<()> { + env::set_var( + "RUST_LOG", + "actix_web=debug,actix_server=info,matrix_sdk=debug", + ); + tracing_subscriber::fmt::init(); + + let homeserver_url = "http://localhost:8008"; + let server_name = "localhost"; + let registration = + AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml").unwrap(); + + let appservice = Appservice::new(homeserver_url, server_name, registration) + .await + .unwrap(); + + let event_handler = AppserviceEventHandler::new(appservice.clone()); + + appservice + .client() + .set_event_handler(Box::new(event_handler)) + .await; + + HttpServer::new(move || App::new().service(appservice.actix_service())) + .bind(("0.0.0.0", 8090))? + .run() + .await +} diff --git a/matrix_sdk_appservice/src/actix.rs b/matrix_sdk_appservice/src/actix.rs new file mode 100644 index 00000000..3e899822 --- /dev/null +++ b/matrix_sdk_appservice/src/actix.rs @@ -0,0 +1,182 @@ +// Copyright 2021 Famedly GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + convert::{TryFrom, TryInto}, + pin::Pin, +}; + +use actix_web::{ + dev::Payload, + error::PayloadError, + get, + http::PathAndQuery, + put, + web::{self, BytesMut, Data}, + App, FromRequest, HttpRequest, HttpResponse, HttpServer, +}; +use futures::Future; +use futures_util::{TryFutureExt, TryStreamExt}; +use matrix_sdk::api_appservice as api; + +pub use actix_web::Scope; + +use crate::{error::Error, Appservice}; + +pub async fn run_server( + appservice: Appservice, + host: impl AsRef, + port: impl Into, +) -> Result<(), Error> { + HttpServer::new(move || App::new().service(appservice.actix_service())) + .bind((host.as_ref(), port.into()))? + .run() + .await?; + + Ok(()) +} + +pub fn get_scope() -> Scope { + gen_scope("/"). // handle legacy routes + service(gen_scope("/_matrix/app/v1")) +} + +fn gen_scope(scope: &str) -> Scope { + web::scope(scope) + .service(push_transactions) + .service(query_user_id) + .service(query_room_alias) +} + +#[tracing::instrument] +#[put("/transactions/{txn_id}")] +async fn push_transactions( + request: IncomingRequest, + appservice: Data, +) -> Result { + if !appservice.verify_hs_token(request.access_token) { + return Ok(HttpResponse::Unauthorized().finish()); + } + + appservice + .client() + .receive_transaction(request.incoming) + .await + .unwrap(); + + Ok(HttpResponse::Ok().json("{}")) +} + +#[tracing::instrument] +#[get("/users/{user_id}")] +async fn query_user_id( + request: IncomingRequest, + appservice: Data, +) -> Result { + if !appservice.verify_hs_token(request.access_token) { + return Ok(HttpResponse::Unauthorized().finish()); + } + + Ok(HttpResponse::Ok().json("{}")) +} + +#[tracing::instrument] +#[get("/rooms/{room_alias}")] +async fn query_room_alias( + request: IncomingRequest, + appservice: Data, +) -> Result { + if !appservice.verify_hs_token(request.access_token) { + return Ok(HttpResponse::Unauthorized().finish()); + } + + Ok(HttpResponse::Ok().json("{}")) +} + +#[derive(Debug)] +pub struct IncomingRequest { + access_token: String, + incoming: T, +} + +impl FromRequest for IncomingRequest { + type Error = Error; + type Future = Pin>>>; + type Config = (); + + fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future { + let request = request.to_owned(); + let payload = payload.take(); + + Box::pin(async move { + let uri = request.uri().to_owned(); + + let uri = if !uri.path().starts_with("/_matrix/app/v1") { + // rename legacy routes + let mut parts = uri.into_parts(); + let path_and_query = match parts.path_and_query { + Some(path_and_query) => format!("/_matrix/app/v1{}", path_and_query), + None => "/_matrix/app/v1".to_owned(), + }; + parts.path_and_query = + Some(PathAndQuery::try_from(path_and_query).map_err(http::Error::from)?); + parts.try_into().map_err(http::Error::from)? + } else { + uri + }; + + let mut builder = http::request::Builder::new() + .method(request.method()) + .uri(uri); + + let headers = builder + .headers_mut() + .ok_or(Error::UnknownHttpRequestBuilder)?; + for (key, value) in request.headers().iter() { + headers.append(key, value.to_owned()); + } + + let bytes = payload + .try_fold(BytesMut::new(), |mut body, chunk| async move { + body.extend_from_slice(&chunk); + Ok::<_, PayloadError>(body) + }) + .and_then(|bytes| async move { Ok::, _>(bytes.into_iter().collect()) }) + .await?; + + let access_token = match request.uri().query() { + Some(query) => { + let query: Vec<(String, String)> = matrix_sdk::urlencoded::from_str(query)?; + query + .into_iter() + .find(|(key, _)| key == "access_token") + .map(|(_, value)| value) + } + None => None, + }; + + let access_token = match access_token { + Some(access_token) => access_token, + None => return Err(Error::MissingAccessToken), + }; + + let request = builder.body(bytes)?; + + Ok(IncomingRequest { + access_token, + incoming: matrix_sdk::IncomingRequest::try_from_http_request(request)?, + }) + }) + } +} diff --git a/matrix_sdk_appservice/src/error.rs b/matrix_sdk_appservice/src/error.rs new file mode 100644 index 00000000..fcbdf997 --- /dev/null +++ b/matrix_sdk_appservice/src/error.rs @@ -0,0 +1,74 @@ +// Copyright 2021 Famedly GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("tried to run without webserver configured")] + RunWithoutServer, + + #[error("missing access token")] + MissingAccessToken, + + #[error("missing host on registration url")] + MissingRegistrationHost, + + #[error("http request builder error")] + UnknownHttpRequestBuilder, + + #[error("no port found")] + MissingRegistrationPort, + + #[error(transparent)] + HttpRequest(#[from] matrix_sdk::FromHttpRequestError), + + #[error(transparent)] + Identifier(#[from] matrix_sdk::identifiers::Error), + + #[error(transparent)] + Http(#[from] http::Error), + + #[error(transparent)] + Url(#[from] url::ParseError), + + #[error(transparent)] + Serde(#[from] matrix_sdk::SerdeError), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + InvalidUri(#[from] http::uri::InvalidUri), + + #[error(transparent)] + Matrix(#[from] matrix_sdk::Error), + + #[error(transparent)] + Regex(#[from] regex::Error), + + #[error(transparent)] + SerdeYaml(#[from] serde_yaml::Error), + + #[cfg(feature = "actix")] + #[error(transparent)] + Actix(#[from] actix_web::Error), + + #[cfg(feature = "actix")] + #[error(transparent)] + ActixPayload(#[from] actix_web::error::PayloadError), +} + +#[cfg(feature = "actix")] +impl actix_web::error::ResponseError for Error {} diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs new file mode 100644 index 00000000..7eb6c33d --- /dev/null +++ b/matrix_sdk_appservice/src/lib.rs @@ -0,0 +1,314 @@ +// Copyright 2021 Famedly GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Matrix [Application Service] library +//! +//! # Quickstart +//! +//! ```no_run +//! # async { +//! use matrix_sdk_appservice::{Appservice, AppserviceRegistration}; +//! +//! let homeserver_url = "http://localhost:8008"; +//! let server_name = "localhost"; +//! let registration = AppserviceRegistration::try_from_yaml_str( +//! r" +//! id: appservice +//! url: http://localhost:9009 +//! as_token: as_token +//! hs_token: hs_token +//! sender_localpart: _appservice +//! namespaces: +//! users: +//! - exclusive: true +//! regex: '@_appservice_.*' +//! ") +//! .unwrap(); +//! +//! let appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap(); +//! let (host, port) = appservice.get_host_and_port_from_registration().unwrap(); +//! appservice.run(host, port).await.unwrap(); +//! # }; +//! ``` +//! +//! [Application Service]: https://matrix.org/docs/spec/application_service/r0.1.2 + +use std::{ + convert::{TryFrom, TryInto}, + fs::File, + ops::Deref, + path::PathBuf, +}; + +use http::Uri; +use matrix_sdk::{ + api::{ + error::ErrorKind, + r0::{ + account::register::{LoginType, Request as RegistrationRequest}, + uiaa::UiaaResponse, + }, + }, + api_appservice::Registration, + assign, + identifiers::{self, DeviceId, ServerNameBox, UserId}, + reqwest::Url, + Client, ClientConfig, FromHttpResponseError, HttpError, RequestConfig, ServerError, Session, +}; +use regex::Regex; +#[cfg(not(feature = "actix"))] +use tracing::error; +use tracing::warn; + +#[doc(inline)] +pub use matrix_sdk::api_appservice as api; + +#[cfg(feature = "actix")] +mod actix; +mod error; + +pub use error::Error; +pub type Result = std::result::Result; +pub type Host = String; +pub type Port = u16; + +/// Appservice Registration +#[derive(Debug, Clone)] +pub struct AppserviceRegistration { + inner: Registration, +} + +impl AppserviceRegistration { + /// Try to load registration from yaml string + pub fn try_from_yaml_str(value: impl AsRef) -> Result { + Ok(Self { + inner: serde_yaml::from_str(value.as_ref())?, + }) + } + + /// Try to load registration from yaml file + pub fn try_from_yaml_file(path: impl Into) -> Result { + let file = File::open(path.into())?; + + Ok(Self { + inner: serde_yaml::from_reader(file)?, + }) + } +} + +impl From for AppserviceRegistration { + fn from(value: Registration) -> Self { + Self { inner: value } + } +} + +impl Deref for AppserviceRegistration { + type Target = Registration; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +async fn create_client( + homeserver_url: &Url, + server_name: &ServerNameBox, + registration: &AppserviceRegistration, + localpart: Option<&str>, +) -> Result { + let client = if localpart.is_some() { + let request_config = RequestConfig::default().assert_identity(); + let config = ClientConfig::default().request_config(request_config); + Client::new_with_config(homeserver_url.clone(), config)? + } else { + Client::new(homeserver_url.clone())? + }; + + let session = Session { + access_token: registration.as_token.clone(), + user_id: UserId::parse_with_server_name( + localpart.unwrap_or(®istration.sender_localpart), + &server_name, + )?, + device_id: DeviceId::new(), + }; + client.restore_login(session).await?; + + Ok(client) +} + +/// Appservice +#[derive(Debug, Clone)] +pub struct Appservice { + homeserver_url: Url, + server_name: ServerNameBox, + registration: AppserviceRegistration, + client_sender_localpart: Client, +} + +impl Appservice { + /// Create new Appservice + pub async fn new( + homeserver_url: impl TryInto, + server_name: impl TryInto, + registration: AppserviceRegistration, + ) -> Result { + let homeserver_url = homeserver_url.try_into()?; + let server_name = server_name.try_into()?; + + let client = create_client(&homeserver_url, &server_name, ®istration, None).await?; + + Ok(Appservice { + homeserver_url, + server_name, + registration, + client_sender_localpart: client, + }) + } + + /// Get `Client` for the user associated with the application service + /// (`sender_localpart` of the [registration]) + /// + /// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration + pub fn client(&self) -> Client { + self.client_sender_localpart.clone() + } + + /// Get `Client` for the given `localpart` + /// + /// If the `localpart` is covered by the `namespaces` in the [registration] all requests to the + /// homeserver will [assert the identity] to the according virtual user. + /// + /// [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 client_with_localpart( + &self, + localpart: impl AsRef + Into>, + ) -> Result { + let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?; + let localpart = user_id.localpart().to_owned(); + + let client = create_client( + &self.homeserver_url, + &self.server_name, + &self.registration, + Some(&localpart), + ) + .await?; + + self.ensure_registered(localpart).await?; + + Ok(client) + } + + async fn ensure_registered(&self, localpart: impl AsRef) -> Result<()> { + let request = assign!(RegistrationRequest::new(), { + username: Some(localpart.as_ref()), + login_type: Some(&LoginType::ApplicationService), + }); + + match self.client().register(request).await { + Ok(_) => (), + Err(error) => match error { + matrix_sdk::Error::Http(HttpError::UiaaError(FromHttpResponseError::Http( + ServerError::Known(UiaaResponse::MatrixError(ref matrix_error)), + ))) => { + match matrix_error.kind { + ErrorKind::UserInUse => { + // TODO: persist the fact that we registered that user + warn!("{}", matrix_error.message); + } + _ => return Err(error.into()), + } + } + _ => return Err(error.into()), + }, + } + + Ok(()) + } + + /// Get the Appservice [registration] + /// + /// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration + pub fn registration(&self) -> &Registration { + &self.registration + } + + /// Compare the given `hs_token` against `registration.hs_token` + pub fn verify_hs_token(&self, hs_token: impl AsRef) -> bool { + self.registration.hs_token == hs_token.as_ref() + } + + /// Check if given `user_id` is in any of the registration user namespaces + pub fn user_id_is_in_namespace(&self, user_id: impl AsRef) -> Result { + for user in &self.registration.namespaces.users { + // TODO: precompile on Appservice construction + let re = Regex::new(&user.regex)?; + if re.is_match(user_id.as_ref()) { + return Ok(true); + } + } + + Ok(false) + } + + /// Get host and port from registration url + /// + /// If no port is found it falls back to scheme defaults: 80 for http and 443 for https + pub fn get_host_and_port_from_registration(&self) -> Result<(Host, Port)> { + let uri = Uri::try_from(&self.registration.url)?; + + let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned(); + let port = match uri.port() { + Some(port) => Ok(port.as_u16()), + None => match uri.scheme_str() { + Some("http") => Ok(80), + Some("https") => Ok(443), + _ => Err(Error::MissingRegistrationPort), + }, + }?; + + Ok((host, port)) + } + + /// Service to register on an Actix `App` + #[cfg(feature = "actix")] + #[cfg_attr(docs, doc(cfg(feature = "actix")))] + pub fn actix_service(&self) -> actix::Scope { + actix::get_scope().data(self.clone()) + } + + /// Convenience method that runs an http server depending on the selected server feature + /// + /// This is a blocking call that tries to listen on the provided host and port + pub async fn run(&self, host: impl AsRef, port: impl Into) -> Result<()> { + #[cfg(feature = "actix")] + { + actix::run_server(self.clone(), host, port).await?; + Ok(()) + } + + #[cfg(not(feature = "actix"))] + { + error!( + "tried to bind {}:{} but no server feature activated", + host.as_ref(), + port.into() + ); + unimplemented!(); + } + } +} diff --git a/matrix_sdk_appservice/tests/actix.rs b/matrix_sdk_appservice/tests/actix.rs new file mode 100644 index 00000000..eab23010 --- /dev/null +++ b/matrix_sdk_appservice/tests/actix.rs @@ -0,0 +1,115 @@ +#[cfg(feature = "actix")] +mod actix { + use actix_web::{test, App}; + use matrix_sdk_appservice::*; + use std::env; + + async fn appservice() -> Appservice { + env::set_var( + "RUST_LOG", + "mockito=debug,matrix_sdk=debug,ruma=debug,actix_web=debug", + ); + let _ = tracing_subscriber::fmt::try_init(); + + Appservice::new( + mockito::server_url().as_ref(), + "test.local", + AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml").unwrap(), + ) + .await + .unwrap() + } + + #[actix_rt::test] + async fn test_transactions() { + let appservice = appservice().await; + let app = test::init_service(App::new().service(appservice.actix_service())).await; + + let transactions = r#"{ + "events": [ + { + "content": {}, + "type": "m.dummy" + } + ] + }"#; + + let transactions: serde_json::Value = serde_json::from_str(transactions).unwrap(); + + let req = test::TestRequest::put() + .uri("/_matrix/app/v1/transactions/1?access_token=hs_token") + .set_json(&transactions) + .to_request(); + + let resp = test::call_service(&app, req).await; + + assert_eq!(resp.status(), 200); + } + + #[actix_rt::test] + async fn test_users() { + let appservice = appservice().await; + let app = test::init_service(App::new().service(appservice.actix_service())).await; + + let req = test::TestRequest::get() + .uri("/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token") + .to_request(); + + let resp = test::call_service(&app, req).await; + + assert_eq!(resp.status(), 200); + } + + #[actix_rt::test] + async fn test_invalid_access_token() { + let appservice = appservice().await; + let app = test::init_service(App::new().service(appservice.actix_service())).await; + + let transactions = r#"{ + "events": [ + { + "content": {}, + "type": "m.dummy" + } + ] + }"#; + + let transactions: serde_json::Value = serde_json::from_str(transactions).unwrap(); + + let req = test::TestRequest::put() + .uri("/_matrix/app/v1/transactions/1?access_token=invalid_token") + .set_json(&transactions) + .to_request(); + + let resp = test::call_service(&app, req).await; + + assert_eq!(resp.status(), 401); + } + + #[actix_rt::test] + async fn test_no_access_token() { + let appservice = appservice().await; + let app = test::init_service(App::new().service(appservice.actix_service())).await; + + let transactions = r#"{ + "events": [ + { + "content": {}, + "type": "m.dummy" + } + ] + }"#; + + let transactions: serde_json::Value = serde_json::from_str(transactions).unwrap(); + + let req = test::TestRequest::put() + .uri("/_matrix/app/v1/transactions/1") + .set_json(&transactions) + .to_request(); + + let resp = test::call_service(&app, req).await; + + // TODO: this should actually return a 401 but is 500 because something in the extractor fails + assert_eq!(resp.status(), 500); + } +} diff --git a/matrix_sdk_appservice/tests/registration.yaml b/matrix_sdk_appservice/tests/registration.yaml new file mode 100644 index 00000000..eb51a814 --- /dev/null +++ b/matrix_sdk_appservice/tests/registration.yaml @@ -0,0 +1,13 @@ +id: appservice +url: http://localhost:9009 +as_token: as_token +hs_token: hs_token +sender_localpart: _appservice +namespaces: + aliases: [] + rooms: [] + users: + - exclusive: true + regex: '@_appservice_.*' +rate_limited: false +protocols: [] diff --git a/matrix_sdk_appservice/tests/tests.rs b/matrix_sdk_appservice/tests/tests.rs new file mode 100644 index 00000000..9b81b81c --- /dev/null +++ b/matrix_sdk_appservice/tests/tests.rs @@ -0,0 +1,157 @@ +use std::env; + +use matrix_sdk::{ + api_appservice, + api_appservice::Registration, + async_trait, + events::{room::member::MemberEventContent, AnyEvent, AnyStateEvent, SyncStateEvent}, + room::Room, + EventHandler, Raw, +}; +use matrix_sdk_appservice::*; +use matrix_sdk_test::async_test; +use serde_json::json; + +fn registration_string() -> String { + include_str!("../tests/registration.yaml").to_owned() +} + +async fn appservice(registration: Option) -> Result { + env::set_var("RUST_LOG", "mockito=debug,matrix_sdk=debug"); + let _ = tracing_subscriber::fmt::try_init(); + + let registration = match registration { + Some(registration) => registration.into(), + None => AppserviceRegistration::try_from_yaml_str(registration_string()).unwrap(), + }; + + let homeserver_url = mockito::server_url(); + let server_name = "localhost"; + + Ok(Appservice::new(homeserver_url.as_ref(), server_name, registration).await?) +} + +fn member_json() -> serde_json::Value { + json!({ + "content": { + "avatar_url": null, + "displayname": "example", + "membership": "join" + }, + "event_id": "$151800140517rfvjc:localhost", + "membership": "join", + "origin_server_ts": 151800140, + "room_id": "!ahpSDaDUPCCqktjUEF:localhost", + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + "prev_content": { + "avatar_url": null, + "displayname": "example", + "membership": "invite" + }, + "unsigned": { + "age": 297036, + "replaces_state": "$151800111315tsynI:localhost" + } + }) +} + +#[async_test] +async fn test_event_handler() -> Result<()> { + let appservice = appservice(None).await?; + + struct Example {} + + impl Example { + pub fn new() -> Self { + Self {} + } + } + + #[async_trait] + impl EventHandler for Example { + async fn on_state_member(&self, room: Room, event: &SyncStateEvent) { + dbg!(room, event); + } + } + + appservice + .client() + .set_event_handler(Box::new(Example::new())) + .await; + + let event = serde_json::from_value::(member_json()).unwrap(); + let event: Raw = AnyEvent::State(event).into(); + let events = vec![event]; + + let incoming = api_appservice::event::push_events::v1::IncomingRequest::new( + "any_txn_id".to_owned(), + events, + ); + + appservice.client().receive_transaction(incoming).await?; + + Ok(()) +} + +#[async_test] +async fn test_transaction() -> Result<()> { + let appservice = appservice(None).await?; + + let event = serde_json::from_value::(member_json()).unwrap(); + let event: Raw = AnyEvent::State(event).into(); + let events = vec![event]; + + let incoming = api_appservice::event::push_events::v1::IncomingRequest::new( + "any_txn_id".to_owned(), + events, + ); + + appservice.client().receive_transaction(incoming).await?; + + Ok(()) +} + +#[async_test] +async fn test_verify_hs_token() -> Result<()> { + let appservice = appservice(None).await?; + + let registration = appservice.registration(); + + assert!(appservice.verify_hs_token(®istration.hs_token)); + + Ok(()) +} + +mod registration { + use super::*; + + #[test] + fn test_registration() -> Result<()> { + let registration: Registration = serde_yaml::from_str(®istration_string())?; + let registration: AppserviceRegistration = registration.into(); + + assert_eq!(registration.id, "appservice"); + + Ok(()) + } + + #[test] + fn test_registration_from_yaml_file() -> Result<()> { + let registration = AppserviceRegistration::try_from_yaml_file("./tests/registration.yaml")?; + + assert_eq!(registration.id, "appservice"); + + Ok(()) + } + + #[test] + fn test_registration_from_yaml_str() -> Result<()> { + let registration = AppserviceRegistration::try_from_yaml_str(registration_string())?; + + assert_eq!(registration.id, "appservice"); + + Ok(()) + } +} diff --git a/matrix_sdk_common/Cargo.toml b/matrix_sdk_common/Cargo.toml index 0f258e78..21d0c00c 100644 --- a/matrix_sdk_common/Cargo.toml +++ b/matrix_sdk_common/Cargo.toml @@ -12,6 +12,7 @@ version = "0.2.0" [features] markdown = ["ruma/markdown"] +appservice = ["ruma/appservice-api", "ruma/appservice-api-helper", "ruma/rand"] [dependencies] instant = { version = "0.1.9", features = ["wasm-bindgen", "now"] } diff --git a/matrix_sdk_common/src/lib.rs b/matrix_sdk_common/src/lib.rs index fd0e43d9..9e10d525 100644 --- a/matrix_sdk_common/src/lib.rs +++ b/matrix_sdk_common/src/lib.rs @@ -1,5 +1,10 @@ pub use async_trait::async_trait; pub use instant; +#[cfg(feature = "appservice")] +pub use ruma::{ + api::{appservice as api_appservice, IncomingRequest, OutgoingRequestAppserviceExt}, + serde::{exports::serde::de::value::Error as SerdeError, urlencoded}, +}; pub use ruma::{ api::{ client as api, From 3b24d33822ad9a6dc0c9065fff975efcdd8cc372 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 10 May 2021 07:56:00 +0200 Subject: [PATCH 2/7] appservice: Rely on cfg-toggle in send_request --- matrix_sdk/src/client.rs | 2 + matrix_sdk/src/http_client.rs | 130 ++++++++++++++++++++-------------- 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index a5154fe2..983fb086 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -405,6 +405,7 @@ pub struct RequestConfig { pub(crate) retry_limit: Option, pub(crate) retry_timeout: Option, pub(crate) force_auth: bool, + #[cfg(feature = "appservice")] pub(crate) assert_identity: bool, } @@ -427,6 +428,7 @@ impl Default for RequestConfig { retry_limit: Default::default(), retry_timeout: Default::default(), force_auth: false, + #[cfg(feature = "appservice")] assert_identity: false, } } diff --git a/matrix_sdk/src/http_client.rs b/matrix_sdk/src/http_client.rs index 63c3142a..4f91143a 100644 --- a/matrix_sdk/src/http_client.rs +++ b/matrix_sdk/src/http_client.rs @@ -115,68 +115,88 @@ impl HttpClient { None => self.request_config, }; + #[cfg(not(feature = "appservice"))] + let request = self.try_into_http_request(request, session, config).await?; + + #[cfg(feature = "appservice")] let request = if !self.request_config.assert_identity { - let read_guard; - let access_token = if config.force_auth { - read_guard = session.read().await; - if let Some(session) = read_guard.as_ref() { - SendAccessToken::Always(session.access_token.as_str()) - } else { - return Err(HttpError::ForcedAuthenticationWithoutAccessToken); - } - } else { - match Request::METADATA.authentication { - AuthScheme::AccessToken => { - read_guard = session.read().await; - - if let Some(session) = read_guard.as_ref() { - SendAccessToken::IfRequired(session.access_token.as_str()) - } else { - return Err(HttpError::AuthenticationRequired); - } - } - AuthScheme::None => SendAccessToken::None, - _ => return Err(HttpError::NotClientRequest), - } - }; - - request - .try_into_http_request::(&self.homeserver.to_string(), access_token)? - .map(|body| body.freeze()) + self.try_into_http_request(request, session, config).await? } else { - // this should be unreachable since assert_identity on request_config can only be set - // with the appservice feature active - #[cfg(not(feature = "appservice"))] - return Err(HttpError::NeedsAppserviceFeature); - - #[cfg(feature = "appservice")] - { - let read_guard = session.read().await; - let access_token = if let Some(session) = read_guard.as_ref() { - SendAccessToken::Always(session.access_token.as_str()) - } else { - return Err(HttpError::AuthenticationRequired); - }; - - let user_id = if let Some(session) = read_guard.as_ref() { - session.user_id.clone() - } else { - return Err(HttpError::UserIdRequired); - }; - - request - .try_into_http_request_with_user_id::( - &self.homeserver.to_string(), - access_token, - user_id, - )? - .map(|body| body.freeze()) - } + self.try_into_http_request_with_identy_assertion(request, session, config) + .await? }; self.inner.send_request(request, config).await } + async fn try_into_http_request( + &self, + request: Request, + session: Arc>>, + config: RequestConfig, + ) -> Result, HttpError> { + let read_guard; + let access_token = if config.force_auth { + read_guard = session.read().await; + if let Some(session) = read_guard.as_ref() { + SendAccessToken::Always(session.access_token.as_str()) + } else { + return Err(HttpError::ForcedAuthenticationWithoutAccessToken); + } + } else { + match Request::METADATA.authentication { + AuthScheme::AccessToken => { + read_guard = session.read().await; + + if let Some(session) = read_guard.as_ref() { + SendAccessToken::IfRequired(session.access_token.as_str()) + } else { + return Err(HttpError::AuthenticationRequired); + } + } + AuthScheme::None => SendAccessToken::None, + _ => return Err(HttpError::NotClientRequest), + } + }; + + let http_request = request + .try_into_http_request::(&self.homeserver.to_string(), access_token)? + .map(|body| body.freeze()); + + Ok(http_request) + } + + #[cfg(feature = "appservice")] + async fn try_into_http_request_with_identy_assertion( + &self, + request: Request, + session: Arc>>, + _: RequestConfig, + ) -> Result, HttpError> { + let read_guard = session.read().await; + let access_token = if let Some(session) = read_guard.as_ref() { + SendAccessToken::Always(session.access_token.as_str()) + } else { + return Err(HttpError::AuthenticationRequired); + }; + + let user_id = if let Some(session) = read_guard.as_ref() { + session.user_id.clone() + } else { + return Err(HttpError::UserIdRequired); + }; + + let http_request = request + .try_into_http_request_with_user_id::( + &self.homeserver.to_string(), + access_token, + user_id, + )? + .map(|body| body.freeze()); + + Ok(http_request) + } + pub async fn upload( &self, request: create_content::Request<'_>, From 87099676f9e4022b2fe2fe90275ca97df7fd9fe3 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 10 May 2021 08:43:06 +0200 Subject: [PATCH 3/7] appservice: Improve docs --- matrix_sdk/src/client.rs | 6 ++++-- matrix_sdk/src/lib.rs | 2 ++ matrix_sdk_appservice/src/lib.rs | 27 ++++++++++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 983fb086..9c8a5454 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -474,9 +474,11 @@ impl RequestConfig { self } - /// [Assert the identity][identity-assertion] of requests to the `user_id` in the `Session` + /// All outgoing http requests will have a GET query key-value appended with `user_id` being + /// the key and the `user_id` from the `Session` being the value. Will error if there's no + /// `Session`. This is called [identity assertion] in the Matrix Appservice Spec /// - /// [identity-assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion + /// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion #[cfg(feature = "appservice")] #[cfg_attr(feature = "docs", doc(cfg(appservice)))] pub fn assert_identity(mut self) -> Self { diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 74b2ff40..4ebc7ef1 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -47,6 +47,8 @@ //! * `require_auth_for_profile_requests`: Whether to send the access token in the authentication //! header when calling endpoints that retrieve profile data. This matches the synapse //! configuration `require_auth_for_profile_requests`. Enabled by default. +//! * `appservice`: Enables low-level appservice functionality. For an high-level API there's the +//! `matrix-sdk-appservice` crate #![deny( missing_debug_implementations, diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index 7eb6c33d..946a7ec9 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -14,18 +14,24 @@ //! Matrix [Application Service] library //! +//! The appservice crate aims to provide a batteries-included experience. That means that we +//! * ship with functionality to configure your webserver crate or simply run the webserver for you +//! * receive and validate requests from the homeserver correctly +//! * allow calling the homeserver with proper virtual user identity assertion +//! * have the goal to have a consistent room state available by leveraging the stores that the matrix-sdk provides +//! //! # Quickstart //! //! ```no_run //! # async { //! use matrix_sdk_appservice::{Appservice, AppserviceRegistration}; //! -//! let homeserver_url = "http://localhost:8008"; +//! let homeserver_url = "http://127.0.0.1:8008"; //! let server_name = "localhost"; //! let registration = AppserviceRegistration::try_from_yaml_str( //! r" //! id: appservice -//! url: http://localhost:9009 +//! url: http://127.0.0.1:9009 //! as_token: as_token //! hs_token: hs_token //! sender_localpart: _appservice @@ -37,6 +43,7 @@ //! .unwrap(); //! //! let appservice = Appservice::new(homeserver_url, server_name, registration).await.unwrap(); +//! // set event handler with `appservice.client().set_event_handler()` here //! let (host, port) = appservice.get_host_and_port_from_registration().unwrap(); //! appservice.run(host, port).await.unwrap(); //! # }; @@ -91,6 +98,8 @@ pub struct AppserviceRegistration { impl AppserviceRegistration { /// Try to load registration from yaml string + /// + /// See the fields of [`Registration`] for the required format pub fn try_from_yaml_str(value: impl AsRef) -> Result { Ok(Self { inner: serde_yaml::from_str(value.as_ref())?, @@ -98,6 +107,8 @@ impl AppserviceRegistration { } /// Try to load registration from yaml file + /// + /// See the fields of [`Registration`] for the required format pub fn try_from_yaml_file(path: impl Into) -> Result { let file = File::open(path.into())?; @@ -159,6 +170,14 @@ pub struct Appservice { impl Appservice { /// Create new Appservice + /// + /// # Arguments + /// + /// * `homeserver_url` - The homeserver that the client should connect to. + /// * `server_name` - The server name to use when constructing user ids from the localpart. + /// * `registration` - The [Appservice Registration] to use when interacting with the homserver. + /// + /// [Appservice Registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration pub async fn new( homeserver_url: impl TryInto, server_name: impl TryInto, @@ -248,6 +267,8 @@ impl Appservice { } /// Compare the given `hs_token` against `registration.hs_token` + /// + /// Returns `true` if the tokens match, `false` otherwise. pub fn verify_hs_token(&self, hs_token: impl AsRef) -> bool { self.registration.hs_token == hs_token.as_ref() } @@ -265,7 +286,7 @@ impl Appservice { Ok(false) } - /// Get host and port from registration url + /// Get the host and port from the registration URL /// /// If no port is found it falls back to scheme defaults: 80 for http and 443 for https pub fn get_host_and_port_from_registration(&self) -> Result<(Host, Port)> { From 325531d13ff2b6ce8ea8ea24986cc61b3522ffdd Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 10 May 2021 09:04:51 +0200 Subject: [PATCH 4/7] appservice: Compile time webserver feature check --- matrix_sdk_appservice/src/lib.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index 946a7ec9..8c27376f 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -51,6 +51,9 @@ //! //! [Application Service]: https://matrix.org/docs/spec/application_service/r0.1.2 +#[cfg(not(any(feature = "actix",)))] +compile_error!("one webserver feature must be enabled. available ones: `actix`"); + use std::{ convert::{TryFrom, TryInto}, fs::File, @@ -322,14 +325,7 @@ impl Appservice { Ok(()) } - #[cfg(not(feature = "actix"))] - { - error!( - "tried to bind {}:{} but no server feature activated", - host.as_ref(), - port.into() - ); - unimplemented!(); - } + #[cfg(not(any(feature = "actix",)))] + unreachable!() } } From 14bc4eb7e0be8a5bae8739e1760500242bdfba30 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 10 May 2021 09:11:15 +0200 Subject: [PATCH 5/7] appservice: Rename verify_hs_token to hs_token_matches --- matrix_sdk_appservice/src/actix.rs | 6 +++--- matrix_sdk_appservice/src/lib.rs | 2 +- matrix_sdk_appservice/tests/tests.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/matrix_sdk_appservice/src/actix.rs b/matrix_sdk_appservice/src/actix.rs index 3e899822..e260e822 100644 --- a/matrix_sdk_appservice/src/actix.rs +++ b/matrix_sdk_appservice/src/actix.rs @@ -65,7 +65,7 @@ async fn push_transactions( request: IncomingRequest, appservice: Data, ) -> Result { - if !appservice.verify_hs_token(request.access_token) { + if !appservice.hs_token_matches(request.access_token) { return Ok(HttpResponse::Unauthorized().finish()); } @@ -84,7 +84,7 @@ async fn query_user_id( request: IncomingRequest, appservice: Data, ) -> Result { - if !appservice.verify_hs_token(request.access_token) { + if !appservice.hs_token_matches(request.access_token) { return Ok(HttpResponse::Unauthorized().finish()); } @@ -97,7 +97,7 @@ async fn query_room_alias( request: IncomingRequest, appservice: Data, ) -> Result { - if !appservice.verify_hs_token(request.access_token) { + if !appservice.hs_token_matches(request.access_token) { return Ok(HttpResponse::Unauthorized().finish()); } diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index 8c27376f..3f40bacc 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -272,7 +272,7 @@ impl Appservice { /// Compare the given `hs_token` against `registration.hs_token` /// /// Returns `true` if the tokens match, `false` otherwise. - pub fn verify_hs_token(&self, hs_token: impl AsRef) -> bool { + pub fn hs_token_matches(&self, hs_token: impl AsRef) -> bool { self.registration.hs_token == hs_token.as_ref() } diff --git a/matrix_sdk_appservice/tests/tests.rs b/matrix_sdk_appservice/tests/tests.rs index 9b81b81c..19e8bc29 100644 --- a/matrix_sdk_appservice/tests/tests.rs +++ b/matrix_sdk_appservice/tests/tests.rs @@ -119,7 +119,7 @@ async fn test_verify_hs_token() -> Result<()> { let registration = appservice.registration(); - assert!(appservice.verify_hs_token(®istration.hs_token)); + assert!(appservice.hs_token_matches(®istration.hs_token)); Ok(()) } From a2125adeee0a8bf57f992455a560ea43a216830e Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 10 May 2021 09:56:33 +0200 Subject: [PATCH 6/7] ci: Dedicated matrix-sdk-appservice pipeline --- .github/workflows/ci.yml | 64 +++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8236d07..bde7757c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,17 +52,17 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --all-targets -- -D warnings + args: --workspace --exclude matrix-sdk-appservice --all-targets -- -D warnings - name: Clippy without default features uses: actions-rs/cargo@v1 with: command: clippy # TODO: add `--all-targets` once all warnings in examples are resolved - args: --no-default-features --features native-tls -- -D warnings + args: --workspace --exclude matrix-sdk-appservice --no-default-features --features native-tls -- -D warnings check-wasm: - name: Check if WASM support compiles + name: linux / WASM needs: [clippy] runs-on: ubuntu-latest @@ -86,6 +86,58 @@ jobs: cd matrix_sdk/examples/wasm_command_bot cargo check --target wasm32-unknown-unknown + test-appservice: + name: ${{ matrix.name }} + needs: [clippy] + + runs-on: ${{ matrix.os || 'ubuntu-latest' }} + strategy: + matrix: + name: + - linux / appservice / stable + - macOS / appservice / stable + - windows / appservice / stable-x86_64-msvc + + include: + - name: linux / appservice / stable + + - name: macOS / appservice / stable + os: macOS-latest + + - name: windows / appservice / stable-x86_64-msvc + os: windows-latest + target: x86_64-pc-windows-msvc + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust || 'stable' }} + target: ${{ matrix.target }} + profile: minimal + override: true + + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --manifest-path matrix_sdk_appservice/Cargo.toml -- -D warnings + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --manifest-path matrix_sdk_appservice/Cargo.toml + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path matrix_sdk_appservice/Cargo.toml + test-features: name: ${{ matrix.name }} needs: [clippy] @@ -103,7 +155,6 @@ jobs: - linux / features-socks - linux / features-sso_login - linux / features-require_auth_for_profile_requests - - linux / features-appservice include: - name: linux / features-no-encryption @@ -133,9 +184,6 @@ jobs: - name: linux / features-sso_login cargo_args: --features sso_login - - name: linux / features-appservice - cargo_args: --no-default-features --features "appservice, native-tls" - steps: - name: Checkout uses: actions/checkout@v1 @@ -202,8 +250,10 @@ jobs: uses: actions-rs/cargo@v1 with: command: build + args: --workspace --exclude matrix-sdk-appservice - name: Test uses: actions-rs/cargo@v1 with: command: test + args: --workspace --exclude matrix-sdk-appservice From 753302394f1f88d87e83d1084119a319dac1d0e4 Mon Sep 17 00:00:00 2001 From: Johannes Becker Date: Mon, 10 May 2021 12:08:04 +0200 Subject: [PATCH 7/7] appservice: Remove outdated error --- matrix_sdk/src/error.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/matrix_sdk/src/error.rs b/matrix_sdk/src/error.rs index fc284640..3647bf2f 100644 --- a/matrix_sdk/src/error.rs +++ b/matrix_sdk/src/error.rs @@ -83,10 +83,6 @@ pub enum HttpError { #[error("missing user_id in session")] #[cfg(feature = "appservice")] UserIdRequired, - - /// Tried to assert identity without appservice feature enabled - #[error("appservice feature not enabled")] - NeedsAppserviceFeature, } /// Internal representation of errors.