Skip to content
Snippets Groups Projects
membership.rs 49 KiB
Newer Older
				})
				.collect(),
			_ => Vec::new(),
		};

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

		let mut authorized_user: 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)
					.await
					.unwrap_or(false)
				{
					authorized_user = 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(),
			join_authorized_via_users_server: authorized_user,
		};

		// Try normal join first
		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()
🥺's avatar
🥺 committed
			&& servers
				.iter()
				.any(|server_name| !server_is_ours(server_name))
		{
			info!(
				"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(sender_user, room_id, servers).await?;

			let room_version_id = match make_join_response.room_version {
🥺's avatar
🥺 committed
				Some(room_version_id)
					if services()
						.globals
						.supported_room_versions()
						.contains(&room_version_id) =>
				{
					room_version_id
				},
				_ => return Err(Error::BadServerResponse("Room version is not supported")),
			};
			let mut join_event_stub: CanonicalJsonObject = serde_json::from_str(make_join_response.event.get())
				.map_err(|_| Error::BadServerResponse("Invalid make_join event json received from server."))?;
			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)?,
					reason,
					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: PduEvent::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());
🥺's avatar
🥺 committed
				services()
					.rooms
					.event_handler
					.fetch_required_signing_keys([&signed_value], &pub_key_map)
					.await?;
				services()
					.rooms
					.event_handler
Jason Volk's avatar
Jason Volk committed
					.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(
	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(Error::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 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(),
				},
			)
			.await;

		trace!("make_join response: {:?}", make_join_response);
		make_join_counter = make_join_counter.saturating_add(1);

		if let Err(ref e) = make_join_response {
			trace!("make_join ErrorKind string: {:?}", e.error_code().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
			if e.error_code()
				.to_string()
				.contains("M_INCOMPATIBLE_ROOM_VERSION")
				|| e.error_code()
					.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(Error::BadServerResponse("Room version is not supported by Conduwuit"));
				return make_join_response_and_server;
			}

			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(Error::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
async fn validate_and_add_event_id(
	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| {
		error!("Invalid PDU in server response: {:?}: {:?}", pdu, e);
		Error::BadServerResponse("Invalid PDU in server response")
	})?;
	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");

🥺's avatar
🥺 committed
		match services()
			.globals
			.bad_event_ratelimiter
			.write()
			.await
			.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));
🥺's avatar
🥺 committed
	if let Some((time, tries)) = services()
		.globals
		.bad_event_ratelimiter
		.read()
		.await
		.get(&event_id)
	{
		// Exponential backoff
		const MAX_DURATION: Duration = Duration::from_secs(60 * 60 * 24);
		let min_elapsed_duration = cmp::min(MAX_DURATION, Duration::from_secs(5 * 60) * (*tries) * (*tries));

		if time.elapsed() < min_elapsed_duration {
			debug!("Backing off from {}", event_id);
			return Err(Error::BadServerResponse("bad event, still backing off"));
		}
	}

	if let Err(e) = ruma::signatures::verify_event(&*pub_key_map.read().await, &value, room_version) {
		warn!("Event {} failed verification {:?} {}", event_id, pdu, e);
		return Err(Error::BadServerResponse("Event failed verification."));
	}

	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(
	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 !user_is_local(user_id) {
		let (pdu, pdu_json, invite_room_state) = {
🥺's avatar
🥺 committed
			let mutex_state = Arc::clone(
				services()
					.globals
					.roomid_mutex_state
					.write()
					.await
					.entry(room_id.to_owned())
					.or_default(),
			);
			let state_lock = mutex_state.lock().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: PduEvent::convert_to_outgoing_federation_event(pdu_json.clone()),
					invite_room_state,
🥺's avatar
🥺 committed
					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."))?;

🥺's avatar
🥺 committed
		services()
			.rooms
			.event_handler
			.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)?;
🥺's avatar
🥺 committed
	if !services()
		.rooms
		.state_cache
		.is_joined(sender_user, room_id)?
	{
		return Err(Error::BadRequest(
			"You don't have permission to view this room.",
		));
	}

🥺's avatar
🥺 committed
	let mutex_state = Arc::clone(
		services()
			.globals
			.roomid_mutex_state
			.write()
			.await
			.entry(room_id.to_owned())
			.or_default(),
	);
	let state_lock = mutex_state.lock().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(crate) async fn leave_all_rooms(user_id: &UserId) {
	let all_rooms = services()
		.rooms
		.state_cache
		.rooms_joined(user_id)
🥺's avatar
🥺 committed
		.chain(
			services()
				.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
		_ = services().rooms.state_cache.forget(&room_id, user_id);
🥺's avatar
🥺 committed
		_ = leave_room(user_id, &room_id, None).await;
pub(crate) async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> {
	// Ask a remote server if we don't have this room
	if !services()
		.rooms
		.state_cache
		.server_in_room(services().globals.server_name(), room_id)?
	{
		if let Err(e) = remote_leave_room(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
🥺's avatar
🥺 committed
		services().rooms.state_cache.update_membership(
			room_id,
			user_id,
			RoomMemberEventContent::new(MembershipState::Leave),
			user_id,
			last_state,
			None,
			true,
		)?;
🥺's avatar
🥺 committed
		let mutex_state = Arc::clone(
			services()
				.globals
				.roomid_mutex_state
				.write()
				.await
				.entry(room_id.to_owned())
				.or_default(),
		);
		let state_lock = mutex_state.lock().await;

		let member_event =
🥺's avatar
🥺 committed
			services()
				.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.");

🥺's avatar
🥺 committed
				services().rooms.state_cache.update_membership(
					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(())
Timo Kösters's avatar
Timo Kösters committed
async fn remote_leave_room(user_id: &UserId, room_id: &RoomId) -> Result<()> {
	let mut make_leave_response_and_server = Err(Error::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."))?;

🥺's avatar
🥺 committed
	let servers: HashSet<OwnedServerName> = services()
		.rooms
		.state_cache
		.servers_invite_via(room_id)?
		.map_or(
			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())
				.map(|user| user.server_name().to_owned())
				.collect::<HashSet<OwnedServerName>>(),
			HashSet::from_iter,
		);
	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()
				.globals
				.supported_room_versions()
				.contains(&version) =>
		{
			version
		},
		_ => return Err(Error::BadServerResponse("Room version is not supported")),
	};

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

	// 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: PduEvent::convert_to_outgoing_federation_event(leave_event.clone()),
			},
		)
		.await?;

	Ok(())