Compare commits

..

24 Commits

Author SHA1 Message Date
Jade Ellis 082c44f355 fix: Only sync LDAP admin status when admin_filter is configured
Closes #1307
2026-02-15 16:17:26 +00:00
Jade Ellis 117c581948 fix: Correct incorrectly inverted boolean expression 2026-02-15 16:11:19 +00:00
timedout cb846a3ad1 style: Invert pending_invite_state check 2026-02-15 16:11:19 +00:00
timedout 81b984b2cc style: Compress should_rescind_invite 2026-02-15 16:11:19 +00:00
timedout e2961390ee feat: Support rescinding invites over federation 2026-02-15 16:11:19 +00:00
timedout cb75e836e0 style: Update error messages in make_join.rs 2026-02-15 16:11:19 +00:00
nexy7574 cb7a988b1b chore: Add news frag 2026-02-15 16:11:19 +00:00
nexy7574 aa5400bcef style: Fix IncompatibleRoomVersion log line 2026-02-15 16:11:18 +00:00
nexy7574 ff4dddd673 fix: Refactor local join process 2026-02-15 16:11:18 +00:00
nexy7574 c22b17fb29 fix: Return accurate errors in make_join for restricted rooms 2026-02-15 16:11:18 +00:00
timedout 3da7fa24db fix: Produce more useful errors in make_join_request 2026-02-15 16:11:18 +00:00
timedout d15ac1d3c1 fix: Use 404 instead of 400 (and include sender) 2026-02-15 15:55:36 +00:00
timedout a9ebdf58e2 feat: Filter ignored PDUs in relations 2026-02-15 15:55:35 +00:00
timedout f1ab27d344 feat: Return SENDER_IGNORED error for context and relations 2026-02-15 15:55:35 +00:00
timedout 8bc6e6ccca feat: Return SENDER_IGNORED error in is_ignored_pdu 2026-02-15 15:55:32 +00:00
Jade Ellis 60a3abe752 refactor: Use HashSet 2026-02-15 15:35:29 +00:00
Ellie e3b874d336 fix(sync): handle wildcard state keys in sliding sync required_state 2026-02-15 15:35:29 +00:00
Jade Ellis f3f82831b4 docs: Changelog 2026-02-15 15:23:15 +00:00
Jade Ellis 26aac1408e fix: Correct user agent changes
Correct the domain
Remove "embed" in the UA because the
global UA was modified, rather than
just the one for preview requests
2026-02-15 15:21:06 +00:00
Trash Panda be8f62396a feat(core): Change default user agent 2026-02-15 15:21:06 +00:00
Trash Panda 40996a6602 feat(core): Add config option for the url preview user agent 2026-02-15 15:21:05 +00:00
Jade Ellis 9cae531f90 doc: Changelog 2026-02-15 15:19:03 +00:00
Jade Ellis 56eea935b6 feat: Deadlock detector thread 2026-02-15 15:19:02 +00:00
Renovate Bot fcb646f8c4 chore(deps): update rust-patch-updates 2026-02-15 05:02:30 +00:00
30 changed files with 479 additions and 431 deletions
Generated
+38 -43
View File
@@ -841,7 +841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
dependencies = [
"serde",
"toml 0.9.11+spec-1.1.0",
"toml 0.9.12+spec-1.1.0",
]
[[package]]
@@ -917,9 +917,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.57"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
dependencies = [
"clap_builder",
"clap_derive",
@@ -927,9 +927,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.57"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
dependencies = [
"anstyle",
"clap_lex",
@@ -949,9 +949,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "cmake"
@@ -1030,6 +1030,7 @@ dependencies = [
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry_sdk",
"parking_lot",
"sentry",
"sentry-tower",
"sentry-tracing",
@@ -1149,14 +1150,14 @@ dependencies = [
"serde_json",
"serde_regex",
"smallstr",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"tikv-jemalloc-ctl",
"tikv-jemalloc-sys",
"tikv-jemallocator",
"tokio",
"tokio-metrics",
"toml 0.9.11+spec-1.1.0",
"toml 0.9.12+spec-1.1.0",
"tracing",
"tracing-core",
"tracing-subscriber",
@@ -1917,7 +1918,7 @@ dependencies = [
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec 1.15.1",
"smallvec",
"zune-inflate",
]
@@ -2152,7 +2153,7 @@ checksum = "3a74b56a4039a46e8c91cc9d84e8a7df4e1f8b24239ca57d1304b3263cb599b9"
dependencies = [
"compact_str",
"garde_derive",
"smallvec 1.15.1",
"smallvec",
]
[[package]]
@@ -2364,7 +2365,7 @@ dependencies = [
"rand 0.9.2",
"resolv-conf",
"serde",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -2482,7 +2483,7 @@ dependencies = [
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec 1.15.1",
"smallvec",
"tokio",
"want",
]
@@ -2577,7 +2578,7 @@ dependencies = [
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec 1.15.1",
"smallvec",
"zerovec",
]
@@ -2635,7 +2636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec 1.15.1",
"smallvec",
"utf8_iter",
]
@@ -2920,9 +2921,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.180"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libfuzzer-sys"
@@ -3224,7 +3225,7 @@ dependencies = [
"equivalent",
"parking_lot",
"portable-atomic",
"smallvec 1.15.1",
"smallvec",
"tagptr",
"uuid",
]
@@ -3721,7 +3722,7 @@ dependencies = [
"libc",
"petgraph",
"redox_syscall",
"smallvec 1.15.1",
"smallvec",
"windows-link",
]
@@ -4466,7 +4467,7 @@ dependencies = [
"serde",
"serde_html_form",
"serde_json",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"time",
"tracing",
@@ -4493,7 +4494,7 @@ dependencies = [
"ruma-macros",
"serde",
"serde_json",
"smallvec 1.15.1",
"smallvec",
"thiserror 2.0.18",
"tracing",
"url",
@@ -4751,12 +4752,12 @@ dependencies = [
[[package]]
name = "saphyr-parser-bw"
version = "0.0.607"
version = "0.0.608"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f9bae8d059bf1ca32753cf3cdafbf5d391502de2fc2ca54510811fe9c100d90"
checksum = "d55ae5ea09894b6d5382621db78f586df37ef18ab581bf32c754e75076b124b1"
dependencies = [
"arraydeque",
"smallvec 2.0.0-alpha.12",
"smallvec",
"thiserror 2.0.18",
]
@@ -4963,13 +4964,13 @@ dependencies = [
[[package]]
name = "serde-saphyr"
version = "0.0.17"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc14a55107113a16346915d7e3d78acc539a923458385db89670e22cac106d7a"
checksum = "191a4f997fef5e095212c5790898516e9567d2d8502c4159317603ff0321e394"
dependencies = [
"ahash",
"annotate-snippets",
"base64 0.21.7",
"base64 0.22.1",
"encoding_rs_io",
"figment",
"garde",
@@ -4981,7 +4982,7 @@ dependencies = [
"saphyr-parser-bw",
"serde",
"serde_json",
"smallvec 2.0.0-alpha.12",
"smallvec",
"validator",
"zmij",
]
@@ -5195,7 +5196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862077b1e764f04c251fe82a2ef562fd78d7cadaeb072ca7c2bcaf7217b1ff3b"
dependencies = [
"serde",
"smallvec 1.15.1",
"smallvec",
]
[[package]]
@@ -5207,12 +5208,6 @@ dependencies = [
"serde",
]
[[package]]
name = "smallvec"
version = "2.0.0-alpha.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857"
[[package]]
name = "socket2"
version = "0.5.10"
@@ -5330,9 +5325,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "2.0.114"
version = "2.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
dependencies = [
"proc-macro2",
"quote",
@@ -5657,9 +5652,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.11+spec-1.1.0"
version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"indexmap",
"serde_core",
@@ -5716,9 +5711,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.6+spec-1.1.0"
version = "1.0.8+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
dependencies = [
"winnow",
]
@@ -5903,7 +5898,7 @@ checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
dependencies = [
"js-sys",
"opentelemetry",
"smallvec 1.15.1",
"smallvec",
"tracing",
"tracing-core",
"tracing-log",
@@ -5922,7 +5917,7 @@ dependencies = [
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec 1.15.1",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
+1 -1
View File
@@ -158,7 +158,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.17"
version = "0.0.18"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
+1
View File
@@ -0,0 +1 @@
LDAP-enabled servers will no longer have all admins demoted when LDAP-controlled admins are not configured. Contributed by @Jade
+2
View File
@@ -0,0 +1,2 @@
Added unstable support for [MSC4406: `M_SENDER_IGNORED`](https://github.com/matrix-org/matrix-spec-proposals/pull/4406).
Contributed by @nex
+1
View File
@@ -0,0 +1 @@
Continuwuity will now print information to the console when it detects a deadlock
+1
View File
@@ -0,0 +1 @@
Improved the handling of restricted join rules and improved the performance of local-first joins. Contributed by @nex.
+1
View File
@@ -0,0 +1 @@
You can now set a custom User Agent for URL previews; the default one has been modified to be less likely to be rejected. Contributed by @trashpanda
+4
View File
@@ -1474,6 +1474,10 @@
#
#url_preview_check_root_domain = false
# User agent that is used specifically when fetching url previews.
#
#url_preview_user_agent = "continuwuity/<version> (bot; +https://continuwuity.org)"
# List of forbidden room aliases and room IDs as strings of regex
# patterns.
#
+5 -20
View File
@@ -1,28 +1,13 @@
# Troubleshooting Continuwuity
:::warning{title="Docker users:"}
Docker can be difficult to use and debug. It's common for Docker
misconfigurations to cause issues, particularly with networking and permissions.
Please check that your issues are not due to problems with your Docker setup.
:::
> **Docker users ⚠️**
>
> Docker can be difficult to use and debug. It's common for Docker
> misconfigurations to cause issues, particularly with networking and permissions.
> Please check that your issues are not due to problems with your Docker setup.
## Continuwuity and Matrix issues
### Slow joins to rooms
Some slowness is to be expected if you're the first person on your homserver to join a room (which will
always be the case for single-user homeservers). In this situation, your homeserver has to verify the signatures of
all of the state events sent by other servers before your join. To make this process as fast as possible, make sure you have
multiple fast, trusted servers listed in `trusted_servers` in your configuration, and ensure
`query_trusted_key_servers_first_on_join` is set to true (the default).
If you need suggestions for trusted servers, ask in the Continuwuity main room.
However, _very_ slow joins, especially to rooms with only a few users in them or rooms created by another user
on your homeserver, may be caused by [issue !779](https://forgejo.ellis.link/continuwuation/continuwuity/issues/779),
which is a longstanding bug with synchronizing room joins to clients. In this situation, you did succeed in joining the room, but
the bug caused your homeserver to forget to tell your client. **To fix this, clear your client's cache.** Both Element and Cinny
have a button to clear their cache in the "About" section of their settings.
### Lost access to admin room
You can reinvite yourself to the admin room through the following methods:
+1 -5
View File
@@ -140,7 +140,6 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services.globals.server_name().to_owned(),
room_server_name.to_owned(),
],
None,
&None,
)
.await
@@ -549,7 +548,6 @@ pub(super) async fn force_join_list_of_local_users(
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
None,
&None,
)
.await
@@ -635,7 +633,6 @@ pub(super) async fn force_join_all_local_users(
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
None,
&None,
)
.await
@@ -675,8 +672,7 @@ pub(super) async fn force_join_room(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, None, &None)
.await?;
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, &None).await?;
self.write_str(&format!("{user_id} has been joined to {room_id}.",))
.await
-1
View File
@@ -583,7 +583,6 @@ pub(crate) async fn register_route(
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
None,
&body.appservice_info,
)
.boxed()
+7 -1
View File
@@ -16,7 +16,10 @@ use ruma::{OwnedEventId, UserId, api::client::context::get_context, events::Stat
use crate::{
Ruma,
client::message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
client::{
is_ignored_pdu,
message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
},
};
const LIMIT_MAX: usize = 100;
@@ -78,6 +81,9 @@ pub(crate) async fn get_context_route(
return Err!(Request(NotFound("Event not found.")));
}
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
is_ignored_pdu(&services, &base_pdu, sender_user).await?;
let base_count = base_id.pdu_count();
let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user);
+84 -268
View File
@@ -3,7 +3,7 @@ use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, error, info,
Err, Result, debug, debug_info, debug_warn, err, error, info, is_true,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
@@ -26,7 +26,7 @@ use ruma::{
api::{
client::{
error::ErrorKind,
membership::{ThirdPartySigned, join_room_by_id, join_room_by_id_or_alias},
membership::{join_room_by_id, join_room_by_id_or_alias},
},
federation::{self},
},
@@ -34,7 +34,7 @@ use ruma::{
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule},
join_rules::JoinRule,
member::{MembershipState, RoomMemberEventContent},
},
},
@@ -48,9 +48,13 @@ use service::{
timeline::pdu_fits,
},
};
use tokio::join;
use super::{banned_room_check, validate_remote_member_event_stub};
use crate::Ruma;
use crate::{
Ruma,
server::{select_authorising_user, user_can_perform_restricted_join},
};
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
///
@@ -116,7 +120,6 @@ pub(crate) async fn join_room_by_id_route(
&body.room_id,
body.reason.clone(),
&servers,
body.third_party_signed.as_ref(),
&body.appservice_info,
)
.boxed()
@@ -248,7 +251,6 @@ pub(crate) async fn join_room_by_id_or_alias_route(
&room_id,
body.reason.clone(),
&servers,
body.third_party_signed.as_ref(),
appservice_info,
)
.boxed()
@@ -263,7 +265,6 @@ pub async fn join_room_by_id_helper(
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
third_party_signed: Option<&ThirdPartySigned>,
appservice_info: &Option<RegistrationInfo>,
) -> Result<join_room_by_id::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
@@ -351,17 +352,9 @@ pub async fn join_room_by_id_helper(
}
if server_in_room {
join_room_by_id_helper_local(
services,
sender_user,
room_id,
reason,
servers,
third_party_signed,
state_lock,
)
.boxed()
.await?;
join_room_by_id_helper_local(services, sender_user, room_id, reason, servers, state_lock)
.boxed()
.await?;
} else {
// Ask a remote server if we are not participating in this room
join_room_by_id_helper_remote(
@@ -370,7 +363,6 @@ pub async fn join_room_by_id_helper(
room_id,
reason,
servers,
third_party_signed,
state_lock,
)
.boxed()
@@ -386,7 +378,6 @@ async fn join_room_by_id_helper_remote(
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard,
) -> Result {
info!("Joining {room_id} over federation.");
@@ -396,11 +387,10 @@ async fn join_room_by_id_helper_remote(
info!("make_join finished");
let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
let room_version_id = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
if !services.server.supported_room_version(&room_version_id) {
// How did we get here?
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
@@ -429,10 +419,6 @@ async fn join_room_by_id_helper_remote(
}
};
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(
@@ -744,87 +730,45 @@ async fn join_room_by_id_helper_local(
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard,
) -> Result {
debug_info!("We can join locally");
let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await;
info!("Joining room locally");
let mut restricted_join_authorized = None;
match join_rules {
| JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
for restriction in restricted.allow {
match restriction {
| AllowRule::RoomMembership(membership) => {
if services
.rooms
.state_cache
.is_joined(sender_user, &membership.room_id)
.await
{
restricted_join_authorized = Some(true);
break;
}
},
| AllowRule::UnstableSpamChecker => {
match services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.await
{
| Ok(()) => {
restricted_join_authorized = Some(true);
break;
},
| Err(_) =>
return Err!(Request(Forbidden(
"Antispam rejected join request."
))),
}
},
| _ => {},
}
let (room_version, join_rules, is_invited) = join!(
services.rooms.state.get_room_version(room_id),
services.rooms.state_accessor.get_join_rules(room_id),
services.rooms.state_cache.is_invited(sender_user, room_id)
);
let room_version = room_version?;
let mut auth_user: Option<OwnedUserId> = None;
if !is_invited && matches!(join_rules, JoinRule::Restricted(_) | JoinRule::KnockRestricted(_))
{
use RoomVersionId::*;
if !matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
// This is a restricted room, check if we can complete the join requirements
// locally.
let needs_auth_user =
user_can_perform_restricted_join(services, sender_user, room_id, &room_version)
.await;
if needs_auth_user.is_ok_and(is_true!()) {
// If there was an error or the value is false, we'll try joining over
// federation. Since it's Ok(true), we can authorise this locally.
// If we can't select a local user, this will remain None, the join will fail,
// and we'll fall back to federation.
auth_user = select_authorising_user(services, room_id, sender_user, &state_lock)
.await
.ok();
}
},
| _ => {},
}
let join_authorized_via_users_server = if restricted_join_authorized.is_none() {
None
} else {
match restricted_join_authorized.unwrap() {
| true => services
.rooms
.state_cache
.local_users_in_room(room_id)
.filter(|user| {
trace!("Checking if {user} can invite {sender_user} to {room_id}");
services.rooms.state_accessor.user_can_invite(
room_id,
user,
sender_user,
&state_lock,
)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned),
| false => {
warn!(
"Join authorization failed for restricted join in room {room_id} for user \
{sender_user}"
);
return Err!(Request(Forbidden("You are not authorized to join this room.")));
},
}
};
}
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
join_authorized_via_users_server,
join_authorized_via_users_server: auth_user,
..RoomMemberEventContent::new(MembershipState::Join)
};
@@ -840,6 +784,7 @@ async fn join_room_by_id_helper_local(
)
.await
else {
info!("Joined room locally");
return Ok(());
};
@@ -847,138 +792,13 @@ async fn join_room_by_id_helper_local(
return Err(error);
}
warn!(
info!(
?error,
servers = %servers.len(),
"Could not join restricted room locally, attempting remote join",
remote_servers = %servers.len(),
"Could not join room locally, attempting remote join",
);
let Ok((make_join_response, remote_server)) =
make_join_request(services, sender_user, room_id, servers).await
else {
return Err(error);
};
let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
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:?}"))
})?;
validate_remote_member_event_stub(
&MembershipState::Join,
sender_user,
room_id,
&join_event_stub,
)?;
let join_authorized_via_users_server = join_event_stub
.get("content")
.map(|s| {
s.as_object()?
.get("join_authorised_via_users_server")?
.as_str()
})
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok());
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(
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 {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join)
})
.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
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let join_event = join_event_stub;
let send_join_response = services
.sending
.send_synapse_request(
&remote_server,
federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
},
)
.await?;
if let Some(signed_raw) = send_join_response.room_state.event {
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(&signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if signed_event_id != event_id {
return Err!(Request(BadJson(
warn!(%signed_event_id, %event_id, "Server {remote_server} sent event with wrong event ID")
)));
}
drop(state_lock);
services
.rooms
.event_handler
.handle_incoming_pdu(&remote_server, room_id, &signed_event_id, signed_value, true)
.boxed()
.await?;
} else {
return Err(error);
}
Ok(())
join_room_by_id_helper_remote(services, sender_user, room_id, reason, servers, state_lock)
.await
}
async fn make_join_request(
@@ -987,17 +807,16 @@ async fn make_join_request(
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: usize = 0;
let mut incompatible_room_version_count: usize = 0;
let mut make_join_counter: usize = 1;
for remote_server in servers {
if services.globals.server_is_ours(remote_server) {
continue;
}
info!("Asking {remote_server} for make_join ({make_join_counter})");
info!(
"Asking {remote_server} for make_join (attempt {make_join_counter}/{})",
servers.len()
);
let make_join_response = services
.sending
.send_federation_request(
@@ -1025,47 +844,44 @@ async fn make_join_request(
warn!("make_join response from {remote_server} failed validation: {e}");
continue;
}
make_join_response_and_server = Ok((response, remote_server.clone()));
break;
return Ok((response, remote_server.clone()));
},
| Err(e) => {
info!("make_join request to {remote_server} failed: {e}");
if matches!(
e.kind(),
ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion
) {
incompatible_room_version_count =
incompatible_room_version_count.saturating_add(1);
}
if incompatible_room_version_count > 15 {
| Err(e) => match e.kind() {
| ErrorKind::UnableToAuthorizeJoin => {
info!(
"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
M_UNSUPPORTED_ROOM_VERSION, assuming that conduwuit does not support \
the room version {room_id}: {e}"
"{remote_server} was unable to verify the joining user satisfied \
restricted join requirements: {e}. Will continue trying."
);
make_join_response_and_server =
Err!(BadServerResponse("Room version is not supported by Conduwuit"));
return make_join_response_and_server;
}
if make_join_counter > 40 {
},
| ErrorKind::UnableToGrantJoin => {
info!(
"{remote_server} believes the joining user satisfies restricted join \
rules, but is unable to authorise a join for us. Will continue trying."
);
},
| ErrorKind::IncompatibleRoomVersion { room_version } => {
warn!(
"40 servers failed to provide valid make_join response, assuming no \
server can assist in joining."
"{remote_server} reports the room we are trying to join is \
v{room_version}, which we do not support."
);
make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
return make_join_response_and_server;
}
return Err(e);
},
| ErrorKind::Forbidden { .. } => {
warn!("{remote_server} refuses to let us join: {e}.");
return Err(e);
},
| ErrorKind::NotFound => {
info!(
"{remote_server} does not know about {room_id}: {e}. Will continue \
trying."
);
},
| _ => {
info!("{remote_server} failed to make_join: {e}. Will continue trying.");
},
},
}
if make_join_response_and_server.is_ok() {
break;
}
}
make_join_response_and_server
info!("All {} servers were unable to assist in joining {room_id} :(", servers.len());
Err!(BadServerResponse("No server available to assist in joining."))
}
-1
View File
@@ -253,7 +253,6 @@ async fn knock_room_by_id_helper(
room_id,
reason.clone(),
servers,
None,
&None,
)
.await
+21 -8
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, at, debug_warn,
Err, Error, Result, at, debug_warn,
matrix::{
event::{Event, Matches},
pdu::PduCount,
@@ -26,7 +26,7 @@ use ruma::{
DeviceId, RoomId, UserId,
api::{
Direction,
client::{filter::RoomEventFilter, message::get_message_events},
client::{error::ErrorKind, filter::RoomEventFilter, message::get_message_events},
},
events::{
AnyStateEvent, StateEventType,
@@ -279,23 +279,30 @@ pub(crate) async fn ignored_filter(
is_ignored_pdu(services, pdu, user_id)
.await
.unwrap_or(true)
.eq(&false)
.then_some(item)
}
/// Determine whether a PDU should be ignored for a given recipient user.
/// Returns True if this PDU should be ignored, returns False otherwise.
///
/// The error SenderIgnored is returned if the sender or the sender's server is
/// ignored by the relevant user. If the error cannot be returned to the user,
/// it should equate to a true value (i.e. ignored).
#[inline]
pub(crate) async fn is_ignored_pdu<Pdu>(
services: &Services,
event: &Pdu,
recipient_user: &UserId,
) -> bool
) -> Result<bool>
where
Pdu: Event + Send + Sync,
{
// exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this.
if event.kind().to_cow_str() == "org.matrix.dummy_event" {
return true;
return Ok(true);
}
let sender_user = event.sender();
@@ -310,21 +317,27 @@ where
if !type_ignored {
// We cannot safely ignore this type
return false;
return Ok(false);
}
if server_ignored {
// the sender's server is ignored, so ignore this event
return true;
return Err(Error::BadRequest(
ErrorKind::SenderIgnored { sender: None },
"The sender's server is ignored by this server.",
));
}
if user_ignored && !services.config.send_messages_from_ignored_users_to_client {
// the recipient of this PDU has the sender ignored, and we're not
// configured to send ignored messages to clients
return true;
return Err(Error::BadRequest(
ErrorKind::SenderIgnored { sender: Some(event.sender().to_owned()) },
"You have ignored this sender.",
));
}
false
Ok(false)
}
#[inline]
+25 -2
View File
@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Err, Result, at, debug_warn,
Err, Result, at, debug_warn, err,
matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
};
@@ -18,7 +18,7 @@ use ruma::{
events::{TimelineEventType, relation::RelationType},
};
use crate::Ruma;
use crate::{Ruma, client::is_ignored_pdu};
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
@@ -118,6 +118,14 @@ async fn paginate_relations_with_filter(
debug_warn!(req_evt = %target, %room_id, "Event relations requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}
let target_pdu = services
.rooms
.timeline
.get_pdu(target)
.await
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
is_ignored_pdu(services, &target_pdu, sender_user).await?;
let start: PduCount = from
.map(str::parse)
@@ -159,6 +167,7 @@ async fn paginate_relations_with_filter(
.ready_take_while(|(count, _)| Some(*count) != to)
.take(limit)
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
.wide_filter_map(|item| ignored_filter(services, item, sender_user))
.then(async |mut pdu| {
if let Err(e) = services
.rooms
@@ -214,3 +223,17 @@ async fn visibility_filter<Pdu: Event + Send + Sync>(
.await
.then_some(item)
}
async fn ignored_filter<Pdu: Event + Send + Sync>(
services: &Services,
item: (PduCount, Pdu),
sender_user: &UserId,
) -> Option<(PduCount, Pdu)> {
let (_, pdu) = &item;
if is_ignored_pdu(services, pdu, sender_user).await.ok()? {
None
} else {
Some(item)
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ pub(crate) async fn get_room_event_route(
let (mut event, visible) = try_join(event, visible).await?;
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await {
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await? {
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
+10 -6
View File
@@ -107,7 +107,7 @@ pub(super) async fn ldap_login(
) -> Result<OwnedUserId> {
let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() {
| Some(bind_dn) if bind_dn.contains("{username}") =>
(bind_dn.replace("{username}", lowercased_user_id.localpart()), false),
(bind_dn.replace("{username}", lowercased_user_id.localpart()), None),
| _ => {
debug!("Searching user in LDAP");
@@ -144,12 +144,16 @@ pub(super) async fn ldap_login(
.await?;
}
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
// Only sync admin status if LDAP can actually determine it.
// None means LDAP cannot determine admin status (manual config required).
if let Some(is_ldap_admin) = is_ldap_admin {
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
if is_ldap_admin && !is_conduwuit_admin {
Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?;
} else if !is_ldap_admin && is_conduwuit_admin {
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
if is_ldap_admin && !is_conduwuit_admin {
Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?;
} else if !is_ldap_admin && is_conduwuit_admin {
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
}
}
Ok(user_id)
+7 -1
View File
@@ -685,8 +685,15 @@ async fn collect_required_state(
required_state_request: &BTreeSet<TypeStateKey>,
) -> Vec<Raw<AnySyncStateEvent>> {
let mut required_state = Vec::new();
let mut wildcard_types: HashSet<&StateEventType> = HashSet::new();
for (event_type, state_key) in required_state_request {
if wildcard_types.contains(event_type) {
continue;
}
if state_key.as_str() == "*" {
wildcard_types.insert(event_type);
if let Ok(keys) = services
.rooms
.state_accessor
@@ -703,7 +710,6 @@ async fn collect_required_state(
required_state.push(Event::into_format(event));
}
}
break;
}
} else if let Ok(event) = services
.rooms
+93 -48
View File
@@ -16,6 +16,8 @@ use ruma::{
},
};
use serde_json::value::to_raw_value;
use service::rooms::state::RoomMutexGuard;
use tokio::join;
use crate::Ruma;
@@ -85,16 +87,24 @@ pub(crate) async fn create_join_event_template_route(
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let is_invited = services
.rooms
.state_cache
.is_invited(&body.user_id, &body.room_id)
.await;
let (is_invited, is_joined) = join!(
services
.rooms
.state_cache
.is_invited(&body.user_id, &body.room_id),
services
.rooms
.state_cache
.is_joined(&body.user_id, &body.room_id)
);
let join_authorized_via_users_server: Option<OwnedUserId> = {
use RoomVersionId::*;
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) || is_invited {
// room version does not support restricted join rules, or the user is currently
// already invited
if is_joined || is_invited {
// User is already joined or invited and consequently does not need an
// authorising user
None
} else if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
// room version does not support restricted join rules
None
} else if user_can_perform_restricted_join(
&services,
@@ -104,32 +114,10 @@ pub(crate) async fn create_join_event_template_route(
)
.await?
{
let Some(auth_user) = services
.rooms
.state_cache
.local_users_in_room(&body.room_id)
.filter(|user| {
services.rooms.state_accessor.user_can_invite(
&body.room_id,
user,
&body.user_id,
&state_lock,
)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned)
else {
info!(
"No local user is able to authorize the join of {} into {}",
&body.user_id, &body.room_id
);
return Err!(Request(UnableToGrantJoin(
"No user on this server is able to assist in joining."
)));
};
Some(auth_user)
Some(
select_authorising_user(&services, &body.room_id, &body.user_id, &state_lock)
.await?,
)
} else {
None
}
@@ -159,9 +147,7 @@ pub(crate) async fn create_join_event_template_route(
)
.await?;
drop(state_lock);
// room v3 and above removed the "event_id" field from remote PDU format
maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
pdu_json.remove("event_id");
Ok(prepare_join_event::v1::Response {
room_version: Some(room_version_id),
@@ -169,6 +155,38 @@ pub(crate) async fn create_join_event_template_route(
})
}
/// Attempts to find a user who is able to issue an invite in the target room.
pub(crate) async fn select_authorising_user(
services: &Services,
room_id: &RoomId,
user_id: &UserId,
state_lock: &RoomMutexGuard,
) -> Result<OwnedUserId> {
let auth_user = services
.rooms
.state_cache
.local_users_in_room(room_id)
.filter(|user| {
services
.rooms
.state_accessor
.user_can_invite(room_id, user, user_id, state_lock)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned);
match auth_user {
| Some(auth_user) => Ok(auth_user),
| None => {
Err!(Request(UnableToGrantJoin(
"No user on this server is able to assist in joining."
)))
},
}
}
/// Checks whether the given user can join the given room via a restricted join.
pub(crate) async fn user_can_perform_restricted_join(
services: &Services,
@@ -180,12 +198,9 @@ pub(crate) async fn user_can_perform_restricted_join(
// restricted rooms are not supported on <=v7
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
return Ok(false);
}
if services.rooms.state_cache.is_joined(user_id, room_id).await {
// joining user is already joined, there is nothing we need to do
return Ok(false);
// This should be impossible as it was checked earlier on, but retain this check
// for safety.
unreachable!("user_can_perform_restricted_join got incompatible room version");
}
let Ok(join_rules_event_content) = services
@@ -205,17 +220,31 @@ pub(crate) async fn user_can_perform_restricted_join(
let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) =
join_rules_event_content.join_rule
else {
// This is not a restricted room
return Ok(false);
};
if r.allow.is_empty() {
debug_info!("{room_id} is restricted but the allow key is empty");
return Ok(false);
// This will never be authorisable, return forbidden.
return Err!(Request(Forbidden("You are not invited to this room.")));
}
let mut could_satisfy = true;
for allow_rule in &r.allow {
match allow_rule {
| AllowRule::RoomMembership(membership) => {
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &membership.room_id)
.await
{
// Since we can't check this room, mark could_satisfy as false
// so that we can return M_UNABLE_TO_AUTHORIZE_JOIN later.
could_satisfy = false;
continue;
}
if services
.rooms
.state_cache
@@ -239,6 +268,8 @@ pub(crate) async fn user_can_perform_restricted_join(
| Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))),
},
| _ => {
// We don't recognise this join rule, so we cannot satisfy the request.
could_satisfy = false;
debug_info!(
"Unsupported allow rule in restricted join for room {}: {:?}",
room_id,
@@ -248,9 +279,23 @@ pub(crate) async fn user_can_perform_restricted_join(
}
}
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
if could_satisfy {
// We were able to check all the restrictions and can be certain that the
// prospective member is not permitted to join.
Err!(Request(Forbidden(
"You do not belong to any of the rooms or spaces required to join this room."
)))
} else {
// We were unable to check all the restrictions. This usually means we aren't in
// one of the rooms this one is restricted to, ergo can't check its state for
// the user's membership, and consequently the user *might* be able to join if
// they ask another server.
Err!(Request(UnableToAuthorizeJoin(
"You do not belong to any of the recognised rooms or spaces required to join this \
room, but this server is unable to verify every requirement. You may be able to \
join via another server."
)))
}
}
pub(crate) fn maybe_strip_event_id(
+5
View File
@@ -1696,6 +1696,11 @@ pub struct Config {
#[serde(default)]
pub url_preview_check_root_domain: bool,
/// User agent that is used specifically when fetching url previews.
///
/// default: "continuwuity/<version> (bot; +https://continuwuity.org)"
pub url_preview_user_agent: Option<String>,
/// List of forbidden room aliases and room IDs as strings of regex
/// patterns.
///
+2 -1
View File
@@ -85,7 +85,8 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
// 404
| NotFound | NotImplemented | FeatureDisabled => StatusCode::NOT_FOUND,
| NotFound | NotImplemented | FeatureDisabled | SenderIgnored { .. } =>
StatusCode::NOT_FOUND,
// 403
| GuestAccessForbidden
+10 -1
View File
@@ -8,9 +8,11 @@
use std::sync::OnceLock;
static BRANDING: &str = "continuwuity";
static WEBSITE: &str = "https://continuwuity.org";
static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
static VERSION: OnceLock<String> = OnceLock::new();
static VERSION_UA: OnceLock<String> = OnceLock::new();
static USER_AGENT: OnceLock<String> = OnceLock::new();
#[inline]
@@ -19,11 +21,18 @@ pub fn name() -> &'static str { BRANDING }
#[inline]
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
#[inline]
pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) }
#[inline]
pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
fn init_user_agent() -> String { format!("{}/{}", name(), version()) }
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
fn init_version_ua() -> String {
conduwuit_build_metadata::version_tag()
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}"))
}
fn init_version() -> String {
conduwuit_build_metadata::version_tag()
+1
View File
@@ -230,6 +230,7 @@ tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracing-journald = { workspace = true, optional = true }
parking_lot.workspace = true
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
+36
View File
@@ -0,0 +1,36 @@
use std::{thread, time::Duration};
/// Runs a loop that checks for deadlocks every 10 seconds.
///
/// Note that this requires the `deadlock_detection` parking_lot feature to be
/// enabled.
pub(crate) fn deadlock_detection_thread() {
loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = parking_lot::deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
eprintln!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
eprintln!("Deadlock #{i}");
for t in threads {
eprintln!("Thread Id {:#?}", t.thread_id());
eprintln!("{:#?}", t.backtrace());
}
}
}
}
/// Spawns the deadlock detection thread.
///
/// This thread will run in the background and check for deadlocks every 10
/// seconds. When a deadlock is detected, it will print detailed information to
/// stderr.
pub(crate) fn spawn() {
thread::Builder::new()
.name("deadlock_detector".to_owned())
.spawn(deadlock_detection_thread)
.expect("failed to spawn deadlock detection thread");
}
+4
View File
@@ -5,6 +5,7 @@ use std::sync::{Arc, atomic::Ordering};
use conduwuit_core::{debug_info, error};
mod clap;
mod deadlock;
mod logging;
mod mods;
mod panic;
@@ -27,6 +28,9 @@ pub fn run() -> Result<()> {
}
pub fn run_with_args(args: &Args) -> Result<()> {
// Spawn deadlock detection thread
deadlock::spawn();
let runtime = runtime::new(args)?;
let server = Server::new(args, Some(runtime.handle()))?;
+6
View File
@@ -36,6 +36,11 @@ impl crate::Service for Service {
.clone()
.and_then(Either::right);
let url_preview_user_agent = config
.url_preview_user_agent
.clone()
.unwrap_or_else(|| conduwuit::version::user_agent().to_owned());
Ok(Arc::new(Self {
default: base(config)?
.dns_resolver(resolver.resolver.clone())
@@ -49,6 +54,7 @@ impl crate::Service for Service {
.dns_resolver(resolver.resolver.clone())
.timeout(Duration::from_secs(config.url_preview_timeout))
.redirect(redirect::Policy::limited(3))
.user_agent(url_preview_user_agent)
.build()?,
extern_media: base(config)?
@@ -4,18 +4,83 @@ use std::{
};
use conduwuit::{
Err, Event, Result, debug::INFO_SPAN_LEVEL, defer, err, implement, info,
utils::stream::IterStream, warn,
Err, Event, PduEvent, Result, debug::INFO_SPAN_LEVEL, debug_error, debug_info, defer, err,
implement, info, trace, utils::stream::IterStream, warn,
};
use futures::{
FutureExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, try_join5},
future::{OptionFuture, try_join4},
};
use ruma::{
CanonicalJsonValue, EventId, OwnedUserId, RoomId, ServerName, UserId,
events::{
StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use ruma::{CanonicalJsonValue, EventId, RoomId, ServerName, UserId, events::StateEventType};
use tracing::debug;
use crate::rooms::timeline::{RawPduId, pdu_fits};
async fn should_rescind_invite(
services: &crate::rooms::event_handler::Services,
content: &mut BTreeMap<String, CanonicalJsonValue>,
sender: &UserId,
room_id: &RoomId,
) -> Result<Option<PduEvent>> {
// We insert a bogus event ID since we can't actually calculate the right one
content.insert("event_id".to_owned(), CanonicalJsonValue::String("$rescind".to_owned()));
let pdu_event = serde_json::from_value::<PduEvent>(
serde_json::to_value(&content).expect("CanonicalJsonObj is a valid JsonValue"),
)
.map_err(|e| err!("invalid PDU: {e}"))?;
if pdu_event.room_id().is_none_or(|r| r != room_id)
&& pdu_event.sender() != sender
&& pdu_event.event_type() != &TimelineEventType::RoomMember
&& pdu_event.state_key().is_none_or(|v| v == sender.as_str())
{
return Ok(None);
}
let target_user_id = UserId::parse(pdu_event.state_key().unwrap())?;
if pdu_event
.get_content::<RoomMemberEventContent>()?
.membership
!= MembershipState::Leave
{
return Ok(None); // Not a leave event
}
// Does the target user have a pending invite?
let Ok(pending_invite_state) = services
.state_cache
.invite_state(target_user_id, room_id)
.await
else {
return Ok(None); // No pending invite, so nothing to rescind
};
for event in pending_invite_state {
if event
.get_field::<String>("type")?
.is_some_and(|t| t == "m.room.member")
|| event
.get_field::<OwnedUserId>("state_key")?
.is_some_and(|s| s == *target_user_id)
|| event
.get_field::<OwnedUserId>("sender")?
.is_some_and(|s| s == *sender)
|| event
.get_field::<RoomMemberEventContent>("content")?
.is_some_and(|c| c.membership == MembershipState::Invite)
{
return Ok(Some(pdu_event));
}
}
Ok(None)
}
/// When receiving an event one needs to:
/// 0. Check the server is in the room
/// 1. Skip the PDU if we already know about it
@@ -69,6 +134,7 @@ pub async fn handle_incoming_pdu<'a>(
);
return Err!(Request(TooLarge("PDU is too large")));
}
trace!("processing incoming pdu from {origin} for room {room_id} with event id {event_id}");
// 1.1 Check we even know about the room
let meta_exists = self.services.metadata.exists(room_id).map(Ok);
@@ -91,24 +157,14 @@ pub async fn handle_incoming_pdu<'a>(
.then(|| self.acl_check(sender.server_name(), room_id))
.into();
// Fetch create event
let create_event =
self.services
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "");
let (meta_exists, is_disabled, (), (), ref create_event) = try_join5(
let (meta_exists, is_disabled, (), ()) = try_join4(
meta_exists,
is_disabled,
origin_acl_check,
sender_acl_check.map(|o| o.unwrap_or(Ok(()))),
create_event,
)
.await?;
if !meta_exists {
return Err!(Request(NotFound("Room is unknown to this server")));
}
.await
.inspect_err(|e| debug_error!("failed to handle incoming PDU: {e}"))?;
if is_disabled {
return Err!(Request(Forbidden("Federation of this room is disabled by this server.")));
@@ -120,6 +176,23 @@ pub async fn handle_incoming_pdu<'a>(
.server_in_room(self.services.globals.server_name(), room_id)
.await
{
// Is this a federated invite rescind?
// copied from https://github.com/element-hq/synapse/blob/7e4588a/synapse/handlers/federation_event.py#L255-L300
if value.get("type").and_then(|t| t.as_str()) == Some("m.room.member") {
if let Some(pdu) =
should_rescind_invite(&self.services, &mut value.clone(), sender, room_id).await?
{
debug_info!(
"Invite to {room_id} appears to have been rescinded by {sender}, marking as \
left"
);
self.services
.state_cache
.mark_as_left(sender, room_id, Some(pdu))
.await;
return Ok(None);
}
}
info!(
%origin,
"Dropping inbound PDU for room we aren't participating in"
@@ -127,6 +200,17 @@ pub async fn handle_incoming_pdu<'a>(
return Err!(Request(NotFound("This server is not participating in that room.")));
}
if !meta_exists {
return Err!(Request(NotFound("Room is unknown to this server")));
}
// Fetch create event
let create_event = &(self
.services
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await?);
let (incoming_pdu, val) = self
.handle_outlier_pdu(origin, create_event, event_id, room_id, value, false)
.await?;
@@ -56,7 +56,7 @@ pub async fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<Parsed> {
.state
.get_room_version(&room_id)
.await
.map_err(|_| err!("Server is not in room {room_id}"))?;
.unwrap_or(RoomVersionId::V1);
let (event_id, value) = gen_event_id_canonical_json(pdu, &room_version_id).map_err(|e| {
err!(Request(InvalidParam("Could not convert event to canonical json: {e}")))
})?;
+10 -5
View File
@@ -1269,12 +1269,12 @@ impl Service {
}
#[cfg(not(feature = "ldap"))]
pub async fn search_ldap(&self, _user_id: &UserId) -> Result<Vec<(String, bool)>> {
pub async fn search_ldap(&self, _user_id: &UserId) -> Result<Vec<(String, Option<bool>)>> {
Err!(FeatureDisabled("ldap"))
}
#[cfg(feature = "ldap")]
pub async fn search_ldap(&self, user_id: &UserId) -> Result<Vec<(String, bool)>> {
pub async fn search_ldap(&self, user_id: &UserId) -> Result<Vec<(String, Option<bool>)>> {
let localpart = user_id.localpart().to_owned();
let lowercased_localpart = localpart.to_lowercase();
@@ -1318,7 +1318,7 @@ impl Service {
.inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Search"))
.map_err(|e| err!(Ldap(error!(?attr, ?user_filter, "LDAP search error: {e}"))))?;
let mut dns: HashMap<String, bool> = entries
let mut dns: HashMap<String, Option<bool>> = entries
.into_iter()
.filter_map(|entry| {
let search_entry = SearchEntry::construct(entry);
@@ -1329,11 +1329,16 @@ impl Service {
.into_iter()
.chain(search_entry.attrs.get(&config.name_attribute))
.any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart))
.then_some((search_entry.dn, false))
.then_some((search_entry.dn, None))
})
.collect();
if !config.admin_filter.is_empty() {
// Update all existing entries to Some(false) since we can now determine admin
// status
for admin_status in dns.values_mut() {
*admin_status = Some(false);
}
let admin_base_dn = if config.admin_base_dn.is_empty() {
&config.base_dn
} else {
@@ -1362,7 +1367,7 @@ impl Service {
.into_iter()
.chain(search_entry.attrs.get(&config.name_attribute))
.any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart))
.then_some((search_entry.dn, true))
.then_some((search_entry.dn, Some(true)))
}));
}