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