Skip to content
Open
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
16 changes: 16 additions & 0 deletions plugins/samples/add_geo_query/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//:plugins.bzl", "proxy_wasm_plugin_go", "proxy_wasm_tests")

licenses(["notice"]) # Apache 2

proxy_wasm_plugin_go(
name = "plugin_go.wasm",
srcs = ["plugin.go"],
)

proxy_wasm_tests(
name = "tests",
plugins = [
":plugin_go.wasm",
],
tests = ":tests.textpb",
)
97 changes: 97 additions & 0 deletions plugins/samples/add_geo_query/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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]
package main

import (
"strings"

"github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm"
"github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types"
)

func main() {}

func init() {
proxywasm.SetVMContext(&vmContext{})
}

type vmContext struct {
types.DefaultVMContext
}

type pluginContext struct {
types.DefaultPluginContext
}

type httpContext struct {
types.DefaultHttpContext
}

func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
return &pluginContext{}
}

func (*pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
return &httpContext{}
}

func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
// Get country value from Cloud CDN headers or default to "unknown"
countryValue := ctx.getCountryValue()

// Log the country value for GCP logs
proxywasm.LogInfof("country: %s", countryValue)

// Get current path and add country query parameter
path, err := proxywasm.GetHttpRequestHeader(":path")
if err != nil {
return types.ActionContinue
}

newPath := ctx.addCountryParameter(path, countryValue)
proxywasm.ReplaceHttpRequestHeader(":path", newPath)

return types.ActionContinue
}

func (ctx *httpContext) getCountryValue() string {
// Try common CDN country headers
countryHeaders := []string{
"X-Country",
"CloudFront-Viewer-Country",
"X-Client-Geo-Location",
"X-AppEngine-Country",
}

for _, header := range countryHeaders {
value, err := proxywasm.GetHttpRequestHeader(header)
if err == nil && value != "" {
return value
}
}

return "unknown"
}

func (ctx *httpContext) addCountryParameter(path, countryValue string) string {
// Check if query string already exists
if strings.Contains(path, "?") {
return path + "&country=" + countryValue
}
return path + "?country=" + countryValue
}

// [END serviceextensions_plugin_country_query]
149 changes: 149 additions & 0 deletions plugins/samples/add_geo_query/tests.textpb
Original file line number Diff line number Diff line change
@@ -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.*" }
}
}
}