Skip to content

Commit

Permalink
Merge pull request #118 from AikidoSec/AIK-4267
Browse files Browse the repository at this point in the history
AIK-4267 Add full Spring Webflux support
  • Loading branch information
willem-delbare authored Jan 31, 2025
2 parents 2e6a151 + bc87dd8 commit 692fdd4
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 170 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ Zen operates autonomously on the same server as your Java app to:
### Web frameworks
#### Java
*[`Spring MVC`](docs/spring.md) 3.x
*[`Spring Webflux`](docs/spring_webflux.md) 3.x
*[`Javalin`](docs/javalin.md) 6.x
* 🚧 [`Spring Webflux`](docs/spring_webflux.md) 3.x

#### Kotlin
*[`Spring MVC`](docs/spring.md) 3.x
* 🚧 [`Spring Webflux`](docs/spring_webflux.md) 3.x
* [`Spring Webflux`](docs/spring_webflux.md) 3.x
* 🚧 `Ktor`

#### Groovy
*[`Spring MVC`](docs/spring.md) 3.x
* 🚧 [`Spring Webflux`](docs/spring_webflux.md) 3.x
* [`Spring Webflux`](docs/spring_webflux.md) 3.x

#### 🚧 Scala
* 🚧 `Akka`
Expand Down
1 change: 1 addition & 0 deletions agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
compileOnly 'jakarta.servlet:jakarta.servlet-api:6.1.0' // For Context object (Spring Boot)
compileOnly 'io.projectreactor.netty:reactor-netty-http:1.2.1' // For Spring Webflux
compileOnly 'io.javalin:javalin:6.4.0'
compileOnly 'org.springframework:spring-web:5.3.20'
}

