Skip to content

Store the http.route tag value inside the appsec request context in Play #8991

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: malvarez/vertx-http-route
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ public void init() {
subscriptionService.registerCallback(EVENTS.shellCmd(), this::onShellCmd);
subscriptionService.registerCallback(EVENTS.user(), this::onUser);
subscriptionService.registerCallback(EVENTS.loginEvent(), this::onLoginEvent);
subscriptionService.registerCallback(EVENTS.httpRoute(), this::onHttpRoute);

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

private void onHttpRoute(final RequestContext ctx_, final String route) {
final AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
if (ctx == null) {
return;
}
ctx.setRoute(route);
}

private Flow<Void> onLoginEvent(
final RequestContext ctx_, final LoginEvent event, final String login) {
final AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class GatewayBridgeSpecification extends DDSpecification {
BiFunction<RequestContext, String, Flow<Void>> shellCmdCB
BiFunction<RequestContext, String, Flow<Void>> userCB
TriFunction<RequestContext, LoginEvent, String, Flow<Void>> loginEventCB
BiConsumer<RequestContext, String> httpRouteCB

WafMetricCollector wafMetricCollector = Mock(WafMetricCollector)

Expand Down Expand Up @@ -464,6 +465,7 @@ class GatewayBridgeSpecification extends DDSpecification {
1 * ig.registerCallback(EVENTS.shellCmd(), _) >> { shellCmdCB = it[1]; null }
1 * ig.registerCallback(EVENTS.user(), _) >> { userCB = it[1]; null }
1 * ig.registerCallback(EVENTS.loginEvent(), _) >> { loginEventCB = it[1]; null }
1 * ig.registerCallback(EVENTS.httpRoute(), _) >> { httpRouteCB = it[1]; null }
0 * ig.registerCallback(_, _)

bridge.init()
Expand Down Expand Up @@ -1314,4 +1316,15 @@ class GatewayBridgeSpecification extends DDSpecification {
0 * traceSegment.setTagTop(_, _)
}

void 'test on httpRoute'() {
given:
final route = 'dummy-route'

when:
httpRouteCB.accept(ctx, route)

then:
arCtx.getRoute() == route
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package datadog.trace.instrumentation.play23;

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

import datadog.trace.api.Config;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
Expand All @@ -12,13 +16,17 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.concurrent.CompletionException;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.api.Routes;
import play.api.mvc.Headers;
import play.api.mvc.Request;
import scala.Option;

public class PlayHttpServerDecorator
extends HttpServerDecorator<Request, Request, play.api.mvc.Result, Headers> {
private static final Logger LOG = LoggerFactory.getLogger(PlayHttpServerDecorator.class);
public static final boolean REPORT_HTTP_STATUS = Config.get().getPlayReportHttpStatus();
public static final CharSequence PLAY_REQUEST = UTF8BytesString.create("play.request");
public static final CharSequence PLAY_ACTION = UTF8BytesString.create("play-action");
Expand Down Expand Up @@ -88,11 +96,35 @@ public AgentSpan onRequest(
if (!pathOption.isEmpty()) {
final String path = (String) pathOption.get();
HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path);
dispatchRoute(span, path);
}
}
return span;
}

/**
* Play does not set the http.route in the local root span so we need to store it in the context
* for API security
*/
private void dispatchRoute(final AgentSpan span, final String route) {
try {
final RequestContext ctx = span.getRequestContext();
if (ctx == null) {
return;
}
final CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC);
if (cbp == null) {
return;
}
final BiConsumer<RequestContext, String> cb = cbp.getCallback(EVENTS.httpRoute());
if (cb != null) {
cb.accept(ctx, route);
}
} catch (final Throwable t) {
LOG.debug("Failed to dispatch route", t);
}
}

@Override
public AgentSpan onError(final AgentSpan span, Throwable throwable) {
if (REPORT_HTTP_STATUS) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package datadog.trace.instrumentation.play24;

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

import datadog.trace.api.Config;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
Expand All @@ -12,13 +16,17 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.concurrent.CompletionException;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.api.mvc.Headers;
import play.api.mvc.Request;
import play.api.mvc.Result;
import scala.Option;

public class PlayHttpServerDecorator
extends HttpServerDecorator<Request, Request, Result, Headers> {
private static final Logger LOG = LoggerFactory.getLogger(PlayHttpServerDecorator.class);
public static final boolean REPORT_HTTP_STATUS = Config.get().getPlayReportHttpStatus();
public static final CharSequence PLAY_REQUEST = UTF8BytesString.create("play.request");
public static final CharSequence PLAY_ACTION = UTF8BytesString.create("play-action");
Expand Down Expand Up @@ -88,11 +96,35 @@ public AgentSpan onRequest(
if (!pathOption.isEmpty()) {
final String path = (String) pathOption.get();
HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path);
dispatchRoute(span, path);
}
}
return span;
}

/**
* Play does not set the http.route in the local root span so we need to store it in the context
* for API security
*/
private void dispatchRoute(final AgentSpan span, final String route) {
try {
final RequestContext ctx = span.getRequestContext();
if (ctx == null) {
return;
}
final CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC);
if (cbp == null) {
return;
}
final BiConsumer<RequestContext, String> cb = cbp.getCallback(EVENTS.httpRoute());
if (cb != null) {
cb.accept(ctx, route);
}
} catch (final Throwable t) {
LOG.debug("Failed to dispatch route", t);
}
}

@Override
public AgentSpan onError(final AgentSpan span, Throwable throwable) {
if (REPORT_HTTP_STATUS) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package datadog.trace.instrumentation.play26;

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

import datadog.trace.api.Config;
import datadog.trace.api.cache.DDCache;
import datadog.trace.api.cache.DDCaches;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
import datadog.trace.bootstrap.instrumentation.api.URIUtils;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator;
import java.lang.invoke.MethodHandle;
Expand All @@ -18,6 +23,9 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.concurrent.CompletionException;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.api.mvc.Headers;
import play.api.mvc.Request;
import play.api.mvc.Result;
Expand All @@ -29,6 +37,7 @@

public class PlayHttpServerDecorator
extends HttpServerDecorator<Request, Request, Result, Headers> {
private static final Logger LOG = LoggerFactory.getLogger(PlayHttpServerDecorator.class);
public static final boolean REPORT_HTTP_STATUS = Config.get().getPlayReportHttpStatus();
public static final CharSequence PLAY_REQUEST = UTF8BytesString.create("play.request");
public static final CharSequence PLAY_ACTION = UTF8BytesString.create("play-action");
Expand Down Expand Up @@ -143,11 +152,35 @@ public AgentSpan onRequest(
PATH_CACHE.computeIfAbsent(
defOption.get().path(), p -> addMissingSlash(p, request.path()));
HTTP_RESOURCE_DECORATOR.withRoute(span, request.method(), path, true);
dispatchRoute(span, path);
}
}
return span;
}

/**
* Play does not set the http.route in the local root span so we need to store it in the context
* for API security
*/
private void dispatchRoute(final AgentSpan span, final CharSequence route) {
try {
final RequestContext ctx = span.getRequestContext();
if (ctx == null) {
return;
}
final CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC);
if (cbp == null) {
return;
}
final BiConsumer<RequestContext, String> cb = cbp.getCallback(EVENTS.httpRoute());
if (cb != null) {
cb.accept(ctx, URIUtils.decode(route.toString()));
}
} catch (final Throwable t) {
LOG.debug("Failed to dispatch route", t);
}
}

/*
This is a workaround to add a `/` if it is missing when using split routes.

Expand Down
11 changes: 11 additions & 0 deletions dd-smoke-tests/play-2.4/app/controllers/AppSecController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package controllers

import play.api.mvc.{Action, AnyContent, Controller}

class AppSecController extends Controller {

def apiSecuritySampling(statusCode: Int, test: String): Action[AnyContent] = Action {
Status(statusCode)("EXECUTED")
}

}
1 change: 1 addition & 0 deletions dd-smoke-tests/play-2.4/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ dependencies {
implementation group: 'io.opentracing', name: 'opentracing-util', version: '0.32.0'

testImplementation project(':dd-smoke-tests')
testImplementation project(':dd-smoke-tests:appsec')
}

configurations.testImplementation {
Expand Down
3 changes: 3 additions & 0 deletions dd-smoke-tests/play-2.4/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
# An example controller showing a sample home page
GET /welcomej controllers.JController.doGet(id: Int ?= 0)
GET /welcomes controllers.SController.doGet(id: Option[Int])

# AppSec endpoints for testing
GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package datadog.smoketest

import datadog.smoketest.appsec.AbstractAppSecServerSmokeTest
import datadog.trace.agent.test.utils.OkHttpUtils
import okhttp3.Request
import okhttp3.Response
import spock.lang.Shared

import java.nio.file.Files

import static java.util.concurrent.TimeUnit.SECONDS

class AppSecPlayNettySmokeTest extends AbstractAppSecServerSmokeTest {

@Shared
File playDirectory = new File("${buildDirectory}/stage/main")

@Override
ProcessBuilder createProcessBuilder() {
// If the server is not shut down correctly, this file can be left there and will block
// the start of a new test
def runningPid = new File(playDirectory.getPath(), "RUNNING_PID")
if (runningPid.exists()) {
runningPid.delete()
}
def command = isWindows() ? 'main.bat' : 'main'
ProcessBuilder processBuilder = new ProcessBuilder("${playDirectory}/bin/${command}")
processBuilder.directory(playDirectory)
processBuilder.environment().put("JAVA_OPTS",
(defaultAppSecProperties + defaultJavaProperties).collect({ it.replace(' ', '\\ ')}).join(" ")
+ " -Dconfig.file=${playDirectory}/conf/application.conf"
+ " -Dhttp.port=${httpPort}"
+ " -Dhttp.address=127.0.0.1"
+ " -Dplay.server.provider=play.core.server.NettyServerProvider"
+ " -Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter")
return processBuilder
}

@Override
File createTemporaryFile() {
return new File("${buildDirectory}/tmp/trace-structure-play-2.4-appsec-netty.out")
}

void 'API Security samples only one request per endpoint'() {
given:
def url = "http://localhost:${httpPort}/api_security/sampling/200?test=value"
def client = OkHttpUtils.clientBuilder().build()
def request = new Request.Builder()
.url(url)
.addHeader('X-My-Header', "value")
.get()
.build()

when:
List<Response> responses = (1..3).collect {
client.newCall(request).execute()
}

then:
responses.each {
assert it.code() == 200
}
waitForTraceCount(3)
def spans = rootSpans.toList().toSorted { it.span.duration }
spans.size() == 3
def sampledSpans = spans.findAll { it.meta.keySet().any { it.startsWith('_dd.appsec.s.req.') } }
sampledSpans.size() == 1
def span = sampledSpans[0]
span.meta.containsKey('_dd.appsec.s.req.query')
span.meta.containsKey('_dd.appsec.s.req.headers')
}

// Ensure to clean up server and not only the shell script that starts it
def cleanupSpec() {
def pid = runningServerPid()
if (pid) {
def commands = isWindows() ? ['taskkill', '/PID', pid, '/T', '/F'] : ['kill', '-9', pid]
new ProcessBuilder(commands).start().waitFor(10, SECONDS)
}
}

def runningServerPid() {
def runningPid = new File(playDirectory.getPath(), 'RUNNING_PID')
if (runningPid.exists()) {
return Files.lines(runningPid.toPath()).findAny().orElse(null)
}
}

static isWindows() {
return System.getProperty('os.name').toLowerCase().contains('win')
}
}
11 changes: 11 additions & 0 deletions dd-smoke-tests/play-2.5/app/controllers/AppSecController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package controllers

import play.api.mvc.{Action, AnyContent, Controller}

class AppSecController extends Controller {

def apiSecuritySampling(statusCode: Int, test: String): Action[AnyContent] = Action {
Status(statusCode)("EXECUTED")
}

}
Loading