From b41ec426e61b7418fecef70531d48d367880272c Mon Sep 17 00:00:00 2001 From: MedzikUser Date: Tue, 20 Sep 2022 19:36:57 +0200 Subject: [PATCH] feat(auth): add ratelimit Added ratelimit to disallow brute force login. --- Cargo.lock | 172 +++++++++++++++++++++++++++++++++- Cargo.toml | 4 + src/server/api/auth/login.rs | 9 +- src/server/error.rs | 3 + src/server/utils/mod.rs | 3 +- src/server/utils/ratelimit.rs | 58 ++++++++++++ 6 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 src/server/utils/ratelimit.rs diff --git a/Cargo.lock b/Cargo.lock index 46ed0f8..af30204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,6 +376,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.3", +] + [[package]] name = "digest" version = "0.10.3" @@ -474,6 +487,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.24" @@ -509,9 +537,15 @@ checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" dependencies = [ "futures-core", "lock_api", - "parking_lot", + "parking_lot 0.11.2", ] +[[package]] +name = "futures-io" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" + [[package]] name = "futures-macro" version = "0.3.24" @@ -535,16 +569,25 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -577,6 +620,24 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +[[package]] +name = "governor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de1b4626e87b9eb1d603ed23067ba1e29ec1d0b35325a2b96c3fe1cf20871f56" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.1", + "quanta", + "rand", + "smallvec", +] + [[package]] name = "h2" version = "0.3.14" @@ -658,8 +719,10 @@ dependencies = [ "byte-unit", "crypto-utils", "futures-util", + "governor", "hex", "hyper", + "once_cell", "serde", "sqlx", "thiserror", @@ -867,6 +930,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "matchit" version = "0.6.0" @@ -940,6 +1012,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.1" @@ -950,6 +1028,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "num-bigint" version = "0.4.3" @@ -1022,7 +1106,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.3", ] [[package]] @@ -1039,6 +1133,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "paste" version = "1.0.9" @@ -1098,6 +1205,12 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro2" version = "1.0.43" @@ -1107,6 +1220,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quanta" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" +dependencies = [ + "crossbeam-utils", + "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.21" @@ -1116,6 +1245,45 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-cpuid" +version = "10.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index bb8a0e0..02afd72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,7 @@ axum-server = { version = "0.4", features = ["tls-rustls"] } tower-http = { version = "0.3", features = ["full"] } hyper = { version = "0.14", features = ["full"] } byte-unit = "4.0.14" + +# HTTP rate limiting +once_cell = "1.14.0" +governor = "0.5.0" diff --git a/src/server/api/auth/login.rs b/src/server/api/auth/login.rs index b57efd6..2019ef2 100644 --- a/src/server/api/auth/login.rs +++ b/src/server/api/auth/login.rs @@ -5,14 +5,21 @@ use serde::{Deserialize, Serialize}; use crate::{ config::Config, database::{error::Error as DatabaseError, Database, User}, - server::error::*, + server::{ + error::*, + utils::ratelimit::{check_limit_login, ClientIp}, + }, }; pub async fn login( Extension(db): Extension, Extension(config): Extension, + ClientIp(ip): ClientIp, request: Json, ) -> Result> { + // check rate limit + check_limit_login(&ip)?; + let user = User::new(&request.username, &request.password, false); let response = match db.find_user(&user).await { diff --git a/src/server/error.rs b/src/server/error.rs index 079e779..45b1225 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -5,6 +5,8 @@ use tracing::error; #[derive(Debug, Clone, Error)] pub enum Error { // auth error + #[error("Too many request, please slow down.")] + RateLimit, #[error("User not found")] UserNotFound, #[error("User already exists")] @@ -62,6 +64,7 @@ impl axum::response::IntoResponse for Error { error!("Error: {:?}", self); let status = match self { + Error::RateLimit => StatusCode::TOO_MANY_REQUESTS, Error::GenerateToken => StatusCode::INTERNAL_SERVER_ERROR, Error::Database => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::BAD_REQUEST, diff --git a/src/server/utils/mod.rs b/src/server/utils/mod.rs index 7663a74..2036f50 100644 --- a/src/server/utils/mod.rs +++ b/src/server/utils/mod.rs @@ -1,2 +1,3 @@ -pub mod token; pub mod path; +pub mod ratelimit; +pub mod token; diff --git a/src/server/utils/ratelimit.rs b/src/server/utils/ratelimit.rs new file mode 100644 index 0000000..64ae96f --- /dev/null +++ b/src/server/utils/ratelimit.rs @@ -0,0 +1,58 @@ +use std::{net::IpAddr, num::NonZeroU32, time::Duration}; + +use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; +use governor::{clock::DefaultClock, state::keyed::DashMapStateStore, Quota, RateLimiter}; +use once_cell::sync::Lazy; +use tracing::warn; + +use crate::server::error::{Error, Result}; + +type Limiter = RateLimiter, DefaultClock>; + +static LIMITER_LOGIN: Lazy = Lazy::new(|| { + let seconds = Duration::from_secs(60); + let burst = NonZeroU32::new(10).expect("Non-zero login ratelimit burst"); + RateLimiter::keyed( + Quota::with_period(seconds) + .expect("Non-zero login ratelimit seconds") + .allow_burst(burst), + ) +}); + +pub fn check_limit_login(ip: &IpAddr) -> Result<()> { + match LIMITER_LOGIN.check_key(ip) { + Ok(_) => Ok(()), + Err(_e) => Err(Error::RateLimit), + } +} + +pub struct ClientIp(pub IpAddr); + +const X_FORWARDED_FOR: &str = "x-forwarded-for"; + +#[async_trait] +impl FromRequestParts for ClientIp +where + S: Send + Sync, +{ + type Rejection = Error; + + async fn from_request_parts(req: &mut Parts, _state: &S) -> Result { + let addr = req + .headers + .get(X_FORWARDED_FOR) + .and_then(|hv| hv.to_str().ok()) + .and_then(|ip| { + match ip.find(',') { + Some(idx) => &ip[..idx], + None => ip, + } + .parse() + .map_err(|_| warn!("'{}' header is malformed: {}", X_FORWARDED_FOR, ip)) + .ok() + }) + .unwrap_or_else(|| "0.0.0.0".parse().unwrap()); + + Ok(Self(addr)) + } +}