From 095677980285cd62575eae33f9a499ea5c66e2e6 Mon Sep 17 00:00:00 2001 From: timedout Date: Mon, 5 Jan 2026 01:19:11 +0000 Subject: [PATCH] feat: Add Meowlnir invite interception support Co-authored-by: Jade Ellis --- Cargo.lock | 33 +++++--- Cargo.toml | 124 ++++++++++++++-------------- conduwuit-example.toml | 21 +++++ src/api/client/membership/invite.rs | 26 +++++- src/api/server/invite.rs | 19 ++++- src/core/config/mod.rs | 30 ++++++- src/service/sending/antispam.rs | 72 ++++++++++++++++ src/service/sending/mod.rs | 17 +++- 8 files changed, 265 insertions(+), 77 deletions(-) create mode 100644 src/service/sending/antispam.rs diff --git a/Cargo.lock b/Cargo.lock index a4cb5ab7..c8d56884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2982,6 +2982,16 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "meowlnir-antispam" +version = "0.1.0" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" +dependencies = [ + "ruma-common", + "serde", + "serde_json", +] + [[package]] name = "mime" version = "0.3.17" @@ -4065,11 +4075,12 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "ruma" version = "0.10.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "assign", "js_int", "js_option", + "meowlnir-antispam", "ruma-appservice-api", "ruma-client-api", "ruma-common", @@ -4085,7 +4096,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.10.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "js_int", "ruma-common", @@ -4097,7 +4108,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "as_variant", "assign", @@ -4120,7 +4131,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "as_variant", "base64 0.22.1", @@ -4152,7 +4163,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "as_variant", "indexmap", @@ -4177,7 +4188,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "bytes", "headers", @@ -4199,7 +4210,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "js_int", "thiserror 2.0.17", @@ -4208,7 +4219,7 @@ dependencies = [ [[package]] name = "ruma-identity-service-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "js_int", "ruma-common", @@ -4218,7 +4229,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "cfg-if", "proc-macro-crate", @@ -4233,7 +4244,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "js_int", "ruma-common", @@ -4245,7 +4256,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.15.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=377d801fa035480b772c640b430097c1ec0ddb16#377d801fa035480b772c640b430097c1ec0ddb16" dependencies = [ "base64 0.22.1", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index a8911017..3ab0b5c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,11 +33,11 @@ features = ["serde"] [workspace.dependencies.smallvec] version = "1.14.0" features = [ - "const_generics", - "const_new", - "serde", - "union", - "write", + "const_generics", + "const_new", + "serde", + "union", + "write", ] [workspace.dependencies.smallstr] @@ -96,13 +96,13 @@ version = "1.11.1" version = "0.7.9" default-features = false features = [ - "form", - "http1", - "http2", - "json", - "matched-path", - "tokio", - "tracing", + "form", + "http1", + "http2", + "json", + "matched-path", + "tokio", + "tracing", ] [workspace.dependencies.axum-extra] @@ -149,10 +149,10 @@ features = ["aws_lc_rs"] version = "0.12.15" default-features = false features = [ - "rustls-tls-native-roots", - "socks", - "hickory-dns", - "http2", + "rustls-tls-native-roots", + "socks", + "hickory-dns", + "http2", ] [workspace.dependencies.serde] @@ -188,18 +188,18 @@ default-features = false version = "0.25.5" default-features = false features = [ - "jpeg", - "png", - "gif", - "webp", + "jpeg", + "png", + "gif", + "webp", ] [workspace.dependencies.blurhash] version = "0.2.3" default-features = false features = [ - "fast-linear-to-srgb", - "image", + "fast-linear-to-srgb", + "image", ] # logging @@ -229,13 +229,13 @@ default-features = false version = "4.5.35" default-features = false features = [ - "derive", - "env", - "error-context", - "help", - "std", - "string", - "usage", + "derive", + "env", + "error-context", + "help", + "std", + "string", + "usage", ] [workspace.dependencies.futures] @@ -247,15 +247,15 @@ features = ["std", "async-await"] version = "1.44.2" default-features = false features = [ - "fs", - "net", - "macros", - "sync", - "signal", - "time", - "rt-multi-thread", - "io-util", - "tracing", + "fs", + "net", + "macros", + "sync", + "signal", + "time", + "rt-multi-thread", + "io-util", + "tracing", ] [workspace.dependencies.tokio-metrics] @@ -280,18 +280,18 @@ default-features = false version = "1.6.0" default-features = false features = [ - "server", - "http1", - "http2", + "server", + "http1", + "http2", ] [workspace.dependencies.hyper-util] version = "=0.1.17" default-features = false features = [ - "server-auto", - "server-graceful", - "tokio", + "server-auto", + "server-graceful", + "tokio", ] # to support multiple variations of setting a config option @@ -310,9 +310,9 @@ features = ["env", "toml"] version = "0.25.1" default-features = false features = [ - "serde", - "system-config", - "tokio", + "serde", + "system-config", + "tokio", ] # Used for conduwuit::Error type @@ -351,7 +351,7 @@ version = "0.1.2" # Used for matrix spec type definitions and helpers [workspace.dependencies.ruma] git = "https://forgejo.ellis.link/continuwuation/ruwuma" -rev = "27abe0dcd33fd4056efc94bab3582646b31b6ce9" +rev = "377d801fa035480b772c640b430097c1ec0ddb16" features = [ "compat", "rand", @@ -381,13 +381,13 @@ features = [ "unstable-msc4095", "unstable-msc4121", "unstable-msc4125", - "unstable-msc4155", + "unstable-msc4155", "unstable-msc4186", "unstable-msc4203", # sending to-device events to appservices "unstable-msc4210", # remove legacy mentions "unstable-extensible-events", "unstable-pdu", - "unstable-msc4155" + "unstable-msc4155" ] [workspace.dependencies.rust-rocksdb] @@ -395,11 +395,11 @@ git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1" rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23" default-features = false features = [ - "multi-threaded-cf", - "mt_static", - "lz4", - "zstd", - "bzip2", + "multi-threaded-cf", + "mt_static", + "lz4", + "zstd", + "bzip2", ] [workspace.dependencies.sha2] @@ -458,16 +458,16 @@ git = "https://forgejo.ellis.link/continuwuation/jemallocator" rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8" default-features = false features = [ - "background_threads_runtime_support", - "unprefixed_malloc_on_supported_platforms", + "background_threads_runtime_support", + "unprefixed_malloc_on_supported_platforms", ] [workspace.dependencies.tikv-jemallocator] git = "https://forgejo.ellis.link/continuwuation/jemallocator" rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8" default-features = false features = [ - "background_threads_runtime_support", - "unprefixed_malloc_on_supported_platforms", + "background_threads_runtime_support", + "unprefixed_malloc_on_supported_platforms", ] [workspace.dependencies.tikv-jemalloc-ctl] git = "https://forgejo.ellis.link/continuwuation/jemallocator" @@ -491,9 +491,9 @@ default-features = false version = "0.1.2" default-features = false features = [ - "static", - "gcc", - "light", + "static", + "gcc", + "light", ] [workspace.dependencies.rustyline-async] diff --git a/conduwuit-example.toml b/conduwuit-example.toml index d1871309..77b89002 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1757,6 +1757,10 @@ # #ldap = false +# Configuration for antispam support +# +#antispam = false + [global.tls] # Path to a valid TLS certificate file. @@ -1923,3 +1927,20 @@ # example: "(objectClass=conduwuitAdmin)" or "(uid={username})" # #admin_filter = "" + +[global.antispam.meowlnir] + +# The base URL on which to contact meowlnir (before /_meowlnir/antispam). +# +# Example: "http://127.0.0.1:29339" +# +#base_url = + +# The authentication secret defined in antispam->secret. Required for +# continuwuity to talk to Meowlnir. +# +#secret = + +# The management room for which to send requests +# +#management_room = diff --git a/src/api/client/membership/invite.rs b/src/api/client/membership/invite.rs index 646852aa..2a546475 100644 --- a/src/api/client/membership/invite.rs +++ b/src/api/client/membership/invite.rs @@ -1,8 +1,11 @@ use axum::extract::State; use axum_client_ip::InsecureClientIp; use conduwuit::{ - Err, Result, debug_error, err, info, + Err, Result, + config::Antispam, + debug_error, err, info, matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder}, + trace, }; use futures::FutureExt; use ruma::{ @@ -12,6 +15,7 @@ use ruma::{ invite_permission_config::FilterLevel, room::member::{MembershipState, RoomMemberEventContent}, }, + meowlnir_antispam::user_may_invite, }; use service::Services; @@ -124,6 +128,26 @@ pub(crate) async fn invite_helper( return Err!(Request(Forbidden("Invites are not allowed on this server."))); } + trace!("maybe ask meowlnir"); + if let Some(Antispam { meowlnir: Some(cfg) }) = &services.config.antispam { + trace!("asking meowlnir"); + services + .sending + .send_meowlnir_antispam_request( + cfg, + user_may_invite::v1::Request::new( + cfg.management_room.clone(), + sender_user.to_owned(), + recipient_user.to_owned(), + ), + ) + .await + .inspect(|_| trace!("meowlnir :D")) + .inspect_err(|e| debug_error!("meowlnir sad: {e}"))?; + } else { + trace!("no meowlnir configured"); + } + if !services.globals.user_is_local(recipient_user) { let (pdu, pdu_json, invite_room_state) = { let state_lock = services.rooms.state.mutex.lock(room_id).await; diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index bea79510..09b20d58 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -2,7 +2,9 @@ use axum::extract::State; use axum_client_ip::InsecureClientIp; use base64::{Engine as _, engine::general_purpose}; use conduwuit::{ - Err, Error, PduEvent, Result, err, + Err, Error, PduEvent, Result, + config::Antispam, + err, matrix::{Event, event::gen_event_id}, utils::{self, hash::sha256}, warn, @@ -11,6 +13,7 @@ use ruma::{ CanonicalJsonValue, OwnedUserId, UserId, api::{client::error::ErrorKind, federation::membership::create_invite}, events::room::member::{MembershipState, RoomMemberEventContent}, + meowlnir_antispam::user_may_invite, serde::JsonObject, }; @@ -148,6 +151,20 @@ pub(crate) async fn create_invite_route( return Err!(Request(Forbidden("This server does not allow room invites."))); } + if let Some(Antispam { meowlnir: Some(cfg) }) = &services.config.antispam { + services + .sending + .send_meowlnir_antispam_request( + cfg, + user_may_invite::v1::Request::new( + cfg.management_room.clone(), + sender_user.to_owned(), + recipient_user.clone(), + ), + ) + .await?; + } + let mut invite_state = body.invite_room_state.clone(); let mut event: JsonObject = serde_json::from_str(body.event.get()) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 665ce055..6b474d4e 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -18,7 +18,7 @@ use figment::providers::{Env, Format, Toml}; pub use figment::{Figment, value::Value as FigmentValue}; use regex::RegexSet; use ruma::{ - OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId, + OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId, api::client::discovery::discover_support::ContactRole, }; use serde::{Deserialize, de::IgnoredAny}; @@ -2024,6 +2024,10 @@ pub struct Config { #[serde(default)] pub ldap: LdapConfig, + /// Configuration for antispam support + #[serde(default)] + pub antispam: Option, + // external structure; separate section #[serde(default)] pub blurhashing: BlurhashConfig, @@ -2240,6 +2244,30 @@ struct ListeningAddr { addrs: Either>, } +#[derive(Clone, Debug, Deserialize)] +pub struct Antispam { + pub meowlnir: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[config_example_generator( + filename = "conduwuit-example.toml", + section = "global.antispam.meowlnir" +)] +pub struct MeowlnirConfig { + /// The base URL on which to contact meowlnir (before /_meowlnir/antispam). + /// + /// Example: "http://127.0.0.1:29339" + pub base_url: Url, + + /// The authentication secret defined in antispam->secret. Required for + /// continuwuity to talk to Meowlnir. + pub secret: String, + + /// The management room for which to send requests + pub management_room: OwnedRoomId, +} + const DEPRECATED_KEYS: &[&str; 9] = &[ "cache_capacity", "conduit_cache_capacity_modifier", diff --git a/src/service/sending/antispam.rs b/src/service/sending/antispam.rs new file mode 100644 index 00000000..c21b29a7 --- /dev/null +++ b/src/service/sending/antispam.rs @@ -0,0 +1,72 @@ +use std::{fmt::Debug, mem}; + +use bytes::BytesMut; +use conduwuit::{Err, Result, config::MeowlnirConfig, debug_error, err, utils, warn}; +use reqwest::Client; +use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken}; + +/// Sends a request to an antispam service +pub(crate) async fn send_meowlnir_request( + client: &Client, + config: &MeowlnirConfig, + request: T, +) -> Result> +where + T: OutgoingRequest + Debug + Send, +{ + const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_15]; + if config.secret.is_empty() { + return Ok(None); + } + let secret = config.secret.as_str(); + let http_request = request + .try_into_http_request::( + config.base_url.as_str(), + SendAccessToken::Always(secret), + &VERSIONS, + )? + .map(BytesMut::freeze); + let reqwest_request = reqwest::Request::try_from(http_request)?; + + let mut response = client.execute(reqwest_request).await.map_err(|e| { + warn!("Could not send request to antispam: {e:?}"); + e + })?; + + // reqwest::Response -> http::Response conversion + let status = response.status(); + let mut http_response_builder = http::Response::builder() + .status(status) + .version(response.version()); + mem::swap( + response.headers_mut(), + http_response_builder + .headers_mut() + .expect("http::response::Builder is usable"), + ); + + let body = response.bytes().await?; // TODO: handle timeout + + if !status.is_success() { + debug_error!("Antispam response bytes: {:?}", utils::string_from_bytes(&body)); + return match status { + | http::StatusCode::FORBIDDEN => + Err!(Request(Forbidden("Request was rejected by antispam service.",))), + | _ => Err!(BadServerResponse(warn!( + "Antispam returned unsuccessful HTTP response {status}", + ))), + }; + } + + let response = T::IncomingResponse::try_from_http_response( + http_response_builder + .body(body) + .expect("reqwest body is valid http body"), + ); + + response.map(Some).map_err(|e| { + err!(BadServerResponse(warn!( + "Antispam returned invalid/malformed response bytes: {e}", + ))) + }) +} diff --git a/src/service/sending/mod.rs b/src/service/sending/mod.rs index 08ca7010..b82818c4 100644 --- a/src/service/sending/mod.rs +++ b/src/service/sending/mod.rs @@ -1,3 +1,4 @@ +mod antispam; mod appservice; mod data; mod dest; @@ -12,7 +13,9 @@ use std::{ use async_trait::async_trait; use conduwuit::{ - Result, Server, debug, debug_warn, err, error, + Result, Server, + config::MeowlnirConfig, + debug, debug_warn, err, error, smallvec::SmallVec, utils::{ReadyExt, TryReadyExt, available_parallelism, math::usize_from_u64_truncated}, warn, @@ -334,6 +337,18 @@ impl Service { appservice::send_request(client, registration, request).await } + /// Sends a request to the chosen antispam configuration + pub async fn send_meowlnir_antispam_request( + &self, + config: &MeowlnirConfig, + request: T, + ) -> Result> + where + T: OutgoingRequest + Debug + Send, + { + antispam::send_meowlnir_request(&self.services.client.appservice, config, request).await + } + /// Clean up queued sending event data /// /// Used after we remove an appservice registration or a user deletes a push