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",
|
"const-str",
|
||||||
"either",
|
"either",
|
||||||
"futures",
|
"futures",
|
||||||
|
"governor",
|
||||||
"hickory-resolver",
|
"hickory-resolver",
|
||||||
"http",
|
"http",
|
||||||
"image",
|
"image",
|
||||||
@@ -1182,6 +1183,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"loole",
|
"loole",
|
||||||
"lru-cache",
|
"lru-cache",
|
||||||
|
"nonzero_ext",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"recaptcha-verify",
|
"recaptcha-verify",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -2003,6 +2005,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -2119,6 +2127,12 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-timer"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -2209,6 +2223,25 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
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]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -2289,7 +2322,7 @@ version = "0.15.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foldhash",
|
"foldhash 0.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2297,6 +2330,11 @@ name = "hashbrown"
|
|||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hdrhistogram"
|
name = "hdrhistogram"
|
||||||
@@ -3367,6 +3405,12 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nonzero_ext"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "noop_proc_macro"
|
name = "noop_proc_macro"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -5357,6 +5401,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "spki"
|
name = "spki"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -5383,7 +5436,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"psm",
|
"psm",
|
||||||
"windows-sys 0.52.0",
|
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -561,6 +561,14 @@ version = "0.11.19"
|
|||||||
default-features = false
|
default-features = false
|
||||||
features = ["smtp-transport", "pool", "hostname", "builder", "rustls", "aws-lc-rs", "rustls-native-certs", "tokio1", "tokio1-rustls", "tracing", "serde"]
|
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
|
# Patches
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ conduwuit-database.workspace = true
|
|||||||
const-str.workspace = true
|
const-str.workspace = true
|
||||||
either.workspace = true
|
either.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
governor.workspace = true
|
||||||
hickory-resolver.workspace = true
|
hickory-resolver.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
@@ -102,6 +103,7 @@ ldap3.optional = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
loole.workspace = true
|
loole.workspace = true
|
||||||
lru-cache.workspace = true
|
lru-cache.workspace = true
|
||||||
|
nonzero_ext.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|||||||
+33
-21
@@ -1,9 +1,13 @@
|
|||||||
use std::{borrow::Cow, collections::HashMap, sync::Arc};
|
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 database::{Deserialized, Map};
|
||||||
|
use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter};
|
||||||
use lettre::{Address, message::Mailbox};
|
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;
|
mod session;
|
||||||
|
|
||||||
@@ -18,6 +22,7 @@ pub struct Service {
|
|||||||
services: Services,
|
services: Services,
|
||||||
sessions: tokio::sync::Mutex<ValidationSessions>,
|
sessions: tokio::sync::Mutex<ValidationSessions>,
|
||||||
send_attempts: std::sync::Mutex<HashMap<(OwnedClientSecret, Address), usize>>,
|
send_attempts: std::sync::Mutex<HashMap<(OwnedClientSecret, Address), usize>>,
|
||||||
|
ratelimiter: DefaultKeyedRateLimiter<Address>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
@@ -43,6 +48,7 @@ impl crate::Service for Service {
|
|||||||
},
|
},
|
||||||
sessions: tokio::sync::Mutex::default(),
|
sessions: tokio::sync::Mutex::default(),
|
||||||
send_attempts: std::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 {
|
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";
|
const VALIDATION_URL_PATH: &str = "/_continuwuity/3pid/email/validate";
|
||||||
|
|
||||||
/// Send a validation message to an email address.
|
/// Send a validation message to an email address.
|
||||||
@@ -76,29 +88,29 @@ impl Service {
|
|||||||
return Ok(session.session_id.clone());
|
return Ok(session.session_id.clone());
|
||||||
},
|
},
|
||||||
| ValidationState::Pending(ref mut token) => {
|
| 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();
|
let mut send_attempts = self.send_attempts.lock().unwrap();
|
||||||
|
|
||||||
match send_attempts
|
let last_send_attempt = send_attempts
|
||||||
.get_mut(&(session.client_secret.clone(), session.email.clone()))
|
.entry((session.client_secret.clone(), session.email.clone()))
|
||||||
{
|
.or_default();
|
||||||
| 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise save the supplied send attempt.
|
if send_attempt <= *last_send_attempt {
|
||||||
*last_send_attempt = send_attempt;
|
// If the supplied send attempt isn't higher than the last
|
||||||
},
|
// one, don't send an email.
|
||||||
| None => {
|
return Ok(session.session_id.clone());
|
||||||
// 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.
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save this send attempt.
|
||||||
|
*last_send_attempt = send_attempt;
|
||||||
drop(send_attempts);
|
drop(send_attempts);
|
||||||
|
|
||||||
// Create a new token for the existing session.
|
// Create a new token for the existing session.
|
||||||
|
|||||||
Reference in New Issue
Block a user