Skip to content

Commit e9bc7e8

Browse files
committed
more ratelimits
1 parent 7f6485c commit e9bc7e8

File tree

10 files changed

+257
-196
lines changed

10 files changed

+257
-196
lines changed

Cargo.lock

Lines changed: 180 additions & 156 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/backend/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "web"
3-
version = "3.1.19"
3+
version = "3.1.20"
44
edition = "2024"
55

66
[dependencies]
@@ -14,7 +14,7 @@ axum = "0.8.1"
1414
colored = "3.0.0"
1515
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring-webpki", "postgres", "chrono", "ipnetwork", "uuid"] }
1616
dotenvy = "0.15.7"
17-
rustis = "0.16.1"
17+
rustis = "0.19.1"
1818
serde = { version = "1.0.218", features = ["derive"] }
1919
serde_json = { version = "1.0.140", features = ["preserve_order"] }
2020
tokio = { version = "1.43.0", features = ["full"] }

apps/backend/src/cache.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use crate::env::RedisMode;
1+
use crate::{env::RedisMode, response::ApiResponse};
2+
use axum::http::StatusCode;
23
use colored::Colorize;
34
use rustis::{
45
client::Client,
5-
commands::{GenericCommands, SetCondition, SetExpiration, StringCommands},
6+
commands::{GenericCommands, SetExpiration, StringCommands},
67
resp::{BulkString, cmd},
78
};
89
use serde::{Serialize, de::DeserializeOwned};
@@ -59,6 +60,43 @@ impl Cache {
5960
instance
6061
}
6162

