Skip to content

Commit 7914659

Browse files
committed
feat: init
1 parent e5fe6da commit 7914659

File tree

16 files changed

+738
-0
lines changed

16 files changed

+738
-0
lines changed

.github/workflows/ci.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- "**"
7+
8+
jobs:
9+
10+
test:
11+
permissions:
12+
contents: 'read'
13+
runs-on: ubuntu-22.04
14+
15+
steps:
16+
- uses: actions/[email protected]
17+
18+
- name: Build
19+
run: |
20+
make build-docker
21+
22+
- name: Test
23+
run: |
24+
make test-docker

LICENSE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) 2023 KAUCHE, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Makefile

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
OS := $(shell uname | awk '{print tolower($$0)}')
2+
ARCH := $(shell case $$(uname -m) in (x86_64) echo amd64 ;; (aarch64) echo arm64 ;; (*) echo $$(uname -m) ;; esac)
3+
4+
BIN_DIR := ./.bin
5+
6+
TINYGO_VERSION := 0.26.0
7+
TINYGO := $(abspath $(BIN_DIR)/tinygo-$(TINYGO_VERSION))/bin/tinygo
8+
9+
DOCKER_NETWORK := proxy-wasm-http-header-rename_default
10+
11+
tinygo: $(TINYGO)
12+
$(TINYGO):
13+
@curl -sSL "https://github.com/tinygo-org/tinygo/releases/download/v$(TINYGO_VERSION)/tinygo$(TINYGO_VERSION).$(OS)-$(ARCH).tar.gz" | tar -C $(BIN_DIR) -xzv tinygo
14+
@mv $(BIN_DIR)/tinygo $(BIN_DIR)/tinygo-$(TINYGO_VERSION)
15+
16+
.PHONY: test
17+
test:
18+
@cd ./test && go test -race -shuffle=on .
19+
20+
.PHONY: test-docker
21+
test-docker:
22+
docker compose stop
23+
docker compose up --detach
24+
25+
docker run --rm --network $(DOCKER_NETWORK) jwilder/dockerize:0.6.1 -wait tcp://envoy:8080 -timeout 10s
26+
docker run --rm --network $(DOCKER_NETWORK) jwilder/dockerize:0.6.1 -wait tcp://upstream:5000 -timeout 10s
27+
28+
docker run \
29+
--rm \
30+
--env ENVOY_ADDRESS=envoy:8080 \
31+
--volume "$(shell pwd):/workspace" \
32+
--workdir /workspace \
33+
--network $(DOCKER_NETWORK) \
34+
golang:1.19.5-bullseye make test
35+
36+
.PHONY: build
37+
build: $(TINYGO)
38+
@$(TINYGO) build -o $(BIN_DIR)/proxy-wasm-http-header-rename.wasm -scheduler=none -target=wasi .
39+
40+
.PHONY: build-docker
41+
build-docker:
42+
@docker run \
43+
--rm \
44+
--env XDG_CACHE_HOME=/tmp/.cache \
45+
--volume "$(shell pwd):/workspace" \
46+
--user "$(shell id -u):$(shell id -g)" \
47+
--workdir /workspace \
48+
golang:1.19.5-bullseye \
49+
make build

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# proxy-wasm-http-header-rename
2+
3+
A [proxy-wasm](https://github.com/proxy-wasm/spec) compliant WebAssembly module for renaming HTTP Headers.
4+
5+
## Usage
6+
7+
1. Download the latest WebAssembly module binary from the [release page](https://github.com/kauche/proxy-wasm-http-header-rename/releases).
8+
9+
2. Configure the proxy to use the WebAssembly module like below (this assumes [Envoy](https://www.envoyproxy.io/) as the proxy):
10+
11+
```yaml
12+
listeners:
13+
- name: example
14+
filter_chains:
15+
- filters:
16+
- name: envoy.filters.network.http_connection_manager
17+
typed_config:
18+
# ...
19+
http_filters:
20+
- name: envoy.filters.http.wasm
21+
typed_config:
22+
'@type': type.googleapis.com/udpa.type.v1.TypedStruct
23+
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
24+
value:
25+
config:
26+
vm_config:
27+
runtime: envoy.wasm.runtime.v8
28+
code:
29+
local:
30+
filename: /etc/envoy/proxy-wasm-http-header-rename.wasm
31+
configuration:
32+
"@type": type.googleapis.com/google.protobuf.StringValue
33+
value: |
34+
{
35+
"request_headers_to_rename": [
36+
{
37+
"header": {
38+
"key": "original-header-name",
39+
"value": "new-header-name"
40+
}
41+
}
42+
]
43+
}
44+
- name: envoy.filters.http.router
45+
typed_config:
46+
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
47+
# ...
48+
```
49+
50+
## Motivation
51+
52+
For now, Envoy does not support renaming HTTP Headers natively as described in [this issue](https://github.com/envoyproxy/envoy/issues/8947). So, we can use this WebAssembly module to rename HTTP Headers until the issue is resolved.

compose.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
services:
3+
envoy:
4+
image: envoyproxy/envoy:v1.24.1
5+
ports:
6+
- ${PORT-8080}:8080
7+
volumes:
8+
- ./test/envoy.yaml:/etc/envoy/envoy.yaml
9+
- ./.bin/proxy-wasm-http-header-rename.wasm:/etc/envoy/proxy-wasm-http-header-rename.wasm
10+
command: /docker-entrypoint.sh envoy -c /etc/envoy/envoy.yaml --log-format '%L%m%d %T.%e %t envoy/%@] [%t][%n]%v' --log-format-escaped
11+
12+
upstream:
13+
image: ghcr.io/110y/echoserver/echoserver:0.0.6
14+
ports:
15+
- 9091:5000
16+
17+
volumes:
18+
go-pkg-mod:

go.mod

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module github.com/kauche/proxy-wasm-http-header-rename
2+
3+
go 1.19
4+
5+
require (
6+
github.com/tetratelabs/proxy-wasm-go-sdk v0.20.0
7+
github.com/tidwall/gjson v1.14.4
8+
)
9+
10+
require (
11+
github.com/tidwall/match v1.1.1 // indirect
12+
github.com/tidwall/pretty v1.2.0 // indirect
13+
)

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3+
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
4+
github.com/tetratelabs/proxy-wasm-go-sdk v0.20.0 h1:i/xtxt/jHXtp/yImhlp4pAWx0eVzIBKOaay3Nu4Iw3k=
5+
github.com/tetratelabs/proxy-wasm-go-sdk v0.20.0/go.mod h1:7uUubjgZpmccNAPqSS6Il6CF+tk3BGf4qSCJZp3W8s8=
6+
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
7+
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
8+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
9+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
10+
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
11+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
12+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

internal/config.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package internal
2+
3+
const (
4+
configKeyRequestHeadersToRename = "request_headers_to_rename"
5+
configKeyHeader = "header"
6+
configKeyKey = "key"
7+
configKeyValue = "value"
8+
)
9+
10+
type pluginConfiguration struct {
11+
requestHeadersToRename []requestHeaderToRename
12+
}
13+
14+
type requestHeaderToRename struct {
15+
header headerValue
16+
}
17+
18+
type headerValue struct {
19+
key string
20+
value string
21+
}

internal/http.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
7+
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
8+
)
9+
10+
var _ types.HttpContext = (*httpContext)(nil)
11+
12+
type httpContext struct {
13+
types.DefaultHttpContext
14+
15+
configuration *pluginConfiguration
16+
}
17+
18+
func (c *httpContext) OnHttpRequestHeaders(_ int, _ bool) types.Action {
19+
for _, requestHeaderToRename := range c.configuration.requestHeadersToRename {
20+
if err := c.renameRequestHeader(requestHeaderToRename.header.key, requestHeaderToRename.header.value); err != nil {
21+
setErrorHTTPResponseWithLog("failed to rename the header: %s", err)
22+
return types.ActionPause
23+
}
24+
}
25+
26+
return types.ActionContinue
27+
}
28+
29+
func (c *httpContext) renameRequestHeader(origName, newName string) error {
30+
value, err := proxywasm.GetHttpRequestHeader(origName)
31+
if err != nil {
32+
if err == types.ErrorStatusNotFound {
33+
return nil
34+
}
35+
36+
return fmt.Errorf("failed to get the original header, `%s`: %w", origName, err)
37+
}
38+
39+
if err := proxywasm.ReplaceHttpRequestHeader(newName, value); err != nil {
40+
return fmt.Errorf("failed to set the new header, `%s`: %w", newName, err)
41+
}
42+
43+
if err := proxywasm.RemoveHttpRequestHeader(origName); err != nil {
44+
return fmt.Errorf("failed to delete the original header, `%s`: %w", origName, err)
45+
}
46+
47+
return nil
48+
}
49+
50+
func setErrorHTTPResponseWithLog(format string, args ...interface{}) {
51+
proxywasm.LogErrorf(format, args...)
52+
if err := proxywasm.SendHttpResponse(500, nil, []byte(`{"error": "internal server error"}`), -1); err != nil {
53+
proxywasm.LogErrorf("failed to set the http error response: %s", err)
54+
}
55+
}

internal/plugin.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package internal
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
8+
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
9+
"github.com/tidwall/gjson"
10+
)
11+
12+
var _ types.PluginContext = (*pluginContext)(nil)
13+
14+
type pluginContext struct {
15+
types.DefaultPluginContext
16+
17+
configuration *pluginConfiguration
18+
}
19+
20+
func (c *pluginContext) NewHttpContext(_ uint32) types.HttpContext {
21+
return &httpContext{
22+
configuration: c.configuration,
23+
}
24+
}
25+
26+
func (c *pluginContext) OnPluginStart(_ int) types.OnPluginStartStatus {
27+
config, err := getPluginConfiguration()
28+
if err != nil {
29+
proxywasm.LogErrorf("failed to get the plugin configuration: %s", err)
30+
return types.OnPluginStartStatusFailed
31+
}
32+
33+
c.configuration = config
34+
35+
return types.OnPluginStartStatusOK
36+
}
37+
38+
func getPluginConfiguration() (*pluginConfiguration, error) {
39+
config, err := proxywasm.GetPluginConfiguration()
40+
if err != nil {
41+
if err == types.ErrorStatusNotFound {
42+
return nil, errors.New("the plugin configuration is not found")
43+
}
44+
45+
return nil, fmt.Errorf("failed to get the plugin configuration: %w", err)
46+
}
47+
48+
if len(config) == 0 {
49+
return nil, errors.New("the plugin configuration is empty")
50+
}
51+
52+
if !gjson.ValidBytes(config) {
53+
return nil, errors.New("the plugin configuration is not valid JSON")
54+
}
55+
56+
jsonConfig := gjson.ParseBytes(config)
57+
58+
requestHeadersToRename := jsonConfig.Get(configKeyRequestHeadersToRename).Array()
59+
if len(requestHeadersToRename) == 0 {
60+
return nil, errors.New("the request headers to rename are not found")
61+
}
62+
63+
headersToRename := make([]requestHeaderToRename, len(requestHeadersToRename))
64+
65+
for i, header := range requestHeadersToRename {
66+
h := header.Get(configKeyHeader)
67+
68+
key := h.Get(configKeyKey).String()
69+
if key == "" {
70+
return nil, errors.New("the header key for renaming is empty")
71+
}
72+
73+
value := h.Get(configKeyValue).String()
74+
if value == "" {
75+
return nil, errors.New("the header value for renaming is empty")
76+
}
77+
78+
headersToRename[i] = requestHeaderToRename{
79+
header: headerValue{
80+
key: key,
81+
value: value,
82+
},
83+
}
84+
}
85+
86+
return &pluginConfiguration{
87+
requestHeadersToRename: headersToRename,
88+
}, nil
89+
}

0 commit comments

Comments
 (0)