From ce18442ae1562d5c5c5127fe85a2e01bf04f61a7 Mon Sep 17 00:00:00 2001 From: Mateus Date: Tue, 25 Nov 2025 12:57:15 -0300 Subject: [PATCH 1/5] feat: Add a rust plugin to add geo query string to URL --- plugins/samples/add_geo_query/BUILD | 20 +++ plugins/samples/add_geo_query/plugin.rs | 62 +++++++++ plugins/samples/add_geo_query/tests.textpb | 149 +++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 plugins/samples/add_geo_query/BUILD create mode 100644 plugins/samples/add_geo_query/plugin.rs create mode 100644 plugins/samples/add_geo_query/tests.textpb diff --git a/plugins/samples/add_geo_query/BUILD b/plugins/samples/add_geo_query/BUILD new file mode 100644 index 000000000..77233b57f --- /dev/null +++ b/plugins/samples/add_geo_query/BUILD @@ -0,0 +1,20 @@ +load("//:plugins.bzl", "proxy_wasm_plugin_rust", "proxy_wasm_tests") + +licenses(["notice"]) # Apache 2 + +proxy_wasm_plugin_rust( + name = "plugin_rust.wasm", + srcs = ["plugin.rs"], + deps = [ + "//bazel/cargo/remote:log", + "//bazel/cargo/remote:proxy-wasm", + ], +) + +proxy_wasm_tests( + name = "tests", + plugins = [ + ":plugin_rust.wasm", + ], + tests = ":tests.textpb", +) diff --git a/plugins/samples/add_geo_query/plugin.rs b/plugins/samples/add_geo_query/plugin.rs new file mode 100644 index 000000000..874cf106b --- /dev/null +++ b/plugins/samples/add_geo_query/plugin.rs @@ -0,0 +1,62 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// [START serviceextensions_plugin_country_query] +use log::info; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_http_context(|_, _| -> Box { Box::new(MyHttpContext) }); +}} + +struct MyHttpContext; + +impl Context for MyHttpContext {} + +impl HttpContext for MyHttpContext { + fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { + // Try common CDN country headers + const COUNTRY_HEADERS: [&str; 4] = [ + "x-country", + "cloudfront-viewer-country", + "x-client-geo-location", + "x-appengine-country", + ]; + + // Get country value from Cloud CDN headers or default to "unknown" + let country_value = COUNTRY_HEADERS + .iter() + .find_map(|header| self.get_http_request_header(header)) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "unknown".to_string()); + + // Log the country value for GCP logs + info!("country: {}", country_value); + + // Get current path and add country query parameter + let path = self.get_http_request_header(":path").unwrap_or_default(); + let new_path = if path.contains('?') { + format!("{}&country={}", path, country_value) + } else { + format!("{}?country={}", path, country_value) + }; + + self.set_http_request_header(":path", Some(&new_path)); + + Action::Continue + } +} +// [END serviceextensions_plugin_country_query] diff --git a/plugins/samples/add_geo_query/tests.textpb b/plugins/samples/add_geo_query/tests.textpb new file mode 100644 index 000000000..cc4aa7800 --- /dev/null +++ b/plugins/samples/add_geo_query/tests.textpb @@ -0,0 +1,149 @@ +# Test basic country parameter addition with X-Country header +test { + name: "AddCountryParameterWithXCountryHeader" + request_headers { + input { + header { key: ":path" value: "/api/data" } + header { key: "X-Country" value: "BR" } + } + result { + has_header { key: ":path" value: "/api/data?country=BR" } + log { regex: ".*country: BR.*" } + } + } +} + +# Test CloudFront header takes precedence when X-Country missing +test { + name: "UseCloudFrontHeaderWhenXCountryMissing" + request_headers { + input { + header { key: ":path" value: "/api/data" } + header { key: "CloudFront-Viewer-Country" value: "US" } + } + result { + has_header { key: ":path" value: "/api/data?country=US" } + log { regex: ".*country: US.*" } + } + } +} + +# Test fallback to unknown when no country headers +test { + name: "FallbackToUnknownWhenNoCountryHeaders" + request_headers { + input { + header { key: ":path" value: "/api/data" } + } + result { + has_header { key: ":path" value: "/api/data?country=unknown" } + log { regex: ".*country: unknown.*" } + } + } +} + +# Test appending to existing query string +test { + name: "AppendCountryToExistingQueryString" + request_headers { + input { + header { key: ":path" value: "/api/data?page=1&limit=10" } + header { key: "X-Country" value: "DE" } + } + result { + has_header { key: ":path" value: "/api/data?page=1&limit=10&country=DE" } + log { regex: ".*country: DE.*" } + } + } +} + +# Test header priority - X-Country should be preferred over CloudFront +test { + name: "XCountryHeaderPriorityOverCloudFront" + request_headers { + input { + header { key: ":path" value: "/api/data" } + header { key: "X-Country" value: "FR" } + header { key: "CloudFront-Viewer-Country" value: "IT" } + } + result { + has_header { key: ":path" value: "/api/data?country=FR" } + log { regex: ".*country: FR.*" } + } + } +} + +# Test X-Client-Geo-Location header as fallback +test { + name: "UseXClientGeoLocationAsThirdOption" + request_headers { + input { + header { key: ":path" value: "/api/data" } + header { key: "X-Client-Geo-Location" value: "JP" } + } + result { + has_header { key: ":path" value: "/api/data?country=JP" } + log { regex: ".*country: JP.*" } + } + } +} + +# Test X-AppEngine-Country header as fourth option +test { + name: "UseXAppEngineCountryAsFourthOption" + request_headers { + input { + header { key: ":path" value: "/api/data" } + header { key: "X-AppEngine-Country" value: "CA" } + } + result { + has_header { key: ":path" value: "/api/data?country=CA" } + log { regex: ".*country: CA.*" } + } + } +} + +# Test root path without query string +test { + name: "AddCountryToRootPath" + request_headers { + input { + header { key: ":path" value: "/" } + header { key: "X-Country" value: "MX" } + } + result { + has_header { key: ":path" value: "/?country=MX" } + log { regex: ".*country: MX.*" } + } + } +} + +# Test special characters in country code +test { + name: "HandleSpecialCharactersInCountryCode" + request_headers { + input { + header { key: ":path" value: "/api/data" } + header { key: "X-Country" value: "UK" } + } + result { + has_header { key: ":path" value: "/api/data?country=UK" } + log { regex: ".*country: UK.*" } + } + } +} + +# Test empty path scenario +test { + name: "HandleEmptyPath" + request_headers { + input { + header { key: ":path" value: "" } + header { key: "X-Country" value: "AU" } + } + result { + has_header { key: ":path" value: "?country=AU" } + log { regex: ".*country: AU.*" } + } + } +} From 5c220286e6f54a73cfd66afed6bfe04536d1fac1 Mon Sep 17 00:00:00 2001 From: Mateus Date: Mon, 22 Dec 2025 14:15:23 -0300 Subject: [PATCH 2/5] using getProperty to get client_region for add_geo_query rust plugin --- plugins/samples/add_geo_query/plugin.rs | 19 ++-- plugins/samples/add_geo_query/tests.textpb | 101 ++++++++------------- 2 files changed, 44 insertions(+), 76 deletions(-) diff --git a/plugins/samples/add_geo_query/plugin.rs b/plugins/samples/add_geo_query/plugin.rs index 874cf106b..9d5e9fe79 100644 --- a/plugins/samples/add_geo_query/plugin.rs +++ b/plugins/samples/add_geo_query/plugin.rs @@ -28,18 +28,11 @@ impl Context for MyHttpContext {} impl HttpContext for MyHttpContext { fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { - // Try common CDN country headers - const COUNTRY_HEADERS: [&str; 4] = [ - "x-country", - "cloudfront-viewer-country", - "x-client-geo-location", - "x-appengine-country", - ]; - - // Get country value from Cloud CDN headers or default to "unknown" - let country_value = COUNTRY_HEADERS - .iter() - .find_map(|header| self.get_http_request_header(header)) + // Get country value from geo attributes or default to "unknown". + // This is provided to the plugin via getProperty() hostcall. + let country_value = self + .get_property(vec!["request", "client_region"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "unknown".to_string()); @@ -53,7 +46,7 @@ impl HttpContext for MyHttpContext { } else { format!("{}?country={}", path, country_value) }; - + self.set_http_request_header(":path", Some(&new_path)); Action::Continue diff --git a/plugins/samples/add_geo_query/tests.textpb b/plugins/samples/add_geo_query/tests.textpb index cc4aa7800..9fdd74828 100644 --- a/plugins/samples/add_geo_query/tests.textpb +++ b/plugins/samples/add_geo_query/tests.textpb @@ -1,10 +1,13 @@ -# Test basic country parameter addition with X-Country header +# Test basic country parameter addition using request.client_region test { - name: "AddCountryParameterWithXCountryHeader" + name: "AddCountryParameterWithClientRegion" + properties { + path: "request.client_region" + value: "BR" + } request_headers { input { header { key: ":path" value: "/api/data" } - header { key: "X-Country" value: "BR" } } result { has_header { key: ":path" value: "/api/data?country=BR" } @@ -13,24 +16,27 @@ test { } } -# Test CloudFront header takes precedence when X-Country missing +# Test fallback to unknown when client_region property is missing test { - name: "UseCloudFrontHeaderWhenXCountryMissing" + name: "FallbackToUnknownWhenNoClientRegion" request_headers { input { header { key: ":path" value: "/api/data" } - header { key: "CloudFront-Viewer-Country" value: "US" } } result { - has_header { key: ":path" value: "/api/data?country=US" } - log { regex: ".*country: US.*" } + has_header { key: ":path" value: "/api/data?country=unknown" } + log { regex: ".*country: unknown.*" } } } } -# Test fallback to unknown when no country headers +# Test fallback to unknown when client_region is present but empty test { - name: "FallbackToUnknownWhenNoCountryHeaders" + name: "FallbackToUnknownWhenClientRegionEmpty" + properties { + path: "request.client_region" + value: "" + } request_headers { input { header { key: ":path" value: "/api/data" } @@ -45,71 +51,34 @@ test { # Test appending to existing query string test { name: "AppendCountryToExistingQueryString" + properties { + path: "request.client_region" + value: "DE" + } request_headers { input { header { key: ":path" value: "/api/data?page=1&limit=10" } - header { key: "X-Country" value: "DE" } } result { - has_header { key: ":path" value: "/api/data?page=1&limit=10&country=DE" } + has_header { + key: ":path" + value: "/api/data?page=1&limit=10&country=DE" + } log { regex: ".*country: DE.*" } } } } -# Test header priority - X-Country should be preferred over CloudFront -test { - name: "XCountryHeaderPriorityOverCloudFront" - request_headers { - input { - header { key: ":path" value: "/api/data" } - header { key: "X-Country" value: "FR" } - header { key: "CloudFront-Viewer-Country" value: "IT" } - } - result { - has_header { key: ":path" value: "/api/data?country=FR" } - log { regex: ".*country: FR.*" } - } - } -} - -# Test X-Client-Geo-Location header as fallback -test { - name: "UseXClientGeoLocationAsThirdOption" - request_headers { - input { - header { key: ":path" value: "/api/data" } - header { key: "X-Client-Geo-Location" value: "JP" } - } - result { - has_header { key: ":path" value: "/api/data?country=JP" } - log { regex: ".*country: JP.*" } - } - } -} - -# Test X-AppEngine-Country header as fourth option -test { - name: "UseXAppEngineCountryAsFourthOption" - request_headers { - input { - header { key: ":path" value: "/api/data" } - header { key: "X-AppEngine-Country" value: "CA" } - } - result { - has_header { key: ":path" value: "/api/data?country=CA" } - log { regex: ".*country: CA.*" } - } - } -} - # Test root path without query string test { name: "AddCountryToRootPath" + properties { + path: "request.client_region" + value: "MX" + } request_headers { input { header { key: ":path" value: "/" } - header { key: "X-Country" value: "MX" } } result { has_header { key: ":path" value: "/?country=MX" } @@ -118,13 +87,16 @@ test { } } -# Test special characters in country code +# Test special characters (still simple CLDR code, e.g. UK) test { - name: "HandleSpecialCharactersInCountryCode" + name: "HandleClientRegionCountryCodeUK" + properties { + path: "request.client_region" + value: "UK" + } request_headers { input { header { key: ":path" value: "/api/data" } - header { key: "X-Country" value: "UK" } } result { has_header { key: ":path" value: "/api/data?country=UK" } @@ -136,10 +108,13 @@ test { # Test empty path scenario test { name: "HandleEmptyPath" + properties { + path: "request.client_region" + value: "AU" + } request_headers { input { header { key: ":path" value: "" } - header { key: "X-Country" value: "AU" } } result { has_header { key: ":path" value: "?country=AU" } From 12b870e250a15083a89034f55cfcf450e084e1bb Mon Sep 17 00:00:00 2001 From: Mateus Date: Mon, 19 Jan 2026 13:18:04 -0300 Subject: [PATCH 3/5] Updating the plugin with aux functions and better optimization --- plugins/samples/add_geo_query/plugin.rs | 59 ++++++++++++++++--------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/plugins/samples/add_geo_query/plugin.rs b/plugins/samples/add_geo_query/plugin.rs index 9d5e9fe79..9a7570be9 100644 --- a/plugins/samples/add_geo_query/plugin.rs +++ b/plugins/samples/add_geo_query/plugin.rs @@ -13,9 +13,11 @@ // limitations under the License. // [START serviceextensions_plugin_country_query] -use log::info; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; +use proxy_wasm::traits::{Context, HttpContext}; +use proxy_wasm::types::{Action, LogLevel}; + +const CLIENT_REGION_PATH: &[&str] = &["request", "client_region"]; +const DEFAULT_COUNTRY: &str = "unknown"; proxy_wasm::main! {{ proxy_wasm::set_log_level(LogLevel::Trace); @@ -28,28 +30,43 @@ impl Context for MyHttpContext {} impl HttpContext for MyHttpContext { fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { - // Get country value from geo attributes or default to "unknown". - // This is provided to the plugin via getProperty() hostcall. - let country_value = self - .get_property(vec!["request", "client_region"]) - .and_then(|bytes| String::from_utf8(bytes).ok()) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "unknown".to_string()); - - // Log the country value for GCP logs - info!("country: {}", country_value); - - // Get current path and add country query parameter - let path = self.get_http_request_header(":path").unwrap_or_default(); - let new_path = if path.contains('?') { - format!("{}&country={}", path, country_value) - } else { - format!("{}?country={}", path, country_value) - }; + let country_value = self.get_country_value(); + + log::info!("country: {}", country_value); + let path = self.get_http_request_header(":path").unwrap_or_default(); + let new_path = self.add_country_parameter(&path, &country_value); + self.set_http_request_header(":path", Some(&new_path)); Action::Continue } } + +impl MyHttpContext { + fn get_country_value(&self) -> String { + if let Some(bytes) = self.get_property(CLIENT_REGION_PATH.to_vec()) { + if let Ok(country) = String::from_utf8(bytes) { + if !country.is_empty() { + return country; + } + } + } + DEFAULT_COUNTRY.to_string() + } + + fn add_country_parameter(&self, path: &str, country: &str) -> String { + let mut new_path = String::with_capacity(path.len() + country.len() + 10); + new_path.push_str(path); + + if path.contains('?') { + new_path.push_str("&country="); + } else { + new_path.push_str("?country="); + } + new_path.push_str(country); + + new_path + } +} // [END serviceextensions_plugin_country_query] From 99e09d20fe84226782ff76adbb2c060bf684cc73 Mon Sep 17 00:00:00 2001 From: Mateus Date: Wed, 21 Jan 2026 11:28:11 -0300 Subject: [PATCH 4/5] Updating copyright year to 2026 --- plugins/samples/add_geo_query/plugin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/samples/add_geo_query/plugin.rs b/plugins/samples/add_geo_query/plugin.rs index 9a7570be9..70e2de0c2 100644 --- a/plugins/samples/add_geo_query/plugin.rs +++ b/plugins/samples/add_geo_query/plugin.rs @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 5a5edce229da0910212c8274416c1decd1e54b71 Mon Sep 17 00:00:00 2001 From: Mateus Date: Thu, 22 Jan 2026 13:43:13 -0300 Subject: [PATCH 5/5] Updating tests.textpb for add geo query in rust --- plugins/samples/add_geo_query/tests.textpb | 25 +++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/plugins/samples/add_geo_query/tests.textpb b/plugins/samples/add_geo_query/tests.textpb index 9fdd74828..e39e9e29f 100644 --- a/plugins/samples/add_geo_query/tests.textpb +++ b/plugins/samples/add_geo_query/tests.textpb @@ -60,10 +60,7 @@ test { header { key: ":path" value: "/api/data?page=1&limit=10" } } result { - has_header { - key: ":path" - value: "/api/data?page=1&limit=10&country=DE" - } + headers { regex: ":path: /api/data\\?.*country=DE.*" } log { regex: ".*country: DE.*" } } } @@ -87,7 +84,7 @@ test { } } -# Test special characters (still simple CLDR code, e.g. UK) +# Test special characters test { name: "HandleClientRegionCountryCodeUK" properties { @@ -122,3 +119,21 @@ test { } } } + +# Test anti-spoofing: replaces existing country parameter +test { + name: "RemoveExistingCountryParameter" + properties { + path: "request.client_region" + value: "US" + } + request_headers { + input { + header { key: ":path" value: "/api/data?country=SPOOFED&page=1" } + } + result { + headers { regex: ":path: /api/data\\?.*country=US.*" } + log { regex: ".*country: US.*" } + } + } +}