diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Helpers.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Helpers.java index ca906a6..fe5af09 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Helpers.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Helpers.java @@ -62,6 +62,9 @@ public static int int32(byte[] bytes) { } public static String string(byte[] value) { + if (value == null) { + return null; + } return new String(value, StandardCharsets.UTF_8); } diff --git a/proxy-wasm-java-host/src/test/go-examples/build.sh b/proxy-wasm-java-host/src/test/go-examples/build.sh index 925a531..fc14dbb 100755 --- a/proxy-wasm-java-host/src/test/go-examples/build.sh +++ b/proxy-wasm-java-host/src/test/go-examples/build.sh @@ -7,5 +7,5 @@ docker run -it --rm \ -e GOARCH=wasm golang:1.24-alpine sh -c " find . -mindepth 1 -type f -name 'main.go' \ | xargs -I {} sh -c 'dirname {}' \ - | xargs -I {} sh -c 'cd {} && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./main.go' + | xargs -I {} sh -c 'cd {} && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm .' " \ No newline at end of file diff --git a/proxy-wasm-java-host/src/test/go-examples/dispatch_call_on_tick/main.wasm b/proxy-wasm-java-host/src/test/go-examples/dispatch_call_on_tick/main.wasm index 24a1808..23c7b8d 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/dispatch_call_on_tick/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/dispatch_call_on_tick/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/foreign_call_on_tick/main.wasm b/proxy-wasm-java-host/src/test/go-examples/foreign_call_on_tick/main.wasm index 95291dc..ec0003e 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/foreign_call_on_tick/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/foreign_call_on_tick/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/helloworld/main.wasm b/proxy-wasm-java-host/src/test/go-examples/helloworld/main.wasm index 5fcd361..404fc44 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/helloworld/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/helloworld/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/http_auth_random/main.wasm b/proxy-wasm-java-host/src/test/go-examples/http_auth_random/main.wasm index c6ab09b..8a2cea1 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/http_auth_random/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/http_auth_random/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/http_body/main.wasm b/proxy-wasm-java-host/src/test/go-examples/http_body/main.wasm index 10f3c90..bf2365d 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/http_body/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/http_body/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/http_body_chunk/main.wasm b/proxy-wasm-java-host/src/test/go-examples/http_body_chunk/main.wasm index 61edaa8..d243bb0 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/http_body_chunk/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/http_body_chunk/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/http_headers/main.wasm b/proxy-wasm-java-host/src/test/go-examples/http_headers/main.wasm index 1198cc9..9b60ea0 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/http_headers/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/http_headers/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/http_routing/main.wasm b/proxy-wasm-java-host/src/test/go-examples/http_routing/main.wasm index 7884072..24a07d4 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/http_routing/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/http_routing/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/json_validation/main.wasm b/proxy-wasm-java-host/src/test/go-examples/json_validation/main.wasm index e81bb85..33c47f0 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/json_validation/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/json_validation/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/metrics/main.wasm b/proxy-wasm-java-host/src/test/go-examples/metrics/main.wasm index 317cf0e..ea03ac5 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/metrics/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/metrics/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/multiple_dispatches/main.wasm b/proxy-wasm-java-host/src/test/go-examples/multiple_dispatches/main.wasm index 9e67af3..d808d51 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/multiple_dispatches/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/multiple_dispatches/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/network/main.wasm b/proxy-wasm-java-host/src/test/go-examples/network/main.wasm index 1f30303..3c62f90 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/network/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/network/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/postpone_requests/main.wasm b/proxy-wasm-java-host/src/test/go-examples/postpone_requests/main.wasm index 3245e0b..8ca42e1 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/postpone_requests/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/postpone_requests/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/properties/main.wasm b/proxy-wasm-java-host/src/test/go-examples/properties/main.wasm index 40c38ac..82ec2ae 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/properties/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/properties/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/shared_data/main.wasm b/proxy-wasm-java-host/src/test/go-examples/shared_data/main.wasm index 52f5ced..037ac40 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/shared_data/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/shared_data/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/shared_queue/receiver/main.wasm b/proxy-wasm-java-host/src/test/go-examples/shared_queue/receiver/main.wasm index 9648147..4a699ce 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/shared_queue/receiver/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/shared_queue/receiver/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/shared_queue/sender/main.wasm b/proxy-wasm-java-host/src/test/go-examples/shared_queue/sender/main.wasm index 5cf579f..b355145 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/shared_queue/sender/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/shared_queue/sender/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/README.md b/proxy-wasm-java-host/src/test/go-examples/unit_tester/README.md new file mode 100644 index 0000000..cb18777 --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/README.md @@ -0,0 +1,4 @@ +## About + +This wasm module is used to test the proxy-wasm-java-host. It is a simple module that can be used to test the +host's ability to interact with a wasm module. diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/ffi.go b/proxy-wasm-java-host/src/test/go-examples/unit_tester/ffi.go new file mode 100644 index 0000000..b50c4e6 --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/ffi.go @@ -0,0 +1,83 @@ +// Copyright 2020-2024 Tetrate +// +// 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. + +package main + +import ( + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" + "strings" +) + +// ffiTests implements types.HttpContext. +type ffiTests struct { + types.DefaultHttpContext + contextID uint32 + pluginContext *pluginContext + path string +} + +func ( +p *pluginContext) ffiTests(contextID uint32) types.HttpContext { + return &ffiTests{ + contextID: contextID, + pluginContext: p, + } +} + +func (ctx *ffiTests) OnHttpRequestHeaders(int, bool) types.Action { + pathBytes, err := proxywasm.GetProperty([]string{"request", "path"}) + if err != nil { + proxywasm.LogCriticalf("failed to get :path : %v", err) + } else { + ctx.path = string(pathBytes) + } + return types.ActionContinue +} + +func (ctx *ffiTests) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action { + if strings.HasPrefix(ctx.path, "/ffiTests/") { + + // we need the full request body to call the FFI function + if !endOfStream { + // Wait until we see the entire body to replace. + return types.ActionPause + } + + funcName := strings.TrimPrefix(ctx.path, "/ffiTests/") + proxywasm.LogInfof("calling ffi: %s", funcName) + + body, err := proxywasm.GetHttpRequestBody(0, bodySize) + if err != nil { + proxywasm.LogErrorf("failed to get request body: %v", err) + return types.ActionContinue + } + + result, err := proxywasm.CallForeignFunction(funcName, body) + if err != nil { + proxywasm.LogErrorf("failed to call FFI: %v", err) + return types.ActionContinue + } + + if err := proxywasm.SendHttpResponse(200, [][2]string{}, result, -1); err != nil { + proxywasm.LogErrorf("failed to send FFI response: %v", err) + return types.ActionContinue + } + + return types.ActionContinue + } + proxywasm.LogInfo("noop") + + return types.ActionContinue +} diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/go.mod b/proxy-wasm-java-host/src/test/go-examples/unit_tester/go.mod new file mode 100644 index 0000000..aaec77e --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/go.mod @@ -0,0 +1,13 @@ +module github.com/proxy-wasm/proxy-wasm-go-sdk/examples/http_headers + +go 1.24 + +require ( + github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 + github.com/tidwall/gjson v1.18.0 +) + +require ( + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/go.sum b/proxy-wasm-java-host/src/test/go-examples/unit_tester/go.sum new file mode 100644 index 0000000..bdbfc34 --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 h1:wTcK6gcyTKJMeDka69AMjZYvisdI8CBXzTEfZ+2pOxI= +github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924/go.mod h1:9mBRvh8I6Td6sg3CwEY+zGFE4DKaIoieCaca1kQnDBE= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/http_call.go b/proxy-wasm-java-host/src/test/go-examples/unit_tester/http_call.go new file mode 100644 index 0000000..4af33de --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/http_call.go @@ -0,0 +1,119 @@ +// Copyright 2020-2024 Tetrate +// +// 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. + +package main + +import ( + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" +) + +// httpCallTests implements types.HttpContext. +type httpCallTests struct { + types.DefaultHttpContext + contextID uint32 + pluginContext *pluginContext + headers [][2]string + responseHeaders [][2]string + responseBody []byte +} + +func ( + p *pluginContext) httpCallTests(contextID uint32) types.HttpContext { + return &httpCallTests{ + DefaultHttpContext: types.DefaultHttpContext{}, + contextID: contextID, + pluginContext: p, + headers: nil, + } +} + +func (ctx *httpCallTests) OnHttpRequestHeaders(int, bool) types.Action { + proxywasm.LogDebug("OnHttpRequestHeaders") + var err error + ctx.headers, err = proxywasm.GetHttpRequestHeaders() + if err != nil { + proxywasm.LogCriticalf("failed to get request headers: %v", err) + } + + method, err := proxywasm.GetProperty([]string{"request", "method"}) + if err != nil { + proxywasm.LogCriticalf("failed to get request method: %v", err) + } + ctx.headers = append(ctx.headers, [2]string{":method", string(method)}) + + host, err := proxywasm.GetProperty([]string{"request", "host"}) + if err != nil { + proxywasm.LogCriticalf("failed to get request host: %v", err) + } + ctx.headers = append(ctx.headers, [2]string{":authority", string(host)}) + + path := gjson.Get(ctx.pluginContext.config, "path").Str + ctx.headers = append(ctx.headers, [2]string{":path", path}) + + return types.ActionContinue +} + +func (ctx *httpCallTests) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action { + proxywasm.LogDebug("OnHttpRequestBody") + if !endOfStream { + // Wait until we see the entire body to replace. + return types.ActionPause + } + + body := []byte{} + var err error + if bodySize != 0 { + body, err = proxywasm.GetHttpRequestBody(0, bodySize) + if err != nil { + proxywasm.LogErrorf("failed to get request body: %v", err) + return types.ActionContinue + } + } + + upstream := gjson.Get(ctx.pluginContext.config, "upstream").Str + if upstream == "" { + upstream = "upstream" + } + + proxywasm.LogDebug("DispatchHttpCall") + _, err = proxywasm.DispatchHttpCall(upstream, ctx.headers, body, [][2]string{}, 5000, func(numHeaders, bodySize, numTrailers int) { + + proxywasm.LogDebug("On DispatchHttpCall Response") + var err error + ctx.responseHeaders, err = proxywasm.GetHttpCallResponseHeaders() + if err != nil { + proxywasm.LogCriticalf("failed to get response headers: %v", err) + } + + ctx.responseBody, err = proxywasm.GetHttpCallResponseBody(0, bodySize) + if err != nil { + proxywasm.LogCriticalf("failed to get response body: %v", err) + } + + err = proxywasm.SendHttpResponse(200, ctx.responseHeaders, ctx.responseBody, -1) + if err != nil { + proxywasm.LogCriticalf("failed to send local response: %v", err) + } + + }) + if err != nil { + proxywasm.LogCriticalf("failed to dispatch http call: %v", err) + return types.ActionContinue + } + + // Pause the stream to wait for the http call response. + return types.ActionPause +} diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.go b/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.go new file mode 100644 index 0000000..f6c8d91 --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.go @@ -0,0 +1,94 @@ +// Copyright 2020-2024 Tetrate +// +// 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. + +package main + +import ( + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" + "strings" +) + +func main() {} + +type vmContext struct { + types.DefaultVMContext +} + +func init() { + proxywasm.SetVMContext(&vmContext{}) +} + +// pluginContext implements types.PluginContext. +type pluginContext struct { + types.DefaultPluginContext + config string + newHttpContext func(contextID uint32) types.HttpContext + requestCounter int + tickCounter int +} + +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{} +} + +// OnPluginStart implements types.PluginContext. +func (p *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { + proxywasm.LogDebug("loading plugin config") + data, err := proxywasm.GetPluginConfiguration() + if data == nil { + return types.OnPluginStartStatusOK + } + + if err != nil { + proxywasm.LogCriticalf("error reading plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + + p.config = string(data) + + if !gjson.Valid(p.config) { + proxywasm.LogCritical(`invalid configuration format; expected json`) + return types.OnPluginStartStatusFailed + } + + handlerType := strings.TrimSpace(gjson.Get(p.config, "type").Str) + switch handlerType { + case "headerTests": + p.newHttpContext = p.headerTests + case "tickTests": + p.newHttpContext = p.tickTests + case "httpCallTests": + p.newHttpContext = p.httpCallTests + case "ffiTests": + p.newHttpContext = p.ffiTests + default: + proxywasm.LogCritical(`invalid config type"}`) + return types.OnPluginStartStatusFailed + } + proxywasm.LogDebugf("using handlerType: %s", handlerType) + + return types.OnPluginStartStatusOK +} + +// NewHttpContext implements types.PluginContext. +func (p *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return p.newHttpContext(contextID) +} + +// OnTick implements types.PluginContext. +func (ctx *pluginContext) OnTick() { + ctx.tickCounter++ +} diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.wasm b/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.wasm new file mode 100644 index 0000000..bac5dba Binary files /dev/null and b/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.wasm differ diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/set_counter_header.go b/proxy-wasm-java-host/src/test/go-examples/unit_tester/set_counter_header.go new file mode 100644 index 0000000..945bf6c --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/set_counter_header.go @@ -0,0 +1,56 @@ +// Copyright 2020-2024 Tetrate +// +// 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. + +package main + +import ( + "fmt" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" +) + +// headerTests implements types.HttpContext. +type headerTests struct { + // Embed the default http context here, + // so that we don't need to reimplement all the methods. + types.DefaultHttpContext + contextID uint32 + pluginContext *pluginContext + counter int +} + +func (p *pluginContext) headerTests(contextID uint32) types.HttpContext { + return &headerTests{ + contextID: contextID, + pluginContext: p, + } +} + +func (ctx *headerTests) OnHttpRequestHeaders(_ int, _ bool) types.Action { + ctx.pluginContext.requestCounter++ + ctx.counter = ctx.pluginContext.requestCounter + if err := proxywasm.AddHttpRequestHeader("x-request-counter", fmt.Sprintf("%d", ctx.counter)); err != nil { + proxywasm.LogCriticalf("failed to set request counter header: %v", err) + } + return types.ActionContinue +} + +// OnHttpResponseHeaders implements types.HttpContext. +func (ctx *headerTests) OnHttpResponseHeaders(_ int, _ bool) types.Action { + + if err := proxywasm.AddHttpResponseHeader("x-response-counter", fmt.Sprintf("%d", ctx.counter)); err != nil { + proxywasm.LogCriticalf("failed to set response counter header: %v", err) + } + return types.ActionContinue +} diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/set_tick.go b/proxy-wasm-java-host/src/test/go-examples/unit_tester/set_tick.go new file mode 100644 index 0000000..b645e3b --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/set_tick.go @@ -0,0 +1,73 @@ +// Copyright 2020-2024 Tetrate +// +// 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. + +package main + +import ( + "fmt" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" +) + +// tickTests implements types.HttpContext. +type tickTests struct { + types.DefaultHttpContext + contextID uint32 + pluginContext *pluginContext +} + +func ( + p *pluginContext) tickTests(contextID uint32) types.HttpContext { + return &tickTests{ + contextID: contextID, + pluginContext: p, + } +} + +func (ctx *tickTests) OnHttpRequestHeaders(int, bool) types.Action { + pathBytes, err := proxywasm.GetProperty([]string{"request", "path"}) + if err != nil { + proxywasm.LogCriticalf("failed to get :path : %v", err) + } + path := string(pathBytes) + + switch path { + case "/tickTests/enable": + if err := proxywasm.SetTickPeriodMilliSeconds(100); err != nil { + proxywasm.LogCriticalf("failed to set tick period: %v", err) + return types.ActionPause + } + if err := proxywasm.SendHttpResponse(200, [][2]string{}, []byte("ok"), -1); err != nil { + proxywasm.LogCriticalf("failed to send local response: %v", err) + } + return types.ActionPause + + case "/tickTests/disable": + if err := proxywasm.SetTickPeriodMilliSeconds(0); err != nil { + proxywasm.LogCriticalf("failed to clear tick period: %v", err) + return types.ActionPause + } + if err := proxywasm.SendHttpResponse(200, [][2]string{}, []byte("ok"), -1); err != nil { + proxywasm.LogCriticalf("failed to send local response: %v", err) + } + return types.ActionPause + case "/tickTests/get": + if err := proxywasm.SendHttpResponse(200, [][2]string{}, []byte(fmt.Sprint(ctx.pluginContext.tickCounter)), -1); err != nil { + proxywasm.LogCriticalf("failed to send local response: %v", err) + } + return types.ActionPause + } + + return types.ActionContinue +} diff --git a/proxy-wasm-java-host/src/test/go-examples/vm_plugin_configuration/main.wasm b/proxy-wasm-java-host/src/test/go-examples/vm_plugin_configuration/main.wasm index 0e19805..54f1aa9 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/vm_plugin_configuration/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/vm_plugin_configuration/main.wasm differ diff --git a/proxy-wasm-jaxrs-quarkus-example/pom.xml b/proxy-wasm-jaxrs-quarkus-example/pom.xml index cd61f09..c1c957e 100644 --- a/proxy-wasm-jaxrs-quarkus-example/pom.xml +++ b/proxy-wasm-jaxrs-quarkus-example/pom.xml @@ -26,6 +26,11 @@ + + com.google.code.gson + gson + 2.12.1 + io.quarkus quarkus-arc diff --git a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/App.java b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/App.java index ac248b6..ec7b877 100644 --- a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/App.java +++ b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/App.java @@ -2,6 +2,7 @@ import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.WasmModule; +import com.google.gson.Gson; import io.roastedroot.proxywasm.StartException; import io.roastedroot.proxywasm.jaxrs.WasmPlugin; import io.roastedroot.proxywasm.jaxrs.WasmPluginFactory; @@ -14,49 +15,75 @@ public class App { public static final String EXAMPLES_DIR = "../proxy-wasm-java-host/src/test"; + private static final Gson gson = new Gson(); public static WasmModule parseTestModule(String file) { return Parser.parse(Path.of(EXAMPLES_DIR + file)); } @Produces - public WasmPluginFactory foreignCallOnTickTest() throws StartException { + public WasmPluginFactory headerTests() throws StartException { return () -> WasmPlugin.builder() - .withName("foreignCallOnTickTest") - .withLogger(new MockLogger()) - .withMinTickPeriodMilliseconds( - 100) // plugin wants a tick every 1 ms, that's too often - .withForeignFunctions(Map.of("compress", data -> data)) - .build(parseTestModule("/go-examples/foreign_call_on_tick/main.wasm")); + .withName("headerTests") + .withLogger(new MockLogger("headerTests")) + .withPluginConfig(gson.toJson(Map.of("type", "headerTests"))) + .build(parseTestModule("/go-examples/unit_tester/main.wasm")); } @Produces - public WasmPluginFactory notSharedHttpHeaders() throws StartException { + public WasmPluginFactory headerTestsNotShared() throws StartException { return () -> WasmPlugin.builder() - .withName("notSharedHttpHeaders") + .withName("headerTestsNotShared") .withShared(false) - .withPluginConfig("{\"header\": \"x-wasm-header\", \"value\": \"foo\"}") - .build(parseTestModule("/go-examples/http_headers/main.wasm")); + .withLogger(new MockLogger("headerTestsNotShared")) + .withPluginConfig(gson.toJson(Map.of("type", "headerTests"))) + .build(parseTestModule("/go-examples/unit_tester/main.wasm")); } @Produces - public WasmPluginFactory httpHeaders() throws StartException { + public WasmPluginFactory tickTests() throws StartException { return () -> WasmPlugin.builder() - .withName("httpHeaders") - .withPluginConfig("{\"header\": \"x-wasm-header\", \"value\": \"foo\"}") - .build(parseTestModule("/go-examples/http_headers/main.wasm")); + .withName("tickTests") + .withLogger(new MockLogger("tickTests")) + .withPluginConfig(gson.toJson(Map.of("type", "tickTests"))) + .build(parseTestModule("/go-examples/unit_tester/main.wasm")); } @Produces - public WasmPluginFactory dispatchCallOnTickTest() throws StartException { + public WasmPluginFactory ffiTests() throws StartException { return () -> WasmPlugin.builder() - .withName("dispatchCallOnTickTest") - .withLogger(new MockLogger()) + .withName("ffiTests") + .withLogger(new MockLogger("ffiTests")) + .withPluginConfig(gson.toJson(Map.of("type", "ffiTests"))) + .withForeignFunctions(Map.of("reverse", App::reverse)) + .build(parseTestModule("/go-examples/unit_tester/main.wasm")); + } + + public static byte[] reverse(byte[] data) { + byte[] reversed = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + reversed[i] = data[data.length - 1 - i]; + } + return reversed; + } + + @Produces + public WasmPluginFactory httpCallTests() throws StartException { + return () -> + WasmPlugin.builder() + .withName("httpCallTests") + .withLogger(new MockLogger("httpCallTests")) + .withPluginConfig( + gson.toJson( + Map.of( + "type", "httpCallTests", + "upstream", "web_service", + "path", "/ok"))) .withUpstreams(Map.of("web_service", "localhost:8081")) - .build(parseTestModule("/go-examples/dispatch_call_on_tick/main.wasm")); + .build(parseTestModule("/go-examples/unit_tester/main.wasm")); } } diff --git a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/MockLogger.java b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/MockLogger.java index 21f3b52..68b9229 100644 --- a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/MockLogger.java +++ b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/MockLogger.java @@ -9,11 +9,16 @@ public class MockLogger implements Logger { static final boolean DEBUG = "true".equals(System.getenv("DEBUG")); final ArrayList loggedMessages = new ArrayList<>(); + private final String name; + + public MockLogger(String name) { + this.name = name; + } @Override public synchronized void log(LogLevel level, String message) { if (DEBUG) { - System.out.println(level + ": " + message); + System.out.printf("%s: [%s] %s\n", level, name, message); } loggedMessages.add(message); } diff --git a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java index 770c29f..51b1624 100644 --- a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java +++ b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java @@ -2,7 +2,10 @@ import io.roastedroot.proxywasm.jaxrs.NamedWasmPlugin; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; @@ -12,20 +15,6 @@ public class Resources { @Context ContainerRequestContext requestContext; - @Path("/test/httpHeaders") - @GET - @NamedWasmPlugin("httpHeaders") - public String httpHeaders() { - return "hello world"; - } - - @Path("/test/notSharedHttpHeaders") - @GET - @NamedWasmPlugin("notSharedHttpHeaders") - public String notSharedHttpHeaders() { - return "hello world"; - } - @Path("/fail") @GET public Response fail() { @@ -45,4 +34,39 @@ public Response ok() { } return builder.entity("ok").build(); } + + @Path("/headerTests") + @GET + @NamedWasmPlugin("headerTests") + public String uhttpHeaders(@HeaderParam("x-request-counter") String counter) { + return String.format("counter: %s", counter); + } + + @Path("/headerTestsNotShared") + @GET + @NamedWasmPlugin("headerTestsNotShared") + public String unotSharedHttpHeaders(@HeaderParam("x-request-counter") String counter) { + return String.format("counter: %s", counter); + } + + @Path("/tickTests/{sub: .+ }") + @GET + @NamedWasmPlugin("tickTests") + public String tickTests(@PathParam("sub") String sub) { + return "hello world"; + } + + @Path("/ffiTests/reverse") + @POST + @NamedWasmPlugin("ffiTests") + public String ffiTests(String body) { + return body; + } + + @Path("/httpCallTests") + @GET + @NamedWasmPlugin("httpCallTests") + public String httpCallTests() { + return "hello world"; + } } diff --git a/proxy-wasm-jaxrs-quarkus-example/src/main/resources/application.properties b/proxy-wasm-jaxrs-quarkus-example/src/main/resources/application.properties index e69de29..6e516f2 100644 --- a/proxy-wasm-jaxrs-quarkus-example/src/main/resources/application.properties +++ b/proxy-wasm-jaxrs-quarkus-example/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.log.level=INFO +quarkus.log.category."org.hibernate".level=DEBUG diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/DispatchCallOnTickTest.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/DispatchCallOnTickTest.java deleted file mode 100644 index 9e659b7..0000000 --- a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/DispatchCallOnTickTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.roastedroot.proxywasm.jaxrs.example; - -import static io.roastedroot.proxywasm.jaxrs.example.Helpers.assertLogsContain; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.quarkus.test.junit.QuarkusTest; -import io.roastedroot.proxywasm.StartException; -import io.roastedroot.proxywasm.jaxrs.WasmPlugin; -import io.roastedroot.proxywasm.jaxrs.WasmPluginFeature; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class DispatchCallOnTickTest { - - @Inject WasmPluginFeature feature; - - @Test - public void test() throws InterruptedException, StartException { - WasmPlugin plugin = feature.pool("dispatchCallOnTickTest").borrow(); - assertNotNull(plugin); - - var logger = (MockLogger) plugin.logger(); - Thread.sleep(300); - - // for (var l : logger.loggedMessages()) { - // System.out.println(l); - // } - assertLogsContain( - logger.loggedMessages(), - "set tick period milliseconds: 100", - "called 1 for contextID=1", - "called 2 for contextID=1", - "response header for the dispatched call: Content-Type: text/plain;charset=UTF-8", - "response header for the dispatched call: echo-accept: */*", - "response header for the dispatched call: echo-content-length: 0", - "response header for the dispatched call: echo-Host: some_authority"); - plugin.close(); // so that the ticks don't keep running in the background. - } -} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/FFITest.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/FFITest.java new file mode 100644 index 0000000..97f8aee --- /dev/null +++ b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/FFITest.java @@ -0,0 +1,22 @@ +package io.roastedroot.proxywasm.jaxrs.example; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class FFITest { + + @Test + public void reverse() throws InterruptedException { + + given().body("My Test") + .when() + .post("/ffiTests/reverse") + .then() + .statusCode(200) + .body(equalTo("tseT yM")); + } +} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/ForeignCallOnTickTest.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/ForeignCallOnTickTest.java deleted file mode 100644 index 905172b..0000000 --- a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/ForeignCallOnTickTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.roastedroot.proxywasm.jaxrs.example; - -import static io.roastedroot.proxywasm.jaxrs.example.Helpers.assertLogsContain; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import io.quarkus.test.junit.QuarkusTest; -import io.roastedroot.proxywasm.StartException; -import io.roastedroot.proxywasm.jaxrs.WasmPlugin; -import io.roastedroot.proxywasm.jaxrs.WasmPluginFeature; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class ForeignCallOnTickTest { - - @Inject WasmPluginFeature feature; - - @Test - public void testRequest() throws InterruptedException, StartException { - WasmPlugin plugin = feature.pool("foreignCallOnTickTest").borrow(); - assertNotNull(plugin); - - var logger = (MockLogger) plugin.logger(); - Thread.sleep(200); - assertLogsContain( - logger.loggedMessages(), - String.format( - "foreign function (compress) called: %d, result: %s", - 1, "68656c6c6f20776f726c6421")); - plugin.close(); // so that the ticks don't keep running in the background. - } -} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HeadersTest.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HeadersTest.java new file mode 100644 index 0000000..fbb1a95 --- /dev/null +++ b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HeadersTest.java @@ -0,0 +1,50 @@ +package io.roastedroot.proxywasm.jaxrs.example; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +/** + * This test verifies that the plugin can modify request and response headers. + *

