mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-14 13:36:59 +00:00
163 lines
4.9 KiB
Rust
163 lines
4.9 KiB
Rust
use async_trait::async_trait;
|
|
use log::info;
|
|
use rocket::fairing::{Fairing, Info, Kind};
|
|
use rocket::http::uri::Origin;
|
|
use rocket::http::{Method, Status};
|
|
use rocket::request::{FromRequest, Outcome};
|
|
use rocket::serde::json::Json;
|
|
use rocket::{Data, Request, Response, State};
|
|
|
|
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
|
|
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
|
|
|
|
use authifier::models::Session;
|
|
|
|
use crate::ratelimiter::RequestKind;
|
|
use crate::ratelimiter::{RatelimitInformation, Ratelimiter};
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct RocketRequestKind;
|
|
|
|
impl RequestKind for RocketRequestKind {
|
|
type R<'a> = Request<'a>;
|
|
}
|
|
|
|
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<RocketRequestKind>;
|
|
|
|
/// Find the remote IP of the client
|
|
fn to_ip(request: &'_ rocket::Request<'_>) -> String {
|
|
request
|
|
.remote()
|
|
.map(|x| x.ip().to_string())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Find the actual IP of the client
|
|
fn to_real_ip(request: &'_ rocket::Request<'_>) -> String {
|
|
if let Ok(true) = std::env::var("TRUST_CLOUDFLARE").map(|x| x == "1") {
|
|
request
|
|
.headers()
|
|
.get_one("CF-Connecting-IP")
|
|
.map(|x| x.to_string())
|
|
.unwrap_or_else(|| to_ip(request))
|
|
} else {
|
|
to_ip(request)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for Ratelimiter {
|
|
type Error = Ratelimiter;
|
|
|
|
async fn from_request<'a>(request: &'r rocket::Request<'a>) -> Outcome<Self, Self::Error> {
|
|
let ratelimiter = request
|
|
.local_cache_async(async {
|
|
use rocket::outcome::Outcome;
|
|
|
|
let storage = request.guard::<&State<RatelimitStorage>>().await.unwrap();
|
|
|
|
let identifier = if let Outcome::Success(session) = request.guard::<Session>().await
|
|
{
|
|
session.id
|
|
} else {
|
|
to_real_ip(request)
|
|
};
|
|
|
|
let (bucket, resource) = storage.resolver.resolve_bucket(request);
|
|
let limit = storage.resolver.resolve_bucket_limit(bucket);
|
|
|
|
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource))
|
|
})
|
|
.await;
|
|
|
|
match ratelimiter {
|
|
Ok(ratelimiter) => Outcome::Success(*ratelimiter),
|
|
Err(ratelimiter) => Outcome::Error((Status::TooManyRequests, *ratelimiter)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl OpenApiFromRequest<'_> for Ratelimiter {
|
|
fn from_request_input(
|
|
_gen: &mut OpenApiGenerator,
|
|
_name: String,
|
|
_required: bool,
|
|
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
|
Ok(RequestHeaderInput::None)
|
|
}
|
|
}
|
|
|
|
/// Attach ratelimiter to the Rocket application
|
|
pub struct RatelimitFairing;
|
|
|
|
#[async_trait]
|
|
impl Fairing for RatelimitFairing {
|
|
fn info(&self) -> Info {
|
|
Info {
|
|
name: "Ratelimiter",
|
|
kind: Kind::Request | Kind::Response,
|
|
}
|
|
}
|
|
|
|
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
|
|
use rocket::outcome::Outcome;
|
|
if let Outcome::Error(_) = request.guard::<Ratelimiter>().await {
|
|
info!(
|
|
"User rate-limited on route {}! (IP = {:?})",
|
|
request.uri(),
|
|
to_real_ip(request)
|
|
);
|
|
|
|
request.set_method(Method::Get);
|
|
request.set_uri(Origin::parse("/ratelimit").unwrap())
|
|
}
|
|
}
|
|
|
|
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
|
|
let guard = request.guard::<Ratelimiter>().await;
|
|
let (Outcome::Success(ratelimiter) | Outcome::Error((_, ratelimiter))) = guard else {
|
|
unreachable!()
|
|
};
|
|
let Ratelimiter {
|
|
key,
|
|
limit,
|
|
remaining,
|
|
reset,
|
|
} = ratelimiter;
|
|
|
|
response.set_raw_header("X-RateLimit-Limit", limit.to_string());
|
|
response.set_raw_header("X-RateLimit-Bucket", key.to_string());
|
|
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
|
|
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
|
|
|
|
if guard.is_error() {
|
|
response.set_status(Status::TooManyRequests);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<'r> FromRequest<'r> for RatelimitInformation {
|
|
type Error = u128;
|
|
|
|
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
|
|
let info = match request.guard::<Ratelimiter>().await {
|
|
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
|
|
Outcome::Error((_, ratelimiter)) => RatelimitInformation::Failure {
|
|
retry_after: ratelimiter.reset,
|
|
},
|
|
_ => unreachable!(),
|
|
};
|
|
Outcome::Success(info)
|
|
}
|
|
}
|
|
|
|
#[rocket::get("/ratelimit")]
|
|
fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
|
|
Json(info)
|
|
}
|
|
|
|
pub fn routes() -> Vec<rocket::Route> {
|
|
rocket::routes![ratelimit_info]
|
|
}
|