diff --git a/integration/envoy.yaml b/integration/envoy.yaml index c288333..ed68757 100644 --- a/integration/envoy.yaml +++ b/integration/envoy.yaml @@ -40,6 +40,18 @@ static_resources: "num_workers": 2, "dirname": "/tmp/" } + - name: dynamic_modules/header_mutation + typed_config: + # https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/dynamic_modules/v3/dynamic_modules.proto#envoy-v3-api-msg-extensions-dynamic-modules-v3-dynamicmoduleconfig + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter + dynamic_module_config: + name: rust_module + filter_name: header_mutation + filter_config: | + { + "request_headers": [["X-Envoy-Header", "envoy-header"], ["X-Envoy-Header2", "envoy-header2"]], + "response_headers": [["Foo", "bar"], ["Foo2", "bar2"]] + } - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/integration/main_test.go b/integration/main_test.go index 72050b0..b616d05 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -132,6 +132,43 @@ func TestIntegration(t *testing.T) { }, 30*time.Second, 1*time.Second) }) + t.Run("http_header_mutation", func(t *testing.T) { + require.Eventually(t, func() bool { + req, err := http.NewRequest("GET", "http://localhost:1062/headers", nil) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Logf("Envoy not ready yet: %v", err) + return false + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Logf("Envoy not ready yet: %v", err) + return false + } + + t.Logf("response: headers=%v, body=%s", resp.Header, string(body)) + require.Equal(t, 200, resp.StatusCode) + + // HttpBin returns a JSON object containing the request headers. + type httpBinHeadersBody struct { + Headers map[string]string `json:"headers"` + } + var headersBody httpBinHeadersBody + require.NoError(t, json.Unmarshal(body, &headersBody)) + + require.Equal(t, "envoy-header", headersBody.Headers["X-Envoy-Header"]) + require.Equal(t, "envoy-header2", headersBody.Headers["X-Envoy-Header2"]) + + // We also need to check that the response headers were mutated. + require.Equal(t, "bar", resp.Header.Get("Foo")) + require.Equal(t, "bar2", resp.Header.Get("Foo2")) + return true + }, 30*time.Second, 200*time.Millisecond) + }) + t.Run("http_random_auth", func(t *testing.T) { got200 := false got403 := false diff --git a/rust/src/http_header_mutation.rs b/rust/src/http_header_mutation.rs new file mode 100644 index 0000000..47666b1 --- /dev/null +++ b/rust/src/http_header_mutation.rs @@ -0,0 +1,109 @@ +use envoy_proxy_dynamic_modules_rust_sdk::*; +use serde::{Deserialize, Serialize}; + +/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilterConfig`] trait. +/// +/// The trait corresponds to a Envoy filter chain configuration. +#[derive(Serialize, Deserialize, Debug)] +pub struct FilterConfig { + request_headers: Vec<(String, String)>, + response_headers: Vec<(String, String)>, +} + +impl FilterConfig { + /// This is the constructor for the [`FilterConfig`]. + /// + /// filter_config is the filter config from the Envoy config here: + /// https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/dynamic_modules/v3/dynamic_modules.proto#envoy-v3-api-msg-extensions-dynamic-modules-v3-dynamicmoduleconfig + pub fn new(filter_config: &str) -> Option { + let filter_config: FilterConfig = match serde_json::from_str(filter_config) { + Ok(cfg) => cfg, + Err(err) => { + eprintln!("Error parsing filter config: {}", err); + return None; + } + }; + Some(filter_config) + } +} + +impl HttpFilterConfig for FilterConfig { + /// This is called for each new HTTP filter. + fn new_http_filter(&mut self, _envoy: &mut EC) -> Box> { + Box::new(Filter { + request_headers: self.request_headers.clone(), + response_headers: self.response_headers.clone(), + }) + } +} + +/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilter`] trait. +/// +/// This sets the request and response headers to the values specified in the filter config. +pub struct Filter { + request_headers: Vec<(String, String)>, + response_headers: Vec<(String, String)>, +} + +/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilter`] trait. +impl HttpFilter for Filter { + fn on_request_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status { + for (key, value) in &self.request_headers { + envoy_filter.set_request_header(key, value.as_bytes()); + } + abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + } + + fn on_response_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_response_headers_status { + for (key, value) in &self.response_headers { + envoy_filter.set_response_header(key, value.as_bytes()); + } + abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + /// This demonstrates how to write a test without Envoy using a mock provided by the SDK. + fn test_filter() { + let mut envoy_filter = envoy_proxy_dynamic_modules_rust_sdk::MockEnvoyHttpFilter::new(); + let mut filter = Filter { + request_headers: vec![("X-Foo".to_string(), "bar".to_string())], + response_headers: vec![("X-Bar".to_string(), "foo".to_string())], + }; + + envoy_filter + .expect_set_request_header() + .returning(|key, value| { + assert_eq!(key, "X-Foo"); + assert_eq!(value, b"bar"); + return true; + }); + envoy_filter + .expect_set_response_header() + .returning(|key, value| { + assert_eq!(key, "X-Bar"); + assert_eq!(value, b"foo"); + return true; + }); + assert_eq!( + filter.on_request_headers(&mut envoy_filter, false), + abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + ); + assert_eq!( + filter.on_response_headers(&mut envoy_filter, false), + abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue + ); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4315338..a33da40 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,6 +1,7 @@ use envoy_proxy_dynamic_modules_rust_sdk::*; mod http_access_logger; +mod http_header_mutation; mod http_passthrough; mod http_random_auth; mod http_zero_copy_regex_waf; @@ -39,6 +40,8 @@ fn new_http_filter_config_fn( "random_auth" => Some(Box::new(http_random_auth::FilterConfig::new(filter_config))), "zero_copy_regex_waf" => http_zero_copy_regex_waf::FilterConfig::new(filter_config) .map(|config| Box::new(config) as Box>), + "header_mutation" => http_header_mutation::FilterConfig::new(filter_config) + .map(|config| Box::new(config) as Box>), _ => panic!("Unknown filter name: {}", filter_name), } }