feat: Ratelimit CSAPI endpoints

This commit is contained in:
Ginger authored and Ellis Git committed 2026-11-05 16:49:48 +00:00
1 parent 15f7aabca6
commit 4a3b0366ac
6 files changed
+102 -6

No files matched your search

Generated
+1
View File
@@ -1037,6 +1037,7 @@ dependencies = [
"either",
"figment",
"futures",
"governor",
"hardened_malloc-rs",
"http",
"http-body-util",
+7
View File
@@ -685,6 +685,13 @@
# env: CONTINUWUITY_SENDER_SHUTDOWN_TIMEOUT
#sender_shutdown_timeout = 5
# Maximum number of requests per minute that unauthenticated clients
# may make to ratelimited client-server API routes. This ratelimit is
# global across all endpoints for each IP.
#
# env: CONTINUWUITY_UNAUTHENTICATED_REQUEST_RATELIMIT
#unauthenticated_request_ratelimit = 30
# Enables registration. If set to false, no users can register on this
# server.
#
+13 -4
View File
@@ -1,18 +1,21 @@
use std::ops::Deref;
use std::{
net::{IpAddr, Ipv4Addr},
ops::Deref,
};
use axum::{
RequestExt, RequestPartsExt,
body::Body,
extract::{FromRequest, Path, Query},
};
use conduwuit::{Error, Result, err};
use conduwuit::{Error, Result, err, error};
use ruma::{
CanonicalJsonObject,
api::{IncomingRequest, IncomingRequestExt, auth_scheme::AuthScheme},
};
use serde::Deserialize;
use crate::{State, router::auth::CheckAuth};
use crate::{State, client_ip::ClientIp, router::auth::CheckAuth};
/// Query parameters needed to authenticate requests
#[derive(Deserialize)]
@@ -87,6 +90,11 @@ where
let Path(path): Path<Vec<String>> = parts.extract().await?;
let borrowed_path: Vec<_> = path.iter().map(String::as_str).collect();
let Query(auth_query): Query<AuthQueryParams> = parts.extract().await?;
let ClientIp(ip) = parts
.extract_with_state(services)
.await
.inspect_err(|err| error!("Failed to extract client IP: {err:?}"))
.unwrap_or(ClientIp(IpAddr::V4(Ipv4Addr::UNSPECIFIED)));
// Assemble a new request from the read body and parts
let request = hyper::Request::from_parts(parts, body.as_ref());
@@ -102,7 +110,8 @@ where
})?;
let identity =
R::Authentication::check::<R>(services, &request, authentication, auth_query).await?;
R::Authentication::check::<R>(services, &request, ip, authentication, auth_query)
.await?;
// Deserialize the body
let body = R::try_from_http_request(request, &borrowed_path)
+37 -1
View File
@@ -1,6 +1,7 @@
use std::{
any::{Any, TypeId},
collections::BTreeSet,
net::IpAddr,
};
use conduwuit::{
@@ -18,7 +19,10 @@ use ruma::{
AuthScheme, NoAccessToken, NoAuthentication,
},
client,
error::{ErrorKind, InsufficientUserAuthenticationErrorData, UnknownTokenErrorData},
error::{
ErrorKind, InsufficientUserAuthenticationErrorData, LimitExceededErrorData,
UnknownTokenErrorData,
},
federation::authentication::ServerSignatures,
},
assign,
@@ -90,6 +94,7 @@ pub(crate) trait CheckAuth: AuthScheme {
fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
incoming_request: &hyper::Request<&[u8]>,
request_ip: IpAddr,
authentication: Self::Output,
query: AuthQueryParams,
) -> impl Future<Output = Result<Self::Identity>> + Send;
@@ -101,6 +106,7 @@ impl CheckAuth for ServerSignatures {
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
incoming_request: &hyper::Request<&[u8]>,
_request_ip: IpAddr,
authentication: Self::Output,
_query: AuthQueryParams,
) -> Result<Self::Identity> {
@@ -161,6 +167,7 @@ impl CheckAuth for AccessToken {
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
_request_ip: IpAddr,
authentication: Self::Output,
query: AuthQueryParams,
) -> Result<Self::Identity> {
@@ -181,6 +188,7 @@ impl CheckAuth for AccessTokenOptional {
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
request_ip: IpAddr,
authentication: Self::Output,
query: AuthQueryParams,
) -> Result<Self::Identity> {
@@ -195,6 +203,10 @@ impl CheckAuth for AccessTokenOptional {
.await
.map(Some)
} else {
if R::RATE_LIMITED {
check_unauthenticated_request_ratelimiting(services, request_ip)?;
}
Ok(None)
}
}
@@ -206,6 +218,7 @@ impl CheckAuth for AppserviceToken {
async fn check<R: IncomingRequest + Any>(
services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
_request_ip: IpAddr,
authentication: Self::Output,
_query: AuthQueryParams,
) -> Result<Self::Identity> {
@@ -219,6 +232,7 @@ impl CheckAuth for AppserviceTokenOptional {
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
_request_ip: IpAddr,
authentication: Self::Output,
_query: AuthQueryParams,
) -> Result<Self::Identity> {
@@ -238,6 +252,7 @@ impl CheckAuth for NoAuthentication {
fn check<R: IncomingRequest<Authentication = Self> + Any>(
_services: &Services,
_incoming_request: &hyper::Request<&[u8]>,
_request_ip: IpAddr,
_authentication: Self::Output,
_query: AuthQueryParams,
) -> impl Future<Output = Result<Self::Identity>> + Send {
@@ -251,6 +266,7 @@ impl CheckAuth for NoAccessToken {
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
services: &Services,
incoming_request: &hyper::Request<&[u8]>,
request_ip: IpAddr,
_authentication: Self::Output,
query: AuthQueryParams,
) -> Result<Self::Identity> {
@@ -267,6 +283,10 @@ impl CheckAuth for NoAccessToken {
.await
.map(Some)
} else {
if R::RATE_LIMITED {
check_unauthenticated_request_ratelimiting(services, request_ip)?;
}
Ok(None)
}
}
@@ -408,3 +428,19 @@ async fn check_appservice_token(services: &Services, token: &str) -> Result<Regi
Ok(appservice_info)
}
fn check_unauthenticated_request_ratelimiting(services: &Services, ip: IpAddr) -> Result<()> {
if services
.config
.unauthenticated_request_ratelimit
.check_key(&ip)
.is_err()
{
return Err(Error::BadRequest(
ErrorKind::LimitExceeded(LimitExceededErrorData::new()),
"Rate limit exceeded.",
));
}
Ok(())
}
+1
View File
@@ -77,6 +77,7 @@ cyborgtime.workspace = true
either.workspace = true
figment.workspace = true
futures.workspace = true
governor.workspace = true
http-body-util.workspace = true
http.workspace = true
ipaddress.workspace = true
+43 -1
View File
@@ -5,7 +5,10 @@ pub mod proxy;
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
hash::Hash,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
num::NonZero,
ops::Deref,
path::PathBuf,
time::Duration,
};
@@ -17,6 +20,7 @@ use either::{
};
use figment::providers::{Env, Format, Toml};
pub use figment::{Figment, value::Value as FigmentValue};
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
use lettre::message::Mailbox;
use openidconnect::{Audience, ClientId, ClientSecret, Scope};
use regex::RegexSet;
@@ -40,7 +44,7 @@ use crate::{Result, err, error::Error, utils::sys};
/// All the config options for continuwuity.
#[allow(clippy::struct_excessive_bools)]
#[allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)]
#[derive(Clone, Debug, Deserialize)]
#[derive(Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global",
@@ -790,6 +794,14 @@ pub struct Config {
#[serde(default = "default_sender_shutdown_timeout")]
pub sender_shutdown_timeout: u64,
/// Maximum number of requests per minute that unauthenticated clients
/// may make to ratelimited client-server API routes. This ratelimit is
/// global across all endpoints for each IP.
///
/// default: 30
#[serde(default = "default_unauthenticated_request_ratelimit")]
pub unauthenticated_request_ratelimit: PerMinuteRatelimiter<IpAddr>,
/// Enables registration. If set to false, no users can register on this
/// server.
///
@@ -2960,6 +2972,32 @@ impl TrustedServerConfig {
}
}
#[derive(Debug)]
pub struct PerMinuteRatelimiter<K: Hash + Eq + Clone> {
ratelimiter: DefaultKeyedRateLimiter<K>,
}
impl<K: Hash + Eq + Clone> PerMinuteRatelimiter<K> {
fn new(quota: Quota) -> Self { Self { ratelimiter: RateLimiter::keyed(quota) } }
}
impl<K: Hash + Eq + Clone> Deref for PerMinuteRatelimiter<K> {
type Target = DefaultKeyedRateLimiter<K>;
fn deref(&self) -> &Self::Target { &self.ratelimiter }
}
impl<'de, K: Hash + Eq + Clone> Deserialize<'de> for PerMinuteRatelimiter<K> {
fn deserialize<D>(deserializer: D) -> std::prelude::v1::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let max_burst = NonZero::<u32>::deserialize(deserializer)?;
Ok(Self::new(Quota::per_minute(max_burst)))
}
}
const DEPRECATED_KEYS: &[&str] = &[
"cache_capacity",
"conduit_cache_capacity_modifier",
@@ -3403,3 +3441,7 @@ fn default_preferred_username_claim() -> String { "preferred_username".to_owned(
fn default_profile_key_map() -> HashMap<String, String> {
HashMap::from_iter([(ProfileFieldName::DisplayName.to_string(), "name".to_owned())])
}
fn default_unauthenticated_request_ratelimit<K: Hash + Eq + Clone>() -> PerMinuteRatelimiter<K> {
PerMinuteRatelimiter::new(Quota::per_minute(30.try_into().unwrap()))
}