Skip to content

Commit 692fdd4

Browse files
Merge pull request #118 from AikidoSec/AIK-4267
AIK-4267 Add full Spring Webflux support
2 parents 2e6a151 + bc87dd8 commit 692fdd4

File tree

12 files changed

+213
-170
lines changed

12 files changed

+213
-170
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@ Zen operates autonomously on the same server as your Java app to:
3535
### Web frameworks
3636
#### Java
3737
*[`Spring MVC`](docs/spring.md) 3.x
38+
*[`Spring Webflux`](docs/spring_webflux.md) 3.x
3839
*[`Javalin`](docs/javalin.md) 6.x
39-
* 🚧 [`Spring Webflux`](docs/spring_webflux.md) 3.x
4040

4141
#### Kotlin
4242
*[`Spring MVC`](docs/spring.md) 3.x
43-
* 🚧 [`Spring Webflux`](docs/spring_webflux.md) 3.x
43+
* [`Spring Webflux`](docs/spring_webflux.md) 3.x
4444
* 🚧 `Ktor`
4545

4646
#### Groovy
4747
*[`Spring MVC`](docs/spring.md) 3.x
48-
* 🚧 [`Spring Webflux`](docs/spring_webflux.md) 3.x
48+
* [`Spring Webflux`](docs/spring_webflux.md) 3.x
4949

5050
#### 🚧 Scala
5151
* 🚧 `Akka`

agent/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies {
1010
compileOnly 'jakarta.servlet:jakarta.servlet-api:6.1.0' // For Context object (Spring Boot)
1111
compileOnly 'io.projectreactor.netty:reactor-netty-http:1.2.1' // For Spring Webflux
1212
compileOnly 'io.javalin:javalin:6.4.0'
13+
compileOnly 'org.springframework:spring-web:5.3.20'
1314
}
1415

