Skip to main content

tuwunel_api/server/
invite.rs

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