Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/test/go-examples/json_validation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Attribution

This example originally came from:
https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/json_validation
```
13 changes: 13 additions & 0 deletions src/test/go-examples/json_validation/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
16 changes: 16 additions & 0 deletions src/test/go-examples/json_validation/go.sum
Original file line number Diff line number Diff line change
@@ -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=
159 changes: 159 additions & 0 deletions src/test/go-examples/json_validation/main.go
Original file line number Diff line number Diff line change
@@ -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
}
Binary file added src/test/go-examples/json_validation/main.wasm
Binary file not shown.
112 changes: 112 additions & 0 deletions src/test/java/io/roastedroot/proxywasm/JsonValidationTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}