Skip to content

Commit 281a573

Browse files
authored
Merge pull request #6875 from ferrous-systems/pa-rate-limits-part2
Add application-level rate limits for crate updates, yanking and unyanking
2 parents 72d2346 + 7ef5ac8 commit 281a573

File tree

17 files changed

+462
-114
lines changed

17 files changed

+462
-114
lines changed

config/nginx.conf.erb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,6 @@ http {
185185
client_body_timeout 30;
186186
client_max_body_size 50m;
187187

188-
limit_req_zone $remote_addr zone=publish:10m rate=1r/m;
189-
190188
upstream app_server {
191189
server localhost:8888 fail_timeout=0;
192190
}
@@ -262,12 +260,5 @@ http {
262260
proxy_pass http://app_server;
263261
}
264262
<% end %>
265-
266-
location ~ ^/api/v./crates/new$ {
267-
proxy_pass http://app_server;
268-
269-
limit_req zone=publish burst=30 nodelay;
270-
limit_req_status 429;
271-
}
272263
}
273264
}

src/app.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::downloads_counter::DownloadsCounter;
1010
use crate::email::Emails;
1111
use crate::github::{GitHubClient, RealGitHubClient};
1212
use crate::metrics::{InstanceMetrics, ServiceMetrics};
13+
use crate::rate_limiter::RateLimiter;
1314
use crate::storage::Storage;
1415
use axum::extract::{FromRef, FromRequestParts, State};
1516
use diesel::r2d2;
@@ -68,6 +69,9 @@ pub struct App {
6869

6970
/// In-flight request counters for the `balance_capacity` middleware.
7071
pub balance_capacity: BalanceCapacityState,
72+
73+
/// Rate limit select actions.
74+
pub rate_limiter: RateLimiter,
7175
}
7276

7377
impl App {
@@ -178,6 +182,7 @@ impl App {
178182
http_client,
179183
fastboot_client,
180184
balance_capacity: Default::default(),
185+
rate_limiter: RateLimiter::new(config.rate_limiter.clone()),
181186
config,
182187
}
183188
}

src/config/server.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ use anyhow::{anyhow, Context};
22
use ipnetwork::IpNetwork;
33
use oauth2::{ClientId, ClientSecret};
44

5-
use crate::rate_limiter::RateLimiter;
5+
use crate::rate_limiter::{LimitedAction, RateLimiterConfig};
66
use crate::{env, env_optional, Env};
77

88
use super::base::Base;
99
use super::database_pools::DatabasePools;
1010
use crate::config::balance_capacity::BalanceCapacityConfig;
1111
use crate::storage::StorageConfig;
1212
use http::HeaderValue;
13-
use std::collections::HashSet;
13+
use std::collections::{HashMap, HashSet};
1414
use std::net::IpAddr;
1515
use std::time::Duration;
1616

