diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 8bd6c08..8ab2125 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -97,7 +97,7 @@ jobs: ~/go/bin key: go-test-${{ hashFiles('**/go.mod', '**/go.sum') }} - - run: go test ./... -v + - run: CGO_ENABLED=0 go test ./... -v - run: go build -buildmode=c-shared -o main.so - run: go tool golangci-lint run diff --git a/go/go.mod b/go/go.mod index 4be9368..253834e 100644 --- a/go/go.mod +++ b/go/go.mod @@ -46,6 +46,8 @@ require ( github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -54,6 +56,7 @@ require ( github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.9 // indirect github.com/go-critic/go-critic v0.12.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -75,6 +78,7 @@ require ( github.com/golangci/revgrep v0.8.0 // indirect github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect diff --git a/go/go.sum b/go/go.sum index 52ba461..fac4cd9 100644 --- a/go/go.sum +++ b/go/go.sum @@ -134,6 +134,10 @@ github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42 github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 h1:aQYWswi+hRL2zJqGacdCZx32XjKYV8ApXFGntw79XAM= +github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -169,6 +173,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= diff --git a/go/javascript.go b/go/javascript.go new file mode 100644 index 0000000..2629b78 --- /dev/null +++ b/go/javascript.go @@ -0,0 +1,205 @@ +package main + +import ( + "fmt" + "io" + "log" + "math/rand" + "os" + "sync" + + "github.com/dop251/goja" + "github.com/envoyproxy/dynamic-modules-examples/go/gosdk" +) + +const ( + javaScriptExportedSymbolOnConfig = "OnConfigure" + javaScriptExportedSymbolOnRequestHeaders = "OnRequestHeaders" + javaScriptExportedSymbolOnResponseHeaders = "OnResponseHeaders" + + functionDeclTemplate = `globalThis.%[1]s = %[1]s` + numberOfVMPool = 24 +) + +type ( + // javaScriptFilterConfig implements [gosdk.HttpFilterConfig]. + javaScriptFilterConfig struct { + vms [numberOfVMPool]*javaScriptVM + } + // javaScriptFilter implements [gosdk.HttpFilter]. + javaScriptFilter struct { + vm *javaScriptVM + requestHeaders map[string]string + responseHeaders map[string]string + } + javaScriptVM struct { + *goja.Runtime + mux sync.Mutex + onRequestHeaders goja.Callable + onResponseHeaders goja.Callable + } +) + +func newJavaScriptFilterConfig(userCode string) gosdk.HttpFilterConfig { + c := &javaScriptFilterConfig{} + + for i := range numberOfVMPool { + vm, err := newJavaScriptVM(userCode, os.Stdout) + if err != nil { + log.Printf("failed to create JavaScript VM: %v", err) + return nil + } + c.vms[i] = vm + } + return c +} + +func newJavaScriptVM(script string, w io.Writer) (*javaScriptVM, error) { + vm := goja.New() + console := vm.NewObject() + err := console.Set("log", func(call goja.FunctionCall) goja.Value { + args := make([]interface{}, 0, len(call.Arguments)) + for _, a := range call.Arguments { + args = append(args, a.Export()) + } + _, _ = fmt.Fprint(w, args...) + return goja.Undefined() + }) + if err != nil { + return nil, fmt.Errorf("failed to set console.log: %w", err) + } + err = vm.Set("console", console) + if err != nil { + return nil, fmt.Errorf("failed to set console: %w", err) + } + + _, err = vm.RunString(script) + if err != nil { + return nil, fmt.Errorf("failed to run script: %w", err) + } + + // Call OnConfigure. + onConfigure, ok := goja.AssertFunction(vm.GlobalObject().Get(javaScriptExportedSymbolOnConfig)) + if !ok { + return nil, fmt.Errorf("failed to get %s function", javaScriptExportedSymbolOnConfig) + } + _, err = onConfigure(goja.Undefined()) + if err != nil { + return nil, fmt.Errorf("failed to call %s function: %w", javaScriptExportedSymbolOnConfig, err) + } + + ret := &javaScriptVM{Runtime: vm} + // Check two exported functions. + ret.onRequestHeaders, ok = goja.AssertFunction(vm.GlobalObject().Get(javaScriptExportedSymbolOnRequestHeaders)) + if !ok { + return nil, fmt.Errorf("failed to get %s function", javaScriptExportedSymbolOnRequestHeaders) + } + ret.onResponseHeaders, ok = goja.AssertFunction(vm.GlobalObject().Get(javaScriptExportedSymbolOnResponseHeaders)) + if !ok { + return nil, fmt.Errorf("failed to get %s function", javaScriptExportedSymbolOnResponseHeaders) + } + return ret, nil +} + +// NewFilter implements [gosdk.HttpFilterConfig]. +func (p *javaScriptFilterConfig) NewFilter() gosdk.HttpFilter { + vm := p.vms[rand.Intn(numberOfVMPool)] + return &javaScriptFilter{vm: vm, requestHeaders: make(map[string]string), responseHeaders: make(map[string]string)} +} + +// RequestHeaders implements [gosdk.HttpFilter]. +func (p *javaScriptFilter) RequestHeaders(e gosdk.EnvoyHttpFilter, _ bool) gosdk.RequestHeadersStatus { + headers := e.GetRequestHeaders() + for k, vs := range headers { + p.requestHeaders[k] = vs[0] + } + p.vm.mux.Lock() + defer p.vm.mux.Unlock() + vm := p.vm + obj := vm.NewObject() + _ = obj.Set("getRequestHeader", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 1 { + return vm.ToValue("") + } + key := call.Argument(0).String() + return vm.ToValue(p.requestHeaders[key]) + }) + _ = obj.Set("setRequestHeader", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 2 { + return goja.Undefined() + } + key := call.Argument(0).String() + value := call.Argument(1).String() + p.requestHeaders[key] = value + e.SetRequestHeader(key, []byte(value)) + return goja.Undefined() + }) + if _, err := vm.onRequestHeaders(goja.Undefined(), obj); err != nil { + log.Printf("failed to call %s: %v", javaScriptExportedSymbolOnRequestHeaders, err) + return gosdk.RequestHeadersStatusStopIteration + } + return gosdk.RequestHeadersStatusContinue +} + +// ResponseHeaders implements [gosdk.HttpFilter]. +func (p *javaScriptFilter) ResponseHeaders(e gosdk.EnvoyHttpFilter, _ bool) gosdk.ResponseHeadersStatus { + headers := e.GetResponseHeaders() + for k, vs := range headers { + p.responseHeaders[k] = vs[0] + } + p.vm.mux.Lock() + defer p.vm.mux.Unlock() + vm := p.vm + obj := vm.NewObject() + _ = obj.Set("getRequestHeader", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 1 { + return vm.ToValue("") + } + key := call.Argument(0).String() + return vm.ToValue(p.requestHeaders[key]) + }) + + // Setting request header in response phase is not allowed. + + _ = obj.Set("getResponseHeader", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 1 { + return vm.ToValue("") + } + key := call.Argument(0).String() + return vm.ToValue(p.responseHeaders[key]) + }) + _ = obj.Set("setResponseHeader", func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 2 { + return goja.Undefined() + } + key := call.Argument(0).String() + value := call.Argument(1).String() + p.responseHeaders[key] = value + e.SetResponseHeader(key, []byte(value)) + return goja.Undefined() + }) + if _, err := vm.onResponseHeaders(goja.Undefined(), obj); err != nil { + log.Printf("failed to call %s: %v", javaScriptExportedSymbolOnResponseHeaders, err) + return gosdk.ResponseHeadersStatusStopIteration + } + return gosdk.ResponseHeadersStatusContinue +} + +// Destroy implements [gosdk.HttpFilterConfig]. +func (p *javaScriptFilterConfig) Destroy() {} + +// Scheduled implements gosdk.HttpFilter. +func (p *javaScriptFilter) Scheduled(gosdk.EnvoyHttpFilter, uint64) {} + +// Destroy implements [gosdk.HttpFilter]. +func (p *javaScriptFilter) Destroy() {} + +// RequestBody implements [gosdk.HttpFilter]. +func (p *javaScriptFilter) RequestBody(gosdk.EnvoyHttpFilter, bool) gosdk.RequestBodyStatus { + return gosdk.RequestBodyStatusContinue +} + +// ResponseBody implements [gosdk.HttpFilter]. +func (p *javaScriptFilter) ResponseBody(gosdk.EnvoyHttpFilter, bool) gosdk.ResponseBodyStatus { + return gosdk.ResponseBodyStatusContinue +} diff --git a/go/javascript_test.go b/go/javascript_test.go new file mode 100644 index 0000000..eff303b --- /dev/null +++ b/go/javascript_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "bytes" + "testing" + + "github.com/envoyproxy/dynamic-modules-examples/go/gosdk" + "github.com/stretchr/testify/require" +) + +func Test_newJavaScriptFilterConfig(t *testing.T) { + f := newJavaScriptFilterConfig(` +function OnConfigure () {} +function OnRequestHeaders(ctx) {} +function OnResponseHeaders(ctx) {} +`) + require.NotNil(t, f) +} + +func Test_newJavasScriptVM(t *testing.T) { + for _, tc := range []struct { + name string + script string + expOut string + expErr string + }{ + { + name: "valid script with all functions", + expOut: `OnConfigure called`, + script: ` +function OnConfigure () { + console.log("OnConfigure called"); +} +function OnRequestHeaders(ctx) { + console.log("OnRequestHeader called"); +} +function OnResponseHeaders(ctx) { + console.log("OnResponseHeader called"); +} +`, + }, + { + name: "invalid script with missing functions", + script: ` +function OnConfigure () { + console.log("OnConfigure called"); +} +`, + expErr: `failed to get OnRequestHeaders function`, + }, + { + name: "invalid script", + script: `invalid`, + expErr: `failed to run script: ReferenceError: invalid is not defined at :1:1(0)`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + logout := &bytes.Buffer{} + _, err := newJavaScriptVM(tc.script, logout) + if tc.expErr == "" { + require.Equal(t, tc.expOut, logout.String()) + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.expErr) + } + }) + } +} + +func Test_javaScriptFilter_RequestHeaders(t *testing.T) { + logout := &bytes.Buffer{} + vm, err := newJavaScriptVM( + `function OnConfigure () {} +function OnRequestHeaders(ctx) { + ctx.setRequestHeader("x-hello", "world"); + let reqId = ctx.getRequestHeader("x-request-id"); + console.log("Request ID: ", reqId); +} +function OnResponseHeaders(ctx) {}`, logout) + require.NoError(t, err) + + f := &javaScriptFilter{vm: vm, requestHeaders: map[string]string{ + "x-request-id": "12345", + }} + called := false + m := &mockEnvoyHttpFilter{ + getRequestHeaders: func() map[string][]string { return map[string][]string{"x-request-id": {"12345"}} }, + setRequestHeader: func(key string, value []byte) bool { + require.Equal(t, "x-hello", key) + require.Equal(t, "world", string(value)) + called = true + return true + }, + } + + status := f.RequestHeaders(m, false) + require.Equal(t, gosdk.RequestHeadersStatusContinue, status) + require.True(t, called) + + require.Contains(t, logout.String(), "Request ID: 12345") +} + +func Test_javaScriptFilter_ResponseHeaders(t *testing.T) { + logout := &bytes.Buffer{} + vm, err := newJavaScriptVM( + `function OnConfigure () {} +function OnRequestHeaders(ctx) {} +function OnResponseHeaders(ctx) { + ctx.setResponseHeader("x-hello", "world"); + let status = ctx.getResponseHeader(":status"); + console.log("Response status: ", status); +}`, logout) + require.NoError(t, err) + + f := &javaScriptFilter{vm: vm, responseHeaders: map[string]string{ + ":status": "200", + }} + called := false + m := &mockEnvoyHttpFilter{ + getResponseHeaders: func() map[string][]string { return map[string][]string{":status": {"200"}} }, + setResponseHeader: func(key string, value []byte) bool { + require.Equal(t, "x-hello", key) + require.Equal(t, "world", string(value)) + called = true + return true + }, + } + + status := f.ResponseHeaders(m, false) + require.Equal(t, gosdk.ResponseHeadersStatusContinue, status) + require.True(t, called) + + require.Contains(t, logout.String(), "Response status: 200") +} diff --git a/go/main.go b/go/main.go index 241ef13..c2e3f9f 100644 --- a/go/main.go +++ b/go/main.go @@ -20,6 +20,8 @@ func newHttpFilterConfig(name string, config []byte) gosdk.HttpFilterConfig { return headerAuthFilterConfig{authHeaderName: string(config)} case "delay": return delayFilterConfig{} + case "javascript": + return newJavaScriptFilterConfig(string(config)) default: panic("unknown filter: " + name) } diff --git a/go/mock_test.go b/go/mock_test.go new file mode 100644 index 0000000..64f4b57 --- /dev/null +++ b/go/mock_test.go @@ -0,0 +1,119 @@ +package main + +import ( + "io" + + "github.com/envoyproxy/dynamic-modules-examples/go/gosdk" +) + +// mockEnvoyHttpFilter is a mock implementation of [gosdk.EnvoyHttpFilter] for testing. +type mockEnvoyHttpFilter struct { + getRequestHeader func(key string) (string, bool) + getRequestHeaders func() map[string][]string + setRequestHeader func(key string, value []byte) bool + getResponseHeader func(key string) (string, bool) + getResponseHeaders func() map[string][]string + setResponseHeader func(key string, value []byte) bool + getRequestBody func() (io.Reader, bool) + drainRequestBody func(n int) bool + appendRequestBody func(data []byte) bool + getResponseBody func() (io.Reader, bool) + drainResponseBody func(n int) bool + appendResponseBody func(data []byte) bool + sendLocalReply func(statusCode uint32, headers [][2]string, body []byte) + getSourceAddress func() string + getRequestProtocol func() string + newScheduler func() gosdk.Scheduler + continueRequest func() + continueResponse func() +} + +// GetRequestHeader implements [gosdk.EnvoyHttpFilter.GetRequestHeader]. +func (m mockEnvoyHttpFilter) GetRequestHeader(key string) (string, bool) { + return m.getRequestHeader(key) +} + +// GetRequestHeaders implements [gosdk.EnvoyHttpFilter.GetRequestHeaders]. +func (m mockEnvoyHttpFilter) GetRequestHeaders() map[string][]string { + return m.getRequestHeaders() +} + +// SetRequestHeader implements [gosdk.EnvoyHttpFilter.SetRequestHeader]. +func (m mockEnvoyHttpFilter) SetRequestHeader(key string, value []byte) bool { + return m.setRequestHeader(key, value) +} + +// GetResponseHeader implements [gosdk.EnvoyHttpFilter.GetResponseHeader]. +func (m mockEnvoyHttpFilter) GetResponseHeader(key string) (string, bool) { + return m.getResponseHeader(key) +} + +// GetResponseHeaders implements [gosdk.EnvoyHttpFilter.GetResponseHeaders]. +func (m mockEnvoyHttpFilter) GetResponseHeaders() map[string][]string { + return m.getResponseHeaders() +} + +// SetResponseHeader implements [gosdk.EnvoyHttpFilter.SetResponseHeader]. +func (m mockEnvoyHttpFilter) SetResponseHeader(key string, value []byte) bool { + return m.setResponseHeader(key, value) +} + +// GetRequestBody implements [gosdk.EnvoyHttpFilter.GetRequestBody]. +func (m mockEnvoyHttpFilter) GetRequestBody() (io.Reader, bool) { + return m.getRequestBody() +} + +// DrainRequestBody implements [gosdk.EnvoyHttpFilter.DrainRequestBody]. +func (m mockEnvoyHttpFilter) DrainRequestBody(n int) bool { + return m.drainRequestBody(n) +} + +// AppendRequestBody implements [gosdk.EnvoyHttpFilter.AppendRequestBody]. +func (m mockEnvoyHttpFilter) AppendRequestBody(data []byte) bool { + return m.appendRequestBody(data) +} + +// GetResponseBody implements [gosdk.EnvoyHttpFilter.GetResponseBody]. +func (m mockEnvoyHttpFilter) GetResponseBody() (io.Reader, bool) { + return m.getResponseBody() +} + +// DrainResponseBody implements [gosdk.EnvoyHttpFilter.DrainResponseBody]. +func (m mockEnvoyHttpFilter) DrainResponseBody(n int) bool { + return m.drainResponseBody(n) +} + +// AppendResponseBody implements [gosdk.EnvoyHttpFilter.AppendResponseBody]. +func (m mockEnvoyHttpFilter) AppendResponseBody(data []byte) bool { + return m.appendResponseBody(data) +} + +// SendLocalReply implements [gosdk.EnvoyHttpFilter.SendLocalReply]. +func (m mockEnvoyHttpFilter) SendLocalReply(statusCode uint32, headers [][2]string, body []byte) { + m.sendLocalReply(statusCode, headers, body) +} + +// GetSourceAddress implements [gosdk.EnvoyHttpFilter.GetSourceAddress]. +func (m mockEnvoyHttpFilter) GetSourceAddress() string { + return m.getSourceAddress() +} + +// GetRequestProtocol implements [gosdk.EnvoyHttpFilter.GetRequestProtocol]. +func (m mockEnvoyHttpFilter) GetRequestProtocol() string { + return m.getRequestProtocol() +} + +// NewScheduler implements [gosdk.EnvoyHttpFilter.NewScheduler]. +func (m mockEnvoyHttpFilter) NewScheduler() gosdk.Scheduler { + return m.newScheduler() +} + +// ContinueRequest implements [gosdk.EnvoyHttpFilter.ContinueRequest]. +func (m mockEnvoyHttpFilter) ContinueRequest() { + m.continueRequest() +} + +// ContinueResponse implements [gosdk.EnvoyHttpFilter.ContinueResponse]. +func (m mockEnvoyHttpFilter) ContinueResponse() { + m.continueResponse() +} diff --git a/integration/envoy.yaml b/integration/envoy.yaml index 7e5a6b1..0494abb 100644 --- a/integration/envoy.yaml +++ b/integration/envoy.yaml @@ -25,6 +25,40 @@ static_resources: route: cluster: httpbin http_filters: + - name: dynamic_modules/passthrough/javascript + 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: go_module + do_not_close: true + filter_name: javascript + filter_config: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: | + /// Called when the filter is configured. This is called once per VM instance. + function OnConfigure () {} + /// Called when a request header is received. `ctx` object has the following properties: + /// + /// - `getRequestHeader(name: String): String`: Function to get a request header value. + /// - `setRequestHeader(name: String, value: String): void`: Function to set a request header value. + function OnRequestHeaders(ctx) { + console.log("OnRequestHeader called"); + let foo = ctx.getRequestHeader("foo"); + ctx.setRequestHeader("x-foo", foo); + } + /// Called when a response header is received. `ctx` object has the following properties: + /// + /// - `getRequestHeader(name: String): String`: Function to get a request header value. + /// - `getResponseHeader(name: String): String`: Function to get a response header value. + /// - `setResponseHeader(name: String, value: String): void`: Function to set a response header value. + function OnResponseHeaders(ctx) { + let dog = ctx.getRequestHeader("dog"); + ctx.setResponseHeader("x-dog", dog); + let status = ctx.getResponseHeader(":status"); + ctx.setResponseHeader("x-status", status); + console.log("Response status: ", status); + } - name: dynamic_modules/passthrough 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 diff --git a/integration/main_test.go b/integration/main_test.go index 9fd5189..332849f 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -332,6 +332,48 @@ func TestIntegration(t *testing.T) { } }) + t.Run("javascript", func(t *testing.T) { + require.Eventually(t, func() bool { + req, err := http.NewRequest("GET", "http://localhost:1062/headers", nil) + require.NoError(t, err) + req.Header.Set("dog", "cat") + req.Header.Set("foo", "bar") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Logf("Envoy not ready yet: %v", err) + return false + } + defer func() { + require.NoError(t, 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 in this format. + type httpBinHeadersBody struct { + Headers map[string][]string `json:"headers"` + } + var headersBody httpBinHeadersBody + require.NoError(t, json.Unmarshal(body, &headersBody)) + + require.Contains(t, headersBody.Headers["X-Foo"], "bar") + require.Contains(t, headersBody.Headers["Foo"], "bar") + require.Contains(t, headersBody.Headers["Dog"], "cat") + + // We also need to check that the response headers were mutated. + require.Equal(t, "cat", resp.Header.Get("x-dog")) + require.Equal(t, "200", resp.Header.Get("x-status")) + return true + }, 30*time.Second, 200*time.Millisecond) + }) + t.Run("http_metrics", func(t *testing.T) { // Send test request require.Eventually(t, func() bool {