63+
pub async fn ratelimit(
64+
&self,
65+
limit_identifier: impl AsRef<str>,
66+
limit: u64,
67+
limit_window: u64,
68+
client: impl AsRef<str>,
69+
) -> Result<(), ApiResponse> {
70+
let key = format!(
71+
"ratelimit::{}::{}",
72+
limit_identifier.as_ref(),
73+
client.as_ref()
74+
);
75+
76+
let now = chrono::Utc::now().timestamp();
77+
let expiry = self.client.expiretime(&key).await.unwrap_or_default();
78+
let expire_unix: u64 = if expiry > now + 2 {
79+
expiry as u64
80+
} else {
81+
now as u64 + limit_window
82+
};
83+
84+
let limit_used = self.client.get::<u64>(&key).await.unwrap_or_default() + 1;
85+
self.client
86+
.set_with_options(key, limit_used, None, SetExpiration::Exat(expire_unix))
87+
.await?;
88+
89+
if limit_used >= limit {
90+
return Err(ApiResponse::error(&format!(
91+
"you are ratelimited, retry in {}s",
92+
expiry - now
93+
))
94+
.with_status(StatusCode::TOO_MANY_REQUESTS));
95+
}
96+
97+
Ok(())
98+
}
99+
62100
#[tracing::instrument(skip(self, fn_compute))]
63101
pub async fn cached<T, F, Fut>(
64102
&self,
@@ -83,13 +121,7 @@ impl Cache {
83121

84122
let serialized = rmp_serde::to_vec(&result)?;
85123
self.client
86-
.set_with_options(
87-
key,
88-
serialized,
89-
SetCondition::None,
90-
SetExpiration::Ex(ttl),
91-
false,
92-
)
124+
.set_with_options(key, serialized, None, SetExpiration::Ex(ttl))
93125
.await?;
94126

95127
Ok(result)

apps/backend/src/env.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pub struct Env {
6565
pub bind: String,
6666
pub port: u16,
6767

68-
pub telemetry_ratelimit_per_day: i64,
68+
pub telemetry_ratelimit_per_day: u64,
6969

7070
pub update_prices: bool,
7171
pub sxc_token: Option<String>,

apps/backend/src/routes/auth/login/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ mod post {
5353
.ok();
5454
}
5555

56+
state
57+
.cache
58+
.ratelimit("login", 5, 60, ip.to_string())
59+
.await?;
60+
5661
if let Err(error) = state.captcha.verify(ip, data.captcha).await {
5762
return ApiResponse::error(&error)
5863
.with_status(StatusCode::BAD_REQUEST)

apps/backend/src/routes/auth/password/forgot.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ mod post {
3939
.ok();
4040
}
4141

42+
state
43+
.cache
44+
.ratelimit("password_forgot", 2, 5 * 60, ip.to_string())
45+
.await?;
46+
state
47+
.cache
48+
.ratelimit("password_forgot", 2, 5 * 60, &data.email)
49+
.await?;
50+
4251
if let Err(error) = state.captcha.verify(ip, data.captcha).await {
4352
return ApiResponse::error(&error)
4453
.with_status(StatusCode::BAD_REQUEST)

apps/backend/src/routes/auth/register.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ mod post {
5757
.ok();
5858
}
5959

60+
state
61+
.cache
62+
.ratelimit("register", 2, 5 * 60, ip.to_string())
63+
.await?;
64+
6065
if let Err(error) = state.captcha.verify(ip, data.captcha).await {
6166
return ApiResponse::error(&error)
6267
.with_status(StatusCode::BAD_REQUEST)

apps/backend/src/routes/telemetry.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,9 @@ pub fn router(state: &State) -> OpenApiRouter<State> {
2222
}
2323
};
2424

25-
let telemetry = state.telemetry.log(ip, data).await;
26-
if telemetry.is_none() {
27-
return ApiResponse::error("too many requests")
28-
.with_status(StatusCode::TOO_MANY_REQUESTS)
29-
.ok();
30-
}
25+
state.cache.ratelimit("telemetry", state.env.telemetry_ratelimit_per_day, 24 * 60 * 60, ip.to_string()).await?;
26+
27+
state.telemetry.log(ip, data).await;
3128

3229
ApiResponse::new_serialized(json!({})).ok()
3330
},

apps/backend/src/routes/user/email/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ mod patch {
4141
.ok();
4242
}
4343

44+
state
45+
.cache
46+
.ratelimit("update_email", 3, 5 * 60, ip.to_string())
47+
.await?;
48+
state
49+
.cache
50+
.ratelimit("update_email", 2, 5 * 60, user.id.to_string())
51+
.await?;
52+
4453
if let Err(error) = state.captcha.verify(ip, data.captcha).await {
4554
return ApiResponse::error(&error)
4655
.with_status(StatusCode::BAD_REQUEST)

apps/backend/src/telemetry.rs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use chrono::NaiveDateTime;
22
use colored::Colorize;
3-
use rustis::commands::{ExpireOption, GenericCommands, StringCommands};
43
use serde::{Deserialize, Serialize};
54
use sha2::Digest;
65
use sqlx::types::Uuid;
@@ -101,24 +100,7 @@ impl TelemetryLogger {
101100
}
102101

103102
#[inline]
104-
pub async fn log(&self, ip: IpAddr, telemetry: TelemetryData) -> Option<()> {
105-
let mut processing = self.processing.lock().await;
106-
107-
let ratelimit_key = format!("blueprint_api::ratelimit::{ip}");
108-
109-
let count = self.cache.client.incr(&ratelimit_key).await.ok()?;
110-
if count == 1 {
111-
self.cache
112-
.client
113-
.expire(&ratelimit_key, 86400, ExpireOption::None)
114-
.await
115-
.ok()?;
116-
}
117-
118-
if count > self.env.telemetry_ratelimit_per_day {
119-
return None;
120-
}
121-
103+
pub async fn log(&self, ip: IpAddr, telemetry: TelemetryData) {
122104
let data = Telemetry {
123105
panel_id: telemetry.id,
124106
telemetry_version: telemetry.telemetry_version as i16,
@@ -129,9 +111,7 @@ impl TelemetryLogger {
129111
created: chrono::Utc::now().naive_utc(),
130112
};
131113

132-
processing.push(data);
133-
134-
Some(())
114+
self.processing.lock().await.push(data);
135115
}
136116

137117
#[inline]

0 commit comments

Comments
 (0)