1use axum::extract::State;
2use base64::{Engine as _, engine::general_purpose};
3use futures::StreamExt;
4use ruma::{
5 CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
6 ServerName, UserId,
7 api::{
8 appservice::event::push_events,
9 error::{ErrorKind, IncompatibleRoomVersionErrorData},
10 federation::membership::create_invite,
11 },
12 events::{
13 AnyStrippedStateEvent, GlobalAccountDataEventType, StateEventType,
14 push_rules::PushRulesEvent,
15 room::member::{MembershipState, RoomMemberEventContent},
16 },
17 push,
18 serde::{JsonObject, Raw},
19};
20use tuwunel_core::{
21 Err, Error, Result, debug_warn, err,
22 matrix::{Event, PduCount, PduEvent, event::gen_event_id},
23 utils,
24 utils::hash::sha256,
25};
26use tuwunel_service::{
27 Services,
28 membership::{
29 StrippedCreateVerdict, enforce_stripped_create, into_client_stripped, v12_room_ids,
30 },
31};
32
33use crate::{ClientIp, Ruma};
34
35#[tracing::instrument(skip_all, fields(%client), name = "invite")]
39pub(crate) async fn create_invite_route(
40 State(services): State<crate::State>,
41 ClientIp(client): ClientIp,
42 body: Ruma<create_invite::v2::Request>,
43) -> Result<create_invite::v2::Response> {
44 validate_request(&services, &body).await?;
45
46 enforce_stripped_state(&services, &body).await?;
47
48 let (mut signed_event, invited_user) = parse_and_validate_event(&services, &body).await?;
49
50 sign_event(&services, &mut signed_event, &body.room_version)?;
51
52 let sender = validate_origins(&signed_event, body.origin())?;
53
54 check_invite_permitted(&services, &body, &invited_user).await?;
55
56 let pdu = build_pdu(&body)?;
57
58 let invite_state: Vec<_> = body
59 .invite_room_state
60 .clone()
61 .into_iter()
62 .filter_map(|state| into_client_stripped(&body.room_id, state))
63 .chain([pdu.to_format()])
64 .collect();
65
66 let _federation_lock = services
69 .event_handler
70 .mutex_federation
71 .lock(&body.room_id)
72 .await;
73
74 if !services
75 .state_cache
76 .server_in_room(services.globals.server_name(), &body.room_id)
77 .await
78 {
79 record_local_invite(&services, &body, &invited_user, sender, invite_state, &pdu).await?;
80 }
81
82 Ok(create_invite::v2::Response {
83 event: services
84 .federation
85 .format_pdu_into(signed_event, Some(&body.room_version))
86 .await,
87 })
88}
89
90async fn validate_request(
91 services: &Services,
92 body: &Ruma<create_invite::v2::Request>,
93) -> Result<()> {
94 services
95 .event_handler
96 .acl_check(body.origin(), &body.room_id)
97 .await?;
98
99 if !services
100 .config
101 .supported_room_version(&body.room_version)
102 {
103 return Err(Error::BadRequest(
104 ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData::new(
105 body.room_version.clone(),
106 )),
107 "Server does not support this room version.",
108 ));
109 }
110
111 if let Some(server) = body.room_id.server_name()
112 && services
113 .config
114 .is_forbidden_remote_server_name(server)
115 {
116 return Err!(Request(Forbidden("Server is banned on this homeserver.")));
117 }
118
119 Ok(())
120}
121
122async fn enforce_stripped_state(
125 services: &Services,
126 body: &Ruma<create_invite::v2::Request>,
127) -> Result<()> {
128 let verdict = services
129 .membership
130 .validate_stripped_create(&body.invite_room_state, &body.room_id, &body.room_version)
131 .await?;
132
133 if verdict != StrippedCreateVerdict::Valid {
134 debug_warn!(
135 ?verdict,
136 room_id = %body.room_id,
137 "MSC4311 invite create-event validation failed",
138 );
139 }
140
141 if enforce_stripped_create(
142 verdict,
143 v12_room_ids(&body.room_version),
144 services
145 .config
146 .enforce_stripped_state_pdu_validation,
147 ) {
148 return Err!(Request(MissingParam(
149 "The invite's m.room.create event is missing or does not validate for this room."
150 )));
151 }
152
153 Ok(())
154}
155
156async fn parse_and_validate_event(
157 services: &Services,
158 body: &Ruma<create_invite::v2::Request>,
159) -> Result<(CanonicalJsonObject, OwnedUserId)> {
160 let signed_event = utils::to_canonical_object(&body.event)
161 .map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
162
163 let room_id: OwnedRoomId = signed_event
164 .get("room_id")
165 .try_into()
166 .map(RoomId::to_owned)
167 .map_err(|e| err!(Request(InvalidParam("Invalid room_id property: {e}"))))?;
168
169 if body.room_id != room_id {
170 return Err!(Request(InvalidParam("Event room_id does not match the request path.")));
171 }
172
173 let kind: StateEventType = signed_event
174 .get("type")
175 .and_then(CanonicalJsonValue::as_str)
176 .ok_or_else(|| err!(Request(BadJson("Missing type in event."))))?
177 .into();
178
179 if kind != StateEventType::RoomMember {
180 return Err!(Request(InvalidParam("Event must be m.room.member type.")));
181 }
182
183 let invited_user: OwnedUserId = signed_event
184 .get("state_key")
185 .try_into()
186 .map(UserId::to_owned)
187 .map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
188
189 if !services.globals.user_is_local(&invited_user) {
190 return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
191 }
192
193 if services
194 .users
195 .invites_blocked(&invited_user)
196 .await
197 {
198 return Err!(Request(InviteBlocked("{invited_user} has blocked invites.")));
199 }
200
201 let content: RoomMemberEventContent = signed_event
202 .get("content")
203 .cloned()
204 .map(Into::into)
205 .map(serde_json::from_value)
206 .transpose()
207 .map_err(|e| err!(Request(InvalidParam("Invalid content object in event: {e}"))))?
208 .ok_or_else(|| err!(Request(BadJson("Missing content in event."))))?;
209
210 if content.membership != MembershipState::Invite {
211 return Err!(Request(InvalidParam("Event membership must be invite.")));
212 }
213
214 services
215 .event_handler
216 .acl_check(invited_user.server_name(), &body.room_id)
217 .await?;
218
219 Ok((signed_event, invited_user))
220}
221
222fn sign_event(
223 services: &Services,
224 signed_event: &mut CanonicalJsonObject,
225 room_version: &RoomVersionId,
226) -> Result<()> {
227 services
228 .server_keys
229 .hash_and_sign_event(signed_event, room_version)
230 .map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
231
232 let event_id = gen_event_id(signed_event, room_version)?;
233 signed_event.insert("event_id".into(), CanonicalJsonValue::String(event_id.to_string()));
234
235 Ok(())
236}
237
238fn validate_origins<'a>(
239 signed_event: &'a CanonicalJsonObject,
240 body_origin: &ServerName,
241) -> Result<&'a UserId> {
242 let origin: Option<&str> = signed_event
243 .get("origin")
244 .and_then(CanonicalJsonValue::as_str);
245
246 let sender: &UserId = signed_event
247 .get("sender")
248 .try_into()
249 .map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
250
251 if sender.server_name() != body_origin {
252 return Err!(Request(Forbidden("Can only send invites on behalf of your users.")));
253 }
254
255 if origin.is_some_and(|origin| origin != sender.server_name()) {
256 return Err!(Request(Forbidden("Your users can only be from your origin.")));
257 }
258
259 if origin.is_some_and(|origin| origin != body_origin) {
260 return Err!(Request(Forbidden("Can only send events from your origin.")));
261 }
262
263 Ok(sender)
264}
265
266async fn check_invite_permitted(
267 services: &Services,
268 body: &Ruma<create_invite::v2::Request>,
269 invited_user: &UserId,
270) -> Result<()> {
271 if services.metadata.is_banned(&body.room_id).await
272 && !services.admin.user_is_admin(invited_user).await
273 {
274 return Err!(Request(Forbidden("This room is banned on this homeserver.")));
275 }
276
277 if services.config.block_non_admin_invites
278 && !services.admin.user_is_admin(invited_user).await
279 {
280 return Err!(Request(Forbidden("This server does not allow room invites.")));
281 }
282
283 Ok(())
284}
285
286fn build_pdu(body: &Ruma<create_invite::v2::Request>) -> Result<PduEvent> {
287 let mut event: JsonObject = serde_json::from_str(body.event.get())
288 .map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
289
290 event.insert("event_id".into(), "$placeholder".into());
291
292 serde_json::from_value(event.into())
293 .map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))
294}
295
296async fn record_local_invite(
303 services: &Services,
304 body: &Ruma<create_invite::v2::Request>,
305 invited_user: &UserId,
306 sender: &UserId,
307 invite_state: Vec<Raw<AnyStrippedStateEvent>>,
308 pdu: &PduEvent,
309) -> Result<()> {
310 if services
311 .state_accessor
312 .room_state_get_content::<RoomMemberEventContent>(
313 &body.room_id,
314 &StateEventType::RoomMember,
315 invited_user.as_str(),
316 )
317 .await
318 .is_ok_and(|content| content.membership == MembershipState::Ban)
319 {
320 debug_warn!(
321 room_id = %body.room_id,
322 user_id = %invited_user,
323 "Recording invite while local room state shows banned membership.",
324 );
325 }
326
327 let count = services.globals.next_count();
328 services
329 .state_cache
330 .update_membership(
331 &body.room_id,
332 invited_user,
333 RoomMemberEventContent::new(MembershipState::Invite),
334 sender,
335 Some(invite_state),
336 body.via.clone(),
337 true,
338 PduCount::Normal(*count),
339 )
340 .await?;
341 drop(count);
342
343 notify_pushers(services, invited_user, pdu).await;
344
345 for appservice in services.appservice.read().await.values() {
346 if appservice.is_user_match(invited_user) {
347 services
348 .appservice
349 .send_request(appservice.registration.clone(), push_events::v1::Request {
350 events: vec![pdu.to_format()],
351 txn_id: general_purpose::URL_SAFE_NO_PAD
352 .encode(sha256::hash(pdu.event_id.as_bytes()))
353 .into(),
354 ephemeral: Vec::new(),
355 to_device: Vec::new(),
356 })
357 .await?;
358 }
359 }
360
361 Ok(())
362}
363
364async fn notify_pushers(services: &Services, invited_user: &UserId, pdu: &PduEvent) {
365 services
366 .pusher
367 .get_pushkeys(invited_user)
368 .map(ToOwned::to_owned)
369 .for_each(async |pushkey| {
370 let Ok(pusher) = services
371 .pusher
372 .get_pusher(invited_user, &pushkey)
373 .await
374 else {
375 return;
376 };
377
378 let ruleset = services
379 .account_data
380 .get_global(invited_user, GlobalAccountDataEventType::PushRules)
381 .await
382 .map_or_else(
383 |_| push::Ruleset::server_default(invited_user),
384 |ev: PushRulesEvent| ev.content.global,
385 );
386
387 services
388 .pusher
389 .send_push_notice(invited_user, &pusher, &ruleset, pdu)
390 .await
391 .ok();
392 })
393 .await;
394}