diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd5663c..51f3a66 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,6 +15,15 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] version: [11, 17, 21] + exclude: + - os: windows-latest + version: 21 + - os: windows-latest + version: 17 + - os: macos-latest + version: 21 + - os: macos-latest + version: 17 steps: - name: Checkout sources uses: actions/checkout@v4 diff --git a/src/main/java/io/roastedroot/proxywasm/impl/Exports.java b/src/main/java/io/roastedroot/proxywasm/impl/Exports.java index fb78e96..bb4c9c3 100644 --- a/src/main/java/io/roastedroot/proxywasm/impl/Exports.java +++ b/src/main/java/io/roastedroot/proxywasm/impl/Exports.java @@ -68,8 +68,10 @@ public int proxyOnNewConnection(int arg0) { return (int) result; } - public int proxyOnDownstreamData(int arg0, int arg1, int arg2) { - long result = exports.function("proxy_on_downstream_data").apply(arg0, arg1, arg2)[0]; + public int proxyOnDownstreamData(int contextId, int dataSize, int endOfStream) { + long result = + exports.function("proxy_on_downstream_data") + .apply(contextId, dataSize, endOfStream)[0]; return (int) result; } diff --git a/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java b/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java index a1ade0d..0a09a18 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java @@ -5,7 +5,6 @@ public class HttpContext extends Context { private final Handler handler; - Action action; HttpContext(ProxyWasm proxyWasm, Handler handler) { super(proxyWasm); diff --git a/src/main/java/io/roastedroot/proxywasm/v1/MapType.java b/src/main/java/io/roastedroot/proxywasm/v1/MapType.java index 66d7319..ec6f0f3 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/MapType.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/MapType.java @@ -2,7 +2,6 @@ /** * Represents the type of map in proxy WASM. - * Converted from Go's MapType type. */ public enum MapType { HTTP_REQUEST_HEADERS(0), diff --git a/src/main/java/io/roastedroot/proxywasm/v1/NetworkContext.java b/src/main/java/io/roastedroot/proxywasm/v1/NetworkContext.java new file mode 100644 index 0000000..435e1c7 --- /dev/null +++ b/src/main/java/io/roastedroot/proxywasm/v1/NetworkContext.java @@ -0,0 +1,49 @@ +package io.roastedroot.proxywasm.v1; + +import static io.roastedroot.proxywasm.v1.Helpers.len; + +public class NetworkContext extends Context { + + private final Handler handler; + + NetworkContext(ProxyWasm proxyWasm, Handler handler) { + super(proxyWasm); + this.handler = handler; + } + + Handler handler() { + return handler; + } + + public Action callOnNewConnection() { + int result = proxyWasm.exports().proxyOnNewConnection(id); + Action action = Action.fromInt(result); + handler.setAction(StreamType.DOWNSTREAM, action); + return action; + } + + public Action callOnDownstreamData(boolean endOfStream) { + var data = handler.getDownStreamData(); + var result = proxyWasm.exports().proxyOnDownstreamData(id, len(data), endOfStream ? 1 : 0); + Action action = Action.fromInt(result); + handler.setAction(StreamType.DOWNSTREAM, action); + return action; + } + + public Action callOnUpstreamData(boolean endOfStream) { + var data = handler.getUpstreamData(); + var result = proxyWasm.exports().proxyOnUpstreamData(id, len(data), endOfStream ? 1 : 0); + Action action = Action.fromInt(result); + handler.setAction(StreamType.UPSTREAM, action); + return action; + } + + public void callOnDownstreamConnectionClose(PeerType type) { + proxyWasm.exports().proxyOnDownstreamConnectionClose(id, type.getValue()); + } + + public void callOnDownstreamConnectionClose() { + // peerType will be removed in the next ABI + callOnDownstreamConnectionClose(PeerType.LOCAL); + } +} diff --git a/src/main/java/io/roastedroot/proxywasm/v1/PeerType.java b/src/main/java/io/roastedroot/proxywasm/v1/PeerType.java new file mode 100644 index 0000000..c059516 --- /dev/null +++ b/src/main/java/io/roastedroot/proxywasm/v1/PeerType.java @@ -0,0 +1,45 @@ +package io.roastedroot.proxywasm.v1; + +/** + * Represents the type of peer in proxy WASM. + */ +public enum PeerType { + UNKNOWN(0), + LOCAL(1), + REMOTE(2); + + private final int value; + + /** + * Constructor for PeerType enum. + * + * @param value The integer value of the peer type + */ + PeerType(int value) { + this.value = value; + } + + /** + * Get the integer value of this peer type. + * + * @return The integer value + */ + public int getValue() { + return value; + } + + /** + * Convert an integer value to a PeerType. + * + * @param value The integer value to convert + * @return The corresponding PeerType or null if the value doesn't match any PeerType + */ + public static PeerType fromInt(int value) { + for (PeerType type : values()) { + if (type.value == value) { + return type; + } + } + return null; + } +} diff --git a/src/main/java/io/roastedroot/proxywasm/v1/ProxyWasm.java b/src/main/java/io/roastedroot/proxywasm/v1/ProxyWasm.java index 4a3e28f..fc4ba83 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/ProxyWasm.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/ProxyWasm.java @@ -214,6 +214,12 @@ public HttpContext createHttpContext(Handler handler) { return context; } + public NetworkContext createNetworkContext(Handler handler) { + NetworkContext context = new NetworkContext(this, handler); + registerContext(context, this.pluginContext.id()); + return context; + } + /** * Delivers a tick event to the plugin. *

diff --git a/src/test/go-examples/network/README.md b/src/test/go-examples/network/README.md new file mode 100644 index 0000000..3066e9f --- /dev/null +++ b/src/test/go-examples/network/README.md @@ -0,0 +1,5 @@ +## Attribution + +This example originally came from: +https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/network +``` diff --git a/src/test/go-examples/network/go.mod b/src/test/go-examples/network/go.mod new file mode 100644 index 0000000..270055a --- /dev/null +++ b/src/test/go-examples/network/go.mod @@ -0,0 +1,5 @@ +module github.com/proxy-wasm/proxy-wasm-go-sdk/examples/network + +go 1.24 + +require github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 diff --git a/src/test/go-examples/network/go.sum b/src/test/go-examples/network/go.sum new file mode 100644 index 0000000..3ddb896 --- /dev/null +++ b/src/test/go-examples/network/go.sum @@ -0,0 +1,10 @@ +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= +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/network/main.go b/src/test/go-examples/network/main.go new file mode 100644 index 0000000..52b8e1a --- /dev/null +++ b/src/test/go-examples/network/main.go @@ -0,0 +1,125 @@ +// 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" +) + +func main() {} +func init() { + 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{counter: proxywasm.DefineCounterMetric("proxy_wasm_go.connection_counter")} +} + +// 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 + counter proxywasm.MetricCounter +} + +// NewTcpContext implements types.PluginContext. +func (ctx *pluginContext) NewTcpContext(contextID uint32) types.TcpContext { + return &networkContext{counter: ctx.counter} +} + +// networkContext implements types.TcpContext. +type networkContext struct { + // Embed the default tcp context here, + // so that we don't need to reimplement all the methods. + types.DefaultTcpContext + counter proxywasm.MetricCounter +} + +// OnNewConnection implements types.TcpContext. +func (ctx *networkContext) OnNewConnection() types.Action { + proxywasm.LogInfo("new connection!") + return types.ActionContinue +} + +// OnDownstreamData implements types.TcpContext. +func (ctx *networkContext) OnDownstreamData(dataSize int, endOfStream bool) types.Action { + if dataSize == 0 { + return types.ActionContinue + } + + data, err := proxywasm.GetDownstreamData(0, dataSize) + if err != nil && err != types.ErrorStatusNotFound { + proxywasm.LogCriticalf("failed to get downstream data: %v", err) + return types.ActionContinue + } + + proxywasm.LogInfof(">>>>>> downstream data received >>>>>>\n%s", string(data)) + return types.ActionContinue +} + +// OnDownstreamClose implements types.TcpContext. +func (ctx *networkContext) OnDownstreamClose(types.PeerType) { + proxywasm.LogInfo("downstream connection close!") +} + +// OnUpstreamData implements types.TcpContext. +func (ctx *networkContext) OnUpstreamData(dataSize int, endOfStream bool) types.Action { + if dataSize == 0 { + return types.ActionContinue + } + + // Get the remote ip address of the upstream cluster. + address, err := proxywasm.GetProperty([]string{"upstream", "address"}) + if err != nil { + proxywasm.LogWarnf("failed to get upstream remote address: %v", err) + } + + proxywasm.LogInfof("remote address: %s", string(address)) + + // Get the upstream cluster's metadata in the cluster configuration. + metadataKeyValues, err := proxywasm.GetPropertyMap([]string{"cluster_metadata", "filter_metadata", "location"}) + if err != nil { + proxywasm.LogWarnf("failed to get upstream location metadata: %v", err) + } + + for _, metadata := range metadataKeyValues { + key, value := metadata[0], metadata[1] + proxywasm.LogInfof("upstream cluster metadata location[%s]=%s", string(key), string(value)) + } + + data, err := proxywasm.GetUpstreamData(0, dataSize) + if err != nil && err != types.ErrorStatusNotFound { + proxywasm.LogCritical(err.Error()) + } + + proxywasm.LogInfof("<<<<<< upstream data received <<<<<<\n%s", string(data)) + return types.ActionContinue +} + +// OnStreamDone implements types.TcpContext. +func (ctx *networkContext) OnStreamDone() { + ctx.counter.Increment(1) + proxywasm.LogInfo("connection complete!") +} diff --git a/src/test/go-examples/network/main.wasm b/src/test/go-examples/network/main.wasm new file mode 100644 index 0000000..1ca708e Binary files /dev/null and b/src/test/go-examples/network/main.wasm differ diff --git a/src/test/java/io/roastedroot/proxywasm/NetworkTest.java b/src/test/java/io/roastedroot/proxywasm/NetworkTest.java new file mode 100644 index 0000000..6c34e4e --- /dev/null +++ b/src/test/java/io/roastedroot/proxywasm/NetworkTest.java @@ -0,0 +1,108 @@ +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.MetricType; +import io.roastedroot.proxywasm.v1.NetworkContext; +import io.roastedroot.proxywasm.v1.ProxyWasm; +import io.roastedroot.proxywasm.v1.StartException; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Java port of https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/network/main_test.go + */ +public class NetworkTest { + private final MockHandler handler = new MockHandler(); + private ProxyWasm host; + private NetworkContext context; + + @BeforeEach + void setUp() throws StartException { + var module = Parser.parse(Path.of("./src/test/go-examples/network/main.wasm")); + this.host = ProxyWasm.builder().withPluginHandler(handler).build(module); + this.context = host.createNetworkContext(handler); + } + + @AfterEach + void tearDown() { + context.close(); + host.close(); + } + + @Test + public void testNetworkOnNewConnection() throws StartException { + Action action = context.callOnNewConnection(); + + // Verify the action is CONTINUE + assertEquals(Action.CONTINUE, action, "Expected CONTINUE action for new connection"); + + // Check logs for expected message + handler.assertLogsContain("new connection!"); + } + + @Test + public void testNetworkOnDownstreamClose() throws StartException { + Action action = context.callOnNewConnection(); + assertEquals(Action.CONTINUE, action, "Expected CONTINUE action for new connection"); + + // Call onDownstreamClose + context.callOnDownstreamConnectionClose(); + + // Check logs for expected message + handler.assertLogsContain("downstream connection close!"); + } + + @Test + public void testNetworkOnDownstreamData() throws StartException { + Action action = context.callOnNewConnection(); + assertEquals(Action.CONTINUE, action, "Expected CONTINUE action for new connection"); + + // Call onDownstreamData with test message + String msg = "this is downstream data"; + byte[] data = bytes(msg); + handler.setDownStreamData(data); + context.callOnDownstreamData(false); // false = not end of stream + + // Check logs for expected message + handler.assertLogsContain(">>>>>> downstream data received >>>>>>\n" + msg); + } + + @Test + public void testNetworkOnUpstreamData() throws StartException { + Action action = context.callOnNewConnection(); + assertEquals(Action.CONTINUE, action, "Expected CONTINUE action for new connection"); + + // Call onUpstreamData with test message + String msg = "this is upstream data"; + byte[] data = bytes(msg); + handler.setUpstreamData(data); + context.callOnUpstreamData(false); // false = not end of stream + + // Check logs for expected message + handler.assertLogsContain("<<<<<< upstream data received <<<<<<\n" + msg); + } + + @Test + public void testNetworkCounter() throws StartException { + Action action = context.callOnNewConnection(); + assertEquals(Action.CONTINUE, action, "Expected CONTINUE action for new connection"); + + // Complete the connection + context.close(); + + // Check logs for expected message + handler.assertLogsContain("connection complete!"); + + // Check counter metric + String metricName = "proxy_wasm_go.connection_counter"; + MockHandler.Metric metric = handler.getMetric(metricName); + assertEquals(MetricType.COUNTER, metric.type, "Expected metric to be a counter"); + assertEquals(1, metric.value, "Expected connection counter to be 1"); + } +}