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