Skip to content

Commit 4700281

Browse files
authored
Adds header mutation example (#13)
Signed-off-by: Takeshi Yoneda <[email protected]>
1 parent 942cf1f commit 4700281

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

integration/envoy.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ static_resources:
4040
"num_workers": 2,
4141
"dirname": "/tmp/"
4242
}
43+
- name: dynamic_modules/header_mutation
44+
typed_config:
45+
# 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
46+
"@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter
47+
dynamic_module_config:
48+
name: rust_module
49+
filter_name: header_mutation
50+
filter_config: |
51+
{
52+
"request_headers": [["X-Envoy-Header", "envoy-header"], ["X-Envoy-Header2", "envoy-header2"]],
53+
"response_headers": [["Foo", "bar"], ["Foo2", "bar2"]]
54+
}
4355
- name: envoy.filters.http.router
4456
typed_config:
4557
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

integration/main_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,43 @@ func TestIntegration(t *testing.T) {
132132
}, 30*time.Second, 1*time.Second)
133133
})
134134

135+
t.Run("http_header_mutation", func(t *testing.T) {
136+
require.Eventually(t, func() bool {
137+
req, err := http.NewRequest("GET", "http://localhost:1062/headers", nil)
138+
require.NoError(t, err)
139+
140+
resp, err := http.DefaultClient.Do(req)
141+
if err != nil {
142+
t.Logf("Envoy not ready yet: %v", err)
143+
return false
144+
}
145+
defer resp.Body.Close()
146+
body, err := io.ReadAll(resp.Body)
147+
if err != nil {
148+
t.Logf("Envoy not ready yet: %v", err)
149+
return false
150+
}
151+
152+
t.Logf("response: headers=%v, body=%s", resp.Header, string(body))
153+
require.Equal(t, 200, resp.StatusCode)
154+
155+
// HttpBin returns a JSON object containing the request headers.
156+
type httpBinHeadersBody struct {
157+
Headers map[string]string `json:"headers"`
158+
}
159+
var headersBody httpBinHeadersBody
160+
require.NoError(t, json.Unmarshal(body, &headersBody))
161+
162+
require.Equal(t, "envoy-header", headersBody.Headers["X-Envoy-Header"])
163+
require.Equal(t, "envoy-header2", headersBody.Headers["X-Envoy-Header2"])
164+
165+
// We also need to check that the response headers were mutated.
166+
require.Equal(t, "bar", resp.Header.Get("Foo"))
167+
require.Equal(t, "bar2", resp.Header.Get("Foo2"))
168+
return true
169+
}, 30*time.Second, 200*time.Millisecond)
170+
})
171+
135172
t.Run("http_random_auth", func(t *testing.T) {
136173
got200 := false
137174
got403 := false

rust/src/http_header_mutation.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use envoy_proxy_dynamic_modules_rust_sdk::*;
2+
use serde::{Deserialize, Serialize};
3+
4+
/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilterConfig`] trait.
5+
///
6+
/// The trait corresponds to a Envoy filter chain configuration.
7+
#[derive(Serialize, Deserialize, Debug)]
8+
pub struct FilterConfig {
9+
request_headers: Vec<(String, String)>,
10+
response_headers: Vec<(String, String)>,
11+
}
12+
13+
impl FilterConfig {
14+
/// This is the constructor for the [`FilterConfig`].
15+
///
16+
/// filter_config is the filter config from the Envoy config here:
17+
/// 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
18+
pub fn new(filter_config: &str) -> Option<Self> {
19+
let filter_config: FilterConfig = match serde_json::from_str(filter_config) {
20+
Ok(cfg) => cfg,
21+
Err(err) => {
22+
eprintln!("Error parsing filter config: {}", err);
23+
return None;
24+
}
25+
};
26+
Some(filter_config)
27+
}
28+
}
29+
30+
impl<EC: EnvoyHttpFilterConfig, EHF: EnvoyHttpFilter> HttpFilterConfig<EC, EHF> for FilterConfig {
31+
/// This is called for each new HTTP filter.
32+
fn new_http_filter(&mut self, _envoy: &mut EC) -> Box<dyn HttpFilter<EHF>> {
33+
Box::new(Filter {
34+
request_headers: self.request_headers.clone(),
35+
response_headers: self.response_headers.clone(),
36+
})
37+
}
38+
}
39+
40+
/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilter`] trait.
41+
///
42+
/// This sets the request and response headers to the values specified in the filter config.
43+
pub struct Filter {
44+
request_headers: Vec<(String, String)>,
45+
response_headers: Vec<(String, String)>,
46+
}
47+
48+
/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::HttpFilter`] trait.
49+
impl<EHF: EnvoyHttpFilter> HttpFilter<EHF> for Filter {
50+
fn on_request_headers(
51+
&mut self,
52+
envoy_filter: &mut EHF,
53+
_end_of_stream: bool,
54+
) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status {
55+
for (key, value) in &self.request_headers {
56+
envoy_filter.set_request_header(key, value.as_bytes());
57+
}
58+
abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue
59+
}
60+
61+
fn on_response_headers(
62+
&mut self,
63+
envoy_filter: &mut EHF,
64+
_end_of_stream: bool,
65+
) -> abi::envoy_dynamic_module_type_on_http_filter_response_headers_status {
66+
for (key, value) in &self.response_headers {
67+
envoy_filter.set_response_header(key, value.as_bytes());
68+
}
69+
abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue
70+
}
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::*;
76+
77+
#[test]
78+
/// This demonstrates how to write a test without Envoy using a mock provided by the SDK.
79+
fn test_filter() {
80+
let mut envoy_filter = envoy_proxy_dynamic_modules_rust_sdk::MockEnvoyHttpFilter::new();
81+
let mut filter = Filter {
82+
request_headers: vec![("X-Foo".to_string(), "bar".to_string())],
83+
response_headers: vec![("X-Bar".to_string(), "foo".to_string())],
84+
};
85+
86+
envoy_filter
87+
.expect_set_request_header()
88+
.returning(|key, value| {
89+
assert_eq!(key, "X-Foo");
90+
assert_eq!(value, b"bar");
91+
return true;
92+
});
93+
envoy_filter
94+
.expect_set_response_header()
95+
.returning(|key, value| {
96+
assert_eq!(key, "X-Bar");
97+
assert_eq!(value, b"foo");
98+
return true;
99+
});
100+
assert_eq!(
101+
filter.on_request_headers(&mut envoy_filter, false),
102+
abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue
103+
);
104+
assert_eq!(
105+
filter.on_response_headers(&mut envoy_filter, false),
106+
abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue
107+
);
108+
}
109+
}

rust/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use envoy_proxy_dynamic_modules_rust_sdk::*;
22

33
mod http_access_logger;
4+
mod http_header_mutation;
45
mod http_passthrough;
56
mod http_random_auth;
67
mod http_zero_copy_regex_waf;
@@ -39,6 +40,8 @@ fn new_http_filter_config_fn<EC: EnvoyHttpFilterConfig, EHF: EnvoyHttpFilter>(
3940
"random_auth" => Some(Box::new(http_random_auth::FilterConfig::new(filter_config))),
4041
"zero_copy_regex_waf" => http_zero_copy_regex_waf::FilterConfig::new(filter_config)
4142
.map(|config| Box::new(config) as Box<dyn HttpFilterConfig<EC, EHF>>),
43+
"header_mutation" => http_header_mutation::FilterConfig::new(filter_config)
44+
.map(|config| Box::new(config) as Box<dyn HttpFilterConfig<EC, EHF>>),
4245
_ => panic!("Unknown filter name: {}", filter_name),
4346
}
4447
}

0 commit comments

Comments
 (0)