Skip to content

Commit 0078896

Browse files
Store the http.route inside the appsec request context in Play
1 parent 9b5f0d7 commit 0078896

File tree

26 files changed

+721
-3
lines changed

26 files changed

+721
-3
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ public void init() {
156156
subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd);
157157
subscriptionService.registerCallback(EVENTS.user(), this::onUser);
158158
subscriptionService.registerCallback(EVENTS.loginEvent(), this::onLoginEvent);
159+
subscriptionService.registerCallback(EVENTS.httpRoute(), this::onHttpRoute);
159160

160161
if (additionalIGEvents.contains(EVENTS.requestPathParams())) {
161162
subscriptionService.registerCallback(EVENTS.requestPathParams(), this::onRequestPathParams);
@@ -222,6 +223,14 @@ private Flow<Void> onUser(final RequestContext ctx_, final String user) {
222223
}
223224
}
224225

226+
private void onHttpRoute(final RequestContext ctx_, final String route) {
227+
final AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
228+
if (ctx == null) {
229+
return;
230+
}
231+
ctx.setRoute(route);
232+
}
233+
225234
private Flow<Void> onLoginEvent(
226235
final RequestContext ctx_, final LoginEvent event, final String login) {
227236
final AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);

dd-java-agent/instrumentation/play-2.3/src/main/java/datadog/trace/instrumentation/play23/PlayHttpServerDecorator.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
package datadog.trace.instrumentation.play23;
22

3+
import static datadog.trace.api.gateway.Events.EVENTS;
34
import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR;
45

56
import datadog.trace.api.Config;
7+
import datadog.trace.api.gateway.RequestContext;
8+
import datadog.trace.api.gateway.RequestContextSlot;
69
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
710
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
811
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
12+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
913
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
1014
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
1115
import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator;
1216
import java.lang.reflect.InvocationTargetException;
1317
import java.lang.reflect.UndeclaredThrowableException;
1418
import java.util.concurrent.CompletionException;
19+
import java.util.function.BiConsumer;
1520
import play.api.Routes;
1621
import play.api.mvc.Headers;
1722
import play.api.mvc.Request;
@@ -87,12 +92,28 @@ public AgentSpan onRequest(
8792
final Option pathOption = request.tags().get(Routes.ROUTE_PATTERN());
8893
if (!pathOption.isEmpty()) {
8994
final String path = (String) pathOption.get();
90-
HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path);
95+
handleRoute(span, request.method(), path);
9196
}
9297
}
9398
return span;
9499
}
95100

101+
private void handleRoute(final AgentSpan span, final String method, final String route) {
102+
HTTP_RESOURCE_DECORATOR.withRoute(span, method, route);
103+
// play does not set the http.route in the local root span so we need to store it in the context
104+
// for API security
105+
final RequestContext ctx = span.getRequestContext();
106+
if (ctx != null) {
107+
final BiConsumer<RequestContext, String> cb =
108+
AgentTracer.get()
109+
.getCallbackProvider(RequestContextSlot.APPSEC)
110+
.getCallback(EVENTS.httpRoute());
111+
if (cb != null) {
112+
cb.accept(ctx, route);
113+
}
114+
}
115+
}
116+
96117
@Override
97118
public AgentSpan onError(final AgentSpan span, Throwable throwable) {
98119
if (REPORT_HTTP_STATUS) {

dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayHttpServerDecorator.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
package datadog.trace.instrumentation.play24;
22

3+
import static datadog.trace.api.gateway.Events.EVENTS;
34
import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR;
45

56
import datadog.trace.api.Config;
7+
import datadog.trace.api.gateway.RequestContext;
8+
import datadog.trace.api.gateway.RequestContextSlot;
69
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
710
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
811
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
12+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
913
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
1014
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
1115
import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator;
1216
import java.lang.reflect.InvocationTargetException;
1317
import java.lang.reflect.UndeclaredThrowableException;
1418
import java.util.concurrent.CompletionException;
19+
import java.util.function.BiConsumer;
1520
import play.api.mvc.Headers;
1621
import play.api.mvc.Request;
1722
import play.api.mvc.Result;
@@ -87,12 +92,28 @@ public AgentSpan onRequest(
8792
final Option pathOption = request.tags().get("ROUTE_PATTERN");
8893
if (!pathOption.isEmpty()) {
8994
final String path = (String) pathOption.get();
90-
HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path);
95+
handleRoute(span, request.method(), path);
9196
}
9297
}
9398
return span;
9499
}
95100

