Skip to content

Commit 1097171

Browse files
committed
Auto merge of #3790 - pietroalbini:pa-rate-limit-updates, r=jtgeibel
Move new versions rate limits to the application This PR refactors the rate limiting code to support more kinds of rate limits, and moves the new versions rate limit from nginx to the application. See #3780 for the rationale of this change. * The rate limiting for new publishes of existing crates is now tracked per-user instead of per-IP, with the ability to add overrides similar to the rate limiting for new crates. The limit is still the usual 1 version/min with a burst of 30, but in practice this **could lower** the rate limiting, as before the limit was between 30 and 60 depending how lucky users were with the load balancing. * `PublishRateLimit` is now called `RateLimiter`, and for every operation it requires a `LimitedAction`. This allows different rate limits to exist for `LimitedAction::PublishNew` and `LimitedAction::PublishExisting`, and adding new kinds of rate limits only requires adding a new enum variant. * The environment variables to override the default rate limits changed to `RATE_LIMITING_{KIND}_RATE_SECONDS` and `RATE_LIMITING_{KIND}_BURST`. There are two things that will be done in followup PRs: * Remove the default for the `action` column. * Rename the `publish_limit_buckets` to `rate_limit_buckets` and `publish_rate_overrides` to `rate_limit_overrides`
2 parents 6be0846 + 1a5b0e8 commit 1097171

25 files changed

+1212
-531
lines changed

config/nginx.conf.erb

-9
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,6 @@ http {
176176
client_body_timeout 30;
177177
client_max_body_size 50m;
178178

179-
limit_req_zone $remote_addr zone=publish:10m rate=1r/m;
180-
181179
upstream app_server {
182180
server localhost:8888 fail_timeout=0;
183181
}
@@ -261,12 +259,5 @@ http {
261259
proxy_pass http://app_server;
262260
}
263261
<% end %>
264-
265-
location ~ ^/api/v./crates/new$ {
266-
proxy_pass http://app_server;
267-
268-
limit_req zone=publish burst=30 nodelay;
269-
limit_req_status 429;
270-
}
271262
}
272263
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
DELETE FROM publish_limit_buckets WHERE action != 0;
2+
ALTER TABLE publish_limit_buckets DROP CONSTRAINT publish_limit_buckets_pkey;
3+
ALTER TABLE publish_limit_buckets ADD CONSTRAINT publish_limit_buckets_pkey PRIMARY KEY (user_id);
4+
ALTER TABLE publish_limit_buckets DROP COLUMN action;
5+
6+
DELETE FROM publish_rate_overrides WHERE action != 0;
7+
ALTER TABLE publish_rate_overrides DROP CONSTRAINT publish_rate_overrides_pkey;
8+
ALTER TABLE publish_rate_overrides ADD CONSTRAINT publish_rate_overrides_pkey PRIMARY KEY (user_id);
9+
ALTER TABLE publish_rate_overrides DROP COLUMN action;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ALTER TABLE publish_limit_buckets ADD COLUMN action INTEGER NOT NULL DEFAULT 0;
2+
ALTER TABLE publish_limit_buckets DROP CONSTRAINT publish_limit_buckets_pkey;
3+
ALTER TABLE publish_limit_buckets ADD CONSTRAINT publish_limit_buckets_pkey PRIMARY KEY (user_id, action);
4+
5+
ALTER TABLE publish_rate_overrides ADD COLUMN action INTEGER NOT NULL DEFAULT 0;
6+
ALTER TABLE publish_rate_overrides DROP CONSTRAINT publish_rate_overrides_pkey;
7+
ALTER TABLE publish_rate_overrides ADD CONSTRAINT publish_rate_overrides_pkey PRIMARY KEY (user_id, action);

