|
2 | 2 |
|
3 | 3 | import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; |
4 | 4 | import static datadog.trace.api.gateway.Events.EVENTS; |
| 5 | +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; |
5 | 6 | import static net.bytebuddy.matcher.ElementMatchers.takesArguments; |
6 | 7 |
|
7 | 8 | import com.google.auto.service.AutoService; |
|
20 | 21 | import java.io.IOException; |
21 | 22 | import java.io.InputStream; |
22 | 23 | import java.util.Collection; |
| 24 | +import java.util.Collections; |
23 | 25 | import java.util.List; |
24 | 26 | import java.util.Map; |
25 | 27 | import java.util.function.BiFunction; |
| 28 | +import javax.servlet.http.Part; |
26 | 29 | import net.bytebuddy.asm.Advice; |
27 | 30 | import net.bytebuddy.jar.asm.ClassReader; |
28 | 31 | import net.bytebuddy.jar.asm.ClassVisitor; |
@@ -52,6 +55,9 @@ public String[] helperClassNames() { |
52 | 55 | public void methodAdvice(MethodTransformer transformer) { |
53 | 56 | transformer.applyAdvice( |
54 | 57 | named("getParts").and(takesArguments(0)), getClass().getName() + "$GetFilenamesAdvice"); |
| 58 | + transformer.applyAdvice( |
| 59 | + named("getPart").and(takesArguments(1)).and(takesArgument(0, String.class)), |
| 60 | + getClass().getName() + "$GetPartAdvice"); |
55 | 61 | } |
56 | 62 |
|
57 | 63 | @Override |
@@ -189,4 +195,82 @@ static void after( |
189 | 195 | } |
190 | 196 | } |
191 | 197 | } |
| 198 | + |
| 199 | + /** |
| 200 | + * Fires AppSec events for a single-part upload via {@code getPart(String)}, which in Jetty 8.x |
| 201 | + * does NOT delegate to {@code getParts()} — it calls {@code |
| 202 | + * _multiPartInputStream.getPart(String)} directly. Without this advice, single-file uploads that |
| 203 | + * never call the public {@code getParts()} would be missed. |
| 204 | + */ |
| 205 | + @RequiresRequestContext(RequestContextSlot.APPSEC) |
| 206 | + public static class GetPartAdvice { |
| 207 | + @Advice.OnMethodEnter(suppress = Throwable.class) |
| 208 | + static boolean before() { |
| 209 | + return CallDepthThreadLocalMap.incrementCallDepth(Part.class) == 0; |
| 210 | + } |
| 211 | + |
| 212 | + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) |
| 213 | + static void after( |
| 214 | + @Advice.Enter boolean proceed, |
| 215 | + @Advice.Return Part part, |
| 216 | + @ActiveRequestContext RequestContext reqCtx, |
| 217 | + @Advice.Thrown(readOnly = false) Throwable t) { |
| 218 | + CallDepthThreadLocalMap.decrementCallDepth(Part.class); |
| 219 | + if (!proceed || t != null || part == null) { |
| 220 | + return; |
| 221 | + } |
| 222 | + |
| 223 | + Collection<Part> parts = Collections.singletonList(part); |
| 224 | + |
| 225 | + // Fire requestBodyProcessed with form-field name→value (if not a file upload) |
| 226 | + Map<String, List<String>> formFields = PartHelper.extractFormFields(parts); |
| 227 | + if (!formFields.isEmpty()) { |
| 228 | + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); |
| 229 | + BiFunction<RequestContext, Object, Flow<Void>> bodyCallback = |
| 230 | + cbp.getCallback(EVENTS.requestBodyProcessed()); |
| 231 | + if (bodyCallback != null) { |
| 232 | + Flow<Void> flow = bodyCallback.apply(reqCtx, formFields); |
| 233 | + Flow.Action action = flow.getAction(); |
| 234 | + if (action instanceof Flow.Action.RequestBlockingAction) { |
| 235 | + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; |
| 236 | + BlockResponseFunction brf = reqCtx.getBlockResponseFunction(); |
| 237 | + if (brf != null) { |
| 238 | + brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); |
| 239 | + if (t == null) { |
| 240 | + t = new BlockingException("Blocked request (multipart form field)"); |
| 241 | + reqCtx.getTraceSegment().effectivelyBlocked(); |
| 242 | + } |
| 243 | + } |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + if (t != null) { |
| 249 | + return; |
| 250 | + } |
| 251 | + |
| 252 | + // Fire requestFilesFilenames with file-upload filename (if a file upload) |
| 253 | + List<String> filenames = PartHelper.extractFilenames(parts); |
| 254 | + if (!filenames.isEmpty()) { |
| 255 | + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); |
| 256 | + BiFunction<RequestContext, List<String>, Flow<Void>> filenamesCallback = |
| 257 | + cbp.getCallback(EVENTS.requestFilesFilenames()); |
| 258 | + if (filenamesCallback != null) { |
| 259 | + Flow<Void> flow = filenamesCallback.apply(reqCtx, filenames); |
| 260 | + Flow.Action action = flow.getAction(); |
| 261 | + if (action instanceof Flow.Action.RequestBlockingAction) { |
| 262 | + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; |
| 263 | + BlockResponseFunction brf = reqCtx.getBlockResponseFunction(); |
| 264 | + if (brf != null) { |
| 265 | + brf.tryCommitBlockingResponse(reqCtx.getTraceSegment(), rba); |
| 266 | + if (t == null) { |
| 267 | + t = new BlockingException("Blocked request (multipart file upload)"); |
| 268 | + reqCtx.getTraceSegment().effectivelyBlocked(); |
| 269 | + } |
| 270 | + } |
| 271 | + } |
| 272 | + } |
| 273 | + } |
| 274 | + } |
| 275 | + } |
192 | 276 | } |
0 commit comments