Skip to content

Commit 0000722

Browse files
Rate limit per app/api-key on the authed endpoint (#405)
* configurable rate limits per authed app * cleanup, resolve comments: --------- Co-authored-by: Haardik H <[email protected]>
1 parent 97b03f1 commit 0000722

File tree

5 files changed

+919
-164
lines changed

5 files changed

+919
-164
lines changed

crates/websocket-proxy/src/auth.rs

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
use crate::auth::AuthenticationParseError::{
22
DuplicateAPIKeyArgument, DuplicateApplicationArgument, MissingAPIKeyArgument,
3-
MissingApplicationArgument, NoData, TooManyComponents,
3+
MissingApplicationArgument, MissingRateLimitArgument, NoData, TooManyComponents,
44
};
55
use std::collections::{HashMap, HashSet};
66

77
#[derive(Clone, Debug)]
88
pub struct Authentication {
99
key_to_application: HashMap<String, String>,
10+
app_to_rate_limit: HashMap<String, usize>,
1011
}
1112

1213
#[derive(Debug, PartialEq)]
1314
pub enum AuthenticationParseError {
1415
NoData(),
1516
MissingApplicationArgument(String),
1617
MissingAPIKeyArgument(String),
18+
MissingRateLimitArgument(String),
1719
TooManyComponents(String),
1820
DuplicateApplicationArgument(String),
1921
DuplicateAPIKeyArgument(String),
@@ -23,9 +25,10 @@ impl std::fmt::Display for AuthenticationParseError {
2325
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2426
match self {
2527
NoData() => write!(f, "No API Keys Provided"),
26-
MissingApplicationArgument(arg) => write!(f, "Missing application argument: [{arg}]"),
27-
MissingAPIKeyArgument(app) => write!(f, "Missing API Key argument: [{app}]"),
28-
TooManyComponents(app) => write!(f, "Too many components: [{app}]"),
28+
MissingApplicationArgument(arg) => write!(f, "Missing application argument: [{}]", arg),
29+
MissingAPIKeyArgument(app) => write!(f, "Missing API Key argument: [{}]", app),
30+
MissingRateLimitArgument(app) => write!(f, "Missing rate limit argument: [{}]", app),
31+
TooManyComponents(app) => write!(f, "Too many components: [{}]", app),
2932
DuplicateApplicationArgument(app) => {
3033
write!(f, "Duplicate application argument: [{app}]")
3134
}
@@ -42,6 +45,7 @@ impl TryFrom<Vec<String>> for Authentication {
4245
fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
4346
let mut applications = HashSet::new();
4447
let mut key_to_application: HashMap<String, String> = HashMap::new();
48+
let mut app_to_rate_limit: HashMap<String, usize> = HashMap::new();
4549

4650
if args.is_empty() {
4751
return Err(NoData());
@@ -61,6 +65,13 @@ impl TryFrom<Vec<String>> for Authentication {
6165
return Err(MissingAPIKeyArgument(app.to_string()));
6266
}
6367

68+
let rate_limit = parts
69+
.next()
70+
.ok_or(MissingRateLimitArgument(app.to_string()))?;
71+
if rate_limit.is_empty() {
72+
return Err(MissingRateLimitArgument(app.to_string()));
73+
}
74+
6475
if parts.count() > 0 {
6576
return Err(TooManyComponents(app.to_string()));
6677
}
@@ -75,29 +86,42 @@ impl TryFrom<Vec<String>> for Authentication {
7586

7687
applications.insert(app.to_string());
7788
key_to_application.insert(key.to_string(), app.to_string());
89+
app_to_rate_limit.insert(app.to_string(), rate_limit.parse().unwrap());
7890
}
7991

80-
Ok(Self { key_to_application })
92+
Ok(Self {
93+
key_to_application,
94+
app_to_rate_limit,
95+
})
8196
}
8297
}
8398

8499
impl Authentication {
85100
pub fn none() -> Self {
86101
Self {
87102
key_to_application: HashMap::new(),
103+
app_to_rate_limit: HashMap::new(),
88104
}
89105
}
90106

91107
#[allow(dead_code)]
92-
pub fn new(api_keys: HashMap<String, String>) -> Self {
108+
pub fn new(
109+
api_keys: HashMap<String, String>,
110+
app_to_rate_limit: HashMap<String, usize>,
111+
) -> Self {
93112
Self {
94113
key_to_application: api_keys,
114+
app_to_rate_limit,
95115
}
96116
}
97117

98118
pub fn get_application_for_key(&self, api_key: &String) -> Option<&String> {
99119
self.key_to_application.get(api_key)
100120
}
121+
122+
pub fn get_rate_limits(&self) -> HashMap<String, usize> {
123+
self.app_to_rate_limit.clone()
124+
}
101125
}
102126

103127
#[cfg(test)]
@@ -107,69 +131,90 @@ mod tests {
107131
#[test]
108132
fn test_parsing() {
109133
let auth = Authentication::try_from(vec![
110-
"app1:key1".to_string(),
111-
"app2:key2".to_string(),
112-
"app3:key3".to_string(),
134+
"app1:key1:10".to_string(),
135+
"app2:key2:10".to_string(),
136+
"app3:key3:10".to_string(),
113137
])
114138
.unwrap();
115139

116140
assert_eq!(auth.key_to_application.len(), 3);
117141
assert_eq!(auth.key_to_application["key1"], "app1");
118142
assert_eq!(auth.key_to_application["key2"], "app2");
119143
assert_eq!(auth.key_to_application["key3"], "app3");
144+
assert_eq!(auth.app_to_rate_limit.len(), 3);
145+
assert_eq!(auth.app_to_rate_limit["app1"], 10);
146+
assert_eq!(auth.app_to_rate_limit["app2"], 10);
147+
assert_eq!(auth.app_to_rate_limit["app3"], 10);
120148

121149
let auth = Authentication::try_from(vec![
122-
"app1:key1".to_string(),
150+
"app1:key1:10".to_string(),
123151
"".to_string(),
124-
"app3:key3".to_string(),
152+
"app3:key3:10".to_string(),
125153
]);
126154
assert!(auth.is_err());
127155
assert_eq!(auth.unwrap_err(), MissingApplicationArgument("".into()));
128156

129157
let auth = Authentication::try_from(vec![
130-
"app1:key1".to_string(),
158+
"app1:key1:10".to_string(),
131159
"app2".to_string(),
132-
"app3:key3".to_string(),
160+
"app3:key3:10".to_string(),
133161
]);
134162
assert!(auth.is_err());
135163
assert_eq!(auth.unwrap_err(), MissingAPIKeyArgument("app2".into()));
136164

137165
let auth = Authentication::try_from(vec![
138-
"app1:key1".to_string(),
139-
":".to_string(),
166+
"app1:key1:10".to_string(),
167+
"app2:key2:10".to_string(),
140168
"app3:key3".to_string(),
141169
]);
142170
assert!(auth.is_err());
171+
assert_eq!(auth.unwrap_err(), MissingRateLimitArgument("app3".into()));
172+
173+
let auth = Authentication::try_from(vec![
174+
"app1:key1:10".to_string(),
175+
":".to_string(),
176+
"app3:key3:10".to_string(),
177+
]);
178+
assert!(auth.is_err());
143179
assert_eq!(auth.unwrap_err(), MissingApplicationArgument(":".into()));
144180

145181
let auth = Authentication::try_from(vec![
146-
"app1:key1".to_string(),
182+
"app1:key1:10".to_string(),
147183
"app2:".to_string(),
148-
"app3:key3".to_string(),
184+
"app3:key3:10".to_string(),
149185
]);
150186
assert!(auth.is_err());
151187
assert_eq!(auth.unwrap_err(), MissingAPIKeyArgument("app2".into()));
152188

153189
let auth = Authentication::try_from(vec![
154-
"app1:key1".to_string(),
155-
"app2:key2:unexpected2".to_string(),
156-
"app3:key3".to_string(),
190+
"app1:key1:10".to_string(),
191+
"app2:key2:10".to_string(),
192+
"app3:key3:".to_string(),
193+
]);
194+
assert!(auth.is_err());
195+
assert_eq!(auth.unwrap_err(), MissingRateLimitArgument("app3".into()));
196+
197+
let auth = Authentication::try_from(vec![
198+
"app1:key1:10".to_string(),
199+
"app2:key2:10:unexpected2".to_string(),
200+
"app3:key3:10".to_string(),
157201
]);
158202
assert!(auth.is_err());
159203
assert_eq!(auth.unwrap_err(), TooManyComponents("app2".into()));
160204

161205
let auth = Authentication::try_from(vec![
162-
"app1:key1".to_string(),
163-
"app1:key3".to_string(),
164-
"app2:key2".to_string(),
206+
"app1:key1:10".to_string(),
207+
"app1:key3:10".to_string(),
208+
"app2:key2:10".to_string(),
165209
]);
166210
assert!(auth.is_err());
167211
assert_eq!(
168212
auth.unwrap_err(),
169213
DuplicateApplicationArgument("app1".into())
170214
);
171215

172-
let auth = Authentication::try_from(vec!["app1:key1".to_string(), "app2:key1".to_string()]);
216+
let auth =
217+
Authentication::try_from(vec!["app1:key1:10".to_string(), "app2:key1:10".to_string()]);
173218
assert!(auth.is_err());
174219
assert_eq!(auth.unwrap_err(), DuplicateAPIKeyArgument("app2".into()));
175220
}

crates/websocket-proxy/src/main.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use metrics_exporter_prometheus::PrometheusBuilder;
1515
use rate_limit::{InMemoryRateLimit, RateLimit, RedisRateLimit};
1616
use registry::Registry;
1717
use server::Server;
18+
use std::collections::HashMap;
1819
use std::io::Write;
1920
use std::net::SocketAddr;
2021
use std::sync::Arc;
@@ -66,9 +67,10 @@ struct Args {
6667
long,
6768
env,
6869
default_value = "10",
69-
help = "Maximum number of concurrently connected clients per IP"
70+
help = "Maximum number of concurrently connected clients per IP. 0 here means no limit."
7071
)]
7172
per_ip_connection_limit: usize,
73+
7274
#[arg(
7375
long,
7476
env,
@@ -96,7 +98,7 @@ struct Args {
9698
#[arg(long, env, default_value = "true")]
9799
metrics: bool,
98100

99-
/// API Keys, if not provided will be an unauthenticated endpoint, should be in the format <app1>:<apiKey1>,<app2>:<apiKey2>,..
101+
/// API Keys, if not provided will be an unauthenticated endpoint, should be in the format <app1>:<apiKey1>:<rateLimit1>,<app2>:<apiKey2>:<rateLimit2>,..
100102
#[arg(long, env, value_delimiter = ',', help = "API keys to allow")]
101103
api_keys: Vec<String>,
102104

@@ -337,13 +339,20 @@ async fn main() {
337339
args.client_pong_timeout_ms,
338340
);
339341

342+
let app_rate_limits = if let Some(auth) = &authentication {
343+
auth.get_rate_limits()
344+
} else {
345+
HashMap::new()
346+
};
347+
340348
let rate_limiter = match &args.redis_url {
341349
Some(redis_url) => {
342350
info!(message = "Using Redis rate limiter", redis_url = redis_url);
343351
match RedisRateLimit::new(
344352
redis_url,
345353
args.instance_connection_limit,
346354
args.per_ip_connection_limit,
355+
app_rate_limits.clone(),
347356
&args.redis_key_prefix,
348357
) {
349358
Ok(limiter) => {
@@ -359,6 +368,7 @@ async fn main() {
359368
Arc::new(InMemoryRateLimit::new(
360369
args.instance_connection_limit,
361370
args.per_ip_connection_limit,
371+
app_rate_limits.clone(),
362372
)) as Arc<dyn RateLimit>
363373
}
364374
}
@@ -368,6 +378,7 @@ async fn main() {
368378
Arc::new(InMemoryRateLimit::new(
369379
args.instance_connection_limit,
370380
args.per_ip_connection_limit,
381+
app_rate_limits,
371382
)) as Arc<dyn RateLimit>
372383
}
373384
};

0 commit comments

Comments
 (0)