Skip to content

Commit 4ba4ebf

Browse files
feat(aap): analyze okhttp client redirections as separated request
1 parent fbdb1cf commit 4ba4ebf

File tree

8 files changed

+289
-83
lines changed

8 files changed

+289
-83
lines changed

dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy

Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import datadog.trace.api.gateway.Events
1515
import datadog.trace.api.gateway.Flow
1616
import datadog.trace.api.gateway.RequestContext
1717
import datadog.trace.api.gateway.RequestContextSlot
18+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan
1819
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
1920
import datadog.trace.bootstrap.instrumentation.api.TagContext
2021
import datadog.trace.bootstrap.instrumentation.api.Tags
@@ -69,7 +70,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
6970
}
7071
prefix("redirect") {
7172
handleDistributedRequest()
72-
redirect(server.address.resolve("/success").toURL().toString())
73+
redirect(server.address.resolve(request.getHeader('Location') ?: "/success").toURL().toString())
7374
}
7475
prefix("another-redirect") {
7576
handleDistributedRequest()
@@ -95,23 +96,21 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
9596
handleDistributedRequest()
9697
String msg = "Hello."
9798
response.status(200)
98-
.addHeader('x-datadog-test-response-header', 'baz')
99-
.send(msg)
99+
.addHeader('x-datadog-test-response-header', 'baz')
100+
.send(msg)
100101
}
101102
prefix("/timeout") {
102103
Thread.sleep(10_000)
103104
throw new IllegalStateException("Should never happen")
104105
}
105106
prefix("/json") {
106-
if (request.getContentType() != 'application/json') {
107-
response.status(400).send('Bad content type')
108-
} else {
109-
response
110-
.status(200)
111-
.addHeader('Content-Type', 'application/json')
112-
.addHeader('X-AppSec-Test', 'true')
113-
.sendWithType('application/json', request.body)
114-
}
107+
// echo if input is json
108+
final responseBody = request.getContentType() == 'application/json' ? request.body : '{"goodbye": "world!"}'.bytes
109+
response
110+
.status(200)
111+
.addHeader('Content-Type', 'application/json')
112+
.addHeader('X-AppSec-Test', 'true')
113+
.sendWithType('application/json', responseBody)
115114
}
116115
}
117116
}
@@ -146,19 +145,19 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
146145
def setupSpec() {
147146
List<Proxy> proxyList = Collections.singletonList(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxy.port)))
148147
proxySelector = new ProxySelector() {
149-
@Override
150-
List<Proxy> select(URI uri) {
151-
if (uri.fragment == "proxy") {
152-
return proxyList
153-
}
154-
return getDefault().select(uri)
148+
@Override
149+
List<Proxy> select(URI uri) {
150+
if (uri.fragment == "proxy") {
151+
return proxyList
155152
}
153+
return getDefault().select(uri)
154+
}
156155

157-
@Override
158-
void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
159-
getDefault().connectFailed(uri, sa, ioe)
160-
}
156+
@Override
157+
void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
158+
getDefault().connectFailed(uri, sa, ioe)
161159
}
160+
}
162161