src/app.rs

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::downloads_counter::DownloadsCounter;
88
use crate::email::Emails;
99
use crate::github::GitHubClient;
1010
use crate::metrics::{InstanceMetrics, ServiceMetrics};
11+
use crate::rate_limiter::RateLimiter;
1112
use diesel::r2d2;
1213
use oauth2::basic::BasicClient;
1314
use reqwest::blocking::Client;
@@ -43,6 +44,8 @@ pub struct App {
4344
/// Metrics related to this specific instance of the service
4445
pub instance_metrics: InstanceMetrics,
4546

47+
pub rate_limiter: RateLimiter,
48+
4649
/// A configured client for outgoing HTTP requests
4750
///
4851
/// In production this shares a single connection pool across requests. In tests
@@ -165,6 +168,7 @@ impl App {
165168
read_only_replica_database: replica_database,
166169
github,
167170
github_oauth,
171+
rate_limiter: RateLimiter::new(config.rate_limiter.clone()),
168172
config,
169173
downloads_counter: DownloadsCounter::new(),
170174
emails: Emails::from_environment(),

src/config.rs

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use crate::publish_rate_limit::PublishRateLimit;
1+
use crate::rate_limiter::{LimitedAction, RateLimiterConfig};
22
use crate::{env, env_optional, uploaders::Uploader, Env};
3+
use std::collections::HashMap;
4+
use std::time::Duration;
35

46
mod base;
57
mod database_pools;
@@ -16,7 +18,6 @@ pub struct Server {
1618
pub gh_base_url: String,
1719
pub max_upload_size: u64,
1820
pub max_unpack_size: u64,
19-
pub publish_rate_limit: PublishRateLimit,
2021
pub blocked_traffic: Vec<(String, Vec<String>)>,
2122
pub max_allowed_page_offset: u32,
2223
pub page_offset_ua_blocklist: Vec<String>,
@@ -27,6 +28,7 @@ pub struct Server {
2728
pub metrics_authorization_token: Option<String>,
2829
pub use_test_database_pool: bool,
2930
pub instance_metrics_log_every_seconds: Option<u64>,
31+
pub rate_limiter: HashMap<LimitedAction, RateLimiterConfig>,
3032
}
3133

3234
impl Default for Server {
@@ -64,12 +66,34 @@ impl Default for Server {
6466
.split(',')
6567
.map(ToString::to_string)
6668
.collect();
69+
6770
let page_offset_ua_blocklist = match env_optional::<String>("WEB_PAGE_OFFSET_UA_BLOCKLIST")
6871
{
6972
None => vec![],
7073
Some(s) if s.is_empty() => vec![],
7174
Some(s) => s.split(',').map(String::from).collect(),
7275
};
76+
77+
// Dynamically load the configuration for all the rate limiting actions. See
78+
// `src/rate_limiter.rs` for their definition.
79+
let mut rate_limiter = HashMap::new();
80+
for action in LimitedAction::VARIANTS {
81+
rate_limiter.insert(
82+
*action,
83+
RateLimiterConfig {
84+
rate: Duration::from_secs(
85+
env_optional(&format!(
86+
"RATE_LIMITER_{}_RATE_SECONDS",
87+
action.env_var_key()
88+
))
89+
.unwrap_or_else(|| action.default_rate_seconds()),
90+
),
91+
burst: env_optional(&format!("RATE_LIMITER_{}_BURST", action.env_var_key()))
92+
.unwrap_or_else(|| action.default_burst()),
93+
},
94+
);
95+
}
96+
7397
Server {
7498
db: DatabasePools::full_from_environment(),
7599
base: Base::from_environment(),
@@ -79,7 +103,6 @@ impl Default for Server {
79103
gh_base_url: "https://api.github.com".to_string(),
80104
max_upload_size: 10 * 1024 * 1024, // 10 MB default file upload size limit
81105
max_unpack_size: 512 * 1024 * 1024, // 512 MB max when decompressed
82-
publish_rate_limit: Default::default(),
83106
blocked_traffic: blocked_traffic(),
84107
max_allowed_page_offset: env_optional("WEB_MAX_ALLOWED_PAGE_OFFSET").unwrap_or(200),
85108
page_offset_ua_blocklist,
@@ -96,6 +119,7 @@ impl Default for Server {
96119
metrics_authorization_token: dotenv::var("METRICS_AUTHORIZATION_TOKEN").ok(),
97120
use_test_database_pool: false,
98121
instance_metrics_log_every_seconds: env_optional("INSTANCE_METRICS_LOG_EVERY_SECONDS"),
122+
rate_limiter,
99123
}
100124
}
101125
}

src/controllers/krate/publish.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::models::{
1111
NewVersion, Rights, VersionAction,
1212
};
1313

14+
use crate::rate_limiter::LimitedAction;
1415
use crate::render;
1516
use crate::schema::*;
1617
use crate::util::errors::{cargo_err, AppResult};
@@ -72,6 +73,16 @@ pub fn publish(req: &mut dyn RequestExt) -> EndpointResult {
7273
))
7374
})?;
7475

76+
// Check the rate limits on the endpoint. Publishing a new crate uses a different (stricter)
77+
// limiting pool than publishing a new version of an existing crate to prevent mass squatting.
78+
let limited_action = if Crate::by_name(&new_crate.name).execute(&*conn)? == 0 {
79+
LimitedAction::PublishNew
80+
} else {
81+
LimitedAction::PublishExisting
82+
};
83+
app.rate_limiter
84+
.check_rate_limit(user.id, limited_action, &conn)?;
85+
7586
// Create a transaction on the database, if there are no errors,
7687
// commit the transactions to record a new or updated crate.
7788
conn.transaction(|| {
@@ -107,8 +118,7 @@ pub fn publish(req: &mut dyn RequestExt) -> EndpointResult {
107118
};
108119

109120
let license_file = new_crate.license_file.as_deref();
110-
let krate =
111-
persist.create_or_update(&conn, user.id, Some(&app.config.publish_rate_limit))?;
121+
let krate = persist.create_or_update(&conn, user.id)?;
112122

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

src/downloads_counter.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ mod tests {
438438
name: "foo",
439439
..NewCrate::default()
440440
}
441-
.create_or_update(conn, user.id, None)
441+
.create_or_update(conn, user.id)
442442
.expect("failed to create crate");
443443

444444
Self {

src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ pub mod git;
4444
pub mod github;
4545
pub mod metrics;
4646
pub mod middleware;
47-
mod publish_rate_limit;
47+
pub mod rate_limiter;
4848
pub mod render;
4949
pub mod schema;
5050
pub mod tasks;
5151
mod test_util;
5252
pub mod uploaders;
53+
#[macro_use]
5354
pub mod util;
5455

5556
pub mod controllers;

src/models/action.rs

+8-34
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
1-
use chrono::NaiveDateTime;
2-
use diesel::prelude::*;
3-
use diesel::{
4-
deserialize::{self, FromSql},
5-
pg::Pg,
6-
serialize::{self, Output, ToSql},
7-
sql_types::Integer,
8-
};
9-
use std::io::Write;
10-
111
use crate::models::{ApiToken, User, Version};
122
use crate::schema::*;
3+
use chrono::NaiveDateTime;
4+
use diesel::prelude::*;
135

14-
#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)]
15-
#[repr(i32)]
16-
#[sql_type = "Integer"]
17-
pub enum VersionAction {
18-
Publish = 0,
19-
Yank = 1,
20-
Unyank = 2,
6+
pg_enum! {
7+
pub enum VersionAction {
8+
Publish = 0,
9+
Yank = 1,
10+
Unyank = 2,
11+
}
2112
}
2213

2314
impl From<VersionAction> for &'static str {
@@ -38,23 +29,6 @@ impl From<VersionAction> for String {
3829
}
3930
}
4031

41-
impl FromSql<Integer, Pg> for VersionAction {
42-
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
43-
match <i32 as FromSql<Integer, Pg>>::from_sql(bytes)? {
44-
0 => Ok(VersionAction::Publish),
45-
1 => Ok(VersionAction::Yank),
46-
2 => Ok(VersionAction::Unyank),
47-
n => Err(format!("unknown version action: {}", n).into()),
48-
}
49-
}
50-
}
51-
52-
impl ToSql<Integer, Pg> for VersionAction {
53-
fn to_sql<W: Write>(&self, out: &mut Output<'_, W, Pg>) -> serialize::Result {
54-
ToSql::<Integer, Pg>::to_sql(&(*self as i32), out)
55-
}
56-
}
57-
5832
#[derive(Debug, Clone, Copy, Queryable, Identifiable, Associations)]
5933
#[belongs_to(Version)]
6034
#[belongs_to(User, foreign_key = "user_id")]

src/models/dependency.rs

+5-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
use diesel::deserialize::{self, FromSql};
2-
use diesel::pg::Pg;
3-
use diesel::sql_types::Integer;
4-
51
use crate::models::{Crate, Version};
62
use crate::schema::*;
73

@@ -32,23 +28,10 @@ pub struct ReverseDependency {
3228
pub name: String,
3329
}
3430

35-
#[derive(Copy, Clone, Serialize, Deserialize, Debug, FromSqlRow)]
36-
#[serde(rename_all = "lowercase")]
37-
#[repr(u32)]
38-
pub enum DependencyKind {
39-
Normal = 0,
40-
Build = 1,
41-
Dev = 2,
42-
// if you add a kind here, be sure to update `from_row` below.
43-
}
44-
45-
impl FromSql<Integer, Pg> for DependencyKind {
46-
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
47-
match <i32 as FromSql<Integer, Pg>>::from_sql(bytes)? {
48-
0 => Ok(DependencyKind::Normal),
49-
1 => Ok(DependencyKind::Build),
50-
2 => Ok(DependencyKind::Dev),
51-
n => Err(format!("unknown kind: {}", n).into()),
52-
}
31+
pg_enum! {
32+
pub enum DependencyKind {
33+
Normal = 0,
34+
Build = 1,
35+
Dev = 2,
5336
}
5437
}

src/models/krate.rs

+1-10
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ use crate::models::{
1515
use crate::util::errors::{cargo_err, AppResult};
1616

1717
use crate::models::helpers::with_count::*;
18-
use crate::publish_rate_limit::PublishRateLimit;
1918
use crate::schema::*;
2019

2120
#[derive(Debug, Queryable, Identifiable, Associations, Clone, Copy)]
@@ -93,12 +92,7 @@ pub struct NewCrate<'a> {
9392
}
9493

9594
impl<'a> NewCrate<'a> {
96-
pub fn create_or_update(
97-
self,
98-
conn: &PgConnection,
99-
uploader: i32,
100-
rate_limit: Option<&PublishRateLimit>,
101-
) -> AppResult<Crate> {
95+
pub fn create_or_update(self, conn: &PgConnection, uploader: i32) -> AppResult<Crate> {
10296
use diesel::update;
10397

10498
self.validate()?;
@@ -108,9 +102,6 @@ impl<'a> NewCrate<'a> {
108102
// To avoid race conditions, we try to insert
109103
// first so we know whether to add an owner
110104
if let Some(krate) = self.save_new_crate(conn, uploader)? {
111-
if let Some(rate_limit) = rate_limit {
112-
rate_limit.check_rate_limit(uploader, conn)?;
113-
}
114105
return Ok(krate);
115106
}
116107

0 commit comments

Comments
 (0)