+ * It also verifies that the plugin state is shared between requests or can be isolated using configuration. + */ +@QuarkusTest +public class HeadersTest { + + @Test + public void testShared() { + given().when() + .get("/headerTests") + .then() + .statusCode(200) + .header("x-response-counter", "1") + .body(equalTo("counter: 1")); + + given().when() + .get("/headerTests") + .then() + .statusCode(200) + .header("x-response-counter", "2") + .body(equalTo("counter: 2")); + } + + @Test + public void testNotShared() { + given().when() + .get("/headerTestsNotShared") + .then() + .statusCode(200) + .header("x-response-counter", "1") + .body(equalTo("counter: 1")); + + given().when() + .get("/headerTestsNotShared") + .then() + .statusCode(200) + .header("x-response-counter", "1") + .body(equalTo("counter: 1")); + } +} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Helpers.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Helpers.java index 66b093b..9c54d15 100644 --- a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Helpers.java +++ b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Helpers.java @@ -12,4 +12,8 @@ public static void assertLogsContain(ArrayList loggedMessages, String... loggedMessages.contains(m), "logged messages does not contain: " + m); } } + + public static IsTrueMatcher isTrue(IsTrueMatcher.Predicate predicate) { + return new IsTrueMatcher(predicate); + } } diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpCallTests.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpCallTests.java new file mode 100644 index 0000000..c73453b --- /dev/null +++ b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpCallTests.java @@ -0,0 +1,29 @@ +package io.roastedroot.proxywasm.jaxrs.example; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.junit.QuarkusTest; +import io.roastedroot.proxywasm.StartException; +import io.roastedroot.proxywasm.jaxrs.WasmPluginFeature; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class HttpCallTests { + + @Inject WasmPluginFeature feature; + + @Test + public void test() throws InterruptedException, StartException { + + // the wasm plugin will forward the request to the /ok endpoint + given().header("test", "ok") + .when() + .get("/httpCallTests") + .then() + .statusCode(200) + .header("echo-test", "ok") + .body(equalTo("ok")); + } +} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpHeadersNotSharedTest.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpHeadersNotSharedTest.java deleted file mode 100644 index 15439bd..0000000 --- a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpHeadersNotSharedTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.roastedroot.proxywasm.jaxrs.example; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.equalTo; - -import io.quarkus.test.junit.QuarkusTest; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class HttpHeadersNotSharedTest { - - @Test - public void testRequest() { - - // since the plugin is not shared, the counter should not increment since each request gets - // a new plugin instance. - given().when() - .get("/test/notSharedHttpHeaders") - .then() - .statusCode(200) - .body(equalTo("hello world")) - .header("x-proxy-wasm-counter", "1"); - - given().when() - .get("/test/notSharedHttpHeaders") - .then() - .statusCode(200) - .body(equalTo("hello world")) - .header("x-proxy-wasm-counter", "1"); - } -} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpHeadersTest.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpHeadersTest.java deleted file mode 100644 index 7d56a90..0000000 --- a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/HttpHeadersTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.roastedroot.proxywasm.jaxrs.example; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.equalTo; - -import io.quarkus.test.junit.QuarkusTest; -import org.junit.jupiter.api.Test; - -@QuarkusTest -public class HttpHeadersTest { - - @Test - public void testRequest() { - given().when() - .get("/test/httpHeaders") - .then() - .statusCode(200) - .header("x-proxy-wasm-go-sdk-example", "http_headers") - .header("x-wasm-header", "foo") - .header("x-proxy-wasm-counter", "1") - .body(equalTo("hello world")); - - given().when() - .get("/test/httpHeaders") - .then() - .statusCode(200) - .header("x-proxy-wasm-go-sdk-example", "http_headers") - .header("x-wasm-header", "foo") - .header("x-proxy-wasm-counter", "2") - .body(equalTo("hello world")); - } -} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/IsTrueMatcher.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/IsTrueMatcher.java new file mode 100644 index 0000000..3792521 --- /dev/null +++ b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/IsTrueMatcher.java @@ -0,0 +1,27 @@ +package io.roastedroot.proxywasm.jaxrs.example; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +public class IsTrueMatcher extends TypeSafeMatcher { + + public interface Predicate { + boolean matchesSafely(T value); + } + + Predicate predicate; + + public IsTrueMatcher(Predicate predicate) { + this.predicate = predicate; + } + + @Override + protected boolean matchesSafely(T item) { + return predicate.matchesSafely(item); + } + + @Override + public void describeTo(Description description) { + description.appendText("is not true"); + } +} diff --git a/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/TickTest.java b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/TickTest.java new file mode 100644 index 0000000..950bd1d --- /dev/null +++ b/proxy-wasm-jaxrs-quarkus-example/src/test/java/io/roastedroot/proxywasm/jaxrs/example/TickTest.java @@ -0,0 +1,47 @@ +package io.roastedroot.proxywasm.jaxrs.example; + +import static io.restassured.RestAssured.given; +import static io.roastedroot.proxywasm.jaxrs.example.Helpers.isTrue; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class TickTest { + + @Test + public void tick() throws InterruptedException { + + // plugin should not have received any ticks. + given().when().get("/tickTests/get").then().statusCode(200).body(equalTo("0")); + + // ask the plugin to enable tick events.. + given().when().get("/tickTests/enable").then().statusCode(200).body(equalTo("ok")); + + // wait a little to allow the plugin to receive some ticks. (every 100 ms) + Thread.sleep(300); + + // stop getting ticks. + given().when().get("/tickTests/disable").then().statusCode(200).body(equalTo("ok")); + + var counter = new String[] {"0"}; + + // plugin should have received at least 1 tick. + given().when() + .get("/tickTests/get") + .then() + .statusCode(200) + .body( + isTrue( + (String x) -> { + counter[0] = x; + return Integer.parseInt(x) >= 1; + })); + + // since ticks were disabled the tick counter should not have changed. + Thread.sleep(300); + + given().when().get("/tickTests/get").then().statusCode(200).body(equalTo(counter[0])); + } +} diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/HttpHandler.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/HttpHandler.java index 69542b1..9586b7a 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/HttpHandler.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/HttpHandler.java @@ -11,7 +11,6 @@ import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_SHA256_PEER_CERTIFICATE_DIGEST; import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_SUBJECT_LOCAL_CERTIFICATE; import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_SUBJECT_PEER_CERTIFICATE; -import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_TLS_VERSION; import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_URI_SAN_LOCAL_CERTIFICATE; import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_URI_SAN_PEER_CERTIFICATE; import static io.roastedroot.proxywasm.WellKnownProperties.DESTINATION_ADDRESS; @@ -50,6 +49,7 @@ import io.roastedroot.proxywasm.StreamType; import io.roastedroot.proxywasm.WasmException; import io.roastedroot.proxywasm.WasmResult; +import io.roastedroot.proxywasm.WellKnownProperties; import io.roastedroot.proxywasm.jaxrs.spi.HttpServerRequest; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; @@ -60,6 +60,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.concurrent.CountDownLatch; class HttpHandler extends ChainedHandler { @@ -259,6 +260,10 @@ public WasmResult sendHttpResponse( responseBody, additionalHeaders, grpcStatus); + + if (resumeLatch != null) { + resumeLatch.countDown(); + } return WasmResult.OK; } @@ -273,10 +278,14 @@ public HttpResponse consumeSentHttpResponse() { } private Action action; + private CountDownLatch resumeLatch; @Override public WasmResult setAction(StreamType streamType, Action action) { this.action = action; + if (this.action == Action.CONTINUE && resumeLatch != null) { + resumeLatch.countDown(); + } return WasmResult.OK; } @@ -284,6 +293,29 @@ public Action getAction() { return action; } + void maybePause(WasmPlugin plugin) { + // don't pause if plugin wants us to continue + if (action == Action.CONTINUE) { + return; + } + // don't pause if the plugin wants to respond to the request + if (senthttpResponse != null) { + return; + } + + // ok, lets pause the request processing. A tick or http call response event will + // need to resume the processing + resumeLatch = new CountDownLatch(1); + plugin.unlock(); + try { + resumeLatch.await(); + } catch (InterruptedException ignore) { + } finally { + plugin.lock(); + resumeLatch = null; + } + } + // ////////////////////////////////////////////////////////////////////// // Properties // ////////////////////////////////////////////////////////////////////// @@ -310,7 +342,7 @@ public byte[] getProperty(List path) throws WasmException { } // TODO: get TLS connection properties - else if (CONNECTION_TLS_VERSION.equals(path)) { + else if (WellKnownProperties.CONNECTION_TLS_VERSION.equals(path)) { // TODO: return null; } else if (CONNECTION_REQUESTED_SERVER_NAME.equals(path)) { diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsProxyMap.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsProxyMap.java index 7f6295b..c8db392 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsProxyMap.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsProxyMap.java @@ -66,7 +66,7 @@ public Iterable> entries() { @Override public String get(String key) { - return entries.get(key).stream().findFirst().map(JaxrsProxyMap::asString).orElse(null); + return asString(entries.getFirst(key)); } private static String asString(Object x) { diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java index 6c548f9..e56a6f8 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java @@ -11,18 +11,13 @@ import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.ReaderInterceptor; -import jakarta.ws.rs.ext.ReaderInterceptorContext; import jakarta.ws.rs.ext.WriterInterceptor; import jakarta.ws.rs.ext.WriterInterceptorContext; import java.io.ByteArrayOutputStream; import java.io.IOException; public class ProxyWasmFilter - implements ContainerRequestFilter, - ReaderInterceptor, - WriterInterceptor, - ContainerResponseFilter { + implements ContainerRequestFilter, WriterInterceptor, ContainerResponseFilter { private static final String FILTER_CONTEXT_PROPERTY_NAME = "WasmHttpFilterContext"; private final WasmPluginPool pluginPool; @@ -67,53 +62,35 @@ public void filter(ContainerRequestContext requestContext) throws IOException { plugin.lock(); try { - var ctx = new WasmHttpFilterContext(plugin, this.httpServer.get()); - requestContext.setProperty(FILTER_CONTEXT_PROPERTY_NAME, ctx); + var wasmHttpFilterContext = new WasmHttpFilterContext(plugin, this.httpServer.get()); + requestContext.setProperty(FILTER_CONTEXT_PROPERTY_NAME, wasmHttpFilterContext); // the plugin may not be interested in the request headers. - if (ctx.httpContext.hasOnRequestHeaders()) { + if (wasmHttpFilterContext.httpContext.hasOnRequestHeaders()) { - ctx.httpHandler.setRequestContext(requestContext); - var action = ctx.httpContext.callOnRequestHeaders(false); + wasmHttpFilterContext.httpHandler.setRequestContext(requestContext); + var action = wasmHttpFilterContext.httpContext.callOnRequestHeaders(false); if (action == Action.CONTINUE) { // continue means plugin is done reading the headers. - ctx.httpHandler.setRequestContext(null); + wasmHttpFilterContext.httpHandler.setRequestContext(null); + } else { + wasmHttpFilterContext.httpHandler.maybePause(wasmHttpFilterContext.plugin); } // does the plugin want to respond early? - HttpHandler.HttpResponse sendResponse = ctx.httpHandler.consumeSentHttpResponse(); + HttpHandler.HttpResponse sendResponse = + wasmHttpFilterContext.httpHandler.consumeSentHttpResponse(); if (sendResponse != null) { requestContext.abortWith(sendResponse.toResponse()); } } - } finally { - plugin.unlock(); // allow another request to use the plugin. - } - } - - private static Response interalServerError() { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } - - @Override - public Object aroundReadFrom(ReaderInterceptorContext ctx) - throws IOException, WebApplicationException { - - var wasmHttpFilterContext = - (WasmHttpFilterContext) ctx.getProperty(FILTER_CONTEXT_PROPERTY_NAME); - if (wasmHttpFilterContext == null) { - throw new WebApplicationException(interalServerError()); - } - // the plugin may not be interested in the request body. - if (wasmHttpFilterContext.httpContext.hasOnRequestBody()) { - // TODO: find out if it's more efficient to read the body in chunks and do multiple - // callOnRequestBody calls. - byte[] bytes = ctx.getInputStream().readAllBytes(); + // the plugin may not be interested in the request body. + if (wasmHttpFilterContext.httpContext.hasOnRequestBody()) { - try { - // we are about to call into the plugin which may mutate the plugin state.. - wasmHttpFilterContext.plugin.lock(); + // TODO: find out if it's more efficient to read the body in chunks and do multiple + // callOnRequestBody calls. + byte[] bytes = requestContext.getEntityStream().readAllBytes(); wasmHttpFilterContext.httpHandler.setHttpRequestBody(bytes); var action = wasmHttpFilterContext.httpContext.callOnRequestBody(true); @@ -121,6 +98,8 @@ public Object aroundReadFrom(ReaderInterceptorContext ctx) if (action == Action.CONTINUE) { // continue means plugin is done reading the body. wasmHttpFilterContext.httpHandler.setHttpRequestBody(null); + } else { + wasmHttpFilterContext.httpHandler.maybePause(wasmHttpFilterContext.plugin); } // TODO: find out more details about what to do here in a PAUSE condition. @@ -129,19 +108,22 @@ public Object aroundReadFrom(ReaderInterceptorContext ctx) // does the plugin want to respond early? HttpHandler.HttpResponse sendResponse = - wasmHttpFilterContext.httpHandler.getSentHttpResponse(); + wasmHttpFilterContext.httpHandler.consumeSentHttpResponse(); if (sendResponse != null) { throw new WebApplicationException(sendResponse.toResponse()); } - } finally { - // allow other request to use the plugin. - wasmHttpFilterContext.plugin.unlock(); + + // plugin may have modified the body + requestContext.setEntityStream(new java.io.ByteArrayInputStream(bytes)); } - // plugin may have modified the body - ctx.setInputStream(new java.io.ByteArrayInputStream(bytes)); + } finally { + plugin.unlock(); // allow another request to use the plugin. } - return ctx.proceed(); + } + + private static Response interalServerError() { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } @Override @@ -164,13 +146,18 @@ public void filter( if (action == Action.CONTINUE) { // continue means plugin is done reading the headers. wasmHttpFilterContext.httpHandler.setResponseContext(null); + } else { + wasmHttpFilterContext.httpHandler.maybePause(wasmHttpFilterContext.plugin); } // does the plugin want to respond early? HttpHandler.HttpResponse sendResponse = - wasmHttpFilterContext.httpHandler.getSentHttpResponse(); + wasmHttpFilterContext.httpHandler.consumeSentHttpResponse(); if (sendResponse != null) { - requestContext.abortWith(sendResponse.toResponse()); + Response response = sendResponse.toResponse(); + responseContext.setStatus(response.getStatus()); + responseContext.getHeaders().putAll(response.getHeaders()); + responseContext.setEntity(response.getEntity()); } } finally { // allow other request to use the plugin. @@ -221,11 +208,14 @@ public void close() throws IOException { if (action == Action.CONTINUE) { // continue means plugin is done reading the body. wasmHttpFilterContext.httpHandler.setHttpResponseBody(null); + } else { + wasmHttpFilterContext.httpHandler.maybePause( + wasmHttpFilterContext.plugin); } // does the plugin want to respond early? HttpHandler.HttpResponse sendResponse = - wasmHttpFilterContext.httpHandler.getSentHttpResponse(); + wasmHttpFilterContext.httpHandler.consumeSentHttpResponse(); if (sendResponse != null) { throw new WebApplicationException(sendResponse.toResponse()); }