send rate-limiting errors as JSON
This commit is contained in:
parent
5cdb9a417d
commit
98775d88b3
|
@ -1362,6 +1362,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
|
@ -1553,7 +1564,7 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
|||
|
||||
[[package]]
|
||||
name = "mensa-upb-api"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-governor",
|
||||
|
@ -2866,17 +2877,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.1"
|
||||
version = "1.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
|
|
|
@ -20,6 +20,6 @@ dotenvy = "0.15.7"
|
|||
itertools = "0.14.0"
|
||||
sqlx = "0.8.2"
|
||||
strum = "0.27.1"
|
||||
tokio = "1.41.1"
|
||||
tokio = "1.46.0"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
|
@ -5,7 +5,7 @@ license.workspace = true
|
|||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ impl FromStr for Canteen {
|
|||
"zm2" => Ok(Self::ZM2),
|
||||
"basilica" => Ok(Self::Basilica),
|
||||
"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 dish;
|
||||
pub mod endpoints;
|
||||
mod governor;
|
||||
mod menu;
|
||||
|
||||
use std::{error::Error, fmt::Display};
|
||||
use std::{error::Error, fmt::Display, sync::LazyLock};
|
||||
|
||||
pub use canteen::Canteen;
|
||||
pub use dish::{Dish, DishPrices};
|
||||
pub use governor::get_governor;
|
||||
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)]
|
||||
struct CustomError(String);
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use std::env;
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_governor::{Governor, GovernorConfigBuilder};
|
||||
use actix_governor::Governor;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use mensa_upb_api::get_governor;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use tracing::{debug, error, info, level_filters::LevelFilter};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
@ -41,7 +42,7 @@ async fn main() -> Result<()> {
|
|||
let burst_size = env::var("API_RATE_LIMIT_BURST")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(5);
|
||||
.unwrap_or(20);
|
||||
|
||||
let allowed_cors = env::var("API_CORS_ALLOWED")
|
||||
.map(|val| {
|
||||
|
@ -52,11 +53,7 @@ async fn main() -> Result<()> {
|
|||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let governor_conf = GovernorConfigBuilder::default()
|
||||
.seconds_per_request(seconds_replenish)
|
||||
.burst_size(burst_size)
|
||||
.finish()
|
||||
.unwrap();
|
||||
let governor_conf = get_governor(seconds_replenish, burst_size);
|
||||
|
||||
info!("Starting server on {}:{}", interface, port);
|
||||
|
||||
|
|
Loading…
Reference in New Issue