appservice: Support appservice located on sub path

This commit is contained in:
Johannes Becker 2021-08-04 11:04:51 +02:00
parent 0bd438e617
commit 937d0aca79
4 changed files with 104 additions and 15 deletions

View file

@ -34,6 +34,12 @@ pub enum Error {
#[error("could not convert host:port to socket addr")] #[error("could not convert host:port to socket addr")]
HostPortToSocketAddrs, HostPortToSocketAddrs,
#[error("uri has empty path")]
UriEmptyPath,
#[error("uri path is unknown")]
UriPathUnknown,
#[error(transparent)] #[error(transparent)]
HttpRequest(#[from] ruma::api::error::FromHttpRequestError), HttpRequest(#[from] ruma::api::error::FromHttpRequestError),

View file

@ -87,7 +87,7 @@ use std::{
use dashmap::DashMap; use dashmap::DashMap;
pub use error::Error; pub use error::Error;
use http::{uri::PathAndQuery, Uri}; use http::Uri;
pub use matrix_sdk; pub use matrix_sdk;
#[doc(no_inline)] #[doc(no_inline)]
pub use matrix_sdk::ruma; pub use matrix_sdk::ruma;
@ -475,26 +475,63 @@ impl AppService {
} }
} }
/// Transforms [legacy routes] to the correct route so ruma can parse them /// Ruma always expects the path to start with `/_matrix`, so we transform
/// properly /// 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 /// [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<Bytes>, mut request: http::Request<Bytes>,
) -> Result<http::Request<Bytes>> { ) -> Result<http::Request<Bytes>> {
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") { if !path.starts_with("/_matrix/app/v1/") {
// rename legacy routes let path = match path {
let mut parts = uri.into_parts(); // special-case paths without value at the end
let path_and_query = match parts.path_and_query { _ if path.ends_with("/_matrix/app/unstable/thirdparty/user") => {
Some(path_and_query) => format!("/_matrix/app/v1{}", path_and_query), "/_matrix/app/v1/thirdparty/user".to_owned()
None => "/_matrix/app/v1".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; *request.uri_mut() = uri;
} }

View file

@ -98,7 +98,7 @@ mod filters {
.and(filters::valid_access_token(appservice.registration().hs_token.clone())) .and(filters::valid_access_token(appservice.registration().hs_token.clone()))
.map(move || appservice.clone()) .map(move || appservice.clone())
.and(http_request().and_then(|request| async move { .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::<http::Request<Bytes>, Rejection>(request) Ok::<http::Request<Bytes>, Rejection>(request)
})) }))
.boxed() .boxed()

View file

@ -11,6 +11,7 @@ use matrix_sdk::{
}; };
use matrix_sdk_appservice::*; use matrix_sdk_appservice::*;
use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson}; use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson};
use ruma::room_id;
use serde_json::json; use serde_json::json;
#[cfg(feature = "warp")] #[cfg(feature = "warp")]
use warp::{Filter, Reply}; use warp::{Filter, Reply};
@ -271,6 +272,51 @@ async fn test_unrelated_path() -> Result<()> {
Ok(()) 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 { mod registration {
use super::*; use super::*;