@@ -22,8 +22,6 @@ import datadog.trace.bootstrap.instrumentation.api.URIUtils
2222import datadog.trace.core.DDSpan
2323import datadog.trace.core.datastreams.StatsGroup
2424import datadog.trace.test.util.Flaky
25- import groovy.json.JsonOutput
26- import groovy.json.JsonSlurper
2725import spock.lang.AutoCleanup
2826import spock.lang.IgnoreIf
2927import spock.lang.Requires
@@ -42,7 +40,6 @@ import static datadog.trace.api.config.TraceInstrumentationConfig.HTTP_CLIENT_TA
4240import static datadog.trace.api.config.TracerConfig.HEADER_TAGS
4341import static datadog.trace.api.config.TracerConfig.REQUEST_HEADER_TAGS
4442import static datadog.trace.api.config.TracerConfig.RESPONSE_HEADER_TAGS
45- import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan
4643import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.get
4744
4845abstract class HttpClientTest extends VersionedNamingTestBase {
@@ -69,7 +66,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
6966 }
7067 prefix(" redirect" ) {
7168 handleDistributedRequest()
72- redirect(server. address. resolve(" /success" ). toURL(). toString())
69+ redirect(server. address. resolve(request . getHeader( ' Location ' ) ?: " /success" ). toURL(). toString())
7370 }
7471 prefix(" another-redirect" ) {
7572 handleDistributedRequest()
@@ -95,23 +92,21 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
9592 handleDistributedRequest()
9693 String msg = " Hello."
9794 response. status(200 )
98- .addHeader(' x-datadog-test-response-header' , ' baz' )
99- .send(msg)
95+ .addHeader(' x-datadog-test-response-header' , ' baz' )
96+ .send(msg)
10097 }
10198 prefix(" /timeout" ) {
10299 Thread . sleep(10_000)
103100 throw new IllegalStateException (" Should never happen" )
104101 }
105102 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- }
103+ // echo if input is json
104+ final responseBody = request. getContentType() == ' application/json' ? request. body : ' {"goodbye": "world!"}' . bytes
105+ response
106+ .status(200 )
107+ .addHeader(' Content-Type' , ' application/json' )
108+ .addHeader(' X-AppSec-Test' , ' true' )
109+ .sendWithType(' application/json' , responseBody)
115110 }
116111 }
117112 }
@@ -146,19 +141,19 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
146141 def setupSpec () {
147142 List<Proxy > proxyList = Collections . singletonList(new Proxy (Proxy.Type . HTTP , new InetSocketAddress (proxy. port)))
148143 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)
144+ @Override
145+ List<Proxy > select (URI uri ) {
146+ if (uri. fragment == " proxy" ) {
147+ return proxyList
155148 }
149+ return getDefault(). select(uri)
150+ }
156151
157- @Override
158- void connectFailed (URI uri , SocketAddress sa , IOException ioe ) {
159- getDefault(). connectFailed(uri, sa, ioe)
160- }
152+ @Override
153+ void connectFailed (URI uri , SocketAddress sa , IOException ioe ) {
154+ getDefault(). connectFailed(uri, sa, ioe)
161155 }
156+ }
162157
163158 // Register the Instrumentation Gateway callbacks
164159 def ss = get(). getSubscriptionService(RequestContextSlot . APPSEC )
@@ -910,16 +905,9 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
910905 void ' test appsec client request analysis' () {
911906 given :
912907 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- ]
920908
921909 when :
922- final status = runUnderAppSecTrace {
910+ def (ctx, status) = runUnderAppSecTrace {
923911 doRequest(method, url, [' Content-Type' : contentType] + headers, body) {
924912 InputStream response ->
925913 assert response. text == body
@@ -928,25 +916,66 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
928916
929917 then :
930918 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
919+ final request = ctx . requests . first( )
920+ request . method == method
921+ request . url == url . toString()
922+ request . body . bytes == body . bytes
923+ headers . each {
924+ assert request . headers[ it. key] == [ it. value]
937925 }
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 > >
926+
927+ final response = ctx. responses. first()
928+ response. status == 200
929+ response. body. bytes == body. bytes
940930 headers. each {
941- assert requestHeaders[it. key] == [it. value]
942- assert responseHeaders[it. key] == [it. value]
931+ assert response. headers[it. key] == [it. value]
943932 }
944933
945934 where :
946935 endpoint | method | contentType | headers | body
947936 ' /json' | ' POST' | ' application/json' | [' X-AppSec-Test' : ' true' ] | ' {"hello": "world!" }'
948937 }
949938
939+ @IgnoreIf ({
940+ !instance.testAppSecClientRedirect()
941+ })
942+ void ' test appsec client redirect analysis' () {
943+ given :
944+ final url = server. address. resolve(endpoint)
945+
946+ when :
947+ def (ctx, status) = runUnderAppSecTrace {
948+ doRequest(method, url, [' Content-Type' : contentType] + headers, requestBody)
949+ }
950+
951+ then :
952+ status == 200
953+
954+ def (initialRequest, redirectRequest) = ctx. requests
955+ initialRequest. method == method
956+ initialRequest. url == url. toString()
957+ initialRequest. body. bytes == requestBody. bytes
958+ headers. each {
959+ assert initialRequest. headers[it. key] == [it. value]
960+ }
961+
962+ redirectRequest. method == ' GET'
963+ redirectRequest. url. toString(). endsWith(' /json' )
964+ redirectRequest. body == null
965+
966+ def (redirectResponse, finalResponse) = ctx. responses
967+ redirectResponse. status == 302
968+ redirectResponse. body == null
969+ redirectResponse. headers[' Location' ][0 ]. endsWith(' /json' )
970+
971+ finalResponse. status == 200
972+ finalResponse. body. bytes == responseBody. bytes
973+
974+ where :
975+ endpoint | method | contentType | headers | requestBody | responseBody
976+ ' /redirect' | ' POST' | ' application/json' | [' X-AppSec-Test' : ' true' , ' Location' : ' /json' ] | ' {"hello": "world!" }' | ' {"goodbye": "world!"}'
977+ }
978+
950979 // parent span must be cast otherwise it breaks debugging classloading (junit loads it early)
951980 void clientSpan (
952981 TraceAssert trace ,
@@ -1070,11 +1099,16 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
10701099 false
10711100 }
10721101
1073- protected <E> E runUnderAppSecTrace (Closure<E> cl ) {
1074- final ddctx = new TagContext (). withRequestContextDataAppSec(new IGCallbacks.Context ())
1102+ boolean testAppSecClientRedirect () {
1103+ false
1104+ }
1105+
1106+ protected <E> Tuple2<IGCallbacks.Context , E> runUnderAppSecTrace (Closure<E> cl ) {
1107+ final ctx = new IGCallbacks.Context ()
1108+ final ddctx = new TagContext (). withRequestContextDataAppSec(ctx)
10751109 final span = TEST_TRACER . startSpan(" test" , " test-appsec-span" , ddctx)
10761110 try {
1077- return AgentTracer . activateSpan(span). withCloseable(cl)
1111+ return Tuple . tuple(ctx, AgentTracer . activateSpan(span). withCloseable(cl) )
10781112 } finally {
10791113 span. finish()
10801114 }
@@ -1084,6 +1118,8 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
10841118
10851119 static class Context {
10861120 boolean hasAppSecData
1121+ List<HttpClientRequest > requests = []
1122+ List<HttpClientResponse > responses = []
10871123 }
10881124
10891125 final BiFunction<RequestContext , Long , Flow<Boolean > > httpClientBodySamplingCb = {
@@ -1093,16 +1129,11 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
10931129
10941130 final BiFunction<RequestContext , HttpClientRequest , Flow<Void > > httpClientRequestCb = {
10951131 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- }
1132+ final context = rqCtxt. getData(RequestContextSlot . APPSEC ) as Context
1133+ final boolean isAppSec = req. headers?. containsKey(' X-AppSec-Test' )
1134+ if (isAppSec || context?. hasAppSecData) {
1135+ context. hasAppSecData = true
1136+ context. requests. add(req)
11061137 }
11071138 Flow.ResultFlow . empty()
11081139 } as BiFunction<RequestContext , HttpClientRequest , Flow<Void > >
@@ -1111,10 +1142,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase {
11111142 RequestContext rqCtxt, HttpClientResponse res ->
11121143 final context = rqCtxt. getData(RequestContextSlot . APPSEC ) as Context
11131144 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)
1145+ context. responses. add(res)
11181146 }
11191147 Flow.ResultFlow . empty()
11201148 } as BiFunction<RequestContext , HttpClientResponse , Flow<Void > >
0 commit comments