Skip to main content

tuwunel_api/server/
send_join.rs

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