@@ -30,7 +30,7 @@ pub struct Server {
3030
pub gh_client_secret: ClientSecret,
3131
pub max_upload_size: u64,
3232
pub max_unpack_size: u64,
33-
pub rate_limiter: RateLimiter,
33+
pub rate_limiter: HashMap<LimitedAction, RateLimiterConfig>,
3434
pub new_version_rate_limit: Option<u32>,
3535
pub blocked_traffic: Vec<(String, Vec<String>)>,
3636
pub max_allowed_page_offset: u32,
@@ -140,6 +140,24 @@ impl Default for Server {
140140
.map(|s| s.parse().expect("SERVER_THREADS was not a valid number"))
141141
.ok();
142142

143+
// Dynamically load the configuration for all the rate limiting actions. See
144+
// `src/rate_limiter.rs` for their definition.
145+
let mut rate_limiter = HashMap::new();
146+
for action in LimitedAction::VARIANTS {
147+
let env_var_key = action.env_var_key();
148+
rate_limiter.insert(
149+
*action,
150+
RateLimiterConfig {
151+
rate: Duration::from_secs(
152+
env_optional(&format!("RATE_LIMITER_{env_var_key}_RATE_SECONDS"))
153+
.unwrap_or_else(|| action.default_rate_seconds()),
154+
),
155+
burst: env_optional(&format!("RATE_LIMITER_{env_var_key}_BURST"))
156+
.unwrap_or_else(|| action.default_burst()),
157+
},
158+
);
159+
}
160+
143161
Server {
144162
db: DatabasePools::full_from_environment(&base),
145163
storage: StorageConfig::from_environment(),
@@ -153,7 +171,7 @@ impl Default for Server {
153171
gh_client_secret: ClientSecret::new(env("GH_CLIENT_SECRET")),
154172
max_upload_size: 10 * 1024 * 1024, // 10 MB default file upload size limit
155173
max_unpack_size: 512 * 1024 * 1024, // 512 MB max when decompressed
156-
rate_limiter: Default::default(),
174+
rate_limiter,
157175
new_version_rate_limit: env_optional("MAX_NEW_VERSIONS_DAILY"),
158176
blocked_traffic: blocked_traffic(),
159177
max_allowed_page_offset: env_optional("WEB_MAX_ALLOWED_PAGE_OFFSET").unwrap_or(200),

src/controllers/krate/publish.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::models::{
1919

2020
use crate::middleware::log_request::RequestLogExt;
2121
use crate::models::token::EndpointScope;
22+
use crate::rate_limiter::LimitedAction;
2223
use crate::schema::*;
2324
use crate::util::errors::{cargo_err, internal, AppResult};
2425
use crate::util::Maximums;
@@ -101,6 +102,14 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
101102
))
102103
})?;
103104

105+
// Use a different rate limit whether this is a new or an existing crate.
106+
let rate_limit_action = match existing_crate {
107+
Some(_) => LimitedAction::PublishUpdate,
108+
None => LimitedAction::PublishNew,
109+
};
110+
app.rate_limiter
111+
.check_rate_limit(user.id, rate_limit_action, conn)?;
112+
104113
// Create a transaction on the database, if there are no errors,
105114
// commit the transactions to record a new or updated crate.
106115
conn.transaction(|conn| {
@@ -137,7 +146,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
137146
};
138147

139148
let license_file = new_crate.license_file.as_deref();
140-
let krate = persist.create_or_update(conn, user.id, Some(&app.config.rate_limiter))?;
149+
let krate = persist.create_or_update(conn, user.id)?;
141150

142151
let owners = krate.owners(conn)?;
143152
if user.rights(&app, &owners)? < Rights::Publish {

src/controllers/version/yank.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::controllers::cargo_prelude::*;
88
use crate::models::token::EndpointScope;
99
use crate::models::Rights;
1010
use crate::models::{insert_version_owner_action, VersionAction};
11+
use crate::rate_limiter::LimitedAction;
1112
use crate::schema::versions;
1213

1314
/// Handles the `DELETE /crates/:crate_id/:version/yank` route.
@@ -58,6 +59,10 @@ fn modify_yank(
5859
.for_crate(crate_name)
5960
.check(req, conn)?;
6061

62+
state
63+
.rate_limiter
64+
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, conn)?;
65+
6166
let (version, krate) = version_and_crate(conn, crate_name, version)?;
6267
let api_token_id = auth.api_token_id();
6368
let user = auth.user();

src/downloads_counter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ mod tests {
439439
name: "foo",
440440
..NewCrate::default()
441441
}
442-
.create_or_update(conn, user.id, None)
442+
.create_or_update(conn, user.id)
443443
.expect("failed to create crate");
444444

445445
Self {

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub mod github;
4646
pub mod headers;
4747
pub mod metrics;
4848
pub mod middleware;
49-
mod rate_limiter;
49+
pub mod rate_limiter;
5050
pub mod schema;
5151
pub mod sql;
5252
pub mod ssh;

src/models/krate.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use crate::models::{
1717
use crate::util::errors::{cargo_err, AppResult};
1818

1919
use crate::models::helpers::with_count::*;
20-
use crate::rate_limiter::RateLimiter;
2120
use crate::schema::*;
2221
use crate::sql::canon_crate_name;
2322

@@ -103,12 +102,7 @@ pub struct NewCrate<'a> {
103102
}
104103

105104
impl<'a> NewCrate<'a> {
106-
pub fn create_or_update(
107-
self,
108-
conn: &mut PgConnection,
109-
uploader: i32,
110-
rate_limit: Option<&RateLimiter>,
111-
) -> AppResult<Crate> {
105+
pub fn create_or_update(self, conn: &mut PgConnection, uploader: i32) -> AppResult<Crate> {
112106
use diesel::update;
113107

114108
self.validate()?;
@@ -118,9 +112,6 @@ impl<'a> NewCrate<'a> {
118112
// To avoid race conditions, we try to insert
119113
// first so we know whether to add an owner
120114
if let Some(krate) = self.save_new_crate(conn, uploader)? {
121-
if let Some(rate_limit) = rate_limit {
122-
rate_limit.check_rate_limit(uploader, conn)?;
123-
}
124115
return Ok(krate);
125116
}
126117

0 commit comments

Comments
 (0)