diff --git a/Cargo.lock b/Cargo.lock index 5a54a8e..f9257bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,6 +1193,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + [[package]] name = "funty" version = "2.0.0" @@ -1217,6 +1227,7 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1305,6 +1316,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[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.28" @@ -1425,6 +1442,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "governor" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "821239e5672ff23e2a7060901fa622950bbd80b649cdaadd78d1c1767ed14eb4" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "quanta", + "rand", + "smallvec", +] + [[package]] name = "h2" version = "0.3.20" @@ -2013,6 +2048,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -2214,6 +2258,12 @@ dependencies = [ "tracing-subscriber", ] +[[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.3" @@ -2224,6 +2274,18 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +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 = "nu-ansi-term" version = "0.46.0" @@ -2756,6 +2818,22 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2807,6 +2885,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -3451,6 +3538,7 @@ dependencies = [ "tower", "tower-http", "tower-sessions", + "tower_governor", "tracing", "tracing-subscriber", "trust-dns-resolver", @@ -4277,6 +4365,26 @@ dependencies = [ "uuid", ] +[[package]] +name = "tower_governor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db81d9313372d714152194f3f2b66badda23a783fb6a97462e35f632814f4cff" +dependencies = [ + "axum", + "forwarded-header-value", + "futures", + "futures-core", + "governor", + "http", + "pin-project", + "thiserror", + "tokio", + "tower", + "tower-layer", + "tracing", +] + [[package]] name = "tracing" version = "0.1.37" diff --git a/server/Cargo.toml b/server/Cargo.toml index 671adf1..39b8e6b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -32,5 +32,8 @@ tera = "1.19.0" tower-sessions = {version = "0.3.1"} time = { workspace = true } +# login rate limits +tower_governor = "0.1" + [dependencies.entities] path = "../entities" \ No newline at end of file diff --git a/server/src/lib.rs b/server/src/lib.rs index e9769fc..81a8108 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -7,7 +7,7 @@ use axum::{ http::HeaderValue, response::{Html, Redirect}, routing::{get, get_service}, - Router, + Router, BoxError, }; use chrono::TimeZone; use entities::state::{scanner::ScannerConfig, AppState}; @@ -17,6 +17,7 @@ use sea_orm::DatabaseConnection; use tera::{Tera, from_value, to_value}; use thiserror::Error; use tower::ServiceBuilder; +use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder}; use tower_http::{ cors::CorsLayer, limit::RequestBodyLimitLayer, @@ -108,6 +109,21 @@ pub async fn start( login_client, }; + let per_ip_governor_conf = Box::new( + GovernorConfigBuilder::default() + .per_second(2) + .burst_size(2) + .finish() + .unwrap(), + ); + let rate_limit_layer = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|e: BoxError| async move { + tower_governor::errors::display_error(e) // too many requests + })) + .layer(GovernorLayer { + config: Box::leak(per_ip_governor_conf), + }); + let router = Router::new() .nest_service( "/static", @@ -119,7 +135,7 @@ pub async fn start( .route("/errors/:host", get(admin::errors_view)) // .route("/history/:host", get(admin::history_view)) // .route("/api/history", get(admin::history_json)) - .route("/login", get(admin::login_view).post(admin::login)) + .route("/login", get(admin::login_view).post(admin::login).route_layer(rate_limit_layer)) .route("/logout", get(admin::logout)) // .layer(ServiceBuilder::new().layer(SetResponseHeaderLayer::overriding(header::CACHE_CONTROL, "must-revalidate"))) .layer(session_service)