diff --git a/pom.xml b/pom.xml index 7e415c4..f051be0 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ proxy-wasm-java-host + proxy-wasm-jaxrs @@ -32,18 +33,24 @@ true true 2023-01-01T00:00:00Z + true 10.21.4 3.6.0 2.44.3 3.14.0 + 3.5.2 5.12.0 1.1.0 + quarkus-bom + io.quarkus.platform + 3.19.3 + @@ -188,6 +195,7 @@ maven-compiler-plugin ${maven.compiler.version} + true 11 11 true diff --git a/proxy-wasm-java-host/pom.xml b/proxy-wasm-java-host/pom.xml index b972aa1..3692360 100644 --- a/proxy-wasm-java-host/pom.xml +++ b/proxy-wasm-java-host/pom.xml @@ -8,7 +8,6 @@ 1.0-SNAPSHOT ../pom.xml - proxy-wasm-java-host jar diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ABI.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ABI.java index ff9003e..0002c09 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ABI.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ABI.java @@ -21,35 +21,35 @@ class ABI { private Handler handler; private Memory memory; - private ExportFunction initializeFn; - private ExportFunction mainFn; - private ExportFunction startFn; - private ExportFunction proxyOnContextCreateFn; - private ExportFunction proxyOnDoneFn; - private ExportFunction mallocFn; - private ExportFunction proxyOnLogFn; - private ExportFunction proxyOnDeleteFn; - private ExportFunction proxyOnVmStartFn; - private ExportFunction proxyOnConfigureFn; - private ExportFunction proxyOnTickFn; - private ExportFunction proxyOnNewConnectionFn; - private ExportFunction proxyOnDownstreamDataFn; - private ExportFunction proxyOnDownstreamConnectionCloseFn; - private ExportFunction proxyOnUpstreamDataFn; - private ExportFunction proxyOnUpstreamConnectionCloseFn; - private ExportFunction proxyOnRequestHeadersFn; - private ExportFunction proxyOnRequestBodyFn; - private ExportFunction proxyOnRequestTrailersFn; - private ExportFunction proxyOnResponseHeadersFn; - private ExportFunction proxyOnResponseBodyFn; - private ExportFunction proxyOnResponseTrailersFn; - private ExportFunction proxyOnHttpCallResponseFn; - private ExportFunction proxyOnGrpcReceiveInitialMetadataFn; - private ExportFunction proxyOnGrpcReceiveFn; - private ExportFunction proxyOnGrpcReceiveTrailingMetadataFn; - private ExportFunction proxyOnGrpcCloseFn; - private ExportFunction proxyOnQueueReadyFn; - private ExportFunction proxyOnForeignFunctionFn; + ExportFunction initializeFn; + ExportFunction mainFn; + ExportFunction startFn; + ExportFunction proxyOnContextCreateFn; + ExportFunction proxyOnDoneFn; + ExportFunction mallocFn; + ExportFunction proxyOnLogFn; + ExportFunction proxyOnDeleteFn; + ExportFunction proxyOnVmStartFn; + ExportFunction proxyOnConfigureFn; + ExportFunction proxyOnTickFn; + ExportFunction proxyOnNewConnectionFn; + ExportFunction proxyOnDownstreamDataFn; + ExportFunction proxyOnDownstreamConnectionCloseFn; + ExportFunction proxyOnUpstreamDataFn; + ExportFunction proxyOnUpstreamConnectionCloseFn; + ExportFunction proxyOnRequestHeadersFn; + ExportFunction proxyOnRequestBodyFn; + ExportFunction proxyOnRequestTrailersFn; + ExportFunction proxyOnResponseHeadersFn; + ExportFunction proxyOnResponseBodyFn; + ExportFunction proxyOnResponseTrailersFn; + ExportFunction proxyOnHttpCallResponseFn; + ExportFunction proxyOnGrpcReceiveInitialMetadataFn; + ExportFunction proxyOnGrpcReceiveFn; + ExportFunction proxyOnGrpcReceiveTrailingMetadataFn; + ExportFunction proxyOnGrpcCloseFn; + ExportFunction proxyOnQueueReadyFn; + ExportFunction proxyOnForeignFunctionFn; Handler getHandler() { return handler; @@ -866,7 +866,29 @@ int proxyGetHeaderMapValue( @WasmExport int proxyAddHeaderMapValue( int mapType, int keyDataPtr, int keySize, int valueDataPtr, int valueSize) { - return proxyReplaceHeaderMapValue(mapType, keyDataPtr, keySize, valueDataPtr, valueSize); + try { + // Get the header map based on the map type + ProxyMap headerMap = getMap(mapType); + if (headerMap == null) { + return WasmResult.BAD_ARGUMENT.getValue(); + } + + // Get key from memory + String key = string(readMemory(keyDataPtr, keySize)); + + // Get value from memory + String value = string(readMemory(valueDataPtr, valueSize)); + + // Add value in map + headerMap.add(key, value); + + return WasmResult.OK.getValue(); + + } catch (WasmRuntimeException e) { + return WasmResult.INVALID_MEMORY_ACCESS.getValue(); + } catch (WasmException e) { + return e.result().getValue(); + } } /** @@ -897,9 +919,7 @@ int proxyReplaceHeaderMapValue( String value = string(readMemory(valueDataPtr, valueSize)); // Replace value in map - var copy = new ArrayProxyMap(headerMap); - copy.put(key, value); - setMap(mapType, copy); + headerMap.put(key, value); return WasmResult.OK.getValue(); @@ -935,9 +955,7 @@ int proxyRemoveHeaderMapValue(int mapType, int keyDataPtr, int keySize) { } // Remove key from map - var copy = new ArrayProxyMap(headerMap); - copy.remove(key); - setMap(mapType, copy); + headerMap.remove(key); return WasmResult.OK.getValue(); @@ -982,40 +1000,6 @@ private ProxyMap getMap(int mapType) { return null; } - /** - * Set a header map based on the map type. - * - * @param mapType The type of map to set - * @param map The header map to set - * @return WasmResult indicating success or failure - */ - private WasmResult setMap(int mapType, ProxyMap map) { - var knownType = MapType.fromInt(mapType); - if (knownType == null) { - return handler.setCustomHeaders(mapType, map); - } - - switch (knownType) { - case HTTP_REQUEST_HEADERS: - return handler.setHttpRequestHeaders(map); - case HTTP_REQUEST_TRAILERS: - return handler.setHttpRequestTrailers(map); - case HTTP_RESPONSE_HEADERS: - return handler.setHttpResponseHeaders(map); - case HTTP_RESPONSE_TRAILERS: - return handler.setHttpResponseTrailers(map); - case HTTP_CALL_RESPONSE_HEADERS: - return handler.setHttpCallResponseHeaders(map); - case HTTP_CALL_RESPONSE_TRAILERS: - return handler.setHttpCallResponseTrailers(map); - case GRPC_RECEIVE_INITIAL_METADATA: - return handler.setGrpcReceiveInitialMetaData(map); - case GRPC_RECEIVE_TRAILING_METADATA: - return handler.setGrpcReceiveTrailerMetaData(map); - } - return WasmResult.NOT_FOUND; - } - /** * Decodes a byte array containing map data into a Map of String key-value pairs. *

diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ChainedHandler.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ChainedHandler.java index ddb68da..b1946eb 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ChainedHandler.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ChainedHandler.java @@ -64,46 +64,6 @@ public WasmResult setCustomHeaders(int mapType, ProxyMap map) { return next().setCustomHeaders(mapType, map); } - @Override - public WasmResult setHttpRequestHeaders(ProxyMap headers) { - return next().setHttpRequestHeaders(headers); - } - - @Override - public WasmResult setHttpRequestTrailers(ProxyMap trailers) { - return next().setHttpRequestTrailers(trailers); - } - - @Override - public WasmResult setHttpResponseHeaders(ProxyMap headers) { - return next().setHttpResponseHeaders(headers); - } - - @Override - public WasmResult setHttpResponseTrailers(ProxyMap trailers) { - return next().setHttpResponseTrailers(trailers); - } - - @Override - public WasmResult setHttpCallResponseHeaders(ProxyMap headers) { - return next().setHttpCallResponseHeaders(headers); - } - - @Override - public WasmResult setHttpCallResponseTrailers(ProxyMap trailers) { - return next().setHttpCallResponseTrailers(trailers); - } - - @Override - public WasmResult setGrpcReceiveInitialMetaData(ProxyMap metadata) { - return next().setGrpcReceiveInitialMetaData(metadata); - } - - @Override - public WasmResult setGrpcReceiveTrailerMetaData(ProxyMap metadata) { - return next().setGrpcReceiveTrailerMetaData(metadata); - } - @Override public byte[] getProperty(List key) throws WasmException { return next().getProperty(key); diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Context.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Context.java index d6559dc..e44554d 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Context.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Context.java @@ -20,6 +20,14 @@ public int id() { return id; } + public ProxyWasm getProxyWasm() { + return proxyWasm; + } + + ProxyWasm proxyWasm() { + return proxyWasm; + } + public void close() { if (closeStarted) { return; diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java index 1557aec..232d2db 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java @@ -298,86 +298,6 @@ default WasmResult setCustomHeaders(int mapType, ProxyMap map) { return WasmResult.UNIMPLEMENTED; } - /** - * Set the HTTP request headers. - * - * @param headers The headers to set - * @return WasmResult indicating success or failure - */ - default WasmResult setHttpRequestHeaders(ProxyMap headers) { - return WasmResult.UNIMPLEMENTED; - } - - /** - * Set the HTTP request trailers. - * - * @param trailers The trailers to set - * @return WasmResult indicating success or failure - */ - default WasmResult setHttpRequestTrailers(ProxyMap trailers) { - return WasmResult.UNIMPLEMENTED; - } - - /** - * Set the HTTP response headers. - * - * @param headers The headers to set - * @return WasmResult indicating success or failure - */ - default WasmResult setHttpResponseHeaders(ProxyMap headers) { - return WasmResult.UNIMPLEMENTED; - } - - /** - * Set the HTTP response trailers. - * - * @param trailers The trailers to set - * @return WasmResult indicating success or failure - */ - default WasmResult setHttpResponseTrailers(ProxyMap trailers) { - return WasmResult.UNIMPLEMENTED; - } - - /** - * Set the HTTP call response headers. - * - * @param headers The headers to set - * @return WasmResult indicating success or failure - */ - default WasmResult setHttpCallResponseHeaders(ProxyMap headers) { - return WasmResult.UNIMPLEMENTED; - } - - /** - * Set the HTTP call response trailers. - * - * @param trailers The trailers to set - * @return WasmResult indicating success or failure - */ - default WasmResult setHttpCallResponseTrailers(ProxyMap trailers) { - return WasmResult.UNIMPLEMENTED; - } - - /** - * Set the gRPC receive initial metadata. - * - * @param metadata The metadata to set - * @return WasmResult indicating success or failure - */ - default WasmResult setGrpcReceiveInitialMetaData(ProxyMap metadata) { - return WasmResult.UNIMPLEMENTED; - } - - /** - * Set the gRPC receive trailer metadata. - * - * @param metadata The metadata to set - * @return WasmResult indicating success or failure - */ - default WasmResult setGrpcReceiveTrailerMetaData(ProxyMap metadata) { - return WasmResult.UNIMPLEMENTED; - } - default WasmResult setAction(StreamType streamType, Action action) { return WasmResult.UNIMPLEMENTED; } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/HttpContext.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/HttpContext.java index 54ab2d0..defdb93 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/HttpContext.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/HttpContext.java @@ -15,6 +15,10 @@ Handler handler() { return handler; } + public boolean hasOnRequestHeaders() { + return proxyWasm.abi().proxyOnRequestHeadersFn != null; + } + public Action callOnRequestHeaders(boolean endOfStream) { var headers = handler.getHttpRequestHeaders(); int result = proxyWasm.abi().proxyOnRequestHeaders(id, len(headers), endOfStream ? 1 : 0); @@ -23,6 +27,10 @@ public Action callOnRequestHeaders(boolean endOfStream) { return action; } + public boolean hasOnResponseHeaders() { + return proxyWasm.abi().proxyOnResponseHeadersFn != null; + } + public Action callOnResponseHeaders(boolean endOfStream) { var headers = handler.getHttpResponseHeaders(); int result = proxyWasm.abi().proxyOnResponseHeaders(id, len(headers), endOfStream ? 1 : 0); @@ -31,6 +39,10 @@ public Action callOnResponseHeaders(boolean endOfStream) { return action; } + public boolean hasOnRequestBody() { + return proxyWasm.abi().proxyOnRequestBodyFn != null; + } + public Action callOnRequestBody(boolean endOfStream) { var requestBody = handler.getHttpRequestBody(); int result = @@ -42,6 +54,10 @@ public Action callOnRequestBody(boolean endOfStream) { return action; } + public boolean hasOnResponseBody() { + return proxyWasm.abi().proxyOnResponseBodyFn != null; + } + public Action callOnResponseBody(boolean endOfStream) { var responseBody = handler.getHttpResponseBody(); int result = 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 new file mode 100644 index 0000000..9d479a3 --- /dev/null +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownProperties.java @@ -0,0 +1,69 @@ +package io.roastedroot.proxywasm; + +import java.util.List; + +public final class WellKnownProperties { + private WellKnownProperties() {} + + // Proxy-Wasm properties + public static final List PLUGIN_NAME = List.of("plugin_name"); + public static final List PLUGIN_ROOT_ID = List.of("plugin_root_id"); + 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_REQUESTED_SERVER_NAME = + 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"); + public static final List 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"); + public static final List 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"); + public static final List 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"); + + // 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_SUBJECT_LOCAL_CERTIFICATE = + List.of("upstream.subject_local_certificate"); + public static final List 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"); + public static final List 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"); + public static final List 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"); + + // 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"); + + // 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"); +} diff --git a/proxy-wasm-java-host/src/test/java/io/roastedroot/proxywasm/examples/MockHandler.java b/proxy-wasm-java-host/src/test/java/io/roastedroot/proxywasm/examples/MockHandler.java index a0a1dd8..cd9ed54 100644 --- a/proxy-wasm-java-host/src/test/java/io/roastedroot/proxywasm/examples/MockHandler.java +++ b/proxy-wasm-java-host/src/test/java/io/roastedroot/proxywasm/examples/MockHandler.java @@ -158,7 +158,6 @@ public ProxyMap getGrpcReceiveTrailerMetaData() { return grpcReceiveTrailerMetadata; } - @Override public WasmResult setHttpRequestHeaders(ProxyMap headers) { this.httpRequestHeaders = headers; return WasmResult.OK; @@ -168,7 +167,6 @@ public WasmResult setHttpRequestHeaders(Map headers) { return this.setHttpRequestHeaders(new ArrayProxyMap(headers)); } - @Override public WasmResult setHttpRequestTrailers(ProxyMap trailers) { this.httpRequestTrailers = trailers; return WasmResult.OK; @@ -178,7 +176,6 @@ public WasmResult setHttpRequestTrailers(Map headers) { return this.setHttpRequestTrailers(new ArrayProxyMap(headers)); } - @Override public WasmResult setHttpResponseHeaders(ProxyMap headers) { this.httpResponseHeaders = headers; return WasmResult.OK; @@ -188,19 +185,16 @@ public WasmResult setHttpResponseHeaders(Map headers) { return this.setHttpResponseHeaders(new ArrayProxyMap(headers)); } - @Override public WasmResult setHttpResponseTrailers(ProxyMap trailers) { this.httpResponseTrailers = trailers; return WasmResult.OK; } - @Override public WasmResult setGrpcReceiveInitialMetaData(ProxyMap metadata) { this.grpcReceiveInitialMetadata = metadata; return WasmResult.OK; } - @Override public WasmResult setGrpcReceiveTrailerMetaData(ProxyMap metadata) { this.grpcReceiveTrailerMetadata = metadata; return WasmResult.OK; diff --git a/proxy-wasm-jaxrs/.gitignore b/proxy-wasm-jaxrs/.gitignore new file mode 100644 index 0000000..91a800a --- /dev/null +++ b/proxy-wasm-jaxrs/.gitignore @@ -0,0 +1,45 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ +# TLS Certificates +.certs/ diff --git a/proxy-wasm-jaxrs/pom.xml b/proxy-wasm-jaxrs/pom.xml new file mode 100644 index 0000000..938a7a8 --- /dev/null +++ b/proxy-wasm-jaxrs/pom.xml @@ -0,0 +1,161 @@ + + + 4.0.0 + + + io.roastedroot + proxy-wasm-java-host-parent + 1.0-SNAPSHOT + ../pom.xml + + + proxy-wasm-jaxrs + jar + proxy-wasm-jaxrs + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + + io.roastedroot + proxy-wasm-java-host + 1.0-SNAPSHOT + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + jakarta.inject + jakarta.inject-api + provided + + + + + jakarta.ws.rs + jakarta.ws.rs-api + provided + + + + + io.quarkus + quarkus-arc + test + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-rest-jackson + test + + + io.rest-assured + rest-assured + test + + + + + + + + jdk-lower-than-17 + + (,17] + + + true + true + + + + + + jdk-17-and-newer + + [17,) + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + native-image-agent + + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + integration-test + verify + + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + native + + + + false + true + + + + + 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 new file mode 100644 index 0000000..7a39640 --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/HttpHandler.java @@ -0,0 +1,252 @@ +package io.roastedroot.proxywasm.jaxrs; + +import static io.roastedroot.proxywasm.Helpers.string; + +import io.roastedroot.proxywasm.Action; +import io.roastedroot.proxywasm.ChainedHandler; +import io.roastedroot.proxywasm.Handler; +import io.roastedroot.proxywasm.Helpers; +import io.roastedroot.proxywasm.ProxyMap; +import io.roastedroot.proxywasm.StreamType; +import io.roastedroot.proxywasm.WasmException; +import io.roastedroot.proxywasm.WasmResult; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; + +class HttpHandler extends ChainedHandler { + + private final PluginHandler next; + + HttpHandler(PluginHandler pluginHandler) { + this.next = pluginHandler; + } + + @Override + protected Handler next() { + return next; + } + + // ////////////////////////////////////////////////////////////////////// + // HTTP fields + // ////////////////////////////////////////////////////////////////////// + + private ContainerRequestContext requestContext; + + public ContainerRequestContext getRequestContext() { + return requestContext; + } + + public void setRequestContext(ContainerRequestContext requestContext) { + this.requestContext = requestContext; + } + + @Override + public ProxyMap getHttpRequestHeaders() { + return new JaxrsProxyMap(requestContext.getHeaders()); + } + + @Override + public ProxyMap getHttpRequestTrailers() { + return null; + } + + private ContainerResponseContext responseContext; + + public void setResponseContext(ContainerResponseContext responseContext) { + this.responseContext = responseContext; + } + + @Override + public ProxyMap getHttpResponseHeaders() { + return new JaxrsProxyMap(responseContext.getHeaders()); + } + + @Override + public ProxyMap getHttpResponseTrailers() { + return null; + } + + @Override + public ProxyMap getGrpcReceiveInitialMetaData() { + return null; + } + + @Override + public ProxyMap getGrpcReceiveTrailerMetaData() { + return null; + } + + // ////////////////////////////////////////////////////////////////////// + // Buffers + // ////////////////////////////////////////////////////////////////////// + + private byte[] httpRequestBody = new byte[0]; + + @Override + public byte[] getHttpRequestBody() { + return this.httpRequestBody; + } + + @Override + public WasmResult setHttpRequestBody(byte[] body) { + this.httpRequestBody = body; + return WasmResult.OK; + } + + public void appendHttpRequestBody(byte[] body) { + this.httpRequestBody = Helpers.append(this.httpRequestBody, body); + } + + private byte[] grpcReceiveBuffer = new byte[0]; + + @Override + public byte[] getGrpcReceiveBuffer() { + return this.grpcReceiveBuffer; + } + + @Override + public WasmResult setGrpcReceiveBuffer(byte[] buffer) { + this.grpcReceiveBuffer = buffer; + return WasmResult.OK; + } + + private byte[] upstreamData = new byte[0]; + + @Override + public byte[] getUpstreamData() { + return this.upstreamData; + } + + @Override + public WasmResult setUpstreamData(byte[] data) { + this.upstreamData = data; + return WasmResult.OK; + } + + private byte[] downStreamData = new byte[0]; + + @Override + public byte[] getDownStreamData() { + return this.downStreamData; + } + + @Override + public WasmResult setDownStreamData(byte[] data) { + this.downStreamData = data; + return WasmResult.OK; + } + + private byte[] httpResponseBody = new byte[0]; + + @Override + public byte[] getHttpResponseBody() { + return this.httpResponseBody; + } + + @Override + public WasmResult setHttpResponseBody(byte[] body) { + this.httpResponseBody = body; + return WasmResult.OK; + } + + public void appendHttpResponseBody(byte[] body) { + this.httpResponseBody = Helpers.append(this.httpResponseBody, body); + } + + // ////////////////////////////////////////////////////////////////////// + // HTTP streams + // ////////////////////////////////////////////////////////////////////// + + public static class HttpResponse { + + public final int statusCode; + public final byte[] statusCodeDetails; + public final byte[] body; + public final ProxyMap headers; + public final int grpcStatus; + + public HttpResponse( + int responseCode, + byte[] responseCodeDetails, + byte[] responseBody, + ProxyMap additionalHeaders, + int grpcStatus) { + this.statusCode = responseCode; + this.statusCodeDetails = responseCodeDetails; + this.body = responseBody; + this.headers = additionalHeaders; + this.grpcStatus = grpcStatus; + } + + public Response toResponse() { + Response.ResponseBuilder builder = + Response.status(statusCode, string(statusCodeDetails)); + if (headers != null) { + for (var entry : headers.entries()) { + builder = builder.header(entry.getKey(), entry.getValue()); + } + } + builder.entity(body); + return builder.build(); + } + } + + private HttpResponse senthttpResponse; + + @Override + public WasmResult sendHttpResponse( + int responseCode, + byte[] responseCodeDetails, + byte[] responseBody, + ProxyMap additionalHeaders, + int grpcStatus) { + this.senthttpResponse = + new HttpResponse( + responseCode, + responseCodeDetails, + responseBody, + additionalHeaders, + grpcStatus); + return WasmResult.OK; + } + + public HttpResponse getSentHttpResponse() { + return senthttpResponse; + } + + private Action action; + + @Override + public WasmResult setAction(StreamType streamType, Action action) { + this.action = action; + return WasmResult.OK; + } + + public Action getAction() { + return action; + } + + // ////////////////////////////////////////////////////////////////////// + // Properties + // ////////////////////////////////////////////////////////////////////// + + final HashMap, byte[]> properties = new HashMap<>(); + + @Override + public byte[] getProperty(List path) throws WasmException { + byte[] result = properties.get(path); + if (result == null) { + return next().getProperty(path); + } + return result; + } + + @Override + public WasmResult setProperty(List path, byte[] value) { + properties.put(path, value); + return WasmResult.OK; + } +} diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsProxyMap.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsProxyMap.java new file mode 100644 index 0000000..7f6295b --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsProxyMap.java @@ -0,0 +1,105 @@ +package io.roastedroot.proxywasm.jaxrs; + +import io.roastedroot.proxywasm.ProxyMap; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +public class JaxrsProxyMap implements ProxyMap { + + final MultivaluedMap entries; + + public JaxrsProxyMap() { + this.entries = new MultivaluedHashMap<>(); + } + + public JaxrsProxyMap(int mapSize) { + this.entries = new MultivaluedHashMap<>(); + } + + public JaxrsProxyMap(ProxyMap other) { + this(other.size()); + for (Map.Entry entry : other.entries()) { + add(entry.getKey(), entry.getValue()); + } + } + + public JaxrsProxyMap(MultivaluedMap other) { + this.entries = other; + } + + @Override + public int size() { + return entries.size(); + } + + @Override + public void add(String key, String value) { + entries.add(key, (T) value); + } + + @Override + public void put(String key, String value) { + entries.put(key, List.of((T) value)); + } + + static Iterable toIterable(Stream stream) { + return stream::iterator; + } + + @Override + public Iterable> entries() { + return toIterable( + entries.entrySet().stream() + .flatMap( + entry -> + entry.getValue().stream() + .map( + value -> + Map.entry( + entry.getKey(), + asString(value))))); + } + + @Override + public String get(String key) { + return entries.get(key).stream().findFirst().map(JaxrsProxyMap::asString).orElse(null); + } + + private static String asString(Object x) { + if (x == null) { + return null; + } + if (x.getClass() == String.class) { + return (String) x; + } + return x.toString(); + } + + @Override + public void remove(String key) { + entries.remove(key); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + JaxrsProxyMap that = (JaxrsProxyMap) o; + return Objects.equals(entries, that.entries); + } + + @Override + public int hashCode() { + return Objects.hashCode(entries); + } + + @Override + public String toString() { + return entries.toString(); + } +} diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/NamedWasmPlugin.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/NamedWasmPlugin.java new file mode 100644 index 0000000..89de27f --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/NamedWasmPlugin.java @@ -0,0 +1,14 @@ +package io.roastedroot.proxywasm.jaxrs; + +import jakarta.ws.rs.NameBinding; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@NameBinding // Marks this annotation as being used for JAX-RS filtering +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface NamedWasmPlugin { + String value(); +} diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/PluginHandler.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/PluginHandler.java new file mode 100644 index 0000000..cc5bcb3 --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/PluginHandler.java @@ -0,0 +1,283 @@ +package io.roastedroot.proxywasm.jaxrs; + +import static io.roastedroot.proxywasm.Helpers.bytes; +import static io.roastedroot.proxywasm.WellKnownProperties.PLUGIN_NAME; +import static io.roastedroot.proxywasm.WellKnownProperties.PLUGIN_VM_ID; + +import io.roastedroot.proxywasm.ChainedHandler; +import io.roastedroot.proxywasm.Handler; +import io.roastedroot.proxywasm.LogLevel; +import io.roastedroot.proxywasm.MetricType; +import io.roastedroot.proxywasm.ProxyMap; +import io.roastedroot.proxywasm.WasmException; +import io.roastedroot.proxywasm.WasmResult; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +class PluginHandler extends ChainedHandler { + + // ////////////////////////////////////////////////////////////////////// + // Filter Chain Methods + // ////////////////////////////////////////////////////////////////////// + private Handler next; + + PluginHandler() { + this(new Handler() {}); + } + + PluginHandler(Handler next) { + this.next = next; + } + + @Override + protected Handler next() { + return next; + } + + // ////////////////////////////////////////////////////////////////////// + // Properties + // ////////////////////////////////////////////////////////////////////// + + String name = "default"; + + public String getName() { + return name; + } + + public void setName(String name) { + Objects.requireNonNull(name); + this.name = name; + } + + private final HashMap, byte[]> properties = new HashMap<>(); + + @Override + public byte[] getProperty(List path) throws WasmException { + // TODO: do we need field for vm_id and root_id? + if (PLUGIN_VM_ID.equals(path)) { + return bytes(name); + } + if (PLUGIN_NAME.equals(path)) { + return bytes(name); + } + return properties.get(path); + } + + @Override + public WasmResult setProperty(List path, byte[] value) { + properties.put(path, value); + return WasmResult.OK; + } + + // ////////////////////////////////////////////////////////////////////// + // Logging + // ////////////////////////////////////////////////////////////////////// + + static final boolean DEBUG = "true".equals(System.getenv("DEBUG")); + + @Override + public void log(LogLevel level, String message) throws WasmException { + // TODO: improve + if (DEBUG) { + System.out.println(level + ": " + message); + } + } + + @Override + public LogLevel getLogLevel() throws WasmException { + // TODO: improve + return super.getLogLevel(); + } + + // ////////////////////////////////////////////////////////////////////// + // Timers + // ////////////////////////////////////////////////////////////////////// + + private int tickPeriodMilliseconds; + + public int getTickPeriodMilliseconds() { + return tickPeriodMilliseconds; + } + + @Override + public WasmResult setTickPeriodMilliseconds(int tickPeriodMilliseconds) { + this.tickPeriodMilliseconds = tickPeriodMilliseconds; + return WasmResult.OK; + } + + // ////////////////////////////////////////////////////////////////////// + // Foreign function interface (FFI) + // ////////////////////////////////////////////////////////////////////// + + private byte[] funcCallData = new byte[0]; + + @Override + public byte[] getFuncCallData() { + return this.funcCallData; + } + + @Override + public WasmResult setFuncCallData(byte[] data) { + this.funcCallData = data; + return WasmResult.OK; + } + + // ////////////////////////////////////////////////////////////////////// + // HTTP calls + // ////////////////////////////////////////////////////////////////////// + + public static class HttpCall { + public enum Type { + REGULAR, + DISPATCH + } + + public final int id; + public final Type callType; + public final String uri; + public final Object headers; + public final byte[] body; + public final ProxyMap trailers; + public final int timeoutMilliseconds; + + public HttpCall( + int id, + Type callType, + String uri, + ProxyMap headers, + byte[] body, + ProxyMap trailers, + int timeoutMilliseconds) { + this.id = id; + this.callType = callType; + this.uri = uri; + this.headers = headers; + this.body = body; + this.trailers = trailers; + this.timeoutMilliseconds = timeoutMilliseconds; + } + } + + private final AtomicInteger lastCallId = new AtomicInteger(0); + private final HashMap httpCalls = new HashMap(); + + public HashMap getHttpCalls() { + return httpCalls; + } + + @Override + public int httpCall( + String uri, ProxyMap headers, byte[] body, ProxyMap trailers, int timeoutMilliseconds) + throws WasmException { + var id = lastCallId.incrementAndGet(); + HttpCall value = + new HttpCall( + id, + HttpCall.Type.REGULAR, + uri, + headers, + body, + trailers, + timeoutMilliseconds); + httpCalls.put(id, value); + return id; + } + + @Override + public int dispatchHttpCall( + String upstreamName, + ProxyMap headers, + byte[] body, + ProxyMap trailers, + int timeoutMilliseconds) + throws WasmException { + var id = lastCallId.incrementAndGet(); + HttpCall value = + new HttpCall( + id, + HttpCall.Type.DISPATCH, + upstreamName, + headers, + body, + trailers, + timeoutMilliseconds); + httpCalls.put(id, value); + return id; + } + + // ////////////////////////////////////////////////////////////////////// + // Metrics + // ////////////////////////////////////////////////////////////////////// + + public static class Metric { + + public final int id; + public final MetricType type; + public final String name; + public long value; + + public Metric(int id, MetricType type, String name) { + this.id = id; + this.type = type; + this.name = name; + } + } + + private final AtomicInteger lastMetricId = new AtomicInteger(0); + private HashMap metrics = new HashMap(); + private HashMap metricsByName = new HashMap(); + + @Override + public int defineMetric(MetricType type, String name) throws WasmException { + var id = lastMetricId.incrementAndGet(); + Metric value = new Metric(id, type, name); + metrics.put(id, value); + metricsByName.put(name, value); + return id; + } + + @Override + public long getMetric(int metricId) throws WasmException { + var metric = metrics.get(metricId); + if (metric == null) { + throw new WasmException(WasmResult.NOT_FOUND); + } + return metric.value; + } + + public Metric getMetric(String name) { + return metricsByName.get(name); + } + + @Override + public WasmResult incrementMetric(int metricId, long value) { + var metric = metrics.get(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metric.value += value; + return WasmResult.OK; + } + + @Override + public WasmResult recordMetric(int metricId, long value) { + var metric = metrics.get(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metric.value = value; + return WasmResult.OK; + } + + @Override + public WasmResult removeMetric(int metricId) { + Metric metric = metrics.remove(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metricsByName.remove(metric.name); + 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 new file mode 100644 index 0000000..e1bf473 --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java @@ -0,0 +1,212 @@ +package io.roastedroot.proxywasm.jaxrs; + +import io.roastedroot.proxywasm.Action; +import io.roastedroot.proxywasm.HttpContext; +import io.roastedroot.proxywasm.StartException; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ReaderInterceptor; +import jakarta.ws.rs.ext.ReaderInterceptorContext; +import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.ext.WriterInterceptorContext; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@PreMatching +public class ProxyWasmFilter + implements ContainerRequestFilter, + ReaderInterceptor, + WriterInterceptor, + ContainerResponseFilter { + private static final String FILTER_CONTEXT_PROPERTY_NAME = "WasmHttpFilterContext"; + + private final WasmPluginFactory pluginFactory; + + @Inject + public ProxyWasmFilter(WasmPluginFactory pluginFactory) { + this.pluginFactory = pluginFactory; + } + + // 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 PluginHandler pluginHandler; + final HttpHandler handler; + final HttpContext wasm; + + public WasmHttpFilterContext(WasmPlugin plugin) { + this.pluginHandler = plugin.pluginHandler(); + this.handler = new HttpHandler(plugin.pluginHandler()); + this.wasm = plugin.proxyWasm().createHttpContext(this.handler); + } + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + + WasmPlugin plugin = null; + try { + plugin = pluginFactory.create(); + } catch (StartException e) { + requestContext.abortWith( + Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); + } + + var wasmHttpFilterContext = new WasmHttpFilterContext(plugin); + 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()); + } + } + } + + @Override + public Object aroundReadFrom(ReaderInterceptorContext ctx) + throws IOException, WebApplicationException { + + var wasmHttpFilterContext = + (WasmHttpFilterContext) ctx.getProperty(FILTER_CONTEXT_PROPERTY_NAME); + if (wasmHttpFilterContext == null) { + throw new WebApplicationException( + Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); + } + + // the plugin may not be interested in the request body. + if (wasmHttpFilterContext.wasm.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()); + } + + // plugin may have modified the body + ctx.setInputStream(new java.io.ByteArrayInputStream(bytes)); + } + return ctx.proceed(); + } + + @Override + public void filter( + ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + var wasmHttpFilterContext = + (WasmHttpFilterContext) requestContext.getProperty(FILTER_CONTEXT_PROPERTY_NAME); + if (wasmHttpFilterContext == null) { + throw new WebApplicationException( + Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()); + } + + // 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()); + } + } + } + + @Override + public void aroundWriteTo(WriterInterceptorContext ctx) + throws IOException, WebApplicationException { + var wasmHttpFilterContext = + (WasmHttpFilterContext) ctx.getProperty(FILTER_CONTEXT_PROPERTY_NAME); + if (wasmHttpFilterContext == null) { + throw new WebApplicationException( + 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); + } + + // does the plugin want to respond early? + HttpHandler.HttpResponse sendResponse = + wasmHttpFilterContext.handler.getSentHttpResponse(); + if (sendResponse != null) { + throw new WebApplicationException(sendResponse.toResponse()); + } + + // 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(); + } +} 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 new file mode 100644 index 0000000..d76d7c7 --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java @@ -0,0 +1,89 @@ +package io.roastedroot.proxywasm.jaxrs; + +import com.dylibso.chicory.runtime.ImportMemory; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.WasmModule; +import io.roastedroot.proxywasm.ProxyWasm; +import io.roastedroot.proxywasm.StartException; +import java.util.Objects; + +public class WasmPlugin { + + private final ProxyWasm proxyWasm; + private final PluginHandler handler; + + public WasmPlugin(ProxyWasm proxyWasm, PluginHandler handler) { + Objects.requireNonNull(proxyWasm); + Objects.requireNonNull(handler); + this.proxyWasm = proxyWasm; + this.handler = handler; + } + + public String name() { + return handler.getName(); + } + + ProxyWasm proxyWasm() { + return proxyWasm; + } + + PluginHandler pluginHandler() { + return handler; + } + + public static WasmPlugin.Builder builder() { + return new WasmPlugin.Builder(); + } + + public static class Builder implements Cloneable { + + PluginHandler handler = new PluginHandler(); + ProxyWasm.Builder proxyWasmBuilder = ProxyWasm.builder().withPluginHandler(handler); + + public WasmPlugin.Builder withName(String name) { + this.handler.name = name; + return this; + } + + public WasmPlugin.Builder withVmConfig(byte[] vmConfig) { + proxyWasmBuilder = proxyWasmBuilder.withVmConfig(vmConfig); + return this; + } + + public WasmPlugin.Builder withVmConfig(String vmConfig) { + proxyWasmBuilder = proxyWasmBuilder.withVmConfig(vmConfig); + return this; + } + + public WasmPlugin.Builder withPluginConfig(byte[] pluginConfig) { + proxyWasmBuilder = proxyWasmBuilder.withPluginConfig(pluginConfig); + return this; + } + + public WasmPlugin.Builder withPluginConfig(String pluginConfig) { + proxyWasmBuilder = proxyWasmBuilder.withPluginConfig(pluginConfig); + return this; + } + + public WasmPlugin.Builder withImportMemory(ImportMemory memory) { + proxyWasmBuilder = proxyWasmBuilder.withImportMemory(memory); + return this; + } + + public WasmPlugin build(WasmModule module) throws StartException { + return build(proxyWasmBuilder.build(module)); + } + + public WasmPlugin build(Instance.Builder instanceBuilder) throws StartException { + return build(proxyWasmBuilder.build(instanceBuilder)); + } + + public WasmPlugin build(Instance instance) throws StartException { + return build(proxyWasmBuilder.build(instance)); + } + + public WasmPlugin build(ProxyWasm proxyWasm) throws StartException { + return new WasmPlugin(proxyWasm, handler); + } + } +} diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFactory.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFactory.java new file mode 100644 index 0000000..b298fc4 --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFactory.java @@ -0,0 +1,7 @@ +package io.roastedroot.proxywasm.jaxrs; + +import io.roastedroot.proxywasm.StartException; + +public interface WasmPluginFactory { + WasmPlugin create() throws StartException; +} 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 new file mode 100644 index 0000000..0220db2 --- /dev/null +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java @@ -0,0 +1,49 @@ +package io.roastedroot.proxywasm.jaxrs; + +import io.roastedroot.proxywasm.StartException; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ext.Provider; +import java.util.HashMap; + +@Provider +public class WasmPluginFeature implements DynamicFeature { + + private HashMap plugins = new HashMap<>(); + + @Inject + 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()); + } + this.plugins.put(plugin.name(), factory); + } + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + + var resourceMethod = resourceInfo.getResourceMethod(); + if (resourceMethod != null) { + NamedWasmPlugin pluignNameAnnotation = + resourceMethod.getAnnotation(NamedWasmPlugin.class); + if (pluignNameAnnotation == null) { + // If no annotation on method, check the class level + pluignNameAnnotation = + resourceInfo.getResourceClass().getAnnotation(NamedWasmPlugin.class); + } + if (pluignNameAnnotation != null) { + WasmPluginFactory factory = plugins.get(pluignNameAnnotation.value()); + if (factory != null) { + context.register(new ProxyWasmFilter(factory)); + } + } + } + } +} diff --git a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/App.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/App.java new file mode 100644 index 0000000..fd8d574 --- /dev/null +++ b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/App.java @@ -0,0 +1,34 @@ +package io.roastedroot.proxywasm.jaxrs; + +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.WasmModule; +import io.roastedroot.proxywasm.StartException; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.nio.file.Path; + +@ApplicationScoped +public class App { + + private static final WasmModule httpHeadersModule = + Parser.parse( + Path.of("../proxy-wasm-java-host/src/test/go-examples/http_headers/main.wasm")); + + @Produces + public WasmPluginFactory createFoo() throws StartException { + return () -> + WasmPlugin.builder() + .withName("foo") + .withPluginConfig("{\"header\": \"x-wasm-header\", \"value\": \"foo\"}") + .build(httpHeadersModule); + } + + @Produces + public WasmPluginFactory createBar() throws StartException { + return () -> + WasmPlugin.builder() + .withName("bar") + .withPluginConfig("{\"header\": \"x-wasm-header\", \"value\": \"bar\"}") + .build(httpHeadersModule); + } +} diff --git a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/ExampleResource.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/ExampleResource.java new file mode 100644 index 0000000..c07393b --- /dev/null +++ b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/ExampleResource.java @@ -0,0 +1,22 @@ +package io.roastedroot.proxywasm.jaxrs; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/example") +public class ExampleResource { + + @GET + @NamedWasmPlugin("bar") + @Path("/bar") + public String bar() { + return "bar"; + } + + @GET + @NamedWasmPlugin("foo") + @Path("/foo") + public String foo() { + return "foo"; + } +} diff --git a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/ExampleResourceTest.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/ExampleResourceTest.java new file mode 100644 index 0000000..d5398b8 --- /dev/null +++ b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/ExampleResourceTest.java @@ -0,0 +1,26 @@ +package io.roastedroot.proxywasm.jaxrs; + +import static io.restassured.RestAssured.given; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class ExampleResourceTest { + + @Test + public void testFooBar() { + given().when() + .get("/example/foo") + .then() + .statusCode(200) + .header("x-proxy-wasm-go-sdk-example", "http_headers") + .header("x-wasm-header", "foo"); + given().when() + .get("/example/bar") + .then() + .statusCode(200) + .header("x-proxy-wasm-go-sdk-example", "http_headers") + .header("x-wasm-header", "bar"); + } +} diff --git a/proxy-wasm-jaxrs/src/test/resources/application.properties b/proxy-wasm-jaxrs/src/test/resources/application.properties new file mode 100644 index 0000000..e69de29