From 536c674139a8fb0be4a294be49b4b883758d02bd Mon Sep 17 00:00:00 2001 From: Gustavo Lopes Date: Wed, 6 Sep 2023 23:33:48 +0100 Subject: [PATCH] Appsec support for play 2.5+ --- .../src/main/groovy/InstrumentPlugin.groovy | 4 +- .../decorator/HttpServerDecoratorTest.groovy | 4 +- .../bytebuddy/matcher/ScalaTraitMatchers.java | 44 +++ .../appsec/BlockingResponseHelper.java | 10 +- ...MultipartUnmarshallersInstrumentation.java | 2 +- ...romEntityUnmarshallersInstrumentation.java | 2 +- .../SprayUnmarshallerInstrumentation.java | 2 +- .../akkahttp/iast/TraitMethodMatchers.java | 35 +- .../instrumentation/play-2.4/build.gradle | 149 +++++++- .../play24/PlayInstrumentation.java | 5 + .../DelegatingBodyParserInstrumentation.java | 50 +++ .../appsec/FormUrlEncodedInstrumentation.java | 52 +++ .../HttpErrorHandlerInstrumentation.java | 87 +++++ .../play25/appsec/MuzzleReferences.java | 14 + .../appsec/PathPatternInstrumentation.java | 54 +++ .../PlayBodyParsersInstrumentation.java | 79 +++++ .../appsec/RoutingDslInstrumentation.java | 54 +++ .../SirdPathExtractorInstrumentation.java | 52 +++ .../appsec/TolerantJsonInstrumentation.java | 54 +++ .../appsec/TolerantTextInstrumentation.java | 51 +++ .../appsec/ArgumentCaptureWrappers.java | 130 +++++++ ...ParserDelegatingBodyParserApplyAdvice.java | 25 ++ .../BodyParserFormUrlEncodedParseAdvice.java | 24 ++ .../play25/appsec/BodyParserHelpers.java | 303 +++++++++++++++++ .../BodyParserTolerantJsonParseAdvice.java | 19 ++ .../BodyParserTolerantTextParseAdvice.java | 19 ++ .../JavaMultipartFormDataRegisterExcF.java | 38 +++ .../play25/appsec/PathExtractionHelpers.java | 62 ++++ .../play25/appsec/PathPatternApplyAdvice.java | 45 +++ ...layBodyParsersMultipartFormDataAdvice.java | 18 + ...dyParsersTolerantFormUrlEncodedAdvice.java | 18 + .../PlayBodyParsersTolerantJsonAdvice.java | 16 + .../PlayBodyParsersTolerantTextAdvice.java | 15 + .../RoutingDslRouteConstructorAdvice.java | 19 ++ .../SirdPathExtractorExtractAdvice.java | 34 ++ .../scala/generator/CompileRoutes.scala | 17 + .../play25}/client/PlayWSClientTest.groovy | 5 +- .../play25/server/PlayAsyncServerTest.groovy | 14 + .../play25/server/PlayHttpServer.groovy | 77 +++++ .../play25/server/PlayRouters.groovy | 300 ++++++++++++++++ .../server/PlayScalaAsyncServerTest.groovy | 30 ++ .../server/PlayScalaRoutesServerTest.groovy | 110 ++++++ .../play25/server/PlayServerTest.groovy | 145 ++++++++ .../play25/server/TestHttpErrorHandler.groovy | 45 +++ .../groovy/server/PlayAsyncServerTest.groovy | 86 ----- .../test/groovy/server/PlayHttpServer.groovy | 33 -- .../test/groovy/server/PlayServerTest.groovy | 125 ------- .../play-2.4/src/test/routes/conf/routes | 16 + .../play25/PlayController.scala | 93 +++++ .../play25/PlayRoutersScala.scala | 151 +++++++++ .../trace/instrumentation/play25/Util.scala | 21 ++ .../instrumentation/play-2.6/build.gradle | 167 ++++++++- .../play26/server/PlayAsyncServerTest.groovy | 23 ++ ...PlayAsyncServerWithErrorHandlerTest.groovy | 21 ++ .../play26}/server/PlayHttpServer.groovy | 2 +- .../play26/server/PlayRouters.groovy | 320 ++++++++++++++++++ .../play26}/server/PlayServerTest.groovy | 110 +++--- .../PlayServerWithErrorHandlerTest.groovy | 46 +++ .../play26}/server/TestHttpErrorHandler.java | 21 +- ...rverRoutesScalaWithErrorHandlerTest.groovy | 111 ++++++ ...syncServerScalaWithErrorHandlerTest.groovy | 36 ++ .../latestdep/PlayHttpServerScala.groovy | 97 ++++++ .../server/latestdep/PlayRouters.groovy | 266 +++++++++++++++ .../src/latestDepTest/routes/conf/routes | 16 + .../latestdep/ImplicitConversions.scala | 17 + .../server/latestdep/PlayController.scala | 94 +++++ .../server/latestdep/PlayRoutersScala.scala | 155 +++++++++ .../play26/MuzzleReferences.java | 16 + .../play26/PlayHttpServerDecorator.java | 2 +- .../play26/PlayInstrumentation.java | 5 + .../appsec/ArgumentCaptureWrappers.java | 130 +++++++ .../play26/appsec/BodyParserHelpers.java | 303 +++++++++++++++++ .../DelegatingBodyParserInstrumentation.java | 76 +++++ .../appsec/FormUrlEncodedInstrumentation.java | 67 ++++ .../HttpErrorHandlerInstrumentation.java | 88 +++++ .../JavaMultipartFormDataRegisterExcF.java | 39 +++ .../appsec/NoDeclaredMethodMatcher.java | 23 ++ .../play26/appsec/PathExtractionHelpers.java | 62 ++++ .../appsec/PathPatternInstrumentation.java | 100 ++++++ .../PlayBodyParsersInstrumentation.java | 137 ++++++++ .../appsec/RoutingDslInstrumentation.java | 84 +++++ .../SirdPathExtractorInstrumentation.java | 83 +++++ .../appsec/TolerantJsonInstrumentation.java | 72 ++++ .../appsec/TolerantTextInstrumentation.java | 72 ++++ .../appsec/RoutingDsl27Instrumentation.java | 64 ++++ .../play27/appsec/ArgumentCaptureAdvice.java | 151 +++++++++ .../scala/generator/CompileRoutes.scala | 17 + .../groovy/server/PlayAsyncServerTest.groovy | 97 ------ ...PlayAsyncServerWithErrorHandlerTest.groovy | 105 ------ .../PlayServerWithErrorHandlerTest.groovy | 112 ------ .../agent/test/base/HttpServerTest.groovy | 2 +- .../normalize/SimpleHttpPathNormalizer.java | 3 + .../SimpleHttpPathNormalizerTest.groovy | 8 + 93 files changed, 5583 insertions(+), 694 deletions(-) create mode 100644 dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/matcher/ScalaTraitMatchers.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/DelegatingBodyParserInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/FormUrlEncodedInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/HttpErrorHandlerInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/MuzzleReferences.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PathPatternInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/RoutingDslInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantJsonInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantTextInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/ArgumentCaptureWrappers.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserDelegatingBodyParserApplyAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserFormUrlEncodedParseAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserHelpers.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantJsonParseAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantTextParseAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/JavaMultipartFormDataRegisterExcF.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathExtractionHelpers.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathPatternApplyAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersMultipartFormDataAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantFormUrlEncodedAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantJsonAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantTextAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/RoutingDslRouteConstructorAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorExtractAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.4/src/routeGenerator/scala/generator/CompileRoutes.scala rename dd-java-agent/instrumentation/play-2.4/src/test/groovy/{ => datadog/trace/instrumentation/play25}/client/PlayWSClientTest.groovy (90%) create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayAsyncServerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayHttpServer.groovy create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayRouters.groovy create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaAsyncServerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaRoutesServerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayServerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/TestHttpErrorHandler.groovy delete mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayAsyncServerTest.groovy delete mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayHttpServer.groovy delete mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayServerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/routes/conf/routes create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayController.scala create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayRoutersScala.scala create mode 100644 dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/Util.scala create mode 100644 dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerWithErrorHandlerTest.groovy rename dd-java-agent/instrumentation/play-2.6/src/{test/groovy => baseTest/groovy/datadog/trace/instrumentation/play26}/server/PlayHttpServer.groovy (97%) create mode 100644 dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayRouters.groovy rename dd-java-agent/instrumentation/play-2.6/src/{test/groovy => baseTest/groovy/datadog/trace/instrumentation/play26}/server/PlayServerTest.groovy (52%) create mode 100644 dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayServerWithErrorHandlerTest.groovy rename dd-java-agent/instrumentation/play-2.6/src/{test/java => baseTest/java/datadog/trace/instrumentation/play26}/server/TestHttpErrorHandler.java (58%) create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerRoutesScalaWithErrorHandlerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerScalaWithErrorHandlerTest.groovy create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayHttpServerScala.groovy create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayRouters.groovy create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/routes/conf/routes create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/ImplicitConversions.scala create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayController.scala create mode 100644 dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayRoutersScala.scala create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/MuzzleReferences.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/ArgumentCaptureWrappers.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/BodyParserHelpers.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/DelegatingBodyParserInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/FormUrlEncodedInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/HttpErrorHandlerInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/JavaMultipartFormDataRegisterExcF.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/NoDeclaredMethodMatcher.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathExtractionHelpers.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathPatternInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PlayBodyParsersInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/RoutingDslInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/SirdPathExtractorInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantJsonInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantTextInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play27/appsec/RoutingDsl27Instrumentation.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/main/java_play27/datadog/trace/instrumentation/play27/appsec/ArgumentCaptureAdvice.java create mode 100644 dd-java-agent/instrumentation/play-2.6/src/routeGenerator/scala/generator/CompileRoutes.scala delete mode 100644 dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerTest.groovy delete mode 100644 dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerWithErrorHandlerTest.groovy delete mode 100644 dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerWithErrorHandlerTest.groovy diff --git a/buildSrc/src/main/groovy/InstrumentPlugin.groovy b/buildSrc/src/main/groovy/InstrumentPlugin.groovy index d19514e6543..34e41e48faa 100644 --- a/buildSrc/src/main/groovy/InstrumentPlugin.groovy +++ b/buildSrc/src/main/groovy/InstrumentPlugin.groovy @@ -116,7 +116,9 @@ abstract class InstrumentTask extends DefaultTask { parameters.buildStartedTime.set(invocationDetails.buildStartedTime) parameters.pluginClassPath.setFrom(project.configurations.findByName('instrumentPluginClasspath') ?: []) parameters.plugins.set(extension.plugins) - parameters.instrumentingClassPath.setFrom(project.configurations.compileClasspath.findAll { + def matcher = instrumentTask.name =~ /instrument([A-Z].+)Java/ + def cfgName = matcher.matches() ? "${matcher.group(1).uncapitalize()}CompileClasspath" : 'compileClasspath' + parameters.instrumentingClassPath.setFrom(project.configurations[cfgName].findAll { it.name != 'previous-compilation-data.bin' && !it.name.endsWith(".gz") } + sourceDirectory + (extension.additionalClasspath[instrumentTask.name] ?: [])*.get()) parameters.sourceDirectory.set(sourceDirectory.asFile) diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy index a9d08bb226b..7eb53ab5d06 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpServerDecoratorTest.groovy @@ -145,9 +145,9 @@ class HttpServerDecoratorTest extends ServerDecoratorTest { where: rawQuery | rawResource | url | expectedUrl | expectedQuery | expectedResource - false | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query??" | "/path" + false | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query??" | "/p ath" false | true | "http://host/p%20ath?query%3F?" | "http://host/p%20ath" | "query??" | "/p%20ath" - true | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query%3F?" | "/path" + true | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query%3F?" | "/p ath" true | true | "http://host/p%20ath?query%3F?" | "http://host/p%20ath" | "query%3F?" | "/p%20ath" req = [url: url == null ? null : new URI(url)] diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/matcher/ScalaTraitMatchers.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/matcher/ScalaTraitMatchers.java new file mode 100644 index 00000000000..51464848f33 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/matcher/ScalaTraitMatchers.java @@ -0,0 +1,44 @@ +package datadog.trace.agent.tooling.bytebuddy.matcher; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ScalaTraitMatchers { + public static ElementMatcher.Junction isTraitMethod( + String traitName, String name, Object... argumentTypes) { + + ElementMatcher.Junction scalaOldArgs = + isStatic() + .and(takesArguments(argumentTypes.length + 1)) + .and(takesArgument(0, named(traitName))); + ElementMatcher.Junction scalaNewArgs = + not(isStatic()).and(takesArguments(argumentTypes.length)); + + for (int i = 0; i < argumentTypes.length; i++) { + Object argumentType = argumentTypes[i]; + ElementMatcher matcher; + if (argumentType instanceof ElementMatcher) { + matcher = (ElementMatcher) argumentType; + } else if (argumentType instanceof String) { + matcher = named((String) argumentType); + } else if (argumentType instanceof Class) { + matcher = is((Class) argumentType); + } else { + throw new IllegalArgumentException("Unexpected type for argument type specification"); + } + scalaOldArgs = scalaOldArgs.and(takesArgument(i + 1, matcher)); + scalaNewArgs = scalaNewArgs.and(takesArgument(i, matcher)); + } + + return isMethod().and(named(name)).and(scalaOldArgs.or(scalaNewArgs)); + } +} diff --git a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java index 064448c764f..046d6c96bdb 100644 --- a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java +++ b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/BlockingResponseHelper.java @@ -10,6 +10,7 @@ import akka.http.scaladsl.model.HttpResponse; import akka.http.scaladsl.model.ResponseEntity; import akka.http.scaladsl.model.StatusCode; +import akka.http.scaladsl.model.StatusCodes; import akka.util.ByteString; import datadog.appsec.api.blocking.BlockingContentType; import datadog.trace.api.gateway.BlockResponseFunction; @@ -95,7 +96,12 @@ public static HttpResponse maybeCreateBlockingResponse( RawHeader.create(e.getKey(), e.getValue())) .collect(ScalaListCollector.toScalaList()); - return HttpResponse.apply( - StatusCode.int2StatusCode(httpCode), headersList, entity, request.protocol()); + StatusCode code; + try { + code = StatusCode.int2StatusCode(httpCode); + } catch (RuntimeException e) { + code = StatusCodes.custom(httpCode, "Request Blocked", "", false, true); + } + return HttpResponse.apply(code, headersList, entity, request.protocol()); } } diff --git a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/MultipartUnmarshallersInstrumentation.java b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/MultipartUnmarshallersInstrumentation.java index bf96d650dac..13ad0f1d5c4 100644 --- a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/MultipartUnmarshallersInstrumentation.java +++ b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/MultipartUnmarshallersInstrumentation.java @@ -1,7 +1,7 @@ package datadog.trace.instrumentation.akkahttp.appsec; import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; -import static datadog.trace.instrumentation.akkahttp.iast.TraitMethodMatchers.isTraitMethod; +import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod; import static net.bytebuddy.matcher.ElementMatchers.returns; import akka.http.scaladsl.unmarshalling.MultipartUnmarshallers; diff --git a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/PredefinedFromEntityUnmarshallersInstrumentation.java b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/PredefinedFromEntityUnmarshallersInstrumentation.java index 72c52c057c2..5a262b6c5ec 100644 --- a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/PredefinedFromEntityUnmarshallersInstrumentation.java +++ b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/PredefinedFromEntityUnmarshallersInstrumentation.java @@ -2,7 +2,7 @@ import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf; -import static datadog.trace.instrumentation.akkahttp.iast.TraitMethodMatchers.isTraitMethod; +import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod; import static net.bytebuddy.matcher.ElementMatchers.returns; import akka.http.scaladsl.unmarshalling.PredefinedFromEntityUnmarshallers; diff --git a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/SprayUnmarshallerInstrumentation.java b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/SprayUnmarshallerInstrumentation.java index ecf150e7d17..46e3ff78460 100644 --- a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/SprayUnmarshallerInstrumentation.java +++ b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/appsec/SprayUnmarshallerInstrumentation.java @@ -1,7 +1,7 @@ package datadog.trace.instrumentation.akkahttp.appsec; import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; -import static datadog.trace.instrumentation.akkahttp.iast.TraitMethodMatchers.isTraitMethod; +import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod; import static net.bytebuddy.matcher.ElementMatchers.returns; import akka.http.scaladsl.unmarshalling.Unmarshaller; diff --git a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/iast/TraitMethodMatchers.java b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/iast/TraitMethodMatchers.java index d9db76ece90..fb58538102e 100644 --- a/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/iast/TraitMethodMatchers.java +++ b/dd-java-agent/instrumentation/akka-http-10.0/src/main/java/datadog/trace/instrumentation/akkahttp/iast/TraitMethodMatchers.java @@ -1,46 +1,15 @@ package datadog.trace.instrumentation.akkahttp.iast; import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; -import static net.bytebuddy.matcher.ElementMatchers.isMethod; -import static net.bytebuddy.matcher.ElementMatchers.isStatic; -import static net.bytebuddy.matcher.ElementMatchers.not; +import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod; import static net.bytebuddy.matcher.ElementMatchers.returns; -import static net.bytebuddy.matcher.ElementMatchers.takesArgument; -import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import net.bytebuddy.description.method.MethodDescription; -import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; public class TraitMethodMatchers { - public static ElementMatcher.Junction isTraitMethod( - String traitName, String name, Object... argumentTypes) { - - ElementMatcher.Junction scalaOldArgs = - isStatic() - .and(takesArguments(argumentTypes.length + 1)) - .and(takesArgument(0, named(traitName))); - ElementMatcher.Junction scalaNewArgs = - not(isStatic()).and(takesArguments(argumentTypes.length)); - - for (int i = 0; i < argumentTypes.length; i++) { - Object argumentType = argumentTypes[i]; - ElementMatcher matcher; - if (argumentType instanceof ElementMatcher) { - matcher = (ElementMatcher) argumentType; - } else { - matcher = named((String) argumentType); - } - scalaOldArgs = scalaOldArgs.and(takesArgument(i + 1, matcher)); - scalaNewArgs = scalaNewArgs.and(takesArgument(i, matcher)); - } - - return isMethod().and(named(name)).and(scalaOldArgs.or(scalaNewArgs)); - } - public static ElementMatcher.Junction isTraitDirectiveMethod( - String traitName, String name, String... argumentTypes) { - + String traitName, String name, Object... argumentTypes) { return isTraitMethod(traitName, name, (Object[]) argumentTypes) .and(returns(named("akka.http.scaladsl.server.Directive"))); } diff --git a/dd-java-agent/instrumentation/play-2.4/build.gradle b/dd-java-agent/instrumentation/play-2.4/build.gradle index d188127c139..52dac9aab00 100644 --- a/dd-java-agent/instrumentation/play-2.4/build.gradle +++ b/dd-java-agent/instrumentation/play-2.4/build.gradle @@ -5,17 +5,27 @@ ext { muzzle { pass { + name = "play24and25" group = 'com.typesafe.play' module = 'play_2.11' versions = '[2.4.0,2.6)' assertInverse = true } + pass { + name = "play25only" + group = 'com.typesafe.play' + module = 'play_2.11' + versions = '[2.5.0,2.6)' + assertInverse = true + } fail { + name = "play24and25" group = 'com.typesafe.play' module = 'play_2.12' versions = '[,]' } fail { + name = "play24and25" group = 'com.typesafe.play' module = 'play_2.13' versions = '[,]' @@ -23,6 +33,7 @@ muzzle { } apply from: "$rootDir/gradle/java.gradle" +apply plugin: 'scala' repositories { maven { @@ -33,19 +44,45 @@ repositories { } } +tasks.withType(org.gradle.api.tasks.scala.ScalaCompile) { + it.javaLauncher = getJavaLauncherFor(8) +} + addTestSuiteForDir('latestDepTest', 'test') +sourceSets { + main_play25 { + java.srcDirs "${project.projectDir}/src/main/java_play25" + } +} +jar { + from sourceSets.main_play25.output +} +project.afterEvaluate { p -> + instrumentJava.dependsOn compileMain_play25Java + forbiddenApisMain_play25.dependsOn instrumentMain_play25Java +} +instrument { + additionalClasspath = [ + instrumentJava: compileMain_play25Java.destinationDirectory + ] +} + dependencies { compileOnly group: 'com.typesafe.play', name: 'play_2.11', version: '2.4.0' + main_play25CompileOnly group: 'com.typesafe.play', name: 'play_2.11', version: '2.5.0' + main_play25CompileOnly project(':internal-api') + main_play25CompileOnly project(':dd-java-agent:agent-tooling') + main_play25CompileOnly project(':dd-java-agent:agent-bootstrap') - testImplementation project(':dd-java-agent:instrumentation:netty-4.0') - testImplementation project(':dd-java-agent:instrumentation:netty-4.1') - testImplementation project(':dd-java-agent:instrumentation:akka-http-10.0') - testImplementation project(':dd-java-agent:instrumentation:akka-concurrent') - testImplementation project(':dd-java-agent:instrumentation:akka-init') - testImplementation project(':dd-java-agent:instrumentation:scala-concurrent') - testImplementation project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.10') - testImplementation project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.13') + testRuntimeOnly project(':dd-java-agent:instrumentation:netty-4.0') + testRuntimeOnly project(':dd-java-agent:instrumentation:netty-4.1') + testRuntimeOnly project(':dd-java-agent:instrumentation:akka-http-10.0') + testRuntimeOnly project(':dd-java-agent:instrumentation:akka-concurrent') + testRuntimeOnly project(':dd-java-agent:instrumentation:akka-init') + testRuntimeOnly project(':dd-java-agent:instrumentation:scala-concurrent') + testRuntimeOnly project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.10') + testRuntimeOnly project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.13') // Before 2.5, play used netty 3.x which isn't supported, so for better test consistency, we test with just 2.5 testImplementation group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.5.0' @@ -53,6 +90,7 @@ dependencies { testImplementation(group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.5.0') { exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' } + testRuntimeOnly sourceSets.main_play25.output latestDepTestImplementation group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.5.+' latestDepTestImplementation group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.5.+' @@ -60,3 +98,98 @@ dependencies { exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' } } +compileTestGroovy { + classpath = classpath + files(compileTestScala.destinationDirectory) + dependsOn 'compileTestScala' +} +compileLatestDepTestGroovy { + classpath = classpath + files(compileLatestDepTestScala.destinationDirectory) + dependsOn 'compileLatestDepTestScala' +} + +sourceSets { + routeGenerator { + scala { + srcDir "${project.projectDir}/src/routeGenerator/scala" + } + } + testGenerated { + scala { + srcDir layout.buildDirectory.dir('generated/sources/testRoutes/scala') + } + } + latestDepTestGenerated { + scala { + srcDir layout.buildDirectory.dir('generated/sources/latestDepTestRoutes/scala') + } + } +} +dependencies { + routeGeneratorImplementation deps.scala211 + routeGeneratorImplementation group: 'com.typesafe.play', name: "routes-compiler_2.11", version: '2.5.0' +} +configurations { + testGeneratedCompileClasspath.extendsFrom testCompileClasspath + latestDepTestGeneratedCompileClasspath.extendsFrom latestDepTestCompileClasspath +} + +['buildTestRoutes', 'buildLatestDepTestRoutes'].each { taskName -> + tasks.register(taskName, JavaExec) { + String routesFile = "${project.projectDir}/src/test/routes/conf/routes" + def subdir = taskName == 'buildTestRoutes' ? 'testRoutes' : 'latestDepTestRoutes' + def outputDir = + layout.buildDirectory.dir("generated/sources/$subdir/scala") + + it.inputs.file routesFile + it.outputs.dir outputDir + + it.mainClass.set 'generator.CompileRoutes' + it.args routesFile, outputDir.get().asFile.absolutePath + + it.classpath configurations.routeGeneratorRuntimeClasspath + it.classpath compileRouteGeneratorScala.destinationDirectory + + if (taskName == 'buildTestRoutes') { + it.classpath compileTestScala.destinationDirectory + dependsOn compileTestScala + } else { + it.classpath compileLatestDepTestScala.destinationDirectory + dependsOn compileLatestDepTestScala + } + + dependsOn compileRouteGeneratorScala + } +} +compileTestGeneratedScala { + classpath = classpath + files(compileTestScala.destinationDirectory) + dependsOn buildTestRoutes, compileLatestDepTestScala +} +compileLatestDepTestGeneratedScala { + classpath = classpath + files(compileLatestDepTestScala.destinationDirectory) + dependsOn buildLatestDepTestRoutes, compileLatestDepTestScala +} +compileTestGroovy { + classpath = classpath + + files(compileTestGeneratedScala.destinationDirectory) + dependsOn 'compileTestGeneratedScala' +} +compileLatestDepTestGroovy { + classpath = classpath + + files(compileLatestDepTestGeneratedScala.destinationDirectory) + dependsOn 'compileLatestDepTestGeneratedScala' +} +// do it this way rather than through dependencies {} because +// latestDepTestImplementation extends testImplementation +test { + classpath = classpath + files(compileTestGeneratedScala.destinationDirectory) +} +latestDepTest { + classpath = classpath + files(compileLatestDepTestGeneratedScala.destinationDirectory) +} + +forbiddenApisTestGenerated { + enabled = false +} +forbiddenApisLatestDepTestGenerated { + enabled = false +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayInstrumentation.java index 80e571afaf9..b18260de59d 100644 --- a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayInstrumentation.java +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayInstrumentation.java @@ -18,6 +18,11 @@ public PlayInstrumentation() { super("play"); } + @Override + public String muzzleDirective() { + return "play24and25"; + } + @Override public String hierarchyMarkerType() { return "play.api.mvc.Action"; diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/DelegatingBodyParserInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/DelegatingBodyParserInstrumentation.java new file mode 100644 index 00000000000..e0cb3dcb60a --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/DelegatingBodyParserInstrumentation.java @@ -0,0 +1,50 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; + +@AutoService(Instrumenter.class) +public class DelegatingBodyParserInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public DelegatingBodyParserInstrumentation() { + super("play"); + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$DelegatingBodyParser"; + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".JavaMultipartFormDataRegisterExcF", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("apply") + .and(takesArguments(1)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(returns(named("play.libs.streams.Accumulator"))), + packageName + ".BodyParserDelegatingBodyParserApplyAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/FormUrlEncodedInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/FormUrlEncodedInstrumentation.java new file mode 100644 index 00000000000..12e7925406f --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/FormUrlEncodedInstrumentation.java @@ -0,0 +1,52 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import java.util.Map; + +@AutoService(Instrumenter.class) +public class FormUrlEncodedInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public FormUrlEncodedInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$FormUrlEncoded"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("parse") + .and(takesArguments(2)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(takesArgument(1, named("akka.util.ByteString"))) + .and(returns(Map.class)), + packageName + ".BodyParserFormUrlEncodedParseAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/HttpErrorHandlerInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/HttpErrorHandlerInstrumentation.java new file mode 100644 index 00000000000..3e1d71708b5 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/HttpErrorHandlerInstrumentation.java @@ -0,0 +1,87 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import play.api.http.HttpErrorHandler; +import play.api.mvc.RequestHeader; + +/** @see HttpErrorHandler#onServerError(RequestHeader, Throwable) */ +@AutoService(Instrumenter.class) +public class HttpErrorHandlerInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForTypeHierarchy { + public HttpErrorHandlerInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isPublic() + .and(named("onServerError")) + .and(takesArguments(2)) + .and(takesArgument(0, named("play.api.mvc.RequestHeader"))) + .and(takesArgument(1, Throwable.class)) + .and(returns(named("scala.concurrent.Future"))), + HttpErrorHandlerInstrumentation.class.getName() + "$OnServerErrorAdvice"); + } + + @Override + public String hierarchyMarkerType() { + return "play.api.http.HttpErrorHandler"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named("play.api.http.HttpErrorHandler")); + } + + static class OnServerErrorAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static void before(@Advice.Argument(1) Throwable t) { + int i = CallDepthThreadLocalMap.incrementCallDepth(HttpErrorHandler.class); + if (i > 0) { + return; + } + + if (!(t instanceof BlockingException)) { + return; + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return; + } + agentSpan.addThrowable(t); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after() { + CallDepthThreadLocalMap.decrementCallDepth(HttpErrorHandler.class); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/MuzzleReferences.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/MuzzleReferences.java new file mode 100644 index 00000000000..b56b9ea27d5 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/MuzzleReferences.java @@ -0,0 +1,14 @@ +package datadog.trace.instrumentation.play25.appsec; + +import datadog.trace.agent.tooling.muzzle.Reference; + +public class MuzzleReferences { + + public static final Reference[] PLAY_25_PLUS = new Reference[] {}; + + public static final Reference[] PLAY_25_ONLY = + new Reference[] { + new Reference.Builder("play.libs.concurrent.Futures").build(), + new Reference.Builder("play.Routes").build() + }; +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PathPatternInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PathPatternInstrumentation.java new file mode 100644 index 00000000000..c9409d1d916 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PathPatternInstrumentation.java @@ -0,0 +1,54 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; + +/** @see play.core.routing.PathPattern#apply(String) */ +@AutoService(Instrumenter.class) +public class PathPatternInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public PathPatternInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".PathExtractionHelpers", + }; + } + + @Override + public String instrumentedType() { + return "play.core.routing.PathPattern"; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("apply") + .and(not(isStatic())) + .and(takesArguments(1)) + .and(takesArgument(0, String.class)) + .and(returns(named("scala.Option"))), + packageName + ".PathPatternApplyAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersInstrumentation.java new file mode 100644 index 00000000000..10d6d49e143 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersInstrumentation.java @@ -0,0 +1,79 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; + +/** @see play.api.mvc.BodyParsers.parse$#tolerantText(long) */ +@AutoService(Instrumenter.class) +public class PlayBodyParsersInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForKnownTypes { + + public PlayBodyParsersInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String[] knownMatchingTypes() { + return new String[] { + "play.api.mvc.BodyParsers$parse$", + }; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("tolerantText") + .and(not(isStatic())) + .and(takesArguments(1)) + .and(takesArgument(0, long.class)) + .and(returns(named("play.api.mvc.BodyParser"))), + packageName + ".PlayBodyParsersTolerantTextAdvice"); + transformation.applyAdvice( + named("tolerantJson") + .and(not(isStatic())) + .and(takesArguments(1)) + .and(takesArgument(0, int.class)) + .and(returns(named("play.api.mvc.BodyParser"))), + packageName + ".PlayBodyParsersTolerantJsonAdvice"); + transformation.applyAdvice( + named("tolerantFormUrlEncoded") + .and(not(isStatic())) + .and(takesArguments(1)) + .and(takesArgument(0, int.class)) + .and(returns(named("play.api.mvc.BodyParser"))), + packageName + ".PlayBodyParsersTolerantFormUrlEncodedAdvice"); + transformation.applyAdvice( + named("multipartFormData") + .and(not(isStatic())) + .and(takesArguments(2)) + .and(takesArgument(0, named("scala.Function1"))) + .and(takesArgument(1, long.class)) + .and(returns(named("play.api.mvc.BodyParser"))), + packageName + ".PlayBodyParsersMultipartFormDataAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/RoutingDslInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/RoutingDslInstrumentation.java new file mode 100644 index 00000000000..1bb10bbea78 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/RoutingDslInstrumentation.java @@ -0,0 +1,54 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; + +/** @see play.routing.RoutingDsl.Route */ +@AutoService(Instrumenter.class) +public class RoutingDslInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public RoutingDslInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String instrumentedType() { + return "play.routing.RoutingDsl$Route"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".ArgumentCaptureWrappers", + packageName + ".ArgumentCaptureWrappers$ArgumentCaptureFunction", + packageName + ".ArgumentCaptureWrappers$ArgumentCaptureBiFunction", + packageName + ".ArgumentCaptureWrappers$ArgumentCaptureFunction3", + packageName + ".PathExtractionHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isConstructor() + .and(takesArguments(5)) + .and(takesArgument(3, Object.class)) + .and(takesArgument(4, java.lang.reflect.Method.class)), + packageName + ".RoutingDslRouteConstructorAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorInstrumentation.java new file mode 100644 index 00000000000..ff73f218ff2 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorInstrumentation.java @@ -0,0 +1,52 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; + +/** @see play.api.routing.sird.PathExtractor */ +@AutoService(Instrumenter.class) +public class SirdPathExtractorInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public SirdPathExtractorInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String instrumentedType() { + return "play.api.routing.sird.PathExtractor"; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + namedOneOf("extract", "play$api$routing$sird$PathExtractor$$extract") + .and(takesArguments(1)) + .and(takesArgument(0, String.class)) + .and(returns(named("scala.Option"))), + packageName + ".SirdPathExtractorExtractAdvice"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".PathExtractionHelpers", + }; + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantJsonInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantJsonInstrumentation.java new file mode 100644 index 00000000000..242acfe4107 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantJsonInstrumentation.java @@ -0,0 +1,54 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import akka.util.ByteString; +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import play.mvc.Http; + +/** @see play.mvc.BodyParser.TolerantJson#parse(Http.RequestHeader, ByteString) */ +@AutoService(Instrumenter.class) +public class TolerantJsonInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public TolerantJsonInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$TolerantJson"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("parse") + .and(takesArguments(2)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(takesArgument(1, named("akka.util.ByteString"))) + .and(returns(named("com.fasterxml.jackson.databind.JsonNode"))), + packageName + ".BodyParserTolerantJsonParseAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantTextInstrumentation.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantTextInstrumentation.java new file mode 100644 index 00000000000..01b21d5b49d --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play25/appsec/TolerantTextInstrumentation.java @@ -0,0 +1,51 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; + +@AutoService(Instrumenter.class) +public class TolerantTextInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public TolerantTextInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play25only"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_25_ONLY; + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$TolerantText"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("parse") + .and(takesArguments(2)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(takesArgument(1, named("akka.util.ByteString"))) + .and(returns(String.class)), + packageName + ".BodyParserTolerantTextParseAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/ArgumentCaptureWrappers.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/ArgumentCaptureWrappers.java new file mode 100644 index 00000000000..2eb39277e5d --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/ArgumentCaptureWrappers.java @@ -0,0 +1,130 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import play.libs.F; + +public class ArgumentCaptureWrappers { + public static class ArgumentCaptureFunction implements Function { + private final Function delegate; + + public ArgumentCaptureFunction(Function delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Object o) { + if (o == null) { + return delegate.apply(null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(o); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(o); + } + + Map conv = Collections.singletonMap("0", o); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routeTo"); + if (t != null) { + throw t; + } + + return delegate.apply(o); + } + } + + public static class ArgumentCaptureBiFunction implements BiFunction { + private final BiFunction delegate; + + public ArgumentCaptureBiFunction(BiFunction delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Object o1, Object o2) { + if (o1 == null && o2 == null) { + return delegate.apply(null, null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(o1, o2); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(o1, o2); + } + + Map conv = new HashMap<>(); + conv.put("0", o1); + conv.put("1", o2); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routeTo"); + if (t != null) { + throw t; + } + + return delegate.apply(o1, o2); + } + } + + public static class ArgumentCaptureFunction3 + implements F.Function3 { + private final F.Function3 delegate; + + public ArgumentCaptureFunction3(F.Function3 delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Object o1, Object o2, Object o3) throws Throwable { + if (o1 == null && o2 == null && o3 == null) { + return delegate.apply(null, null, null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(o1, o2, o3); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(o1, o2, o3); + } + + Map conv = new HashMap<>(); + conv.put("0", o1); + conv.put("1", o2); + conv.put("2", o3); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routeTo"); + if (t != null) { + throw t; + } + + return delegate.apply(o1, o2, o3); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserDelegatingBodyParserApplyAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserDelegatingBodyParserApplyAdvice.java new file mode 100644 index 00000000000..c74ca29dace --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserDelegatingBodyParserApplyAdvice.java @@ -0,0 +1,25 @@ +package datadog.trace.instrumentation.play25.appsec; + +import net.bytebuddy.asm.Advice; +import play.core.j.JavaParsers$; +import play.libs.streams.Accumulator; +import play.mvc.BodyParser; +import play.mvc.Http; + +/** @see BodyParser.DelegatingBodyParser#apply(Http.RequestHeader) */ +public class BodyParserDelegatingBodyParserApplyAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after( + @Advice.This BodyParser thiz, + @Advice.Return(readOnly = false) play.libs.streams.Accumulator ret) { + if (!thiz.getClass().getName().equals("play.mvc.BodyParser$MultipartFormData")) { + return; + } + Accumulator< + akka.util.ByteString, play.libs.F.Either>> + acc = ret; + + ret = + acc.recover(JavaMultipartFormDataRegisterExcF.INSTANCE, JavaParsers$.MODULE$.trampoline()); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserFormUrlEncodedParseAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserFormUrlEncodedParseAdvice.java new file mode 100644 index 00000000000..a73eed6a3dc --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserFormUrlEncodedParseAdvice.java @@ -0,0 +1,24 @@ +package datadog.trace.instrumentation.play25.appsec; + +import akka.util.ByteString; +import datadog.appsec.api.blocking.BlockingException; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import play.mvc.Http; + +/** @see play.mvc.BodyParser.FormUrlEncoded#parse(Http.RequestHeader, ByteString) */ +public class BodyParserFormUrlEncodedParseAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return Map ret, @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null) { + return; + } + try { + // error is reported as client error, which doesn't preserve the exception + BodyParserHelpers.handleArbitraryPostDataWithSpanError(ret, "FormUrlEncoded#parse"); + } catch (BlockingException be) { + t = be; + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserHelpers.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserHelpers.java new file mode 100644 index 00000000000..c745a460954 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserHelpers.java @@ -0,0 +1,303 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.api.gateway.Events.EVENTS; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import play.api.libs.json.JsArray; +import play.api.libs.json.JsBoolean; +import play.api.libs.json.JsNumber; +import play.api.libs.json.JsObject; +import play.api.libs.json.JsString; +import play.api.libs.json.JsValue; +import play.api.mvc.MultipartFormData; +import scala.Function1; +import scala.Tuple2; +import scala.collection.Iterable; +import scala.collection.Iterator; +import scala.collection.Seq; +import scala.compat.java8.JFunction1; + +public class BodyParserHelpers { + + public static final int MAX_CONVERSION_DEPTH = 10; + private static final Logger log = LoggerFactory.getLogger(BodyParserHelpers.class); + public static final int MAX_RECURSION = 15; + + private static JFunction1< + scala.collection.immutable.Map>, + scala.collection.immutable.Map>> + HANDLE_URL_ENCODED = BodyParserHelpers::handleUrlEncoded; + private static JFunction1 HANDLE_TEXT = BodyParserHelpers::handleText; + private static JFunction1, MultipartFormData> HANDLE_MULTIPART_FORM_DATA = + BodyParserHelpers::handleMultipartFormData; + private static JFunction1 HANDLE_JSON = BodyParserHelpers::handleJson; + + private BodyParserHelpers() {} + + public static Function1< + scala.collection.immutable.Map>, + scala.collection.immutable.Map>> + getHandleUrlEncodedMapF() { + return HANDLE_URL_ENCODED; + } + + private static scala.collection.immutable.Map> handleUrlEncoded( + scala.collection.immutable.Map> data) { + if (data == null || data.isEmpty()) { + return data; + } + + try { + Object conv = tryConvertingScalaContainers(data, MAX_CONVERSION_DEPTH); + handleArbitraryPostData(conv, "tolerantFormUrlEncoded"); + } catch (Exception e) { + handleException(e, "Error handling result of tolerantFormUrlEncoded BodyParser"); + } + return data; + } + + public static Function1 getHandleStringMapF() { + return HANDLE_TEXT; + } + + private static String handleText(String s) { + if (s == null || s.isEmpty()) { + return s; + } + + try { + handleArbitraryPostData(s, "tolerantText"); + } catch (Exception e) { + handleException(e, "Error handling result of tolerantText BodyParser"); + } + + return s; + } + + public static Function1, MultipartFormData> + getHandleMultipartFormDataF() { + return HANDLE_MULTIPART_FORM_DATA; + } + + private static MultipartFormData handleMultipartFormData(MultipartFormData data) { + scala.collection.immutable.Map> mpfd = data.asFormUrlEncoded(); + + if (mpfd == null || mpfd.isEmpty()) { + return data; + } + + try { + Object conv = tryConvertingScalaContainers(mpfd, MAX_CONVERSION_DEPTH); + handleArbitraryPostData(conv, "multipartFormData"); + } catch (Exception e) { + handleException(e, "Error handling result of multipartFormData BodyParser"); + } + return data; + } + + public static Function1 getHandleJsonF() { + return HANDLE_JSON; + } + + private static JsValue handleJson(JsValue data) { + if (data == null) { + return null; + } + + try { + Object conv = jsValueToJavaObject(data, MAX_RECURSION); + handleArbitraryPostData(conv, "json"); + } catch (Exception e) { + handleException(e, "Error handling result of json BodyParser"); + } + return data; + } + + private static void executeCallback( + RequestContext reqCtx, + BiFunction> callback, + Object conv, + String details) { + Flow flow = callback.apply(reqCtx, conv); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction(); + if (blockResponseFunction != null) { + boolean success = + blockResponseFunction.tryCommitBlockingResponse( + reqCtx.getTraceSegment(), + rba.getStatusCode(), + rba.getBlockingContentType(), + rba.getExtraHeaders()); + if (success) { + throw new BlockingException("Blocked request (for " + details + ")"); + } + } + } + } + + private static Object tryConvertingScalaContainers(Object obj, int depth) { + if (depth == 0) { + return obj; + } + if (obj instanceof scala.collection.Map) { + scala.collection.Map map = (scala.collection.Map) obj; + Map ret = new HashMap<>(); + Iterator iterator = map.iterator(); + while (iterator.hasNext()) { + Tuple2 next = iterator.next(); + ret.put(next._1(), tryConvertingScalaContainers(next._2(), depth - 1)); + } + return ret; + } else if (obj instanceof Iterable) { + List ret = new ArrayList<>(); + Iterator iterator = ((Iterable) obj).iterator(); + while (iterator.hasNext()) { + Object next = iterator.next(); + ret.add(tryConvertingScalaContainers(next, depth - 1)); + } + return ret; + } + return obj; + } + + public static void handleJsonNode(JsonNode n, String source) { + Object o = jsNodeToJavaObject(n, MAX_RECURSION); + handleArbitraryPostDataWithSpanError(o, source); + } + + public static void handleArbitraryPostDataWithSpanError(Object o, String source) { + AgentSpan span = activeSpan(); + try { + doHandleArbitraryPostData(span, o, source); + } catch (BlockingException be) { + span.addThrowable(be); + throw be; + } + } + + public static void handleArbitraryPostData(Object o, String source) { + doHandleArbitraryPostData(activeSpan(), o, source); + } + + public static void doHandleArbitraryPostData(AgentSpan span, Object o, String source) { + RequestContext reqCtx; + if (span == null + || (reqCtx = span.getRequestContext()) == null + || reqCtx.getData(RequestContextSlot.APPSEC) == null) { + return; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> callback = + cbp.getCallback(EVENTS.requestBodyProcessed()); + if (callback == null) { + return; + } + + // callback execution + executeCallback(reqCtx, callback, o, source); + } + + private static void handleException(Exception e, String logMessage) { + if (e instanceof BlockingException) { + throw (BlockingException) e; + } + + log.warn(logMessage, e); + } + + private static Object jsValueToJavaObject(JsValue value, int maxRecursion) { + if (value == null || maxRecursion <= 0) { + return null; + } + + if (value instanceof JsString) { + return ((JsString) value).value(); + } else if (value instanceof JsNumber) { + return ((JsNumber) value).value(); + } else if (value instanceof JsBoolean) { + return ((JsBoolean) value).value(); + } else if (value instanceof JsObject) { + Map map = new HashMap<>(); + JsObject jsonObject = (JsObject) value; + Iterator> iterator = jsonObject.fields().iterator(); + while (iterator.hasNext()) { + Tuple2 e = iterator.next(); + map.put(e._1(), jsValueToJavaObject(e._2(), maxRecursion - 1)); + } + return map; + } else if (value instanceof JsArray) { + List list = new ArrayList<>(); + JsArray jsArray = (JsArray) value; + Iterator iterator = jsArray.value().iterator(); + while (iterator.hasNext()) { + JsValue next = iterator.next(); + list.add(jsValueToJavaObject(next, maxRecursion - 1)); + } + return list; + } else { + return null; + } + } + + private static Object jsNodeToJavaObject(JsonNode value, int maxRecursion) { + if (value == null || maxRecursion <= 0) { + return null; + } + + if (value instanceof TextNode) { + return value.asText(); + } else if (value instanceof FloatNode || value instanceof DoubleNode) { + return value.asDouble(); + } else if (value instanceof NumericNode) { + return value.asLong(); + } else if (value instanceof ObjectNode) { + Map map = new HashMap<>(); + ObjectNode jsonObject = (ObjectNode) value; + java.util.Iterator> iterator = jsonObject.fields(); + while (iterator.hasNext()) { + Map.Entry e = iterator.next(); + map.put(e.getKey(), jsNodeToJavaObject(e.getValue(), maxRecursion - 1)); + } + return map; + } else if (value instanceof ArrayNode) { + List list = new ArrayList<>(); + ArrayNode arrayNode = (ArrayNode) value; + java.util.Iterator iterator = arrayNode.elements(); + while (iterator.hasNext()) { + JsonNode next = iterator.next(); + list.add(jsNodeToJavaObject(next, maxRecursion - 1)); + } + return list; + } else if (value instanceof NullNode) { + return null; + } else { + return value.asText(""); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantJsonParseAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantJsonParseAdvice.java new file mode 100644 index 00000000000..3648168a780 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantJsonParseAdvice.java @@ -0,0 +1,19 @@ +package datadog.trace.instrumentation.play25.appsec; + +import com.fasterxml.jackson.databind.JsonNode; +import datadog.appsec.api.blocking.BlockingException; +import net.bytebuddy.asm.Advice; + +public class BodyParserTolerantJsonParseAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after(@Advice.Return JsonNode ret, @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null) { + return; + } + try { + BodyParserHelpers.handleJsonNode(ret, "TolerantJson#parse"); + } catch (BlockingException be) { + t = be; + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantTextParseAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantTextParseAdvice.java new file mode 100644 index 00000000000..43761f4f59b --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/BodyParserTolerantTextParseAdvice.java @@ -0,0 +1,19 @@ +package datadog.trace.instrumentation.play25.appsec; + +import datadog.appsec.api.blocking.BlockingException; +import net.bytebuddy.asm.Advice; + +public class BodyParserTolerantTextParseAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after(@Advice.Return String ret, @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null) { + return; + } + try { + // error is reported as client error, which doesn't preserve the exception + BodyParserHelpers.handleArbitraryPostDataWithSpanError(ret, "TolerantText#parse"); + } catch (BlockingException be) { + t = be; + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/JavaMultipartFormDataRegisterExcF.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/JavaMultipartFormDataRegisterExcF.java new file mode 100644 index 00000000000..0c249d912cc --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/JavaMultipartFormDataRegisterExcF.java @@ -0,0 +1,38 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.CompletionException; +import java.util.function.Function; +import play.libs.F; +import play.mvc.Http; +import play.mvc.Result; + +public class JavaMultipartFormDataRegisterExcF + implements Function>> { + public static Function>> INSTANCE = + new JavaMultipartFormDataRegisterExcF(); + + private JavaMultipartFormDataRegisterExcF() {} + + @Override + public F.Either> apply(Throwable exc) { + if (exc instanceof CompletionException) { + exc = exc.getCause(); + } + if (exc instanceof BlockingException) { + AgentSpan agentSpan = activeSpan(); + if (agentSpan != null) { + agentSpan.addThrowable(exc); + } + } + if (exc instanceof RuntimeException) { + throw (RuntimeException) exc; + } else { + throw new UndeclaredThrowableException(exc); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathExtractionHelpers.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathExtractionHelpers.java new file mode 100644 index 00000000000..87fc8bff851 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathExtractionHelpers.java @@ -0,0 +1,62 @@ +package datadog.trace.instrumentation.play25.appsec; + +import static datadog.trace.api.gateway.Events.EVENTS; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.Map; +import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PathExtractionHelpers { + private static final Logger log = LoggerFactory.getLogger(PathExtractionHelpers.class); + + private PathExtractionHelpers() {} + + public static BlockingException callRequestPathParamsCallback( + RequestContext reqCtx, Map params, String origin) { + try { + return doCallRequestPathParamsCallback(reqCtx, params, origin); + } catch (Exception e) { + log.warn("Error calling " + origin, e); + return null; + } + } + + private static BlockingException doCallRequestPathParamsCallback( + RequestContext reqCtx, Map params, String origin) { + if (params == null || params.isEmpty()) { + return null; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction, Flow> callback = + cbp.getCallback(EVENTS.requestPathParams()); + if (callback == null) { + return null; + } + + Flow flow = callback.apply(reqCtx, params); + Flow.Action action = flow.getAction(); + if (!(action instanceof Flow.Action.RequestBlockingAction)) { + return null; + } + + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction brf = reqCtx.getBlockResponseFunction(); + if (brf != null) { + brf.tryCommitBlockingResponse( + reqCtx.getTraceSegment(), + rba.getStatusCode(), + rba.getBlockingContentType(), + rba.getExtraHeaders()); + } + return new BlockingException("Blocked request (for " + origin + ")"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathPatternApplyAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathPatternApplyAdvice.java new file mode 100644 index 00000000000..7eab9542568 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PathPatternApplyAdvice.java @@ -0,0 +1,45 @@ +package datadog.trace.instrumentation.play25.appsec; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.advice.ActiveRequestContext; +import datadog.trace.advice.RequiresRequestContext; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import net.bytebuddy.asm.Advice; +import scala.Tuple2; +import scala.collection.Iterator; +import scala.util.Either; + +@RequiresRequestContext(RequestContextSlot.APPSEC) +public class PathPatternApplyAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return(readOnly = false) + scala.Option>> ret, + @Advice.Thrown(readOnly = false) Throwable t, + @ActiveRequestContext RequestContext reqCtx) { + if (t != null) { + return; + } + if (ret.isEmpty()) { + return; + } + + java.util.Map conv = new java.util.HashMap<>(); + + Iterator>> iterator = ret.get().iterator(); + while (iterator.hasNext()) { + Tuple2> next = iterator.next(); + Either value = next._2(); + if (value.isLeft()) { + continue; + } + + conv.put(next._1(), value.right().get()); + } + + BlockingException blockingException = + PathExtractionHelpers.callRequestPathParamsCallback(reqCtx, conv, "PathPattern#apply"); + t = blockingException; + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersMultipartFormDataAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersMultipartFormDataAdvice.java new file mode 100644 index 00000000000..58c364e1675 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersMultipartFormDataAdvice.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.play25.appsec; + +import net.bytebuddy.asm.Advice; +import play.api.mvc.BodyParser; +import play.core.Execution; +import scala.Function1; + +/** @see play.api.mvc.BodyParsers.parse$#multipartFormData(Function1, long) */ +public class PlayBodyParsersMultipartFormDataAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after( + @Advice.Return(readOnly = false) BodyParser> parser) { + parser = + parser.map( + BodyParserHelpers.getHandleMultipartFormDataF(), + Execution.Implicits$.MODULE$.internalContext()); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantFormUrlEncodedAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantFormUrlEncodedAdvice.java new file mode 100644 index 00000000000..edeb7a2e014 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantFormUrlEncodedAdvice.java @@ -0,0 +1,18 @@ +package datadog.trace.instrumentation.play25.appsec; + +import net.bytebuddy.asm.Advice; +import play.api.mvc.BodyParser; +import play.core.Execution; +import scala.collection.Seq; + +public class PlayBodyParsersTolerantFormUrlEncodedAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after( + @Advice.Return(readOnly = false) + BodyParser>> parser) { + parser = + parser.map( + BodyParserHelpers.getHandleUrlEncodedMapF(), + Execution.Implicits$.MODULE$.internalContext()); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantJsonAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantJsonAdvice.java new file mode 100644 index 00000000000..ac72c6eadc9 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantJsonAdvice.java @@ -0,0 +1,16 @@ +package datadog.trace.instrumentation.play25.appsec; + +import net.bytebuddy.asm.Advice; +import play.api.libs.json.JsValue; +import play.api.mvc.BodyParser; +import play.core.Execution; + +/** @see play.api.mvc.BodyParsers.parse$#tolerantJson(int) */ +public class PlayBodyParsersTolerantJsonAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after(@Advice.Return(readOnly = false) BodyParser parser) { + parser = + parser.map( + BodyParserHelpers.getHandleJsonF(), Execution.Implicits$.MODULE$.internalContext()); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantTextAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantTextAdvice.java new file mode 100644 index 00000000000..2c519597326 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/PlayBodyParsersTolerantTextAdvice.java @@ -0,0 +1,15 @@ +package datadog.trace.instrumentation.play25.appsec; + +import net.bytebuddy.asm.Advice; +import play.api.mvc.BodyParser; +import play.core.Execution; + +public class PlayBodyParsersTolerantTextAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after(@Advice.Return(readOnly = false) BodyParser parser) { + parser = + parser.map( + BodyParserHelpers.getHandleStringMapF(), + Execution.Implicits$.MODULE$.internalContext()); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/RoutingDslRouteConstructorAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/RoutingDslRouteConstructorAdvice.java new file mode 100644 index 00000000000..9ebc0a8bd6c --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/RoutingDslRouteConstructorAdvice.java @@ -0,0 +1,19 @@ +package datadog.trace.instrumentation.play25.appsec; + +import java.util.function.BiFunction; +import java.util.function.Function; +import net.bytebuddy.asm.Advice; +import play.libs.F; + +public class RoutingDslRouteConstructorAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static void before(@Advice.Argument(value = 3, readOnly = false) Object action) { + if (action instanceof Function) { + action = new ArgumentCaptureWrappers.ArgumentCaptureFunction<>((Function) action); + } else if (action instanceof BiFunction) { + action = new ArgumentCaptureWrappers.ArgumentCaptureBiFunction<>((BiFunction) action); + } else if (action instanceof F.Function3) { + action = new ArgumentCaptureWrappers.ArgumentCaptureFunction3<>((F.Function3) action); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorExtractAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorExtractAdvice.java new file mode 100644 index 00000000000..2b42660650a --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java_play25/datadog/trace/instrumentation/play25/appsec/SirdPathExtractorExtractAdvice.java @@ -0,0 +1,34 @@ +package datadog.trace.instrumentation.play25.appsec; + +import datadog.trace.advice.ActiveRequestContext; +import datadog.trace.advice.RequiresRequestContext; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import scala.collection.immutable.List; + +/** @see play.api.routing.sird.PathExtractor#extract(java.lang.String) */ +@RequiresRequestContext(RequestContextSlot.APPSEC) +public class SirdPathExtractorExtractAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return scala.Option> ret, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + if (ret.isEmpty() || t != null) { + return; + } + + Map conv = new HashMap<>(); + List stringList = ret.get(); + for (int i = 0; i < stringList.size(); i++) { + conv.put(Integer.toString(i), stringList.apply(i)); + } + + t = + PathExtractionHelpers.callRequestPathParamsCallback( + reqCtx, conv, "sird.PathExtractor#extract"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/routeGenerator/scala/generator/CompileRoutes.scala b/dd-java-agent/instrumentation/play-2.4/src/routeGenerator/scala/generator/CompileRoutes.scala new file mode 100644 index 00000000000..505135029a3 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/routeGenerator/scala/generator/CompileRoutes.scala @@ -0,0 +1,17 @@ +package generator + +import play.routes.compiler.{InjectedRoutesGenerator, RoutesCompiler} + +import java.io.File +import scala.collection.immutable + +object CompileRoutes extends App { + val routesFile = args(0) + val destinationDir = args(1) + + val routesCompilerTask = RoutesCompiler.RoutesCompilerTask( + new File(routesFile), immutable.Seq.empty, + true, true, false) + RoutesCompiler.compile( + routesCompilerTask, InjectedRoutesGenerator, new File(destinationDir)) +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/client/PlayWSClientTest.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/client/PlayWSClientTest.groovy similarity index 90% rename from dd-java-agent/instrumentation/play-2.4/src/test/groovy/client/PlayWSClientTest.groovy rename to dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/client/PlayWSClientTest.groovy index 4f860671e34..2e500fe2f93 100644 --- a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/client/PlayWSClientTest.groovy +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/client/PlayWSClientTest.groovy @@ -1,8 +1,7 @@ -package client +package datadog.trace.instrumentation.play25.client import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.naming.TestingNettyHttpNamingConventions -import datadog.trace.instrumentation.netty40.client.NettyHttpClientDecorator import play.libs.ws.WS import spock.lang.AutoCleanup import spock.lang.Shared @@ -36,7 +35,7 @@ abstract class PlayWSClientTest extends HttpClientTest { @Override String component() { - return NettyHttpClientDecorator.DECORATE.component() + 'netty-client' } diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayAsyncServerTest.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayAsyncServerTest.groovy new file mode 100644 index 00000000000..fec90b5ae72 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayAsyncServerTest.groovy @@ -0,0 +1,14 @@ +package datadog.trace.instrumentation.play25.server + +import datadog.trace.agent.test.base.HttpServer +import groovy.transform.CompileStatic +import play.libs.concurrent.HttpExecution + +class PlayAsyncServerTest extends PlayServerTest { + + @Override + @CompileStatic + HttpServer server() { + new PlayHttpServer(PlayRouters.async(HttpExecution.defaultContext())) + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayHttpServer.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayHttpServer.groovy new file mode 100644 index 00000000000..d21243146ba --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayHttpServer.groovy @@ -0,0 +1,77 @@ +package datadog.trace.instrumentation.play25.server + +import com.typesafe.config.ConfigFactory +import datadog.trace.agent.test.base.HttpServer +import groovy.transform.CompileStatic +import play.api.Application +import play.api.BuiltInComponentsFromContext +import play.api.Environment +import play.api.Mode +import play.api.Play +import play.api.http.HttpErrorHandler +import play.core.server.NettyServerProvider +import play.core.server.ServerConfig +import play.routing.Router +import scala.Option + +import java.util.concurrent.TimeoutException + +class PlayHttpServer implements HttpServer { + final Router router + def server + def port + + PlayHttpServer(Router router) { + this.router = router + } + + @Override + @CompileStatic + void start() throws TimeoutException { + play.api.routing.Router router = this.router.asScala() + + play.api.ApplicationLoader.Context context = play.api.ApplicationLoader.Context$.MODULE$.apply( + Environment.simple(new File("."), Mode.Test()), + (scala.Option) scala.None$.MODULE$, + new play.core.DefaultWebCommands(), + play.api.Configuration$.MODULE$.apply(ConfigFactory.load()) + ) + + BuiltInComponentsFromContext components = new BuiltInComponentsFromContext(context) { + @Override + play.api.routing.Router router() { + router + } + + @Override + HttpErrorHandler httpErrorHandler() { + new play.core.j.JavaHttpErrorHandlerAdapter(TestHttpErrorHandler.INSTANCE) + } + } + + Application application = components.application() + Play.start(application) + ServerConfig serverConfig = ServerConfig.apply( + getClass().getClassLoader(), + new File('.'), + (scala.Option) Option.apply(0), + Option.empty(), + '0.0.0.0', + Mode.Test(), + System.getProperties()) + play.core.server.Server server = new NettyServerProvider().createServer(serverConfig, application) + + this.server = server + this.port = server.httpPort().get() + } + + @Override + void stop() { + server.stop() + } + + @Override + URI address() { + return new URI("http://localhost:$port/") + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayRouters.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayRouters.groovy new file mode 100644 index 00000000000..f0b49a51bfe --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayRouters.groovy @@ -0,0 +1,300 @@ +package datadog.trace.instrumentation.play25.server + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import datadog.appsec.api.blocking.Blocking +import datadog.trace.agent.test.base.HttpServerTest +import groovy.transform.CompileStatic +import play.api.mvc.RequestHeader +import play.mvc.Http +import play.mvc.Result +import play.mvc.Results +import play.routing.Router +import play.routing.RoutingDsl +import scala.Option +import scala.collection.Seq + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.concurrent.Executor +import java.util.function.Function +import java.util.function.Supplier + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_JSON +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.NOT_HERE +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.USER_BLOCK +import static datadog.trace.agent.test.base.HttpServerTest.controller + +class PlayRouters { + static Router sync() { + new RoutingDsl() + .GET(SUCCESS.path).routeTo({ + controller(SUCCESS) { + Results.ok(SUCCESS.body) + } + } as Supplier) + .GET(FORWARDED.path).routeTo({ + controller(FORWARDED) { + Option headerValue = requestHeader.headers().get('x-forwarded-for') + Results.ok(headerValue.empty ? 'x-forwarded-for header not present' : headerValue.get()) + } + } as Supplier) + .GET(QUERY_PARAM.path).routeTo({ + controller(QUERY_PARAM) { + Option> some = requestHeader.queryString().get('some') + Results.ok("some=${some.empty ? '(null)' : some.get().apply(0)}") + } + } as Supplier) + .GET(QUERY_ENCODED_QUERY.path).routeTo({ + controller(QUERY_ENCODED_QUERY) { + Option> some = requestHeader.queryString().get('some') + Results.ok("some=${some.empty ? '(null)' : some.get().apply(0)}") + } + } as Supplier) + .GET(QUERY_ENCODED_BOTH.rawPath).routeTo({ + controller(QUERY_ENCODED_BOTH) { + Option> some = requestHeader.queryString().get('some') + Results.ok("some=${some.empty ? '(null)' : some.get().apply(0)}"). + withHeader(HttpServerTest.IG_RESPONSE_HEADER, HttpServerTest.IG_RESPONSE_HEADER_VALUE) + } + } as Supplier) + .GET(REDIRECT.path).routeTo({ + controller(REDIRECT) { + Results.found(REDIRECT.body) + } + } as Supplier) + .GET(ERROR.path).routeTo({ + controller(ERROR) { + Results.status(ERROR.status, ERROR.body) + } + } as Supplier) + .GET(EXCEPTION.path).routeTo({ + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.body) + } + } as Supplier) + .GET(CUSTOM_EXCEPTION.path).routeAsync({ + controller(CUSTOM_EXCEPTION) { + throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.body) + } + } as Supplier) + .GET(NOT_HERE.path).routeTo({ + controller(NOT_HERE) { + Results.notFound(NOT_HERE.body) + } + } as Supplier) + .GET("/path/:id/param").routeTo(PathParamSyncFunction.INSTANCE) + .GET(USER_BLOCK.path).routeTo({ + controller(USER_BLOCK) { + controller(USER_BLOCK) { + Blocking.forUser('user-to-block').blockIfMatch() + Results.status(200, "should never be reached") + } + } + } as Supplier) + .POST(CREATED.path).routeTo({ + -> + controller(CREATED) { + String body = body().asText() + Results.status(CREATED.status, "created: $body") + } + } as Supplier) + .POST(BODY_URLENCODED.path).routeTo({ + -> + controller(BODY_URLENCODED) { + Map body = body().asFormUrlEncoded() + Results.status(BODY_URLENCODED.status, body as String) + } + } as Supplier) + .POST(BODY_MULTIPART.path).routeTo({ + -> + controller(BODY_MULTIPART) { + Map body = body().asMultipartFormData().asFormUrlEncoded() + Results.status(BODY_MULTIPART.status, body as String) + } + } as Supplier) + .POST(BODY_JSON.path).routeTo({ + -> + JsonNode json = body().asJson() + controller(BODY_JSON) { + Results.status(BODY_JSON.status, new ObjectMapper().writeValueAsString(json)) + } + } as Supplier) + .build() + } + + @CompileStatic + static Router async(Executor execContext) { + new RoutingDsl() + .GET(SUCCESS.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(SUCCESS) { + Results.ok(SUCCESS.body) + } + }, execContext) + } as Supplier) + .GET(FORWARDED.getPath()).routeAsync({ + Option header = requestHeader.headers().get('x-forwarded-for') + CompletableFuture.supplyAsync({ + controller(FORWARDED) { + Results.ok(header.empty ? '(null)' : header.get()) + } + }, execContext) + } as Supplier) + .GET(QUERY_PARAM.getPath()).routeAsync({ + Option> some = requestHeader.queryString().get('some') + CompletableFuture.supplyAsync({ + controller(QUERY_PARAM) { + Results.ok("some=${some.empty ? '(null)' : some.get().apply(0)}") + } + }, execContext) + } as Supplier) + .GET(QUERY_ENCODED_QUERY.getPath()).routeAsync({ + Option> some = requestHeader.queryString().get('some') + CompletableFuture.supplyAsync({ + controller(QUERY_ENCODED_QUERY) { + Results.ok("some=${some.empty ? '(null)' : some.get().apply(0)}") + } + }, execContext) + } as Supplier) + .GET(QUERY_ENCODED_BOTH.getRawPath()).routeAsync({ + Option> some = requestHeader.queryString().get('some') + CompletableFuture.supplyAsync({ + controller(QUERY_ENCODED_BOTH) { + Results.ok("some=${some.empty ? '(null)' : some.get().apply(0)}"). + withHeader(HttpServerTest.IG_RESPONSE_HEADER, HttpServerTest.IG_RESPONSE_HEADER_VALUE) // cheating + } + }, execContext) + } as Supplier) + .GET(REDIRECT.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(REDIRECT) { + Results.found(REDIRECT.getBody()) + } + }, execContext) + } as Supplier) + .GET(ERROR.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(ERROR) { + Results.status(ERROR.getStatus(), ERROR.getBody()) + } + }, execContext) + } as Supplier) + .GET(EXCEPTION.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.getBody()) + } + }, execContext) + } as Supplier) + .GET(CUSTOM_EXCEPTION.path).routeAsync({ + CompletableFuture.supplyAsync({ + controller(CUSTOM_EXCEPTION) { + throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.body) + } + }, execContext) + } as Supplier) + .GET("/path/:id/param").routeAsync(new PathParamAsyncFunction(execContext)) + .GET(USER_BLOCK.path).routeAsync({ + CompletableFuture.supplyAsync({ + -> + controller(USER_BLOCK) { + Blocking.forUser('user-to-block').blockIfMatch() + Results.status(200, "should never be reached") + } + }, execContext) + } as Supplier) + .POST(CREATED.path).routeAsync({ + -> + String body = body().asText() + CompletableFuture.supplyAsync({ + -> + controller(CREATED) { + Results.status(CREATED.status, "created: $body") + } + }, execContext) + } as Supplier) + .POST(BODY_URLENCODED.path).routeAsync({ + -> + Map body = body().asFormUrlEncoded() + CompletableFuture.supplyAsync({ + -> + controller(BODY_URLENCODED) { + Results.status(BODY_URLENCODED.status, body as String) + } + }, execContext) + } as Supplier) + .POST(BODY_MULTIPART.path).routeAsync({ + -> + Map body = body().asMultipartFormData().asFormUrlEncoded() + CompletableFuture.supplyAsync({ + -> + controller(BODY_MULTIPART) { + Results.status(BODY_MULTIPART.status, body as String) + } + }, execContext) + } as Supplier) + .POST(BODY_JSON.path).routeAsync({ + -> + JsonNode json = body().asJson() + CompletableFuture.supplyAsync({ + -> + controller(BODY_JSON) { + Results.status(BODY_JSON.status, new ObjectMapper().writeValueAsString(json)) + } + }, execContext) + } as Supplier) + .build() + } + + @CompileStatic + static enum PathParamSyncFunction implements Function { + INSTANCE + + @Override + Result apply(Integer i) { + controller(PATH_PARAM) { + Results.ok(i as String) + } + } + } + + @CompileStatic + static class PathParamAsyncFunction implements Function> { + private final Executor executor + + PathParamAsyncFunction(Executor executor) { + this.executor = executor + } + + @Override + CompletionStage apply(Integer i) { + CompletableFuture.supplyAsync({ + controller(PATH_PARAM) { + Results.ok(i as String) + } + }, executor) + } + } + + private static RequestHeader getRequestHeader() { + Http.Context.current()._requestHeader() + } + + private static play.mvc.Http.RequestBody body() { + Http.Context.current()._requestHeader().body + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaAsyncServerTest.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaAsyncServerTest.groovy new file mode 100644 index 00000000000..378f911499b --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaAsyncServerTest.groovy @@ -0,0 +1,30 @@ +package datadog.trace.instrumentation.play25.server + +import datadog.trace.agent.test.base.HttpServer +import datadog.trace.instrumentation.play25.PlayRoutersScala +import groovy.transform.CompileStatic +import spock.lang.Shared + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class PlayScalaAsyncServerTest extends PlayServerTest { + @Shared + ExecutorService executor + + def cleanupSpec() { + executor.shutdown() + } + + @Override + @CompileStatic + HttpServer server() { + executor = Executors.newCachedThreadPool() + new PlayHttpServer(PlayRoutersScala.async(executor).asJava()) + } + + @Override + Map expectedIGPathParams() { + ['0': '123'] + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaRoutesServerTest.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaRoutesServerTest.groovy new file mode 100644 index 00000000000..dc5b4317ae2 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayScalaRoutesServerTest.groovy @@ -0,0 +1,110 @@ +package datadog.trace.instrumentation.play25.server + +import datadog.trace.agent.test.asserts.TraceAssert +import datadog.trace.agent.test.base.HttpServer +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.DDTags +import datadog.trace.api.config.TraceInstrumentationConfig +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.play25.PlayController +import groovy.transform.CompileStatic +import scala.concurrent.ExecutionContext$ +import spock.lang.Shared + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +class PlayScalaRoutesServerTest extends PlayServerTest { + @Shared + ExecutorService executor + + def cleanupSpec() { + executor.shutdown() + } + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig(TraceInstrumentationConfig.HTTP_SERVER_ROUTE_BASED_NAMING, 'false') + } + + @Override + @CompileStatic + HttpServer server() { + executor = Executors.newCachedThreadPool() + + def router = new router.Routes( + new play.core.j.JavaHttpErrorHandlerAdapter(TestHttpErrorHandler.INSTANCE), + new PlayController(ExecutionContext$.MODULE$.fromExecutor(executor)) + ) + new PlayHttpServer(router.asJava()) + } + + @Override + String testPathParam() { + '/path/?/param' + } + + @Override + Map expectedIGPathParams() { + [path: '123'] + } + + private String routeFor(HttpServerTest.ServerEndpoint endpoint) { + switch (endpoint) { + case PATH_PARAM: + return '/path/$path<[^/]+>/param' + case NOT_FOUND: + return null + default: + endpoint.rawPath + } + } + + @Override + Map expectedExtraServerTags(HttpServerTest.ServerEndpoint endpoint) { + [(Tags.HTTP_ROUTE): routeFor(endpoint)] + } + + @Override + boolean hasHandlerSpan() { + true + } + + @Override + void handlerSpan(TraceAssert trace, HttpServerTest.ServerEndpoint endpoint = SUCCESS) { + def expectedQueryTag = expectedQueryTag(endpoint) + trace.span { + serviceName expectedServiceName() + operationName 'play.request' + spanType DDSpanTypes.HTTP_SERVER + errored endpoint == EXCEPTION || endpoint == CUSTOM_EXCEPTION + childOfPrevious() + tags { + "$Tags.COMPONENT" 'play-action' + "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER + "$Tags.PEER_HOST_IPV4" { it == (endpoint == FORWARDED ? endpoint.body : '127.0.0.1') } + "$Tags.HTTP_CLIENT_IP" { it == (endpoint == FORWARDED ? endpoint.body : '127.0.0.1') } + "$Tags.HTTP_URL" String + "$Tags.HTTP_HOSTNAME" address.host + "$Tags.HTTP_METHOD" String + "$Tags.HTTP_ROUTE" this.routeFor(endpoint) + if (endpoint == EXCEPTION || endpoint == CUSTOM_EXCEPTION) { + errorTags(endpoint == CUSTOM_EXCEPTION ? TestHttpErrorHandler.CustomRuntimeException : RuntimeException, endpoint.body) + } + if (endpoint.query) { + "$DDTags.HTTP_QUERY" expectedQueryTag + } + defaultTags() + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayServerTest.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayServerTest.groovy new file mode 100644 index 00000000000..dab1f327069 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/PlayServerTest.groovy @@ -0,0 +1,145 @@ +package datadog.trace.instrumentation.play25.server + +import datadog.trace.agent.test.asserts.TraceAssert +import datadog.trace.agent.test.base.HttpServer +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.DDTags +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.play24.PlayHttpServerDecorator +import groovy.transform.CompileStatic +import play.server.Server + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +class PlayServerTest extends HttpServerTest { + + @Override + @CompileStatic + HttpServer server() { + new PlayHttpServer(PlayRouters.sync()) + } + + @Override + void stopServer(Server server) { + server.stop() + } + + @Override + String component() { + 'netty' + } + + @Override + String expectedOperationName() { + 'netty.request' + } + + @Override + boolean hasHandlerSpan() { + true + } + + @Override + boolean hasExtraErrorInformation() { + true + } + + @Override + boolean changesAll404s() { + true + } + + boolean testExceptionBody() { + // I can't figure out how to set a proper exception handler to customize the response body. + false + } + + @Override + boolean testRequestBody() { + true + } + + @Override + boolean isRequestBodyNoStreaming() { + true + } + + @Override + boolean testBodyUrlencoded() { + true + } + + @Override + boolean testBodyMultipart() { + true + } + + @Override + boolean testBodyJson() { + true + } + + @Override + boolean testBlocking() { + true + } + + @Override + boolean testBlockingOnResponse() { + true + } + + @Override + String testPathParam() { + '/path/?/param' + } + + @Override + Map expectedIGPathParams() { + ['0': 123] + } + + @Override + Class expectedExceptionType() { + RuntimeException + } + + @Override + Class expectedCustomExceptionType() { + TestHttpErrorHandler.CustomRuntimeException + } + + @Override + void handlerSpan(TraceAssert trace, ServerEndpoint endpoint = SUCCESS) { + def expectedQueryTag = expectedQueryTag(endpoint) + trace.span { + serviceName expectedServiceName() + operationName "play.request" + spanType DDSpanTypes.HTTP_SERVER + errored endpoint == EXCEPTION || endpoint == CUSTOM_EXCEPTION + childOfPrevious() + tags { + "$Tags.COMPONENT" PlayHttpServerDecorator.DECORATE.component() + "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER + "$Tags.PEER_HOST_IPV4" { it == (endpoint == FORWARDED ? endpoint.body : "127.0.0.1") } + "$Tags.HTTP_CLIENT_IP" { it == (endpoint == FORWARDED ? endpoint.body : "127.0.0.1") } + "$Tags.HTTP_URL" String + "$Tags.HTTP_HOSTNAME" address.host + "$Tags.HTTP_METHOD" String + // BUG + // "$Tags.HTTP_ROUTE" String + if (endpoint == EXCEPTION || endpoint == CUSTOM_EXCEPTION) { + errorTags(endpoint == CUSTOM_EXCEPTION ? TestHttpErrorHandler.CustomRuntimeException : RuntimeException, endpoint.body) + } + if (endpoint.query) { + "$DDTags.HTTP_QUERY" expectedQueryTag + } + defaultTags() + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/TestHttpErrorHandler.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/TestHttpErrorHandler.groovy new file mode 100644 index 00000000000..1ae8f075361 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/datadog/trace/instrumentation/play25/server/TestHttpErrorHandler.groovy @@ -0,0 +1,45 @@ +package datadog.trace.instrumentation.play25.server + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import play.http.HttpErrorHandler +import play.mvc.Http.RequestHeader +import play.mvc.Result +import play.mvc.Results + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION + +enum TestHttpErrorHandler implements HttpErrorHandler { + INSTANCE + + private static final Logger LOG = LoggerFactory.getLogger(TestHttpErrorHandler) + + static class CustomRuntimeException extends RuntimeException { + CustomRuntimeException(String message) { + super(message) + } + } + + CompletionStage onClientError( + RequestHeader request, int statusCode, String message) { + return CompletableFuture.completedFuture(Results.status(statusCode, message)) + } + + CompletionStage onServerError(RequestHeader request, Throwable exception) { + LOG.warn('server error', exception) + + Throwable cause = exception.getCause() + if (cause) { + exception = cause + } + if (exception instanceof CustomRuntimeException) { + return CompletableFuture.completedFuture( + Results.status(CUSTOM_EXCEPTION.status, exception.message)) + } + + CompletableFuture.completedFuture(Results.internalServerError(exception.message)) + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayAsyncServerTest.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayAsyncServerTest.groovy deleted file mode 100644 index 20809d198a4..00000000000 --- a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayAsyncServerTest.groovy +++ /dev/null @@ -1,86 +0,0 @@ -package server - -import datadog.trace.agent.test.base.HttpServer -import play.libs.concurrent.HttpExecution -import play.mvc.Results -import play.routing.RoutingDsl - -import java.util.concurrent.CompletableFuture -import java.util.function.Supplier - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class PlayAsyncServerTest extends PlayServerTest { - - @Override - HttpServer server() { - def router = - new RoutingDsl() - .GET(SUCCESS.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(SUCCESS) { - Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) - } - }, HttpExecution.defaultContext()) - } as Supplier) - .GET(FORWARDED.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(FORWARDED) { - Results.status(FORWARDED.getStatus(), FORWARDED.getBody()) // cheating - } - }, HttpExecution.defaultContext()) - } as Supplier) - .GET(QUERY_PARAM.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_PARAM) { - Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) // cheating - } - }, HttpExecution.defaultContext()) - } as Supplier) - .GET(QUERY_ENCODED_QUERY.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_ENCODED_QUERY) { - Results.status(QUERY_ENCODED_QUERY.getStatus(), QUERY_ENCODED_QUERY.getBody()) // cheating - } - }, HttpExecution.defaultContext()) - } as Supplier) - .GET(QUERY_ENCODED_BOTH.getRawPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_ENCODED_BOTH) { - Results.status(QUERY_ENCODED_BOTH.getStatus(), QUERY_ENCODED_BOTH.getBody()). - withHeader(IG_RESPONSE_HEADER, IG_RESPONSE_HEADER_VALUE) // cheating - } - }, HttpExecution.defaultContext()) - } as Supplier) - .GET(REDIRECT.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(REDIRECT) { - Results.found(REDIRECT.getBody()) - } - }, HttpExecution.defaultContext()) - } as Supplier) - .GET(ERROR.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(ERROR) { - Results.status(ERROR.getStatus(), ERROR.getBody()) - } - }, HttpExecution.defaultContext()) - } as Supplier) - .GET(EXCEPTION.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(EXCEPTION) { - throw new Exception(EXCEPTION.getBody()) - } - }, HttpExecution.defaultContext()) - } as Supplier) - - return new PlayHttpServer(router.build()) - } -} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayHttpServer.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayHttpServer.groovy deleted file mode 100644 index efc2948baea..00000000000 --- a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayHttpServer.groovy +++ /dev/null @@ -1,33 +0,0 @@ -package server - -import datadog.trace.agent.test.base.HttpServer -import play.routing.Router -import play.server.Server - -import java.util.concurrent.TimeoutException - -class PlayHttpServer implements HttpServer { - final Router router - def server - def port - - PlayHttpServer(Router router) { - this.router = router - } - - @Override - void start() throws TimeoutException { - server = Server.forRouter(router, 0) - port = server.httpPort() - } - - @Override - void stop() { - server.stop() - } - - @Override - URI address() { - return new URI("http://localhost:$port/") - } -} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayServerTest.groovy b/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayServerTest.groovy deleted file mode 100644 index e28300dabec..00000000000 --- a/dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayServerTest.groovy +++ /dev/null @@ -1,125 +0,0 @@ -package server - -import datadog.trace.agent.test.asserts.TraceAssert -import datadog.trace.agent.test.base.HttpServer -import datadog.trace.agent.test.base.HttpServerTest -import datadog.trace.api.DDSpanTypes -import datadog.trace.api.DDTags -import datadog.trace.bootstrap.instrumentation.api.Tags -import datadog.trace.instrumentation.netty40.server.NettyHttpServerDecorator -import datadog.trace.instrumentation.play24.PlayHttpServerDecorator -import play.mvc.Results -import play.routing.RoutingDsl -import play.server.Server - -import java.util.function.Supplier - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class PlayServerTest extends HttpServerTest { - - @Override - HttpServer server() { - def router = - new RoutingDsl() - .GET(SUCCESS.getPath()).routeTo({ - controller(SUCCESS) { - Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) - } - } as Supplier) - .GET(FORWARDED.getPath()).routeTo({ - controller(FORWARDED) { - Results.status(FORWARDED.getStatus(), FORWARDED.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_PARAM.getPath()).routeTo({ - controller(QUERY_PARAM) { - Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_ENCODED_QUERY.getPath()).routeTo({ - controller(QUERY_ENCODED_QUERY) { - Results.status(QUERY_ENCODED_QUERY.getStatus(), QUERY_ENCODED_QUERY.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_ENCODED_BOTH.getRawPath()).routeTo({ - controller(QUERY_ENCODED_BOTH) { - Results.status(QUERY_ENCODED_BOTH.getStatus(), QUERY_ENCODED_BOTH.getBody()). - withHeader(IG_RESPONSE_HEADER, IG_RESPONSE_HEADER_VALUE) // cheating - } - } as Supplier) - .GET(REDIRECT.getPath()).routeTo({ - controller(REDIRECT) { - Results.found(REDIRECT.getBody()) - } - } as Supplier) - .GET(ERROR.getPath()).routeTo({ - controller(ERROR) { - Results.status(ERROR.getStatus(), ERROR.getBody()) - } - } as Supplier) - .GET(EXCEPTION.getPath()).routeTo({ - controller(EXCEPTION) { - throw new Exception(EXCEPTION.getBody()) - } - } as Supplier) - return new PlayHttpServer(router.build()) - } - - @Override - String component() { - return NettyHttpServerDecorator.DECORATE.component() - } - - @Override - String expectedOperationName() { - return "netty.request" - } - - @Override - boolean hasHandlerSpan() { - true - } - - boolean testExceptionBody() { - // I can't figure out how to set a proper exception handler to customize the response body. - false - } - - @Override - void handlerSpan(TraceAssert trace, ServerEndpoint endpoint = SUCCESS) { - def expectedQueryTag = expectedQueryTag(endpoint) - trace.span { - serviceName expectedServiceName() - operationName "play.request" - spanType DDSpanTypes.HTTP_SERVER - errored endpoint == EXCEPTION - childOfPrevious() - tags { - "$Tags.COMPONENT" PlayHttpServerDecorator.DECORATE.component() - "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER - "$Tags.PEER_HOST_IPV4" { it == (endpoint == FORWARDED ? endpoint.body : "127.0.0.1") } - "$Tags.HTTP_CLIENT_IP" { it == (endpoint == FORWARDED ? endpoint.body : "127.0.0.1") } - "$Tags.HTTP_URL" String - "$Tags.HTTP_HOSTNAME" address.host - "$Tags.HTTP_METHOD" String - // BUG - // "$Tags.HTTP_ROUTE" String - if (endpoint == EXCEPTION) { - errorTags(Exception, EXCEPTION.body) - } - if (endpoint.query) { - "$DDTags.HTTP_QUERY" expectedQueryTag - } - defaultTags() - } - } - } -} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/routes/conf/routes b/dd-java-agent/instrumentation/play-2.4/src/test/routes/conf/routes new file mode 100644 index 00000000000..83cbf85dd11 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/routes/conf/routes @@ -0,0 +1,16 @@ +GET /success datadog.trace.instrumentation.play25.PlayController.success() +GET /redirect datadog.trace.instrumentation.play25.PlayController.redirect() +GET /forwarded datadog.trace.instrumentation.play25.PlayController.forwarded() +GET /error-status datadog.trace.instrumentation.play25.PlayController.errorStatus() +GET /exception datadog.trace.instrumentation.play25.PlayController.exception() +GET /custom-exception datadog.trace.instrumentation.play25.PlayController.customException() +GET /not-here datadog.trace.instrumentation.play25.PlayController.notHere() +GET /user-block datadog.trace.instrumentation.play25.PlayController.userBlock() +GET /query datadog.trace.instrumentation.play25.PlayController.query(some: String) +GET /encoded_query datadog.trace.instrumentation.play25.PlayController.encodedQuery(some: String) +GET /encoded%20path%20query datadog.trace.instrumentation.play25.PlayController.encodedPathQuery(some: String) +GET /path/:path/param datadog.trace.instrumentation.play25.PlayController.pathParam(path: Integer) +POST /created datadog.trace.instrumentation.play25.PlayController.created() +POST /body-urlencoded datadog.trace.instrumentation.play25.PlayController.bodyUrlencoded() +POST /body-multipart datadog.trace.instrumentation.play25.PlayController.bodyMultipart() +POST /body-json datadog.trace.instrumentation.play25.PlayController.bodyJson() diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayController.scala b/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayController.scala new file mode 100644 index 00000000000..ff00fa780a9 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayController.scala @@ -0,0 +1,93 @@ +package datadog.trace.instrumentation.play25 + +import datadog.appsec.api.blocking.Blocking +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.base.HttpServerTest.{ServerEndpoint, getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE} +import datadog.trace.instrumentation.play25.Util.MapExtensions +import groovy.lang.Closure +import play.api.libs.json.{JsNull, JsValue, Json} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +class PlayController(implicit ec: ExecutionContext) extends Controller { + def success() = controller(ServerEndpoint.SUCCESS) { _ => + Results.Ok(ServerEndpoint.SUCCESS.getBody) + } + + def redirect = controller(ServerEndpoint.REDIRECT) { _ => + Results.Redirect(ServerEndpoint.REDIRECT.getBody, 302) + } + + def forwarded = controller(ServerEndpoint.FORWARDED) { request => + Results.Status(ServerEndpoint.FORWARDED.getStatus)(request.headers.get("X-Forwarded-For").getOrElse("(no header)")) + } + + def errorStatus = controller(ServerEndpoint.ERROR) { _ => + Results.Status(ServerEndpoint.ERROR.getStatus)(ServerEndpoint.ERROR.getBody) + } + + def exception = controller(ServerEndpoint.EXCEPTION) { _ => + throw new RuntimeException(ServerEndpoint.EXCEPTION.getBody) + } + + def customException = controller(ServerEndpoint.CUSTOM_EXCEPTION) { _ => + throw Util.createCustomException(ServerEndpoint.CUSTOM_EXCEPTION.getBody) + } + + def userBlock = controller(ServerEndpoint.USER_BLOCK) { _ => + Blocking.forUser("user-to-block").blockIfMatch() + Results.Ok("should never be reached") + } + + def query(some: String) = controller(ServerEndpoint.QUERY_PARAM) { _ => + Results.Status(ServerEndpoint.QUERY_PARAM.getStatus)(s"some=$some") + } + + def encodedQuery(some: String) = controller(ServerEndpoint.QUERY_ENCODED_QUERY) { _ => + Results.Status(ServerEndpoint.QUERY_ENCODED_QUERY.getStatus)(s"some=$some") + } + + def encodedPathQuery(some: String) = controller(ServerEndpoint.QUERY_ENCODED_BOTH) { _ => + Results.Status(ServerEndpoint.QUERY_ENCODED_BOTH.getStatus)(s"some=$some") + } + + def notHere = controller(ServerEndpoint.NOT_HERE) { _ => + Results.NotFound(ServerEndpoint.NOT_HERE.getBody) + } + + def pathParam(id: Integer) = controller(ServerEndpoint.PATH_PARAM) { _ => + Results.Ok(id.toString) + } + + def created = controller(ServerEndpoint.CREATED) { request => + val body: String = request.body.asText.getOrElse("") + Results.Created(s"created: $body") + } + + def bodyUrlencoded = controller(ServerEndpoint.BODY_URLENCODED) { request => + val body: Map[String, Seq[String]] = request.body.asFormUrlEncoded.getOrElse(Map.empty) + Results.Ok(body.toStringAsGroovy) + } + + def bodyMultipart = controller(ServerEndpoint.BODY_MULTIPART) { request => + val body: Map[String, Seq[String]] = request.body.asMultipartFormData.map(_.asFormUrlEncoded).getOrElse(Map.empty) + Results.Ok(body.toStringAsGroovy) + } + + def bodyJson = controller(ServerEndpoint.BODY_JSON) { request => + val body: JsValue = request.body.asJson.getOrElse(JsNull) + Results.Ok(Json.stringify(body)) + } + + private def controller(endpoint: ServerEndpoint)(block: Request[AnyContent] => Result) : Action[AnyContent] = { + Action.async { request => + Future { + HttpServerTest.controller(endpoint, new Closure[Result](this) { + def doCall() = block(request).withHeaders( + (getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE)) + }) + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayRoutersScala.scala b/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayRoutersScala.scala new file mode 100644 index 00000000000..5f6b313fd7b --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/PlayRoutersScala.scala @@ -0,0 +1,151 @@ +package datadog.trace.instrumentation.play25 + +import datadog.appsec.api.blocking.Blocking +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint._ +import datadog.trace.agent.test.base.HttpServerTest.{ServerEndpoint, getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE} +import datadog.trace.instrumentation.play25.Util.MapExtensions +import groovy.lang.Closure +import play.api.BuiltInComponents +import play.api.libs.json._ +import play.api.mvc._ +import play.api.routing.Router +import play.api.routing.sird._ + +import java.util.concurrent.{Executor, ExecutorService} +import scala.concurrent.{ExecutionContext, Future} + +object PlayRoutersScala { + + def async(executor: Executor): Router = { + val ec: ExecutionContext = ExecutionContext.fromExecutor(executor) + val parser = BodyParsers.parse.default + + def controller(endpoint: ServerEndpoint)(block: => Result): Future[Result] = { + Future { + HttpServerTest.controller(endpoint, new Closure[Result](this) { + def doCall(): Result = block.withHeaders( + (getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE)) + }) + }(ec) + } + + Router.from { + case GET(p"/success") => + Action.async { + controller(SUCCESS) { + Results.Ok(SUCCESS.getBody) + } + } + + case GET(p"/redirect") => + Action.async { + controller(REDIRECT) { + Results.Redirect(REDIRECT.getBody, 302) + } + } + + case GET(p"/forwarded") => + Action.async { request => + controller(FORWARDED) { + Results.Status(FORWARDED.getStatus)(request.headers.get("X-Forwarded-For").getOrElse("(no header)")) + } + } + + // endpoint is not special; error results are returned the same way + case GET(p"/error-status") => + Action.async { + controller(ERROR) { + Results.Status(ERROR.getStatus)(ERROR.getBody) + } + } + + case GET(p"/exception") => + Action.async { + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.getBody) + } + } + + case GET(p"/custom-exception") => + Action.async { + controller(CUSTOM_EXCEPTION) { + throw Util.createCustomException(CUSTOM_EXCEPTION.getBody) + } + } + + case GET(p"/not-here") => + Action.async { + controller(NOT_HERE) { + Results.NotFound + } + } + + case GET(p"/user-block") => + Action.async { + controller(USER_BLOCK) { + Blocking.forUser("user-to-block").blockIfMatch() + Results.Ok("should never be reached") + } + } + + case GET(p"/query" ? q"some=$some") => + Action.async { + controller(QUERY_PARAM) { + Results.Status(QUERY_PARAM.getStatus)(s"some=$some") + } + } + + case GET(p"/encoded_query" ? q"some=$some") => + Action.async { + controller(QUERY_ENCODED_QUERY) { + Results.Status(QUERY_ENCODED_QUERY.getStatus)(s"some=$some") + } + } + + case GET(p"/encoded%20path%20query" ? q"some=$some") => + Action.async { + controller(QUERY_ENCODED_BOTH) { + Results.Status(QUERY_ENCODED_BOTH.getStatus)(s"some=$some") + } + } + + case GET(p"/path/$path/param") => + Action.async { + controller(PATH_PARAM) { + Results.Status(PATH_PARAM.getStatus)(path) + } + } + + case POST(p"/created") => Action.async(parser) { request => + controller(CREATED) { + val body: String = request.body.asText.getOrElse("") + Results.Created(s"created: $body") + } + } + + case POST(p"/body-urlencoded") => Action.async(parser) { request => + controller(BODY_URLENCODED) { + val body: Map[String, Seq[String]] = request.body.asFormUrlEncoded.getOrElse(Map.empty) + Results.Ok(body.toStringAsGroovy) + } + } + + case POST(p"/body-multipart") => Action.async(parser) { request => + controller(BODY_MULTIPART) { + val body: Map[String, scala.Seq[String]] = request.body.asMultipartFormData.getOrElse( + MultipartFormData(Map.empty, scala.Seq.empty, scala.Seq.empty) + ).asFormUrlEncoded + Results.Ok(body.toStringAsGroovy) + } + } + + case POST(p"/body-json") => Action.async(parser) { request => + controller(BODY_JSON) { + val body: JsValue = request.body.asJson.getOrElse(JsNull) + Results.Ok(Json.stringify(body)) + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/Util.scala b/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/Util.scala new file mode 100644 index 00000000000..076d6efca13 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.4/src/test/scala/datadog/trace/instrumentation/play25/Util.scala @@ -0,0 +1,21 @@ +package datadog.trace.instrumentation.play25 + +object Util { + implicit class MapExtensions[A](m: Iterable[(String, A)]) { + def toStringAsGroovy: String = { + def valueToString(value: Object) : String = value match { + case seq: Seq[_] => seq.map(x => valueToString(x.asInstanceOf[Object])).mkString("[", ",", "]") + case other => other.toString + } + + m.map { case (key, value) => s"$key:${valueToString(value.asInstanceOf[Object])}" } + .mkString("[", ",", "]") + } + } + + def createCustomException(msg: String): Exception = { + val clazz = Class.forName("datadog.trace.instrumentation.play25.server.TestHttpErrorHandler$CustomRuntimeException") + val constructor = clazz.getConstructor(classOf[String]) + constructor.newInstance(msg).asInstanceOf[Exception] + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/build.gradle b/dd-java-agent/instrumentation/play-2.6/build.gradle index 551dbaa114f..a0bb7cf8858 100644 --- a/dd-java-agent/instrumentation/play-2.6/build.gradle +++ b/dd-java-agent/instrumentation/play-2.6/build.gradle @@ -8,26 +8,47 @@ def playVersion = '2.6.0' muzzle { pass { + name = 'play26Plus' group = 'com.typesafe.play' module = "play_$scalaVersion" versions = "[$playVersion,)" assertInverse = true } pass { + name = 'play26Plus' group = 'com.typesafe.play' module = 'play_2.12' versions = "[$playVersion,)" assertInverse = true } pass { + name = 'play26Plus' group = 'com.typesafe.play' module = 'play_2.13' versions = "[$playVersion,)" assertInverse = true } + + pass { + name = 'play26Only' + group = 'com.typesafe.play' + module = 'play-java_2.11' + versions = "[2.6.0,2.7.0)" + assertInverse = true + } + + pass { + name = 'play27' + group = 'com.typesafe.play' + module = 'play-java_2.13' + versions = "[2.7.0,)" + assertInverse = true + javaVersion = 11 + } } apply from: "$rootDir/gradle/java.gradle" +apply plugin: 'scala' repositories { maven { @@ -38,30 +59,144 @@ repositories { } } -addTestSuiteForDir('latestDepTest', 'test') +addTestSuiteForDir('baseTest', 'baseTest') +addTestSuiteForDir('latestDepTest', 'latestDepTest') + +sourceSets { + main_play27 { + java.srcDirs "${project.projectDir}/src/main/java_play27" + } +} +jar { + from sourceSets.main_play27.output +} +compileMain_play27Java.dependsOn compileJava +project.afterEvaluate { p -> + instrumentJava.dependsOn compileMain_play27Java + forbiddenApisMain_play27.dependsOn instrumentMain_play27Java +} +instrument { + additionalClasspath = [ + instrumentJava: compileMain_play27Java.destinationDirectory + ] +} dependencies { compileOnly group: 'com.typesafe.play', name: "play_$scalaVersion", version: playVersion + compileOnly group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: playVersion - testImplementation project(':dd-java-agent:instrumentation:netty-4.0') - testImplementation project(':dd-java-agent:instrumentation:netty-4.1') - testImplementation project(':dd-java-agent:instrumentation:akka-http-10.0') - testImplementation project(':dd-java-agent:instrumentation:akka-concurrent') - testImplementation project(':dd-java-agent:instrumentation:akka-init') - testImplementation project(':dd-java-agent:instrumentation:scala-concurrent') - testImplementation project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.10') - testImplementation project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.13') - testImplementation(project(path: ':dd-java-agent:instrumentation:akka-http-10.0', configuration: 'testArtifacts')) - - testImplementation group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: playVersion + main_play27CompileOnly group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: '2.7.0' + main_play27CompileOnly project(':internal-api') + main_play27CompileOnly project(':dd-java-agent:agent-tooling') + main_play27CompileOnly project(':dd-java-agent:agent-bootstrap') + main_play27CompileOnly files("${project.buildDir}/classes/java/raw") { + builtBy = ['compileJava'] + } + + baseTestImplementation group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: playVersion // TODO: Play WS is a separately versioned library starting with 2.6 and needs separate instrumentation. - testImplementation(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: playVersion) { + baseTestImplementation(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: playVersion) { exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' } - // TODO: This should be changed to the latest in scala 2.13 instead of 2.11 since its ahead - latestDepTestImplementation group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: '2.+' - latestDepTestImplementation(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: '2.+') { + testRuntimeOnly project(':dd-java-agent:instrumentation:netty-4.0') + testRuntimeOnly project(':dd-java-agent:instrumentation:netty-4.1') + testRuntimeOnly project(':dd-java-agent:instrumentation:akka-http-10.0') + testRuntimeOnly project(':dd-java-agent:instrumentation:akka-concurrent') + testRuntimeOnly project(':dd-java-agent:instrumentation:akka-init') + testRuntimeOnly project(':dd-java-agent:instrumentation:scala-concurrent') + testRuntimeOnly project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.10') + testRuntimeOnly project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.13') + + latestDepTestRuntimeOnly sourceSets.baseTest.output + latestDepTestImplementation deps.scala213 + latestDepTestImplementation group: 'com.typesafe.play', name: "play-java_2.13", version: '2.+' + latestDepTestImplementation(group: 'com.typesafe.play', name: "play-test_2.13", version: '2.+') { exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' } + latestDepTestImplementation group: 'com.typesafe.play', name: 'play-akka-http-server_2.13', version: '2.+' +} +configurations.matching({ it.name.startsWith('latestDepTest') }).each({ + it.resolutionStrategy { + // logback-classic 1.4.11 doesn't like being loaded in the bootstrap classloader (NPE) + force group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.5' + } +}) +tasks.named("compileLatestDepTestJava").configure { + it.sourceCompatibility = JavaVersion.VERSION_11 + it.targetCompatibility = JavaVersion.VERSION_11 + setJavaVersion(it, 11) +} +compileLatestDepTestScala { + javaLauncher = getJavaLauncherFor(11) + classpath = classpath + files(compileBaseTestJava.destinationDirectory) + dependsOn 'compileBaseTestJava' +} +latestDepTest { + javaLauncher = getJavaLauncherFor(11) + testClassesDirs = testClassesDirs + sourceSets.baseTest.output.classesDirs +} + +final generatedRoutes = layout.buildDirectory.dir('generated/sources/latestDepTestRoutes/scala') +sourceSets { + routeGenerator { + scala { + srcDir "${project.projectDir}/src/routeGenerator/scala" + } + } + latestDepTestGenerated { + scala { + srcDir generatedRoutes + } + } +} +dependencies { + routeGeneratorImplementation deps.scala213 + routeGeneratorImplementation group: 'com.typesafe.play', name: "routes-compiler_2.13", version: '2.+' +} +configurations { + latestDepTestGeneratedCompileClasspath.extendsFrom(latestDepTestCompileClasspath) +} + +tasks.register('buildLatestDepTestRoutes', JavaExec) { + String routesFile = "${project.projectDir}/src/latestDepTest/routes/conf/routes" + def outputDir = generatedRoutes + + it.inputs.file routesFile + it.outputs.dir outputDir + + it.mainClass.set 'generator.CompileRoutes' + it.args routesFile, outputDir.get().asFile.absolutePath + + it.classpath configurations.routeGeneratorRuntimeClasspath + it.classpath compileRouteGeneratorScala.destinationDirectory + it.classpath compileLatestDepTestScala.destinationDirectory + + it.javaLauncher.set getJavaLauncherFor(11) + + dependsOn compileRouteGeneratorScala, compileLatestDepTestScala +} +compileLatestDepTestGeneratedScala { + javaLauncher = getJavaLauncherFor(11) + classpath = classpath + files(compileLatestDepTestScala.destinationDirectory) + dependsOn buildLatestDepTestRoutes, compileLatestDepTestScala +} +forbiddenApisLatestDepTestGenerated { + enabled = false +} + +compileLatestDepTestGroovy { + javaLauncher = getJavaLauncherFor(11) + classpath = classpath + + files(compileLatestDepTestScala.destinationDirectory) + + files(compileBaseTestGroovy.destinationDirectory) + + files(compileBaseTestJava.destinationDirectory) + + files(compileLatestDepTestGeneratedScala.destinationDirectory) + dependsOn 'compileLatestDepTestScala' + dependsOn 'compileBaseTestGroovy' + dependsOn 'compileBaseTestJava' + dependsOn 'compileLatestDepTestGeneratedScala' +} +dependencies { + latestDepTestRuntimeOnly sourceSets.latestDepTestGenerated.output } diff --git a/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerTest.groovy new file mode 100644 index 00000000000..4d9e443369d --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerTest.groovy @@ -0,0 +1,23 @@ +package datadog.trace.instrumentation.play26.server + +import datadog.trace.agent.test.base.HttpServer +import groovy.transform.CompileStatic +import spock.lang.Shared + +import java.util.concurrent.Executors + +class PlayAsyncServerTest extends PlayServerTest { + @Shared + def executor + + def cleanupSpec() { + executor.shutdown() + } + + @CompileStatic + @Override + HttpServer server() { + executor = Executors.newCachedThreadPool() + return new PlayHttpServer(PlayRouters.&async.curry(executor)) + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerWithErrorHandlerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerWithErrorHandlerTest.groovy new file mode 100644 index 00000000000..8e84b25e8cf --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayAsyncServerWithErrorHandlerTest.groovy @@ -0,0 +1,21 @@ +package datadog.trace.instrumentation.play26.server + +import datadog.trace.agent.test.base.HttpServer +import spock.lang.Shared + +import java.util.concurrent.Executors + +class PlayAsyncServerWithErrorHandlerTest extends PlayServerWithErrorHandlerTest { + @Shared + def executor + + def cleanupSpec() { + executor.shutdown() + } + + @Override + HttpServer server() { + executor = Executors.newCachedThreadPool() + return new PlayHttpServer(PlayRouters.&async.curry(executor), new TestHttpErrorHandler()) + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayHttpServer.groovy b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayHttpServer.groovy similarity index 97% rename from dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayHttpServer.groovy rename to dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayHttpServer.groovy index 5e41e9e7ca5..e4fac0aeb7b 100644 --- a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayHttpServer.groovy +++ b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayHttpServer.groovy @@ -1,4 +1,4 @@ -package server +package datadog.trace.instrumentation.play26.server import datadog.trace.agent.test.base.HttpServer import play.ApplicationLoader diff --git a/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayRouters.groovy b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayRouters.groovy new file mode 100644 index 00000000000..07beb2adab6 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayRouters.groovy @@ -0,0 +1,320 @@ +package datadog.trace.instrumentation.play26.server + +import datadog.appsec.api.blocking.Blocking +import datadog.trace.agent.test.base.HttpServerTest +import groovy.transform.CompileStatic +import play.BuiltInComponents +import play.api.libs.json.JsValue +import play.api.libs.json.Json$ +import play.api.mvc.AnyContent +import play.libs.concurrent.HttpExecution +import play.mvc.Http +import play.mvc.Result +import play.mvc.Results +import play.routing.Router +import play.routing.RoutingDsl +import scala.collection.JavaConverters +import scala.concurrent.ExecutionContextExecutor + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.function.Function +import java.util.function.Supplier + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_JSON +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.USER_BLOCK +import static datadog.trace.agent.test.base.HttpServerTest.controller +import static java.lang.Class.forName + +class PlayRouters { + static Router sync(BuiltInComponents components) { + try { + forName("datadog.trace.instrumentation.play26.server.latestdep.PlayRouters").sync components + } catch (ClassNotFoundException cnf) { + sync26(components) + } + } + + static Router async(ExecutorService executor, BuiltInComponents components) { + try { + forName("datadog.trace.instrumentation.play26.server.latestdep.PlayRouters").async executor, components + } catch (ClassNotFoundException cnf) { + async26(executor, components) + } + } + + private static Router sync26(BuiltInComponents components) { + RoutingDsl.fromComponents(components) + .GET(SUCCESS.path).routeTo({ + controller(SUCCESS) { + Results.status(SUCCESS.status, SUCCESS.body) + } + } as Supplier) + .GET(FORWARDED.path).routeTo({ + controller(FORWARDED) { + Results.status(FORWARDED.status, FORWARDED.body) + } + } as Supplier) + .GET(QUERY_PARAM.path).routeTo({ + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.status, QUERY_PARAM.body) + } + } as Supplier) + .GET(QUERY_ENCODED_QUERY.path).routeTo({ + controller(QUERY_ENCODED_QUERY) { + Results.status(QUERY_ENCODED_QUERY.status, QUERY_ENCODED_QUERY.body) + } + } as Supplier) + .GET(QUERY_ENCODED_BOTH.rawPath).routeTo({ + controller(QUERY_ENCODED_BOTH) { + Results.status(QUERY_ENCODED_BOTH.status, QUERY_ENCODED_BOTH.body). + withHeader(HttpServerTest.IG_RESPONSE_HEADER, HttpServerTest.IG_RESPONSE_HEADER_VALUE) + } + } as Supplier) + .GET(REDIRECT.path).routeTo({ + controller(REDIRECT) { + Results.found(REDIRECT.body) + } + } as Supplier) + .GET(ERROR.path).routeTo({ + controller(ERROR) { + Results.status(ERROR.status, ERROR.body) + } + } as Supplier) + .GET(EXCEPTION.path).routeTo({ + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.body) + } + } as Supplier) + .GET(CUSTOM_EXCEPTION.path).routeAsync({ + controller(CUSTOM_EXCEPTION) { + throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.body) + } + } as Supplier) + .GET("/path/:id/param").routeTo(PathParamSyncFunction.INSTANCE) + .GET(USER_BLOCK.path).routeTo({ + controller(USER_BLOCK) { + controller(USER_BLOCK) { + Blocking.forUser('user-to-block').blockIfMatch() + Results.status(200, "should never be reached") + } + } + } as Supplier) + .POST(CREATED.path).routeTo({ + -> + controller(CREATED) { + def body = body().asText().get() + Results.status(CREATED.status, "created: $body") + } + } as Supplier) + .POST(BODY_URLENCODED.path).routeTo({ + -> + controller(BODY_URLENCODED) { + def body = body().asFormUrlEncoded().get() + def javaMap = JavaConverters.mapAsJavaMapConverter(body).asJava() + def res = javaMap.collectEntries { + [it.key, JavaConverters.asJavaCollectionConverter(it.value).asJavaCollection()] + } + Results.status(BODY_URLENCODED.status, res as String) + } + } as Supplier) + .POST(BODY_MULTIPART.path).routeTo({ + -> + controller(BODY_MULTIPART) { + def body = body().asMultipartFormData().get().asFormUrlEncoded() + def javaMap = JavaConverters.mapAsJavaMapConverter(body).asJava() + def res = javaMap.collectEntries { + [it.key, JavaConverters.asJavaCollectionConverter(it.value).asJavaCollection()] + } + Results.status(BODY_MULTIPART.status, res as String) + } + } as Supplier) + .POST(BODY_JSON.path).routeTo({ + -> + controller(BODY_JSON) { + JsValue json = body().asJson().get() + Results.status(BODY_JSON.status, Json$.MODULE$.stringify(json)) + } + } as Supplier) + .build() + } + + static Router async26(ExecutorService executor, BuiltInComponents components) { + ExecutionContextExecutor execContext = HttpExecution.fromThread(executor) + RoutingDsl.fromComponents(components) + .GET(SUCCESS.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(SUCCESS) { + Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) + } + }, execContext) + } as Supplier) + .GET(FORWARDED.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(FORWARDED) { + Results.status(FORWARDED.getStatus(), FORWARDED.getBody()) // cheating + } + }, execContext) + } as Supplier) + .GET(QUERY_PARAM.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) // cheating + } + }, execContext) + } as Supplier) + .GET(QUERY_ENCODED_QUERY.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(QUERY_ENCODED_QUERY) { + Results.status(QUERY_ENCODED_QUERY.getStatus(), QUERY_ENCODED_QUERY.getBody()) // cheating + } + }, execContext) + } as Supplier) + .GET(QUERY_ENCODED_BOTH.getRawPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(QUERY_ENCODED_BOTH) { + Results.status(QUERY_ENCODED_BOTH.getStatus(), QUERY_ENCODED_BOTH.getBody()). + withHeader(HttpServerTest.IG_RESPONSE_HEADER, HttpServerTest.IG_RESPONSE_HEADER_VALUE) // cheating + } + }, execContext) + } as Supplier) + .GET(REDIRECT.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(REDIRECT) { + Results.found(REDIRECT.getBody()) + } + }, execContext) + } as Supplier) + .GET(ERROR.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(ERROR) { + Results.status(ERROR.getStatus(), ERROR.getBody()) + } + }, execContext) + } as Supplier) + .GET(EXCEPTION.getPath()).routeAsync({ + CompletableFuture.supplyAsync({ + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.getBody()) + } + }, execContext) + } as Supplier) + .GET(CUSTOM_EXCEPTION.path).routeAsync({ + CompletableFuture.supplyAsync({ + controller(CUSTOM_EXCEPTION) { + throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.body) + } + }, execContext) + } as Supplier) + .GET("/path/:id/param").routeAsync(new PathParamAsyncFunction(execContext)) + .GET(USER_BLOCK.path).routeAsync({ + CompletableFuture.supplyAsync({ + -> + controller(USER_BLOCK) { + controller(USER_BLOCK) { + Blocking.forUser('user-to-block').blockIfMatch() + Results.status(200, "should never be reached") + } + } + }, execContext) + } as Supplier) + .POST(CREATED.path).routeAsync({ + -> + def body = body().asText().get() + CompletableFuture.supplyAsync({ + -> + controller(CREATED) { + Results.status(CREATED.status, "created: $body") + } + }, execContext) + } as Supplier) + .POST(BODY_URLENCODED.path).routeAsync({ + -> + def body = body().asFormUrlEncoded().get() + CompletableFuture.supplyAsync({ + -> + controller(BODY_URLENCODED) { + def javaMap = JavaConverters.mapAsJavaMapConverter(body).asJava() + def res = javaMap.collectEntries { + [it.key, JavaConverters.asJavaCollectionConverter(it.value).asJavaCollection()] + } + Results.status(BODY_URLENCODED.status, res as String) + } + }, execContext) + } as Supplier) + .POST(BODY_MULTIPART.path).routeAsync({ + -> + def body = body().asMultipartFormData().get().asFormUrlEncoded() + CompletableFuture.supplyAsync({ + -> + controller(BODY_MULTIPART) { + def javaMap = JavaConverters.mapAsJavaMapConverter(body).asJava() + def res = javaMap.collectEntries { + [it.key, JavaConverters.asJavaCollectionConverter(it.value).asJavaCollection()] + } + Results.status(BODY_MULTIPART.status, res as String) + } + }, execContext) + } as Supplier) + .POST(BODY_JSON.path).routeAsync({ + -> + JsValue json = body().asJson().get() + CompletableFuture.supplyAsync({ + -> + controller(BODY_JSON) { + Results.status(BODY_JSON.status, Json$.MODULE$.stringify(json)) + } + }, execContext) + } as Supplier) + .build() + } + + private static AnyContent body() { + Http.Context.current()._requestHeader().body + } + + @CompileStatic + static enum PathParamSyncFunction implements Function { + INSTANCE + + @Override + Result apply(Integer i) { + controller(PATH_PARAM) { + Results.ok(i as String) + } + } + } + + @CompileStatic + static class PathParamAsyncFunction implements Function> { + private final Executor executor + + PathParamAsyncFunction(Executor executor) { + this.executor = executor + } + + @Override + CompletionStage apply(Integer i) { + CompletableFuture.supplyAsync({ + controller(PATH_PARAM) { + Results.ok(i as String) + } + }, executor) + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayServerTest.groovy similarity index 52% rename from dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerTest.groovy rename to dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayServerTest.groovy index b19f41215b4..11038075b1c 100644 --- a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerTest.groovy +++ b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayServerTest.groovy @@ -1,4 +1,4 @@ -package server +package datadog.trace.instrumentation.play26.server import datadog.trace.agent.test.asserts.TraceAssert import datadog.trace.agent.test.base.HttpServer @@ -6,74 +6,21 @@ import datadog.trace.agent.test.base.HttpServerTest import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags import datadog.trace.bootstrap.instrumentation.api.Tags -import datadog.trace.instrumentation.akkahttp.AkkaHttpServerDecorator import datadog.trace.instrumentation.play26.PlayHttpServerDecorator -import play.BuiltInComponents -import play.mvc.Results -import play.routing.RoutingDsl +import groovy.transform.CompileStatic import play.server.Server -import java.util.function.Supplier - import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS class PlayServerTest extends HttpServerTest { @Override + @CompileStatic HttpServer server() { - return new PlayHttpServer({ BuiltInComponents components -> - RoutingDsl.fromComponents(components) - .GET(SUCCESS.getPath()).routeTo({ - controller(SUCCESS) { - Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) - } - } as Supplier) - .GET(FORWARDED.getPath()).routeTo({ - controller(FORWARDED) { - Results.status(FORWARDED.getStatus(), FORWARDED.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_PARAM.getPath()).routeTo({ - controller(QUERY_PARAM) { - Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_ENCODED_QUERY.getPath()).routeTo({ - controller(QUERY_ENCODED_QUERY) { - Results.status(QUERY_ENCODED_QUERY.getStatus(), QUERY_ENCODED_QUERY.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_ENCODED_BOTH.getRawPath()).routeTo({ - controller(QUERY_ENCODED_BOTH) { - Results.status(QUERY_ENCODED_BOTH.getStatus(), QUERY_ENCODED_BOTH.getBody()). - withHeader(IG_RESPONSE_HEADER, IG_RESPONSE_HEADER_VALUE) // cheating - } - } as Supplier) - .GET(REDIRECT.getPath()).routeTo({ - controller(REDIRECT) { - Results.found(REDIRECT.getBody()) - } - } as Supplier) - .GET(ERROR.getPath()).routeTo({ - controller(ERROR) { - Results.status(ERROR.getStatus(), ERROR.getBody()) - } - } as Supplier) - .GET(EXCEPTION.getPath()).routeTo({ - controller(EXCEPTION) { - throw new RuntimeException(EXCEPTION.getBody()) - } - } as Supplier) - .build() - }) + new PlayHttpServer(PlayRouters.&sync) } @Override @@ -83,12 +30,12 @@ class PlayServerTest extends HttpServerTest { @Override String component() { - return AkkaHttpServerDecorator.DECORATE.component() + 'akka-http-server' } @Override String expectedOperationName() { - return "akka-http.request" + 'akka-http.request' } @Override @@ -116,6 +63,51 @@ class PlayServerTest extends HttpServerTest { false } + @Override + boolean testRequestBody() { + true + } + + @Override + boolean isRequestBodyNoStreaming() { + true + } + + @Override + boolean testBodyUrlencoded() { + true + } + + @Override + boolean testBodyMultipart() { + true + } + + @Override + boolean testBodyJson() { + true + } + + @Override + boolean testBlocking() { + true + } + + @Override + boolean testBlockingOnResponse() { + true + } + + @Override + String testPathParam() { + '/path/?/param' + } + + @Override + Map expectedIGPathParams() { + ['0': 123] + } + @Override Class expectedExceptionType() { RuntimeException diff --git a/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayServerWithErrorHandlerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayServerWithErrorHandlerTest.groovy new file mode 100644 index 00000000000..9d9c1bd99db --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/baseTest/groovy/datadog/trace/instrumentation/play26/server/PlayServerWithErrorHandlerTest.groovy @@ -0,0 +1,46 @@ +package datadog.trace.instrumentation.play26.server + +import datadog.trace.agent.test.base.HttpServer + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION +import static org.junit.Assume.assumeTrue + +class PlayServerWithErrorHandlerTest extends PlayServerTest { + @Override + HttpServer server() { + new PlayHttpServer(PlayRouters.&sync, new TestHttpErrorHandler()) + } + + @Override + boolean testExceptionBody() { + true + } + + def "test exception with custom status"() { + setup: + assumeTrue(testException()) + def request = request(CUSTOM_EXCEPTION, 'GET', null).build() + def response = client.newCall(request).execute() + + expect: + response.code() == CUSTOM_EXCEPTION.status + if (testExceptionBody()) { + assert response.body().string() == CUSTOM_EXCEPTION.body + } + + and: + assertTraces(1) { + trace(spanCount(CUSTOM_EXCEPTION)) { + sortSpansByStart() + serverSpan(it, null, null, 'GET', CUSTOM_EXCEPTION) + if (hasHandlerSpan()) { + handlerSpan(it, CUSTOM_EXCEPTION) + } + controllerSpan(it, CUSTOM_EXCEPTION) + if (hasResponseSpan(CUSTOM_EXCEPTION)) { + responseSpan(it, CUSTOM_EXCEPTION) + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/test/java/server/TestHttpErrorHandler.java b/dd-java-agent/instrumentation/play-2.6/src/baseTest/java/datadog/trace/instrumentation/play26/server/TestHttpErrorHandler.java similarity index 58% rename from dd-java-agent/instrumentation/play-2.6/src/test/java/server/TestHttpErrorHandler.java rename to dd-java-agent/instrumentation/play-2.6/src/baseTest/java/datadog/trace/instrumentation/play26/server/TestHttpErrorHandler.java index c42f046976b..f4a4055480c 100644 --- a/dd-java-agent/instrumentation/play-2.6/src/test/java/server/TestHttpErrorHandler.java +++ b/dd-java-agent/instrumentation/play-2.6/src/baseTest/java/datadog/trace/instrumentation/play26/server/TestHttpErrorHandler.java @@ -1,16 +1,20 @@ -package server; +package datadog.trace.instrumentation.play26.server; import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import play.http.HttpErrorHandler; import play.mvc.Http.RequestHeader; import play.mvc.Result; import play.mvc.Results; public class TestHttpErrorHandler implements HttpErrorHandler { - static class CustomRuntimeException extends RuntimeException { + private static final Logger log = LoggerFactory.getLogger(TestHttpErrorHandler.class); + + public static class CustomRuntimeException extends RuntimeException { public CustomRuntimeException(String message) { super(message); } @@ -22,12 +26,17 @@ public CompletionStage onClientError( } public CompletionStage onServerError(RequestHeader request, Throwable exception) { + log.warn("server error", exception); + Throwable cause = exception.getCause(); - if (cause instanceof CustomRuntimeException) { + if (cause != null) { + exception = cause; + } + if (exception instanceof CustomRuntimeException) { return CompletableFuture.completedFuture( - Results.status(CUSTOM_EXCEPTION.getStatus(), cause.getMessage())); + Results.status(CUSTOM_EXCEPTION.getStatus(), exception.getMessage())); } - return CompletableFuture.completedFuture( - Results.internalServerError(exception.getCause().getMessage())); + + return CompletableFuture.completedFuture(Results.internalServerError(exception.getMessage())); } } diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerRoutesScalaWithErrorHandlerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerRoutesScalaWithErrorHandlerTest.groovy new file mode 100644 index 00000000000..49667fa0ba1 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerRoutesScalaWithErrorHandlerTest.groovy @@ -0,0 +1,111 @@ +package datadog.trace.instrumentation.play26.server.latestdep + +import datadog.trace.agent.test.asserts.TraceAssert +import datadog.trace.agent.test.base.HttpServer +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.DDTags +import datadog.trace.api.config.TraceInstrumentationConfig +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.instrumentation.play26.PlayHttpServerDecorator +import datadog.trace.instrumentation.play26.server.PlayServerWithErrorHandlerTest +import datadog.trace.instrumentation.play26.server.TestHttpErrorHandler +import play.api.BuiltInComponents +import play.libs.concurrent.ClassLoaderExecution +import play.routing.Router +import spock.lang.Shared + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.function.Function + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.NOT_FOUND +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS + +class PlayAsyncServerRoutesScalaWithErrorHandlerTest extends PlayServerWithErrorHandlerTest { + @Shared + ExecutorService executor + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + // otherwise the resource name will be set from the raw path with higher priority. + // We expect non-raw (decoded) paths in resource names + // Bug in HttpResourceDecorator.withRoute(AgentSpan, CharSequence, CharSequence, boolean) + // by not checking value of http.server.raw.resource ? + injectSysConfig(TraceInstrumentationConfig.HTTP_SERVER_ROUTE_BASED_NAMING, 'false') + } + + void cleanupSpec() { + executor.shutdown() + } + + @Override + HttpServer server() { + executor = Executors.newCachedThreadPool() + + Function f = { play.api.BuiltInComponentsFromContext it -> + new router.Routes( + it.httpErrorHandler(), + new PlayController(it.controllerComponents(), ClassLoaderExecution.fromThread(executor)) + ) + } as Function + new PlayHttpServerScala(f, new TestHttpErrorHandler()) + } + + @Override + Map expectedIGPathParams() { + [path: '123'] + } + + private String routeFor(HttpServerTest.ServerEndpoint endpoint) { + + switch (endpoint) { + case PATH_PARAM: + return '/path/$path<[^/]+>/param' + case NOT_FOUND: + return null + default: + endpoint.path + } + } + + @Override + Map expectedExtraServerTags(HttpServerTest.ServerEndpoint endpoint) { + [(Tags.HTTP_ROUTE): routeFor(endpoint)] + } + + // we set Tags.HTTP_ROUTE with a routes file; override method for this + @Override + void handlerSpan(TraceAssert trace, HttpServerTest.ServerEndpoint endpoint = SUCCESS) { + def expectedQueryTag = expectedQueryTag(endpoint) + trace.span { + serviceName expectedServiceName() + operationName "play.request" + spanType DDSpanTypes.HTTP_SERVER + errored endpoint == EXCEPTION || endpoint == CUSTOM_EXCEPTION + childOfPrevious() + tags { + "$Tags.COMPONENT" PlayHttpServerDecorator.DECORATE.component() + "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER + "$Tags.PEER_HOST_IPV4" { it == (endpoint == FORWARDED ? endpoint.body : "127.0.0.1") } + "$Tags.HTTP_CLIENT_IP" { it == (endpoint == FORWARDED ? endpoint.body : "127.0.0.1") } + "$Tags.HTTP_URL" String + "$Tags.HTTP_HOSTNAME" address.host + "$Tags.HTTP_METHOD" String + "$Tags.HTTP_ROUTE" this.routeFor(endpoint) + if (endpoint == EXCEPTION || endpoint == CUSTOM_EXCEPTION) { + errorTags(endpoint == CUSTOM_EXCEPTION ? TestHttpErrorHandler.CustomRuntimeException : RuntimeException, endpoint.body) + } + if (endpoint.query) { + "$DDTags.HTTP_QUERY" expectedQueryTag + } + defaultTags() + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerScalaWithErrorHandlerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerScalaWithErrorHandlerTest.groovy new file mode 100644 index 00000000000..f1efed6da8b --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayAsyncServerScalaWithErrorHandlerTest.groovy @@ -0,0 +1,36 @@ +package datadog.trace.instrumentation.play26.server.latestdep + +import datadog.trace.agent.test.base.HttpServer +import datadog.trace.instrumentation.play26.server.PlayServerWithErrorHandlerTest +import datadog.trace.instrumentation.play26.server.TestHttpErrorHandler +import play.api.BuiltInComponents +import play.routing.Router +import spock.lang.Shared + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.function.Function + +class PlayAsyncServerScalaWithErrorHandlerTest extends PlayServerWithErrorHandlerTest { + @Shared + ExecutorService executor + + def cleanupSpec() { + executor.shutdown() + } + + @Override + HttpServer server() { + executor = Executors.newCachedThreadPool() + + Function f = { play.api.BuiltInComponents it -> + PlayRoutersScala$.MODULE$.async(executor, it) + } as Function + new PlayHttpServerScala(f, new TestHttpErrorHandler()) + } + + @Override + Map expectedIGPathParams() { + ['0': '123'] + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayHttpServerScala.groovy b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayHttpServerScala.groovy new file mode 100644 index 00000000000..2a75fca703f --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayHttpServerScala.groovy @@ -0,0 +1,97 @@ +package datadog.trace.instrumentation.play26.server.latestdep + +import datadog.trace.agent.test.base.HttpServer +import groovy.transform.CompileStatic +import play.Mode +import play.api.ApplicationLoader +import play.api.BuiltInComponents +import play.api.BuiltInComponentsFromContext +import play.api.Configuration +import play.api.Environment +import play.api.Play +import play.api.inject.DefaultApplicationLifecycle +import play.api.routing.Router +import play.core.j.JavaHttpErrorHandlerAdapter +import play.core.server.AkkaHttpServerProvider +import play.core.server.Server +import play.core.server.ServerConfig +import play.http.HttpErrorHandler +import play.mvc.EssentialFilter +import scala.None$ +import scala.Option +import scala.collection.Map$ +import scala.collection.immutable.Map +import scala.collection.immutable.Seq + +import java.util.concurrent.TimeoutException +import java.util.function.Function + +@CompileStatic +class PlayHttpServerScala implements HttpServer { + final Function router + HttpErrorHandler httpErrorHandler + def application + def server + def port + + PlayHttpServerScala(Function router, HttpErrorHandler httpErrorHandler) { + this.router = router + this.httpErrorHandler = httpErrorHandler + } + + @Override + void start() throws TimeoutException { + Environment environment = Environment.simple(new File('.'), play.api.Mode.Test$.MODULE$) + ApplicationLoader.Context context = ApplicationLoader.Context$.MODULE$.create( + environment, + Map$.MODULE$.empty() as Map, + new DefaultApplicationLifecycle(), + None$.empty() + ) + application = new BuiltInComponentsFromContext(context) { + @Override + Router router() { + router.apply(this) + } + + @Override + Seq httpFilters() { + (Seq) scala.collection.immutable.Seq$.MODULE$.empty() + } + + @Override + play.api.http.HttpErrorHandler httpErrorHandler() { + if (httpErrorHandler != null) { + return new JavaHttpErrorHandlerAdapter(httpErrorHandler) + } + super.httpErrorHandler() + } + }.application() + Play.start(this.application as play.api.Application) + + def config = new ServerConfig( + new File('.'), + Option.apply(0) as Option, + Option.empty() as Option, + '0.0.0.0', + Mode.PROD.asScala(), + System.getProperties(), + Configuration.load(environment) + ) + + Server server = new AkkaHttpServerProvider().createServer(config, + application as play.api.Application) + this.server = server + port = server.httpPort().get() + } + + @Override + void stop() { + (this.server as Server).stop() + } + + @Override + URI address() { + new URI("http://localhost:$port/") + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayRouters.groovy b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayRouters.groovy new file mode 100644 index 00000000000..c9773648e1b --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/groovy/datadog/trace/instrumentation/play26/server/latestdep/PlayRouters.groovy @@ -0,0 +1,266 @@ +package datadog.trace.instrumentation.play26.server.latestdep + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import datadog.appsec.api.blocking.Blocking +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.instrumentation.play26.server.TestHttpErrorHandler +import groovy.transform.CompileStatic +import play.BuiltInComponents +import play.libs.concurrent.ClassLoaderExecution +import play.mvc.Http +import play.mvc.Result +import play.mvc.Results +import play.routing.RequestFunctions +import play.routing.Router +import play.routing.RoutingDsl +import scala.concurrent.ExecutionContextExecutor + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage +import java.util.concurrent.ExecutorService + +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_JSON +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_MULTIPART +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.BODY_URLENCODED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CREATED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.PATH_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS +import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.USER_BLOCK +import static datadog.trace.agent.test.base.HttpServerTest.controller + +// TODO: a lot of this routes don't exercise query/parameter extraction, when they should +class PlayRouters { + static Router sync(BuiltInComponents components) { + RoutingDsl.fromComponents(components) + .GET(SUCCESS.path).routingTo({ + controller(SUCCESS) { + Results.ok(SUCCESS.body) + } + } as RequestFunctions.Params0) + .GET(FORWARDED.path).routingTo({ req -> + controller(FORWARDED) { + Results.status(FORWARDED.status, req.header('X-Forwarded-For').orElse('(no header)')) + } + } as RequestFunctions.Params0) + .GET(QUERY_PARAM.path).routingTo({ req -> + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.status, "some=${req.queryString('some').orElse('(null)')}") + } + } as RequestFunctions.Params0) + .GET(QUERY_ENCODED_QUERY.path).routingTo({ req -> + controller(QUERY_ENCODED_QUERY) { + Results.status(QUERY_ENCODED_QUERY.status, "some=${req.queryString('some').orElse('(null)')}") + } + } as RequestFunctions.Params0) + .GET(QUERY_ENCODED_BOTH.rawPath).routingTo({ req -> + controller(QUERY_ENCODED_BOTH) { + Results.status(QUERY_ENCODED_BOTH.status, "some=${req.queryString('some').orElse('(null)')}"). + withHeader(HttpServerTest.IG_RESPONSE_HEADER, HttpServerTest.IG_RESPONSE_HEADER_VALUE) + } + } as RequestFunctions.Params0) + .GET(REDIRECT.path).routingTo({ + controller(REDIRECT) { + Results.found(REDIRECT.body) + } + } as RequestFunctions.Params0) + .GET(ERROR.path).routingTo({ + controller(ERROR) { + Results.status(ERROR.status, ERROR.body) + } + } as RequestFunctions.Params0) + .GET(EXCEPTION.path).routingTo({ + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.body) + } + } as RequestFunctions.Params0) + .GET(CUSTOM_EXCEPTION.path).routingTo({ + controller(CUSTOM_EXCEPTION) { + throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.body) + } + } as RequestFunctions.Params0) + .GET('/path/:id/param').routingTo(PathParamHandlerSync.INSTANCE) + .GET(USER_BLOCK.path).routingTo({ + controller(USER_BLOCK) { + Blocking.forUser('user-to-block').blockIfMatch() + Results.status(200, "should never be reached") + } + } as RequestFunctions.Params0) + .POST(CREATED.path).routingTo({ Http.Request req -> + controller(CREATED) { + String body = req.body().asText() + Results.created("created: $body") + } + } as RequestFunctions.Params0) + .POST(BODY_URLENCODED.path).routingTo({ Http.Request req -> + controller(BODY_URLENCODED) { + Map body = req.body()asFormUrlEncoded() + Results.status(BODY_URLENCODED.status, body as String) + } + } as RequestFunctions.Params0) + .POST(BODY_MULTIPART.path).routingTo({ Http.Request req -> + controller(BODY_MULTIPART) { + Map body = req.body().asMultipartFormData().asFormUrlEncoded() + Results.status(BODY_MULTIPART.status, body as String) + } + } as RequestFunctions.Params0) + .POST(BODY_JSON.path).routingTo({ Http.Request req -> + controller(BODY_JSON) { + JsonNode json = req.body().asJson() + Results.status(BODY_JSON.status, new ObjectMapper().writeValueAsString(json)) + } + } as RequestFunctions.Params0) + .build() + } + + @CompileStatic + private static enum PathParamHandlerSync implements RequestFunctions.Params1 { + INSTANCE + + @Override + Result apply(Http.Request request, Integer id) { + controller(PATH_PARAM) { + Results.ok(id as String) + } + } + } + + static Router async(ExecutorService executor, BuiltInComponents components) { + ExecutionContextExecutor execContext = ClassLoaderExecution.fromThread(executor) + RoutingDsl.fromComponents(components) + .GET(SUCCESS.path).routingAsync({ + CompletableFuture.supplyAsync({ + controller(SUCCESS) { + Results.ok(SUCCESS.body) + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(FORWARDED.path).routingAsync({ req -> + CompletableFuture.supplyAsync({ + controller(FORWARDED) { + Results.status(FORWARDED.status, req.header('X-Forwarded-For').orElse('(no header)')) + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(QUERY_PARAM.path).routingAsync({ req -> + CompletableFuture.supplyAsync({ + controller(QUERY_PARAM) { + Results.status(QUERY_PARAM.status, "some=${req.queryString('some').orElse('(null)')}") + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(QUERY_ENCODED_QUERY.path).routingAsync({ req -> + CompletableFuture.supplyAsync({ + controller(QUERY_ENCODED_QUERY) { + Results.status(QUERY_ENCODED_QUERY.status, "some=${req.queryString('some').orElse('(null)')}") + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(QUERY_ENCODED_BOTH.getRawPath()).routingAsync({ req -> + CompletableFuture.supplyAsync({ + controller(QUERY_ENCODED_BOTH) { + Results.status(QUERY_ENCODED_BOTH.status, "some=${req.queryString('some').orElse('(null)')}"). + withHeader(HttpServerTest.IG_RESPONSE_HEADER, HttpServerTest.IG_RESPONSE_HEADER_VALUE) // cheating + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(REDIRECT.path).routingAsync({ + CompletableFuture.supplyAsync({ + controller(REDIRECT) { + Results.found(REDIRECT.body) + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(ERROR.path).routingAsync({ + CompletableFuture.supplyAsync({ + controller(ERROR) { + Results.status(ERROR.status, ERROR.body) + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(EXCEPTION.path).routingAsync({ + CompletableFuture.supplyAsync({ + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.body) + } + }, execContext) + } as RequestFunctions.Params0>) + .GET(CUSTOM_EXCEPTION.path).routingAsync({ + CompletableFuture.supplyAsync({ + controller(CUSTOM_EXCEPTION) { + throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.body) + } + }, execContext) + } as RequestFunctions.Params0>) + .GET('/path/:id/param').routingAsync(PathParamHandlerAsync.INSTANCE) + .GET(USER_BLOCK.path).routingAsync({ + CompletableFuture.supplyAsync({ + -> + controller(USER_BLOCK) { + Blocking.forUser('user-to-block').blockIfMatch() + Results.status(200, "should never be reached") + } + }, execContext) + } as RequestFunctions.Params0>) + .POST(CREATED.path).routingAsync({ Http.Request req -> + def body = req.body().asText() + CompletableFuture.supplyAsync({ + -> + controller(CREATED) { + Results.created("created: $body") + } + }, execContext) + } as RequestFunctions.Params0>) + .POST(BODY_URLENCODED.path).routingAsync({ Http.Request req -> + CompletableFuture.supplyAsync({ + -> + def body = req.body().asFormUrlEncoded() + controller(BODY_URLENCODED) { + Results.status(BODY_URLENCODED.status, body as String) + } + }, execContext) + } as RequestFunctions.Params0>) + .POST(BODY_MULTIPART.path).routingAsync({ + Map body = it.body().asMultipartFormData().asFormUrlEncoded() + CompletableFuture.supplyAsync({ + -> + controller(BODY_MULTIPART) { + Results.status(BODY_MULTIPART.status, body as String) + } + }, execContext) + } as RequestFunctions.Params0>) + .POST(BODY_JSON.path).routingAsync({ Http.Request req -> + JsonNode json = req.body().asJson() + CompletableFuture.supplyAsync({ + -> + controller(BODY_JSON) { + Results.status(BODY_JSON.status, new ObjectMapper().writeValueAsString(json)) + } + }, execContext) + } as RequestFunctions.Params0>) + .build() + } + + @CompileStatic + private static enum PathParamHandlerAsync implements RequestFunctions.Params1> { + INSTANCE + + @Override + CompletionStage apply(Http.Request request, Integer id) { + CompletableFuture.supplyAsync({ + -> + controller(PATH_PARAM) { + Results.ok(id as String) + } + }) + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/routes/conf/routes b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/routes/conf/routes new file mode 100644 index 00000000000..3fa95e8631a --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/routes/conf/routes @@ -0,0 +1,16 @@ +GET /success datadog.trace.instrumentation.play26.server.latestdep.PlayController.success() +GET /redirect datadog.trace.instrumentation.play26.server.latestdep.PlayController.redirect() +GET /forwarded datadog.trace.instrumentation.play26.server.latestdep.PlayController.forwarded() +GET /error-status datadog.trace.instrumentation.play26.server.latestdep.PlayController.errorStatus() +GET /exception datadog.trace.instrumentation.play26.server.latestdep.PlayController.exception() +GET /custom-exception datadog.trace.instrumentation.play26.server.latestdep.PlayController.customException() +GET /not-here datadog.trace.instrumentation.play26.server.latestdep.PlayController.notHere() +GET /user-block datadog.trace.instrumentation.play26.server.latestdep.PlayController.userBlock() +GET /query datadog.trace.instrumentation.play26.server.latestdep.PlayController.query(some: String) +GET /encoded_query datadog.trace.instrumentation.play26.server.latestdep.PlayController.encodedQuery(some: String) +GET /encoded%20path%20query datadog.trace.instrumentation.play26.server.latestdep.PlayController.encodedPathQuery(some: String) +GET /path/:path/param datadog.trace.instrumentation.play26.server.latestdep.PlayController.pathParam(path: Integer) +POST /created datadog.trace.instrumentation.play26.server.latestdep.PlayController.created() +POST /body-urlencoded datadog.trace.instrumentation.play26.server.latestdep.PlayController.bodyUrlencoded() +POST /body-multipart datadog.trace.instrumentation.play26.server.latestdep.PlayController.bodyMultipart() +POST /body-json datadog.trace.instrumentation.play26.server.latestdep.PlayController.bodyJson() diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/ImplicitConversions.scala b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/ImplicitConversions.scala new file mode 100644 index 00000000000..701f93d0c41 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/ImplicitConversions.scala @@ -0,0 +1,17 @@ +package datadog.trace.instrumentation.play26.server.latestdep + +import scala.collection.Seq + +object ImplicitConversions { + implicit class MapExtensions[A](m: Iterable[(String, A)]) { + def toStringAsGroovy: String = { + def valueToString(value: Object) : String = value match { + case seq: Seq[_] => seq.map(x => valueToString(x.asInstanceOf[Object])).mkString("[", ",", "]") + case other => other.toString + } + + m.map { case (key, value) => s"$key:${valueToString(value.asInstanceOf[Object])}" } + .mkString("[", ",", "]") + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayController.scala b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayController.scala new file mode 100644 index 00000000000..cb9c82aa748 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayController.scala @@ -0,0 +1,94 @@ +package datadog.trace.instrumentation.play26.server.latestdep + +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.base.HttpServerTest.{ServerEndpoint, getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE} +import datadog.appsec.api.blocking.Blocking +import datadog.trace.instrumentation.play26.server.TestHttpErrorHandler +import datadog.trace.instrumentation.play26.server.latestdep.ImplicitConversions.MapExtensions +import groovy.lang.Closure +import play.api.libs.json.{JsNull, JsValue, Json} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +class PlayController(cc: ControllerComponents)(implicit ec: ExecutionContext) extends AbstractController(cc) { + def success() = controller(ServerEndpoint.SUCCESS) { _ => + Results.Ok(ServerEndpoint.SUCCESS.getBody) + } + + def redirect = controller(ServerEndpoint.REDIRECT) { _ => + Results.Redirect(ServerEndpoint.REDIRECT.getBody, 302) + } + + def forwarded = controller(ServerEndpoint.FORWARDED) { request => + Results.Status(ServerEndpoint.FORWARDED.getStatus)(request.headers.get("X-Forwarded-For").getOrElse("(no header)")) + } + + def errorStatus = controller(ServerEndpoint.ERROR) { _ => + Results.Status(ServerEndpoint.ERROR.getStatus)(ServerEndpoint.ERROR.getBody) + } + + def exception = controller(ServerEndpoint.EXCEPTION) { _ => + throw new RuntimeException(ServerEndpoint.EXCEPTION.getBody) + } + + def customException = controller(ServerEndpoint.CUSTOM_EXCEPTION) { _ => + throw new TestHttpErrorHandler.CustomRuntimeException(ServerEndpoint.CUSTOM_EXCEPTION.getBody) + } + + def userBlock = controller(ServerEndpoint.USER_BLOCK) { _ => + Blocking.forUser("user-to-block").blockIfMatch() + Results.Ok("should never be reached") + } + + def query(some: String) = controller(ServerEndpoint.QUERY_PARAM) { _ => + Results.Status(ServerEndpoint.QUERY_PARAM.getStatus)(s"some=$some") + } + + def encodedQuery(some: String) = controller(ServerEndpoint.QUERY_ENCODED_QUERY) { _ => + Results.Status(ServerEndpoint.QUERY_ENCODED_QUERY.getStatus)(s"some=$some") + } + + def encodedPathQuery(some: String) = controller(ServerEndpoint.QUERY_ENCODED_BOTH) { _ => + Results.Status(ServerEndpoint.QUERY_ENCODED_BOTH.getStatus)(s"some=$some") + } + + def notHere = controller(ServerEndpoint.NOT_HERE) { _ => + Results.NotFound(ServerEndpoint.NOT_HERE.getBody) + } + + def pathParam(id: Integer) = controller(ServerEndpoint.PATH_PARAM) { _ => + Results.Ok(id.toString) + } + + def created = controller(ServerEndpoint.CREATED) { request => + val body: String = request.body.asText.getOrElse("") + Results.Created(s"created: $body") + } + + def bodyUrlencoded = controller(ServerEndpoint.BODY_URLENCODED) { request => + val body: Map[String, Seq[String]] = request.body.asFormUrlEncoded.getOrElse(Map.empty) + Results.Ok(body.toStringAsGroovy) + } + + def bodyMultipart = controller(ServerEndpoint.BODY_MULTIPART) { request => + val body: Map[String, Seq[String]] = request.body.asMultipartFormData.map(_.asFormUrlEncoded).getOrElse(Map.empty) + Results.Ok(body.toStringAsGroovy) + } + + def bodyJson = controller(ServerEndpoint.BODY_JSON) { request => + val body: JsValue = request.body.asJson.getOrElse(JsNull) + Results.Ok(Json.stringify(body)) + } + + private def controller(endpoint: ServerEndpoint)(block: Request[AnyContent] => Result) : Action[AnyContent] = { + Action.async { request => + Future { + HttpServerTest.controller(endpoint, new Closure[Result](this) { + def doCall() = block(request).withHeaders( + (getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE)) + }) + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayRoutersScala.scala b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayRoutersScala.scala new file mode 100644 index 00000000000..6a319eee403 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/latestDepTest/scala/datadog/trace/instrumentation/play26/server/latestdep/PlayRoutersScala.scala @@ -0,0 +1,155 @@ +package datadog.trace.instrumentation.play26.server.latestdep + +import datadog.appsec.api.blocking.Blocking +import datadog.trace.agent.test.base.HttpServerTest +import datadog.trace.agent.test.base.HttpServerTest.{ServerEndpoint, getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE} +import datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint._ +import datadog.trace.instrumentation.play26.server.TestHttpErrorHandler.CustomRuntimeException +import datadog.trace.instrumentation.play26.server.latestdep.ImplicitConversions.MapExtensions +import groovy.lang.Closure +import play.api.BuiltInComponents +import play.api.libs.json._ +import play.api.mvc._ +import play.api.routing.Router +import play.api.routing.sird._ + +import java.util.concurrent.ExecutorService +import scala.concurrent.{ExecutionContext, Future} + +object PlayRoutersScala { + + def async(executor: ExecutorService)(components: BuiltInComponents): Router = { + val ec: ExecutionContext = ExecutionContext.fromExecutor(executor) + val parser = components.defaultBodyParser + + import components._ + + def controller(endpoint: ServerEndpoint)(block: => Result): Future[Result] = { + Future { + HttpServerTest.controller(endpoint, new Closure[Result](this) { + def doCall(): Result = block.withHeaders( + (getIG_RESPONSE_HEADER, getIG_RESPONSE_HEADER_VALUE)) + }) + }(ec) + } + + Router.from { + case GET(p"/success") => + defaultActionBuilder.async { + controller(SUCCESS) { + Results.Ok(SUCCESS.getBody) + } + } + + case GET(p"/redirect") => + defaultActionBuilder.async { + controller(REDIRECT) { + Results.Redirect(REDIRECT.getBody, 302) + } + } + + case GET(p"/forwarded") => + defaultActionBuilder.async { request => + controller(FORWARDED) { + Results.Status(FORWARDED.getStatus)(request.headers.get("X-Forwarded-For").getOrElse("(no header)")) + } + } + + // endpoint is not special; error results are returned the same way + case GET(p"/error-status") => + defaultActionBuilder.async { + controller(ERROR) { + Results.Status(ERROR.getStatus)(ERROR.getBody) + } + } + + case GET(p"/exception") => + defaultActionBuilder.async { + controller(EXCEPTION) { + throw new RuntimeException(EXCEPTION.getBody) + } + } + + case GET(p"/custom-exception") => + defaultActionBuilder.async { + controller(CUSTOM_EXCEPTION) { + throw new CustomRuntimeException(CUSTOM_EXCEPTION.getBody) + } + } + + case GET(p"/not-here") => + defaultActionBuilder.async { + controller(NOT_HERE) { + Results.NotFound + } + } + + case GET(p"/user-block") => + defaultActionBuilder.async { + controller(USER_BLOCK) { + Blocking.forUser("user-to-block").blockIfMatch() + Results.Ok("should never be reached") + } + } + + case GET(p"/query" ? q"some=$some") => + defaultActionBuilder.async { + controller(QUERY_PARAM) { + Results.Status(QUERY_PARAM.getStatus)(s"some=$some") + } + } + + case GET(p"/encoded_query" ? q"some=$some") => + defaultActionBuilder.async { + controller(QUERY_ENCODED_QUERY) { + Results.Status(QUERY_ENCODED_QUERY.getStatus)(s"some=$some") + } + } + + case GET(p"/encoded%20path%20query" ? q"some=$some") => + defaultActionBuilder.async { + controller(QUERY_ENCODED_BOTH) { + Results.Status(QUERY_ENCODED_BOTH.getStatus)(s"some=$some") + } + } + + case GET(p"/path/$path/param") => + defaultActionBuilder.async { + controller(PATH_PARAM) { + Results.Status(PATH_PARAM.getStatus)(path) + } + } + + case POST(p"/created") => defaultActionBuilder.async(parser) { request => + controller(CREATED) { + val body: String = request.body.asText.getOrElse("") + Results.Created(s"created: $body") + } + } + + case POST(p"/body-urlencoded") => defaultActionBuilder.async(parser) { request => + controller(BODY_URLENCODED) { + val body: Map[String, Seq[String]] = request.body.asFormUrlEncoded.getOrElse(Map.empty) + Results.Ok(body.toStringAsGroovy) + } + } + + case POST(p"/body-multipart") => defaultActionBuilder.async(parser) { request => + controller(BODY_MULTIPART) { + val body: Map[String, scala.Seq[String]] = request.body.asMultipartFormData.getOrElse( + MultipartFormData(Map.empty, scala.Seq.empty, scala.Seq.empty) + ).asFormUrlEncoded + Results.Ok(body.toStringAsGroovy) + } + } + + case POST(p"/body-json") => defaultActionBuilder.async(parser) { request => + controller(BODY_JSON) { + val body: JsValue = request.body.asJson.getOrElse(JsNull) + Results.Ok(Json.stringify(body)) + } + } + } + } + +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/MuzzleReferences.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/MuzzleReferences.java new file mode 100644 index 00000000000..227d1fdf3bc --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/MuzzleReferences.java @@ -0,0 +1,16 @@ +package datadog.trace.instrumentation.play26; + +import datadog.trace.agent.tooling.muzzle.Reference; + +public class MuzzleReferences { + private MuzzleReferences() {} + + public static final Reference[] PLAY_26_PLUS = + new Reference[] {new Reference.Builder("play.components.BodyParserComponents").build()}; + + public static final Reference[] PLAY_26_ONLY = + new Reference[] { + new Reference.Builder("play.components.BodyParserComponents").build(), + new Reference.Builder("play.Configuration").build() + }; +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java index 4ff4bfd99f3..9e4fd7c8246 100644 --- a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java @@ -134,7 +134,7 @@ public AgentSpan onRequest( CharSequence path = PATH_CACHE.computeIfAbsent( defOption.get().path(), p -> addMissingSlash(p, request.path())); - HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path); + HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path, true); } } return span; diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayInstrumentation.java index 84f2f62c104..bc8dc3a9788 100644 --- a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayInstrumentation.java +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayInstrumentation.java @@ -18,6 +18,11 @@ public PlayInstrumentation() { super("play"); } + @Override + public String muzzleDirective() { + return "play26Plus"; + } + @Override public String hierarchyMarkerType() { return "play.api.mvc.Action"; diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/ArgumentCaptureWrappers.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/ArgumentCaptureWrappers.java new file mode 100644 index 00000000000..853fbb39f4a --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/ArgumentCaptureWrappers.java @@ -0,0 +1,130 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import play.libs.F; + +public class ArgumentCaptureWrappers { + public static class ArgumentCaptureFunction implements Function { + private final Function delegate; + + public ArgumentCaptureFunction(Function delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Object o) { + if (o == null) { + return delegate.apply(null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(o); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(o); + } + + Map conv = Collections.singletonMap("0", o); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routeTo"); + if (t != null) { + throw t; + } + + return delegate.apply(o); + } + } + + public static class ArgumentCaptureBiFunction implements BiFunction { + private final BiFunction delegate; + + public ArgumentCaptureBiFunction(BiFunction delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Object o1, Object o2) { + if (o1 == null && o2 == null) { + return delegate.apply(null, null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(o1, o2); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(o1, o2); + } + + Map conv = new HashMap<>(); + conv.put("0", o1); + conv.put("1", o2); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routeTo"); + if (t != null) { + throw t; + } + + return delegate.apply(o1, o2); + } + } + + public static class ArgumentCaptureFunction3 + implements F.Function3 { + private final F.Function3 delegate; + + public ArgumentCaptureFunction3(F.Function3 delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Object o1, Object o2, Object o3) throws Throwable { + if (o1 == null && o2 == null && o3 == null) { + return delegate.apply(null, null, null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(o1, o2, o3); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(o1, o2, o3); + } + + Map conv = new HashMap<>(); + conv.put("0", o1); + conv.put("1", o2); + conv.put("2", o3); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routeTo"); + if (t != null) { + throw t; + } + + return delegate.apply(o1, o2, o3); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/BodyParserHelpers.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/BodyParserHelpers.java new file mode 100644 index 00000000000..8ace882c0a3 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/BodyParserHelpers.java @@ -0,0 +1,303 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.api.gateway.Events.EVENTS; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import play.api.libs.json.JsArray; +import play.api.libs.json.JsBoolean; +import play.api.libs.json.JsNumber; +import play.api.libs.json.JsObject; +import play.api.libs.json.JsString; +import play.api.libs.json.JsValue; +import play.api.mvc.MultipartFormData; +import scala.Function1; +import scala.Tuple2; +import scala.collection.Iterable; +import scala.collection.Iterator; +import scala.collection.Seq; +import scala.compat.java8.JFunction1; + +public class BodyParserHelpers { + + public static final int MAX_CONVERSION_DEPTH = 10; + private static final Logger log = LoggerFactory.getLogger(BodyParserHelpers.class); + public static final int MAX_RECURSION = 15; + + private static JFunction1< + scala.collection.immutable.Map>, + scala.collection.immutable.Map>> + HANDLE_URL_ENCODED = BodyParserHelpers::handleUrlEncoded; + private static JFunction1 HANDLE_TEXT = BodyParserHelpers::handleText; + private static JFunction1, MultipartFormData> HANDLE_MULTIPART_FORM_DATA = + BodyParserHelpers::handleMultipartFormData; + private static JFunction1 HANDLE_JSON = BodyParserHelpers::handleJson; + + private BodyParserHelpers() {} + + public static Function1< + scala.collection.immutable.Map>, + scala.collection.immutable.Map>> + getHandleUrlEncodedMapF() { + return HANDLE_URL_ENCODED; + } + + private static scala.collection.immutable.Map> handleUrlEncoded( + scala.collection.immutable.Map> data) { + if (data == null || data.isEmpty()) { + return data; + } + + try { + Object conv = tryConvertingScalaContainers(data, MAX_CONVERSION_DEPTH); + handleArbitraryPostData(conv, "tolerantFormUrlEncoded"); + } catch (Exception e) { + handleException(e, "Error handling result of tolerantFormUrlEncoded BodyParser"); + } + return data; + } + + public static Function1 getHandleStringMapF() { + return HANDLE_TEXT; + } + + private static String handleText(String s) { + if (s == null || s.isEmpty()) { + return s; + } + + try { + handleArbitraryPostData(s, "tolerantText"); + } catch (Exception e) { + handleException(e, "Error handling result of tolerantText BodyParser"); + } + + return s; + } + + public static Function1, MultipartFormData> + getHandleMultipartFormDataF() { + return HANDLE_MULTIPART_FORM_DATA; + } + + private static MultipartFormData handleMultipartFormData(MultipartFormData data) { + scala.collection.immutable.Map> mpfd = data.asFormUrlEncoded(); + + if (mpfd == null || mpfd.isEmpty()) { + return data; + } + + try { + Object conv = tryConvertingScalaContainers(mpfd, MAX_CONVERSION_DEPTH); + handleArbitraryPostData(conv, "multipartFormData"); + } catch (Exception e) { + handleException(e, "Error handling result of multipartFormData BodyParser"); + } + return data; + } + + public static Function1 getHandleJsonF() { + return HANDLE_JSON; + } + + private static JsValue handleJson(JsValue data) { + if (data == null) { + return null; + } + + try { + Object conv = jsValueToJavaObject(data, MAX_RECURSION); + handleArbitraryPostData(conv, "json"); + } catch (Exception e) { + handleException(e, "Error handling result of json BodyParser"); + } + return data; + } + + private static void executeCallback( + RequestContext reqCtx, + BiFunction> callback, + Object conv, + String details) { + Flow flow = callback.apply(reqCtx, conv); + Flow.Action action = flow.getAction(); + if (action instanceof Flow.Action.RequestBlockingAction) { + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction(); + if (blockResponseFunction != null) { + boolean success = + blockResponseFunction.tryCommitBlockingResponse( + reqCtx.getTraceSegment(), + rba.getStatusCode(), + rba.getBlockingContentType(), + rba.getExtraHeaders()); + if (success) { + throw new BlockingException("Blocked request (for " + details + ")"); + } + } + } + } + + private static Object tryConvertingScalaContainers(Object obj, int depth) { + if (depth == 0) { + return obj; + } + if (obj instanceof scala.collection.Map) { + scala.collection.Map map = (scala.collection.Map) obj; + Map ret = new HashMap<>(); + Iterator iterator = map.iterator(); + while (iterator.hasNext()) { + Tuple2 next = iterator.next(); + ret.put(next._1(), tryConvertingScalaContainers(next._2(), depth - 1)); + } + return ret; + } else if (obj instanceof Iterable) { + List ret = new ArrayList<>(); + Iterator iterator = ((Iterable) obj).iterator(); + while (iterator.hasNext()) { + Object next = iterator.next(); + ret.add(tryConvertingScalaContainers(next, depth - 1)); + } + return ret; + } + return obj; + } + + public static void handleJsonNode(JsonNode n, String source) { + Object o = jsNodeToJavaObject(n, MAX_RECURSION); + handleArbitraryPostDataWithSpanError(o, source); + } + + public static void handleArbitraryPostDataWithSpanError(Object o, String source) { + AgentSpan span = activeSpan(); + try { + doHandleArbitraryPostData(span, o, source); + } catch (BlockingException be) { + span.addThrowable(be); + throw be; + } + } + + public static void handleArbitraryPostData(Object o, String source) { + doHandleArbitraryPostData(activeSpan(), o, source); + } + + public static void doHandleArbitraryPostData(AgentSpan span, Object o, String source) { + RequestContext reqCtx; + if (span == null + || (reqCtx = span.getRequestContext()) == null + || reqCtx.getData(RequestContextSlot.APPSEC) == null) { + return; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction> callback = + cbp.getCallback(EVENTS.requestBodyProcessed()); + if (callback == null) { + return; + } + + // callback execution + executeCallback(reqCtx, callback, o, source); + } + + private static void handleException(Exception e, String logMessage) { + if (e instanceof BlockingException) { + throw (BlockingException) e; + } + + log.warn(logMessage, e); + } + + private static Object jsValueToJavaObject(JsValue value, int maxRecursion) { + if (value == null || maxRecursion <= 0) { + return null; + } + + if (value instanceof JsString) { + return ((JsString) value).value(); + } else if (value instanceof JsNumber) { + return ((JsNumber) value).value(); + } else if (value instanceof JsBoolean) { + return ((JsBoolean) value).value(); + } else if (value instanceof JsObject) { + Map map = new HashMap<>(); + JsObject jsonObject = (JsObject) value; + Iterator> iterator = jsonObject.fields().iterator(); + while (iterator.hasNext()) { + Tuple2 e = iterator.next(); + map.put(e._1(), jsValueToJavaObject(e._2(), maxRecursion - 1)); + } + return map; + } else if (value instanceof JsArray) { + List list = new ArrayList<>(); + JsArray jsArray = (JsArray) value; + Iterator iterator = jsArray.value().iterator(); + while (iterator.hasNext()) { + JsValue next = iterator.next(); + list.add(jsValueToJavaObject(next, maxRecursion - 1)); + } + return list; + } else { + return null; + } + } + + private static Object jsNodeToJavaObject(JsonNode value, int maxRecursion) { + if (value == null || maxRecursion <= 0) { + return null; + } + + if (value instanceof TextNode) { + return value.asText(); + } else if (value instanceof FloatNode || value instanceof DoubleNode) { + return value.asDouble(); + } else if (value instanceof NumericNode) { + return value.asLong(); + } else if (value instanceof ObjectNode) { + Map map = new HashMap<>(); + ObjectNode jsonObject = (ObjectNode) value; + java.util.Iterator> iterator = jsonObject.fields(); + while (iterator.hasNext()) { + Map.Entry e = iterator.next(); + map.put(e.getKey(), jsNodeToJavaObject(e.getValue(), maxRecursion - 1)); + } + return map; + } else if (value instanceof ArrayNode) { + List list = new ArrayList<>(); + ArrayNode arrayNode = (ArrayNode) value; + java.util.Iterator iterator = arrayNode.elements(); + while (iterator.hasNext()) { + JsonNode next = iterator.next(); + list.add(jsNodeToJavaObject(next, maxRecursion - 1)); + } + return list; + } else if (value instanceof NullNode) { + return null; + } else { + return value.asText(""); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/DelegatingBodyParserInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/DelegatingBodyParserInstrumentation.java new file mode 100644 index 00000000000..5672422c95c --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/DelegatingBodyParserInstrumentation.java @@ -0,0 +1,76 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.instrumentation.play26.MuzzleReferences; +import net.bytebuddy.asm.Advice; +import play.core.j.JavaParsers$; +import play.mvc.BodyParser; +import play.mvc.Http; + +/** @see play.mvc.BodyParser.DelegatingBodyParser */ +@AutoService(Instrumenter.class) +public class DelegatingBodyParserInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public DelegatingBodyParserInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_26_PLUS; + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$DelegatingBodyParser"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".JavaMultipartFormDataRegisterExcF", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("apply") + .and(takesArguments(1)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(returns(named("play.libs.streams.Accumulator"))), + DelegatingBodyParserInstrumentation.class.getName() + "$ApplyAdvice"); + } + + static class ApplyAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after( + @Advice.This BodyParser.DelegatingBodyParser thiz, + @Advice.Return(readOnly = false) play.libs.streams.Accumulator ret) { + if (!thiz.getClass().getName().equals("play.mvc.BodyParser$MultipartFormData")) { + return; + } + play.libs.streams.Accumulator< + akka.util.ByteString, + play.libs.F.Either< + play.mvc.Result, Http.MultipartFormData>> + acc = ret; + + ret = + acc.recover( + JavaMultipartFormDataRegisterExcF.INSTANCE, JavaParsers$.MODULE$.trampoline()); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/FormUrlEncodedInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/FormUrlEncodedInstrumentation.java new file mode 100644 index 00000000000..a97e6422d5a --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/FormUrlEncodedInstrumentation.java @@ -0,0 +1,67 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import akka.util.ByteString; +import com.google.auto.service.AutoService; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.agent.tooling.Instrumenter; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import play.mvc.Http; + +/** @see play.mvc.BodyParser.FormUrlEncoded#parse(Http.RequestHeader, ByteString) */ +@AutoService(Instrumenter.class) +public class FormUrlEncodedInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public FormUrlEncodedInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$FormUrlEncoded"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("parse") + .and(takesArguments(2)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(takesArgument(1, named("akka.util.ByteString"))) + .and(returns(Map.class)), + FormUrlEncodedInstrumentation.class.getName() + "$ParseAdvice"); + } + + static class ParseAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return Map ret, @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null) { + return; + } + try { + // error is reported as client error, which doesn't preserve the exception + BodyParserHelpers.handleArbitraryPostDataWithSpanError(ret, "FormUrlEncoded#parse"); + } catch (BlockingException be) { + t = be; + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/HttpErrorHandlerInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/HttpErrorHandlerInstrumentation.java new file mode 100644 index 00000000000..844cc719dc5 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/HttpErrorHandlerInstrumentation.java @@ -0,0 +1,88 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.instrumentation.play26.MuzzleReferences; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import play.api.http.HttpErrorHandler; +import play.api.mvc.RequestHeader; + +/** @see HttpErrorHandler#onServerError(RequestHeader, Throwable) */ +@AutoService(Instrumenter.class) +public class HttpErrorHandlerInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForTypeHierarchy { + public HttpErrorHandlerInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_26_PLUS; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isPublic() + .and(named("onServerError")) + .and(takesArguments(2)) + .and(takesArgument(0, named("play.api.mvc.RequestHeader"))) + .and(takesArgument(1, Throwable.class)) + .and(returns(named("scala.concurrent.Future"))), + HttpErrorHandlerInstrumentation.class.getName() + "$OnServerErrorAdvice"); + } + + @Override + public String hierarchyMarkerType() { + return "play.api.http.HttpErrorHandler"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named("play.api.http.HttpErrorHandler")); + } + + static class OnServerErrorAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static void before(@Advice.Argument(1) Throwable t) { + int i = CallDepthThreadLocalMap.incrementCallDepth(HttpErrorHandler.class); + if (i > 0) { + return; + } + + if (!(t instanceof BlockingException)) { + return; + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return; + } + agentSpan.addThrowable(t); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after() { + CallDepthThreadLocalMap.decrementCallDepth(HttpErrorHandler.class); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/JavaMultipartFormDataRegisterExcF.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/JavaMultipartFormDataRegisterExcF.java new file mode 100644 index 00000000000..bc6199c89f0 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/JavaMultipartFormDataRegisterExcF.java @@ -0,0 +1,39 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.concurrent.CompletionException; +import java.util.function.Function; +import play.libs.F; +import play.libs.Files; +import play.mvc.Http; +import play.mvc.Result; + +public class JavaMultipartFormDataRegisterExcF + implements Function>> { + public static Function>> + INSTANCE = new JavaMultipartFormDataRegisterExcF(); + + private JavaMultipartFormDataRegisterExcF() {} + + @Override + public F.Either> apply(Throwable exc) { + if (exc instanceof CompletionException) { + exc = exc.getCause(); + } + if (exc instanceof BlockingException) { + AgentSpan agentSpan = activeSpan(); + if (agentSpan != null) { + agentSpan.addThrowable(exc); + } + } + if (exc instanceof RuntimeException) { + throw (RuntimeException) exc; + } else { + throw new UndeclaredThrowableException(exc); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/NoDeclaredMethodMatcher.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/NoDeclaredMethodMatcher.java new file mode 100644 index 00000000000..1075d1c34c7 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/NoDeclaredMethodMatcher.java @@ -0,0 +1,23 @@ +package datadog.trace.instrumentation.play26.appsec; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class NoDeclaredMethodMatcher implements ElementMatcher { + private final ElementMatcher methodMatcher; + + public static ElementMatcher hasNoDeclaredMethod( + ElementMatcher em) { + return new NoDeclaredMethodMatcher(em); + } + + private NoDeclaredMethodMatcher(ElementMatcher methodMatcher) { + this.methodMatcher = methodMatcher; + } + + @Override + public boolean matches(MethodDescription target) { + return !target.getDeclaringType().getDeclaredMethods().stream() + .anyMatch(md -> this.methodMatcher.matches(md)); + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathExtractionHelpers.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathExtractionHelpers.java new file mode 100644 index 00000000000..0ff075cbf3e --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathExtractionHelpers.java @@ -0,0 +1,62 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.api.gateway.Events.EVENTS; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.gateway.BlockResponseFunction; +import datadog.trace.api.gateway.CallbackProvider; +import datadog.trace.api.gateway.Flow; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import java.util.Map; +import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PathExtractionHelpers { + private static final Logger log = LoggerFactory.getLogger(PathExtractionHelpers.class); + + private PathExtractionHelpers() {} + + public static BlockingException callRequestPathParamsCallback( + RequestContext reqCtx, Map params, String origin) { + try { + return doCallRequestPathParamsCallback(reqCtx, params, origin); + } catch (Exception e) { + log.warn("Error calling " + origin, e); + return null; + } + } + + private static BlockingException doCallRequestPathParamsCallback( + RequestContext reqCtx, Map params, String origin) { + if (params == null || params.isEmpty()) { + return null; + } + + CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC); + BiFunction, Flow> callback = + cbp.getCallback(EVENTS.requestPathParams()); + if (callback == null) { + return null; + } + + Flow flow = callback.apply(reqCtx, params); + Flow.Action action = flow.getAction(); + if (!(action instanceof Flow.Action.RequestBlockingAction)) { + return null; + } + + Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action; + BlockResponseFunction brf = reqCtx.getBlockResponseFunction(); + if (brf != null) { + brf.tryCommitBlockingResponse( + reqCtx.getTraceSegment(), + rba.getStatusCode(), + rba.getBlockingContentType(), + rba.getExtraHeaders()); + } + return new BlockingException("Blocked request (for " + origin + ")"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathPatternInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathPatternInstrumentation.java new file mode 100644 index 00000000000..feac036e405 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PathPatternInstrumentation.java @@ -0,0 +1,100 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.advice.ActiveRequestContext; +import datadog.trace.advice.RequiresRequestContext; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.instrumentation.play26.MuzzleReferences; +import net.bytebuddy.asm.Advice; +import scala.Tuple2; +import scala.collection.Iterator; +import scala.util.Either; + +/** @see play.core.routing.PathPattern#apply(String) */ +@AutoService(Instrumenter.class) +public class PathPatternInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public PathPatternInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".PathExtractionHelpers", + }; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_26_PLUS; + } + + @Override + public String instrumentedType() { + return "play.core.routing.PathPattern"; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("apply") + .and(not(isStatic())) + .and(takesArguments(1)) + .and(takesArgument(0, String.class)) + .and(returns(named("scala.Option"))), + PathPatternInstrumentation.class.getName() + "$ApplyAdvice"); + } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + static class ApplyAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return(readOnly = false) + scala.Option< + scala.collection.immutable.Map>> + ret, + @Advice.Thrown(readOnly = false) Throwable t, + @ActiveRequestContext RequestContext reqCtx) { + if (t != null) { + return; + } + if (ret.isEmpty()) { + return; + } + + java.util.Map conv = new java.util.HashMap<>(); + + Iterator>> iterator = ret.get().iterator(); + while (iterator.hasNext()) { + Tuple2> next = iterator.next(); + Either value = next._2(); + if (value.isLeft()) { + continue; + } + + conv.put(next._1(), value.right().get()); + } + + BlockingException blockingException = + PathExtractionHelpers.callRequestPathParamsCallback(reqCtx, conv, "PathPattern#apply"); + t = blockingException; + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PlayBodyParsersInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PlayBodyParsersInstrumentation.java new file mode 100644 index 00000000000..7394b343309 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/PlayBodyParsersInstrumentation.java @@ -0,0 +1,137 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod; +import static datadog.trace.instrumentation.play26.appsec.NoDeclaredMethodMatcher.hasNoDeclaredMethod; +import static net.bytebuddy.matcher.ElementMatchers.is; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import net.bytebuddy.asm.Advice; +import play.api.libs.json.JsValue; +import play.api.mvc.BodyParser; +import play.api.mvc.MultipartFormData; +import play.api.mvc.PlayBodyParsers; +import play.core.Execution; +import scala.collection.Seq; +import scala.collection.immutable.Map; + +/** @see play.api.mvc.PlayBodyParsers$class#tolerantFormUrlEncoded(PlayBodyParsers, int) */ +@AutoService(Instrumenter.class) +public class PlayBodyParsersInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForKnownTypes { + private static final String TRAIT_NAME = "play.api.mvc.PlayBodyParsers"; + + public PlayBodyParsersInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public String[] knownMatchingTypes() { + return new String[] {TRAIT_NAME, TRAIT_NAME + "$class"}; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isTraitMethod(TRAIT_NAME, "tolerantFormUrlEncoded", is(int.class).or(is(long.class))) + .and(returns(named("play.api.mvc.BodyParser"))), + PlayBodyParsersInstrumentation.class.getName() + "$UrlEncodedAdvice"); + transformation.applyAdvice( + isTraitMethod(TRAIT_NAME, "tolerantText", long.class) + .and(returns(named("play.api.mvc.BodyParser"))), + PlayBodyParsersInstrumentation.class.getName() + "$TextAdvice"); + transformation.applyAdvice( + isTraitMethod(TRAIT_NAME, "text", long.class) + .and(returns(named("play.api.mvc.BodyParser"))), + PlayBodyParsersInstrumentation.class.getName() + "$TextAdvice"); + transformation.applyAdvice( + isTraitMethod(TRAIT_NAME, "multipartFormData", "scala.Function1", long.class, boolean.class) + .and(returns(named("play.api.mvc.BodyParser"))), + PlayBodyParsersInstrumentation.class.getName() + "$MultipartFormDataAdvice"); + transformation.applyAdvice( + isTraitMethod(TRAIT_NAME, "multipartFormData", "scala.Function1", long.class) + .and(returns(named("play.api.mvc.BodyParser"))) + .and( + /* only if prev didn't match */ + hasNoDeclaredMethod( + isTraitMethod( + TRAIT_NAME, + "multipartFormData", + "scala.Function1", + long.class, + boolean.class) + .and(returns(named("play.api.mvc.BodyParser"))))), + PlayBodyParsersInstrumentation.class.getName() + "$MultipartFormDataAdvice"); + transformation.applyAdvice( + isTraitMethod(TRAIT_NAME, "tolerantJson", is(int.class).or(is(long.class))) + .and(returns(named("play.api.mvc.BodyParser"))), + PlayBodyParsersInstrumentation.class.getName() + "$JsonAdvice"); + } + + static class UrlEncodedAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after( + @Advice.Return(readOnly = false) BodyParser>> parser) { + + parser = + parser.map( + BodyParserHelpers.getHandleUrlEncodedMapF(), + Execution.Implicits$.MODULE$.trampoline()); + } + } + + static class TextAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static void before() { + CallDepthThreadLocalMap.incrementCallDepth(PlayBodyParsers.class); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return(readOnly = false) BodyParser parser, @Advice.Thrown Throwable t) { + int depth = CallDepthThreadLocalMap.decrementCallDepth(PlayBodyParsers.class); + if (depth > 0 || t != null) { + return; + } + + parser = + parser.map( + BodyParserHelpers.getHandleStringMapF(), Execution.Implicits$.MODULE$.trampoline()); + } + } + + static class MultipartFormDataAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after(@Advice.Return(readOnly = false) BodyParser> parser) { + + parser = + parser.map( + BodyParserHelpers.getHandleMultipartFormDataF(), + Execution.Implicits$.MODULE$.trampoline()); + } + } + + static class JsonAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + static void after(@Advice.Return(readOnly = false) BodyParser parser) { + + parser = + parser.map(BodyParserHelpers.getHandleJsonF(), Execution.Implicits$.MODULE$.trampoline()); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/RoutingDslInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/RoutingDslInstrumentation.java new file mode 100644 index 00000000000..fb44978f5d1 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/RoutingDslInstrumentation.java @@ -0,0 +1,84 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.instrumentation.play26.MuzzleReferences; +import java.util.function.BiFunction; +import java.util.function.Function; +import net.bytebuddy.asm.Advice; +import play.libs.F; +import play.routing.RoutingDsl; + +/** @see RoutingDsl.Route */ +@AutoService(Instrumenter.class) +public class RoutingDslInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public RoutingDslInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Only"; + } + + @Override + public String instrumentedType() { + return "play.routing.RoutingDsl$Route"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return new Reference[] { + new Reference.Builder("play.routing.RoutingDsl$PathPatternMatcher") + .withMethod( + new String[0], + Reference.EXPECTS_NON_STATIC | Reference.EXPECTS_PUBLIC, + "routeTo", + "Lplay/routing/RoutingDsl;", + "Ljava/util/function/Supplier;") + .build(), + MuzzleReferences.PLAY_26_ONLY[0], + MuzzleReferences.PLAY_26_ONLY[1], + }; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".ArgumentCaptureWrappers", + packageName + ".ArgumentCaptureWrappers$ArgumentCaptureFunction", + packageName + ".ArgumentCaptureWrappers$ArgumentCaptureBiFunction", + packageName + ".ArgumentCaptureWrappers$ArgumentCaptureFunction3", + packageName + ".PathExtractionHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isConstructor() + .and(takesArguments(5)) + .and(takesArgument(3, Object.class)) + .and(takesArgument(4, java.lang.reflect.Method.class)), + RoutingDslInstrumentation.class.getName() + "$RouteConstructorAdvice"); + } + + static class RouteConstructorAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static void before(@Advice.Argument(value = 3, readOnly = false) Object action) { + if (action instanceof Function) { + action = new ArgumentCaptureWrappers.ArgumentCaptureFunction<>((Function) action); + } else if (action instanceof BiFunction) { + action = new ArgumentCaptureWrappers.ArgumentCaptureBiFunction<>((BiFunction) action); + } else if (action instanceof F.Function3) { + action = new ArgumentCaptureWrappers.ArgumentCaptureFunction3<>((F.Function3) action); + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/SirdPathExtractorInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/SirdPathExtractorInstrumentation.java new file mode 100644 index 00000000000..667546788b3 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/SirdPathExtractorInstrumentation.java @@ -0,0 +1,83 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.advice.ActiveRequestContext; +import datadog.trace.advice.RequiresRequestContext; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.instrumentation.play26.MuzzleReferences; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import scala.collection.immutable.List; + +/** @see play.api.routing.sird.PathExtractor */ +@AutoService(Instrumenter.class) +public class SirdPathExtractorInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public SirdPathExtractorInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_26_PLUS; // force failure in <2.6 + } + + @Override + public String instrumentedType() { + return "play.api.routing.sird.PathExtractor"; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("extract") + .and(takesArguments(1)) + .and(takesArgument(0, String.class)) + .and(returns(named("scala.Option"))), + SirdPathExtractorInstrumentation.class.getName() + "$ExtractAdvice"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".PathExtractionHelpers", + }; + } + + @RequiresRequestContext(RequestContextSlot.APPSEC) + static class ExtractAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after( + @Advice.Return scala.Option> ret, + @ActiveRequestContext RequestContext reqCtx, + @Advice.Thrown(readOnly = false) Throwable t) { + if (ret.isEmpty() || t != null) { + return; + } + + Map conv = new HashMap<>(); + List stringList = ret.get(); + for (int i = 0; i < stringList.size(); i++) { + conv.put(Integer.toString(i), stringList.apply(i)); + } + + t = + PathExtractionHelpers.callRequestPathParamsCallback( + reqCtx, conv, "sird.PathExtractor#extract"); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantJsonInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantJsonInstrumentation.java new file mode 100644 index 00000000000..291b0f6fdb7 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantJsonInstrumentation.java @@ -0,0 +1,72 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import akka.util.ByteString; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.auto.service.AutoService; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.instrumentation.play26.MuzzleReferences; +import net.bytebuddy.asm.Advice; +import play.mvc.Http; + +/** @see play.mvc.BodyParser.TolerantJson#parse(Http.RequestHeader, ByteString) */ +@AutoService(Instrumenter.class) +public class TolerantJsonInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public TolerantJsonInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_26_PLUS; + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$TolerantJson"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("parse") + .and(takesArguments(2)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(takesArgument(1, named("akka.util.ByteString"))) + .and(returns(named("com.fasterxml.jackson.databind.JsonNode"))), + TolerantJsonInstrumentation.class.getName() + "$ParseAdvice"); + } + + static class ParseAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after(@Advice.Return JsonNode ret, @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null) { + return; + } + try { + BodyParserHelpers.handleJsonNode(ret, "TolerantJson#parse"); + } catch (BlockingException be) { + t = be; + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantTextInstrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantTextInstrumentation.java new file mode 100644 index 00000000000..2db389820d5 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/appsec/TolerantTextInstrumentation.java @@ -0,0 +1,72 @@ +package datadog.trace.instrumentation.play26.appsec; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import akka.util.ByteString; +import com.google.auto.service.AutoService; +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import datadog.trace.instrumentation.play26.MuzzleReferences; +import net.bytebuddy.asm.Advice; +import play.mvc.Http; + +/** @see play.mvc.BodyParser.TolerantText#parse(Http.RequestHeader, ByteString) */ +@AutoService(Instrumenter.class) +public class TolerantTextInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public TolerantTextInstrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play26Plus"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return MuzzleReferences.PLAY_26_PLUS; + } + + @Override + public String instrumentedType() { + return "play.mvc.BodyParser$TolerantText"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".BodyParserHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("parse") + .and(takesArguments(2)) + .and(takesArgument(0, named("play.mvc.Http$RequestHeader"))) + .and(takesArgument(1, named("akka.util.ByteString"))) + .and(returns(String.class)), + TolerantTextInstrumentation.class.getName() + "$ParseAdvice"); + } + + static class ParseAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + static void after(@Advice.Return String ret, @Advice.Thrown(readOnly = false) Throwable t) { + if (t != null) { + return; + } + try { + // error is reported as client error, which doesn't preserve the exception + BodyParserHelpers.handleArbitraryPostDataWithSpanError(ret, "TolerantText#parse"); + } catch (BlockingException be) { + t = be; + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play27/appsec/RoutingDsl27Instrumentation.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play27/appsec/RoutingDsl27Instrumentation.java new file mode 100644 index 00000000000..137f485fbb3 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play27/appsec/RoutingDsl27Instrumentation.java @@ -0,0 +1,64 @@ +package datadog.trace.instrumentation.play27.appsec; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.muzzle.Reference; +import play.routing.RoutingDsl; + +/** @see RoutingDsl.Route */ +@AutoService(Instrumenter.class) +public class RoutingDsl27Instrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType { + public RoutingDsl27Instrumentation() { + super("play"); + } + + @Override + public String muzzleDirective() { + return "play27"; + } + + @Override + public String instrumentedType() { + return "play.routing.RoutingDsl$Route"; + } + + @Override + public Reference[] additionalMuzzleReferences() { + return new Reference[] { + new Reference.Builder("play.routing.RoutingDsl$PathPatternMatcher") + .withMethod( + new String[0], + Reference.EXPECTS_NON_STATIC | Reference.EXPECTS_PUBLIC, + "routingTo", + "Lplay/routing/RoutingDsl;", + "Lplay/routing/RequestFunctions$Params1;") + .build() + }; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".ArgumentCaptureAdvice", + packageName + ".ArgumentCaptureAdvice$ArgumentCaptureFunctionParam1", + packageName + ".ArgumentCaptureAdvice$ArgumentCaptureFunctionParam2", + packageName + ".ArgumentCaptureAdvice$ArgumentCaptureFunctionParam3", + "datadog.trace.instrumentation.play26.appsec.PathExtractionHelpers", + }; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isConstructor() + .and(takesArguments(5)) + .and(takesArgument(3, Object.class)) + .and(takesArgument(4, java.lang.reflect.Method.class)), + packageName + ".ArgumentCaptureAdvice$RouteConstructorAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java_play27/datadog/trace/instrumentation/play27/appsec/ArgumentCaptureAdvice.java b/dd-java-agent/instrumentation/play-2.6/src/main/java_play27/datadog/trace/instrumentation/play27/appsec/ArgumentCaptureAdvice.java new file mode 100644 index 00000000000..fefc4747eb1 --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java_play27/datadog/trace/instrumentation/play27/appsec/ArgumentCaptureAdvice.java @@ -0,0 +1,151 @@ +package datadog.trace.instrumentation.play27.appsec; + +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import datadog.appsec.api.blocking.BlockingException; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.instrumentation.play26.appsec.PathExtractionHelpers; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import play.mvc.Http; +import play.routing.RequestFunctions; + +public class ArgumentCaptureAdvice { + private static Logger log = LoggerFactory.getLogger(ArgumentCaptureAdvice.class); + + public static class RouteConstructorAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + static void before(@Advice.Argument(value = 3, readOnly = false) Object action) { + if (action instanceof RequestFunctions.Params1) { + action = new ArgumentCaptureFunctionParam1((RequestFunctions.Params1) action); + } else if (action instanceof RequestFunctions.Params2) { + action = new ArgumentCaptureFunctionParam2((RequestFunctions.Params2) action); + } else if (action instanceof RequestFunctions.Params3) { + action = new ArgumentCaptureFunctionParam3((RequestFunctions.Params3) action); + } + } + } + + public static class ArgumentCaptureFunctionParam1 + implements RequestFunctions.Params1 { + private final RequestFunctions.Params1 delegate; + + public ArgumentCaptureFunctionParam1(RequestFunctions.Params1 delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Http.Request req, Object o1) { + if (o1 == null) { + return delegate.apply(req, null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(req, o1); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(req, o1); + } + + Map conv = Collections.singletonMap("0", o1); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routingTo"); + if (t != null) { + throw t; + } + + return delegate.apply(req, o1); + } + } + + public static class ArgumentCaptureFunctionParam2 + implements RequestFunctions.Params2 { + private final RequestFunctions.Params2 delegate; + + public ArgumentCaptureFunctionParam2(RequestFunctions.Params2 delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Http.Request req, Object o1, Object o2) throws Throwable { + if (o1 == null && o2 == null) { + return delegate.apply(req, null, null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(req, o1, o2); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(req, o1, o2); + } + + Map conv = new HashMap<>(); + conv.put("0", o1); + conv.put("1", o2); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routingTo"); + if (t != null) { + throw t; + } + + return delegate.apply(req, o1, o2); + } + } + + public static class ArgumentCaptureFunctionParam3 + implements RequestFunctions.Params3 { + private final RequestFunctions.Params3 delegate; + + public ArgumentCaptureFunctionParam3( + RequestFunctions.Params3 delegate) { + this.delegate = delegate; + } + + @Override + public R apply(Http.Request req, Object o1, Object o2, Object o3) throws Throwable { + if (o1 == null && o2 == null && o3 == null) { + return delegate.apply(req, null, null, null); + } + + AgentSpan agentSpan = activeSpan(); + if (agentSpan == null) { + return delegate.apply(req, o1, o2, o3); + } + + RequestContext requestContext = agentSpan.getRequestContext(); + if (requestContext.getData(RequestContextSlot.APPSEC) == null) { + return delegate.apply(req, o1, o2, o3); + } + + Map conv = new HashMap<>(); + conv.put("0", o1); + conv.put("1", o2); + conv.put("2", o3); + + BlockingException t = + PathExtractionHelpers.callRequestPathParamsCallback( + requestContext, conv, "RoutingDsl#routingTo"); + if (t != null) { + throw t; + } + + return delegate.apply(req, o1, o2, o3); + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/routeGenerator/scala/generator/CompileRoutes.scala b/dd-java-agent/instrumentation/play-2.6/src/routeGenerator/scala/generator/CompileRoutes.scala new file mode 100644 index 00000000000..1b8ff7667ba --- /dev/null +++ b/dd-java-agent/instrumentation/play-2.6/src/routeGenerator/scala/generator/CompileRoutes.scala @@ -0,0 +1,17 @@ +package generator + +import play.routes.compiler.{InjectedRoutesGenerator, RoutesCompiler} + +import java.io.File +import scala.collection.immutable + +object CompileRoutes extends App { + val routesFile = args(0) + val destinationDir = args(1) + + val routesCompilerTask = RoutesCompiler.RoutesCompilerTask( + new File(routesFile), immutable.Seq.empty, + true, false, false) + RoutesCompiler.compile( + routesCompilerTask, InjectedRoutesGenerator, new File(destinationDir)) +} diff --git a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerTest.groovy deleted file mode 100644 index 567c6dab618..00000000000 --- a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerTest.groovy +++ /dev/null @@ -1,97 +0,0 @@ -package server - -import datadog.trace.agent.test.base.HttpServer -import play.BuiltInComponents -import play.libs.concurrent.HttpExecution -import play.mvc.Results -import play.routing.RoutingDsl -import spock.lang.Shared - -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors -import java.util.function.Supplier - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class PlayAsyncServerTest extends PlayServerTest { - @Shared - def executor - - def cleanupSpec() { - executor.shutdown() - } - - @Override - HttpServer server() { - executor = Executors.newCachedThreadPool() - def execContext = HttpExecution.fromThread(executor) - return new PlayHttpServer({ BuiltInComponents components -> - RoutingDsl.fromComponents(components) - .GET(SUCCESS.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(SUCCESS) { - Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) - } - }, execContext) - } as Supplier) - .GET(FORWARDED.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(FORWARDED) { - Results.status(FORWARDED.getStatus(), FORWARDED.getBody()) // cheating - } - }, execContext) - } as Supplier) - .GET(QUERY_PARAM.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_PARAM) { - Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) // cheating - } - }, execContext) - } as Supplier) - .GET(QUERY_ENCODED_QUERY.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_ENCODED_QUERY) { - Results.status(QUERY_ENCODED_QUERY.getStatus(), QUERY_ENCODED_QUERY.getBody()) // cheating - } - }, execContext) - } as Supplier) - .GET(QUERY_ENCODED_BOTH.getRawPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_ENCODED_BOTH) { - Results.status(QUERY_ENCODED_BOTH.getStatus(), QUERY_ENCODED_BOTH.getBody()). - withHeader(IG_RESPONSE_HEADER, IG_RESPONSE_HEADER_VALUE) // cheating - } - }, execContext) - } as Supplier) - .GET(REDIRECT.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(REDIRECT) { - Results.found(REDIRECT.getBody()) - } - }, execContext) - } as Supplier) - .GET(ERROR.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(ERROR) { - Results.status(ERROR.getStatus(), ERROR.getBody()) - } - }, execContext) - } as Supplier) - .GET(EXCEPTION.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(EXCEPTION) { - throw new RuntimeException(EXCEPTION.getBody()) - } - }, execContext) - } as Supplier) - .build() - }) - } -} diff --git a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerWithErrorHandlerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerWithErrorHandlerTest.groovy deleted file mode 100644 index 809c91bfd56..00000000000 --- a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayAsyncServerWithErrorHandlerTest.groovy +++ /dev/null @@ -1,105 +0,0 @@ -package server - -import datadog.trace.agent.test.base.HttpServer -import play.BuiltInComponents -import play.libs.concurrent.HttpExecution -import play.mvc.Results -import play.routing.RoutingDsl -import spock.lang.Shared - -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors -import java.util.function.Supplier - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS - -class PlayAsyncServerWithErrorHandlerTest extends PlayServerWithErrorHandlerTest { - @Shared - def executor - - def cleanupSpec() { - executor.shutdown() - } - - @Override - HttpServer server() { - executor = Executors.newCachedThreadPool() - def execContext = HttpExecution.fromThread(executor) - return new PlayHttpServer({ BuiltInComponents components -> - RoutingDsl.fromComponents(components) - .GET(SUCCESS.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(SUCCESS) { - Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) - } - }, execContext) - } as Supplier) - .GET(FORWARDED.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(FORWARDED) { - Results.status(FORWARDED.getStatus(), FORWARDED.getBody()) // cheating - } - }, execContext) - } as Supplier) - .GET(QUERY_PARAM.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_PARAM) { - Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) // cheating - } - }, execContext) - } as Supplier) - .GET(QUERY_ENCODED_QUERY.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_ENCODED_QUERY) { - Results.status(QUERY_ENCODED_QUERY.getStatus(), QUERY_ENCODED_QUERY.getBody()) // cheating - } - }, execContext) - } as Supplier) - .GET(QUERY_ENCODED_BOTH.getRawPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(QUERY_ENCODED_BOTH) { - Results.status(QUERY_ENCODED_BOTH.getStatus(), QUERY_ENCODED_BOTH.getBody()). - withHeader(IG_RESPONSE_HEADER, IG_RESPONSE_HEADER_VALUE) // cheating - } - }, execContext) - } as Supplier) - .GET(REDIRECT.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(REDIRECT) { - Results.found(REDIRECT.getBody()) - } - }, execContext) - } as Supplier) - .GET(ERROR.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(ERROR) { - Results.status(ERROR.getStatus(), ERROR.getBody()) - } - }, execContext) - } as Supplier) - .GET(EXCEPTION.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(EXCEPTION) { - throw new RuntimeException(EXCEPTION.getBody()) - } - }, execContext) - } as Supplier) - .GET(CUSTOM_EXCEPTION.getPath()).routeAsync({ - CompletableFuture.supplyAsync({ - controller(CUSTOM_EXCEPTION) { - throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.getBody()) - } - }, execContext) - } as Supplier) - .build() - }, new TestHttpErrorHandler()) - } -} diff --git a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerWithErrorHandlerTest.groovy b/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerWithErrorHandlerTest.groovy deleted file mode 100644 index 5ef4dbd749e..00000000000 --- a/dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerWithErrorHandlerTest.groovy +++ /dev/null @@ -1,112 +0,0 @@ -package server - -import datadog.trace.agent.test.base.HttpServer -import play.BuiltInComponents -import play.mvc.Results -import play.routing.RoutingDsl - -import java.util.function.Supplier - -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.CUSTOM_EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.FORWARDED -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_BOTH -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_ENCODED_QUERY -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.QUERY_PARAM -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT -import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS -import static org.junit.Assume.assumeTrue - -class PlayServerWithErrorHandlerTest extends PlayServerTest { - @Override - HttpServer server() { - return new PlayHttpServer({ BuiltInComponents components -> - RoutingDsl.fromComponents(components) - .GET(SUCCESS.getPath()).routeTo({ - controller(SUCCESS) { - Results.status(SUCCESS.getStatus(), SUCCESS.getBody()) - } - } as Supplier) - .GET(FORWARDED.getPath()).routeTo({ - controller(FORWARDED) { - Results.status(FORWARDED.getStatus(), FORWARDED.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_PARAM.getPath()).routeTo({ - controller(QUERY_PARAM) { - Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_ENCODED_QUERY.getPath()).routeTo({ - controller(QUERY_ENCODED_QUERY) { - Results.status(QUERY_ENCODED_QUERY.getStatus(), QUERY_ENCODED_QUERY.getBody()) // cheating - } - } as Supplier) - .GET(QUERY_ENCODED_BOTH.getRawPath()).routeTo({ - controller(QUERY_ENCODED_BOTH) { - Results.status(QUERY_ENCODED_BOTH.getStatus(), QUERY_ENCODED_BOTH.getBody()). - withHeader(IG_RESPONSE_HEADER, IG_RESPONSE_HEADER_VALUE) // cheating - } - } as Supplier) - .GET(REDIRECT.getPath()).routeTo({ - controller(REDIRECT) { - Results.found(REDIRECT.getBody()) - } - } as Supplier) - .GET(ERROR.getPath()).routeTo({ - controller(ERROR) { - Results.status(ERROR.getStatus(), ERROR.getBody()) - } - } as Supplier) - .GET(EXCEPTION.getPath()).routeTo({ - controller(EXCEPTION) { - throw new RuntimeException(EXCEPTION.getBody()) - } - } as Supplier) - .GET(CUSTOM_EXCEPTION.getPath()).routeTo({ - controller(CUSTOM_EXCEPTION) { - throw new TestHttpErrorHandler.CustomRuntimeException(CUSTOM_EXCEPTION.getBody()) - } - } as Supplier) - .build() - }, new TestHttpErrorHandler()) - } - - @Override - boolean testExceptionBody() { - true - } - - def "test exception with custom status"() { - setup: - assumeTrue(testException()) - def request = request(CUSTOM_EXCEPTION, method, body).build() - def response = client.newCall(request).execute() - - expect: - response.code() == CUSTOM_EXCEPTION.status - if (testExceptionBody()) { - assert response.body().string() == CUSTOM_EXCEPTION.body - } - - and: - assertTraces(1) { - trace(spanCount(CUSTOM_EXCEPTION)) { - sortSpansByStart() - serverSpan(it, null, null, method, CUSTOM_EXCEPTION) - if (hasHandlerSpan()) { - handlerSpan(it, CUSTOM_EXCEPTION) - } - controllerSpan(it, CUSTOM_EXCEPTION) - if (hasResponseSpan(CUSTOM_EXCEPTION)) { - responseSpan(it, CUSTOM_EXCEPTION) - } - } - } - - where: - method = "GET" - body = null - } -} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy index d04d06f2f1e..53c256493e4 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy @@ -104,7 +104,7 @@ abstract class HttpServerTest extends WithHttpServer { } @CompileStatic - def setupSpec() { + void setupSpec() { // Register the Instrumentation Gateway callbacks def ss = get().getSubscriptionService(RequestContextSlot.APPSEC) def callbacks = new IGCallbacks() diff --git a/internal-api/src/main/java/datadog/trace/api/normalize/SimpleHttpPathNormalizer.java b/internal-api/src/main/java/datadog/trace/api/normalize/SimpleHttpPathNormalizer.java index 7285ab1825b..c13dedc7095 100644 --- a/internal-api/src/main/java/datadog/trace/api/normalize/SimpleHttpPathNormalizer.java +++ b/internal-api/src/main/java/datadog/trace/api/normalize/SimpleHttpPathNormalizer.java @@ -45,6 +45,9 @@ public String normalize(String path, boolean encoded) { if (!numeric) { if (Character.isWhitespace(c)) { sb = ensureStringBuilder(sb, path, j); + if (sb.length() > 0 && !encoded) { + sb.append(c); + } } else if (sb != null) { sb.append(c); } diff --git a/internal-api/src/test/groovy/datadog/trace/api/normalize/SimpleHttpPathNormalizerTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/normalize/SimpleHttpPathNormalizerTest.groovy index b7055704307..12b1e3bdf68 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/normalize/SimpleHttpPathNormalizerTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/normalize/SimpleHttpPathNormalizerTest.groovy @@ -31,6 +31,14 @@ class SimpleHttpPathNormalizerTest extends DDSpecification { "\t/:userId" | "/:userId" } + def 'does not suppress whitespace in non-initial position of decoded strings'() { + String input = ' /a b/' + String output = simplePathNormalizer.normalize(input, false) + + expect: + output == '/a b/' + } + def "should replace all digits"() { when: def norm = simplePathNormalizer.normalize(input) as String