diff --git a/Makefile b/Makefile index 2e73f8c..b4910d4 100755 --- a/Makefile +++ b/Makefile @@ -14,13 +14,10 @@ all: build-go-examples build-rust-examples .PHONY: build-go-examples build-go-examples: - @find ./src/test/go-examples -mindepth 1 -type f -name "main.go" \ - | xargs -I {} bash -c 'dirname {}' \ - | xargs -I {} bash -c 'cd {} && env GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./main.go' - + @./proxy-wasm-java-host/src/test/go-examples/build.sh .PHONY: build-rust-examples build-rust-examples: - @find ./src/test/rust-examples -mindepth 1 -type f -name "Cargo.toml" \ + @find ./proxy-wasm-java-host/src/test/rust-examples -mindepth 1 -type f -name "Cargo.toml" \ | xargs -I {} bash -c 'dirname {}' \ | xargs -I {} bash -c 'cd {} && cargo build --target wasm32-wasip1 --release; cp ./target/wasm32-wasip1/release/*.wasm ./main.wasm' 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 eeaa3e2..ca906a6 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 @@ -2,8 +2,10 @@ import com.dylibso.chicory.runtime.HostFunction; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Map; @@ -28,6 +30,37 @@ public static byte[] bytes(String value) { return value.getBytes(StandardCharsets.UTF_8); } + public static byte[] bytes(Date value) { + // encode using + // https://protobuf.dev/reference/protobuf/google.protobuf/#timestamp + var instant = value.toInstant(); + var rfc3339String = instant.toString(); + return bytes(rfc3339String); + } + + public static byte[] bytes(Duration value) { + // encode using + // https://protobuf.dev/reference/protobuf/google.protobuf/#duration + return bytes(String.format("%d.%09d", value.getSeconds(), value.getNano())); + } + + public static byte[] bytes(int value) { + // TODO: test to check byte order + return new byte[] { + (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value + }; + } + + public static int int32(byte[] bytes) { + if (bytes == null || bytes.length != 4) { + throw new IllegalArgumentException("Byte array must be exactly 4 bytes long"); + } + return ((bytes[0] & 0xFF) << 24) + | ((bytes[1] & 0xFF) << 16) + | ((bytes[2] & 0xFF) << 8) + | (bytes[3] & 0xFF); + } + public static String string(byte[] value) { return new String(value, StandardCharsets.UTF_8); } @@ -143,4 +176,6 @@ public static byte[] replaceBytes( return result; } + + static final int U32_LEN = 4; } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyMap.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyMap.java index 34e97c5..de897a4 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyMap.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyMap.java @@ -1,5 +1,12 @@ package io.roastedroot.proxywasm; +import static io.roastedroot.proxywasm.Helpers.bytes; +import static io.roastedroot.proxywasm.Helpers.len; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; import java.util.Map; public interface ProxyMap { @@ -33,4 +40,39 @@ static ProxyMap copyOf(Map headers) { String get(String key); void remove(String key); + + /** + * Encode the map into a byte array. + */ + default byte[] encode() { + try { + var baos = new ByteArrayOutputStream(); + var o = new DataOutputStream(baos); + // Write header size (number of entries) + int mapSize = this.size(); + o.writeInt(mapSize); + + // write all the key / value sizes. + ArrayList> entries = new ArrayList<>(this.size()); + for (var entry : this.entries()) { + var encoded = Map.entry(bytes(entry.getKey()), bytes(entry.getValue())); + entries.add(encoded); + o.writeInt(len(encoded.getKey())); + o.writeInt(len(encoded.getValue())); + } + + // write all the key / values + for (var entry : entries) { + o.write(entry.getKey()); + o.write(0); + o.write(entry.getValue()); + o.write(0); + } + o.close(); + return baos.toByteArray(); + } catch (IOException e) { + // this should never happen since we are not really doing IO + throw new RuntimeException(e); + } + } } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownProperties.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownProperties.java index 321f5cf..ddef5aa 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownProperties.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownProperties.java @@ -6,6 +6,7 @@ * Holds constants for the well-known properties defined by the Proxy-Wasm ABI. * * see: spec + * seeL envoy docs */ public final class WellKnownProperties { private WellKnownProperties() {} @@ -16,59 +17,92 @@ private WellKnownProperties() {} public static final List PLUGIN_VM_ID = List.of("plugin_vm_id"); // Downstream connection properties - public static final List CONNECTION_ID = List.of("connection.id"); - public static final List SOURCE_ADDRESS = List.of("source.address"); - public static final List SOURCE_PORT = List.of("source.port"); - public static final List DESTINATION_ADDRESS = List.of("destination.address"); - public static final List DESTINATION_PORT = List.of("destination.port"); - public static final List CONNECTION_TLS_VERSION = List.of("connection.tls_version"); + public static final List CONNECTION_ID = List.of("connection", "id"); + public static final List SOURCE_ADDRESS = List.of("source", "address"); + public static final List SOURCE_PORT = List.of("source", "port"); + public static final List DESTINATION_ADDRESS = List.of("destination", "address"); + public static final List DESTINATION_PORT = List.of("destination", "port"); + public static final List CONNECTION_TLS_VERSION = List.of("connection", "tls_version"); public static final List CONNECTION_REQUESTED_SERVER_NAME = - List.of("connection.requested_server_name"); - public static final List CONNECTION_MTLS = List.of("connection.mtls"); + List.of("connection", "requested_server_name"); + public static final List CONNECTION_MTLS = List.of("connection", "mtls"); public static final List CONNECTION_SUBJECT_LOCAL_CERTIFICATE = - List.of("connection.subject_local_certificate"); + List.of("connection", "subject_local_certificate"); public static final List CONNECTION_SUBJECT_PEER_CERTIFICATE = - List.of("connection.subject_peer_certificate"); + List.of("connection", "subject_peer_certificate"); public static final List CONNECTION_DNS_SAN_LOCAL_CERTIFICATE = - List.of("connection.dns_san_local_certificate"); + List.of("connection", "dns_san_local_certificate"); public static final List CONNECTION_DNS_SAN_PEER_CERTIFICATE = - List.of("connection.dns_san_peer_certificate"); + List.of("connection", "dns_san_peer_certificate"); public static final List CONNECTION_URI_SAN_LOCAL_CERTIFICATE = - List.of("connection.uri_san_local_certificate"); + List.of("connection", "uri_san_local_certificate"); public static final List CONNECTION_URI_SAN_PEER_CERTIFICATE = - List.of("connection.uri_san_peer_certificate"); + List.of("connection", "uri_san_peer_certificate"); public static final List CONNECTION_SHA256_PEER_CERTIFICATE_DIGEST = - List.of("connection.sha256_peer_certificate_digest"); + List.of("connection", "sha256_peer_certificate_digest"); // Upstream connection properties - public static final List UPSTREAM_ADDRESS = List.of("upstream.address"); - public static final List UPSTREAM_PORT = List.of("upstream.port"); - public static final List UPSTREAM_LOCAL_ADDRESS = List.of("upstream.local_address"); - public static final List UPSTREAM_LOCAL_PORT = List.of("upstream.local_port"); - public static final List UPSTREAM_TLS_VERSION = List.of("upstream.tls_version"); + public static final List UPSTREAM_ADDRESS = List.of("upstream", "address"); + public static final List UPSTREAM_PORT = List.of("upstream", "port"); + public static final List UPSTREAM_LOCAL_ADDRESS = List.of("upstream", "local_address"); + public static final List UPSTREAM_LOCAL_PORT = List.of("upstream", "local_port"); + public static final List UPSTREAM_TLS_VERSION = List.of("upstream", "tls_version"); public static final List UPSTREAM_SUBJECT_LOCAL_CERTIFICATE = - List.of("upstream.subject_local_certificate"); + List.of("upstream", "subject_local_certificate"); public static final List UPSTREAM_SUBJECT_PEER_CERTIFICATE = - List.of("upstream.subject_peer_certificate"); + List.of("upstream", "subject_peer_certificate"); public static final List UPSTREAM_DNS_SAN_LOCAL_CERTIFICATE = - List.of("upstream.dns_san_local_certificate"); + List.of("upstream", "dns_san_local_certificate"); public static final List UPSTREAM_DNS_SAN_PEER_CERTIFICATE = - List.of("upstream.dns_san_peer_certificate"); + List.of("upstream", "dns_san_peer_certificate"); public static final List UPSTREAM_URI_SAN_LOCAL_CERTIFICATE = - List.of("upstream.uri_san_local_certificate"); + List.of("upstream", "uri_san_local_certificate"); public static final List UPSTREAM_URI_SAN_PEER_CERTIFICATE = - List.of("upstream.uri_san_peer_certificate"); + List.of("upstream", "uri_san_peer_certificate"); public static final List UPSTREAM_SHA256_PEER_CERTIFICATE_DIGEST = - List.of("upstream.sha256_peer_certificate_digest"); + List.of("upstream", "sha256_peer_certificate_digest"); + public static final List UPSTREAM_TRANSPORT_FAILURE_REASON = + List.of("upstream", "transport_failure_reason"); + public static final List UPSTREAM_REQUEST_ATTEMPT_COUNT = + List.of("upstream", "request_attempt_count"); + public static final List UPSTREAM_CX_POOL_READY_DURATION = + List.of("upstream", "cx_pool_ready_duration"); + + // Metadata and filter state properties + public static final List METADATA = List.of("metadata"); + public static final List FILTER_STATE = List.of("filter_state"); + public static final List UPSTREAM_FILTER_STATE = List.of("upstream_filter_state"); // HTTP request properties - public static final List REQUEST_PROTOCOL = List.of("request.protocol"); - public static final List REQUEST_TIME = List.of("request.time"); - public static final List REQUEST_DURATION = List.of("request.duration"); - public static final List REQUEST_SIZE = List.of("request.size"); - public static final List REQUEST_TOTAL_SIZE = List.of("request.total_size"); + public static final List REQUEST_PROTOCOL = List.of("request", "protocol"); + public static final List REQUEST_TIME = List.of("request", "time"); + + public static final List REQUEST_PATH = List.of("request", "path"); + public static final List REQUEST_URL_PATH = List.of("request", "url_path"); + public static final List REQUEST_HOST = List.of("request", "host"); + public static final List REQUEST_SCHEME = List.of("request", "scheme"); + public static final List REQUEST_METHOD = List.of("request", "method"); + public static final List REQUEST_HEADERS = List.of("request", "headers"); + public static final List REQUEST_REFERER = List.of("request", "referer"); + public static final List REQUEST_USERAGENT = List.of("request", "useragent"); + public static final List REQUEST_ID = List.of("request", "id"); + public static final List REQUEST_QUERY = List.of("request", "query"); + + // These properties are available once the request completes: + public static final List REQUEST_DURATION = List.of("request", "duration"); + public static final List REQUEST_SIZE = List.of("request", "size"); + public static final List REQUEST_TOTAL_SIZE = List.of("request", "total_size"); // HTTP response properties - public static final List RESPONSE_SIZE = List.of("response.size"); - public static final List RESPONSE_TOTAL_SIZE = List.of("response.total_size"); + public static final List RESPONSE_SIZE = List.of("response", "size"); + public static final List RESPONSE_TOTAL_SIZE = List.of("response", "total_size"); + + public static final List RESPONSE_CODE = List.of("response", "code"); + public static final List RESPONSE_CODE_DETAILS = List.of("response", "code_details"); + public static final List RESPONSE_FLAGS = List.of("response", "flags"); + public static final List RESPONSE_GRPC_STATUS = List.of("response", "grpc_status"); + public static final List RESPONSE_HEADERS = List.of("response", "headers"); + public static final List RESPONSE_TRAILERS = List.of("response", "trailers"); + public static final List RESPONSE_BACKEND_LATENCY = + List.of("response", "backend_latency"); } diff --git a/proxy-wasm-java-host/src/test/go-examples/build.sh b/proxy-wasm-java-host/src/test/go-examples/build.sh new file mode 100755 index 0000000..925a531 --- /dev/null +++ b/proxy-wasm-java-host/src/test/go-examples/build.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +docker run -it --rm \ + -v `pwd`:/src \ + --workdir /src \ + -e GOOS=wasip1 \ + -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' + " \ 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 e661312..24a1808 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 78f1524..95291dc 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 826e2fb..5fcd361 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 b008faf..c6ab09b 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 3b0be56..10f3c90 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 c965d6e..61edaa8 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.go b/proxy-wasm-java-host/src/test/go-examples/http_headers/main.go index 2db4cff..238739b 100644 --- a/proxy-wasm-java-host/src/test/go-examples/http_headers/main.go +++ b/proxy-wasm-java-host/src/test/go-examples/http_headers/main.go @@ -15,6 +15,7 @@ package main import ( + "fmt" "strings" "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" @@ -49,14 +50,16 @@ type pluginContext struct { // plugin configuration during OnPluginStart. headerName string headerValue string + counter int } // NewHttpContext implements types.PluginContext. func (p *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { return &httpHeaders{ - contextID: contextID, - headerName: p.headerName, - headerValue: p.headerValue, + contextID: contextID, + pluginContext: p, + headerName: p.headerName, + headerValue: p.headerValue, } } @@ -96,9 +99,10 @@ type httpHeaders struct { // Embed the default http context here, // so that we don't need to reimplement all the methods. types.DefaultHttpContext - contextID uint32 - headerName string - headerValue string + contextID uint32 + headerName string + headerValue string + pluginContext *pluginContext } // OnHttpRequestHeaders implements types.HttpContext. @@ -128,6 +132,11 @@ func (ctx *httpHeaders) OnHttpResponseHeaders(_ int, _ bool) types.Action { proxywasm.LogCriticalf("failed to set response constant header: %v", err) } + ctx.pluginContext.counter++ + if err := proxywasm.AddHttpResponseHeader("x-proxy-wasm-counter", fmt.Sprintf("%d", ctx.pluginContext.counter)); err != nil { + proxywasm.LogCriticalf("failed to set response counter header: %v", err) + } + // Add the header passed by arguments if ctx.headerName != "" { if err := proxywasm.AddHttpResponseHeader(ctx.headerName, ctx.headerValue); err != nil { 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 7fd3a43..1198cc9 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 0509aea..7884072 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 733d69b..e81bb85 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 416f492..317cf0e 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 ed40c11..9e67af3 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 1ca708e..1f30303 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 a6fb18d..3245e0b 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 fae1ac7..40c38ac 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 d794230..52f5ced 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 7f75b6a..9648147 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 a74790b..5cf579f 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/vm_plugin_configuration/main.wasm b/proxy-wasm-java-host/src/test/go-examples/vm_plugin_configuration/main.wasm index bb43fd7..0e19805 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-java-host/src/test/java/io/roastedroot/proxywasm/examples/HttpHeadersTest.java b/proxy-wasm-java-host/src/test/java/io/roastedroot/proxywasm/examples/HttpHeadersTest.java index 6c77017..355b470 100644 --- a/proxy-wasm-java-host/src/test/java/io/roastedroot/proxywasm/examples/HttpHeadersTest.java +++ b/proxy-wasm-java-host/src/test/java/io/roastedroot/proxywasm/examples/HttpHeadersTest.java @@ -75,6 +75,7 @@ public void onHttpResponseHeaders() throws StartException { "key1", "value1", "key2", "value2", "x-proxy-wasm-go-sdk-example", "http_headers", + "x-proxy-wasm-counter", "1", "x-wasm-header", "x-value"), handler.getHttpResponseHeaders()); @@ -85,6 +86,7 @@ public void onHttpResponseHeaders() throws StartException { "response header <-- key1: value1", "adding header: x-wasm-header=x-value", "response header <-- x-wasm-header: x-value", + "response header <-- x-proxy-wasm-counter: 1", "response header <-- x-proxy-wasm-go-sdk-example: http_headers"); } } 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 cd7f5d3..6af101c 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 @@ -1,6 +1,7 @@ package io.roastedroot.proxywasm.jaxrs; import static io.roastedroot.proxywasm.Helpers.bytes; +import static io.roastedroot.proxywasm.Helpers.int32; import static io.roastedroot.proxywasm.Helpers.string; import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_DNS_SAN_LOCAL_CERTIFICATE; import static io.roastedroot.proxywasm.WellKnownProperties.CONNECTION_DNS_SAN_PEER_CERTIFICATE; @@ -16,12 +17,28 @@ import static io.roastedroot.proxywasm.WellKnownProperties.DESTINATION_ADDRESS; import static io.roastedroot.proxywasm.WellKnownProperties.DESTINATION_PORT; import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_DURATION; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_HEADERS; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_HOST; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_METHOD; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_PATH; import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_PROTOCOL; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_QUERY; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_REFERER; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_SCHEME; import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_SIZE; import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_TIME; import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_TOTAL_SIZE; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_URL_PATH; +import static io.roastedroot.proxywasm.WellKnownProperties.REQUEST_USERAGENT; +import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_BACKEND_LATENCY; +import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_CODE; +import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_CODE_DETAILS; +import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_FLAGS; +import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_GRPC_STATUS; +import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_HEADERS; import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_SIZE; import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_TOTAL_SIZE; +import static io.roastedroot.proxywasm.WellKnownProperties.RESPONSE_TRAILERS; import static io.roastedroot.proxywasm.WellKnownProperties.SOURCE_ADDRESS; import static io.roastedroot.proxywasm.WellKnownProperties.SOURCE_PORT; @@ -37,6 +54,9 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import java.net.URI; +import java.time.Duration; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -246,6 +266,12 @@ public HttpResponse getSentHttpResponse() { return senthttpResponse; } + public HttpResponse consumeSentHttpResponse() { + var result = senthttpResponse; + senthttpResponse = null; + return result; + } + private Action action; @Override @@ -283,56 +309,100 @@ public byte[] getProperty(List path) throws WasmException { return bytes(httpServer.localPort()); } - // TLS connection properties + // TODO: get TLS connection properties else if (CONNECTION_TLS_VERSION.equals(path)) { + // TODO: return null; } else if (CONNECTION_REQUESTED_SERVER_NAME.equals(path)) { + // TODO: return null; } else if (CONNECTION_MTLS.equals(path)) { + // TODO: return null; } else if (CONNECTION_SUBJECT_LOCAL_CERTIFICATE.equals(path)) { + // TODO: return null; } else if (CONNECTION_SUBJECT_PEER_CERTIFICATE.equals(path)) { + // TODO: return null; } else if (CONNECTION_DNS_SAN_LOCAL_CERTIFICATE.equals(path)) { + // TODO: return null; } else if (CONNECTION_DNS_SAN_PEER_CERTIFICATE.equals(path)) { + // TODO: return null; } else if (CONNECTION_URI_SAN_LOCAL_CERTIFICATE.equals(path)) { + // TODO: return null; } else if (CONNECTION_URI_SAN_PEER_CERTIFICATE.equals(path)) { + // TODO: return null; } else if (CONNECTION_SHA256_PEER_CERTIFICATE_DIGEST.equals(path)) { + // TODO: return null; - } - // Upstream connection properties: we are not directly connecting to an upstream server, so - // these are not implemented. - // else if (UPSTREAM_ADDRESS.equals(path)) { - // return null; - // } else if (UPSTREAM_PORT.equals(path)) { - // return null; - // } else if (UPSTREAM_LOCAL_ADDRESS.equals(path)) { - // return null; - // } else if (UPSTREAM_LOCAL_PORT.equals(path)) { - // return null; - // } else if (UPSTREAM_TLS_VERSION.equals(path)) { - // return null; - // } else if (UPSTREAM_SUBJECT_LOCAL_CERTIFICATE.equals(path)) { - // return null; - // } else if (UPSTREAM_SUBJECT_PEER_CERTIFICATE.equals(path)) { - // return null; - // } else if (UPSTREAM_DNS_SAN_LOCAL_CERTIFICATE.equals(path)) { - // return null; - // } else if (UPSTREAM_DNS_SAN_PEER_CERTIFICATE.equals(path)) { - // return null; - // } else if (UPSTREAM_URI_SAN_LOCAL_CERTIFICATE.equals(path)) { - // return null; - // } else if (UPSTREAM_URI_SAN_PEER_CERTIFICATE.equals(path)) { - // return null; - // } else if (UPSTREAM_SHA256_PEER_CERTIFICATE_DIGEST.equals(path)) { - // return null; - // } + } else if (REQUEST_PATH.equals(path)) { + // The path + query portion of the URL + + if (requestContext == null) { + return null; + } + + URI requestUri = requestContext.getUriInfo().getRequestUri(); + var result = + requestUri.getRawPath() + + (requestUri.getRawQuery() != null + ? "?" + requestUri.getRawQuery() + : ""); + return bytes(result); + } else if (REQUEST_URL_PATH.equals(path)) { + + // The path without query portion of the URL + if (requestContext == null) { + return null; + } + URI requestUri = requestContext.getUriInfo().getRequestUri(); + return bytes(requestUri.getRawPath()); + + } else if (REQUEST_QUERY.equals(path)) { + if (requestContext == null) { + return null; + } + return bytes(requestContext.getUriInfo().getRequestUri().getQuery()); + + } else if (REQUEST_HOST.equals(path)) { + if (requestContext == null) { + return null; + } + return bytes(requestContext.getUriInfo().getRequestUri().getHost()); + } else if (REQUEST_SCHEME.equals(path)) { + if (requestContext == null) { + return null; + } + return bytes(requestContext.getUriInfo().getRequestUri().getScheme()); + } else if (REQUEST_METHOD.equals(path)) { + if (requestContext == null) { + return null; + } + return bytes(requestContext.getMethod()); + } else if (REQUEST_HEADERS.equals(path)) { + var headers = getHttpRequestHeaders(); + if (headers == null) { + return null; + } + return headers.encode(); + + } else if (REQUEST_REFERER.equals(path)) { + if (requestContext == null) { + return null; + } + return bytes(requestContext.getHeaderString("Referer")); + } else if (REQUEST_USERAGENT.equals(path)) { + if (requestContext == null) { + return null; + } + return bytes(requestContext.getHeaderString("User-Agent")); + } // HTTP request properties else if (REQUEST_PROTOCOL.equals(path)) { @@ -341,28 +411,56 @@ else if (REQUEST_PROTOCOL.equals(path)) { } return bytes(requestContext.getUriInfo().getRequestUri().getScheme()); } else if (REQUEST_TIME.equals(path)) { - // TODO: check encoding /w other impls - return bytes(new Date(startedAt).toString()); + return bytes(new Date(startedAt)); } else if (REQUEST_DURATION.equals(path)) { - // TODO: check encoding /w other impls - return bytes("" + (System.currentTimeMillis() - startedAt)); + return bytes(Duration.ofMillis((System.currentTimeMillis() - startedAt))); } else if (REQUEST_SIZE.equals(path)) { if (httpRequestBody == null) { return null; } - // TODO: check encoding /w other impls - return bytes("" + httpRequestBody.length); + return bytes(httpRequestBody.length); } else if (REQUEST_TOTAL_SIZE.equals(path)) { + // TODO: how can we do this? return null; } // HTTP response properties - else if (RESPONSE_SIZE.equals(path)) { + else if (RESPONSE_CODE.equals(path)) { + if (responseContext == null) { + return null; + } + return bytes(responseContext.getStatus()); + } else if (RESPONSE_CODE_DETAILS.equals(path)) { + if (responseContext == null) { + return null; + } + return bytes(responseContext.getStatusInfo().getReasonPhrase()); + } else if (RESPONSE_FLAGS.equals(path)) { + // TODO: implement response flags retrieval + return null; + } else if (RESPONSE_GRPC_STATUS.equals(path)) { + // TODO: implement gRPC status retrieval + return null; + } else if (RESPONSE_HEADERS.equals(path)) { + var headers = getHttpResponseHeaders(); + if (headers == null) { + return null; + } + return headers.encode(); + } else if (RESPONSE_TRAILERS.equals(path)) { + var headers = getHttpResponseTrailers(); + if (headers == null) { + return null; + } + return headers.encode(); + } else if (RESPONSE_BACKEND_LATENCY.equals(path)) { + // TODO: implement backend latency retrieval + return null; + } else if (RESPONSE_SIZE.equals(path)) { if (httpResponseBody == null) { return null; } - // TODO: check encoding /w other impls - return bytes("" + httpResponseBody.length); + return bytes(httpResponseBody.length); } else if (RESPONSE_TOTAL_SIZE.equals(path)) { // TODO: how can we do this? return null; @@ -377,6 +475,100 @@ else if (RESPONSE_SIZE.equals(path)) { @Override public WasmResult setProperty(List path, byte[] value) { + + // Check to see if it's a well known property + if (REQUEST_PATH.equals(path)) { + // The path + query portion of the URL + if (requestContext == null) { + return null; + } + + var pathAndQuery = URI.create(string(value)); + var uri = requestContext.getUriInfo().getRequestUri(); + uri = + UriBuilder.fromUri(uri) + .replacePath(pathAndQuery.getPath()) + .replaceQuery(pathAndQuery.getQuery()) + .build(); + requestContext.setRequestUri(uri); + + } else if (REQUEST_URL_PATH.equals(path)) { + // The path portion of the URL + if (requestContext == null) { + return null; + } + + var uri = requestContext.getUriInfo().getRequestUri(); + uri = UriBuilder.fromUri(uri).replacePath(string(value)).build(); + requestContext.setRequestUri(uri); + } else if (REQUEST_QUERY.equals(path)) { + if (requestContext == null) { + return null; + } + + var uri = requestContext.getUriInfo().getRequestUri(); + uri = UriBuilder.fromUri(uri).replaceQuery(string(value)).build(); + requestContext.setRequestUri(uri); + + } else if (REQUEST_HOST.equals(path)) { + if (requestContext == null) { + return null; + } + var uri = requestContext.getUriInfo().getRequestUri(); + uri = UriBuilder.fromUri(uri).host(string(value)).build(); + requestContext.setRequestUri(uri); + + } else if (REQUEST_SCHEME.equals(path)) { + + if (requestContext == null) { + return null; + } + var uri = requestContext.getUriInfo().getRequestUri(); + uri = UriBuilder.fromUri(uri).scheme(string(value)).build(); + requestContext.setRequestUri(uri); + } else if (REQUEST_METHOD.equals(path)) { + if (requestContext == null) { + return null; + } + requestContext.setMethod(string(value)); + } else if (REQUEST_HEADERS.equals(path)) { + // TODO: + } else if (REQUEST_REFERER.equals(path)) { + if (requestContext == null) { + return null; + } + requestContext.getHeaders().putSingle("Referer", string(value)); + } else if (REQUEST_USERAGENT.equals(path)) { + if (requestContext == null) { + return null; + } + requestContext.getHeaders().putSingle("User-Agent", string(value)); + } + + // HTTP request properties + else if (REQUEST_PROTOCOL.equals(path)) { + if (requestContext == null) { + return null; + } + var uri = requestContext.getUriInfo().getRequestUri(); + uri = UriBuilder.fromUri(uri).scheme(string(value)).build(); + requestContext.setRequestUri(uri); + } + + // HTTP response properties + else if (RESPONSE_CODE.equals(path)) { + if (responseContext == null) { + return null; + } + responseContext.setStatus(int32(value)); + } else if (RESPONSE_CODE_DETAILS.equals(path)) { + // TODO: + } else if (RESPONSE_HEADERS.equals(path)) { + // TODO: + } else if (RESPONSE_TRAILERS.equals(path)) { + // TODO: + } + properties.put(path, value); return WasmResult.OK; } 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 b8223f8..82eb319 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 @@ -28,27 +28,29 @@ public class ProxyWasmFilter ContainerResponseFilter { private static final String FILTER_CONTEXT_PROPERTY_NAME = "WasmHttpFilterContext"; - private final WasmPluginFactory pluginFactory; + private final WasmPluginPool pluginPool; Instance httpServer; @Inject - public ProxyWasmFilter(WasmPluginFactory pluginFactory, Instance httpServer) { - this.pluginFactory = pluginFactory; + public ProxyWasmFilter(WasmPluginPool pluginPool, Instance httpServer) { + this.pluginPool = pluginPool; this.httpServer = httpServer; } // TODO: the HttpContext and ProxyWasm object's should be closed once the request is done. // is there an easy way to hook up cleanup code for this? static class WasmHttpFilterContext { + final WasmPlugin plugin; final PluginHandler pluginHandler; - final HttpHandler handler; - final HttpContext wasm; + final HttpHandler httpHandler; + final HttpContext httpContext; public WasmHttpFilterContext(WasmPlugin plugin, HttpServer httpServer) { + this.plugin = plugin; this.pluginHandler = plugin.pluginHandler(); - this.handler = new HttpHandler(plugin.pluginHandler(), httpServer); - this.wasm = plugin.proxyWasm().createHttpContext(this.handler); + this.httpHandler = new HttpHandler(plugin.pluginHandler(), httpServer); + this.httpContext = plugin.proxyWasm().createHttpContext(this.httpHandler); } } @@ -57,31 +59,34 @@ public void filter(ContainerRequestContext requestContext) throws IOException { WasmPlugin plugin = null; try { - plugin = pluginFactory.create(); + plugin = pluginPool.borrow(); + plugin.lock(); + + var ctx = new WasmHttpFilterContext(plugin, this.httpServer.get()); + requestContext.setProperty(FILTER_CONTEXT_PROPERTY_NAME, ctx); + + // the plugin may not be interested in the request headers. + if (ctx.httpContext.hasOnRequestHeaders()) { + + ctx.httpHandler.setRequestContext(requestContext); + var action = ctx.httpContext.callOnRequestHeaders(false); + if (action == Action.CONTINUE) { + // continue means plugin is done reading the headers. + ctx.httpHandler.setRequestContext(null); + } + + // does the plugin want to respond early? + HttpHandler.HttpResponse sendResponse = ctx.httpHandler.consumeSentHttpResponse(); + if (sendResponse != null) { + requestContext.abortWith(sendResponse.toResponse()); + } + } + } catch (StartException e) { requestContext.abortWith( Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); - } - - 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 (wasmHttpFilterContext.wasm.hasOnRequestHeaders()) { - - wasmHttpFilterContext.handler.setRequestContext(requestContext); - var action = wasmHttpFilterContext.wasm.callOnRequestHeaders(false); - if (action == Action.CONTINUE) { - // continue means plugin is done reading the headers. - wasmHttpFilterContext.handler.setRequestContext(null); - } - - // does the plugin want to respond early? - HttpHandler.HttpResponse sendResponse = - wasmHttpFilterContext.handler.getSentHttpResponse(); - if (sendResponse != null) { - requestContext.abortWith(sendResponse.toResponse()); - } + } finally { + plugin.unlock(); // allow another request to use the plugin. } } @@ -97,27 +102,36 @@ public Object aroundReadFrom(ReaderInterceptorContext ctx) } // the plugin may not be interested in the request body. - if (wasmHttpFilterContext.wasm.hasOnRequestBody()) { + 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(); - wasmHttpFilterContext.handler.setHttpRequestBody(bytes); - var action = wasmHttpFilterContext.wasm.callOnRequestBody(true); - bytes = wasmHttpFilterContext.handler.getHttpRequestBody(); - if (action == Action.CONTINUE) { - // continue means plugin is done reading the body. - wasmHttpFilterContext.handler.setHttpRequestBody(null); - } - // TODO: find out more details about what to do here in a PAUSE condition. - // does it mean that we park the request here and wait for another event like - // tick to resume us before forwarding to the next filter? - - // does the plugin want to respond early? - HttpHandler.HttpResponse sendResponse = - wasmHttpFilterContext.handler.getSentHttpResponse(); - if (sendResponse != null) { - throw new WebApplicationException(sendResponse.toResponse()); + try { + // we are about to call into the plugin which may mutate the plugin state.. + wasmHttpFilterContext.plugin.lock(); + + wasmHttpFilterContext.httpHandler.setHttpRequestBody(bytes); + var action = wasmHttpFilterContext.httpContext.callOnRequestBody(true); + bytes = wasmHttpFilterContext.httpHandler.getHttpRequestBody(); + if (action == Action.CONTINUE) { + // continue means plugin is done reading the body. + wasmHttpFilterContext.httpHandler.setHttpRequestBody(null); + } + + // TODO: find out more details about what to do here in a PAUSE condition. + // does it mean that we park the request here and wait for another event like + // tick to resume us before forwarding to the next filter? + + // does the plugin want to respond early? + HttpHandler.HttpResponse sendResponse = + wasmHttpFilterContext.httpHandler.getSentHttpResponse(); + 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 @@ -138,20 +152,26 @@ public void filter( } // the plugin may not be interested in the request headers. - if (wasmHttpFilterContext.wasm.hasOnResponseHeaders()) { - - wasmHttpFilterContext.handler.setResponseContext(responseContext); - var action = wasmHttpFilterContext.wasm.callOnResponseHeaders(false); - if (action == Action.CONTINUE) { - // continue means plugin is done reading the headers. - wasmHttpFilterContext.handler.setResponseContext(null); - } - - // does the plugin want to respond early? - HttpHandler.HttpResponse sendResponse = - wasmHttpFilterContext.handler.getSentHttpResponse(); - if (sendResponse != null) { - requestContext.abortWith(sendResponse.toResponse()); + if (wasmHttpFilterContext.httpContext.hasOnResponseHeaders()) { + try { + wasmHttpFilterContext.plugin.lock(); + + wasmHttpFilterContext.httpHandler.setResponseContext(responseContext); + var action = wasmHttpFilterContext.httpContext.callOnResponseHeaders(false); + if (action == Action.CONTINUE) { + // continue means plugin is done reading the headers. + wasmHttpFilterContext.httpHandler.setResponseContext(null); + } + + // does the plugin want to respond early? + HttpHandler.HttpResponse sendResponse = + wasmHttpFilterContext.httpHandler.getSentHttpResponse(); + if (sendResponse != null) { + requestContext.abortWith(sendResponse.toResponse()); + } + } finally { + // allow other request to use the plugin. + wasmHttpFilterContext.plugin.unlock(); } } } @@ -166,52 +186,63 @@ public void aroundWriteTo(WriterInterceptorContext ctx) Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); } - // the plugin may not be interested in the request body. - if (wasmHttpFilterContext.wasm.hasOnResponseBody()) { - - var original = ctx.getOutputStream(); - ctx.setOutputStream( - new ByteArrayOutputStream() { - @Override - public void close() throws IOException { - super.close(); - - // TODO: find out if it's more efficient to read the body in chunks and - // do - // multiple callOnRequestBody calls. - - byte[] bytes = this.toByteArray(); - wasmHttpFilterContext.handler.setHttpResponseBody(bytes); - var action = wasmHttpFilterContext.wasm.callOnResponseBody(true); - bytes = wasmHttpFilterContext.handler.getHttpResponseBody(); - if (action == Action.CONTINUE) { - // continue means plugin is done reading the body. - wasmHttpFilterContext.handler.setHttpResponseBody(null); - } + try { - // does the plugin want to respond early? - HttpHandler.HttpResponse sendResponse = - wasmHttpFilterContext.handler.getSentHttpResponse(); - if (sendResponse != null) { - throw new WebApplicationException(sendResponse.toResponse()); + // the plugin may not be interested in the request body. + if (wasmHttpFilterContext.httpContext.hasOnResponseBody()) { + var original = ctx.getOutputStream(); + ctx.setOutputStream( + new ByteArrayOutputStream() { + boolean closed = false; + + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + super.close(); + + // TODO: find out if it's more efficient to read the body in chunks + // and + // do + // multiple callOnRequestBody calls. + + byte[] bytes = this.toByteArray(); + + wasmHttpFilterContext.plugin.lock(); + + wasmHttpFilterContext.httpHandler.setHttpResponseBody(bytes); + var action = + wasmHttpFilterContext.httpContext.callOnResponseBody(false); + bytes = wasmHttpFilterContext.httpHandler.getHttpResponseBody(); + if (action == Action.CONTINUE) { + // continue means plugin is done reading the body. + wasmHttpFilterContext.httpHandler.setHttpResponseBody(null); + } + + // does the plugin want to respond early? + HttpHandler.HttpResponse sendResponse = + wasmHttpFilterContext.httpHandler.getSentHttpResponse(); + if (sendResponse != null) { + throw new WebApplicationException(sendResponse.toResponse()); + } + + // plugin may have modified the body + original.write(bytes); + original.close(); } + }); + } - // plugin may have modified the body - original.write(bytes); - original.close(); - - // clean up... - // wasmHttpFilterContext.wasm.close(); - // - // wasmHttpFilterContext.wasm.getProxyWasm().close(); - } - }); - } else { - // clean up... - // wasmHttpFilterContext.wasm.close(); - // wasmHttpFilterContext.wasm.getProxyWasm().close(); - } + ctx.proceed(); + } finally { + // allow other request to use the plugin. + wasmHttpFilterContext.httpContext.close(); + wasmHttpFilterContext.plugin.unlock(); - ctx.proceed(); + // TODO: will aroundWriteTo always get called so that we can avoid leaking the plugin? + this.pluginPool.release(wasmHttpFilterContext.plugin); + } } } diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java index d76d7c7..a797028 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java @@ -6,17 +6,20 @@ import io.roastedroot.proxywasm.ProxyWasm; import io.roastedroot.proxywasm.StartException; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; public class WasmPlugin { private final ProxyWasm proxyWasm; private final PluginHandler handler; + private final ReentrantLock lock; - public WasmPlugin(ProxyWasm proxyWasm, PluginHandler handler) { + private WasmPlugin(ProxyWasm proxyWasm, PluginHandler handler, boolean shared) { Objects.requireNonNull(proxyWasm); Objects.requireNonNull(handler); this.proxyWasm = proxyWasm; this.handler = handler; + this.lock = shared ? new ReentrantLock() : null; } public String name() { @@ -35,16 +38,40 @@ public static WasmPlugin.Builder builder() { return new WasmPlugin.Builder(); } + public void lock() { + if (lock == null) { + return; + } + lock.lock(); + } + + public void unlock() { + if (lock == null) { + return; + } + lock.unlock(); + } + + public boolean isShared() { + return lock != null; + } + public static class Builder implements Cloneable { - PluginHandler handler = new PluginHandler(); - ProxyWasm.Builder proxyWasmBuilder = ProxyWasm.builder().withPluginHandler(handler); + private PluginHandler handler = new PluginHandler(); + private ProxyWasm.Builder proxyWasmBuilder = ProxyWasm.builder().withPluginHandler(handler); + private boolean shared = true; public WasmPlugin.Builder withName(String name) { this.handler.name = name; return this; } + public WasmPlugin.Builder withShared(boolean shared) { + this.shared = shared; + return this; + } + public WasmPlugin.Builder withVmConfig(byte[] vmConfig) { proxyWasmBuilder = proxyWasmBuilder.withVmConfig(vmConfig); return this; @@ -83,7 +110,7 @@ public WasmPlugin build(Instance instance) throws StartException { } public WasmPlugin build(ProxyWasm proxyWasm) throws StartException { - return new WasmPlugin(proxyWasm, handler); + return new WasmPlugin(proxyWasm, handler, shared); } } } diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java index 703e6ad..6204d43 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java @@ -14,7 +14,7 @@ @Provider public class WasmPluginFeature implements DynamicFeature { - private HashMap plugins = new HashMap<>(); + private HashMap plugins = new HashMap<>(); @Inject @Any Instance requestAdaptor; @@ -22,10 +22,15 @@ public class WasmPluginFeature implements DynamicFeature { public WasmPluginFeature(@Any Instance factories) throws StartException { for (var factory : factories) { var plugin = factory.create(); - if (this.plugins.containsKey(plugin.name())) { - throw new IllegalArgumentException("Duplicate wasm plugin name: " + plugin.name()); + String name = plugin.name(); + if (this.plugins.containsKey(name)) { + throw new IllegalArgumentException("Duplicate wasm plugin name: " + name); } - this.plugins.put(plugin.name(), factory); + WasmPluginPool pool = + plugin.isShared() + ? new WasmPluginPool.AppScoped(plugin) + : new WasmPluginPool.RequestScoped(factory, plugin); + this.plugins.put(name, pool); } } @@ -42,7 +47,7 @@ public void configure(ResourceInfo resourceInfo, FeatureContext context) { resourceInfo.getResourceClass().getAnnotation(NamedWasmPlugin.class); } if (pluignNameAnnotation != null) { - WasmPluginFactory factory = plugins.get(pluignNameAnnotation.value()); + WasmPluginPool factory = plugins.get(pluignNameAnnotation.value()); if (factory != null) { context.register(new ProxyWasmFilter(factory, requestAdaptor)); } diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginPool.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginPool.java new file mode 100644 index 0000000..a32b86a --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginPool.java @@ -0,0 +1,53 @@ +package io.roastedroot.proxywasm.jaxrs; + +import io.roastedroot.proxywasm.StartException; + +public interface WasmPluginPool { + + WasmPlugin borrow() throws StartException; + + void release(WasmPlugin plugin); + + class AppScoped implements WasmPluginPool { + private final WasmPlugin plugin; + + public AppScoped(WasmPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void release(WasmPlugin plugin) { + if (plugin != this.plugin) { + throw new IllegalArgumentException("Plugin not from this pool"); + } + } + + @Override + public WasmPlugin borrow() throws StartException { + return plugin; + } + } + + class RequestScoped implements WasmPluginPool { + + final WasmPluginFactory factory; + + public RequestScoped(WasmPluginFactory factory, WasmPlugin plugin) { + this.factory = factory; + release(plugin); + } + + @Override + public WasmPlugin borrow() throws StartException { + return factory.create(); + } + + // Return the plugin to the pool + @Override + public void release(WasmPlugin plugin) { + // TODO: maybe implementing pooling in the future to reduce GC pressure + // but for now, we just close the plugin + plugin.proxyWasm().close(); + } + } +} diff --git a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersNotSharedTest.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersNotSharedTest.java new file mode 100644 index 0000000..f90b39f --- /dev/null +++ b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersNotSharedTest.java @@ -0,0 +1,47 @@ +package io.roastedroot.proxywasm.jaxrs; + +import static io.restassured.RestAssured.given; +import static io.roastedroot.proxywasm.jaxrs.TestHelpers.EXAMPLES_DIR; + +import com.dylibso.chicory.wasm.Parser; +import io.quarkus.test.junit.QuarkusTest; +import io.roastedroot.proxywasm.StartException; +import jakarta.enterprise.inject.Produces; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class HttpHeadersNotSharedTest { + + @Produces + public WasmPluginFactory create() throws StartException { + return () -> + WasmPlugin.builder() + .withName("notSharedHttpHeaders") + .withShared(false) + .withPluginConfig("{\"header\": \"x-wasm-header\", \"value\": \"foo\"}") + .build( + Parser.parse( + Path.of( + EXAMPLES_DIR + + "/go-examples/http_headers/main.wasm"))); + } + + @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) + .header("x-proxy-wasm-counter", "1"); + + given().when() + .get("/test/notSharedHttpHeaders") + .then() + .statusCode(200) + .header("x-proxy-wasm-counter", "1"); + } +} diff --git a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersResource.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersResource.java deleted file mode 100644 index 711ee9b..0000000 --- a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersResource.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.roastedroot.proxywasm.jaxrs; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; - -@Path("/http_headers") -public class HttpHeadersResource { - - @GET - @NamedWasmPlugin("http_headers") - public String get() { - return "hello world"; - } -} diff --git a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersTest.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersTest.java index cd4bde6..bd4e5c6 100644 --- a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersTest.java +++ b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/HttpHeadersTest.java @@ -18,7 +18,8 @@ public class HttpHeadersTest { public WasmPluginFactory create() throws StartException { return () -> WasmPlugin.builder() - .withName("http_headers") + .withName("httpHeaders") + .withShared(true) .withPluginConfig("{\"header\": \"x-wasm-header\", \"value\": \"foo\"}") .build( Parser.parse( @@ -30,11 +31,21 @@ public WasmPluginFactory create() throws StartException { @Test public void testRequest() { given().when() - .get("/http_headers") + .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/src/test/java/io/roastedroot/proxywasm/jaxrs/Resources.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/Resources.java new file mode 100644 index 0000000..017ddf0 --- /dev/null +++ b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/Resources.java @@ -0,0 +1,22 @@ +package io.roastedroot.proxywasm.jaxrs; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/test") +public class Resources { + + @Path("/httpHeaders") + @GET + @NamedWasmPlugin("httpHeaders") + public String httpHeaders() { + return "hello world"; + } + + @Path("/notSharedHttpHeaders") + @GET + @NamedWasmPlugin("notSharedHttpHeaders") + public String notSharedHttpHeaders() { + return "hello world"; + } +}