1516
shadowJar {

agent/src/main/java/dev/aikido/agent/Wrappers.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import dev.aikido.agent.wrappers.jdbc.MariaDBWrapper;
99
import dev.aikido.agent.wrappers.jdbc.MysqlCJWrapper;
1010
import dev.aikido.agent.wrappers.jdbc.PostgresWrapper;
11+
import dev.aikido.agent.wrappers.spring.SpringWebfluxWrapper;
12+
import dev.aikido.agent.wrappers.spring.SpringControllerWrapper;
13+
import dev.aikido.agent.wrappers.spring.SpringMVCWrapper;
1114

1215
import java.util.Arrays;
1316
import java.util.List;
@@ -17,6 +20,7 @@ private Wrappers() {}
1720
public static final List<Wrapper> WRAPPERS = Arrays.asList(
1821
new PostgresWrapper(),
1922
new SpringMVCWrapper(),
23+
new SpringWebfluxWrapper(),
2024
new SpringControllerWrapper(),
2125
new FileConstructorSingleArgumentWrapper(),
2226
new FileConstructorMultiArgumentWrapper(),
@@ -33,7 +37,6 @@ private Wrappers() {}
3337
new ApacheHttpClientWrapper(),
3438
new PathWrapper(),
3539
new PathsWrapper(),
36-
new NettyWrapper(),
3740
new JavalinWrapper(),
3841
new JavalinDataWrapper(),
3942
new JavalinContextClearWrapper()

agent/src/main/java/dev/aikido/agent/wrappers/NettyWrapper.java

Lines changed: 0 additions & 77 deletions
This file was deleted.

agent/src/main/java/dev/aikido/agent/wrappers/SpringControllerWrapper.java renamed to agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringControllerWrapper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
package dev.aikido.agent.wrappers;
1+
package dev.aikido.agent.wrappers.spring;
22

3+
import dev.aikido.agent.wrappers.Wrapper;
34
import dev.aikido.agent_api.collectors.SpringAnnotationCollector;
45
import net.bytebuddy.asm.Advice;
56
import net.bytebuddy.description.method.MethodDescription;
67
import net.bytebuddy.description.type.TypeDescription;
78
import net.bytebuddy.matcher.ElementMatcher;
89

9-
import java.lang.annotation.Annotation;
1010
import java.lang.reflect.Executable;
1111
import java.lang.reflect.Parameter;
1212

agent/src/main/java/dev/aikido/agent/wrappers/SpringMVCWrapper.java renamed to agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCWrapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
package dev.aikido.agent.wrappers;
1+
package dev.aikido.agent.wrappers.spring;
22

3+
import dev.aikido.agent.wrappers.Wrapper;
34
import dev.aikido.agent_api.collectors.WebRequestCollector;
45
import dev.aikido.agent_api.collectors.WebResponseCollector;
56
import dev.aikido.agent_api.context.ContextObject;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package dev.aikido.agent.wrappers.spring;
2+
3+
import dev.aikido.agent.wrappers.Wrapper;
4+
import dev.aikido.agent_api.collectors.WebRequestCollector;
5+
import dev.aikido.agent_api.collectors.WebResponseCollector;
6+
import dev.aikido.agent_api.context.ContextObject;
7+
import dev.aikido.agent_api.context.SpringWebfluxContextObject;
8+
import dev.aikido.agent_api.helpers.logging.LogManager;
9+
import dev.aikido.agent_api.helpers.logging.Logger;
10+
import net.bytebuddy.asm.Advice;
11+
import net.bytebuddy.description.method.MethodDescription;
12+
import net.bytebuddy.description.type.TypeDescription;
13+
import net.bytebuddy.matcher.ElementMatcher;
14+
import org.springframework.core.io.buffer.DataBuffer;
15+
import org.springframework.core.io.buffer.DataBufferFactory;
16+
import org.springframework.http.HttpCookie;
17+
import org.springframework.http.server.reactive.ServerHttpRequest;
18+
import org.springframework.http.server.reactive.ServerHttpResponse;
19+
import reactor.core.publisher.Mono;
20+
21+
import java.lang.reflect.Executable;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.*;
24+
import java.util.stream.Collectors;
25+
26+
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
27+
import static net.bytebuddy.matcher.ElementMatchers.*;
28+
29+
/**
30+
* Wraps handle() function on HttpWebHandlerAdapter for Spring Webflux.
31+
* Creates context object, writes a response (e.g. ip blocking), and reports status code.
32+
* [github link](https://github.com/spring-projects/spring-framework/blob/7405e2069098400a01ee1e84ce72c45c6498b28d/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java#L269)
33+
*/
34+
public class SpringWebfluxWrapper implements Wrapper {
35+
public static final Logger logger = LogManager.getLogger(SpringWebfluxWrapper.class);
36+
37+
@Override
38+
public String getName() {
39+
return SpringWebfluxAdvice.class.getName();
40+
}
41+
42+
@Override
43+
public ElementMatcher<? super MethodDescription> getMatcher() {
44+
45+
return isDeclaredBy(getTypeMatcher()).and(named("handle"))
46+
.and(takesArgument(0, nameContains("ServerHttpRequest")))
47+
.and(takesArgument(1, nameContains("ServerHttpResponse")));
48+
}
49+
50+
@Override
51+
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
52+
return nameContainsIgnoreCase("org.springframework.web.server.adapter.HttpWebHandlerAdapter");
53+
}
54+
55+
public record SkipOnWrapper(Mono<Void> newReturnValue) {
56+
}
57+
58+
public static class SpringWebfluxAdvice {
59+
@Advice.OnMethodEnter(skipOn = SkipOnWrapper.class, suppress = Throwable.class)
60+
public static Object onEnter(
61+
@Advice.Origin Executable method,
62+
@Advice.Argument(value = 0, typing = DYNAMIC, optional = true) ServerHttpRequest req,
63+
@Advice.Argument(value = 1, typing = DYNAMIC, optional = true) ServerHttpResponse res
64+
) {
65+
if (req == null) {
66+
return null;
67+
}
68+
// Extract headers & query parameters :
69+
Set<Map.Entry<String, List<String>>> headerEntries = req.getHeaders().entrySet();
70+
Map<String, List<String>> query = req.getQueryParams();
71+
72+
// Extract cookies :
73+
HashMap<String, List<String>> cookieMap = new HashMap<>();
74+
for (Map.Entry<String, List<HttpCookie>> entry : req.getCookies().entrySet()) {
75+
List<String> values = entry.getValue().stream().map(HttpCookie::getValue).collect(Collectors.toList());
76+
cookieMap.put(entry.getKey(), values);
77+
}
78+
79+
// Create context object :
80+
ContextObject context = new SpringWebfluxContextObject(
81+
req.getMethod().toString(), req.getURI().toString(),
82+
Objects.requireNonNull(req.getRemoteAddress()),
83+
cookieMap, query, req.getHeaders().toSingleValueMap()
84+
);
85+
86+
// If the request gets blocked (e.g. IP Blocking), write a response here :
87+
WebRequestCollector.Res zenResponse = WebRequestCollector.report(context);
88+
if (zenResponse != null && res != null) {
89+
// Write message :
90+
DataBufferFactory dataBufferFactory = res.bufferFactory();
91+
DataBuffer dataBuffer = dataBufferFactory.wrap(zenResponse.msg().getBytes(StandardCharsets.UTF_8));
92+
93+
res.setRawStatusCode(zenResponse.status()); // Set status code
94+
return new SkipOnWrapper(res.writeWith(Mono.just(dataBuffer)));
95+
}
96+
97+
return res; // Return to analyze status code in OnMethodExit.
98+
}
99+
100+
/** onExit()
101+
* We can use @Advice.Return to overwrite the returned value of handle(...) i.e. to block requests.
102+
*/
103+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
104+
public static void onExit(
105+
@Advice.Enter Object enterResult,
106+
@Advice.Return(readOnly = false) Mono<Void> returnValue
107+
) {
108+
// enterResult can be two things : Either the SkipOnWrapper or the ServerHttpResponse
109+
// ServerHttpResponse -> Extract status code.
110+
// SkipOnWrapper -> we blocked a request (e.g. IP Blocking), and are returning the value below
111+
if (enterResult instanceof SkipOnWrapper wrapper && wrapper.newReturnValue() != null) {
112+
returnValue = wrapper.newReturnValue();
113+
} else if (enterResult instanceof ServerHttpResponse res) {
114+
// Report status code of response :
115+
Integer statusCode = res.getRawStatusCode();
116+
if (statusCode != null) {
117+
WebResponseCollector.report(statusCode);
118+
}
119+
}
120+
}
121+
}
122+
123+
}

agent_api/src/main/java/dev/aikido/agent_api/collectors/SpringAnnotationCollector.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package dev.aikido.agent_api.collectors;
22

33
import dev.aikido.agent_api.context.Context;
4-
import dev.aikido.agent_api.context.NettyContext;
54
import dev.aikido.agent_api.context.SpringContextObject;
6-
import dev.aikido.agent_api.context.SpringMVCContextObject;
75

86
import java.lang.annotation.Annotation;
97
import java.lang.reflect.Parameter;
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
package dev.aikido.agent_api.context;
22

33
import java.net.InetSocketAddress;
4-
import java.util.ArrayList;
5-
import java.util.HashMap;
6-
import java.util.List;
7-
import java.util.Map;
4+
import java.util.*;
85

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

12-
public class NettyContext extends SpringContextObject {
13-
public NettyContext(
9+
public class SpringWebfluxContextObject extends SpringContextObject {
10+
public SpringWebfluxContextObject(
1411
String method, String uri, InetSocketAddress rawIp,
1512
HashMap<String, List<String>> cookies,
1613
Map<String, List<String>> query,
17-
List<Map.Entry<String, String>> headerEntries
14+
Map<String, String> headerEntries
1815

1916
) {
2017
this.method = method;
@@ -25,15 +22,15 @@ public NettyContext(
2522

2623
this.route = buildRouteFromUrl(this.url);
2724
this.remoteAddress = getIpFromRequest(rawIp.getAddress().getHostAddress(), this.headers);
28-
this.source = "ReactorNetty";
25+
this.source = "SpringWebflux";
2926
this.redirectStartNodes = new ArrayList<>();
3027
}
3128

32-
private static HashMap<String, String> extractHeaders(List<Map.Entry<String, String>> entries) {
33-
HashMap<String, String> headers = new HashMap<>();
34-
for(Map.Entry<String, String> entry: entries) {
35-
headers.put(entry.getKey().toLowerCase(), entry.getValue());
29+
private static HashMap<String, String> extractHeaders(Map<String, String> map) {
30+
HashMap<String, String> newMap = new HashMap<>();
31+
for (Map.Entry<String, String> entry : map.entrySet()) {
32+
newMap.put(entry.getKey().toLowerCase(), entry.getValue());
3633
}
37-
return headers;
38-
};
34+
return newMap;
35+
}
3936
}

0 commit comments

Comments
 (0)