diff --git a/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java b/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java index 25886a7..eeccd24 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/HttpContext.java @@ -5,6 +5,7 @@ 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/ProxyWasm.java b/src/main/java/io/roastedroot/proxywasm/v1/ProxyWasm.java index 16c3db7..5b35e6c 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/ProxyWasm.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/ProxyWasm.java @@ -257,7 +257,7 @@ public static ProxyWasm.Builder builder() { return new ProxyWasm.Builder(); } - public void callOnHttpCallResponse( + public void sendHttpCallResponse( int calloutID, Map headers, Map trailers, byte[] body) { this.httpCallResponseHeaders = headers; diff --git a/src/test/go-examples/foreign_call_on_tick/main.wasm b/src/test/go-examples/foreign_call_on_tick/main.wasm index 5f45bbc..78f1524 100644 Binary files a/src/test/go-examples/foreign_call_on_tick/main.wasm and b/src/test/go-examples/foreign_call_on_tick/main.wasm differ diff --git a/src/test/go-examples/http_auth_random/README.md b/src/test/go-examples/http_auth_random/README.md new file mode 100644 index 0000000..53ae46f --- /dev/null +++ b/src/test/go-examples/http_auth_random/README.md @@ -0,0 +1,4 @@ +## Attribution + +This example originally came from: +https://github.com/proxy-wasm/proxy-wasm-go-sdk/tree/main/examples/http_auth_random diff --git a/src/test/go-examples/http_auth_random/go.mod b/src/test/go-examples/http_auth_random/go.mod new file mode 100644 index 0000000..d1790fe --- /dev/null +++ b/src/test/go-examples/http_auth_random/go.mod @@ -0,0 +1,5 @@ +module github.com/proxy-wasm/proxy-wasm-go-sdk/examples/http_auth_random + +go 1.24 + +require github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 diff --git a/src/test/go-examples/http_auth_random/go.sum b/src/test/go-examples/http_auth_random/go.sum new file mode 100644 index 0000000..3ddb896 --- /dev/null +++ b/src/test/go-examples/http_auth_random/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/http_auth_random/main.go b/src/test/go-examples/http_auth_random/main.go new file mode 100644 index 0000000..94052e7 --- /dev/null +++ b/src/test/go-examples/http_auth_random/main.go @@ -0,0 +1,124 @@ +// 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 ( + "hash/fnv" + + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" +) + +const clusterName = "httpbin" + +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{} +} + +// 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 +} + +// NewHttpContext implements types.PluginContext. +func (*pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &httpAuthRandom{contextID: contextID} +} + +// httpAuthRandom implements types.HttpContext. +type httpAuthRandom struct { + // Embed the default http context here, + // so that we don't need to reimplement all the methods. + types.DefaultHttpContext + contextID uint32 +} + +// OnHttpRequestHeaders implements types.HttpContext. +func (ctx *httpAuthRandom) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + hs, err := proxywasm.GetHttpRequestHeaders() + if err != nil { + proxywasm.LogCriticalf("failed to get request headers: %v", err) + return types.ActionContinue + } + for _, h := range hs { + proxywasm.LogInfof("request header: %s: %s", h[0], h[1]) + } + + if _, err := proxywasm.DispatchHttpCall(clusterName, hs, nil, nil, + 50000, httpCallResponseCallback); err != nil { + proxywasm.LogCriticalf("dipatch httpcall failed: %v", err) + return types.ActionContinue + } + + proxywasm.LogInfof("http call dispatched to %s", clusterName) + return types.ActionPause +} + +// httpCallResponseCallback is a callback function when the http call response is received after dispatching. +func httpCallResponseCallback(numHeaders, bodySize, numTrailers int) { + hs, err := proxywasm.GetHttpCallResponseHeaders() + if err != nil { + proxywasm.LogCriticalf("failed to get response body: %v", err) + return + } + + for _, h := range hs { + proxywasm.LogInfof("response header from %s: %s: %s", clusterName, h[0], h[1]) + } + + b, err := proxywasm.GetHttpCallResponseBody(0, bodySize) + if err != nil { + proxywasm.LogCriticalf("failed to get response body: %v", err) + _ = proxywasm.ResumeHttpRequest() + return + } + + s := fnv.New32a() + if _, err := s.Write(b); err != nil { + proxywasm.LogCriticalf("failed to calculate hash: %v", err) + _ = proxywasm.ResumeHttpRequest() + return + } + + if s.Sum32()%2 == 0 { + proxywasm.LogInfo("access granted") + _ = proxywasm.ResumeHttpRequest() + return + } + + body := "access forbidden" + proxywasm.LogInfo(body) + if err := proxywasm.SendHttpResponse(403, [][2]string{ + {"powered-by", "proxy-wasm-go-sdk!!"}, + }, []byte(body), -1); err != nil { + proxywasm.LogErrorf("failed to send local response: %v", err) + _ = proxywasm.ResumeHttpRequest() + } +} diff --git a/src/test/go-examples/http_auth_random/main.wasm b/src/test/go-examples/http_auth_random/main.wasm new file mode 100644 index 0000000..b008faf Binary files /dev/null and b/src/test/go-examples/http_auth_random/main.wasm differ diff --git a/src/test/java/io/roastedroot/proxywasm/DispatchCallOnTickTest.java b/src/test/java/io/roastedroot/proxywasm/DispatchCallOnTickTest.java index 52880c0..b853e03 100644 --- a/src/test/java/io/roastedroot/proxywasm/DispatchCallOnTickTest.java +++ b/src/test/java/io/roastedroot/proxywasm/DispatchCallOnTickTest.java @@ -33,7 +33,7 @@ public void testOnTick() throws StartException { handler.getHttpCalls().entrySet().stream() .forEach( entry -> { - host.callOnHttpCallResponse( + host.sendHttpCallResponse( entry.getKey(), Map.of(), Map.of(), new byte[0]); }); diff --git a/src/test/java/io/roastedroot/proxywasm/HttpAuthRandomTest.java b/src/test/java/io/roastedroot/proxywasm/HttpAuthRandomTest.java new file mode 100644 index 0000000..b5cd835 --- /dev/null +++ b/src/test/java/io/roastedroot/proxywasm/HttpAuthRandomTest.java @@ -0,0 +1,121 @@ +package io.roastedroot.proxywasm; + +import static io.roastedroot.proxywasm.v1.Helpers.bytes; +import static io.roastedroot.proxywasm.v1.Helpers.string; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.dylibso.chicory.wasm.Parser; +import io.roastedroot.proxywasm.v1.Action; +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.Test; + +public class HttpAuthRandomTest { + + private static String clusterName = "httpbin"; + + private MockHandler handler; + private ProxyWasm host; + + @BeforeEach + void setUp() throws StartException { + this.handler = new MockHandler(); + ProxyWasm.Builder builder = ProxyWasm.builder(); + var module = Parser.parse(Path.of("./src/test/go-examples/http_auth_random/main.wasm")); + this.host = builder.build(module); + } + + @AfterEach + void tearDown() { + host.close(); + } + + @Test + public void onHttpRequestHeaders() throws StartException { + try (var context = host.createHttpContext(handler)) { + + // Call OnRequestHeaders. + handler.setHttpRequestHeaders(Map.of("key", "value")); + var action = context.callOnRequestHeaders(false); + assertEquals(Action.PAUSE, action); + + // Verify DispatchHttpCall is called. + var calls = handler.getHttpCalls(); + assertEquals(1, calls.size()); + MockHandler.HttpCall call = calls.values().stream().findFirst().get(); + assertEquals(clusterName, call.uri); + + // Check Envoy logs. + handler.assertLogsContain( + "http call dispatched to " + clusterName, "request header: key: value"); + } + } + + @Test + public void onHttpCallResponse() throws StartException { + var headers = + Map.of( + "HTTP/1.1", "200 OK", + "Date:", "Thu, 17 Sep 2020 02:47:07 GMT", + "Content-Type", "application/json", + "Content-Length", "53", + "Connection", "keep-alive", + "Server", "gunicorn/19.9.0", + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Credentials", "true"); + + // Access granted case -> Local response must not be sent. + try (var context = host.createHttpContext(handler)) { + + // Call OnRequestHeaders. + handler.setHttpRequestHeaders(Map.of()); + var action = context.callOnRequestHeaders(false); + assertEquals(Action.PAUSE, action); + + // Verify DispatchHttpCall is called. + var calls = handler.getHttpCalls(); + assertEquals(1, calls.size()); + var body = bytes("\"uuid\": \"7b10a67a-1c67-4199-835b-cbefcd4a63d4\""); + MockHandler.HttpCall call = calls.values().stream().findFirst().get(); + host.sendHttpCallResponse(call.id, headers, null, body); + calls.remove(call.id); + + // Check local response. + assertNull(handler.getSentHttpResponse()); + + // CHeck Envoy logs. + handler.assertLogsContain("access granted"); + } + + // Access denied case -> Local response must be sent. + try (var context = host.createHttpContext(handler)) { + + // Call OnRequestHeaders. + handler.setHttpRequestHeaders(Map.of()); + var action = context.callOnRequestHeaders(false); + assertEquals(Action.PAUSE, action); + + // Verify DispatchHttpCall is called. + var calls = handler.getHttpCalls(); + assertEquals(1, calls.size()); + var body = bytes("\"uuid\": \"aaaaaaaa-1c67-4199-835b-cbefcd4a63d4\""); + MockHandler.HttpCall call = calls.values().stream().findFirst().get(); + host.sendHttpCallResponse(call.id, headers, null, body); + // Check local response. + MockHandler.HttpResponse localResponse = handler.getSentHttpResponse(); + assertNotNull(localResponse); + assertEquals(403, localResponse.statusCode); + assertEquals("access forbidden", string(localResponse.body)); + assertEquals(Map.of("powered-by", "proxy-wasm-go-sdk!!"), localResponse.headers); + + // CHeck Envoy logs. + handler.assertLogsContain("access forbidden"); + } + } +} diff --git a/src/test/java/io/roastedroot/proxywasm/MockHandler.java b/src/test/java/io/roastedroot/proxywasm/MockHandler.java index e73c134..235dfb9 100644 --- a/src/test/java/io/roastedroot/proxywasm/MockHandler.java +++ b/src/test/java/io/roastedroot/proxywasm/MockHandler.java @@ -83,14 +83,17 @@ public void assertSortedLogsEqual(String... messages) { loggedMessages().stream().sorted().collect(Collectors.toList())); } - public void assertLogsContain(String message) { - assertTrue( - loggedMessages().contains(message), "logged messages does not contain: " + message); + public void assertLogsContain(String... message) { + for (String m : message) { + assertTrue(loggedMessages().contains(m), "logged messages does not contain: " + m); + } } - public void assertLogsDoNotContain(String message) { + public void assertLogsDoNotContain(String... message) { for (String log : loggedMessages()) { - assertFalse(log.contains(message), "logged messages contains: " + message); + for (String m : message) { + assertFalse(log.contains(m), "logged messages contains: " + m); + } } } @@ -271,6 +274,7 @@ public enum Type { DISPATCH } + public final int id; public final Type callType; public final String uri; public final Object headers; @@ -279,12 +283,14 @@ public enum Type { public final int timeoutMilliseconds; public HttpCall( + int id, Type callType, String uri, HashMap headers, byte[] body, HashMap trailers, int timeoutMilliseconds) { + this.id = id; this.callType = callType; this.uri = uri; this.headers = headers; @@ -312,7 +318,13 @@ public int httpCall( var id = lastCallId.incrementAndGet(); HttpCall value = new HttpCall( - HttpCall.Type.REGULAR, uri, headers, body, trailers, timeoutMilliseconds); + id, + HttpCall.Type.REGULAR, + uri, + headers, + body, + trailers, + timeoutMilliseconds); httpCalls.put(id, value); return id; } @@ -328,6 +340,7 @@ public int dispatchHttpCall( var id = lastCallId.incrementAndGet(); HttpCall value = new HttpCall( + id, HttpCall.Type.DISPATCH, upstreamName, headers,