-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from thibault-cne/feature/rate-limiter
Feature/rate limiter
- Loading branch information
Showing
26 changed files
with
362 additions
and
23 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
use rocket::fairing::{Fairing, Info, Kind}; | ||
use rocket::{uri, Data, Orbit, Request, Rocket}; | ||
|
||
use infrastructure::ConnectionPool; | ||
|
||
use crate::fairings::rate_limiter::SlidingWindow; | ||
use crate::fallbacks::rocket_uri_macro_internal_ressource; | ||
|
||
pub struct Formula1Helmet; | ||
|
||
#[rocket::async_trait] | ||
impl Fairing for Formula1Helmet { | ||
fn info(&self) -> Info { | ||
Info { | ||
name: "Formula1Helmet", | ||
kind: Kind::Liftoff | Kind::Request, | ||
} | ||
} | ||
|
||
async fn on_liftoff(&self, rocket: &Rocket<Orbit>) { | ||
if rocket.config().ip_header.is_none() | ||
|| rocket.state::<ConnectionPool>().is_none() | ||
|| rocket.state::<SlidingWindow>().is_none() | ||
{ | ||
rocket.shutdown().notify() | ||
} | ||
} | ||
|
||
async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) { | ||
if req.uri().path().as_str().starts_with("/fallback") { | ||
req.set_uri(uri!("/fallback", internal_ressource)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod helmet; | ||
pub mod rate_limiter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
use chrono::{DateTime, Duration, Utc}; | ||
use redis::Commands; | ||
use rocket::fairing::{Fairing, Info, Kind}; | ||
use rocket::{uri, Data, Request}; | ||
|
||
use infrastructure::ConnectionPool; | ||
|
||
use crate::fallbacks::rocket_uri_macro_rate_limiter_fallback; | ||
|
||
const RATE_LIMITER_KEY_PREFIX: &str = "RATE_LIMITER_"; | ||
|
||
pub struct RateLimiter; | ||
|
||
#[rocket::async_trait] | ||
impl Fairing for RateLimiter { | ||
fn info(&self) -> Info { | ||
Info { | ||
name: "RateLimiter", | ||
kind: Kind::Request, | ||
} | ||
} | ||
|
||
async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) { | ||
// SAFETY: This values should always be defined | ||
let ip_header = req | ||
.rocket() | ||
.config() | ||
.ip_header | ||
.as_ref() | ||
.unwrap() | ||
.to_string(); | ||
let pool = req.rocket().state::<ConnectionPool>().unwrap(); | ||
let window = req.rocket().state::<SlidingWindow>().unwrap(); | ||
let rate_limiter = &mut pool.cache.get().unwrap(); | ||
|
||
let ip_addr = match req.real_ip() { | ||
Some(ip_addr) => ip_addr, | ||
None => { | ||
req.set_uri(uri!("/fallback", rate_limiter_fallback(Some(ip_header), _))); | ||
return; | ||
} | ||
}; | ||
|
||
let key = format!("{}{}", RATE_LIMITER_KEY_PREFIX, ip_addr); | ||
let now = Utc::now(); | ||
let timestamps: String = match rate_limiter.get(&key) { | ||
Ok(res) => res, | ||
Err(_) => { | ||
rate_limiter | ||
.set::<String, String, ()>( | ||
key, | ||
serde_json::to_string(&[(now.timestamp(), now.timestamp_subsec_nanos())]) | ||
.unwrap(), | ||
) | ||
.unwrap(); | ||
return; | ||
} | ||
}; | ||
|
||
let window_validation = |date| now - date > window.duration; | ||
let mut timestamps = cleanup( | ||
serde_json::from_str(×tamps).unwrap(), | ||
window_validation, | ||
); | ||
|
||
if timestamps.len() == window.request_num { | ||
// SAFETY: timestamps is not empty and secs and nsecs | ||
// are from DateTime::timestamp and DateTime::timestamp_subsec_nanos | ||
let first = timestamps | ||
.first() | ||
.map(|&(secs, nsecs)| DateTime::from_timestamp(secs, nsecs).unwrap()) | ||
.unwrap(); | ||
let time_to_wait = (window.duration - (now - first)).num_seconds(); | ||
req.set_uri(uri!( | ||
"/fallback", | ||
rate_limiter_fallback(_, Some(time_to_wait)) | ||
)); | ||
return; | ||
} | ||
|
||
timestamps.push((now.timestamp(), now.timestamp_subsec_nanos())); | ||
rate_limiter | ||
.set::<String, String, ()>(key, serde_json::to_string(×tamps).unwrap()) | ||
.unwrap(); | ||
} | ||
} | ||
|
||
pub struct SlidingWindow { | ||
request_num: usize, | ||
duration: Duration, | ||
} | ||
|
||
impl SlidingWindow { | ||
/// Create a sliding window of `request_num` requests over a given `duration`. | ||
pub fn new(request_num: usize, duration: Duration) -> Self { | ||
Self { | ||
request_num, | ||
duration, | ||
} | ||
} | ||
} | ||
|
||
fn cleanup<F>(timestamps: Vec<(i64, u32)>, mut f: F) -> Vec<(i64, u32)> | ||
where | ||
F: FnMut(DateTime<Utc>) -> bool, | ||
{ | ||
timestamps | ||
.into_iter() | ||
.skip_while(|&(secs, nsecs)| { | ||
// SAFETY: secs and nsecs are from DateTime::timestamp | ||
// and DateTime::timestamp_subsec_nanos | ||
let date = DateTime::from_timestamp(secs, nsecs).unwrap(); | ||
f(date) | ||
}) | ||
.collect() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
use rocket::{get, routes, Route}; | ||
|
||
use shared::{error, prelude::*}; | ||
|
||
#[get("/rate_limiter?<header_not_found>&<too_many_requests>")] | ||
fn rate_limiter_fallback( | ||
header_not_found: Option<String>, | ||
too_many_requests: Option<i64>, | ||
) -> Result<()> { | ||
if let Some(ip_header) = header_not_found { | ||
return Err( | ||
error!(IpHeaderNotFound => "ip header not found, expected to find it under the `{}` header", ip_header), | ||
); | ||
} | ||
|
||
if let Some(time_to_wait) = too_many_requests { | ||
return Err( | ||
error!(RateLimitReached => "you reached the rate limit, please wait `{}s` before your next request", time_to_wait), | ||
); | ||
} | ||
|
||
Err( | ||
error!(InternalServerError => "rate_limiter_fallback should not be called without any parameters, please open an issue"), | ||
) | ||
} | ||
|
||
#[get("/internal_ressource")] | ||
fn internal_ressource() -> Result<()> { | ||
Err( | ||
error!(InternalRessource => "this ressource is intended for internal purposes you can't access it"), | ||
) | ||
} | ||
|
||
pub fn handlers() -> Vec<Route> { | ||
routes![rate_limiter_fallback, internal_ressource] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.