Skip to main content

tuwunel_api_federation/
send_join.rs

1use std::borrow::Borrow;
2
3use axum::extract::State;
4use futures::{FutureExt, StreamExt, future::try_join4};
5use ruma::{
6	CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
7	ServerName, UserId,
8	api::federation::membership::create_join_event,
9	events::{
10		StateEventType,
11		room::member::{MembershipState, RoomMemberEventContent},
12	},
13};
14use serde_json::value::RawValue as RawJsonValue;
15use tuwunel_api_client::calculate_heroes;
16use tuwunel_core::{
17	Err, Result, at, err,
18	itertools::Itertools,
19	utils::{
20		BoolExt,
21		future::{BoolExt as _, ReadyBoolExt},
22		stream::{BroadbandExt, IterStream, ReadyExt},
23	},
24	warn,
25};
26use tuwunel_matrix::{RoomVersionRules, event::gen_event_id_canonical_json, room_version};
27use tuwunel_service::Services;
28
29use super::utils::{auth_chain_pdu, require_known_room, state_pdu, walk_auth_chain};
30use crate::Ruma;
31
32/// # `PUT /_matrix/federation/v2/send_join/{roomId}/{eventId}`
33///
34/// Submits a signed join event.
35pub(crate) async fn create_join_event_v2_route(
36	State(services): State<crate::State>,
37	body: Ruma<create_join_event::v2::Request>,
38) -> Result<create_join_event::v2::Response> {
39	let room_id = &body.room_id;
40	let origin = body.origin();
41	let members_omitted = body.omit_members;
42
43	if let Some(server) = room_id.server_name()
44		&& services
45			.config
46			.is_forbidden_remote_server_name(server)
47	{
48		warn!(
49			"Server {origin} tried joining {room_id} through us which has a server name that is \
50			 globally forbidden. Rejecting.",
51		);
52
53		return Err!(Request(Forbidden(warn!(
54			"Room ID server name {server} is banned on this homeserver."
55		))));
56	}
57
58	services.sending.notify_peer_alive(origin).await;
59
60	// Get the servers in the room BEFORE the join
61	let servers_in_room = members_omitted
62		.then_async(|| {
63			services
64				.state_cache
65				.room_servers(room_id)
66				.map(ToOwned::to_owned)
67				.collect::<Vec<_>>()
68		})
69		.await;
70
71	let mut room_state =
72		create_join_event(&services, origin, room_id, &body.pdu, members_omitted)
73			.boxed() // size firewall
74			.await?;
75
76	room_state.members_omitted = members_omitted;
77	room_state.servers_in_room =
78		servers_in_room.map(|servers| servers.into_iter().map(Into::into).collect());
79
80	Ok(create_join_event::v2::Response { room_state })
81}
82
83async fn create_join_event(
84	services: &Services,
85	origin: &ServerName,
86	room_id: &RoomId,
87	pdu: &RawJsonValue,
88	omit_members: bool,
89) -> Result<create_join_event::v2::RoomState> {
90	require_known_room(services, room_id, origin).await?;
91
92	// We need to return the state prior to joining, let's keep a reference to that
93	// here
94	let shortstatehash = services
95		.state
96		.get_room_shortstatehash(room_id)
97		.await
98		.map_err(|e| err!(Request(NotFound(error!("Room has no state: {e}")))))?;
99
100	// We do not add the event_id field to the pdu here because of signature and
101	// hashes checks
102	let room_version_id = services.state.get_room_version(room_id).await?;
103
104	let Ok((event_id, mut value)) = gen_event_id_canonical_json(pdu, &room_version_id) else {
105		// Event could not be converted to canonical json
106		return Err!(Request(BadJson("Could not convert event to canonical json.")));
107	};
108
109	let (content, joining_user) =
110		validate_join_event_shape(services, &value, origin, room_id).await?;
111
112	let room_version_rules = room_version::rules(&room_version_id)?;
113
114	if let Some(authorising_user) = content.join_authorized_via_users_server {
115		validate_restricted_join(
116			services,
117			&authorising_user,
118			&joining_user,
119			room_id,
120			&room_version_rules,
121		)
122		.await?;
123	}
124
125	services
126		.server_keys
127		.hash_and_sign_event(&mut value, &room_version_id)
128		.map_err(|e| err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}")))))?;
129
130	let origin: OwnedServerName = serde_json::from_value(
131		value
132			.get("origin")
133			.ok_or_else(|| err!(Request(BadJson("Event does not have an origin server name."))))?
134			.clone()
135			.into(),
136	)
137	.map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?;
138
139	// MSC3943: Only include heroes when the room has no name and no
140	// canonical alias (matching Synapse's behavior in PR #14442).
141	let heroes = omit_members
142		.then_async(|| {
143			let has_name = services.state_accessor.state_contains(
144				shortstatehash,
145				&StateEventType::RoomName,
146				"",
147			);
148
149			let has_alias = services.state_accessor.state_contains(
150				shortstatehash,
151				&StateEventType::RoomCanonicalAlias,
152				"",
153			);
154
155			has_name
156				.is_false()
157				.and(has_alias.is_false())
158				.then(|_| calculate_heroes(services, room_id, &joining_user))
159		})
160		.await
161		.unwrap_or_default();
162
163	// Prestart state gather here since it doesn't involve the new join event.
164	let state_ids = services
165		.state_accessor
166		.state_full_ids(shortstatehash)
167		.broad_filter_map(async |(ssk, event_id)| {
168			// Filter state: keep all non-member events, the joining user's
169			// member event, and hero member events. If get_statekey_from_short
170			// fails, keep the event (safe default, matching original behavior).
171			if omit_members
172				&& let Ok((kind, sk)) = services.short.get_statekey_from_short(ssk).await
173				&& kind == StateEventType::RoomMember
174				&& let Ok(user_id) = sk.as_str().try_into()
175				&& joining_user != user_id
176				&& !heroes.contains(&user_id)
177			{
178				return None;
179			}
180
181			Some(event_id)
182		})
183		.collect::<Vec<_>>();
184
185	let mutex_lock = services
186		.event_handler
187		.mutex_federation
188		.lock(room_id)
189		.await;
190
191	let pdu_id = services
192		.event_handler
193		.handle_incoming_pdu(&origin, room_id, &event_id, value.clone(), true)
194		.await?
195		.map(at!(0))
196		.ok_or_else(|| err!(Request(Forbidden("Could not accept as timeline event."))))?;
197
198	drop(mutex_lock);
199
200	// Wait for state gather which the remaining operations depend on.
201	let state_ids = state_ids
202		.await
203		.into_iter()
204		.sorted_unstable()
205		.collect::<Vec<_>>();
206
207	// MSC3706: Any events returned within state can be omitted from auth_chain.
208	let include_auth_event =
209		|event_id: &OwnedEventId| !omit_members || state_ids.binary_search(event_id).is_err();
210
211	let auth_heads = state_ids.iter().map(Borrow::borrow);
212
213	let auth_chain = walk_auth_chain(services, room_id, &room_version_id, auth_heads)
214		.ready_filter(include_auth_event)
215		.broad_filter_map(async |event_id| {
216			auth_chain_pdu(services, &event_id, &room_version_id).await
217		})
218		.collect()
219		.map(Ok);
220
221	let state = state_ids
222		.iter()
223		.stream()
224		.broad_filter_map(|event_id| state_pdu(services, event_id, &room_version_id))
225		.collect()
226		.map(Ok);
227
228	// Join event for new server.
229	let event = services
230		.federation
231		.format_pdu_into(value, Some(&room_version_id))
232		.map(Some)
233		.map(Ok);
234
235	// Join event revealed to existing servers.
236	let broadcast = services.sending.send_pdu_room(room_id, &pdu_id);
237
238	let (auth_chain, state, event, ()) = try_join4(auth_chain, state, event, broadcast)
239		.boxed() // size firewall
240		.await?;
241
242	Ok(create_join_event::v2::RoomState {
243		auth_chain,
244		state,
245		event,
246		..Default::default()
247	})
248}
249
250async fn validate_join_event_shape(
251	services: &Services,
252	value: &CanonicalJsonObject,
253	origin: &ServerName,
254	room_id: &RoomId,
255) -> Result<(RoomMemberEventContent, OwnedUserId)> {
256	let event_room_id: OwnedRoomId = serde_json::from_value(
257		value
258			.get("room_id")
259			.ok_or_else(|| err!(Request(BadJson("Event missing room_id property."))))?
260			.clone()
261			.into(),
262	)
263	.map_err(|e| err!(Request(BadJson(warn!("room_id field is not a valid room ID: {e}")))))?;
264
265	if event_room_id != room_id {
266		return Err!(Request(BadJson("Event room_id does not match request path room ID.")));
267	}
268
269	let event_type: StateEventType = serde_json::from_value(
270		value
271			.get("type")
272			.ok_or_else(|| err!(Request(BadJson("Event missing type property."))))?
273			.clone()
274			.into(),
275	)
276	.map_err(|e| err!(Request(BadJson(warn!("Event has invalid state event type: {e}")))))?;
277
278	if event_type != StateEventType::RoomMember {
279		return Err!(Request(BadJson(
280			"Not allowed to send non-membership state event to join endpoint."
281		)));
282	}
283
284	let content: RoomMemberEventContent = serde_json::from_value(
285		value
286			.get("content")
287			.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
288			.clone()
289			.into(),
290	)
291	.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
292
293	if content.membership != MembershipState::Join {
294		return Err!(Request(BadJson(
295			"Not allowed to send a non-join membership event to join endpoint."
296		)));
297	}
298
299	// ACL check sender user server name
300	let sender: OwnedUserId = serde_json::from_value(
301		value
302			.get("sender")
303			.ok_or_else(|| err!(Request(BadJson("Event missing sender property."))))?
304			.clone()
305			.into(),
306	)
307	.map_err(|e| err!(Request(BadJson(warn!("sender property is not a valid user ID: {e}")))))?;
308
309	services
310		.event_handler
311		.acl_check(sender.server_name(), room_id)
312		.await?;
313
314	// check if origin server is trying to send for another server
315	if sender.server_name() != origin {
316		return Err!(Request(Forbidden("Not allowed to join on behalf of another server.")));
317	}
318
319	let joining_user: OwnedUserId = serde_json::from_value(
320		value
321			.get("state_key")
322			.ok_or_else(|| err!(Request(BadJson("Event missing state_key property."))))?
323			.clone()
324			.into(),
325	)
326	.map_err(|e| err!(Request(BadJson(warn!("State key is not a valid user ID: {e}")))))?;
327
328	if joining_user != sender {
329		return Err!(Request(BadJson("State key does not match sender user.")));
330	}
331
332	Ok((content, joining_user))
333}
334
335async fn validate_restricted_join(
336	services: &Services,
337	authorising_user: &UserId,
338	joining_user: &UserId,
339	room_id: &RoomId,
340	room_version_rules: &RoomVersionRules,
341) -> Result {
342	if !room_version_rules
343		.authorization
344		.restricted_join_rule
345	{
346		return Err!(Request(InvalidParam(
347			"Room version does not support restricted rooms but \
348			 join_authorised_via_users_server ({authorising_user}) was found in the event."
349		)));
350	}
351
352	if !services.globals.user_is_local(authorising_user) {
353		return Err!(Request(InvalidParam(
354			"Cannot authorise membership event through {authorising_user} as they do not belong \
355			 to this homeserver"
356		)));
357	}
358
359	if !services
360		.state_cache
361		.is_joined(authorising_user, room_id)
362		.await
363	{
364		return Err!(Request(InvalidParam(
365			"Authorising user {authorising_user} is not in the room you are trying to join, \
366			 they cannot authorise your join."
367		)));
368	}
369
370	if !super::user_can_perform_restricted_join(
371		services,
372		joining_user,
373		room_id,
374		room_version_rules,
375	)
376	.await?
377	{
378		return Err!(Request(UnableToAuthorizeJoin(
379			"Joining user did not pass restricted room's rules."
380		)));
381	}
382
383	Ok(())
384}