Skip to main content

tuwunel_api_federation/
invite.rs

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