shadowJar {
Expand Down
5 changes: 4 additions & 1 deletion agent/src/main/java/dev/aikido/agent/Wrappers.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import dev.aikido.agent.wrappers.jdbc.MariaDBWrapper;
import dev.aikido.agent.wrappers.jdbc.MysqlCJWrapper;
import dev.aikido.agent.wrappers.jdbc.PostgresWrapper;
import dev.aikido.agent.wrappers.spring.SpringWebfluxWrapper;
import dev.aikido.agent.wrappers.spring.SpringControllerWrapper;
import dev.aikido.agent.wrappers.spring.SpringMVCWrapper;

import java.util.Arrays;
import java.util.List;
Expand All @@ -17,6 +20,7 @@ private Wrappers() {}
public static final List<Wrapper> WRAPPERS = Arrays.asList(
new PostgresWrapper(),
new SpringMVCWrapper(),
new SpringWebfluxWrapper(),
new SpringControllerWrapper(),
new FileConstructorSingleArgumentWrapper(),
new FileConstructorMultiArgumentWrapper(),
Expand All @@ -33,7 +37,6 @@ private Wrappers() {}
new ApacheHttpClientWrapper(),
new PathWrapper(),
new PathsWrapper(),
new NettyWrapper(),
new JavalinWrapper(),
new JavalinDataWrapper(),
new JavalinContextClearWrapper()
Expand Down
77 changes: 0 additions & 77 deletions agent/src/main/java/dev/aikido/agent/wrappers/NettyWrapper.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.aikido.agent.wrappers;
package dev.aikido.agent.wrappers.spring;

import dev.aikido.agent.wrappers.Wrapper;
import dev.aikido.agent_api.collectors.SpringAnnotationCollector;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

import java.lang.annotation.Annotation;
import java.lang.reflect.Executable;
import java.lang.reflect.Parameter;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.aikido.agent.wrappers;
package dev.aikido.agent.wrappers.spring;

import dev.aikido.agent.wrappers.Wrapper;
import dev.aikido.agent_api.collectors.WebRequestCollector;
import dev.aikido.agent_api.collectors.WebResponseCollector;
import dev.aikido.agent_api.context.ContextObject;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package dev.aikido.agent.wrappers.spring;

import dev.aikido.agent.wrappers.Wrapper;
import dev.aikido.agent_api.collectors.WebRequestCollector;
import dev.aikido.agent_api.collectors.WebResponseCollector;
import dev.aikido.agent_api.context.ContextObject;
import dev.aikido.agent_api.context.SpringWebfluxContextObject;
import dev.aikido.agent_api.helpers.logging.LogManager;
import dev.aikido.agent_api.helpers.logging.Logger;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono;

import java.lang.reflect.Executable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
import static net.bytebuddy.matcher.ElementMatchers.*;

/**
* Wraps handle() function on HttpWebHandlerAdapter for Spring Webflux.
* Creates context object, writes a response (e.g. ip blocking), and reports status code.
* [github link](https://github.com/spring-projects/spring-framework/blob/7405e2069098400a01ee1e84ce72c45c6498b28d/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java#L269)
*/
public class SpringWebfluxWrapper implements Wrapper {
public static final Logger logger = LogManager.getLogger(SpringWebfluxWrapper.class);

@Override
public String getName() {
return SpringWebfluxAdvice.class.getName();
}

@Override
public ElementMatcher<? super MethodDescription> getMatcher() {

return isDeclaredBy(getTypeMatcher()).and(named("handle"))
.and(takesArgument(0, nameContains("ServerHttpRequest")))
.and(takesArgument(1, nameContains("ServerHttpResponse")));
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return nameContainsIgnoreCase("org.springframework.web.server.adapter.HttpWebHandlerAdapter");
}

public record SkipOnWrapper(Mono<Void> newReturnValue) {
}

public static class SpringWebfluxAdvice {
@Advice.OnMethodEnter(skipOn = SkipOnWrapper.class, suppress = Throwable.class)
public static Object onEnter(
@Advice.Origin Executable method,
@Advice.Argument(value = 0, typing = DYNAMIC, optional = true) ServerHttpRequest req,
@Advice.Argument(value = 1, typing = DYNAMIC, optional = true) ServerHttpResponse res
) {
if (req == null) {
return null;
}
// Extract headers & query parameters :
Set<Map.Entry<String, List<String>>> headerEntries = req.getHeaders().entrySet();
Map<String, List<String>> query = req.getQueryParams();

// Extract cookies :
HashMap<String, List<String>> cookieMap = new HashMap<>();
for (Map.Entry<String, List<HttpCookie>> entry : req.getCookies().entrySet()) {
List<String> values = entry.getValue().stream().map(HttpCookie::getValue).collect(Collectors.toList());
cookieMap.put(entry.getKey(), values);
}

// Create context object :
ContextObject context = new SpringWebfluxContextObject(
req.getMethod().toString(), req.getURI().toString(),
Objects.requireNonNull(req.getRemoteAddress()),
cookieMap, query, req.getHeaders().toSingleValueMap()
);

// If the request gets blocked (e.g. IP Blocking), write a response here :
WebRequestCollector.Res zenResponse = WebRequestCollector.report(context);
if (zenResponse != null && res != null) {
// Write message :
DataBufferFactory dataBufferFactory = res.bufferFactory();
DataBuffer dataBuffer = dataBufferFactory.wrap(zenResponse.msg().getBytes(StandardCharsets.UTF_8));

res.setRawStatusCode(zenResponse.status()); // Set status code
return new SkipOnWrapper(res.writeWith(Mono.just(dataBuffer)));
}

return res; // Return to analyze status code in OnMethodExit.
}

/** onExit()
* We can use @Advice.Return to overwrite the returned value of handle(...) i.e. to block requests.
*/
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(
@Advice.Enter Object enterResult,
@Advice.Return(readOnly = false) Mono<Void> returnValue
) {
// enterResult can be two things : Either the SkipOnWrapper or the ServerHttpResponse
// ServerHttpResponse -> Extract status code.
// SkipOnWrapper -> we blocked a request (e.g. IP Blocking), and are returning the value below
if (enterResult instanceof SkipOnWrapper wrapper && wrapper.newReturnValue() != null) {
returnValue = wrapper.newReturnValue();
} else if (enterResult instanceof ServerHttpResponse res) {
// Report status code of response :
Integer statusCode = res.getRawStatusCode();
if (statusCode != null) {
WebResponseCollector.report(statusCode);
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package dev.aikido.agent_api.collectors;

import dev.aikido.agent_api.context.Context;
import dev.aikido.agent_api.context.NettyContext;
import dev.aikido.agent_api.context.SpringContextObject;
import dev.aikido.agent_api.context.SpringMVCContextObject;

import java.lang.annotation.Annotation;
import java.lang.reflect.Parameter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package dev.aikido.agent_api.context;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;

import static dev.aikido.agent_api.helpers.net.ProxyForwardedParser.getIpFromRequest;
import static dev.aikido.agent_api.helpers.url.BuildRouteFromUrl.buildRouteFromUrl;

public class NettyContext extends SpringContextObject {
public NettyContext(
public class SpringWebfluxContextObject extends SpringContextObject {
public SpringWebfluxContextObject(
String method, String uri, InetSocketAddress rawIp,
HashMap<String, List<String>> cookies,
Map<String, List<String>> query,
List<Map.Entry<String, String>> headerEntries
Map<String, String> headerEntries

) {
this.method = method;
Expand All @@ -25,15 +22,15 @@ public NettyContext(

this.route = buildRouteFromUrl(this.url);
this.remoteAddress = getIpFromRequest(rawIp.getAddress().getHostAddress(), this.headers);
this.source = "ReactorNetty";
this.source = "SpringWebflux";
this.redirectStartNodes = new ArrayList<>();
}

private static HashMap<String, String> extractHeaders(List<Map.Entry<String, String>> entries) {
HashMap<String, String> headers = new HashMap<>();
for(Map.Entry<String, String> entry: entries) {
headers.put(entry.getKey().toLowerCase(), entry.getValue());
private static HashMap<String, String> extractHeaders(Map<String, String> map) {
HashMap<String, String> newMap = new HashMap<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
newMap.put(entry.getKey().toLowerCase(), entry.getValue());
}
return headers;
};
return newMap;
}
}
Loading

0 comments on commit 692fdd4

Please sign in to comment.