Skip to main content

tuwunel_api/server/
invite.rs

1use axum::extract::State;
2use base64::{Engine as _, engine::general_purpose};
3use futures::StreamExt;
4use ruma::{
5	CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
6	ServerName, UserId,
7	api::{
8		appservice::event::push_events,
9		error::{ErrorKind, IncompatibleRoomVersionErrorData},
10		federation::membership::create_invite,
11	},
12	events::{
13		AnyStrippedStateEvent, GlobalAccountDataEventType, StateEventType,
14		push_rules::PushRulesEvent,
15		room::member::{MembershipState, RoomMemberEventContent},
16	},
17	push,
18	serde::{JsonObject, Raw},
19};
20use tuwunel_core::{
21	Err, Error, Result, debug_warn, err,
22	matrix::{Event, PduCount, PduEvent, event::gen_event_id},
23	utils,
24	utils::hash::sha256,
25};
26use tuwunel_service::{
27	Services,
28	membership::{
29		StrippedCreateVerdict, enforce_stripped_create, into_client_stripped, v12_room_ids,
30	},
31};
32
33use crate::{ClientIp, Ruma};
34
35/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
36///
37/// Invites a remote user to a room.
38#[tracing::instrument(skip_all, fields(%client), name = "invite")]
39pub(crate) async fn create_invite_route(
40	State(services): State<crate::State>,
41	ClientIp(client): ClientIp,
42	body: Ruma<create_invite::v2::Request>,
43) -> Result<create_invite::v2::Response> {
44	validate_request(&services, &body).await?;
45
46	enforce_stripped_state(&services, &body).await?;
47
48	let (mut signed_event, invited_user) = parse_and_validate_event(&services, &body).await?;
49
50	sign_event(&services, &mut signed_event, &body.room_version)?;
51
52	let sender = validate_origins(&signed_event, body.origin())?;
53
54	check_invite_permitted(&services, &body, &invited_user).await?;
55
56	let pdu = build_pdu(&body)?;
57
58	let invite_state: Vec<_> = body
59		.invite_room_state
60		.clone()
61		.into_iter()
62		.filter_map(|state| into_client_stripped(&body.room_id, state))
63		.chain([pdu.to_format()])
64		.collect();
65
66	// Block on the inbound /send applying the departure that removes our last
67	// member, so the residency check observes it rather than stale state.
68	let _federation_lock = services
69		.event_handler
70		.mutex_federation
71		.lock(&body.room_id)
72		.await;
73
74	if !services
75		.state_cache
76		.server_in_room(services.globals.server_name(), &body.room_id)
77		.await
78	{
79		record_local_invite(&services, &body, &invited_user, sender, invite_state, &pdu).await?;
80	}
81
82	Ok(create_invite::v2::Response {
83		event: services
84			.federation
85			.format_pdu_into(signed_event, Some(&body.room_version))
86			.await,
87	})
88}
89
90async fn validate_request(
91	services: &Services,
92	body: &Ruma<create_invite::v2::Request>,
93) -> Result<()> {
94	services
95		.event_handler
96		.acl_check(body.origin(), &body.room_id)
97		.await?;
98
99	if !services
100		.config
101		.supported_room_version(&body.room_version)
102	{
103		return Err(Error::BadRequest(
104			ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData::new(
105				body.room_version.clone(),
106			)),
107			"Server does not support this room version.",
108		));
109	}
110
111	if let Some(server) = body.room_id.server_name()
112		&& services
113			.config
114			.is_forbidden_remote_server_name(server)
115	{
116		return Err!(Request(Forbidden("Server is banned on this homeserver.")));
117	}
118
119	Ok(())
120}
121
122/// Validate the create event in the invite's stripped state (MSC4311) and
123/// reject the invite when the operator's policy requires it.
124async fn enforce_stripped_state(
125	services: &Services,
126	body: &Ruma<create_invite::v2::Request>,
127) -> Result<()> {
128	let verdict = services
129		.membership
130		.validate_stripped_create(&body.invite_room_state, &body.room_id, &body.room_version)
131		.await?;
132
133	if verdict != StrippedCreateVerdict::Valid {
134		debug_warn!(
135			?verdict,
136			room_id = %body.room_id,
137			"MSC4311 invite create-event validation failed",
138		);
139	}
140
141	if enforce_stripped_create(
142		verdict,
143		v12_room_ids(&body.room_version),
144		services
145			.config
146			.enforce_stripped_state_pdu_validation,
147	) {
148		return Err!(Request(MissingParam(
149			"The invite's m.room.create event is missing or does not validate for this room."
150		)));
151	}
152
153	Ok(())
154}
155
156async fn parse_and_validate_event(
157	services: &Services,
158	body: &Ruma<create_invite::v2::Request>,
159) -> Result<(CanonicalJsonObject, OwnedUserId)> {
160	let signed_event = utils::to_canonical_object(&body.event)
161		.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
162
163	let room_id: OwnedRoomId = signed_event
164		.get("room_id")
165		.try_into()
166		.map(RoomId::to_owned)
167		.map_err(|e| err!(Request(InvalidParam("Invalid room_id property: {e}"))))?;
168
169	if body.room_id != room_id {
170		return Err!(Request(InvalidParam("Event room_id does not match the request path.")));
171	}
172
173	let kind: StateEventType = signed_event
174		.get("type")
175		.and_then(CanonicalJsonValue::as_str)
176		.ok_or_else(|| err!(Request(BadJson("Missing type in event."))))?
177		.into();
178
179	if kind != StateEventType::RoomMember {
180		return Err!(Request(InvalidParam("Event must be m.room.member type.")));
181	}
182
183	let invited_user: OwnedUserId = signed_event
184		.get("state_key")
185		.try_into()
186		.map(UserId::to_owned)
187		.map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
188
189	if !services.globals.user_is_local(&invited_user) {
190		return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
191	}
192
193	if services
194		.users
195		.invites_blocked(&invited_user)
196		.await
197	{
198		return Err!(Request(InviteBlocked("{invited_user} has blocked invites.")));
199	}
200
201	let content: RoomMemberEventContent = signed_event
202		.get("content")
203		.cloned()
204		.map(Into::into)
205		.map(serde_json::from_value)
206		.transpose()
207		.map_err(|e| err!(Request(InvalidParam("Invalid content object in event: {e}"))))?
208		.ok_or_else(|| err!(Request(BadJson("Missing content in event."))))?;
209
210	if content.membership != MembershipState::Invite {
211		return Err!(Request(InvalidParam("Event membership must be invite.")));
212	}
213
214	services
215		.event_handler
216		.acl_check(invited_user.server_name(), &body.room_id)
217		.await?;
218
219	Ok((signed_event, invited_user))
220}
221
222fn sign_event(
223	services: &Services,
224	signed_event: &mut CanonicalJsonObject,
225	room_version: &RoomVersionId,
226) -> Result<()> {
227	services
228		.server_keys
229		.hash_and_sign_event(signed_event, room_version)
230		.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
231
232	let event_id = gen_event_id(signed_event, room_version)?;
233	signed_event.insert("event_id".into(), CanonicalJsonValue::String(event_id.to_string()));
234
235	Ok(())
236}
237
238fn validate_origins<'a>(
239	signed_event: &'a CanonicalJsonObject,
240	body_origin: &ServerName,
241) -> Result<&'a UserId> {
242	let origin: Option<&str> = signed_event
243		.get("origin")
244		.and_then(CanonicalJsonValue::as_str);
245
246	let sender: &UserId = signed_event
247		.get("sender")
248		.try_into()
249		.map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
250
251	if sender.server_name() != body_origin {
252		return Err!(Request(Forbidden("Can only send invites on behalf of your users.")));
253	}
254
255	if origin.is_some_and(|origin| origin != sender.server_name()) {
256		return Err!(Request(Forbidden("Your users can only be from your origin.")));
257	}
258
259	if origin.is_some_and(|origin| origin != body_origin) {
260		return Err!(Request(Forbidden("Can only send events from your origin.")));
261	}
262
263	Ok(sender)
264}
265
266async fn check_invite_permitted(
267	services: &Services,
268	body: &Ruma<create_invite::v2::Request>,
269	invited_user: &UserId,
270) -> Result<()> {
271	if services.metadata.is_banned(&body.room_id).await
272		&& !services.admin.user_is_admin(invited_user).await
273	{
274		return Err!(Request(Forbidden("This room is banned on this homeserver.")));
275	}
276
277	if services.config.block_non_admin_invites
278		&& !services.admin.user_is_admin(invited_user).await
279	{
280		return Err!(Request(Forbidden("This server does not allow room invites.")));
281	}
282
283	Ok(())
284}
285
286fn build_pdu(body: &Ruma<create_invite::v2::Request>) -> Result<PduEvent> {
287	let mut event: JsonObject = serde_json::from_str(body.event.get())
288		.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
289
290	event.insert("event_id".into(), "$placeholder".into());
291
292	serde_json::from_value(event.into())
293		.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))
294}
295
296/// Record an invite for a room we are not currently in.
297///
298/// When we are active in the room, the remote server will notify us about the
299/// join/invite through `/send`. When we are not in the room, the invited state
300/// must be recorded manually for client `/sync` through `update_membership()`,
301/// and the invite PDU pushed to the relevant appservices.
302async fn record_local_invite(
303	services: &Services,
304	body: &Ruma<create_invite::v2::Request>,
305	invited_user: &UserId,
306	sender: &UserId,
307	invite_state: Vec<Raw<AnyStrippedStateEvent>>,
308	pdu: &PduEvent,
309) -> Result<()> {
310	if services
311		.state_accessor
312		.room_state_get_content::<RoomMemberEventContent>(
313			&body.room_id,
314			&StateEventType::RoomMember,
315			invited_user.as_str(),
316		)
317		.await
318		.is_ok_and(|content| content.membership == MembershipState::Ban)
319	{
320		debug_warn!(
321			room_id = %body.room_id,
322			user_id = %invited_user,
323			"Recording invite while local room state shows banned membership.",
324		);
325	}
326
327	let count = services.globals.next_count();
328	services
329		.state_cache
330		.update_membership(
331			&body.room_id,
332			invited_user,
333			RoomMemberEventContent::new(MembershipState::Invite),
334			sender,
335			Some(invite_state),
336			body.via.clone(),
337			true,
338			PduCount::Normal(*count),
339		)
340		.await?;
341	drop(count);
342
343	notify_pushers(services, invited_user, pdu).await;
344
345	for appservice in services.appservice.read().await.values() {
346		if appservice.is_user_match(invited_user) {
347			services
348				.appservice
349				.send_request(appservice.registration.clone(), push_events::v1::Request {
350					events: vec![pdu.to_format()],
351					txn_id: general_purpose::URL_SAFE_NO_PAD
352						.encode(sha256::hash(pdu.event_id.as_bytes()))
353						.into(),
354					ephemeral: Vec::new(),
355					to_device: Vec::new(),
356				})
357				.await?;
358		}
359	}
360
361	Ok(())
362}
363
364async fn notify_pushers(services: &Services, invited_user: &UserId, pdu: &PduEvent) {
365	services
366		.pusher
367		.get_pushkeys(invited_user)
368		.map(ToOwned::to_owned)
369		.for_each(async |pushkey| {
370			let Ok(pusher) = services
371				.pusher
372				.get_pusher(invited_user, &pushkey)
373				.await
374			else {
375				return;
376			};
377
378			let ruleset = services
379				.account_data
380				.get_global(invited_user, GlobalAccountDataEventType::PushRules)
381				.await
382				.map_or_else(
383					|_| push::Ruleset::server_default(invited_user),
384					|ev: PushRulesEvent| ev.content.global,
385				);
386
387			services
388				.pusher
389				.send_push_notice(invited_user, &pusher, &ruleset, pdu)
390				.await
391				.ok();
392		})
393		.await;
394}