163162
// Register the Instrumentation Gateway callbacks
164163
def ss = get().getSubscriptionService(RequestContextSlot.APPSEC)
@@ -910,16 +909,9 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
910909
void 'test appsec client request analysis'() {
911910
given:
912911
final url = server.address.resolve(endpoint)
913-
final tags = [
914-
'downstream.request.url': url.toString(),
915-
'downstream.request.method': method,
916-
'downstream.request.body': body,
917-
'downstream.response.status': 200,
918-
'downstream.response.body': body,
919-
]
920912

921913
when:
922-
final status = runUnderAppSecTrace {
914+
def (ctx, status) = runUnderAppSecTrace {
923915
doRequest(method, url, ['Content-Type': contentType] + headers, body) {
924916
InputStream response ->
925917
assert response.text == body
@@ -928,25 +920,66 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
928920

929921
then:
930922
status == 200
931-
TEST_WRITER.waitForTraces(1)
932-
final span = TEST_WRITER.get(0).find {
933-
it.spanType == 'http'
934-
}
935-
tags.each {
936-
assert span.getTag(it.key) == it.value
923+
final request = ctx.requests.first()
924+
request.method == method
925+
request.url == url.toString()
926+
request.body.bytes == body.bytes
927+
headers.each {
928+
assert request.headers[it.key] == [it.value]
937929
}
938-
final requestHeaders = new JsonSlurper().parseText(span.getTag("downstream.request.headers") as String) as Map<String, List<String>>
939-
final responseHeaders = new JsonSlurper().parseText(span.getTag("downstream.response.headers") as String) as Map<String, List<String>>
930+
931+
final response = ctx.responses.first()
932+
response.status == 200
933+
response.body.bytes == body.bytes
940934
headers.each {
941-
assert requestHeaders[it.key] == [it.value]
942-
assert responseHeaders[it.key] == [it.value]
935+
assert response.headers[it.key] == [it.value]
943936
}
944937

945938
where:
946939
endpoint | method | contentType | headers | body
947940
'/json' | 'POST' | 'application/json' | ['X-AppSec-Test': 'true'] | '{"hello": "world!" }'
948941
}
949942

943+
@IgnoreIf({
944+
!instance.testAppSecClientRedirect()
945+
})
946+
void 'test appsec client redirect analysis'() {
947+
given:
948+
final url = server.address.resolve(endpoint)
949+
950+
when:
951+
def (ctx, status) = runUnderAppSecTrace {
952+
doRequest(method, url, ['Content-Type': contentType] + headers, requestBody)
953+
}
954+
955+
then:
956+
status == 200
957+
958+
def (initialRequest, redirectRequest) = ctx.requests
959+
initialRequest.method == method
960+
initialRequest.url == url.toString()
961+
initialRequest.body.bytes == requestBody.bytes
962+
headers.each {
963+
assert initialRequest.headers[it.key] == [it.value]
964+
}
965+
966+
redirectRequest.method == 'GET'
967+
redirectRequest.url.toString().endsWith('/json')
968+
redirectRequest.body == null
969+
970+
def (redirectResponse, finalResponse) = ctx.responses
971+
redirectResponse.status == 302
972+
redirectResponse.body == null
973+
redirectResponse.headers['Location'][0].endsWith('/json')
974+
975+
finalResponse.status == 200
976+
finalResponse.body.bytes == responseBody.bytes
977+
978+
where:
979+
endpoint | method | contentType | headers | requestBody | responseBody
980+
'/redirect' | 'POST' | 'application/json' | ['X-AppSec-Test': 'true', 'Location': '/json'] | '{"hello": "world!" }' | '{"goodbye": "world!"}'
981+
}
982+
950983
// parent span must be cast otherwise it breaks debugging classloading (junit loads it early)
951984
void clientSpan(
952985
TraceAssert trace,
@@ -1070,11 +1103,16 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
10701103
false
10711104
}
10721105

1073-
protected <E> E runUnderAppSecTrace(Closure<E> cl) {
1074-
final ddctx = new TagContext().withRequestContextDataAppSec(new IGCallbacks.Context())
1106+
boolean testAppSecClientRedirect() {
1107+
false
1108+
}
1109+
1110+
protected <E> Tuple2<IGCallbacks.Context, E> runUnderAppSecTrace(Closure<E> cl) {
1111+
final ctx = new IGCallbacks.Context()
1112+
final ddctx = new TagContext().withRequestContextDataAppSec(ctx)
10751113
final span = TEST_TRACER.startSpan("test", "test-appsec-span", ddctx)
10761114
try {
1077-
return AgentTracer.activateSpan(span).withCloseable(cl)
1115+
return Tuple.tuple(ctx, AgentTracer.activateSpan(span).withCloseable(cl))
10781116
} finally {
10791117
span.finish()
10801118
}
@@ -1084,6 +1122,8 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
10841122

10851123
static class Context {
10861124
boolean hasAppSecData
1125+
List<HttpClientRequest> requests = []
1126+
List<HttpClientResponse> responses = []
10871127
}
10881128

10891129
final BiFunction<RequestContext, Long, Flow<Boolean>> httpClientBodySamplingCb = {
@@ -1093,16 +1133,11 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
10931133

10941134
final BiFunction<RequestContext, HttpClientRequest, Flow<Void>> httpClientRequestCb = {
10951135
RequestContext rqCtxt, HttpClientRequest req ->
1096-
if (req.headers?.containsKey('X-AppSec-Test')) {
1097-
final context = rqCtxt.getData(RequestContextSlot.APPSEC) as Context
1098-
if (context != null) {
1099-
context.hasAppSecData = true
1100-
activeSpan()
1101-
.setTag('downstream.request.url', req.url)
1102-
.setTag('downstream.request.method', req.method)
1103-
.setTag('downstream.request.headers', JsonOutput.toJson(req.headers))
1104-
.setTag('downstream.request.body', req.body?.text)
1105-
}
1136+
final context = rqCtxt.getData(RequestContextSlot.APPSEC) as Context
1137+
final boolean isAppSec = req.headers?.containsKey('X-AppSec-Test')
1138+
if (isAppSec || context?.hasAppSecData) {
1139+
context.hasAppSecData = true
1140+
context.requests.add(req)
11061141
}
11071142
Flow.ResultFlow.empty()
11081143
} as BiFunction<RequestContext, HttpClientRequest, Flow<Void>>
@@ -1111,10 +1146,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
11111146
RequestContext rqCtxt, HttpClientResponse res ->
11121147
final context = rqCtxt.getData(RequestContextSlot.APPSEC) as Context
11131148
if (context?.hasAppSecData) {
1114-
activeSpan()
1115-
.setTag('downstream.response.status', res.status)
1116-
.setTag('downstream.response.headers', JsonOutput.toJson(res.headers))
1117-
.setTag('downstream.response.body', res.body?.text)
1149+
context.responses.add(res)
11181150
}
11191151
Flow.ResultFlow.empty()
11201152
} as BiFunction<RequestContext, HttpClientResponse, Flow<Void>>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package datadog.trace.instrumentation.okhttp2;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
5+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
6+
7+
import com.google.auto.service.AutoService;
8+
import com.squareup.okhttp.Request;
9+
import com.squareup.okhttp.Response;
10+
import datadog.trace.agent.tooling.Instrumenter;
11+
import datadog.trace.agent.tooling.InstrumenterModule;
12+
import datadog.trace.api.gateway.RequestContext;
13+
import datadog.trace.api.gateway.RequestContextSlot;
14+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
15+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
16+
import net.bytebuddy.asm.Advice;
17+
18+
@AutoService(InstrumenterModule.class)
19+
public class AppSecHttpEngineInstrumentation extends InstrumenterModule.AppSec
20+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
21+
22+
public AppSecHttpEngineInstrumentation() {
23+
super("okhttp", "okhttp-2");
24+
}
25+
26+
@Override
27+
public String instrumentedType() {
28+
return "com.squareup.okhttp.internal.http.HttpEngine";
29+
}
30+
31+
@Override
32+
public String[] helperClassNames() {
33+
return new String[] {
34+
packageName + ".AppSecInterceptor",
35+
};
36+
}
37+
38+
@Override
39+
public void methodAdvice(final MethodTransformer transformer) {
40+
transformer.applyAdvice(
41+
isMethod().and(named("sendRequest")).and(takesArguments(0)),
42+
AppSecHttpEngineInstrumentation.class.getName() + "$SendRequestAdvice");
43+
}
44+
45+
public static class SendRequestAdvice {
46+
@Advice.OnMethodEnter
47+
public static void onSendRequest(
48+
@Advice.FieldValue("priorResponse") final Response priorResponse,
49+
@Advice.FieldValue("userRequest") final Request userRequest) {
50+
// only redirects
51+
if (priorResponse == null || priorResponse.code() < 300 || priorResponse.code() >= 400) {
52+
return;
53+
}
54+
final AgentSpan span = AgentTracer.activeSpan();
55+
final RequestContext ctx = span.getRequestContext();
56+
if (ctx == null) {
57+
return;
58+
}
59+
if (ctx.getData(RequestContextSlot.APPSEC) == null) {
60+
return;
61+
}
62+
63+
// do not include bodies in the redirect request
64+
AppSecInterceptor.onResponse(span, false, priorResponse);
65+
AppSecInterceptor.onRequest(span, false, userRequest.urlString(), userRequest);
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)