mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Ratelimit sending threepid validation emails
This commit is contained in:
Generated
+54
-2
@@ -1172,6 +1172,7 @@ dependencies = [
|
||||
"const-str",
|
||||
"either",
|
||||
"futures",
|
||||
"governor",
|
||||
"hickory-resolver",
|
||||
"http",
|
||||
"image",
|
||||
@@ -1182,6 +1183,7 @@ dependencies = [
|
||||
"log",
|
||||
"loole",
|
||||
"lru-cache",
|
||||
"nonzero_ext",
|
||||
"rand 0.10.0",
|
||||
"recaptcha-verify",
|
||||
"regex",
|
||||
@@ -2003,6 +2005,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -2119,6 +2127,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
@@ -2209,6 +2223,25 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-sink",
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"hashbrown 0.16.1",
|
||||
"nonzero_ext",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"smallvec",
|
||||
"spinning_top",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -2289,7 +2322,7 @@ version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2297,6 +2330,11 @@ name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hdrhistogram"
|
||||
@@ -3367,6 +3405,12 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
@@ -5357,6 +5401,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spinning_top"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -5383,7 +5436,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -561,6 +561,14 @@ version = "0.11.19"
|
||||
default-features = false
|
||||
features = ["smtp-transport", "pool", "hostname", "builder", "rustls", "aws-lc-rs", "rustls-native-certs", "tokio1", "tokio1-rustls", "tracing", "serde"]
|
||||
|
||||
[workspace.dependencies.governor]
|
||||
version = "0.10.4"
|
||||
default-features = false
|
||||
features = ["std"]
|
||||
|
||||
[workspace.dependencies.nonzero_ext]
|
||||
version = "0.3.0"
|
||||
|
||||
#
|
||||
# Patches
|
||||
#
|
||||
|
||||
@@ -91,6 +91,7 @@ conduwuit-database.workspace = true
|
||||
const-str.workspace = true
|
||||
either.workspace = true
|
||||
futures.workspace = true
|
||||
governor.workspace = true
|
||||
hickory-resolver.workspace = true
|
||||
http.workspace = true
|
||||
image.workspace = true
|
||||
@@ -102,6 +103,7 @@ ldap3.optional = true
|
||||
log.workspace = true
|
||||
loole.workspace = true
|
||||
lru-cache.workspace = true
|
||||
nonzero_ext.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
reqwest.workspace = true
|
||||
|
||||
+33
-21
@@ -1,9 +1,13 @@
|
||||
use std::{borrow::Cow, collections::HashMap, sync::Arc};
|
||||
|
||||
use conduwuit::{Err, Result, result::FlatOk};
|
||||
use conduwuit::{Err, Error, Result, result::FlatOk};
|
||||
use database::{Deserialized, Map};
|
||||
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{ClientSecret, OwnedClientSecret, OwnedSessionId, SessionId};
|
||||
use nonzero_ext::nonzero;
|
||||
use ruma::{
|
||||
ClientSecret, OwnedClientSecret, OwnedSessionId, SessionId, api::client::error::ErrorKind,
|
||||
};
|
||||
|
||||
mod session;
|
||||
|
||||
@@ -18,6 +22,7 @@ pub struct Service {
|
||||
services: Services,
|
||||
sessions: tokio::sync::Mutex<ValidationSessions>,
|
||||
send_attempts: std::sync::Mutex<HashMap<(OwnedClientSecret, Address), usize>>,
|
||||
ratelimiter: DefaultKeyedRateLimiter<Address>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
@@ -43,6 +48,7 @@ impl crate::Service for Service {
|
||||
},
|
||||
sessions: tokio::sync::Mutex::default(),
|
||||
send_attempts: std::sync::Mutex::default(),
|
||||
ratelimiter: RateLimiter::keyed(Self::EMAIL_RATELIMIT),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -50,6 +56,12 @@ impl crate::Service for Service {
|
||||
}
|
||||
|
||||
impl Service {
|
||||
// Each address gets two tickets to send an email, which refill at a rate of one
|
||||
// per ten minutes. This allows two emails to be sent at once without waiting
|
||||
// (in case the first one gets eaten), but requires a wait of at least ten
|
||||
// minutes before sending another.
|
||||
const EMAIL_RATELIMIT: Quota =
|
||||
Quota::per_minute(nonzero!(10_u32)).allow_burst(nonzero!(2_u32));
|
||||
const VALIDATION_URL_PATH: &str = "/_continuwuity/3pid/email/validate";
|
||||
|
||||
/// Send a validation message to an email address.
|
||||
@@ -76,29 +88,29 @@ impl Service {
|
||||
return Ok(session.session_id.clone());
|
||||
},
|
||||
| ValidationState::Pending(ref mut token) => {
|
||||
// Check the send attempt for this session
|
||||
// Check ratelimiting for the target address.
|
||||
if self.ratelimiter.check_key(&recipient.email).is_err() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::LimitExceeded { retry_after: None },
|
||||
"You're sending emails too fast, try again in a few minutes.",
|
||||
));
|
||||
}
|
||||
|
||||
// Check the send attempt for this session.
|
||||
let mut send_attempts = self.send_attempts.lock().unwrap();
|
||||
|
||||
match send_attempts
|
||||
.get_mut(&(session.client_secret.clone(), session.email.clone()))
|
||||
{
|
||||
| Some(last_send_attempt) => {
|
||||
if send_attempt <= *last_send_attempt {
|
||||
// If the supplied send attempt isn't higher than the last
|
||||
// one, don't send an email.
|
||||
return Ok(session.session_id.clone());
|
||||
}
|
||||
let last_send_attempt = send_attempts
|
||||
.entry((session.client_secret.clone(), session.email.clone()))
|
||||
.or_default();
|
||||
|
||||
// Otherwise save the supplied send attempt.
|
||||
*last_send_attempt = send_attempt;
|
||||
},
|
||||
| None => {
|
||||
// Default to sending an email if no previous
|
||||
// attempt could be found. This can happen if
|
||||
// the server was restarted, which clears the
|
||||
// send attempt tracker.
|
||||
},
|
||||
if send_attempt <= *last_send_attempt {
|
||||
// If the supplied send attempt isn't higher than the last
|
||||
// one, don't send an email.
|
||||
return Ok(session.session_id.clone());
|
||||
}
|
||||
|
||||
// Save this send attempt.
|
||||
*last_send_attempt = send_attempt;
|
||||
drop(send_attempts);
|
||||
|
||||
// Create a new token for the existing session.
|
||||
|
||||
Reference in New Issue
Block a user