Skip to main content

tuwunel_service/membership/
join.rs

1use std::{
2	borrow::Borrow,
3	collections::{BTreeSet, HashMap, HashSet},
4	iter::once,
5	mem::take,
6	sync::Arc,
7};
8
9use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::join};
10use ruma::{
11	CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, OwnedUserId, RoomId,
12	RoomOrAliasId, RoomVersionId, UserId,
13	api::{error::ErrorKind, federation},
14	canonical_json::to_canonical_value,
15	events::{
16		StateEventType,
17		room::{
18			create::RoomCreateEventContent,
19			join_rules::RoomJoinRulesEventContent,
20			member::{MembershipState, RoomMemberEventContent},
21		},
22	},
23	room::{AllowRule, JoinRule},
24	room_version_rules::RoomVersionRules,
25};
26use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
27use tuwunel_core::{
28	Err, Result, async_noinline, at, debug, debug_error, debug_info, debug_warn, err, error,
29	implement, info,
30	matrix::{event::gen_event_id_canonical_json, room_version},
31	pdu::{Pdu, PduBuilder, check_rules},
32	trace,
33	utils::{self, BoolExt, IterStream, ReadyExt, math::Expected, shuffle},
34	warn,
35};
36
37use super::Service;
38use crate::{
39	Services,
40	federation::{Candidates, WhenAllBackedOff},
41	rooms::{
42		state::{IdMapState, RoomMutexGuard},
43		state_compressor::{CompressedState, HashSetCompressStateEvent},
44		state_res,
45	},
46};
47
48#[derive(Debug)]
49pub struct Join<'a> {
50	pub sender_user: &'a UserId,
51	pub room_id: &'a RoomId,
52	pub orig_room_id: Option<&'a RoomOrAliasId>,
53	pub reason: Option<String>,
54	pub servers: &'a [OwnedServerName],
55	pub is_appservice: bool,
56	pub extra_content: Option<CanonicalJsonObject>,
57}
58
59#[implement(Service)]
60// cross-crate codegen firewall
61#[async_noinline]
62#[tracing::instrument(
63	name = "join",
64	level = "debug",
65	skip_all,
66	fields(%sender_user, %room_id)
67)]
68pub async fn join<'a>(
69	&'a self,
70	Join {
71		sender_user,
72		room_id,
73		orig_room_id,
74		reason,
75		servers,
76		is_appservice,
77		extra_content,
78	}: Join<'a>,
79) -> Result {
80	let servers =
81		get_servers_for_room(&self.services, sender_user, room_id, orig_room_id, servers).await?;
82
83	let (federation_lock, state_lock) = self.lock_join(room_id, &servers).await;
84
85	let user_is_guest = !is_appservice
86		&& self
87			.services
88			.users
89			.is_deactivated(sender_user)
90			.await
91			.unwrap_or(false);
92
93	if user_is_guest
94		&& !self
95			.services
96			.state_accessor
97			.guest_can_join(room_id)
98			.await
99	{
100		return Err!(Request(Forbidden("Guests are not allowed to join this room")));
101	}
102
103	if self
104		.services
105		.state_cache
106		.is_joined(sender_user, room_id)
107		.await
108	{
109		debug_warn!(%sender_user, %room_id, "User is already joined.");
110		return Ok(());
111	}
112
113	// Resolved state can lag a federated re-invite; trust the invite index.
114	if self
115		.services
116		.state_accessor
117		.get_member(room_id, sender_user)
118		.await
119		.is_ok_and(|content| content.membership == MembershipState::Ban)
120		&& self
121			.services
122			.state_cache
123			.is_invited(sender_user, room_id)
124			.await
125			.is_false()
126	{
127		debug_warn!(%sender_user, %room_id, "Banned user attempted to join.");
128		return Err!(Request(Forbidden("You are banned from the room.")));
129	}
130
131	match federation_lock {
132		| Some(federation_lock) if !self.is_local_join(room_id, &servers).await =>
133			self.join_remote(
134				sender_user,
135				room_id,
136				reason,
137				&servers,
138				federation_lock,
139				state_lock,
140				extra_content,
141			)
142			.boxed() // cold arm: remote join
143			.await?,
144		| federation_lock => {
145			drop(federation_lock);
146			self.join_local(sender_user, room_id, reason, &servers, state_lock, extra_content)
147				.boxed() // size firewall
148				.await?;
149		},
150	}
151
152	self.copy_predecessor_push_rules(sender_user, room_id)
153		.await;
154
155	Ok(())
156}
157
158/// Acquires the room's federation and state mutexes in canonical order.
159///
160/// A remote join needs both mutexes, but the branch is final only under the
161/// state mutex. The unlocked prediction is revalidated under the locks, so
162/// correctness does not depend on it.
163#[implement(Service)]
164async fn lock_join(
165	&self,
166	room_id: &RoomId,
167	servers: &[OwnedServerName],
168) -> (Option<RoomMutexGuard>, RoomMutexGuard) {
169	if !self.is_local_join(room_id, servers).await {
170		let (federation_lock, state_lock) = self.lock_join_remote(room_id).await;
171
172		return (Some(federation_lock), state_lock);
173	}
174
175	let state_lock = self.services.state.mutex.lock(room_id).await;
176
177	if self.is_local_join(room_id, servers).await {
178		return (None, state_lock);
179	}
180
181	drop(state_lock);
182	let (federation_lock, state_lock) = self.lock_join_remote(room_id).await;
183
184	(Some(federation_lock), state_lock)
185}
186
187#[implement(Service)]
188async fn is_local_join(&self, room_id: &RoomId, servers: &[OwnedServerName]) -> bool {
189	servers.is_empty()
190		|| (servers.len() == 1 && self.services.globals.server_is_ours(&servers[0]))
191		|| self
192			.services
193			.state_cache
194			.server_in_room(self.services.globals.server_name(), room_id)
195			.await
196}
197
198#[implement(Service)]
199async fn lock_join_remote(&self, room_id: &RoomId) -> (RoomMutexGuard, RoomMutexGuard) {
200	// Hold federation before state so inbound events stay belayed until the join
201	// response is applied.
202	let federation_lock = self
203		.services
204		.event_handler
205		.mutex_federation
206		.lock(room_id)
207		.await;
208
209	let state_lock = self.services.state.mutex.lock(room_id).await;
210
211	(federation_lock, state_lock)
212}
213
214#[implement(Service)]
215async fn copy_predecessor_push_rules(&self, user_id: &UserId, room_id: &RoomId) {
216	let Ok(create): Result<RoomCreateEventContent> = self
217		.services
218		.state_accessor
219		.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
220		.await
221	else {
222		return;
223	};
224
225	let Some(predecessor) = create.predecessor else {
226		return;
227	};
228
229	self.services
230		.account_data
231		.copy_room_push_rule(user_id, &predecessor.room_id, room_id)
232		.await
233		.ok();
234}
235
236#[implement(Service)]
237#[expect(clippy::too_many_arguments)]
238#[tracing::instrument(
239	name = "remote",
240	level = "debug",
241	skip_all,
242	fields(?servers)
243)]
244async fn join_remote(
245	&self,
246	sender_user: &UserId,
247	room_id: &RoomId,
248	reason: Option<String>,
249	servers: &[OwnedServerName],
250	_federation_lock: RoomMutexGuard,
251	state_lock: RoomMutexGuard,
252	extra_content: Option<CanonicalJsonObject>,
253) -> Result {
254	info!("Joining {room_id} over federation.");
255
256	let (make_join_response, remote_server) = self
257		.make_join_request(sender_user, room_id, servers)
258		.await?;
259
260	info!("make_join finished");
261
262	let room_version_id = self.require_supported_remote_room_version(&make_join_response)?;
263	let room_version_rules = room_version::rules(&room_version_id)?;
264	let (mut join_event, event_id, join_authorized_via_users_server) = self
265		.create_join_event(
266			room_id,
267			sender_user,
268			&make_join_response.event,
269			&room_version_id,
270			&room_version_rules,
271			reason,
272			extra_content,
273		)
274		.await?;
275
276	let mut response = self
277		.execute_send_join(
278			&remote_server,
279			room_id,
280			&event_id,
281			join_event.clone(),
282			&room_version_id,
283		)
284		.await?;
285
286	if response.members_omitted {
287		self.fetch_omitted_state(&remote_server, room_id, &event_id, servers, &mut response)
288			.await?;
289	}
290
291	if join_authorized_via_users_server.is_some() {
292		merge_restricted_signature(
293			&remote_server,
294			&event_id,
295			&room_version_id,
296			&response,
297			&mut join_event,
298		)?;
299	}
300
301	let shortroomid = self
302		.services
303		.short
304		.get_or_create_shortroomid(room_id)
305		.await;
306
307	info!(
308		%room_id,
309		%shortroomid,
310		"Initialized room. Parsing join event..."
311	);
312	let (parsed_join_pdu, join_event) =
313		Pdu::from_object_federation(room_id, &event_id, join_event, &room_version_rules)?;
314
315	info!(
316		events = response
317			.state
318			.len()
319			.expected_add(response.auth_chain.len()),
320		"Acquiring server signing keys for response events..."
321	);
322	self.services
323		.server_keys
324		.acquire_events_pubkeys(
325			response
326				.auth_chain
327				.iter()
328				.chain(response.state.iter()),
329		)
330		.await;
331
332	let state = self
333		.ingest_send_join_state(room_id, &room_version_id, &room_version_rules, &response.state)
334		.await;
335
336	self.ingest_send_join_auth_chain(
337		room_id,
338		&room_version_id,
339		&room_version_rules,
340		&response.auth_chain,
341	)
342	.await;
343
344	debug!("Running send_join auth check...");
345	state_res::auth_check(
346		&room_version_rules,
347		&parsed_join_pdu,
348		&*self.services.timeline,
349		IdMapState { services: &self.services, ids: &state },
350	)
351	.and_then(async |outcome| outcome.into_result())
352	.inspect_err(|e| error!(?e, "send_join auth check failed"))
353	.boxed() // cold arm: remote-join auth check
354	.await?;
355
356	self.apply_send_join_state(room_id, &state, &state_lock)
357		.await?;
358
359	// We append to state before appending the pdu, so we don't have a moment in
360	// time with the pdu without it's state. This is okay because append_pdu can't
361	// fail.
362	let statehash_after_join = self
363		.services
364		.state
365		.append_to_state(&parsed_join_pdu)
366		.await?;
367
368	info!(
369		event_id = %parsed_join_pdu.event_id,
370		"Appending new room join event..."
371	);
372
373	self.services
374		.timeline
375		.append_pdu(
376			&parsed_join_pdu,
377			join_event,
378			once(parsed_join_pdu.event_id.borrow()),
379			&state_lock,
380		)
381		.await?;
382
383	// We set the room state after inserting the pdu, so that we never have a moment
384	// in time where events in the current room state do not exist
385	self.services
386		.state
387		.set_room_state(room_id, statehash_after_join, &state_lock);
388
389	info!(
390		statehash = %statehash_after_join,
391		"Set final room state for new room."
392	);
393
394	Ok(())
395}
396
397#[implement(Service)]
398fn require_supported_remote_room_version(
399	&self,
400	make_join_response: &federation::membership::prepare_join_event::v1::Response,
401) -> Result<RoomVersionId> {
402	let Some(room_version_id) = make_join_response.room_version.clone() else {
403		return Err!(BadServerResponse("Remote room version is not supported by tuwunel"));
404	};
405
406	if !self
407		.services
408		.config
409		.supported_room_version(&room_version_id)
410	{
411		return Err!(BadServerResponse(
412			"Remote room version {room_version_id} is not supported by tuwunel"
413		));
414	}
415
416	Ok(room_version_id)
417}
418
419#[implement(Service)]
420async fn execute_send_join(
421	&self,
422	remote_server: &OwnedServerName,
423	room_id: &RoomId,
424	event_id: &OwnedEventId,
425	join_event: CanonicalJsonObject,
426	room_version_id: &RoomVersionId,
427) -> Result<federation::membership::create_join_event::v2::RoomState> {
428	let send_join_request = federation::membership::create_join_event::v2::Request {
429		room_id: room_id.to_owned(),
430		event_id: event_id.clone(),
431		omit_members: true,
432		pdu: self
433			.services
434			.federation
435			.format_pdu_into(join_event, Some(room_version_id))
436			.await,
437	};
438
439	info!("Asking {remote_server} for fast_join in room {room_id}");
440	let response = self
441		.services
442		.federation
443		.execute(remote_server, send_join_request)
444		.await
445		.inspect_err(|e| error!("send_join failed: {e}"))?
446		.room_state;
447
448	info!(
449		fast_join = response.members_omitted,
450		auth_chain = response.auth_chain.len(),
451		state = response.state.len(),
452		servers = response
453			.servers_in_room
454			.as_ref()
455			.map(Vec::len)
456			.unwrap_or(0),
457		"send_join finished"
458	);
459
460	Ok(response)
461}
462
463#[implement(Service)]
464async fn fetch_omitted_state(
465	&self,
466	remote_server: &OwnedServerName,
467	room_id: &RoomId,
468	event_id: &OwnedEventId,
469	servers: &[OwnedServerName],
470	response: &mut federation::membership::create_join_event::v2::RoomState,
471) -> Result {
472	use federation::event::get_room_state::v1::{Request, Response};
473
474	let eligible =
475		self.omitted_state_servers(remote_server, servers, response.servers_in_room.as_deref());
476
477	let candidates = self
478		.services
479		.federation
480		.rank_candidates(eligible, WhenAllBackedOff::Attempt)
481		.await;
482
483	let mut last_error = Err!(BadServerResponse("No server provided omitted send_join state."));
484	for server in candidates {
485		info!("Asking {server} for state in room {room_id}");
486		let result = self
487			.services
488			.federation
489			.execute(&server, Request {
490				room_id: room_id.to_owned(),
491				event_id: event_id.clone(),
492			})
493			.await;
494
495		match result {
496			| Err(e) => {
497				debug_warn!(?server, "state fetch failed: {e}");
498				last_error = Err(e);
499			},
500			| Ok(Response { mut auth_chain, mut pdus }) => {
501				response.auth_chain = take(&mut auth_chain);
502				response.state = take(&mut pdus);
503
504				info!(
505					auth_chain = response.auth_chain.len(),
506					state = response.state.len(),
507					"state finished"
508				);
509
510				return Ok(());
511			},
512		}
513	}
514
515	last_error
516}
517
518#[implement(Service)]
519fn omitted_state_servers(
520	&self,
521	remote_server: &OwnedServerName,
522	servers: &[OwnedServerName],
523	servers_in_room: Option<&[String]>,
524) -> Candidates {
525	let extracted = servers_in_room
526		.into_iter()
527		.flatten()
528		.filter_map(|server| OwnedServerName::parse(server.as_str()).ok());
529
530	let mut seen = BTreeSet::new();
531	once(remote_server.clone())
532		.chain(extracted)
533		.chain(servers.iter().cloned())
534		.filter(|server| !self.services.globals.server_is_ours(server))
535		.filter(move |server| seen.insert(server.clone()))
536		.take(
537			self.services
538				.config
539				.max_make_join_attempts_per_join_attempt,
540		)
541		.collect()
542}
543
544fn merge_restricted_signature(
545	remote_server: &OwnedServerName,
546	event_id: &OwnedEventId,
547	room_version_id: &RoomVersionId,
548	response: &federation::membership::create_join_event::v2::RoomState,
549	join_event: &mut CanonicalJsonObject,
550) -> Result {
551	let Some(signed_raw) = &response.event else {
552		return Ok(());
553	};
554
555	debug_info!(
556		"There is a signed event with join_authorized_via_users_server. This room is probably \
557		 using restricted joins. Adding signature to our event"
558	);
559
560	let (signed_event_id, signed_value) =
561		gen_event_id_canonical_json(signed_raw, room_version_id).map_err(|e| {
562			err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
563		})?;
564
565	if signed_event_id != *event_id {
566		return Err!(Request(BadJson(warn!(
567			%signed_event_id, %event_id,
568			"Server {remote_server} sent event with wrong event ID"
569		))));
570	}
571
572	let signature = signed_value["signatures"]
573		.as_object()
574		.ok_or_else(|| {
575			err!(BadServerResponse(warn!("Server {remote_server} sent invalid signatures type")))
576		})
577		.and_then(|e| {
578			e.get(remote_server.as_str()).ok_or_else(|| {
579				err!(BadServerResponse(warn!(
580					"Server {remote_server} did not send its signature for a restricted room"
581				)))
582			})
583		});
584
585	match signature {
586		| Ok(signature) => {
587			join_event
588				.get_mut("signatures")
589				.expect("we created a valid pdu")
590				.as_object_mut()
591				.expect("we created a valid pdu")
592				.insert(remote_server.as_str().into(), signature.clone());
593		},
594		| Err(e) => {
595			warn!(
596				"Server {remote_server} sent invalid signature in send_join signatures for \
597				 event {signed_value:?}: {e:?}",
598			);
599		},
600	}
601
602	Ok(())
603}
604
605#[implement(Service)]
606async fn ingest_send_join_state(
607	&self,
608	room_id: &RoomId,
609	room_version_id: &RoomVersionId,
610	room_version_rules: &RoomVersionRules,
611	state_pdus: &[Box<RawJsonValue>],
612) -> HashMap<u64, OwnedEventId> {
613	info!(events = state_pdus.len(), "Going through send_join response room_state...");
614	let cork = self.services.db.cork_and_flush();
615	let state = state_pdus
616		.iter()
617		.stream()
618		.then(|pdu| {
619			self.services
620				.server_keys
621				.validate_and_add_event_id_no_fetch(pdu, room_version_id)
622		})
623		.inspect_err(|e| debug_error!("Invalid send_join state event: {e:?}"))
624		.ready_filter_map(Result::ok)
625		.ready_filter_map(|(event_id, value)| {
626			Pdu::from_object_federation(room_id, &event_id, value, room_version_rules)
627				.inspect_err(|error| {
628					debug_warn!(?event_id, %error, "Invalid PDU in the join response.");
629				})
630				.map(move |(pdu, value)| (event_id, pdu, value))
631				.ok()
632		})
633		.fold(HashMap::new(), async |mut state, (event_id, pdu, value)| {
634			self.services
635				.timeline
636				.add_pdu_outlier(&event_id, &value);
637
638			if let Some(state_key) = &pdu.state_key {
639				let shortstatekey = self
640					.services
641					.short
642					.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)
643					.await;
644
645				state.insert(shortstatekey, pdu.event_id.clone());
646			}
647
648			state
649		})
650		.await;
651
652	drop(cork);
653	state
654}
655
656#[implement(Service)]
657async fn ingest_send_join_auth_chain(
658	&self,
659	room_id: &RoomId,
660	room_version_id: &RoomVersionId,
661	room_version_rules: &RoomVersionRules,
662	auth_chain: &[Box<RawJsonValue>],
663) {
664	info!(events = auth_chain.len(), "Going through send_join response auth_chain...");
665	let cork = self.services.db.cork_and_flush();
666	auth_chain
667		.iter()
668		.stream()
669		.then(|pdu| {
670			self.services
671				.server_keys
672				.validate_and_add_event_id_no_fetch(pdu, room_version_id)
673		})
674		.inspect_err(|e| debug_error!("Invalid send_join auth_chain event: {e:?}"))
675		.ready_filter_map(Result::ok)
676		.ready_for_each(|(event_id, mut value)| {
677			if !room_version_rules
678				.event_format
679				.require_room_create_room_id
680				&& value["type"] == "m.room.create"
681			{
682				let room_id = CanonicalJsonValue::String(room_id.as_str().into());
683				value.insert("room_id".into(), room_id);
684			}
685
686			self.services
687				.timeline
688				.add_pdu_outlier(&event_id, &value);
689		})
690		.await;
691
692	drop(cork);
693}
694
695#[implement(Service)]
696async fn apply_send_join_state(
697	&self,
698	room_id: &RoomId,
699	state: &HashMap<u64, OwnedEventId>,
700	state_lock: &RoomMutexGuard,
701) -> Result {
702	info!(events = state.len(), "Compressing state from send_join...");
703	let compressed: CompressedState = self
704		.services
705		.state_compressor
706		.compress_state_events(state.iter().map(|(ssk, eid)| (ssk, eid.borrow())))
707		.collect()
708		.await;
709
710	debug!("Saving compressed state...");
711	let HashSetCompressStateEvent {
712		shortstatehash: statehash_before_join,
713		added,
714		removed,
715	} = self
716		.services
717		.state_compressor
718		.save_state(room_id, Arc::new(compressed))
719		.await?;
720
721	debug!(
722		state_hash = ?statehash_before_join,
723		"Forcing state for new room..."
724	);
725	self.services
726		.state
727		.force_state(room_id, statehash_before_join, added, removed, state_lock)
728		.await?;
729
730	self.services
731		.state_cache
732		.update_joined_count(room_id)
733		.await;
734
735	Ok(())
736}
737
738#[implement(Service)]
739#[tracing::instrument(name = "local", level = "debug", skip_all)]
740async fn join_local(
741	&self,
742	sender_user: &UserId,
743	room_id: &RoomId,
744	reason: Option<String>,
745	servers: &[OwnedServerName],
746	state_lock: RoomMutexGuard,
747	extra_content: Option<CanonicalJsonObject>,
748) -> Result {
749	debug_info!("We can join locally");
750
751	let join_rules_event_content = self
752		.services
753		.state_accessor
754		.room_state_get_content::<RoomJoinRulesEventContent>(
755			room_id,
756			&StateEventType::RoomJoinRules,
757			"",
758		)
759		.await;
760
761	let restriction_rooms = match join_rules_event_content {
762		| Ok(RoomJoinRulesEventContent {
763			join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted),
764		}) => restricted
765			.allow
766			.into_iter()
767			.filter_map(|a| match a {
768				| AllowRule::RoomMembership(r) => Some(r.room_id),
769				| _ => None,
770			})
771			.collect(),
772		| _ => Vec::new(),
773	};
774
775	let is_joined_restricted_rooms = self
776		.services
777		.state_cache
778		.is_joined_any(sender_user, restriction_rooms.iter().map(AsRef::as_ref))
779		.await;
780
781	let join_authorized_via_users_server = is_joined_restricted_rooms
782		.then_async(async || {
783			self.services
784				.state_cache
785				.local_users_in_room(room_id)
786				.filter(|user| {
787					self.services.state_accessor.user_can_invite(
788						room_id,
789						user,
790						sender_user,
791						&state_lock,
792					)
793				})
794				.map(ToOwned::to_owned)
795				.boxed() // Unpin for next
796				.next()
797				.await
798		})
799		.map(Option::flatten)
800		.await;
801
802	let content = self
803		.services
804		.profile
805		.fill_content(sender_user, RoomMemberEventContent {
806			reason: reason.clone(),
807			join_authorized_via_users_server,
808			..RoomMemberEventContent::new(MembershipState::Join)
809		})
810		.await;
811
812	let content = merge_member_content(content, extra_content.as_ref())?;
813
814	let pdu_builder = PduBuilder {
815		event_type: StateEventType::RoomMember.into(),
816		content: to_raw_value(&content).map(Into::into)?,
817		state_key: Some(sender_user.to_string().into()),
818		..Default::default()
819	};
820
821	// Try normal join first
822	let Err(error) = self
823		.services
824		.timeline
825		.build_and_append_pdu(pdu_builder, sender_user, room_id, &state_lock)
826		.await
827	else {
828		return Ok(());
829	};
830
831	if restriction_rooms.is_empty()
832		&& (servers.is_empty()
833			|| servers.len() == 1 && self.services.globals.server_is_ours(&servers[0]))
834	{
835		return Err(error);
836	}
837
838	warn!(
839		"We couldn't do the join locally, maybe federation can help to satisfy the restricted \
840		 join requirements"
841	);
842
843	// Drop before the federation fallback: handle_incoming_pdu re-acquires
844	// the same per-room state mutex while ingesting prev_events; deadlock.
845	drop(state_lock);
846
847	let Ok((make_join_response, remote_server)) = self
848		.make_join_request(sender_user, room_id, servers)
849		.await
850	else {
851		return Err(error);
852	};
853
854	let room_version_id = self.require_supported_remote_room_version(&make_join_response)?;
855	let room_version_rules = room_version::rules(&room_version_id)?;
856	let (join_event, event_id, _) = self
857		.create_join_event(
858			room_id,
859			sender_user,
860			&make_join_response.event,
861			&room_version_id,
862			&room_version_rules,
863			reason,
864			extra_content,
865		)
866		.await?;
867
868	let send_join_response = self
869		.execute_send_join(&remote_server, room_id, &event_id, join_event, &room_version_id)
870		.await?;
871
872	let Some(signed_raw) = send_join_response.event else {
873		return Err(error);
874	};
875
876	let (signed_event_id, signed_value) =
877		gen_event_id_canonical_json(&signed_raw, &room_version_id).map_err(|e| {
878			err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
879		})?;
880
881	if signed_event_id != event_id {
882		return Err!(Request(BadJson(warn!(
883			%signed_event_id, %event_id, "Server {remote_server} sent event with wrong event ID"
884		))));
885	}
886
887	self.services
888		.event_handler
889		.handle_incoming_pdu(&remote_server, room_id, &signed_event_id, signed_value, true)
890		.await?
891		.ok_or_else(|| {
892			err!(Request(InvalidParam("Signed join was not accepted as a timeline event.")))
893		})?;
894
895	Ok(())
896}
897
898#[implement(Service)]
899#[expect(clippy::too_many_arguments)]
900#[tracing::instrument(name = "make_join", level = "debug", skip_all)]
901async fn create_join_event(
902	&self,
903	room_id: &RoomId,
904	sender_user: &UserId,
905	join_event_stub: &RawJsonValue,
906	room_version_id: &RoomVersionId,
907	room_version_rules: &RoomVersionRules,
908	reason: Option<String>,
909	extra_content: Option<CanonicalJsonObject>,
910) -> Result<(CanonicalJsonObject, OwnedEventId, Option<OwnedUserId>)> {
911	let mut event: CanonicalJsonObject =
912		serde_json::from_str(join_event_stub.get()).map_err(|e| {
913			err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
914		})?;
915
916	let join_authorized_via_users_server = room_version_rules
917		.authorization
918		.restricted_join_rule
919		.then(|| event.get("content"))
920		.flatten()
921		.and_then(|s| {
922			s.as_object()?
923				.get("join_authorised_via_users_server")
924		})
925		.and_then(|s| OwnedUserId::try_from(s.as_str().unwrap_or_default()).ok());
926
927	let content = self
928		.services
929		.profile
930		.fill_content(sender_user, RoomMemberEventContent {
931			reason,
932			join_authorized_via_users_server: join_authorized_via_users_server.clone(),
933			..RoomMemberEventContent::new(MembershipState::Join)
934		})
935		.await;
936
937	let content = merge_member_content(content, extra_content.as_ref())?;
938
939	event.insert("content".into(), content);
940
941	event.insert(
942		"origin".into(),
943		CanonicalJsonValue::String(
944			self.services
945				.globals
946				.server_name()
947				.as_str()
948				.to_owned(),
949		),
950	);
951
952	event.insert(
953		"origin_server_ts".into(),
954		CanonicalJsonValue::Integer(utils::millis_since_unix_epoch().try_into()?),
955	);
956
957	event.insert("room_id".into(), CanonicalJsonValue::String(room_id.as_str().into()));
958
959	event.insert("sender".into(), CanonicalJsonValue::String(sender_user.as_str().into()));
960
961	event.insert("state_key".into(), CanonicalJsonValue::String(sender_user.as_str().into()));
962
963	event.insert("type".into(), CanonicalJsonValue::String("m.room.member".into()));
964
965	let event_id = self
966		.services
967		.server_keys
968		.gen_id_hash_and_sign_event(&mut event, room_version_id)?;
969
970	check_rules(&event, &room_version_rules.event_format)?;
971
972	Ok((event, event_id, join_authorized_via_users_server))
973}
974
975// Server-computed membership fields win; client custom keys only fill the gaps.
976fn merge_member_content(
977	content: RoomMemberEventContent,
978	extra_content: Option<&CanonicalJsonObject>,
979) -> Result<CanonicalJsonValue> {
980	let mut content = to_canonical_value(content)?;
981
982	if let (CanonicalJsonValue::Object(content), Some(extra_content)) =
983		(&mut content, extra_content)
984	{
985		for (key, value) in extra_content {
986			content
987				.entry(key.clone())
988				.or_insert_with(|| value.clone());
989		}
990	}
991
992	Ok(content)
993}
994
995#[implement(Service)]
996#[tracing::instrument(
997	name = "make_join",
998	level = "debug",
999	skip_all,
1000	fields(?servers)
1001)]
1002async fn make_join_request(
1003	&self,
1004	sender_user: &UserId,
1005	room_id: &RoomId,
1006	servers: &[OwnedServerName],
1007) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
1008	let mut make_join_response_and_server =
1009		Err!(BadServerResponse("No server available to assist in joining."));
1010
1011	let mut make_join_counter: usize = 0;
1012	let mut incompatible_room_version_count: usize = 0;
1013
1014	for remote_server in servers {
1015		if self
1016			.services
1017			.globals
1018			.server_is_ours(remote_server)
1019		{
1020			continue;
1021		}
1022		info!("Asking {remote_server} for make_join ({make_join_counter})");
1023		let make_join_response = self
1024			.services
1025			.federation
1026			.execute(remote_server, federation::membership::prepare_join_event::v1::Request {
1027				room_id: room_id.to_owned(),
1028				user_id: sender_user.to_owned(),
1029				ver: self
1030					.services
1031					.config
1032					.supported_room_versions()
1033					.map(at!(0))
1034					.collect(),
1035			})
1036			.await;
1037
1038		trace!("make_join response: {make_join_response:?}");
1039		make_join_counter = make_join_counter.saturating_add(1);
1040
1041		if let Err(ref e) = make_join_response {
1042			if matches!(
1043				e.kind(),
1044				ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion
1045			) {
1046				incompatible_room_version_count =
1047					incompatible_room_version_count.saturating_add(1);
1048			}
1049
1050			if incompatible_room_version_count > 15 {
1051				info!(
1052					"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
1053					 M_UNSUPPORTED_ROOM_VERSION, assuming that tuwunel does not support the \
1054					 room version {room_id}: {e}"
1055				);
1056
1057				make_join_response_and_server =
1058					Err!(BadServerResponse("Room version is not supported by tuwunel"));
1059
1060				return make_join_response_and_server;
1061			}
1062
1063			let max_attempts = self
1064				.services
1065				.config
1066				.max_make_join_attempts_per_join_attempt;
1067
1068			if make_join_counter >= max_attempts {
1069				warn!(?remote_server, "last make_join failure reason: {e}");
1070				warn!(
1071					"{max_attempts} servers failed to provide valid make_join response, \
1072					 assuming no server can assist in joining."
1073				);
1074
1075				make_join_response_and_server =
1076					Err!(BadServerResponse("No server available to assist in joining."));
1077
1078				return make_join_response_and_server;
1079			}
1080		}
1081
1082		make_join_response_and_server = make_join_response.map(|r| (r, remote_server.clone()));
1083
1084		if make_join_response_and_server.is_ok() {
1085			break;
1086		}
1087	}
1088
1089	make_join_response_and_server
1090}
1091
1092pub(super) async fn get_servers_for_room(
1093	services: &Services,
1094	user_id: &UserId,
1095	room_id: &RoomId,
1096	orig_room_id: Option<&RoomOrAliasId>,
1097	via: &[OwnedServerName],
1098) -> Result<Vec<OwnedServerName>> {
1099	// add invited vias
1100	let additional_servers = services
1101		.state_cache
1102		.servers_invite_via(room_id)
1103		.map(ToOwned::to_owned)
1104		.collect::<Vec<_>>();
1105
1106	let invite_state = services
1107		.state_cache
1108		.invite_state(user_id, room_id)
1109		.map(Result::unwrap_or_default);
1110
1111	let (additional_servers, invite_state) = join(additional_servers, invite_state).await;
1112
1113	let mut servers = Vec::from(via);
1114	shuffle(&mut servers);
1115
1116	// Strict via: an explicit remote server in via must not be padded with
1117	// the room owner, otherwise failover-probe semantics break.
1118	let has_remote_via = via
1119		.iter()
1120		.any(|s| !services.globals.server_is_ours(s));
1121
1122	if !has_remote_via {
1123		if let Some(server_name) = room_id.server_name() {
1124			servers.insert(0, server_name.to_owned());
1125		}
1126
1127		if let Some(orig_room_id) = orig_room_id
1128			&& let Some(orig_server_name) = orig_room_id.server_name()
1129		{
1130			servers.insert(0, orig_server_name.to_owned());
1131		}
1132	}
1133
1134	let extend_and_shuffle = |mut servers: Vec<OwnedServerName>| {
1135		servers.extend(
1136			invite_state
1137				.iter()
1138				.filter_map(|event| event.get_field("sender").ok().flatten())
1139				.filter_map(|sender: &str| UserId::parse(sender).ok())
1140				.map(|user| user.server_name().to_owned()),
1141		);
1142
1143		shuffle(&mut servers);
1144		servers
1145	};
1146
1147	let additional_servers = extend_and_shuffle(additional_servers);
1148
1149	servers.extend_from_slice(&additional_servers);
1150
1151	// 1. (room alias server)?
1152	// 2. (room id server)?
1153	// 3. shuffle [via query + resolve servers]?
1154	// 4. shuffle [invited via, inviters servers]?
1155	debug!(?servers);
1156
1157	// dedup preserving order
1158	let mut set = HashSet::new();
1159	servers.retain(|x| set.insert(x.clone()));
1160	debug!(?servers);
1161
1162	// sort deprioritized servers last
1163	if !servers.is_empty() {
1164		for i in 0..servers.len() {
1165			if services
1166				.server
1167				.config
1168				.deprioritize_joins_through_servers
1169				.is_match(servers[i].host())
1170			{
1171				let server = servers.remove(i);
1172				servers.push(server);
1173			}
1174		}
1175	}
1176
1177	debug_info!(?servers);
1178	Ok(servers)
1179}