Skip to content

Commit ecf06b1

Browse files
committed
backend: Add rate limiter for routes that need charger credentials.
1 parent 8744dfd commit ecf06b1

File tree

6 files changed

+139
-26
lines changed

6 files changed

+139
-26
lines changed

backend/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ pub(crate) mod tests {
206206
use lru::LruCache;
207207
use rand::RngCore;
208208
use rand_core::OsRng;
209-
use rate_limit::LoginRateLimiter;
209+
use rate_limit::{ChargerRateLimiter, LoginRateLimiter};
210210
use routes::user::tests::{get_test_uuid, TestUser};
211211

212212
pub struct ScopeCall<F: FnMut()> {
@@ -279,7 +279,9 @@ pub(crate) mod tests {
279279
let state = web::Data::new(state);
280280
let bridge_state = web::Data::new(bridge_state);
281281
let login_rate_limiter = web::Data::new(LoginRateLimiter::new());
282+
let charger_rate_limiter = web::Data::new(ChargerRateLimiter::new());
282283
cfg.app_data(login_rate_limiter);
284+
cfg.app_data(charger_rate_limiter);
283285
cfg.app_data(state);
284286
cfg.app_data(bridge_state);
285287
cfg.app_data(cache);

backend/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use db_connector::{get_connection_pool, run_migrations, Pool};
3535
use diesel::prelude::*;
3636
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
3737
use lru::LruCache;
38-
use rate_limit::LoginRateLimiter;
38+
use rate_limit::{ChargerRateLimiter, LoginRateLimiter};
3939
use simplelog::{ColorChoice, CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
4040
use udp_server::packet::{
4141
ManagementCommand, ManagementCommandId, ManagementCommandPacket, ManagementPacket,
@@ -175,6 +175,7 @@ async fn main() -> std::io::Result<()> {
175175
web::Data::new(Mutex::new(LruCache::new(NonZeroUsize::new(10000).unwrap())));
176176

177177
let login_ratelimiter = web::Data::new(LoginRateLimiter::new());
178+
let charger_ratelimiter = web::Data::new(ChargerRateLimiter::new());
178179

179180
HttpServer::new(move || {
180181
let cors = actix_cors::Cors::permissive();
@@ -184,6 +185,7 @@ async fn main() -> std::io::Result<()> {
184185
.app_data(cache.clone())
185186
.app_data(state.clone())
186187
.app_data(login_ratelimiter.clone())
188+
.app_data(charger_ratelimiter.clone())
187189
.app_data(bridge_state.clone())
188190
.configure(routes::configure)
189191
})

backend/src/rate_limit.rs

Lines changed: 101 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ impl IPExtractor {
6060
}
6161
}
6262

63+
fn ip_from_req(req: &HttpRequest) -> actix_web::Result<String> {
64+
let ip = if let Some(ip) = req.connection_info().realip_remote_addr() {
65+
ip.to_string()
66+
} else {
67+
println!("No ip found for route {}", req.path());
68+
return Err(crate::error::Error::InternalError.into());
69+
};
70+
71+
Ok(ip)
72+
}
73+
6374
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
6475
pub struct LoginRateLimitKey {
6576
user: String,
@@ -79,37 +90,69 @@ const REQUESTS_PER_SECOND: u32 = 5;
7990
const REQUESTS_BURST: u32 = 25;
8091

8192
// RateLimiter for the login route
82-
pub struct LoginRateLimiter {
83-
rate_limiter: RateLimiter<
93+
pub struct LoginRateLimiter(
94+
RateLimiter<
8495
LoginRateLimitKey,
8596
dashmap::DashMap<LoginRateLimitKey, InMemoryState>,
8697
QuantaClock,
8798
governor::middleware::NoOpMiddleware<governor::clock::QuantaInstant>,
8899
>,
89-
}
100+
);
90101

91102
impl LoginRateLimiter {
92103
pub fn new() -> Self {
93-
Self {
94-
rate_limiter: RateLimiter::keyed(
95-
Quota::per_second(NonZeroU32::new(REQUESTS_PER_SECOND).unwrap())
96-
.allow_burst(NonZeroU32::new(REQUESTS_BURST).unwrap()),
97-
),
98-
}
104+
Self(RateLimiter::keyed(
105+
Quota::per_second(NonZeroU32::new(REQUESTS_PER_SECOND).unwrap())
106+
.allow_burst(NonZeroU32::new(REQUESTS_BURST).unwrap()),
107+
))
99108
}
100109

101110
pub fn check(&self, email: String, req: &HttpRequest) -> actix_web::Result<()> {
102-
let ip = if let Some(ip) = req.connection_info().realip_remote_addr() {
103-
ip.to_string()
104-
} else {
105-
println!("No ip found for route {}", req.path());
106-
return Err(crate::error::Error::InternalError.into());
107-
};
111+
let ip = ip_from_req(req)?;
108112

109113
let key = LoginRateLimitKey { user: email, ip };
110-
if let Err(err) = self.rate_limiter.check_key(&key) {
114+
if let Err(err) = self.0.check_key(&key) {
111115
log::warn!("RateLimiter triggered for {:?}", key);
112-
let now = self.rate_limiter.clock().now();
116+
let now = self.0.clock().now();
117+
118+
Err(RateLimitError::new(err, now).into())
119+
} else {
120+
Ok(())
121+
}
122+
}
123+
}
124+
125+
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
126+
pub struct ChargerRateLimitKey {
127+
charger_id: String,
128+
ip: String,
129+
}
130+
131+
// Rate limiter for all routes that get called by chargers
132+
pub struct ChargerRateLimiter(
133+
RateLimiter<
134+
ChargerRateLimitKey,
135+
dashmap::DashMap<ChargerRateLimitKey, InMemoryState>,
136+
QuantaClock,
137+
governor::middleware::NoOpMiddleware<governor::clock::QuantaInstant>,
138+
>,
139+
);
140+
141+
impl ChargerRateLimiter {
142+
pub fn new() -> Self {
143+
Self(RateLimiter::keyed(
144+
Quota::per_second(NonZeroU32::new(REQUESTS_PER_SECOND).unwrap())
145+
.allow_burst(NonZeroU32::new(REQUESTS_BURST).unwrap()),
146+
))
147+
}
148+
149+
pub fn check(&self, charger_id: String, req: &HttpRequest) -> actix_web::Result<()> {
150+
let ip = ip_from_req(req)?;
151+
152+
let key = ChargerRateLimitKey { charger_id, ip };
153+
if let Err(err) = self.0.check_key(&key) {
154+
log::warn!("RateLimiter triggered for {:?}", key);
155+
let now = self.0.clock().now();
113156

114157
Err(RateLimitError::new(err, now).into())
115158
} else {
@@ -155,6 +198,8 @@ impl ResponseError for RateLimitError {
155198
mod tests {
156199
use actix_web::test;
157200

201+
use crate::rate_limit::ChargerRateLimiter;
202+
158203
use super::LoginRateLimiter;
159204

160205
#[actix_web::test]
@@ -195,4 +240,43 @@ mod tests {
195240
let ret = limiter.check(email.clone(), &req);
196241
assert!(ret.is_ok());
197242
}
243+
244+
#[actix_web::test]
245+
async fn test_charger_rate_limiter() {
246+
let limiter = ChargerRateLimiter::new();
247+
let req = test::TestRequest::get()
248+
.uri("/login")
249+
.insert_header(("X-Forwarded-For", "123.123.123.2"))
250+
.to_http_request();
251+
let email = uuid::Uuid::new_v4().to_string();
252+
253+
let ret = limiter.check(email.clone(), &req);
254+
assert!(ret.is_ok());
255+
256+
let ret = limiter.check(email.clone(), &req);
257+
assert!(ret.is_ok());
258+
259+
let ret = limiter.check(email.clone(), &req);
260+
assert!(ret.is_ok());
261+
262+
let ret = limiter.check(email.clone(), &req);
263+
assert!(ret.is_ok());
264+
265+
let ret = limiter.check(email.clone(), &req);
266+
assert!(ret.is_ok());
267+
268+
let ret = limiter.check(email.clone(), &req);
269+
assert!(ret.is_err());
270+
271+
let email2 = uuid::Uuid::new_v4().to_string();
272+
let ret = limiter.check(email2.clone(), &req);
273+
assert!(ret.is_ok());
274+
275+
let req = test::TestRequest::get()
276+
.uri("/login")
277+
.insert_header(("X-Forwarded-For", "123.123.123.3"))
278+
.to_http_request();
279+
let ret = limiter.check(email.clone(), &req);
280+
assert!(ret.is_ok());
281+
}
198282
}

backend/src/routes/charger/allow_user.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* Boston, MA 02111-1307, USA.
1818
*/
1919

20-
use actix_web::{error::ErrorBadRequest, put, web, HttpResponse, Responder};
20+
use actix_web::{error::ErrorBadRequest, put, web, HttpRequest, HttpResponse, Responder};
2121
use base64::Engine;
2222
use db_connector::models::{allowed_users::AllowedUser, wg_keys::WgKey};
2323
use diesel::prelude::*;
@@ -26,6 +26,7 @@ use utoipa::ToSchema;
2626

2727
use crate::{
2828
error::Error,
29+
rate_limit::ChargerRateLimiter,
2930
routes::{
3031
auth::login::{validate_password, FindBy},
3132
charger::add::get_charger_from_db,
@@ -124,7 +125,11 @@ async fn authenticate_user(
124125
pub async fn allow_user(
125126
state: web::Data<AppState>,
126127
allow_user: web::Json<AllowUserSchema>,
128+
rate_limiter: web::Data<ChargerRateLimiter>,
129+
req: HttpRequest,
127130
) -> Result<impl Responder, actix_web::Error> {
131+
rate_limiter.check(allow_user.charger_id.clone(), &req)?;
132+
128133
let cid = parse_uuid(&allow_user.charger_id)?;
129134

130135
let charger = get_charger_from_db(cid, &state).await?;
@@ -226,6 +231,7 @@ pub mod tests {
226231
};
227232
let req = test::TestRequest::put()
228233
.uri("/allow_user")
234+
.append_header(("X-Forwarded-For", "123.123.123.3"))
229235
.set_json(body)
230236
.to_request();
231237
let resp = test::call_service(&app, req).await;
@@ -257,6 +263,7 @@ pub mod tests {
257263
};
258264
let req = test::TestRequest::put()
259265
.uri("/allow_user")
266+
.append_header(("X-Forwarded-For", "123.123.123.3"))
260267
.set_json(allow)
261268
.to_request();
262269
let resp = test::call_service(&app, req).await;
@@ -287,6 +294,7 @@ pub mod tests {
287294
};
288295
let req = test::TestRequest::put()
289296
.uri("/allow_user")
297+
.append_header(("X-Forwarded-For", "123.123.123.3"))
290298
.set_json(allow)
291299
.to_request();
292300
let resp = test::call_service(&app, req).await;
@@ -316,6 +324,7 @@ pub mod tests {
316324
};
317325
let req = test::TestRequest::put()
318326
.uri("/allow_user")
327+
.append_header(("X-Forwarded-For", "123.123.123.3"))
319328
.set_json(allow)
320329
.to_request();
321330
let resp = test::call_service(&app, req).await;
@@ -344,6 +353,7 @@ pub mod tests {
344353
};
345354
let req = test::TestRequest::put()
346355
.uri("/allow_user")
356+
.append_header(("X-Forwarded-For", "123.123.123.3"))
347357
.set_json(allow)
348358
.to_request();
349359
let resp = test::call_service(&app, req).await;
@@ -361,6 +371,7 @@ pub mod tests {
361371
};
362372
let req = test::TestRequest::put()
363373
.uri("/allow_user")
374+
.append_header(("X-Forwarded-For", "123.123.123.3"))
364375
.set_json(allow)
365376
.to_request();
366377
let resp = test::call_service(&app, req).await;

backend/src/routes/management.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use utoipa::ToSchema;
3131

3232
use crate::{
3333
error::Error,
34+
rate_limit::ChargerRateLimiter,
3435
routes::{auth::login::FindBy, charger::add::get_charger_from_db, user::get_user_id},
3536
utils::{get_charger_by_uid, get_connection, parse_uuid, web_block_unpacked},
3637
AppState, BridgeState,
@@ -52,7 +53,7 @@ pub enum ManagementDataVersion {
5253
V2(ManagementDataVersion2),
5354
}
5455

55-
#[derive(Serialize, Deserialize, ToSchema)]
56+
#[derive(Serialize, Deserialize, ToSchema, Debug)]
5657
pub struct ConfiguredUser {
5758
pub email: String,
5859
pub name: Option<String>,
@@ -233,6 +234,7 @@ pub async fn management(
233234
state: web::Data<AppState>,
234235
data: web::Json<ManagementSchema>,
235236
bridge_state: web::Data<BridgeState>,
237+
rate_limiter: web::Data<ChargerRateLimiter>,
236238
) -> actix_web::Result<impl Responder> {
237239
use db_connector::schema::chargers::dsl as chargers;
238240

@@ -248,6 +250,8 @@ pub async fn management(
248250
let charger_id;
249251
let mut output_uuid = None;
250252
let charger = if let Some(charger_uid) = data.id {
253+
rate_limiter.check(charger_uid.to_string(), &req)?;
254+
251255
let charger = get_charger_by_uid(charger_uid, data.password.clone(), &state).await?;
252256
charger_id = charger.id;
253257
output_uuid = Some(charger_id.to_string());
@@ -256,6 +260,8 @@ pub async fn management(
256260
match &data.data {
257261
ManagementDataVersion::V1(_) => return Err(Error::ChargerCredentialsWrong.into()),
258262
ManagementDataVersion::V2(data) => {
263+
rate_limiter.check(data.id.clone(), &req)?;
264+
259265
charger_id = parse_uuid(&data.id)?;
260266
let charger = get_charger_from_db(charger_id, &state).await?;
261267
if !password_matches(&data.password, &charger.password)? {
@@ -415,7 +421,6 @@ mod tests {
415421
let req = test::TestRequest::put()
416422
.uri("/management")
417423
.append_header(("X-Forwarded-For", "123.123.123.3"))
418-
.cookie(Cookie::new("X-Forwarded-For", "123.123.123.3"))
419424
.set_json(body)
420425
.to_request();
421426
let resp: ManagementResponseSchema = test::call_and_read_body_json(&app, req).await;
@@ -447,7 +452,6 @@ mod tests {
447452
let req = test::TestRequest::put()
448453
.uri("/management")
449454
.append_header(("X-Forwarded-For", "123.123.123.3"))
450-
.cookie(Cookie::new("X-Forwarded-For", "123.123.123.3"))
451455
.set_json(body)
452456
.to_request();
453457
let resp: ManagementResponseSchema = test::call_and_read_body_json(&app, req).await;
@@ -482,7 +486,6 @@ mod tests {
482486
let req = test::TestRequest::put()
483487
.uri("/management")
484488
.append_header(("X-Forwarded-For", "123.123.123.3"))
485-
.cookie(Cookie::new("X-Forwarded-For", "123.123.123.3"))
486489
.set_json(body)
487490
.to_request();
488491
let resp: ManagementResponseSchema = test::call_and_read_body_json(&app, req).await;

0 commit comments

Comments
 (0)