Skip to content

Commit c712e41

Browse files
committed
Add server.request.body.filenames support for Jersey and RESTEasy
1 parent 8569434 commit c712e41

7 files changed

Lines changed: 305 additions & 62 deletions

File tree

dd-java-agent/instrumentation/jersey/jersey-2.0/src/jersey2JettyTest/groovy/datadog/trace/instrumentation/jersey2/Jersey2JettyTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ class Jersey2JettyTest extends HttpServerTest<JettyServer> {
5454
true
5555
}
5656

57+
@Override
58+
boolean testBodyFilenames() {
59+
true
60+
}
61+
5762
@Override
5863
boolean testBodyJson() {
5964
true

dd-java-agent/instrumentation/jersey/jersey-2.0/src/jersey3JettyTest/groovy/datadog/trace/instrumentation/jersey3/Jersey3JettyTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ class Jersey3JettyTest extends HttpServerTest<JettyServer> {
5353
true
5454
}
5555

56+
@Override
57+
boolean testBodyFilenames() {
58+
true
59+
}
60+
5661
@Override
5762
boolean testBodyJson() {
5863
true

dd-java-agent/instrumentation/jersey/jersey-appsec/jersey-appsec-2.0/src/main/java/datadog/trace/instrumentation/jersey2/MultiPartReaderServerSideInstrumentation.java

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import net.bytebuddy.asm.Advice;
2828
import org.glassfish.jersey.media.multipart.BodyPart;
2929
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
30+
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
3031
import org.glassfish.jersey.media.multipart.MultiPart;
3132
import org.glassfish.jersey.message.internal.MediaTypes;
3233

@@ -72,42 +73,69 @@ static void after(
7273
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
7374
BiFunction<RequestContext, Object, Flow<Void>> callback =
7475
cbp.getCallback(EVENTS.requestBodyProcessed());
75-
if (callback == null) {
76+
BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCallback =
77+
cbp.getCallback(EVENTS.requestFilesFilenames());
78+
if (callback == null && filenamesCallback == null) {
7679
return;
7780
}
7881

79-
Map<String, List<String>> map = new HashMap<>();
82+
Map<String, List<String>> map = callback != null ? new HashMap<>() : null;
83+
List<String> filenames = filenamesCallback != null ? new ArrayList<>() : null;
8084
for (BodyPart bodyPart : ret.getBodyParts()) {
8185
if (!(bodyPart instanceof FormDataBodyPart)) {
8286
continue;
8387
}
8488
FormDataBodyPart dataBodyPart = (FormDataBodyPart) bodyPart;
85-
if (!MediaTypes.typeEqual(MediaType.TEXT_PLAIN_TYPE, dataBodyPart.getMediaType())) {
86-
continue;
89+
if (map != null
90+
&& MediaTypes.typeEqual(MediaType.TEXT_PLAIN_TYPE, dataBodyPart.getMediaType())) {
91+
// if the type of dataBodyPart.getEntity() is BodyPartEntity, it is safe to read the part
92+
// more than once. So we're not depriving the application of the data by consuming it here
93+
String v = dataBodyPart.getValue();
94+
String name = dataBodyPart.getName();
95+
List<String> values = map.get(name);
96+
if (values == null) {
97+
values = new ArrayList<>();
98+
map.put(name, values);
99+
}
100+
values.add(v);
87101
}
88-
// if the type of dataBodyPart.getEntity() is BodyPartEntity, it is safe to read the part
89-
// more than once. So we're not depriving the application of the data by consuming it here
90-
String v = dataBodyPart.getValue();
91-
92-
String name = dataBodyPart.getName();
93-
List<String> values = map.get(name);
94-
if (values == null) {
95-
values = new ArrayList<>();
96-
map.put(name, values);
102+
if (filenames != null) {
103+
FormDataContentDisposition cd = dataBodyPart.getFormDataContentDisposition();
104+
if (cd != null) {
105+
String filename = cd.getFileName();
106+
if (filename != null && !filename.isEmpty()) {
107+
filenames.add(filename);
108+
}
109+
}
97110
}
111+
}
98112

99-
values.add(v);
113+
if (map != null) {
114+
Flow<Void> flow = callback.apply(reqCtx, map);
115+
Flow.Action action = flow.getAction();
116+
if (action instanceof Flow.Action.RequestBlockingAction) {
117+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
118+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
119+
if (blockResponseFunction != null) {
120+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
121+
t = new BlockingException("Blocked request (for MultiPartReaderClientSide/readFrom)");
122+
reqCtx.getTraceSegment().effectivelyBlocked();
123+
}
124+
}
100125
}
101126

102-
Flow<Void> flow = callback.apply(reqCtx, map);
103-
Flow.Action action = flow.getAction();
104-
if (action instanceof Flow.Action.RequestBlockingAction) {
105-
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
106-
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
107-
if (blockResponseFunction != null) {
108-
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
109-
t = new BlockingException("Blocked request (for MultiPartReaderClientSide/readFrom)");
110-
reqCtx.getTraceSegment().effectivelyBlocked();
127+
if (filenames != null && !filenames.isEmpty()) {
128+
Flow<Void> filenamesFlow = filenamesCallback.apply(reqCtx, filenames);
129+
Flow.Action filenamesAction = filenamesFlow.getAction();
130+
if (t == null && filenamesAction instanceof Flow.Action.RequestBlockingAction) {
131+
Flow.Action.RequestBlockingAction rba =
132+
(Flow.Action.RequestBlockingAction) filenamesAction;
133+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
134+
if (blockResponseFunction != null) {
135+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
136+
t = new BlockingException("Blocked request (multipart file upload)");
137+
reqCtx.getTraceSegment().effectivelyBlocked();
138+
}
111139
}
112140
}
113141
}

dd-java-agent/instrumentation/jersey/jersey-appsec/jersey-appsec-3.0/src/main/java/datadog/trace/instrumentation/jersey3/MultiPartReaderServerSideInstrumentation.java

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import net.bytebuddy.asm.Advice;
2828
import org.glassfish.jersey.media.multipart.BodyPart;
2929
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
30+
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
3031
import org.glassfish.jersey.media.multipart.MultiPart;
3132
import org.glassfish.jersey.message.internal.MediaTypes;
3233

@@ -72,42 +73,69 @@ static void after(
7273
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
7374
BiFunction<RequestContext, Object, Flow<Void>> callback =
7475
cbp.getCallback(EVENTS.requestBodyProcessed());
75-
if (callback == null) {
76+
BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCallback =
77+
cbp.getCallback(EVENTS.requestFilesFilenames());
78+
if (callback == null && filenamesCallback == null) {
7679
return;
7780
}
7881

79-
Map<String, List<String>> map = new HashMap<>();
82+
Map<String, List<String>> map = callback != null ? new HashMap<>() : null;
83+
List<String> filenames = filenamesCallback != null ? new ArrayList<>() : null;
8084
for (BodyPart bodyPart : ret.getBodyParts()) {
8185
if (!(bodyPart instanceof FormDataBodyPart)) {
8286
continue;
8387
}
8488
FormDataBodyPart dataBodyPart = (FormDataBodyPart) bodyPart;
85-
if (!MediaTypes.typeEqual(MediaType.TEXT_PLAIN_TYPE, dataBodyPart.getMediaType())) {
86-
continue;
89+
if (map != null
90+
&& MediaTypes.typeEqual(MediaType.TEXT_PLAIN_TYPE, dataBodyPart.getMediaType())) {
91+
// if the type of dataBodyPart.getEntity() is BodyPartEntity, it is safe to read the part
92+
// more than once. So we're not depriving the application of the data by consuming it here
93+
String v = dataBodyPart.getValue();
94+
String name = dataBodyPart.getName();
95+
List<String> values = map.get(name);
96+
if (values == null) {
97+
values = new ArrayList<>();
98+
map.put(name, values);
99+
}
100+
values.add(v);
87101
}
88-
// if the type of dataBodyPart.getEntity() is BodyPartEntity, it is safe to read the part
89-
// more than once. So we're not depriving the application of the data by consuming it here
90-
String v = dataBodyPart.getValue();
91-
92-
String name = dataBodyPart.getName();
93-
List<String> values = map.get(name);
94-
if (values == null) {
95-
values = new ArrayList<>();
96-
map.put(name, values);
102+
if (filenames != null) {
103+
FormDataContentDisposition cd = dataBodyPart.getFormDataContentDisposition();
104+
if (cd != null) {
105+
String filename = cd.getFileName();
106+
if (filename != null && !filename.isEmpty()) {
107+
filenames.add(filename);
108+
}
109+
}
97110
}
111+
}
98112

99-
values.add(v);
113+
if (map != null) {
114+
Flow<Void> flow = callback.apply(reqCtx, map);
115+
Flow.Action action = flow.getAction();
116+
if (action instanceof Flow.Action.RequestBlockingAction) {
117+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
118+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
119+
if (blockResponseFunction != null) {
120+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
121+
t = new BlockingException("Blocked request (for MultiPartReaderClientSide/readFrom)");
122+
reqCtx.getTraceSegment().effectivelyBlocked();
123+
}
124+
}
100125
}
101126

102-
Flow<Void> flow = callback.apply(reqCtx, map);
103-
Flow.Action action = flow.getAction();
104-
if (action instanceof Flow.Action.RequestBlockingAction) {
105-
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
106-
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
107-
if (blockResponseFunction != null) {
108-
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
109-
t = new BlockingException("Blocked request (for MultiPartReaderClientSide/readFrom)");
110-
reqCtx.getTraceSegment().effectivelyBlocked();
127+
if (filenames != null && !filenames.isEmpty()) {
128+
Flow<Void> filenamesFlow = filenamesCallback.apply(reqCtx, filenames);
129+
Flow.Action filenamesAction = filenamesFlow.getAction();
130+
if (t == null && filenamesAction instanceof Flow.Action.RequestBlockingAction) {
131+
Flow.Action.RequestBlockingAction rba =
132+
(Flow.Action.RequestBlockingAction) filenamesAction;
133+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
134+
if (blockResponseFunction != null) {
135+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
136+
t = new BlockingException("Blocked request (multipart file upload)");
137+
reqCtx.getTraceSegment().effectivelyBlocked();
138+
}
111139
}
112140
}
113141
}

dd-java-agent/instrumentation/resteasy/resteasy-appsec-3.0/src/main/java/datadog/trace/instrumentation/resteasy/MultipartFormDataReaderInstrumentation.java

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import datadog.trace.api.gateway.RequestContextSlot;
1919
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
2020
import java.io.IOException;
21+
import java.lang.reflect.Method;
2122
import java.util.ArrayList;
2223
import java.util.HashMap;
2324
import java.util.List;
@@ -45,6 +46,11 @@ public String instrumentedType() {
4546
return "org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataReader";
4647
}
4748

49+
@Override
50+
public String[] helperClassNames() {
51+
return new String[] {packageName + ".MultipartHelper"};
52+
}
53+
4854
@Override
4955
public void methodAdvice(MethodTransformer transformer) {
5056
transformer.applyAdvice(
@@ -72,28 +78,78 @@ static void after(
7278
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
7379
BiFunction<RequestContext, Object, Flow<Void>> callback =
7480
cbp.getCallback(EVENTS.requestBodyProcessed());
75-
if (callback == null) {
81+
BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCallback =
82+
cbp.getCallback(EVENTS.requestFilesFilenames());
83+
if (callback == null && filenamesCallback == null) {
7684
return;
7785
}
7886

79-
Map<String, List<String>> m = new HashMap<>();
80-
for (Map.Entry<String, List<InputPart>> e : ret.getFormDataMap().entrySet()) {
81-
List<String> strings = new ArrayList<>();
82-
m.put(e.getKey(), strings);
83-
for (InputPart inputPart : e.getValue()) {
84-
strings.add(inputPart.getBodyAsString());
87+
if (callback != null) {
88+
Map<String, List<String>> m = new HashMap<>();
89+
for (Map.Entry<String, List<InputPart>> e : ret.getFormDataMap().entrySet()) {
90+
List<String> strings = new ArrayList<>();
91+
m.put(e.getKey(), strings);
92+
for (InputPart inputPart : e.getValue()) {
93+
strings.add(inputPart.getBodyAsString());
94+
}
95+
}
96+
97+
Flow<Void> flow = callback.apply(reqCtx, m);
98+
Flow.Action action = flow.getAction();
99+
if (action instanceof Flow.Action.RequestBlockingAction) {
100+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
101+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
102+
if (blockResponseFunction != null) {
103+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
104+
t = new BlockingException("Blocked request (for MultipartFormDataInput/readFrom)");
105+
reqCtx.getTraceSegment().effectivelyBlocked();
106+
}
85107
}
86108
}
87109

88-
Flow<Void> flow = callback.apply(reqCtx, m);
89-
Flow.Action action = flow.getAction();
90-
if (action instanceof Flow.Action.RequestBlockingAction) {
91-
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
92-
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
93-
if (blockResponseFunction != null) {
94-
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
95-
t = new BlockingException("Blocked request (for MultipartFormDataInput/readFrom)");
96-
reqCtx.getTraceSegment().effectivelyBlocked();
110+
if (filenamesCallback != null) {
111+
List<String> filenames = new ArrayList<>();
112+
// Reflection avoids a bytecode ref to MultivaluedMap (javax→jakarta in RESTEasy 6)
113+
Method getHeadersMethod = null;
114+
try {
115+
getHeadersMethod = InputPart.class.getMethod("getHeaders");
116+
} catch (NoSuchMethodException ignored) {
117+
}
118+
if (getHeadersMethod != null) {
119+
for (Map.Entry<String, List<InputPart>> e : ret.getFormDataMap().entrySet()) {
120+
for (InputPart inputPart : e.getValue()) {
121+
List<String> cdHeaders;
122+
try {
123+
@SuppressWarnings("unchecked")
124+
Map<String, List<String>> headers =
125+
(Map<String, List<String>>) getHeadersMethod.invoke(inputPart);
126+
cdHeaders = headers != null ? headers.get("Content-Disposition") : null;
127+
} catch (Exception ignored) {
128+
continue;
129+
}
130+
if (cdHeaders == null || cdHeaders.isEmpty()) {
131+
continue;
132+
}
133+
String filename = MultipartHelper.filenameFromContentDisposition(cdHeaders.get(0));
134+
if (filename != null && !filename.isEmpty()) {
135+
filenames.add(filename);
136+
}
137+
}
138+
}
139+
}
140+
if (!filenames.isEmpty()) {
141+
Flow<Void> filenamesFlow = filenamesCallback.apply(reqCtx, filenames);
142+
Flow.Action filenamesAction = filenamesFlow.getAction();
143+
if (t == null && filenamesAction instanceof Flow.Action.RequestBlockingAction) {
144+
Flow.Action.RequestBlockingAction rba =
145+
(Flow.Action.RequestBlockingAction) filenamesAction;
146+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
147+
if (blockResponseFunction != null) {
148+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
149+
t = new BlockingException("Blocked request (multipart file upload)");
150+
reqCtx.getTraceSegment().effectivelyBlocked();
151+
}
152+
}
97153
}
98154
}
99155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package datadog.trace.instrumentation.resteasy;
2+
3+
public final class MultipartHelper {
4+
5+
private MultipartHelper() {}
6+
7+
// Quote-aware: semicolons inside quoted filenames (e.g. filename="a;b.php") are not separators
8+
public static String filenameFromContentDisposition(String cd) {
9+
int i = 0;
10+
int len = cd.length();
11+
while (i < len) {
12+
// advance to the next ';', skipping over any quoted strings
13+
while (i < len && cd.charAt(i) != ';') {
14+
if (cd.charAt(i) == '"') {
15+
i++;
16+
while (i < len && cd.charAt(i) != '"') {
17+
if (cd.charAt(i) == '\\') i++;
18+
i++;
19+
}
20+
}
21+
i++;
22+
}
23+
if (i >= len) break;
24+
i++; // skip ';'
25+
while (i < len && cd.charAt(i) == ' ') i++;
26+
if (cd.regionMatches(true, i, "filename=", 0, 9)) {
27+
i += 9;
28+
if (i >= len) return null;
29+
if (cd.charAt(i) == '"') {
30+
i++; // skip opening '"'
31+
StringBuilder sb = new StringBuilder();
32+
while (i < len && cd.charAt(i) != '"') {
33+
if (cd.charAt(i) == '\\' && i + 1 < len) i++; // unescape
34+
sb.append(cd.charAt(i++));
35+
}
36+
String name = sb.toString();
37+
return name.isEmpty() ? null : name;
38+
} else {
39+
int start = i;
40+
while (i < len && cd.charAt(i) != ';') i++;
41+
String name = cd.substring(start, i).trim();
42+
return name.isEmpty() ? null : name;
43+
}
44+
}
45+
}
46+
return null;
47+
}
48+
}

0 commit comments

Comments
 (0)