Skip to content

Commit

Permalink
Adding response headers in the RLS server so that it implements https…
Browse files Browse the repository at this point in the history
  • Loading branch information
chirino committed Mar 20, 2023
1 parent 3766205 commit c373724
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 22 deletions.
54 changes: 48 additions & 6 deletions limitador-server/src/envoy_rls/server.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::envoy_rls::server::envoy::config::core::v3::HeaderValue;
use crate::envoy_rls::server::envoy::service::ratelimit::v3::rate_limit_response::Code;
use crate::envoy_rls::server::envoy::service::ratelimit::v3::rate_limit_service_server::{
RateLimitService, RateLimitServiceServer,
Expand All @@ -6,6 +7,7 @@ use crate::envoy_rls::server::envoy::service::ratelimit::v3::{
RateLimitRequest, RateLimitResponse,
};
use crate::Limiter;
use limitador::counter::Counter;
use std::collections::HashMap;
use std::sync::Arc;
use tonic::{transport, transport::Server, Request, Response, Status};
Expand Down Expand Up @@ -62,7 +64,7 @@ impl RateLimitService for MyRateLimiter {
req.hits_addend
};

let is_rate_limited_res = match &*self.limiter {
let rate_limited_resp = match &*self.limiter {
Limiter::Blocking(limiter) => {
limiter.check_rate_limited_and_update(&namespace, &values, i64::from(hits_addend))
}
Expand All @@ -73,12 +75,12 @@ impl RateLimitService for MyRateLimiter {
}
};

let resp_code = match is_rate_limited_res {
Ok(rate_limited) => {
let (resp_code, counters, limited_counter) = match rate_limited_resp {
Ok((rate_limited, counters, limited_counter)) => {
if rate_limited {
Code::OverLimit
(Code::OverLimit, counters, limited_counter)
} else {
Code::Ok
(Code::Ok, counters, limited_counter)
}
}
Err(e) => {
Expand All @@ -100,7 +102,7 @@ impl RateLimitService for MyRateLimiter {
overall_code: resp_code.into(),
statuses: vec![],
request_headers_to_add: vec![],
response_headers_to_add: vec![],
response_headers_to_add: to_response_header(counters, limited_counter),
raw_body: vec![],
dynamic_metadata: None,
quota: None,
Expand All @@ -110,6 +112,46 @@ impl RateLimitService for MyRateLimiter {
}
}

// create response headers per https://datatracker.ietf.org/doc/id/draft-polli-ratelimit-headers-03.html
pub fn to_response_header(counters: Vec<Counter>, limited: Option<String>) -> Vec<HeaderValue> {
let mut headers = Vec::new();

if let Some(name) = limited {
headers.push(HeaderValue {
key: "X-RateLimit-Limit-Hit".to_string(),
value: name,
});
}

counters.into_iter().for_each(|counter| {
let mut buf = String::with_capacity(40);
let limit = counter.limit().max_value();
let window = counter.limit().seconds();
buf.push_str(format!("{}, {};w={}", limit, limit, window).as_str());

if let Some(name) = counter.limit().name() {
buf.push_str(format!(";name=\"{}\"", name).as_str());
}
headers.push(HeaderValue {
key: "X-RateLimit-Limit".to_string(),
value: buf,
});
if let Some(remaining) = counter.remaining() {
headers.push(HeaderValue {
key: "X-RateLimit-Remaining".to_string(),
value: format!("{}", remaining),
});
}
if let Some(duration) = counter.expires_in() {
headers.push(HeaderValue {
key: "X-RateLimit-Reset".to_string(),
value: format!("{}", duration.as_secs()),
});
}
});
headers
}

pub async fn run_envoy_rls_server(
address: String,
limiter: Arc<Limiter>,
Expand Down
2 changes: 1 addition & 1 deletion limitador-server/src/http_api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ async fn check_and_report(
};

match rate_limited_and_update_result {
Ok(is_rate_limited) => {
Ok((is_rate_limited, _, _)) => {
if is_rate_limited {
Err(ErrorResponse::TooManyRequests)
} else {
Expand Down
20 changes: 10 additions & 10 deletions limitador/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,27 +375,27 @@ impl RateLimiter {
namespace: &Namespace,
values: &HashMap<String, String>,
delta: i64,
) -> Result<bool, LimitadorError> {
) -> Result<(bool, Vec<Counter>, Option<String>), LimitadorError> {
let counters = self.counters_that_apply(namespace, values)?;

if counters.is_empty() {
self.prometheus_metrics.incr_authorized_calls(namespace);
return Ok(false);
return Ok((false, counters, None));
}

let check_result = self
.storage
.check_and_update(counters.into_iter().collect(), delta)?;
.check_and_update(counters.iter().cloned().collect(), delta)?;

match check_result {
Authorization::Ok => {
self.prometheus_metrics.incr_authorized_calls(namespace);
Ok(false)
Ok((false, counters, None))
}
Authorization::Limited(name) => {
self.prometheus_metrics
.incr_limited_calls(namespace, name.as_deref());
Ok(true)
Ok((true, counters, name))
}
}
}
Expand Down Expand Up @@ -551,29 +551,29 @@ impl AsyncRateLimiter {
namespace: &Namespace,
values: &HashMap<String, String>,
delta: i64,
) -> Result<bool, LimitadorError> {
) -> Result<(bool, Vec<Counter>, Option<String>), LimitadorError> {
// the above where-clause is needed in order to call unwrap().
let counters = self.counters_that_apply(namespace, values).await?;

if counters.is_empty() {
self.prometheus_metrics.incr_authorized_calls(namespace);
return Ok(false);
return Ok((false, counters, None));
}

let check_result = self
.storage
.check_and_update(counters.into_iter().collect(), delta)
.check_and_update(counters.iter().cloned().collect(), delta)
.await?;

match check_result {
Authorization::Ok => {
self.prometheus_metrics.incr_authorized_calls(namespace);
Ok(false)
Ok((false, counters, None))
}
Authorization::Limited(name) => {
self.prometheus_metrics
.incr_limited_calls(namespace, name.as_deref());
Ok(true)
Ok((false, counters, name))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion limitador/tests/helpers/tests_limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ impl TestsLimiter {
namespace: &str,
values: &HashMap<String, String>,
delta: i64,
) -> Result<bool, LimitadorError> {
) -> Result<(bool, Vec<Counter>, Option<String>), LimitadorError> {
match &self.limiter_impl {
LimiterImpl::Blocking(limiter) => {
limiter.check_rate_limited_and_update(&namespace.into(), values, delta)
Expand Down
8 changes: 4 additions & 4 deletions limitador/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,13 +623,13 @@ mod test {
assert!(!rate_limiter
.check_rate_limited_and_update(namespace, &values, 1)
.await
.unwrap());
.unwrap().0);
}

assert!(rate_limiter
.check_rate_limited_and_update(namespace, &values, 1)
.await
.unwrap());
.unwrap().0);
}

async fn check_rate_limited_and_update_returns_true_if_no_limits_apply(
Expand All @@ -655,7 +655,7 @@ mod test {
assert!(!rate_limiter
.check_rate_limited_and_update(namespace, &values, 1)
.await
.unwrap());
.unwrap().0);
}

async fn check_rate_limited_and_update_applies_limit_if_its_unconditional(
Expand All @@ -679,7 +679,7 @@ mod test {
assert!(rate_limiter
.check_rate_limited_and_update(namespace, &values, 1)
.await
.unwrap());
.unwrap().0);
}

async fn get_counters(rate_limiter: &mut TestsLimiter) {
Expand Down

0 comments on commit c373724

Please sign in to comment.