diff --git a/src/test/go-examples/json_validation/README.md b/src/test/go-examples/json_validation/README.md new file mode 100644 index 0000000..94c0412 --- /dev/null +++ b/src/test/go-examples/json_validation/README.md @@ -0,0 +1,5 @@ +## Attribution + +This example originally came from: +https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/json_validation +``` diff --git a/src/test/go-examples/json_validation/go.mod b/src/test/go-examples/json_validation/go.mod new file mode 100644 index 0000000..d2bb7a7 --- /dev/null +++ b/src/test/go-examples/json_validation/go.mod @@ -0,0 +1,13 @@ +module github.com/proxy-wasm/proxy-wasm-go-sdk/examples/json_validation + +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/src/test/go-examples/json_validation/go.sum b/src/test/go-examples/json_validation/go.sum new file mode 100644 index 0000000..bdbfc34 --- /dev/null +++ b/src/test/go-examples/json_validation/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/src/test/go-examples/json_validation/main.go b/src/test/go-examples/json_validation/main.go new file mode 100644 index 0000000..37445e4 --- /dev/null +++ b/src/test/go-examples/json_validation/main.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" + + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" +) + +func main() {} +func init() { + // SetVMContext is the entrypoint for setting up this entire Wasm VM. + // Please make sure that this entrypoint be called during "main()" function, otherwise + // this VM would fail. + proxywasm.SetVMContext(&vmContext{}) +} + +// vmContext implements types.VMContext. +type vmContext struct { + // Embed the default VM context here, + // so that we don't need to reimplement all the methods. + types.DefaultVMContext +} + +// NewPluginContext implements types.VMContext. +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{} +} + +// pluginContext implements types.PluginContext. +type pluginContext struct { + // Embed the default plugin context here, + // so that we don't need to reimplement all the methods. + types.DefaultPluginContext + configuration pluginConfiguration +} + +// pluginConfiguration is a type to represent an example configuration for this wasm plugin. +type pluginConfiguration struct { + // Example configuration field. + // The plugin will validate if those fields exist in the json payload. + requiredKeys []string +} + +// OnPluginStart implements types.PluginContext. +func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { + data, err := proxywasm.GetPluginConfiguration() + if err != nil && err != types.ErrorStatusNotFound { + proxywasm.LogCriticalf("error reading plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + config, err := parsePluginConfiguration(data) + if err != nil { + proxywasm.LogCriticalf("error parsing plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + ctx.configuration = config + return types.OnPluginStartStatusOK +} + +// parsePluginConfiguration parses the json plugin configuration data and returns pluginConfiguration. +// Note that this parses the json data by gjson, since TinyGo doesn't support encoding/json. +// You can also try https://github.com/mailru/easyjson, which supports decoding to a struct. +func parsePluginConfiguration(data []byte) (pluginConfiguration, error) { + if len(data) == 0 { + return pluginConfiguration{}, nil + } + + config := &pluginConfiguration{} + if !gjson.ValidBytes(data) { + return pluginConfiguration{}, fmt.Errorf("the plugin configuration is not a valid json: %q", string(data)) + } + + jsonData := gjson.ParseBytes(data) + requiredKeys := jsonData.Get("requiredKeys").Array() + for _, requiredKey := range requiredKeys { + config.requiredKeys = append(config.requiredKeys, requiredKey.Str) + } + + return *config, nil +} + +// NewHttpContext implements types.PluginContext. +func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &payloadValidationContext{requiredKeys: ctx.configuration.requiredKeys} +} + +// payloadValidationContext implements types.HttpContext. +type payloadValidationContext struct { + // Embed the default root http context here, + // so that we don't need to reimplement all the methods. + types.DefaultHttpContext + totalRequestBodySize int + requiredKeys []string +} + +// OnHttpRequestHeaders implements types.HttpContext. +func (*payloadValidationContext) OnHttpRequestHeaders(numHeaders int, _ bool) types.Action { + contentType, err := proxywasm.GetHttpRequestHeader("content-type") + if err != nil || contentType != "application/json" { + // If the header doesn't have the expected content value, send the 403 response, + if err := proxywasm.SendHttpResponse(403, nil, []byte("content-type must be provided"), -1); err != nil { + panic(err) + } + // and terminates the further processing of this traffic by ActionPause. + return types.ActionPause + } + + // ActionContinue lets the host continue the processing the body. + return types.ActionContinue +} + +// OnHttpRequestBody implements types.HttpContext. +func (ctx *payloadValidationContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action { + ctx.totalRequestBodySize += bodySize + if !endOfStream { + // OnHttpRequestBody may be called each time a part of the body is received. + // Wait until we see the entire body to replace. + return types.ActionPause + } + + body, err := proxywasm.GetHttpRequestBody(0, ctx.totalRequestBodySize) + if err != nil { + proxywasm.LogErrorf("failed to get request body: %v", err) + return types.ActionContinue + } + if !ctx.validatePayload(body) { + // If the validation fails, send the 403 response, + if err := proxywasm.SendHttpResponse(403, nil, []byte("invalid payload"), -1); err != nil { + proxywasm.LogErrorf("failed to send the 403 response: %v", err) + } + // and terminates this traffic. + return types.ActionPause + } + + return types.ActionContinue +} + +// validatePayload validates the given json payload. +// Note that this function parses the json data by gjson, since TinyGo doesn't support encoding/json. +func (ctx *payloadValidationContext) validatePayload(body []byte) bool { + if !gjson.ValidBytes(body) { + proxywasm.LogErrorf("body is not a valid json: %q", string(body)) + return false + } + jsonData := gjson.ParseBytes(body) + + // Do any validation on the json. Check if required keys exist here as an example. + // The required keys are configurable via the plugin configuration. + for _, requiredKey := range ctx.requiredKeys { + if !jsonData.Get(requiredKey).Exists() { + proxywasm.LogErrorf("required key (%v) is missing: %v", requiredKey, jsonData) + return false + } + } + + return true +} diff --git a/src/test/go-examples/json_validation/main.wasm b/src/test/go-examples/json_validation/main.wasm new file mode 100644 index 0000000..733d69b Binary files /dev/null and b/src/test/go-examples/json_validation/main.wasm differ diff --git a/src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java b/src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java new file mode 100644 index 0000000..f6c8d89 --- /dev/null +++ b/src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java @@ -0,0 +1,112 @@ +package io.roastedroot.proxywasm; + +import static io.roastedroot.proxywasm.v1.Helpers.bytes; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.dylibso.chicory.wasm.Parser; +import io.roastedroot.proxywasm.v1.Action; +import io.roastedroot.proxywasm.v1.HttpContext; +import io.roastedroot.proxywasm.v1.ProxyWasm; +import io.roastedroot.proxywasm.v1.StartException; +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Java port of https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/json_validation/main_test.go + */ +public class JsonValidationTest { + private final MockHandler handler = new MockHandler(); + + @Nested + class OnHttpRequestHeaders { + private ProxyWasm host; + private HttpContext context; + + @BeforeEach + void setUp() throws StartException { + var module = Parser.parse(Path.of("./src/test/go-examples/json_validation/main.wasm")); + this.host = ProxyWasm.builder().build(module); + this.context = host.createHttpContext(handler); + } + + @AfterEach + void tearDown() { + context.close(); + host.close(); + } + + @Test + void testFailsDueToUnsupportedContentType() { + var contentType = "application/json"; + var expectedAction = Action.CONTINUE; + + handler.setHttpRequestHeaders(Map.of("content-type", contentType)); + var action = context.callOnRequestHeaders(false); + assertEquals(expectedAction, action); + } + + @Test + void successForJson() { + var contentType = "application/json"; + var expectedAction = Action.CONTINUE; + + handler.setHttpRequestHeaders(Map.of("content-type", contentType)); + var action = context.callOnRequestHeaders(false); + assertEquals(expectedAction, action); + } + } + + @Nested + class OnHttpRequestBody { + private ProxyWasm host; + private HttpContext context; + + @BeforeEach + void setUp() throws StartException { + var config = "{\"requiredKeys\": [\"my_key\"]}"; + var module = Parser.parse(Path.of("./src/test/go-examples/json_validation/main.wasm")); + this.host = ProxyWasm.builder().withPluginConfig(config).build(module); + this.context = host.createHttpContext(handler); + } + + @AfterEach + void tearDown() { + context.close(); + host.close(); + } + + @Test + public void pausesDueToInvalidPayload() { + String body = "invalid_payload"; + Action expectedAction = Action.PAUSE; + + handler.setHttpRequestBody(bytes(body)); + var action = context.callOnRequestBody(true); + assertEquals(expectedAction, action); + } + + @Test + public void pausesDueToUnknownKeys() { + String body = "{\"unknown_key\":\"unknown_value\"}"; + Action expectedAction = Action.PAUSE; + + handler.setHttpRequestBody(bytes(body)); + var action = context.callOnRequestBody(true); + assertEquals(expectedAction, action); + } + + @Test + public void success() { + String body = "{\"my_key\":\"my_value\"}"; + Action expectedAction = Action.CONTINUE; + + handler.setHttpRequestBody(bytes(body)); + var action = context.callOnRequestBody(true); + assertEquals(expectedAction, action); + } + } +}