101+
private void handleRoute(final AgentSpan span, final String method, final String route) {
102+
HTTP_RESOURCE_DECORATOR.withRoute(span, method, route);
103+
// play does not set the http.route in the local root span so we need to store it in the context
104+
// for API security
105+
final RequestContext ctx = span.getRequestContext();
106+
if (ctx != null) {
107+
final BiConsumer<RequestContext, String> cb =
108+
AgentTracer.get()
109+
.getCallbackProvider(RequestContextSlot.APPSEC)
110+
.getCallback(EVENTS.httpRoute());
111+
if (cb != null) {
112+
cb.accept(ctx, route);
113+
}
114+
}
115+
}
116+
96117
@Override
97118
public AgentSpan onError(final AgentSpan span, Throwable throwable) {
98119
if (REPORT_HTTP_STATUS) {

dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package datadog.trace.instrumentation.play26;
22

3+
import static datadog.trace.api.gateway.Events.EVENTS;
34
import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR;
45

56
import datadog.trace.api.Config;
67
import datadog.trace.api.cache.DDCache;
78
import datadog.trace.api.cache.DDCaches;
9+
import datadog.trace.api.gateway.RequestContext;
10+
import datadog.trace.api.gateway.RequestContextSlot;
811
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
912
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
1013
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
1114
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
15+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
1216
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
17+
import datadog.trace.bootstrap.instrumentation.api.URIUtils;
1318
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
1419
import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator;
1520
import java.lang.invoke.MethodHandle;
@@ -18,6 +23,7 @@
1823
import java.lang.reflect.InvocationTargetException;
1924
import java.lang.reflect.UndeclaredThrowableException;
2025
import java.util.concurrent.CompletionException;
26+
import java.util.function.BiConsumer;
2127
import play.api.mvc.Headers;
2228
import play.api.mvc.Request;
2329
import play.api.mvc.Result;
@@ -142,12 +148,28 @@ public AgentSpan onRequest(
142148
CharSequence path =
143149
PATH_CACHE.computeIfAbsent(
144150
defOption.get().path(), p -> addMissingSlash(p, request.path()));
145-
HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path, true);
151+
handleRoute(span, request.method(), path);
146152
}
147153
}
148154
return span;
149155
}
150156

