diff --git a/matrix_sdk_appservice/src/error.rs b/matrix_sdk_appservice/src/error.rs index 3bd1d4f0..1a65a66b 100644 --- a/matrix_sdk_appservice/src/error.rs +++ b/matrix_sdk_appservice/src/error.rs @@ -34,6 +34,12 @@ pub enum Error { #[error("could not convert host:port to socket addr")] HostPortToSocketAddrs, + #[error("uri has empty path")] + UriEmptyPath, + + #[error("uri path is unknown")] + UriPathUnknown, + #[error(transparent)] HttpRequest(#[from] ruma::api::error::FromHttpRequestError), diff --git a/matrix_sdk_appservice/src/lib.rs b/matrix_sdk_appservice/src/lib.rs index 125cb7f0..71de60ec 100644 --- a/matrix_sdk_appservice/src/lib.rs +++ b/matrix_sdk_appservice/src/lib.rs @@ -87,7 +87,7 @@ use std::{ use dashmap::DashMap; pub use error::Error; -use http::{uri::PathAndQuery, Uri}; +use http::Uri; pub use matrix_sdk; #[doc(no_inline)] pub use matrix_sdk::ruma; @@ -475,26 +475,63 @@ impl AppService { } } -/// Transforms [legacy routes] to the correct route so ruma can parse them -/// properly +/// Ruma always expects the path to start with `/_matrix`, so we transform +/// accordingly. Handles [legacy routes] and appservice being located on a sub +/// path. /// /// [legacy routes]: https://matrix.org/docs/spec/application_service/r0.1.2#legacy-routes -pub(crate) fn transform_legacy_route( +// TODO: consider ruma PR +pub(crate) fn transform_request_path( mut request: http::Request, ) -> Result> { - let uri = request.uri().to_owned(); + let uri = request.uri(); + // remove trailing slash from path + let path = uri.path().trim_end_matches('/').to_string(); - 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(), + if !path.starts_with("/_matrix/app/v1/") { + let path = match path { + // special-case paths without value at the end + _ if path.ends_with("/_matrix/app/unstable/thirdparty/user") => { + "/_matrix/app/v1/thirdparty/user".to_owned() + } + _ if path.ends_with("/_matrix/app/unstable/thirdparty/location") => { + "/_matrix/app/v1/thirdparty/location".to_owned() + } + // regular paths with values at the end + _ => { + let mut path = path.split('/').into_iter().rev(); + let value = match path.next() { + Some(value) => value, + None => return Err(Error::UriEmptyPath), + }; + + let mut path = match path.next() { + Some(path_segment) + if ["transactions", "users", "rooms"].contains(&path_segment) => + { + format!("/_matrix/app/v1/{}/{}", path_segment, value) + } + Some(path_segment) => match path.next() { + Some(path_segment2) if path_segment2 == "thirdparty" => { + format!("/_matrix/app/v1/thirdparty/{}/{}", path_segment, value) + } + _ => return Err(Error::UriPathUnknown), + }, + None => return Err(Error::UriEmptyPath), + }; + + if let Some(query) = uri.query() { + path.push_str(&format!("?{}", query)); + } + + path + } }; - parts.path_and_query = - Some(PathAndQuery::try_from(path_and_query).map_err(http::Error::from)?); - let uri = parts.try_into().map_err(http::Error::from)?; + let mut parts = uri.clone().into_parts(); + parts.path_and_query = Some(path.parse()?); + + let uri = parts.try_into().map_err(http::Error::from)?; *request.uri_mut() = uri; } diff --git a/matrix_sdk_appservice/src/webserver/warp.rs b/matrix_sdk_appservice/src/webserver/warp.rs index 8e105252..3c384a29 100644 --- a/matrix_sdk_appservice/src/webserver/warp.rs +++ b/matrix_sdk_appservice/src/webserver/warp.rs @@ -98,7 +98,7 @@ mod filters { .and(filters::valid_access_token(appservice.registration().hs_token.clone())) .map(move || appservice.clone()) .and(http_request().and_then(|request| async move { - let request = crate::transform_legacy_route(request).map_err(Error::from)?; + let request = crate::transform_request_path(request).map_err(Error::from)?; Ok::, Rejection>(request) })) .boxed() diff --git a/matrix_sdk_appservice/tests/tests.rs b/matrix_sdk_appservice/tests/tests.rs index aebb04f8..8174daef 100644 --- a/matrix_sdk_appservice/tests/tests.rs +++ b/matrix_sdk_appservice/tests/tests.rs @@ -11,6 +11,7 @@ use matrix_sdk::{ }; use matrix_sdk_appservice::*; use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson}; +use ruma::room_id; use serde_json::json; #[cfg(feature = "warp")] use warp::{Filter, Reply}; @@ -271,6 +272,51 @@ async fn test_unrelated_path() -> Result<()> { Ok(()) } +#[async_test] +async fn test_appservice_on_sub_path() -> Result<()> { + let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); + let uri_1 = "/sub_path/_matrix/app/v1/transactions/1?access_token=hs_token"; + let uri_2 = "/sub_path/_matrix/app/v1/transactions/2?access_token=hs_token"; + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(EventsJson::Member); + let transaction_1 = transaction_builder.build_json_transaction(); + + let mut transaction_builder = TransactionBuilder::new(); + transaction_builder.add_room_event(EventsJson::MemberNameChange); + let transaction_2 = transaction_builder.build_json_transaction(); + + let appservice = appservice(None).await?; + + #[cfg(feature = "warp")] + { + warp::test::request() + .method("PUT") + .path(uri_1) + .json(&transaction_1) + .filter(&warp::path("sub_path").and(appservice.warp_filter())) + .await?; + + warp::test::request() + .method("PUT") + .path(uri_2) + .json(&transaction_2) + .filter(&warp::path("sub_path").and(appservice.warp_filter())) + .await?; + }; + + let members = appservice + .get_cached_client(None)? + .get_room(&room_id) + .expect("Expected room to be availabe") + .members_no_sync() + .await?; + + assert_eq!(members[0].display_name().unwrap(), "changed"); + + Ok(()) +} + mod registration { use super::*;