Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions integration/envoy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions integration/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 109 additions & 0 deletions rust/src/http_header_mutation.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<EC: EnvoyHttpFilterConfig, EHF: EnvoyHttpFilter> HttpFilterConfig<EC, EHF> for FilterConfig {
/// This is called for each new HTTP filter.
fn new_http_filter(&mut self, _envoy: &mut EC) -> Box<dyn HttpFilter<EHF>> {
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<EHF: EnvoyHttpFilter> HttpFilter<EHF> 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
);
}
}
3 changes: 3 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -39,6 +40,8 @@ fn new_http_filter_config_fn<EC: EnvoyHttpFilterConfig, EHF: EnvoyHttpFilter>(
"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<dyn HttpFilterConfig<EC, EHF>>),
"header_mutation" => http_header_mutation::FilterConfig::new(filter_config)
.map(|config| Box::new(config) as Box<dyn HttpFilterConfig<EC, EHF>>),
_ => panic!("Unknown filter name: {}", filter_name),
}
}
Loading