@@ -15,6 +15,7 @@ import datadog.trace.api.gateway.Events
1515import datadog.trace.api.gateway.Flow
1616import datadog.trace.api.gateway.RequestContext
1717import datadog.trace.api.gateway.RequestContextSlot
18+ import datadog.trace.bootstrap.instrumentation.api.AgentSpan
1819import datadog.trace.bootstrap.instrumentation.api.AgentTracer
1920import datadog.trace.bootstrap.instrumentation.api.TagContext
2021import 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 > >
0 commit comments