Skip to content
Snippets Groups Projects
membership.rs 50.7 KiB
Newer Older
		.state
		.set_room_state(room_id, statehash_after_join, &state_lock)?;

	Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}

#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local")]
async fn join_room_by_id_helper_local(
	services: &Services, sender_user: &UserId, room_id: &RoomId, reason: Option<String>, servers: &[OwnedServerName],
	_third_party_signed: Option<&ThirdPartySigned>, state_lock: RoomMutexGuard,
) -> Result<join_room_by_id::v3::Response> {
Jason Volk's avatar
Jason Volk committed
	debug!("We can join locally");
	let join_rules_event = services
		.rooms
		.state_accessor
		.room_state_get(room_id, &StateEventType::RoomJoinRules, "")?;

	let join_rules_event_content: Option<RoomJoinRulesEventContent> = join_rules_event
		.as_ref()
		.map(|join_rules_event| {
			serde_json::from_str(join_rules_event.content.get()).map_err(|e| {
				warn!("Invalid join rules event: {}", e);
				Error::bad_database("Invalid join rules event in db.")
			})
		})
		.transpose()?;

	let restriction_rooms = match join_rules_event_content {
		Some(RoomJoinRulesEventContent {
			join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted),
		}) => restricted
			.allow
			.into_iter()
			.filter_map(|a| match a {
				AllowRule::RoomMembership(r) => Some(r.room_id),
				_ => None,
			})
			.collect(),
		_ => Vec::new(),
	};

	let local_members = services
		.rooms
		.state_cache
		.room_members(room_id)
		.filter_map(Result::ok)
		.filter(|user| services.globals.user_is_local(user))
		.collect::<Vec<OwnedUserId>>();

	let mut join_authorized_via_users_server: Option<OwnedUserId> = None;

	if restriction_rooms.iter().any(|restriction_room_id| {
		services
			.rooms
			.state_cache
			.is_joined(sender_user, restriction_room_id)
			.unwrap_or(false)
	}) {
		for user in local_members {
			if services
				.rooms
				.state_accessor
				.user_can_invite(room_id, &user, sender_user, &state_lock)
				.unwrap_or(false)
			{
				join_authorized_via_users_server = Some(user);
				break;
			}
		}
	}

	let event = RoomMemberEventContent {
		membership: MembershipState::Join,
		displayname: services.users.displayname(sender_user)?,
		avatar_url: services.users.avatar_url(sender_user)?,
		is_direct: None,
		third_party_invite: None,
		blurhash: services.users.blurhash(sender_user)?,
		reason: reason.clone(),
	let error = match services
		.rooms
		.timeline
		.build_and_append_pdu(
			PduBuilder {
				event_type: TimelineEventType::RoomMember,
				content: to_raw_value(&event).expect("event is valid, we just created it"),
				unsigned: None,
				state_key: Some(sender_user.to_string()),
				redacts: None,
			},
			sender_user,
			room_id,
			&state_lock,
		)
		.await
	{
		Ok(_event_id) => return Ok(join_room_by_id::v3::Response::new(room_id.to_owned())),
		Err(e) => e,
	};
	if !restriction_rooms.is_empty()
		&& servers
			.iter()
			.any(|server_name| !services.globals.server_is_ours(server_name))
Jason Volk's avatar
Jason Volk committed
		warn!("We couldn't do the join locally, maybe federation can help to satisfy the restricted join requirements");
		let (make_join_response, remote_server) = make_join_request(services, sender_user, room_id, servers).await?;

		let room_version_id = match make_join_response.room_version {
			Some(room_version_id)
				if services
🥺's avatar
🥺 committed
					.globals
					.supported_room_versions()
					.contains(&room_version_id) =>
🥺's avatar
🥺 committed
			{
🥺's avatar
🥺 committed
			},
			_ => return Err!(BadServerResponse("Room version is not supported")),
		};
		let mut join_event_stub: CanonicalJsonObject = serde_json::from_str(make_join_response.event.get())
			.map_err(|e| err!(BadServerResponse("Invalid make_join event json received from server: {e:?}")))?;
		let join_authorized_via_users_server = join_event_stub
			.get("content")
🥺's avatar
🥺 committed
			.map(|s| {
				s.as_object()?
					.get("join_authorised_via_users_server")?
					.as_str()
			})
			.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok());
		// TODO: Is origin needed?
		join_event_stub.insert(
			"origin".to_owned(),
			CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
		);
		join_event_stub.insert(
			"origin_server_ts".to_owned(),
			CanonicalJsonValue::Integer(
🥺's avatar
🥺 committed
				utils::millis_since_unix_epoch()
					.try_into()
					.expect("Timestamp is valid js_int value"),
			),
		);
		join_event_stub.insert(
			"content".to_owned(),
			to_canonical_value(RoomMemberEventContent {
				membership: MembershipState::Join,
				displayname: services.users.displayname(sender_user)?,
				avatar_url: services.users.avatar_url(sender_user)?,
				is_direct: None,
				third_party_invite: None,
				blurhash: services.users.blurhash(sender_user)?,
				join_authorized_via_users_server,
			})
			.expect("event is valid, we just created it"),
		);

		// We keep the "event_id" in the pdu only in v1 or
		// v2 rooms
		match room_version_id {
			RoomVersionId::V1 | RoomVersionId::V2 => {},
			_ => {
				join_event_stub.remove("event_id");
			},
		};

		// In order to create a compatible ref hash (EventID) the `hashes` field needs
		// to be present
		ruma::signatures::hash_and_sign_event(
			services.globals.server_name().as_str(),
			services.globals.keypair(),
			&mut join_event_stub,
			&room_version_id,
		)
		.expect("event is valid, we just created it");

		// Generate event id
		let event_id = format!(
			"${}",
			ruma::signatures::reference_hash(&join_event_stub, &room_version_id)
				.expect("ruma can calculate reference hashes")
		);
		let event_id = <&EventId>::try_from(event_id.as_str()).expect("ruma's reference hashes are valid event ids");

		// Add event_id back
		join_event_stub.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned()));

		// It has enough fields to be called a proper event now
		let join_event = join_event_stub;
		let send_join_response = services
			.sending
			.send_federation_request(
				&remote_server,
				federation::membership::create_join_event::v2::Request {
					room_id: room_id.to_owned(),
					event_id: event_id.to_owned(),
					pdu: services
						.sending
						.convert_to_outgoing_federation_event(join_event.clone()),
					omit_members: false,
				},
			)
			.await?;

		if let Some(signed_raw) = send_join_response.room_state.event {
			let Ok((signed_event_id, signed_value)) = gen_event_id_canonical_json(&signed_raw, &room_version_id) else {
				// Event could not be converted to canonical json
				return Err(Error::BadRequest(
					ErrorKind::InvalidParam,
					"Could not convert event to canonical json.",
				));
			if signed_event_id != event_id {
				return Err(Error::BadRequest(
					ErrorKind::InvalidParam,
					"Server sent event with wrong event id",
				));
			drop(state_lock);
			let pub_key_map = RwLock::new(BTreeMap::new());
			services
				.fetch_required_signing_keys([&signed_value], &pub_key_map)
				.await?;
			services
				.event_handler
				.handle_incoming_pdu(&remote_server, room_id, &signed_event_id, signed_value, true, &pub_key_map)
				.await?;
		} else {
			return Err(error);
		}
	} else {
		return Err(error);
	}

	Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
async fn make_join_request(
	services: &Services, sender_user: &UserId, room_id: &RoomId, servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
	let mut make_join_response_and_server = Err!(BadServerResponse("No server available to assist in joining."));
	let mut make_join_counter: u16 = 0;
	let mut incompatible_room_version_count: u8 = 0;
	for remote_server in servers {
		if services.globals.server_is_ours(remote_server) {
		info!("Asking {remote_server} for make_join ({make_join_counter})");
		let make_join_response = services
			.sending
			.send_federation_request(
				remote_server,
				federation::membership::prepare_join_event::v1::Request {
					room_id: room_id.to_owned(),
					user_id: sender_user.to_owned(),
					ver: services.globals.supported_room_versions(),
		trace!("make_join response: {:?}", make_join_response);
		make_join_counter = make_join_counter.saturating_add(1);
Jason Volk's avatar
Jason Volk committed
			trace!("make_join ErrorKind string: {:?}", e.kind().to_string());
			// converting to a string is necessary (i think) because ruma is forcing us to
			// fill in the struct for M_INCOMPATIBLE_ROOM_VERSION
Jason Volk's avatar
Jason Volk committed
			if e.kind().to_string().contains("M_INCOMPATIBLE_ROOM_VERSION")
				|| e.kind().to_string().contains("M_UNSUPPORTED_ROOM_VERSION")
				incompatible_room_version_count = incompatible_room_version_count.saturating_add(1);
			}

			if incompatible_room_version_count > 15 {
				info!(
					"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or M_UNSUPPORTED_ROOM_VERSION, \
					 assuming that Conduwuit does not support the room {room_id}: {e}"
				);
				make_join_response_and_server = Err!(BadServerResponse("Room version is not supported by Conduwuit"));

			if make_join_counter > 50 {
				warn!(
					"50 servers failed to provide valid make_join response, assuming no server can assist in joining."
				);
				make_join_response_and_server = Err!(BadServerResponse("No server available to assist in joining."));
				return make_join_response_and_server;
			}
		make_join_response_and_server = make_join_response.map(|r| (r, remote_server.clone()));

		if make_join_response_and_server.is_ok() {
			break;
		}
	}

	make_join_response_and_server
pub async fn validate_and_add_event_id(
	services: &Services, pdu: &RawJsonValue, room_version: &RoomVersionId,
	pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
Timo Kösters's avatar
Timo Kösters committed
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
	let mut value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
		debug_error!("Invalid PDU in server response: {pdu:#?}");
		err!(BadServerResponse("Invalid PDU in server response: {e:?}"))
	})?;
	let event_id = EventId::parse(format!(
		"${}",
		ruma::signatures::reference_hash(&value, room_version).expect("ruma can calculate reference hashes")
	))
	.expect("ruma's reference hashes are valid event ids");

		match services
🥺's avatar
🥺 committed
			.globals
			.bad_event_ratelimiter
			.write()
			.expect("locked")
🥺's avatar
🥺 committed
			.entry(id)
		{
			Entry::Vacant(e) => {
				e.insert((Instant::now(), 1));
			},
			Entry::Occupied(mut e) => {
🥺's avatar
🥺 committed
				*e.get_mut() = (Instant::now(), e.get().1.saturating_add(1));
	if let Some((time, tries)) = services
🥺's avatar
🥺 committed
		.globals
		.bad_event_ratelimiter
		.read()
		.expect("locked")
🥺's avatar
🥺 committed
		.get(&event_id)
	{
		// Exponential backoff
		const MIN: u64 = 60 * 5;
		const MAX: u64 = 60 * 60 * 24;
		if continue_exponential_backoff_secs(MIN, MAX, time.elapsed(), *tries) {
			return Err!(BadServerResponse("bad event {event_id:?}, still backing off"));
	if let Err(e) = ruma::signatures::verify_event(&*pub_key_map.read().await, &value, room_version) {
		debug_error!("Event {event_id} failed verification {pdu:#?}");
		let e = Err!(BadServerResponse(debug_error!("Event {event_id} failed verification: {e:?}")));
		return e;
	}

	value.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned()));

	Ok((event_id, value))
🥺's avatar
🥺 committed
pub(crate) async fn invite_helper(
	services: &Services, sender_user: &UserId, user_id: &UserId, room_id: &RoomId, reason: Option<String>,
	is_direct: bool,
) -> Result<()> {
	if !services.users.is_admin(user_id)? && services.globals.block_non_admin_invites() {
		info!("User {sender_user} is not an admin and attempted to send an invite to room {room_id}");
		return Err(Error::BadRequest(
			"Invites are not allowed on this server.",
		));
	}

	if !services.globals.user_is_local(user_id) {
		let (pdu, pdu_json, invite_room_state) = {
			let state_lock = services.rooms.state.mutex.lock(room_id).await;
			let content = to_raw_value(&RoomMemberEventContent {
				avatar_url: services.users.avatar_url(user_id)?,
				displayname: None,
				is_direct: Some(is_direct),
				membership: MembershipState::Invite,
				third_party_invite: None,
				blurhash: None,
				reason,
				join_authorized_via_users_server: None,
			})
			.expect("member event is valid value");

			let (pdu, pdu_json) = services.rooms.timeline.create_hash_and_sign_event(
				PduBuilder {
					event_type: TimelineEventType::RoomMember,
					content,
					unsigned: None,
					state_key: Some(user_id.to_string()),
					redacts: None,
				},
				sender_user,
				room_id,
				&state_lock,
			)?;

			let invite_room_state = services.rooms.state.calculate_invite_state(&pdu)?;

			drop(state_lock);

			(pdu, pdu_json, invite_room_state)
		};

		let room_version_id = services.rooms.state.get_room_version(room_id)?;
		let response = services
			.sending
			.send_federation_request(
				user_id.server_name(),
				create_invite::v2::Request {
					room_id: room_id.to_owned(),
					event_id: (*pdu.event_id).to_owned(),
					room_version: room_version_id.clone(),
					event: services
						.sending
						.convert_to_outgoing_federation_event(pdu_json.clone()),
					invite_room_state,
					via: services.rooms.state_cache.servers_route_via(room_id).ok(),
				},
			)
			.await?;

		let pub_key_map = RwLock::new(BTreeMap::new());

		// We do not add the event_id field to the pdu here because of signature and
		// hashes checks
		let Ok((event_id, value)) = gen_event_id_canonical_json(&response.event, &room_version_id) else {
			// Event could not be converted to canonical json
			return Err(Error::BadRequest(
				ErrorKind::InvalidParam,
				"Could not convert event to canonical json.",
			));
		};

		if *pdu.event_id != *event_id {
			warn!(
				"Server {} changed invite event, that's not allowed in the spec: ours: {:?}, theirs: {:?}",
				user_id.server_name(),
				pdu_json,
				value
			);
		}

		let origin: OwnedServerName = serde_json::from_value(
			serde_json::to_value(
				value
					.get("origin")
					.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event needs an origin field."))?,
			)
			.expect("CanonicalJson is valid json value"),
		)
		.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Origin field is invalid."))?;

		services
🥺's avatar
🥺 committed
			.fetch_required_signing_keys([&value], &pub_key_map)
			.await?;
		let pdu_id: Vec<u8> = services
			.rooms
			.event_handler
Jason Volk's avatar
Jason Volk committed
			.handle_incoming_pdu(&origin, room_id, &event_id, value, true, &pub_key_map)
			.await?
			.ok_or(Error::BadRequest(
				ErrorKind::InvalidParam,
				"Could not accept incoming PDU as timeline event.",
			))?;

		services.sending.send_pdu_room(room_id, &pdu_id)?;
	if !services.rooms.state_cache.is_joined(sender_user, room_id)? {
		return Err(Error::BadRequest(
			"You don't have permission to view this room.",
		));
	}

	let state_lock = services.rooms.state.mutex.lock(room_id).await;
	services
		.rooms
		.timeline
		.build_and_append_pdu(
			PduBuilder {
				event_type: TimelineEventType::RoomMember,
				content: to_raw_value(&RoomMemberEventContent {
					membership: MembershipState::Invite,
					displayname: services.users.displayname(user_id)?,
					avatar_url: services.users.avatar_url(user_id)?,
					is_direct: Some(is_direct),
					third_party_invite: None,
					blurhash: services.users.blurhash(user_id)?,
					reason,
					join_authorized_via_users_server: None,
				})
				.expect("event is valid, we just created it"),
				unsigned: None,
				state_key: Some(user_id.to_string()),
				redacts: None,
			},
			sender_user,
			room_id,
			&state_lock,
		)
		.await?;

	drop(state_lock);

	Ok(())
// Make a user leave all their joined rooms, forgets all rooms, and ignores
// errors
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
	let all_rooms = services
		.rooms
		.state_cache
		.rooms_joined(user_id)
🥺's avatar
🥺 committed
		.chain(
			services
🥺's avatar
🥺 committed
				.rooms
				.state_cache
				.rooms_invited(user_id)
				.map(|t| t.map(|(r, _)| r)),
		)
		.collect::<Vec<_>>();

	for room_id in all_rooms {
🥺's avatar
🥺 committed
		let Ok(room_id) = room_id else {
			continue;
🥺's avatar
🥺 committed
		// ignore errors
		if let Err(e) = leave_room(services, user_id, &room_id, None).await {
			warn!(%room_id, %user_id, %e, "Failed to leave room");
		}
		if let Err(e) = services.rooms.state_cache.forget(&room_id, user_id) {
			warn!(%room_id, %user_id, %e, "Failed to forget room");
pub async fn leave_room(services: &Services, user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> {
	// Ask a remote server if we don't have this room
	if !services
		.server_in_room(services.globals.server_name(), room_id)?
		if let Err(e) = remote_leave_room(services, user_id, room_id).await {
			warn!("Failed to leave room {} remotely: {}", user_id, e);
			// Don't tell the client about this error
		}

		let last_state = services
			.rooms
			.state_cache
			.invite_state(user_id, room_id)?
			.map_or_else(|| services.rooms.state_cache.left_state(user_id, room_id), |s| Ok(Some(s)))?;

		// We always drop the invite, we can't rely on other servers
		services.rooms.state_cache.update_membership(
🥺's avatar
🥺 committed
			room_id,
			user_id,
			RoomMemberEventContent::new(MembershipState::Leave),
			user_id,
			last_state,
			None,
			true,
		)?;
		let state_lock = services.rooms.state.mutex.lock(room_id).await;

		let member_event =
			services
🥺's avatar
🥺 committed
				.rooms
				.state_accessor
				.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?;

		// Fix for broken rooms
		let member_event = match member_event {
			None => {
				error!("Trying to leave a room you are not a member of.");

				services.rooms.state_cache.update_membership(
🥺's avatar
🥺 committed
					room_id,
					user_id,
					RoomMemberEventContent::new(MembershipState::Leave),
					user_id,
					None,
					None,
					true,
				)?;
				return Ok(());
			},
			Some(e) => e,
		};

		let mut event: RoomMemberEventContent = serde_json::from_str(member_event.content.get()).map_err(|e| {
			error!("Invalid room member event in database: {}", e);
			Error::bad_database("Invalid member event in database.")
		})?;

		event.membership = MembershipState::Leave;
		event.reason = reason;

		services
			.rooms
			.timeline
			.build_and_append_pdu(
				PduBuilder {
					event_type: TimelineEventType::RoomMember,
					content: to_raw_value(&event).expect("event is valid, we just created it"),
					unsigned: None,
					state_key: Some(user_id.to_string()),
					redacts: None,
				},
				user_id,
				room_id,
				&state_lock,
			)
			.await?;
	}

	Ok(())
async fn remote_leave_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<()> {
	let mut make_leave_response_and_server = Err!(BadServerResponse("No server available to assist in leaving."));
	let invite_state = services
		.rooms
		.state_cache
		.invite_state(user_id, room_id)?
		.ok_or(Error::BadRequest(ErrorKind::BadState, "User is not invited."))?;

	let mut servers: HashSet<OwnedServerName> = services
🥺's avatar
🥺 committed
		.rooms
		.state_cache
		.servers_invite_via(room_id)
		.filter_map(Result::ok)
		.collect();

	servers.extend(
		invite_state
			.iter()
			.filter_map(|event| serde_json::from_str(event.json().get()).ok())
			.filter_map(|event: serde_json::Value| event.get("sender").cloned())
			.filter_map(|sender| sender.as_str().map(ToOwned::to_owned))
			.filter_map(|sender| UserId::parse(sender).ok())
Jason Volk's avatar
Jason Volk committed
			.map(|user| user.server_name().to_owned()),
	debug!("servers in remote_leave_room: {servers:?}");

	for remote_server in servers {
		let make_leave_response = services
			.sending
			.send_federation_request(
				&remote_server,
				federation::membership::prepare_leave_event::v1::Request {
					room_id: room_id.to_owned(),
					user_id: user_id.to_owned(),
				},
			)
			.await;

		make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server));

		if make_leave_response_and_server.is_ok() {
			break;
		}
	}

	let (make_leave_response, remote_server) = make_leave_response_and_server?;

	let room_version_id = match make_leave_response.room_version {
🥺's avatar
🥺 committed
		Some(version)
			if services
🥺's avatar
🥺 committed
				.globals
				.supported_room_versions()
				.contains(&version) =>
		{
			version
		},
		_ => return Err!(BadServerResponse("Room version is not supported")),
	};

	let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(make_leave_response.event.get())
		.map_err(|e| err!(BadServerResponse("Invalid make_leave event json received from server: {e:?}")))?;

	// TODO: Is origin needed?
	leave_event_stub.insert(
		"origin".to_owned(),
		CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
	);
	leave_event_stub.insert(
		"origin_server_ts".to_owned(),
		CanonicalJsonValue::Integer(
🥺's avatar
🥺 committed
			utils::millis_since_unix_epoch()
				.try_into()
				.expect("Timestamp is valid js_int value"),

	// room v3 and above removed the "event_id" field from remote PDU format
	match room_version_id {
		RoomVersionId::V1 | RoomVersionId::V2 => {},
		_ => {
			leave_event_stub.remove("event_id");
		},
	};

	// In order to create a compatible ref hash (EventID) the `hashes` field needs
	// to be present
	ruma::signatures::hash_and_sign_event(
		services.globals.server_name().as_str(),
		services.globals.keypair(),
		&mut leave_event_stub,
		&room_version_id,
	)
	.expect("event is valid, we just created it");

	// Generate event id
	let event_id = EventId::parse(format!(
		"${}",
		ruma::signatures::reference_hash(&leave_event_stub, &room_version_id)
			.expect("ruma can calculate reference hashes")
	))
	.expect("ruma's reference hashes are valid event ids");

	// Add event_id back
	leave_event_stub.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned()));

	// It has enough fields to be called a proper event now
	let leave_event = leave_event_stub;

	services
		.sending
		.send_federation_request(
			&remote_server,
			federation::membership::create_leave_event::v2::Request {
				room_id: room_id.to_owned(),
				event_id,
				pdu: services
					.sending
					.convert_to_outgoing_federation_event(leave_event.clone()),