Skip to main content

tuwunel_service/appservice/
request.rs

1use std::{fmt::Debug, mem};
2
3use bytes::{Bytes, BytesMut};
4use reqwest::Request;
5use ruma::api::{
6	IncomingResponse, OutgoingRequest,
7	appservice::Registration,
8	auth_scheme::{AuthScheme, SendAccessToken},
9	path_builder::PathBuilder,
10};
11use tuwunel_core::{
12	Err, Result, debug_error, err, implement, trace, utils::string_from_bytes, warn,
13};
14
15use crate::client::read_response_capped;
16
17/// Sends a request to an appservice
18///
19/// Only returns Ok(None) if there is no url specified in the appservice
20/// registration file
21#[implement(super::Service)]
22pub async fn send_request<T>(
23	&self,
24	registration: Registration,
25	request: T,
26) -> Result<Option<T::IncomingResponse>>
27where
28	T: OutgoingRequest + Debug + Send,
29	for<'a> T::Authentication: AuthScheme<Input<'a> = SendAccessToken<'a>>,
30	for<'a> T::PathBuilder: PathBuilder<Input<'a> = ()>,
31{
32	let client = &self.services.client.appservice;
33
34	let Some(dest) = registration.url else {
35		return Ok(None);
36	};
37
38	if dest == *"null" || dest.is_empty() {
39		return Ok(None);
40	}
41
42	trace!("Appservice URL \"{dest}\", Appservice ID: {}", registration.id);
43
44	let hs_token = registration.hs_token.as_str();
45	let mut http_request = request
46		.try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(hs_token), ())
47		.map_err(|e| {
48			err!(BadServerResponse(
49				warn!(appservice = %registration.id, "Failed to find destination {dest}: {e:?}")
50			))
51		})?
52		.map(BytesMut::freeze);
53
54	add_access_token_query(&mut http_request, hs_token);
55
56	let reqwest_request = Request::try_from(http_request)?;
57
58	let mut response = client
59		.execute(reqwest_request)
60		.await
61		.map_err(|e| {
62			warn!(
63				"Could not send request to appservice \"{}\" at {dest}: {e:?}",
64				registration.id
65			);
66			e
67		})?;
68
69	// reqwest::Response -> http::Response conversion
70	let status = response.status();
71	let mut http_response_builder = http::Response::builder()
72		.status(status)
73		.version(response.version());
74
75	mem::swap(
76		response.headers_mut(),
77		http_response_builder
78			.headers_mut()
79			.expect("http::response::Builder is usable"),
80	);
81
82	let limit = self.services.config.max_response_size;
83	let body = read_response_capped(response, limit).await?;
84
85	if !status.is_success() {
86		debug_error!("Appservice response bytes: {:?}", string_from_bytes(&body));
87		return Err!(BadServerResponse(warn!(
88			"Appservice \"{}\" returned unsuccessful HTTP response {status} at {dest}",
89			registration.id
90		)));
91	}
92
93	let response = T::IncomingResponse::try_from_http_response(
94		http_response_builder
95			.body(body)
96			.expect("reqwest body is valid http body"),
97	);
98
99	response.map(Some).map_err(|e| {
100		err!(BadServerResponse(warn!(
101			"Appservice \"{}\" returned invalid/malformed response bytes {dest}: {e}",
102			registration.id
103		)))
104	})
105}
106
107/// Appends the `hs_token` as an `access_token` query parameter, the legacy
108/// authentication scheme some appservices still require.
109pub(super) fn add_access_token_query(request: &mut http::Request<Bytes>, hs_token: &str) {
110	let mut parts = request.uri().clone().into_parts();
111	let old_path_and_query = parts
112		.path_and_query
113		.expect("valid request uri path and query")
114		.as_str()
115		.to_owned();
116
117	let symbol = if old_path_and_query.contains('?') { "&" } else { "?" };
118
119	parts.path_and_query = Some(
120		(old_path_and_query + symbol + "access_token=" + hs_token)
121			.parse()
122			.expect("valid path and query"),
123	);
124
125	*request.uri_mut() = parts
126		.try_into()
127		.expect("our manipulation is always valid");
128}