-
Notifications
You must be signed in to change notification settings - Fork 645
WIP: Rate limit the publish crate endpoint #1676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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,167 @@ | ||
use conduit::Request; | ||
use std::collections::HashMap; | ||
|
||
use crate::db::{DieselPool}; | ||
use crate::middleware::app::RequestApp; | ||
use crate::models::{User}; | ||
use crate::util::errors::{CargoResult, TooManyRequests}; | ||
use std::sync::{Arc, Mutex}; | ||
|
||
use chrono::{DateTime, Duration, Utc}; | ||
|
||
/// Settings for a rate-limited route. | ||
#[derive(Debug, Clone)] | ||
pub struct RateLimitSettings { | ||
/// The code for this category. Can be stored in a database, etc. | ||
pub key: String, | ||
/// The maximum number of tokens that can be acquired. | ||
pub max_amount: usize, | ||
/// How often we refill | ||
pub refill_time: Duration, | ||
/// The number of tokens that are added during a refill. | ||
pub refill_amount: usize, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to make this configurable. It'll always be 1 for the foreseeable future. |
||
} | ||
|
||
/// The result from a rate limit check. | ||
#[derive(Debug, Clone, Copy)] | ||
pub struct RateLimitResult { | ||
/// The remaining number of requests available | ||
remaining: usize, | ||
} | ||
|
||
/// A type that can perform rate limiting. | ||
pub trait RateLimiter { | ||
fn check_limit_multiple(&self, tokens: u32, user: &User, category: RateLimitCategory) -> CargoResult<RateLimitResult>; | ||
} | ||
|
||
/// Rate limit using a postgresql database. | ||
#[allow(missing_debug_implementations)] | ||
#[derive(Clone)] | ||
pub struct RateLimiterPostgres { | ||
diesel_database: DieselPool, | ||
} | ||
|
||
impl RateLimiterPostgres { | ||
/// Create a new postgres rate limiter from the given database pool. | ||
pub fn new(diesel_database: DieselPool) -> RateLimiterPostgres { | ||
RateLimiterPostgres { | ||
diesel_database, | ||
} | ||
} | ||
} | ||
|
||
impl RateLimiter for RateLimiterPostgres { | ||
fn check_limit_multiple(&self, _tokens: u32, _user: &User, _category: RateLimitCategory) -> CargoResult<RateLimitResult> { | ||
let _conn = self.diesel_database.get()?; | ||
|
||
// TODO: Database interaction. | ||
Err(Box::new(TooManyRequests)) | ||
} | ||
} | ||
|
||
type UserId = i32; | ||
type RateLimiterMemoryKey = (UserId, RateLimitCategory); | ||
#[derive(Debug, Clone, Copy)] | ||
struct RateLimiterMemoryValue { | ||
pub value: usize, | ||
// TODO: Time. | ||
pub last_update: DateTime<Utc>, | ||
} | ||
|
||
/// Rate limit using an internal memory store. This may not be ideal in a load-balanced | ||
/// environment, unless all requests from a user get routed to the same instance. | ||
#[derive(Debug, Clone)] | ||
pub struct RateLimiterMemory { | ||
data: Arc<Mutex<HashMap<RateLimiterMemoryKey, RateLimiterMemoryValue>>>, | ||
} | ||
|
||
impl RateLimiterMemory { | ||
/// Create a new memory-based rate limiter | ||
pub fn new() -> RateLimiterMemory { | ||
RateLimiterMemory { | ||
data: Arc::new(Mutex::new(HashMap::new())), | ||
} | ||
} | ||
} | ||
|
||
impl RateLimiter for RateLimiterMemory { | ||
fn check_limit_multiple(&self, tokens: u32, user: &User, category: RateLimitCategory) -> CargoResult<RateLimitResult> { | ||
let mut data = self.data.lock().unwrap(); | ||
let settings = category.settings(); | ||
let now = Utc::now(); | ||
let mut entry = data.entry((user.id, category)).or_insert_with(|| RateLimiterMemoryValue { value: settings.max_amount, last_update: now }); | ||
println!("Previous entry: {:?}", entry); | ||
let mut now2 = now; | ||
let mut refill_count = 0; | ||
while now2 > entry.last_update + settings.refill_time { | ||
now2 = now2 - settings.refill_time; | ||
refill_count += 1; | ||
} | ||
entry.value = std::cmp::min( | ||
settings.max_amount, | ||
entry.value + refill_count * settings.refill_amount); | ||
|
||
entry.last_update = std::cmp::min( | ||
now, | ||
entry.last_update + (settings.refill_time * refill_count as i32)); | ||
|
||
if entry.value < tokens as usize { | ||
return Err(Box::new(TooManyRequests)); | ||
} | ||
|
||
entry.value -= tokens as usize; | ||
|
||
Ok(RateLimitResult { remaining: entry.value }) | ||
} | ||
} | ||
|
||
/// A rate limiter that does not limit at all. | ||
#[derive(Debug, Clone, Copy)] | ||
pub struct RateLimiterUnlimited; | ||
|
||
impl RateLimiter for RateLimiterUnlimited { | ||
fn check_limit_multiple(&self, _tokens: u32, _user: &User, category: RateLimitCategory) -> CargoResult<RateLimitResult> { | ||
println!("Unlimited rate limiter!"); | ||
let settings = category.settings(); | ||
Ok(RateLimitResult { remaining: settings.max_amount }) | ||
} | ||
} | ||
|
||
/// All of the possible rate limit buckets. When rate limiting a new endpoint, add it here and set | ||
/// the settings below. | ||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] | ||
pub enum RateLimitCategory { | ||
PublishCrate, | ||
// How often a new crate can be uploaded | ||
NewCrate, | ||
// How often an uploaded crate can be uploaded | ||
NewVersion, | ||
// How often a request for crate info can be made | ||
CrateInfo, | ||
} | ||
|
||
impl RateLimitCategory { | ||
pub fn settings(&self) -> RateLimitSettings { | ||
use RateLimitCategory::*; | ||
match *self { | ||
PublishCrate => RateLimitSettings { key: "publish-crate".into(), max_amount: 3, refill_time: Duration::seconds(60), refill_amount: 1 }, | ||
NewCrate => RateLimitSettings { key: "new-crate".into(), max_amount: 3, refill_time: Duration::seconds(60), refill_amount: 1 }, | ||
NewVersion => RateLimitSettings { key: "new_version".into(), max_amount: 60, refill_time: Duration::seconds(1), refill_amount: 1 }, | ||
CrateInfo => RateLimitSettings { key: "crate-info".into(), max_amount: 5, refill_time: Duration::seconds(10), refill_amount: 1 }, | ||
} | ||
} | ||
} | ||
|
||
/// A trait that makes it possible to call `check_rate_limit` directly on a request object. | ||
pub trait RequestRateLimit { | ||
/// Check the rate limit for the given endpoint. This function consumes a single token from the | ||
/// token bucket. | ||
fn check_rate_limit(&mut self, user: &User, category: RateLimitCategory) -> CargoResult<RateLimitResult>; | ||
} | ||
|
||
impl<T: Request + ?Sized> RequestRateLimit for T { | ||
fn check_rate_limit(&mut self, user: &User, category: RateLimitCategory) -> CargoResult<RateLimitResult> { | ||
let limiter = &self.app().rate_limiter; | ||
limiter.check_limit_multiple(1, user, category) | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have a retry after field.