send rate-limiting errors as JSON
This commit is contained in:
parent
5cdb9a417d
commit
98775d88b3
|
@ -1362,6 +1362,17 @@ dependencies = [
|
||||||
"hashbrown 0.15.4",
|
"hashbrown 0.15.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-uring"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
@ -1553,7 +1564,7 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mensa-upb-api"
|
name = "mensa-upb-api"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
"actix-governor",
|
"actix-governor",
|
||||||
|
@ -2866,17 +2877,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.45.1"
|
version = "1.46.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"io-uring",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
"slab",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
|
|
|
@ -20,6 +20,6 @@ dotenvy = "0.15.7"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
sqlx = "0.8.2"
|
sqlx = "0.8.2"
|
||||||
strum = "0.27.1"
|
strum = "0.27.1"
|
||||||
tokio = "1.41.1"
|
tokio = "1.46.0"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
|
@ -5,7 +5,7 @@ license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ impl FromStr for Canteen {
|
||||||
"zm2" => Ok(Self::ZM2),
|
"zm2" => Ok(Self::ZM2),
|
||||||
"basilica" => Ok(Self::Basilica),
|
"basilica" => Ok(Self::Basilica),
|
||||||
"atrium" => Ok(Self::Atrium),
|
"atrium" => Ok(Self::Atrium),
|
||||||
invalid => Err(format!("Invalid canteen identifier: {}", invalid)),
|
invalid => Err(format!("Invalid canteen identifier: {invalid}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
use std::{net::IpAddr, str::FromStr};
|
||||||
|
|
||||||
|
use actix_governor::{
|
||||||
|
governor::{
|
||||||
|
clock::{Clock, DefaultClock, QuantaInstant},
|
||||||
|
middleware::NoOpMiddleware,
|
||||||
|
},
|
||||||
|
GovernorConfig, GovernorConfigBuilder, KeyExtractor, SimpleKeyExtractionError,
|
||||||
|
};
|
||||||
|
use actix_web::{
|
||||||
|
dev::ServiceRequest,
|
||||||
|
http::{header::ContentType, StatusCode},
|
||||||
|
HttpResponse, HttpResponseBuilder,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::USE_X_FORWARDED_HOST;
|
||||||
|
|
||||||
|
pub fn get_governor(
|
||||||
|
seconds_replenish: u64,
|
||||||
|
burst_size: u32,
|
||||||
|
) -> GovernorConfig<UserToken, NoOpMiddleware<QuantaInstant>> {
|
||||||
|
GovernorConfigBuilder::default()
|
||||||
|
.seconds_per_request(seconds_replenish)
|
||||||
|
.burst_size(burst_size)
|
||||||
|
.key_extractor(UserToken)
|
||||||
|
.finish()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||||
|
pub struct UserToken;
|
||||||
|
|
||||||
|
impl KeyExtractor for UserToken {
|
||||||
|
type Key = IpAddr;
|
||||||
|
type KeyExtractionError = SimpleKeyExtractionError<&'static str>;
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Bearer token"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||||
|
let mut ip = USE_X_FORWARDED_HOST
|
||||||
|
.then(|| {
|
||||||
|
req.headers()
|
||||||
|
.get("X-Forwarded-Host")
|
||||||
|
.and_then(|header| IpAddr::from_str(header.to_str().unwrap_or_default()).ok())
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.or_else(|| req.peer_addr().map(|socket| socket.ip()))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Self::KeyExtractionError::new(
|
||||||
|
r#"{ "code": 500, "msg": "Could not extract peer IP address from request"}"#,
|
||||||
|
)
|
||||||
|
.set_content_type(ContentType::json())
|
||||||
|
.set_status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// customers often get their own /56 prefix, apply rate-limiting per prefix instead of per
|
||||||
|
// address for IPv6
|
||||||
|
if let IpAddr::V6(ipv6) = ip {
|
||||||
|
let mut octets = ipv6.octets();
|
||||||
|
octets[7..16].fill(0);
|
||||||
|
ip = IpAddr::V6(octets.into());
|
||||||
|
}
|
||||||
|
Ok(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exceed_rate_limit_response(
|
||||||
|
&self,
|
||||||
|
negative: &actix_governor::governor::NotUntil<QuantaInstant>,
|
||||||
|
mut response: HttpResponseBuilder,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let wait_time = negative
|
||||||
|
.wait_time_from(DefaultClock::default().now())
|
||||||
|
.as_secs();
|
||||||
|
response.content_type(ContentType::json())
|
||||||
|
.body(
|
||||||
|
format!(
|
||||||
|
r#"{{"code":429, "error": "TooManyRequests", "message": "Too many requests, try again after {wait_time} seconds", "after": {wait_time}}}"#
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn whitelisted_keys(&self) -> Vec<Self::Key> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_name(&self, _key: &Self::Key) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,22 @@
|
||||||
mod canteen;
|
mod canteen;
|
||||||
mod dish;
|
mod dish;
|
||||||
pub mod endpoints;
|
pub mod endpoints;
|
||||||
|
mod governor;
|
||||||
mod menu;
|
mod menu;
|
||||||
|
|
||||||
use std::{error::Error, fmt::Display};
|
use std::{error::Error, fmt::Display, sync::LazyLock};
|
||||||
|
|
||||||
pub use canteen::Canteen;
|
pub use canteen::Canteen;
|
||||||
pub use dish::{Dish, DishPrices};
|
pub use dish::{Dish, DishPrices};
|
||||||
|
pub use governor::get_governor;
|
||||||
pub use menu::Menu;
|
pub use menu::Menu;
|
||||||
|
|
||||||
|
pub(crate) static USE_X_FORWARDED_HOST: LazyLock<bool> = LazyLock::new(|| {
|
||||||
|
std::env::var("API_USE_X_FORWARDED_HOST")
|
||||||
|
.map(|val| val == "true")
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct CustomError(String);
|
struct CustomError(String);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_governor::{Governor, GovernorConfigBuilder};
|
use actix_governor::Governor;
|
||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use mensa_upb_api::get_governor;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use tracing::{debug, error, info, level_filters::LevelFilter};
|
use tracing::{debug, error, info, level_filters::LevelFilter};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
@ -41,7 +42,7 @@ async fn main() -> Result<()> {
|
||||||
let burst_size = env::var("API_RATE_LIMIT_BURST")
|
let burst_size = env::var("API_RATE_LIMIT_BURST")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.parse::<u32>().ok())
|
.and_then(|s| s.parse::<u32>().ok())
|
||||||
.unwrap_or(5);
|
.unwrap_or(20);
|
||||||
|
|
||||||
let allowed_cors = env::var("API_CORS_ALLOWED")
|
let allowed_cors = env::var("API_CORS_ALLOWED")
|
||||||
.map(|val| {
|
.map(|val| {
|
||||||
|
@ -52,11 +53,7 @@ async fn main() -> Result<()> {
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let governor_conf = GovernorConfigBuilder::default()
|
let governor_conf = get_governor(seconds_replenish, burst_size);
|
||||||
.seconds_per_request(seconds_replenish)
|
|
||||||
.burst_size(burst_size)
|
|
||||||
.finish()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
info!("Starting server on {}:{}", interface, port);
|
info!("Starting server on {}:{}", interface, port);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue