Skip to content
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 @@ -22,7 +22,8 @@ public String[] helperClassNames() {
packageName + ".DelegatingRequestProducer",
packageName + ".TraceContinuedFutureCallback",
packageName + ".ApacheHttpAsyncClientDecorator",
packageName + ".HostAndRequestAsHttpUriRequest"
packageName + ".HostAndRequestAsHttpUriRequest",
packageName + ".RedirectHelper"
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void methodAdvice(MethodTransformer transformer) {

public static class ClientRedirectAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
private static void onAfterExecute(
static void onAfterExecute(
@Advice.Argument(value = 2) final HttpContext context,
@Advice.Return(typing = Assigner.Typing.DYNAMIC) final HttpRequest redirect) {
if (redirect == null) {
Expand All @@ -58,31 +58,31 @@ private static void onAfterExecute(
}
HttpRequest original = (HttpRequest) originalRequest;

// Apache HttpClient 4.0.1+ copies headers from original to redirect only
// if redirect headers are empty. Because we add headers
// "x-datadog-" and "x-b3-" to redirect: it means redirect headers never
// will be empty. So in case if not-instrumented redirect had no headers,
// we just copy all not set headers from original to redirect (doing same
// thing as apache httpclient does).
if (!redirect.headerIterator().hasNext()) {
// redirect didn't have other headers besides tracing, so we need to do copy
// (same work as Apache HttpClient 4.0.1+ does w/o instrumentation)
if (original instanceof HttpRequestWrapper) {
// We should use the initial request because the wrapped one might contain more headers
// (i.e. Host) we do not want to copy
// if we cannot access the original request we cannot safely copy.
// At this point we break the propagation not to corrupt the customer request
redirect.setHeaders(((HttpRequestWrapper) original).getOriginal().getAllHeaders());
}
// Apache HttpClient 4.0.1+ copies headers from the original request to the redirect only when
// the redirect request has no headers. Because tracing injects propagation headers before
// redirect handling completes, an otherwise empty redirect request may no longer look empty
// to HttpClient. Preserve that header-copy behavior only for same-origin redirects;
// cross-origin redirects must not receive application headers such as Authorization or
// Cookie.
boolean emptyRedirect = !redirect.headerIterator().hasNext();
if (emptyRedirect && RedirectHelper.isSameOrigin(context, original, redirect)) {
redirect.setHeaders(((HttpRequestWrapper) original).getOriginal().getAllHeaders());
} else {
boolean copiedPropagationHeader = false;
for (final Header header : original.getAllHeaders()) {
if (PropagationUtils.KNOWN_PROPAGATION_HEADERS.contains(
header.getName().toLowerCase(Locale.ROOT))) {
if (!redirect.containsHeader(header.getName())) {
redirect.setHeader(header.getName(), header.getValue());
copiedPropagationHeader = true;
}
}
}
if (emptyRedirect && !copiedPropagationHeader) {
// When there are no propagation headers to copy, add a harmless header to keep HttpClient
// from treating the redirect as empty and copying application headers later.
redirect.setHeader("x-datadog-redirect", "true");
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package datadog.trace.instrumentation.apachehttpasyncclient;

import java.net.URI;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.protocol.HttpContext;

public final class RedirectHelper {
private RedirectHelper() {}

public static boolean isSameOrigin(
final HttpContext context, final HttpRequest original, final HttpRequest redirect) {
if (!(original instanceof HttpRequestWrapper) || !(redirect instanceof HttpUriRequest)) {
return false;
}
HttpRequest unwrappedOriginal = ((HttpRequestWrapper) original).getOriginal();
if (!(unwrappedOriginal instanceof HttpUriRequest)) {
return false;
}
URI originalUri = ((HttpUriRequest) unwrappedOriginal).getURI();
URI redirectUri = ((HttpUriRequest) redirect).getURI();
if (originalUri == null || redirectUri == null) {
return false;
}

originalUri = resolveOriginalUri(context, originalUri);
if (!redirectUri.isAbsolute()) {
redirectUri = originalUri.resolve(redirectUri);
}
if (originalUri.getScheme() == null || redirectUri.getScheme() == null) {
return false;
}

String originalHost = originalUri.getHost();
String redirectHost = redirectUri.getHost();
if (originalHost == null || redirectHost == null) {
return false;
}

return originalUri.getScheme().equalsIgnoreCase(redirectUri.getScheme())
&& originalHost.equalsIgnoreCase(redirectHost)
&& effectivePort(originalUri) == effectivePort(redirectUri);
}

private static URI resolveOriginalUri(final HttpContext context, final URI originalUri) {
if (originalUri.isAbsolute()) {
return originalUri;
}
Object targetHost = context.getAttribute("http.target_host");
if (!(targetHost instanceof HttpHost)) {
return originalUri;
}
HttpHost host = (HttpHost) targetHost;
return URI.create(host.toURI()).resolve(originalUri);
}

private static int effectivePort(final URI uri) {
if (uri.getPort() != -1) {
return uri.getPort();
}
String scheme = uri.getScheme();
if ("http".equalsIgnoreCase(scheme)) {
return 80;
}
if ("https".equalsIgnoreCase(scheme)) {
return 443;
}
return -1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package datadog.trace.instrumentation.apachehttpasyncclient;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import org.apache.http.HttpHost;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.junit.jupiter.api.Test;

class ApacheHttpClientRedirectInstrumentationTest {

@Test
void doesNotCopyApplicationHeadersToCrossOriginRedirects() throws Exception {
HttpGet original = originalRequest("http://example.com/request");
original.addHeader("Authorization", "Bearer secret");
original.addHeader("Cookie", "session=secret");
original.addHeader("X-Api-Key", "secret");
original.addHeader("x-datadog-trace-id", "123");

HttpGet redirect = new HttpGet("http://attacker.example/redirect");

ApacheHttpClientRedirectInstrumentation.ClientRedirectAdvice.onAfterExecute(
contextWith(original), redirect);

assertFalse(redirect.containsHeader("Authorization"));
assertFalse(redirect.containsHeader("Cookie"));
assertFalse(redirect.containsHeader("X-Api-Key"));
assertEquals("123", redirect.getFirstHeader("x-datadog-trace-id").getValue());
}

@Test
void preventsApacheHeaderCopyWhenCrossOriginRedirectHasNoPropagationHeaders() throws Exception {
HttpGet original = originalRequest("http://example.com/request");
original.addHeader("Authorization", "Bearer secret");
original.addHeader("Cookie", "session=secret");

HttpGet redirect = new HttpGet("http://attacker.example/redirect");

ApacheHttpClientRedirectInstrumentation.ClientRedirectAdvice.onAfterExecute(
contextWith(original), redirect);

assertFalse(redirect.containsHeader("Authorization"));
assertFalse(redirect.containsHeader("Cookie"));
assertEquals("true", redirect.getFirstHeader("x-datadog-redirect").getValue());
}

@Test
void copiesApplicationHeadersToSameOriginRedirects() throws Exception {
HttpGet original = originalRequest("https://example.com/request");
original.addHeader("Authorization", "Bearer secret");
original.addHeader("x-datadog-trace-id", "123");

HttpGet redirect = new HttpGet("https://example.com/redirect");

ApacheHttpClientRedirectInstrumentation.ClientRedirectAdvice.onAfterExecute(
contextWith(original), redirect);

assertEquals("Bearer secret", redirect.getFirstHeader("Authorization").getValue());
assertEquals("123", redirect.getFirstHeader("x-datadog-trace-id").getValue());
}

@Test
void treatsDefaultPortsAsSameOrigin() throws Exception {
HttpGet original = originalRequest("https://example.com:443/request");
original.addHeader("Authorization", "Bearer secret");

HttpGet redirect = new HttpGet("https://example.com/redirect");

ApacheHttpClientRedirectInstrumentation.ClientRedirectAdvice.onAfterExecute(
contextWith(original), redirect);

assertEquals("Bearer secret", redirect.getFirstHeader("Authorization").getValue());
}

@Test
void resolvesHostRequestOriginFromContext() throws Exception {
HttpGet original = originalRequest("/request");
original.addHeader("Authorization", "Bearer secret");

HttpGet redirect = new HttpGet("http://example.com/redirect");
HttpContext context = contextWith(original);
context.setAttribute("http.target_host", new HttpHost("example.com", 80, "http"));

ApacheHttpClientRedirectInstrumentation.ClientRedirectAdvice.onAfterExecute(context, redirect);

assertEquals("Bearer secret", redirect.getFirstHeader("Authorization").getValue());
}

private static HttpGet originalRequest(final String uri) {
return new HttpGet(uri);
}

private static HttpContext contextWith(final HttpGet request) throws Exception {
HttpContext context = new BasicHttpContext();
context.setAttribute("http.request", HttpRequestWrapper.wrap(request));
return context;
}
}