diff --git a/README.md b/README.md index ba7a234..f92259a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ To build the project, you need to have Maven installed. You can build the projec mvn clean install ``` -## Quarkus Example +## Quarkus Example The `quarkus-proxy-wasm-example` directory contains a simple example of how to use the proxy-wasm Java host with Quarkus. @@ -35,7 +35,7 @@ public class Resources { } ``` -The `WasmPlugin` annotation is used to specify the name of the plugin to be used for filtering. +The `WasmPlugin` annotation is used to specify the name of the plugin to be used for filtering. ```java package io.roastedroot.proxywasm.jaxrs.example; diff --git a/pom.xml b/pom.xml index e255f7e..ef2894c 100644 --- a/pom.xml +++ b/pom.xml @@ -234,6 +234,7 @@ quarkus-proxy-wasm quarkus-proxy-wasm-example + quarkus-x-corazawaf-example 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 fe5af09..7008775 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 @@ -1,6 +1,8 @@ package io.roastedroot.proxywasm; import com.dylibso.chicory.runtime.HostFunction; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; @@ -45,20 +47,31 @@ public static byte[] bytes(Duration value) { } 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 - }; + return ByteBuffer.allocate(Integer.BYTES) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(value) + .array(); + } + + public static byte[] bytes(long value) { + return ByteBuffer.allocate(Long.BYTES) + .order(ByteOrder.LITTLE_ENDIAN) + .putLong(value) + .array(); } 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); + return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + public static long int64(byte[] bytes) { + if (bytes == null || bytes.length != 8) { + throw new IllegalArgumentException("Byte array must be exactly 8 bytes long"); + } + return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getLong(); } public static String string(byte[] value) { diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/LogHandler.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/LogHandler.java index 511ae55..c096105 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/LogHandler.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/LogHandler.java @@ -4,6 +4,14 @@ public interface LogHandler { LogHandler DEFAULT = new LogHandler() {}; + LogHandler SYSTEM = + new LogHandler() { + @Override + public void log(LogLevel level, String message) throws WasmException { + System.out.println(level + ": " + message); + } + }; + default void log(LogLevel level, String message) throws WasmException {} default LogLevel getLogLevel() throws WasmException { diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/SplitProxyMap.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/SplitProxyMap.java new file mode 100644 index 0000000..8b29147 --- /dev/null +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/SplitProxyMap.java @@ -0,0 +1,60 @@ +package io.roastedroot.proxywasm; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * A ProxyMap implementation that chains to another ProxyMap instance. + */ +public class SplitProxyMap implements ProxyMap { + + private final ProxyMap primary; + private final ProxyMap secondary; + + public SplitProxyMap(ProxyMap primary, ProxyMap secondary) { + this.primary = primary; + this.secondary = secondary; + } + + @Override + public int size() { + return primary.size() + secondary.size(); + } + + @Override + public void add(String key, String value) { + secondary.add(key, value); + } + + @Override + public void put(String key, String value) { + if (primary.get(key) != null) { + primary.put(key, value); + } + secondary.put(key, value); + } + + @Override + public Iterable> entries() { + return Stream.concat( + StreamSupport.stream(primary.entries().spliterator(), false), + StreamSupport.stream(secondary.entries().spliterator(), false)) + .collect(Collectors.toList()); + } + + @Override + public String get(String key) { + String value = primary.get(key); + if (value != null) { + return value; + } + return secondary.get(key); + } + + @Override + public void remove(String key) { + secondary.remove(key); + } +} diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownHeaders.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownHeaders.java index 7bab923..dfdd842 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownHeaders.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/WellKnownHeaders.java @@ -10,4 +10,5 @@ private WellKnownHeaders() {} public static final String AUTHORITY = ":authority"; public static final String PATH = ":path"; public static final String METHOD = ":method"; + public static final String STATUS = ":status"; } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java index cd8b2d1..ce91abf 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java @@ -79,12 +79,16 @@ public byte[] getHttpRequestBody() { return httpRequestBody; } + public void setHttpRequestBody(byte[] httpRequestBody) { + this.httpRequestBody = httpRequestBody; + } + public byte[] getHttpResponseBody() { return httpResponseBody; } - public void setHttpRequestBody(byte[] httpRequestBody) { - this.httpRequestBody = httpRequestBody; + public void setHttpResponseBody(byte[] httpResponseBody) { + this.httpResponseBody = httpResponseBody; } public byte[] getGrpcReceiveBuffer() { @@ -111,10 +115,6 @@ public void setDownStreamData(byte[] downStreamData) { this.downStreamData = downStreamData; } - public void setHttpResponseBody(byte[] httpResponseBody) { - this.httpResponseBody = httpResponseBody; - } - public SendResponse getSendResponse() { return sendResponse; } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpRequestAdaptor.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpRequestAdaptor.java index b6237b3..ff920b2 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpRequestAdaptor.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpRequestAdaptor.java @@ -12,11 +12,13 @@ public interface HttpRequestAdaptor { String remoteAddress(); - String remotePort(); + int remotePort(); String localAddress(); - String localPort(); + int localPort(); + + String protocol(); ProxyMap getHttpRequestHeaders(); diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java index a493dbd..8da53b1 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java @@ -29,7 +29,6 @@ import io.roastedroot.proxywasm.WasmException; import io.roastedroot.proxywasm.WasmResult; import java.net.URI; -import java.net.URISyntaxException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -418,11 +417,8 @@ public int httpCall( URI uri = null; try { - uri = - URI.create( - new URI(scheme, null, authority, connectPort, null, null, null) - + path); - } catch (IllegalArgumentException | URISyntaxException e) { + uri = URI.create(scheme + "://" + authority + path); + } catch (IllegalArgumentException e) { throw new WasmException(WasmResult.BAD_ARGUMENT); } diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/http_call.go b/proxy-wasm-java-host/src/test/go-examples/unit_tester/http_call.go index 4af33de..5c2d0ce 100644 --- a/proxy-wasm-java-host/src/test/go-examples/unit_tester/http_call.go +++ b/proxy-wasm-java-host/src/test/go-examples/unit_tester/http_call.go @@ -31,7 +31,7 @@ type httpCallTests struct { } func ( - p *pluginContext) httpCallTests(contextID uint32) types.HttpContext { +p *pluginContext) httpCallTests(contextID uint32) types.HttpContext { return &httpCallTests{ DefaultHttpContext: types.DefaultHttpContext{}, contextID: contextID, @@ -42,12 +42,8 @@ func ( func (ctx *httpCallTests) OnHttpRequestHeaders(int, bool) types.Action { proxywasm.LogDebug("OnHttpRequestHeaders") - var err error - ctx.headers, err = proxywasm.GetHttpRequestHeaders() - if err != nil { - proxywasm.LogCriticalf("failed to get request headers: %v", err) - } + ctx.headers = nil method, err := proxywasm.GetProperty([]string{"request", "method"}) if err != nil { proxywasm.LogCriticalf("failed to get request method: %v", err) @@ -63,6 +59,11 @@ func (ctx *httpCallTests) OnHttpRequestHeaders(int, bool) types.Action { path := gjson.Get(ctx.pluginContext.config, "path").Str ctx.headers = append(ctx.headers, [2]string{":path", path}) + requestHeaders, err := proxywasm.GetHttpRequestHeaders() + if err != nil { + proxywasm.LogCriticalf("failed to get request headers: %v", err) + } + ctx.headers = append(ctx.headers, requestHeaders...) return types.ActionContinue } diff --git a/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.wasm b/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.wasm index bac5dba..fcb00e6 100644 Binary files a/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.wasm and b/proxy-wasm-java-host/src/test/go-examples/unit_tester/main.wasm differ diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsHttpRequestAdaptor.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsHttpRequestAdaptor.java index 1b64dab..cebd165 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsHttpRequestAdaptor.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/JaxrsHttpRequestAdaptor.java @@ -41,9 +41,12 @@ import static io.roastedroot.proxywasm.WellKnownProperties.SOURCE_ADDRESS; import static io.roastedroot.proxywasm.WellKnownProperties.SOURCE_PORT; +import io.roastedroot.proxywasm.ArrayProxyMap; import io.roastedroot.proxywasm.ProxyMap; +import io.roastedroot.proxywasm.SplitProxyMap; import io.roastedroot.proxywasm.WasmException; import io.roastedroot.proxywasm.WasmResult; +import io.roastedroot.proxywasm.WellKnownHeaders; import io.roastedroot.proxywasm.WellKnownProperties; import io.roastedroot.proxywasm.plugin.HttpContext; import io.roastedroot.proxywasm.plugin.HttpRequestAdaptor; @@ -84,8 +87,8 @@ public String remoteAddress() { } @Override - public String remotePort() { - return ""; + public int remotePort() { + return 0; } @Override @@ -94,8 +97,20 @@ public String localAddress() { } @Override - public String localPort() { - return ""; + public int localPort() { + return 0; + } + + @Override + public String protocol() { + String protocol = requestContext.getUriInfo().getRequestUri().getScheme(); + if (protocol == null) { + protocol = "HTTP"; + } + protocol = protocol.toUpperCase(); + // technically it should be something like "HTTP/1.1" or "HTTP/2" (but JAX-RS doesn't give + // us that info). + return protocol; } // ////////////////////////////////////////////////////////////////////// @@ -104,7 +119,21 @@ public String localPort() { @Override public ProxyMap getHttpRequestHeaders() { - return new MultivaluedMapAdaptor<>(requestContext.getHeaders()); + URI requestUri = requestContext.getUriInfo().getRequestUri(); + ArrayProxyMap wellKnownHeaders = new ArrayProxyMap(); + wellKnownHeaders.add(WellKnownHeaders.AUTHORITY, requestUri.getAuthority()); + wellKnownHeaders.add(WellKnownHeaders.SCHEME, requestUri.getScheme()); + wellKnownHeaders.add(WellKnownHeaders.METHOD, requestContext.getMethod()); + var path = requestUri.getRawPath(); + if (path == null) { + path = ""; + } + if (requestUri.getRawQuery() != null) { + path += "?" + requestUri.getRawQuery(); + } + wellKnownHeaders.add(WellKnownHeaders.PATH, path); + return new SplitProxyMap( + wellKnownHeaders, new MultivaluedMapAdaptor<>(requestContext.getHeaders())); } @Override @@ -114,7 +143,10 @@ public ProxyMap getHttpRequestTrailers() { @Override public ProxyMap getHttpResponseHeaders() { - return new MultivaluedMapAdaptor<>(responseContext.getHeaders()); + ArrayProxyMap wellKnownHeaders = new ArrayProxyMap(); + wellKnownHeaders.add(WellKnownHeaders.STATUS, "" + responseContext.getStatus()); + return new SplitProxyMap( + wellKnownHeaders, new MultivaluedMapAdaptor<>(responseContext.getHeaders())); } @Override @@ -144,11 +176,17 @@ public byte[] getProperty(HttpContext pluginRequest, List path) throws W } else if (SOURCE_ADDRESS.equals(path)) { return bytes(remoteAddress()); } else if (SOURCE_PORT.equals(path)) { - return bytes(remotePort()); + // Port attribute is populated as uint64 (8 byte) + // Ref: + // https://github.com/envoyproxy/envoy/blob/1b3da361279a54956f01abba830fc5d3a5421828/source/common/network/utility.cc#L201 + return bytes((long) remotePort()); } else if (DESTINATION_ADDRESS.equals(path)) { return bytes(localAddress()); } else if (DESTINATION_PORT.equals(path)) { - return bytes(localPort()); + // Port attribute is populated as uint64 (8 byte) + // Ref: + // https://github.com/envoyproxy/envoy/blob/1b3da361279a54956f01abba830fc5d3a5421828/source/common/network/utility.cc#L201 + return bytes((long) localPort()); } // TODO: get TLS connection properties @@ -248,10 +286,7 @@ else if (WellKnownProperties.CONNECTION_TLS_VERSION.equals(path)) { // HTTP request properties else if (REQUEST_PROTOCOL.equals(path)) { - if (requestContext == null) { - return null; - } - return bytes(requestContext.getUriInfo().getRequestUri().getScheme()); + return bytes(protocol()); } else if (REQUEST_TIME.equals(path)) { return bytes(new Date(startedAt)); } else if (REQUEST_DURATION.equals(path)) { diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFilter.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFilter.java index d966bbb..c5fa13e 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFilter.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFilter.java @@ -61,6 +61,7 @@ public void filter(ContainerRequestContext requestContext) throws IOException { var sendResponse = httpContext.consumeSentHttpResponse(); if (sendResponse != null) { requestContext.abortWith(toResponse(sendResponse)); + return; } } @@ -84,7 +85,8 @@ public void filter(ContainerRequestContext requestContext) throws IOException { // does the plugin want to respond early? var sendResponse = httpContext.consumeSentHttpResponse(); if (sendResponse != null) { - throw new WebApplicationException(toResponse(sendResponse)); + requestContext.abortWith(toResponse(sendResponse)); + return; } // plugin may have modified the body @@ -128,7 +130,35 @@ public void filter( responseContext.setStatus(response.getStatus()); responseContext.getHeaders().putAll(response.getHeaders()); responseContext.setEntity(response.getEntity()); + return; + } + + // aroundWriteTo won't be called if there is no entity to send. + if (responseContext.getEntity() == null + && httpContext.context().hasOnResponseBody()) { + + byte[] bytes = new byte[0]; + httpContext.setHttpResponseBody(bytes); + action = httpContext.context().callOnResponseBody(true); + bytes = httpContext.getHttpResponseBody(); + if (action == Action.CONTINUE) { + // continue means plugin is done reading the body. + httpContext.setHttpResponseBody(null); + } else { + httpContext.maybePause(); + } + + // does the plugin want to respond early? + sendResponse = httpContext.consumeSentHttpResponse(); + if (sendResponse != null) { + Response response = toResponse(sendResponse); + responseContext.setStatus(response.getStatus()); + responseContext.getHeaders().putAll(response.getHeaders()); + responseContext.setEntity(response.getEntity()); + return; + } } + } finally { // allow other request to use the plugin. httpContext.plugin().unlock(); @@ -159,7 +189,7 @@ public void aroundWriteTo(WriterInterceptorContext ctx) byte[] bytes = baos.toByteArray(); httpContext.setHttpResponseBody(bytes); - var action = httpContext.context().callOnResponseBody(false); + var action = httpContext.context().callOnResponseBody(true); bytes = httpContext.getHttpResponseBody(); if (action == Action.CONTINUE) { // continue means plugin is done reading the body. diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/servlet/ServletJaxrsHttpRequestAdaptor.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/servlet/ServletJaxrsHttpRequestAdaptor.java index 2e84858..fc50b28 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/servlet/ServletJaxrsHttpRequestAdaptor.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/servlet/ServletJaxrsHttpRequestAdaptor.java @@ -20,11 +20,11 @@ public String remoteAddress() { } @Override - public String remotePort() { + public int remotePort() { if (request == null) { - return ""; + return 0; } - return "" + request.getRemotePort(); + return request.getRemotePort(); } @Override @@ -36,10 +36,15 @@ public String localAddress() { } @Override - public String localPort() { + public int localPort() { if (request == null) { - return ""; + return 0; } - return "" + request.getLocalPort(); + return request.getLocalPort(); + } + + @Override + public String protocol() { + return request.getProtocol(); } } diff --git a/quarkus-proxy-wasm/runtime/src/main/java/io/quarkiverse/proxywasm/runtime/VertxHttpRequestAdaptor.java b/quarkus-proxy-wasm/runtime/src/main/java/io/quarkiverse/proxywasm/runtime/VertxHttpRequestAdaptor.java index 21e5097..e46338d 100644 --- a/quarkus-proxy-wasm/runtime/src/main/java/io/quarkiverse/proxywasm/runtime/VertxHttpRequestAdaptor.java +++ b/quarkus-proxy-wasm/runtime/src/main/java/io/quarkiverse/proxywasm/runtime/VertxHttpRequestAdaptor.java @@ -15,21 +15,35 @@ public class VertxHttpRequestAdaptor extends JaxrsHttpRequestAdaptor { @Override public String remoteAddress() { - return request.remoteAddress().hostAddress(); + return request.remoteAddress().hostAddress() + ":" + request.remoteAddress().port(); } @Override - public String remotePort() { - return "" + request.remoteAddress().port(); + public int remotePort() { + return request.remoteAddress().port(); } @Override public String localAddress() { - return request.localAddress().hostAddress(); + return request.localAddress().hostAddress() + ":" + request.localAddress().port(); } @Override - public String localPort() { - return "" + request.localAddress().port(); + public int localPort() { + return request.localAddress().port(); + } + + @Override + public String protocol() { + switch (request.version()) { + case HTTP_1_0: + return "HTTP/1.0"; + case HTTP_1_1: + return "HTTP/1.1"; + case HTTP_2: + return "HTTP/2"; + default: + return "unknown"; + } } } diff --git a/quarkus-x-corazawaf-example/.gitignore b/quarkus-x-corazawaf-example/.gitignore new file mode 100644 index 0000000..91a800a --- /dev/null +++ b/quarkus-x-corazawaf-example/.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/quarkus-x-corazawaf-example/pom.xml b/quarkus-x-corazawaf-example/pom.xml new file mode 100644 index 0000000..1fca7fe --- /dev/null +++ b/quarkus-x-corazawaf-example/pom.xml @@ -0,0 +1,123 @@ + + + 4.0.0 + + + io.roastedroot + proxy-wasm-java-host-parent + 1.0-SNAPSHOT + ../pom.xml + + + quarkus-corazawaf-example + jar + quarkus-corsa-waf-example + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + com.google.code.gson + gson + 2.12.1 + + + io.quarkiverse.proxy-wasm + quarkus-proxy-wasm + ${project.version} + + + io.quarkus + quarkus-arc + true + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + ${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/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Admin.java b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Admin.java new file mode 100644 index 0000000..2a8cb9f --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Admin.java @@ -0,0 +1,8 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import io.roastedroot.proxywasm.jaxrs.WasmPlugin; +import jakarta.ws.rs.Path; + +@Path("/admin") +@WasmPlugin("waf") // use the corsaWAF filter +public class Admin extends Anything {} diff --git a/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Anything.java b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Anything.java new file mode 100644 index 0000000..23ffdd0 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Anything.java @@ -0,0 +1,74 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import io.roastedroot.proxywasm.jaxrs.WasmPlugin; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HEAD; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; + +/** + * This class is a JAX-RS resource that handles various HTTP methods and paths. + * It uses the @WasmPlugin annotation to specify the use of the "waf" filter. + *

+ * All paths and methods are handled by this class and it responds with a simple + * message indicating the method, path, and body (if applicable). + */ +@Path("/anything") +@WasmPlugin("waf") // use the corsaWAF filter +public class Anything { + + @GET + public Response gext(@Context HttpHeaders headers) { + return process(headers, null); + } + + @DELETE + public Response delete(HttpHeaders headers) { + return process(headers, null); + } + + @OPTIONS + public Response options(HttpHeaders headers) { + return process(headers, null); + } + + @HEAD + public Response head(HttpHeaders headers) { + return process(headers, null); + } + + @POST + public Response postx(HttpHeaders headers, String body) { + return process(headers, body); + } + + @PUT + public Response put(HttpHeaders headers, String body) { + return process(headers, body); + } + + @PATCH + public Response patch(HttpHeaders headers, String body) { + return process(headers, body); + } + + private Response process(HttpHeaders headers, String body) { + Response.ResponseBuilder builder = Response.ok(); + for (var header : headers.getRequestHeaders().entrySet()) { + for (String value : header.getValue()) { + builder = builder.header(header.getKey(), value); + } + } + if (body != null) { + builder = builder.entity(body); + } + return builder.build(); + } +} diff --git a/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/App.java b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/App.java new file mode 100644 index 0000000..f481fb3 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/App.java @@ -0,0 +1,45 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.WasmModule; +import io.roastedroot.proxywasm.LogHandler; +import io.roastedroot.proxywasm.StartException; +import io.roastedroot.proxywasm.plugin.Plugin; +import io.roastedroot.proxywasm.plugin.PluginFactory; +import io.roastedroot.proxywasm.plugin.SimpleMetricsHandler; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +@ApplicationScoped +public class App { + + private static WasmModule module = + Parser.parse(App.class.getResourceAsStream("coraza-proxy-wasm.wasm")); + + static final String CONFIG; + + static { + try (InputStream is = App.class.getResourceAsStream("waf-config.json")) { + CONFIG = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + static final boolean DEBUG = "true".equals(System.getenv("DEBUG")); + + @Produces + public PluginFactory waf() throws StartException { + return () -> + Plugin.builder() + .withName("waf") + .withLogger(DEBUG ? LogHandler.SYSTEM : null) + .withPluginConfig(CONFIG) + .withMetricsHandler(new SimpleMetricsHandler()) + .build(module); + } +} diff --git a/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Status.java b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Status.java new file mode 100644 index 0000000..ebbef1c --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/java/io/roastedroot/proxywasm/corazawaf/example/Status.java @@ -0,0 +1,53 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import io.roastedroot.proxywasm.jaxrs.WasmPlugin; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HEAD; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; + +@Path("/status/{status}") +@WasmPlugin("waf") // use the corsaWAF filter +public class Status { + + @GET + public Response gext(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @DELETE + public Response delete(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @OPTIONS + public Response options(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @HEAD + public Response head(@PathParam("status") int status) { + return Response.status(status).build(); + } + + @POST + public Response postx(@PathParam("status") int status, String body) { + return Response.status(status).build(); + } + + @PUT + public Response put(@PathParam("status") int status, String body) { + return Response.status(status).build(); + } + + @PATCH + public Response patch(@PathParam("status") int status, String body) { + return Response.status(status).build(); + } +} diff --git a/quarkus-x-corazawaf-example/src/main/resources/application.properties b/quarkus-x-corazawaf-example/src/main/resources/application.properties new file mode 100644 index 0000000..6e516f2 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.log.level=INFO +quarkus.log.category."org.hibernate".level=DEBUG diff --git a/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/README.md b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/README.md new file mode 100644 index 0000000..edf5116 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/README.md @@ -0,0 +1,6 @@ +## Attribution + +The coraza-proxy-wasm.wasm plugin comes from: + +https://github.com/corazawaf/coraza-proxy-wasm/releases/tag/0.5.0 + diff --git a/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/coraza-proxy-wasm.wasm b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/coraza-proxy-wasm.wasm new file mode 100644 index 0000000..564ef58 Binary files /dev/null and b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/coraza-proxy-wasm.wasm differ diff --git a/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/waf-config.json b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/waf-config.json new file mode 100644 index 0000000..4f3608d --- /dev/null +++ b/quarkus-x-corazawaf-example/src/main/resources/io/roastedroot/proxywasm/corazawaf/example/waf-config.json @@ -0,0 +1,33 @@ +{ + "directives_map": { + "rs1": [ + "Include @demo-conf", + "Include @crs-setup-conf", + "SecDefaultAction \"phase:3,log,auditlog,pass\"", + "SecDefaultAction \"phase:4,log,auditlog,pass\"", + "SecDefaultAction \"phase:5,log,auditlog,pass\"", + "SecDebugLogLevel 9", + "Include @owasp_crs/*.conf", + "SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\" \nSecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny\" \nSecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny\" \nSecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny\"" + ], + "rs2": [ + "Include @demo-conf", + "Include @crs-setup-conf", + "SecDefaultAction \"phase:3,log,auditlog,pass\"", + "SecDefaultAction \"phase:4,log,auditlog,pass\"", + "SecDefaultAction \"phase:5,log,auditlog,pass\"", + "SecDebugLogLevel 9", + "Include @owasp_crs/*.conf", + "SecRule REQUEST_URI \"@streq /example\" \"id:101,phase:1,t:lowercase,deny\" \nSecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny\" \nSecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny\" \nSecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny\"" + ] + }, + "default_directives": "rs1", + "metric_labels": { + "owner": "coraza", + "identifier": "global" + }, + "per_authority_directives":{ + "foo.example.com":"rs2", + "bar.example.com":"rs2" + } +} \ No newline at end of file diff --git a/quarkus-x-corazawaf-example/src/test/java/io/roastedroot/proxywasm/corazawaf/example/ResourcesTest.java b/quarkus-x-corazawaf-example/src/test/java/io/roastedroot/proxywasm/corazawaf/example/ResourcesTest.java new file mode 100644 index 0000000..ca1c138 --- /dev/null +++ b/quarkus-x-corazawaf-example/src/test/java/io/roastedroot/proxywasm/corazawaf/example/ResourcesTest.java @@ -0,0 +1,148 @@ +package io.roastedroot.proxywasm.corazawaf.example; + +import static io.restassured.RestAssured.given; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +// Derived from: +// https://github.com/corazawaf/coraza-proxy-wasm?tab=readme-ov-file#manual-requests +@QuarkusTest +public class ResourcesTest { + + // # True positive requests: + + @Test + public void testPhase1() throws InterruptedException { + + // # Custom rule phase 1 + // curl -I 'http://localhost:8080/admin' + given().when().get("/admin").then().statusCode(403); + } + + @Test + // @Disabled("not yet working.") + public void testPhase2() throws InterruptedException { + + // # Custom rule phase 2 + // curl -i -X POST 'http://localhost:8080/anything' --data "maliciouspayload" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("maliciouspayload") + .when() + .post("/anything") + .then() + .statusCode(403); + } + + @Test + public void testPhase3() throws InterruptedException { + // # Custom rule phase 3 + // curl -I 'http://localhost:8080/status/406' + given().when().get("/status/406").then().statusCode(403); + } + + @Test + @Disabled( + "Seems like coraza is not loading the reponse body. it logs: 'Skipping response body" + + " processing tx_id=\"xxxx\" response_body_access=true'.") + public void testPhase4() throws InterruptedException { + + // # Custom rule phase 4 + // curl -i -X POST 'http://localhost:8080/anything' --data "responsebodycode" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("responsebodycode") + .when() + .post("/anything") + .then() + .statusCode(403); + } + + @Test + public void testXssPhase1() throws InterruptedException { + // # XSS phase 1 + // curl -I 'http://localhost:8080/anything?arg=' + given().when().get("/anything?arg=").then().statusCode(403); + } + + @Test + public void testSQLIPhase2() throws InterruptedException { + + // # SQLI phase 2 (reading the body request) + // curl -i -X POST 'http://localhost:8080/anything' --data "1%27%20ORDER%20BY%203--%2B" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("1%27%20ORDER%20BY%203--%2B") + .when() + .post("/anything") + .then() + .statusCode(403); + } + + @Test + public void testCRSScannerDetectionRule() throws InterruptedException { + // # Triggers a CRS scanner detection rule (913100) + // curl -I --user-agent "zgrab/0.1 (X11; U; Linux i686; en-US; rv:1.7)" + // -H "Host: localhost" + // -H "Accept: + // text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" + // localhost:8080 + given().when() + .header("User-Agent", "zgrab/0.1 (X11; U; Linux i686; en-US; rv:1.7)") + .header("Host", "localhost") + .header( + "Accept", + "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5") + .get("/anything") + .then() + .statusCode(403); + } + + @Test + public void testNegativeRequest1() throws InterruptedException { + // # True negative requests: + // # A GET request with a harmless argument + // curl -I 'http://localhost:8080/anything?arg=arg_1' + given().when().get("/anything?arg=arg_1").then().statusCode(200); + } + + @Test + public void testNegativeRequest2() throws InterruptedException { + // # A payload (reading the body request) + // curl -i -X POST 'http://localhost:8080/anything' --data "This is a payload" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("This is a payload") + .when() + .post("/anything") + .then() + .statusCode(200); + } + + @Test + public void testNegativeRequest3() throws InterruptedException { + + // # An harmless response body + // curl -i -X POST 'http://localhost:8080/anything' --data "Hello world" + given().header("Content-Type", "application/x-www-form-urlencoded") + .body("Hello world") + .when() + .post("/anything") + .then() + .statusCode(200); + } + + @Test + public void testNegativeRequest4() throws InterruptedException { + + // # An usual user-agent + // curl -I --user-agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like + // Gecko) Chrome/105.0.0.0 Safari/537.36" localhost:8080 + given().when() + .header( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)" + + " Chrome/105.0.0.0 Safari/537.36") + .get("/anything") + .then() + .statusCode(200); + } +}