diff --git a/src/configuration.rs b/src/configuration.rs index 1b61dabc..718fa0d2 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,71 +1,133 @@ -use crate::envoy::RLA_action_specifier; use crate::policy_index::PolicyIndex; use serde::Deserialize; #[derive(Deserialize, Debug, Clone)] -pub struct Rule { - #[serde(default)] - pub paths: Vec, +pub struct SelectorItem { + // Selector of an attribute from the contextual properties provided by kuadrant + // during request and connection processing + pub selector: String, + + // If not set it defaults to `selector` field value as the descriptor key. #[serde(default)] - pub hosts: Vec, + pub key: Option, + + // An optional value to use if the selector is not found in the context. + // If not set and the selector is not found in the context, then no data is generated. #[serde(default)] - pub methods: Vec, + pub default: Option, } #[derive(Deserialize, Debug, Clone)] -pub struct Configuration { - #[serde(default)] - pub actions: Vec, +pub struct StaticItem { + pub value: String, + pub key: String, } +// Mutually exclusive struct fields #[derive(Deserialize, Debug, Clone)] -pub struct GatewayAction { - #[serde(default)] - pub rules: Vec, +#[serde(rename_all = "lowercase")] +pub enum DataType { + Static(StaticItem), + Selector(SelectorItem), +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct DataItem { + #[serde(flatten)] + pub item: DataType, +} + +#[derive(Deserialize, PartialEq, Debug, Clone)] +pub enum WhenConditionOperator { + #[serde(rename = "eq")] + EqualOperator, + #[serde(rename = "neq")] + NotEqualOperator, + #[serde(rename = "starts_with")] + StartsWithOperator, + #[serde(rename = "ends_with")] + EndsWithOperator, + #[serde(rename = "matches")] + MatchesOperator, +} + +impl WhenConditionOperator { + pub fn eval(&self, value: &str, attr_value: &str) -> bool { + match *self { + WhenConditionOperator::EqualOperator => value.eq(attr_value), + WhenConditionOperator::NotEqualOperator => !value.eq(attr_value), + WhenConditionOperator::StartsWithOperator => attr_value.starts_with(value), + WhenConditionOperator::EndsWithOperator => attr_value.ends_with(value), + // TODO(eastizle): implement other operators + _ => true, + } + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PatternExpression { + pub selector: String, + pub operator: WhenConditionOperator, + pub value: String, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Condition { + pub all_of: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Rule { + // #[serde(default)] - pub configurations: Vec, + pub conditions: Vec, + // + pub data: Vec, } #[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct RateLimitPolicy { pub name: String, - pub rate_limit_domain: String, - pub upstream_cluster: String, + pub domain: String, + pub service: String, pub hostnames: Vec, - #[serde(default)] - pub gateway_actions: Vec, + pub rules: Vec, } impl RateLimitPolicy { #[cfg(test)] pub fn new( name: String, - rate_limit_domain: String, - upstream_cluster: String, + domain: String, + service: String, hostnames: Vec, - gateway_actions: Vec, + rules: Vec, ) -> Self { RateLimitPolicy { name, - rate_limit_domain, - upstream_cluster, + domain, + service, hostnames, - gateway_actions, + rules, } } } pub struct FilterConfig { pub index: PolicyIndex, - // Deny request when faced with an irrecoverable failure. - pub failure_mode_deny: bool, + // Deny/Allow request when faced with an irrecoverable failure. + pub failure_mode: FailureMode, } impl FilterConfig { pub fn new() -> Self { Self { index: PolicyIndex::new(), - failure_mode_deny: true, + failure_mode: FailureMode::Deny, } } @@ -80,20 +142,24 @@ impl FilterConfig { Self { index, - failure_mode_deny: config.failure_mode_deny, + failure_mode: config.failure_mode, } } } -// TODO(rahulanand16nov): We can convert the structure of config in such a way -// that it's optimized for lookup in the runtime. For e.g., keying on virtualhost -// to sort through ratelimitpolicies and then further operations. +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FailureMode { + Deny, + Allow, +} #[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct PluginConfiguration { pub rate_limit_policies: Vec, - // Deny request when faced with an irrecoverable failure. - pub failure_mode_deny: bool, + // Deny/Allow request when faced with an irrecoverable failure. + pub failure_mode: FailureMode, } #[cfg(test)] @@ -101,63 +167,52 @@ mod test { use super::*; const CONFIG: &str = r#"{ - "failure_mode_deny": true, - "rate_limit_policies": [ + "failureMode": "deny", + "rateLimitPolicies": [ { - "name": "some-name", - "rate_limit_domain": "RLS-domain", - "upstream_cluster": "limitador-cluster", + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], - "gateway_actions": [ + "rules": [ { - "rules": [ + "conditions": [ { - "paths": ["/admin/toy"], - "hosts": ["cars.toystore.com"], - "methods": ["POST"] - }], - "configurations": [ - { - "actions": [ + "allOf": [ { - "generic_key": { - "descriptor_key": "admin", - "descriptor_value": "1" - } + "selector": "request.path", + "operator": "eq", + "value": "/admin/toy" }, { - "metadata": { - "descriptor_key": "user-id", - "default_value": "no-user", - "metadata_key": { - "key": "envoy.filters.http.ext_authz", - "path": [ - { - "segment": { - "key": "ext_auth_data" - } - }, - { - "segment": { - "key": "user_id" - } - } - ] - }, - "source": "DYNAMIC" - } + "selector": "request.method", + "operator": "eq", + "value": "POST" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "cars.toystore.com" + }] + }], + "data": [ + { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" } - ] - } - ] - } - ] - } - ] + }, + { + "selector": { + "selector": "auth.metadata.username" + } + }] + }] + }] }"#; #[test] - fn parse_config() { + fn parse_config_happy_path() { let res = serde_json::from_str::(CONFIG); if let Err(ref e) = res { eprintln!("{e}"); @@ -167,35 +222,340 @@ mod test { let filter_config = res.unwrap(); assert_eq!(filter_config.rate_limit_policies.len(), 1); - let gateway_actions = &filter_config.rate_limit_policies[0].gateway_actions; - assert_eq!(gateway_actions.len(), 1); + let rules = &filter_config.rate_limit_policies[0].rules; + assert_eq!(rules.len(), 1); + + let conditions = &rules[0].conditions; + assert_eq!(conditions.len(), 1); - let configurations = &gateway_actions[0].configurations; - assert_eq!(configurations.len(), 1); + let all_of_conditions = &conditions[0].all_of; + assert_eq!(all_of_conditions.len(), 3); - let actions = &configurations[0].actions; - assert_eq!(actions.len(), 2); - assert!(std::matches!( - actions[0], - RLA_action_specifier::generic_key(_) - )); + let data_items = &rules[0].data; + assert_eq!(data_items.len(), 2); - if let RLA_action_specifier::metadata(ref metadata_action) = actions[1] { - let metadata_key = metadata_action.get_metadata_key(); - assert_eq!(metadata_key.get_key(), "envoy.filters.http.ext_authz"); + // TODO(eastizle): DataItem does not implement PartialEq, add it only for testing? + //assert_eq!( + // data_items[0], + // DataItem { + // item: DataType::Static(StaticItem { + // key: String::from("rlp-ns-A/rlp-name-A"), + // value: String::from("1") + // }) + // } + //); - let metadata_path = metadata_key.get_path(); - assert_eq!(metadata_path.len(), 2); - assert_eq!(metadata_path[0].get_key(), "ext_auth_data"); - assert_eq!(metadata_path[1].get_key(), "user_id"); + if let DataType::Static(static_item) = &data_items[0].item { + assert_eq!(static_item.key, "rlp-ns-A/rlp-name-A"); + assert_eq!(static_item.value, "1"); } else { - panic!("wrong action type: expected metadata type"); + assert!(false); + } + + if let DataType::Selector(selector_item) = &data_items[1].item { + assert_eq!(selector_item.selector, "auth.metadata.username"); + assert!(selector_item.key.is_none()); + assert!(selector_item.default.is_none()); + } else { + assert!(false); } } + #[test] + fn parse_config_min() { + let config = r#"{ + "failureMode": "deny", + "rateLimitPolicies": [] + }"#; + let res = serde_json::from_str::(config); + if let Err(ref e) = res { + eprintln!("{e}"); + } + assert!(res.is_ok()); + + let filter_config = res.unwrap(); + assert_eq!(filter_config.rate_limit_policies.len(), 0); + } + + #[test] + fn parse_config_data_selector() { + let config = r#"{ + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "limitador-cluster", + "hostnames": ["*.toystore.com", "example.com"], + "rules": [ + { + "data": [ + { + "selector": { + "selector": "my.selector.path", + "key": "mykey", + "default": "my_selector_default_value" + } + }] + }] + }] + }"#; + let res = serde_json::from_str::(config); + if let Err(ref e) = res { + eprintln!("{e}"); + } + assert!(res.is_ok()); + + let filter_config = res.unwrap(); + assert_eq!(filter_config.rate_limit_policies.len(), 1); + + let rules = &filter_config.rate_limit_policies[0].rules; + assert_eq!(rules.len(), 1); + + let data_items = &rules[0].data; + assert_eq!(data_items.len(), 1); + + if let DataType::Selector(selector_item) = &data_items[0].item { + assert_eq!(selector_item.selector, "my.selector.path"); + assert_eq!(selector_item.key.as_ref().unwrap(), "mykey"); + assert_eq!( + selector_item.default.as_ref().unwrap(), + "my_selector_default_value" + ); + } else { + assert!(false); + } + } + + #[test] + fn parse_config_condition_selector_operators() { + let config = r#"{ + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "limitador-cluster", + "hostnames": ["*.toystore.com", "example.com"], + "rules": [ + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/admin/toy" + }, + { + "selector": "request.method", + "operator": "neq", + "value": "POST" + }, + { + "selector": "request.host", + "operator": "starts_with", + "value": "cars." + }, + { + "selector": "request.host", + "operator": "ends_with", + "value": ".com" + }, + { + "selector": "request.host", + "operator": "matches", + "value": "*.com" + }] + }], + "data": [ { "selector": { "selector": "my.selector.path" } }] + }] + }] + }"#; + let res = serde_json::from_str::(config); + if let Err(ref e) = res { + eprintln!("{e}"); + } + assert!(res.is_ok()); + + let filter_config = res.unwrap(); + assert_eq!(filter_config.rate_limit_policies.len(), 1); + + let rules = &filter_config.rate_limit_policies[0].rules; + assert_eq!(rules.len(), 1); + + let conditions = &rules[0].conditions; + assert_eq!(conditions.len(), 1); + + let all_of_conditions = &conditions[0].all_of; + assert_eq!(all_of_conditions.len(), 5); + + let expected_conditions = [ + // selector, value, operator + ( + "request.path", + "/admin/toy", + WhenConditionOperator::EqualOperator, + ), + ( + "request.method", + "POST", + WhenConditionOperator::NotEqualOperator, + ), + ( + "request.host", + "cars.", + WhenConditionOperator::StartsWithOperator, + ), + ( + "request.host", + ".com", + WhenConditionOperator::EndsWithOperator, + ), + ( + "request.host", + "*.com", + WhenConditionOperator::MatchesOperator, + ), + ]; + + for i in 0..expected_conditions.len() { + assert_eq!(all_of_conditions[i].selector, expected_conditions[i].0); + assert_eq!(all_of_conditions[i].value, expected_conditions[i].1); + assert_eq!(all_of_conditions[i].operator, expected_conditions[i].2); + } + } + + #[test] + fn parse_config_conditions_optional() { + let config = r#"{ + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "limitador-cluster", + "hostnames": ["*.toystore.com", "example.com"], + "rules": [ + { + "data": [ + { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + }, + { + "selector": { + "selector": "auth.metadata.username" + } + }] + }] + }] + }"#; + let res = serde_json::from_str::(config); + if let Err(ref e) = res { + eprintln!("{e}"); + } + assert!(res.is_ok()); + + let filter_config = res.unwrap(); + assert_eq!(filter_config.rate_limit_policies.len(), 1); + + let rules = &filter_config.rate_limit_policies[0].rules; + assert_eq!(rules.len(), 1); + + let conditions = &rules[0].conditions; + assert_eq!(conditions.len(), 0); + } + + #[test] + fn parse_config_invalid_data() { + // data item fields are mutually exclusive + let bad_config = r#"{ + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "limitador-cluster", + "hostnames": ["*.toystore.com", "example.com"], + "rules": [ + { + "data": [ + { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + }, + "selector": { + "selector": "auth.metadata.username" + } + }] + }] + }] + }"#; + let res = serde_json::from_str::(bad_config); + assert!(res.is_err()); + + // data item unknown fields are forbidden + let bad_config = r#"{ + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "limitador-cluster", + "hostnames": ["*.toystore.com", "example.com"], + "rules": [ + { + "data": [ + { + "unknown": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + }] + }] + }] + }"#; + let res = serde_json::from_str::(bad_config); + assert!(res.is_err()); + + // condition selector operator unknown + let bad_config = r#"{ + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "limitador-cluster", + "hostnames": ["*.toystore.com", "example.com"], + "rules": [ + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.path", + "operator": "unknown", + "value": "/admin/toy" + }] + }], + "data": [ { "selector": { "selector": "my.selector.path" } }] + }] + }] + }"#; + let res = serde_json::from_str::(bad_config); + assert!(res.is_err()); + } + #[test] fn filter_config_from_configuration() { let res = serde_json::from_str::(CONFIG); + if let Err(ref e) = res { + eprintln!("{e}"); + } assert!(res.is_ok()); let filter_config = FilterConfig::from(res.unwrap()); diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 02ea8895..55c16884 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,14 +1,15 @@ -use crate::configuration::{Configuration, FilterConfig, RateLimitPolicy, Rule}; +use crate::configuration::{ + Condition, DataItem, DataType, FilterConfig, PatternExpression, RateLimitPolicy, +}; use crate::envoy::{ - RLA_action_specifier, RateLimitDescriptor, RateLimitDescriptor_Entry, RateLimitRequest, - RateLimitResponse, RateLimitResponse_Code, + RateLimitDescriptor, RateLimitDescriptor_Entry, RateLimitRequest, RateLimitResponse, + RateLimitResponse_Code, }; -use crate::utils::{match_headers, path_match, request_process_failure, subdomain_match}; +use crate::utils::request_process_failure; use log::{debug, info, warn}; use protobuf::Message; use proxy_wasm::traits::{Context, HttpContext}; use proxy_wasm::types::Action; -use std::collections::HashMap; use std::rc::Rc; use std::time::Duration; @@ -22,26 +23,6 @@ pub struct Filter { } impl Filter { - fn request_path(&self) -> String { - match self.get_http_request_header(":path") { - None => { - warn!(":path header not found"); - String::new() - } - Some(path) => path, - } - } - - fn request_method(&self) -> String { - match self.get_http_request_header(":method") { - None => { - warn!(":method header not found"); - String::new() - } - Some(method) => method, - } - } - fn request_authority(&self) -> String { match self.get_http_request_header(":authority") { None => { @@ -63,14 +44,14 @@ impl Filter { } let mut rl_req = RateLimitRequest::new(); - rl_req.set_domain(rlp.rate_limit_domain.clone()); + rl_req.set_domain(rlp.domain.clone()); rl_req.set_hits_addend(1); rl_req.set_descriptors(descriptors); let rl_req_serialized = Message::write_to_bytes(&rl_req).unwrap(); // TODO(rahulanand16nov): Error Handling match self.dispatch_grpc_call( - rlp.upstream_cluster.as_str(), + rlp.service.as_str(), RATELIMIT_SERVICE_NAME, RATELIMIT_METHOD_NAME, Vec::new(), @@ -80,7 +61,7 @@ impl Filter { Ok(call_id) => info!("Initiated gRPC call (id# {}) to Limitador", call_id), Err(e) => { warn!("gRPC call to Limitador failed! {:?}", e); - request_process_failure(self.config.failure_mode_deny); + request_process_failure(&self.config.failure_mode); } } Action::Pause @@ -91,195 +72,114 @@ impl Filter { rlp: &RateLimitPolicy, ) -> protobuf::RepeatedField { //::protobuf::RepeatedField::default() - rlp.gateway_actions + rlp.rules .iter() - .filter(|ga| self.filter_configurations_by_rules(&ga.rules)) - // flatten the vec to vec - .flat_map(|ga| &ga.configurations) + .filter(|rule| self.filter_rule_by_conditions(&rule.conditions)) + // flatten the vec to vec + .flat_map(|rule| &rule.data) // All actions cannot be flatten! each vec of actions defines one potential descriptor - .flat_map(|configuration| self.build_descriptor(configuration)) + .flat_map(|data| self.build_descriptor(data)) .collect() } - fn filter_configurations_by_rules(&self, rules: &[Rule]) -> bool { - if rules.is_empty() { - // no rules is equivalent to matching all the requests. + fn filter_rule_by_conditions(&self, conditions: &[Condition]) -> bool { + if conditions.is_empty() { + // no conditions is equivalent to matching all the requests. return true; } - rules.iter().any(|rule| self.rule_applies(rule)) + conditions + .iter() + .any(|condition| self.condition_applies(condition)) } - fn rule_applies(&self, rule: &Rule) -> bool { - if !rule.paths.is_empty() - && !rule - .paths - .iter() - .any(|path| path_match(path, self.request_path().as_str())) - { - return false; - } - - if !rule.methods.is_empty() - && !rule - .methods - .iter() - .any(|method| self.request_method().eq(method)) - { - return false; - } + fn condition_applies(&self, condition: &Condition) -> bool { + condition + .all_of + .iter() + .all(|pattern_expression| self.pattern_expression_applies(pattern_expression)) + } - if !rule.hosts.is_empty() - && !rule - .hosts - .iter() - .any(|subdomain| subdomain_match(subdomain, self.request_authority().as_str())) - { - return false; + fn pattern_expression_applies(&self, p_e: &PatternExpression) -> bool { + let attribute_path = p_e.selector.split(".").collect(); + match self.get_property(attribute_path) { + None => { + debug!( + "[context_id: {}]: selector not found: {}", + self.context_id, p_e.selector + ); + false + } + // TODO(eastizle): not all fields are strings + // https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes + Some(attribute_bytes) => match String::from_utf8(attribute_bytes) { + Err(e) => { + debug!( + "[context_id: {}]: failed to parse selector value: {}, error: {}", + self.context_id, p_e.selector, e + ); + false + } + Ok(attribute_value) => p_e + .operator + .eval(p_e.value.as_str(), attribute_value.as_str()), + }, } - - true } - fn build_descriptor(&self, configuration: &Configuration) -> Option { + fn build_descriptor(&self, data: &DataItem) -> Option { let mut entries = ::protobuf::RepeatedField::default(); - for action in configuration.actions.iter() { - let mut descriptor_entry = RateLimitDescriptor_Entry::new(); - match action { - RLA_action_specifier::source_cluster(_) => { - match self.get_property(vec!["connection", "requested_server_name"]) { - None => { - debug!("requested service name not found"); - return None; - } - Some(src_cluster_bytes) => { - match String::from_utf8(src_cluster_bytes) { - // NOTE(rahulanand16nov): not sure if it's correct. - Ok(src_cluster) => { - descriptor_entry.set_key("source_cluster".into()); - descriptor_entry.set_value(src_cluster); - entries.push(descriptor_entry); - } - Err(e) => { - warn!("source_cluster action parsing error! {:?}", e); - return None; - } - } - } - } - } - RLA_action_specifier::destination_cluster(_) => { - match self.get_property(vec!["cluster_name"]) { - None => { - debug!("cluster name not found"); - return None; - } - Some(cluster_name_bytes) => match String::from_utf8(cluster_name_bytes) { - Ok(cluster_name) => { - descriptor_entry.set_key("destination_cluster".into()); - descriptor_entry.set_value(cluster_name); + + match &data.item { + DataType::Static(static_item) => { + let mut descriptor_entry = RateLimitDescriptor_Entry::new(); + descriptor_entry.set_key(static_item.key.to_owned()); + descriptor_entry.set_value(static_item.value.to_owned()); + entries.push(descriptor_entry); + } + DataType::Selector(selector_item) => { + let descriptor_key = match &selector_item.key { + None => selector_item.selector.to_owned(), + Some(key) => key.to_owned(), + }; + + let attribute_path = selector_item.selector.split(".").collect(); + + match self.get_property(attribute_path) { + None => { + debug!( + "[context_id: {}]: selector not found: {}", + self.context_id, selector_item.selector + ); + match &selector_item.default { + None => return None, // skipping descriptors + Some(default_value) => { + let mut descriptor_entry = RateLimitDescriptor_Entry::new(); + descriptor_entry.set_key(descriptor_key); + descriptor_entry.set_value(default_value.to_owned()); entries.push(descriptor_entry); } - Err(e) => { - warn!("cluster_name action parsing error! {:?}", e); - return None; - } - }, - } - } - RLA_action_specifier::request_headers(rh) => { - match self.get_http_request_header(rh.get_header_name()) { - None => { - debug!("header name {} not found", rh.get_header_name()); - return None; - } - Some(header_value) => { - descriptor_entry.set_key(rh.get_descriptor_key().into()); - descriptor_entry.set_value(header_value); - entries.push(descriptor_entry); } } - } - RLA_action_specifier::remote_address(_) => { - match self.get_http_request_header("x-forwarded-for") { - None => { - debug!("x-forwarded-for header not found"); + Some(attribute_bytes) => match String::from_utf8(attribute_bytes) { + Err(e) => { + debug!( + "[context_id: {}]: failed to parse selector value: {}, error: {}", + self.context_id, selector_item.selector, e + ); return None; } - Some(remote_addess) => { - descriptor_entry.set_key("remote_address".into()); - descriptor_entry.set_value(remote_addess); + Ok(attribute_value) => { + let mut descriptor_entry = RateLimitDescriptor_Entry::new(); + descriptor_entry.set_key(descriptor_key); + descriptor_entry.set_value(attribute_value); entries.push(descriptor_entry); } - } + }, } - RLA_action_specifier::generic_key(gk) => { - descriptor_entry.set_key(gk.get_descriptor_key().into()); - descriptor_entry.set_value(gk.get_descriptor_value().into()); - entries.push(descriptor_entry); - } - RLA_action_specifier::header_value_match(hvm) => { - let request_headers: HashMap<_, _> = - self.get_http_request_headers().into_iter().collect(); - - if hvm.get_expect_match().get_value() - == match_headers(&request_headers, hvm.get_headers()) - { - descriptor_entry.set_key("header_match".into()); - descriptor_entry.set_value(hvm.get_descriptor_value().into()); - entries.push(descriptor_entry); - } else { - debug!("header_value_match does not add entry"); - return None; - } - } - RLA_action_specifier::dynamic_metadata(_) => todo!(), - RLA_action_specifier::metadata(md) => { - // Note(rahul): defaulting to dynamic metadata source right now. - let metadata_key = &md.get_metadata_key().key; - let mut metadata_path: Vec<&str> = md - .get_metadata_key() - .get_path() - .iter() - .map(|path_segment| path_segment.get_key()) - .collect(); - let default_value = md.get_default_value(); - let descriptor_key = md.get_descriptor_key(); - - descriptor_entry.set_key(descriptor_key.into()); - - let mut property_path = vec!["metadata", "filter_metadata", metadata_key]; - property_path.append(&mut metadata_path); - debug!("metadata property_path {:?}", property_path); - match self.get_property(property_path) { - None => { - debug!("metadata key not found"); - if default_value.is_empty() { - debug!("skipping descriptor because no metadata and default value present"); - return None; - } - descriptor_entry.set_value(default_value.into()); - entries.push(descriptor_entry); - } - Some(metadata_bytes) => match String::from_utf8(metadata_bytes) { - Err(e) => { - debug!("failed to parse metadata value: {}", e); - if default_value.is_empty() { - debug!("skipping descriptor because no metadata and default value present"); - return None; - } - descriptor_entry.set_value(default_value.into()); - } - Ok(metadata_value) => { - descriptor_entry.set_value(metadata_value); - entries.push(descriptor_entry); - } - }, - } - } - RLA_action_specifier::extension(_) => todo!(), } } + let mut res = RateLimitDescriptor::new(); res.set_entries(entries); Some(res) @@ -325,7 +225,7 @@ impl Context for Filter { Some(bytes) => bytes, None => { warn!("grpc response body is empty!"); - request_process_failure(self.config.failure_mode_deny); + request_process_failure(&self.config.failure_mode); return; } }; @@ -337,7 +237,7 @@ impl Context for Filter { "failed to parse grpc response body into RateLimitResponse message: {}", e ); - request_process_failure(self.config.failure_mode_deny); + request_process_failure(&self.config.failure_mode); return; } }; @@ -347,7 +247,7 @@ impl Context for Filter { overall_code: RateLimitResponse_Code::UNKNOWN, .. } => { - request_process_failure(self.config.failure_mode_deny); + request_process_failure(&self.config.failure_mode); return; } RateLimitResponse { diff --git a/src/utils.rs b/src/utils.rs index 6de4457e..89810bb6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,5 @@ -use crate::envoy::{HeaderMatcher, HeaderMatcher_specifier, StringMatcher_pattern}; +use crate::configuration::FailureMode; use proxy_wasm::hostcalls::{resume_http_request, send_http_response}; -use std::collections::HashMap; #[derive(Debug, thiserror::Error)] pub enum UtilsErr { @@ -15,134 +14,11 @@ pub enum UtilsErr { } // Helper function to handle failure during processing. -pub fn request_process_failure(failure_mode_deny: bool) { - if failure_mode_deny { - send_http_response(500, vec![], Some(b"Internal Server Error.\n")).unwrap(); - } - resume_http_request().unwrap(); -} - -pub fn match_headers( - req_headers: &HashMap, - config_headers: &[HeaderMatcher], -) -> bool { - for header_matcher in config_headers { - let invert_match = header_matcher.get_invert_match(); - if let Some(req_header_value) = req_headers.get(header_matcher.get_name()) { - if let Some(hm_specifier) = &header_matcher.header_match_specifier { - let mut is_match = false; - match hm_specifier { - HeaderMatcher_specifier::exact_match(str) => is_match = str == req_header_value, - HeaderMatcher_specifier::safe_regex_match(_regex_matcher) => todo!(), // TODO(rahulanand16nov): not implemented. - HeaderMatcher_specifier::range_match(range) => { - if let Ok(val) = req_header_value.parse::() { - is_match = range.get_start() <= val && val < range.get_end(); - } - } - HeaderMatcher_specifier::present_match(should_be_present) => { - is_match = *should_be_present - } - HeaderMatcher_specifier::prefix_match(prefix) => { - is_match = req_header_value.starts_with(prefix.as_str()) - } - HeaderMatcher_specifier::suffix_match(suffix) => { - is_match = req_header_value.ends_with(suffix.as_str()) - } - HeaderMatcher_specifier::contains_match(str) => { - is_match = req_header_value.contains(str.as_str()) - } - HeaderMatcher_specifier::string_match(str_matcher) => { - let ignore_case = str_matcher.get_ignore_case(); - if let Some(pattern) = &str_matcher.match_pattern { - match pattern { - StringMatcher_pattern::exact(str) => { - is_match = if ignore_case { - str.eq_ignore_ascii_case(req_header_value) - } else { - str == req_header_value - } - } - StringMatcher_pattern::prefix(str) => { - is_match = if ignore_case { - req_header_value - .to_lowercase() - .starts_with(&str.to_lowercase()) - } else { - req_header_value.starts_with(str.as_str()) - } - } - StringMatcher_pattern::suffix(str) => { - is_match = if ignore_case { - req_header_value - .to_lowercase() - .ends_with(&str.to_lowercase()) - } else { - req_header_value.ends_with(str.as_str()) - } - } - StringMatcher_pattern::safe_regex(_) => todo!(), // TODO(rahulanand16nov): not implemented. - StringMatcher_pattern::contains(str) => { - is_match = if ignore_case { - req_header_value - .to_lowercase() - .contains(&str.to_lowercase()) - } else { - req_header_value.contains(str.as_str()) - } - } - } - } else { - return false; - } - } - } - if is_match ^ invert_match { - return false; - } - } else { - return false; - } - } else { - return false; +pub fn request_process_failure(failure_mode: &FailureMode) { + match failure_mode { + FailureMode::Deny => { + send_http_response(500, vec![], Some(b"Internal Server Error.\n")).unwrap() } - } - true -} - -pub fn subdomain_match(subdomain: &str, authority: &str) -> bool { - authority.ends_with(subdomain.replace('*', "").as_str()) -} - -pub fn path_match(path_pattern: &str, request_path: &str) -> bool { - if let Some(stripped_path_pattern) = path_pattern.strip_suffix('*') { - request_path.starts_with(stripped_path_pattern) - } else { - request_path.eq(path_pattern) - } -} - -#[cfg(test)] -mod tests { - use crate::utils; - - #[test] - fn subdomain_match() { - assert!(utils::subdomain_match("*.example.com", "test.example.com")); - assert!(!utils::subdomain_match("*.example.com", "example.com")); - assert!(utils::subdomain_match("*", "test1.test2.example.com")); - assert!(utils::subdomain_match("example.com", "example.com")); - } - - #[test] - fn path_match() { - assert!(utils::path_match("/cats", "/cats")); - assert!(utils::path_match("/", "/")); - assert!(utils::path_match("/*", "/")); - assert!(utils::path_match("/*", "/cats/something")); - assert!(utils::path_match("/cats/*", "/cats/")); - assert!(utils::path_match("/cats/*", "/cats/something")); - assert!(!utils::path_match("/cats/*", "/cats")); - assert!(utils::path_match("/cats*", "/catsanddogs")); - assert!(utils::path_match("/cats*", "/cats/dogs")); + FailureMode::Allow => resume_http_request().unwrap(), } }