157+
private void handleRoute(final AgentSpan span, final String method, final CharSequence route) {
158+
HTTP_RESOURCE_DECORATOR.withRoute(span, method, route, true);
159+
// play does not set the http.route in the local root span so we need to store it in the context
160+
// for API security
161+
final RequestContext ctx = span.getRequestContext();
162+
if (ctx != null) {
163+
final BiConsumer<RequestContext, String> cb =
164+
AgentTracer.get()
165+
.getCallbackProvider(RequestContextSlot.APPSEC)
166+
.getCallback(EVENTS.httpRoute());
167+
if (cb != null) {
168+
cb.accept(ctx, URIUtils.decode(route.toString()));
169+
}
170+
}
171+
}
172+
151173
/*
152174
This is a workaround to add a `/` if it is missing when using split routes.
153175
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package controllers
2+
3+
import play.api.mvc.{Action, AnyContent, Controller}
4+
5+
class AppSecController extends Controller {
6+
7+
def apiSecuritySampling(statusCode: Int, test: String): Action[AnyContent] = Action {
8+
Status(statusCode)("EXECUTED")
9+
}
10+
11+
}

dd-smoke-tests/play-2.4/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ dependencies {
6363
implementation group: 'io.opentracing', name: 'opentracing-util', version: '0.32.0'
6464

6565
testImplementation project(':dd-smoke-tests')
66+
testImplementation project(':dd-smoke-tests:appsec')
6667
}
6768

6869
configurations.testImplementation {

dd-smoke-tests/play-2.4/conf/routes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@
66
# An example controller showing a sample home page
77
GET /welcomej controllers.JController.doGet(id: Int ?= 0)
88
GET /welcomes controllers.SController.doGet(id: Option[Int])
9+
10+
# AppSec endpoints for testing
11+
GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package datadog.smoketest
2+
3+
import datadog.smoketest.appsec.AbstractAppSecServerSmokeTest
4+
import datadog.trace.agent.test.utils.OkHttpUtils
5+
import okhttp3.Request
6+
import okhttp3.Response
7+
import spock.lang.Shared
8+
9+
import java.nio.file.Files
10+
11+
import static java.util.concurrent.TimeUnit.SECONDS
12+
13+
class AppSecPlayNettySmokeTest extends AbstractAppSecServerSmokeTest {
14+
15+
@Shared
16+
File playDirectory = new File("${buildDirectory}/stage/main")
17+
18+
@Override
19+
ProcessBuilder createProcessBuilder() {
20+
// If the server is not shut down correctly, this file can be left there and will block
21+
// the start of a new test
22+
def runningPid = new File(playDirectory.getPath(), "RUNNING_PID")
23+
if (runningPid.exists()) {
24+
runningPid.delete()
25+
}
26+
def command = isWindows() ? 'main.bat' : 'main'
27+
ProcessBuilder processBuilder = new ProcessBuilder("${playDirectory}/bin/${command}")
28+
processBuilder.directory(playDirectory)
29+
processBuilder.environment().put("JAVA_OPTS",
30+
(defaultAppSecProperties + defaultJavaProperties).collect({ it.replace(' ', '\\ ')}).join(" ")
31+
+ " -Dconfig.file=${playDirectory}/conf/application.conf"
32+
+ " -Dhttp.port=${httpPort}"
33+
+ " -Dhttp.address=127.0.0.1"
34+
+ " -Dplay.server.provider=play.core.server.NettyServerProvider"
35+
+ " -Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter")
36+
return processBuilder
37+
}
38+
39+
@Override
40+
File createTemporaryFile() {
41+
return new File("${buildDirectory}/tmp/trace-structure-play-2.4-appsec-netty.out")
42+
}
43+
44+
void 'API Security samples only one request per endpoint'() {
45+
given:
46+
def url = "http://localhost:${httpPort}/api_security/sampling/200?test=value"
47+
def client = OkHttpUtils.clientBuilder().build()
48+
def request = new Request.Builder()
49+
.url(url)
50+
.addHeader('X-My-Header', "value")
51+
.get()
52+
.build()
53+
54+
when:
55+
List<Response> responses = (1..3).collect {
56+
client.newCall(request).execute()
57+
}
58+
59+
then:
60+
responses.each {
61+
assert it.code() == 200
62+
}
63+
waitForTraceCount(3)
64+
def spans = rootSpans.toList().toSorted { it.span.duration }
65+
spans.size() == 3
66+
def sampledSpans = spans.findAll { it.meta.keySet().any { it.startsWith('_dd.appsec.s.req.') } }
67+
sampledSpans.size() == 1
68+
def span = sampledSpans[0]
69+
span.meta.containsKey('_dd.appsec.s.req.query')
70+
span.meta.containsKey('_dd.appsec.s.req.headers')
71+
}
72+
73+
// Ensure to clean up server and not only the shell script that starts it
74+
def cleanupSpec() {
75+
def pid = runningServerPid()
76+
if (pid) {
77+
def commands = isWindows() ? ['taskkill', '/PID', pid, '/T', '/F'] : ['kill', '-9', pid]
78+
new ProcessBuilder(commands).start().waitFor(10, SECONDS)
79+
}
80+
}
81+
82+
def runningServerPid() {
83+
def runningPid = new File(playDirectory.getPath(), 'RUNNING_PID')
84+
if (runningPid.exists()) {
85+
return Files.lines(runningPid.toPath()).findAny().orElse(null)
86+
}
87+
}
88+
89+
static isWindows() {
90+
return System.getProperty('os.name').toLowerCase().contains('win')
91+
}
92+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package controllers
2+
3+
import play.api.mvc.{Action, AnyContent, Controller}
4+
5+
class AppSecController extends Controller {
6+
7+
def apiSecuritySampling(statusCode: Int, test: String): Action[AnyContent] = Action {
8+
Status(statusCode)("EXECUTED")
9+
}
10+
11+
}

dd-smoke-tests/play-2.5/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies {
6565
implementation group: 'io.opentracing', name: 'opentracing-util', version: '0.32.0'
6666

6767
testImplementation project(':dd-smoke-tests')
68+
testImplementation project(':dd-smoke-tests:appsec')
6869
}
6970

7071
configurations.testImplementation {

dd-smoke-tests/play-2.5/conf/routes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@
66
# An example controller showing a sample home page
77
GET /welcomej controllers.JController.doGet(id: Int ?= 0)
88
GET /welcomes controllers.SController.doGet(id: Option[Int])
9+
10+
# AppSec endpoints for testing
11+
GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String)

0 commit comments

Comments
 (0)