Skip to content

Commit 856a8a3

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

5 files changed

Lines changed: 221 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: 109 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;
@@ -72,30 +73,122 @@ 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>> 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());
82+
if (callback != null) {
83+
Map<String, List<String>> m = new HashMap<>();
84+
for (Map.Entry<String, List<InputPart>> e : ret.getFormDataMap().entrySet()) {
85+
List<String> strings = new ArrayList<>();
86+
m.put(e.getKey(), strings);
87+
for (InputPart inputPart : e.getValue()) {
88+
strings.add(inputPart.getBodyAsString());
89+
}
90+
}
91+
92+
Flow<Void> flow = callback.apply(reqCtx, m);
93+
Flow.Action action = flow.getAction();
94+
if (action instanceof Flow.Action.RequestBlockingAction) {
95+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
96+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
97+
if (blockResponseFunction != null) {
98+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
99+
t = new BlockingException("Blocked request (for MultipartFormDataInput/readFrom)");
100+
reqCtx.getTraceSegment().effectivelyBlocked();
101+
}
85102
}
86103
}
87104

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();
105+
if (filenamesCallback != null) {
106+
List<String> filenames = new ArrayList<>();
107+
// Reflection avoids a bytecode ref to MultivaluedMap (javax→jakarta in RESTEasy 6)
108+
Method getHeadersMethod = null;
109+
try {
110+
getHeadersMethod = InputPart.class.getMethod("getHeaders");
111+
} catch (NoSuchMethodException ignored) {
112+
}
113+
if (getHeadersMethod != null) {
114+
for (Map.Entry<String, List<InputPart>> e : ret.getFormDataMap().entrySet()) {
115+
for (InputPart inputPart : e.getValue()) {
116+
List<String> cdHeaders;
117+
try {
118+
@SuppressWarnings("unchecked")
119+
Map<String, List<String>> headers =
120+
(Map<String, List<String>>) getHeadersMethod.invoke(inputPart);
121+
cdHeaders = headers != null ? headers.get("Content-Disposition") : null;
122+
} catch (Exception ignored) {
123+
continue;
124+
}
125+
if (cdHeaders == null || cdHeaders.isEmpty()) {
126+
continue;
127+
}
128+
String filename = filenameFromContentDisposition(cdHeaders.get(0));
129+
if (filename != null && !filename.isEmpty()) {
130+
filenames.add(filename);
131+
}
132+
}
133+
}
134+
}
135+
if (!filenames.isEmpty()) {
136+
Flow<Void> filenamesFlow = filenamesCallback.apply(reqCtx, filenames);
137+
Flow.Action filenamesAction = filenamesFlow.getAction();
138+
if (t == null && filenamesAction instanceof Flow.Action.RequestBlockingAction) {
139+
Flow.Action.RequestBlockingAction rba =
140+
(Flow.Action.RequestBlockingAction) filenamesAction;
141+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
142+
if (blockResponseFunction != null) {
143+
blockResponseFunction.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba);
144+
t = new BlockingException("Blocked request (multipart file upload)");
145+
reqCtx.getTraceSegment().effectivelyBlocked();
146+
}
147+
}
148+
}
149+
}
150+
}
151+
152+
// Quote-aware: semicolons inside quoted filenames (e.g. filename="a;b.php") are not separators
153+
static String filenameFromContentDisposition(String cd) {
154+
int i = 0;
155+
int len = cd.length();
156+
while (i < len) {
157+
// advance to the next ';', skipping over any quoted strings
158+
while (i < len && cd.charAt(i) != ';') {
159+
if (cd.charAt(i) == '"') {
160+
i++;
161+
while (i < len && cd.charAt(i) != '"') {
162+
if (cd.charAt(i) == '\\') i++;
163+
i++;
164+
}
165+
}
166+
i++;
167+
}
168+
if (i >= len) break;
169+
i++; // skip ';'
170+
while (i < len && cd.charAt(i) == ' ') i++;
171+
if (cd.regionMatches(true, i, "filename=", 0, 9)) {
172+
i += 9;
173+
if (i >= len) return null;
174+
if (cd.charAt(i) == '"') {
175+
i++; // skip opening '"'
176+
StringBuilder sb = new StringBuilder();
177+
while (i < len && cd.charAt(i) != '"') {
178+
if (cd.charAt(i) == '\\' && i + 1 < len) i++; // unescape
179+
sb.append(cd.charAt(i++));
180+
}
181+
String name = sb.toString();
182+
return name.isEmpty() ? null : name;
183+
} else {
184+
int start = i;
185+
while (i < len && cd.charAt(i) != ';') i++;
186+
String name = cd.substring(start, i).trim();
187+
return name.isEmpty() ? null : name;
188+
}
97189
}
98190
}
191+
return null;
99192
}
100193
}
101194
}

0 commit comments

Comments
 (0)