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 @@ -8,6 +8,7 @@
import datadog.trace.util.throwable.FatalAgentMisconfigurationError;
import javax.annotation.Nullable;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -48,7 +49,13 @@ public BackendApi createDirectIntakeApi(Intake intake) {

/** Creates an authenticated API client that sends data directly to a Datadog intake. */
public BackendApi createDirectIntakeApi(Intake intake, boolean responseCompression) {
HttpUrl agentlessUrl = HttpUrl.get(intake.getAgentlessUrl(config));
return createDirectIntakeApi(intake, responseCompression, true);
}

/** Creates an authenticated API client that sends data directly to a Datadog intake. */
public BackendApi createDirectIntakeApi(
Intake intake, boolean responseCompression, boolean followRedirects) {
HttpUrl agentlessUrl = buildDirectIntakeUrl(intake, config);
String apiKey = config.getApiKey();
if (apiKey == null || apiKey.isEmpty()) {
throw new FatalAgentMisconfigurationError(
Expand All @@ -60,10 +67,52 @@ public BackendApi createDirectIntakeApi(Intake intake, boolean responseCompressi
apiKey,
traceId,
retryPolicyFactory(),
sharedCommunicationObjects.getIntakeHttpClient(),
directIntakeHttpClient(sharedCommunicationObjects.getIntakeHttpClient(), followRedirects),
responseCompression);
}

static OkHttpClient directIntakeHttpClient(
final OkHttpClient intakeHttpClient, final boolean followRedirects) {
if (followRedirects) {
return intakeHttpClient;
}
return intakeHttpClient.newBuilder().followRedirects(false).followSslRedirects(false).build();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude working with Aaron:

Two separate points: the second flag is inert, and the HTTPS test you would want is cheap to add.

followSslRedirects(false) is inert. followRedirects(false) already blocks every redirect, including scheme-changing ones, so OkHttp never consults it. I proved it by flipping the second flag to true and re-running BackendApiFactoryTest: still green. Either drop it or say why it stays.

// followRedirects(false) blocks every redirect, including scheme changes, so
// followSslRedirects is never consulted.
return intakeHttpClient.newBuilder().followRedirects(false).build();

An HTTPS MockWebServer case is worth adding. The current test redirects plaintext to plaintext, but production intake is https://. okhttp-tls is published at the same 3.12.12 you already pin for mockwebserver, and it ships HandshakeCertificates, so this is one catalog entry plus a regeneration of communication/gradle.lockfile.

// gradle/libs.versions.toml
// okhttp3-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okhttp3-testing" }

final HandshakeCertificates certificates =
    new HandshakeCertificates.Builder()
        .addPlatformTrustedCertificates()
        .heldCertificate(new HeldCertificate.Builder().build())
        .build();
intake.useHttps(certificates.sslSocketFactory(), false);
// and give the client the matching trust manager:
// .sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager())

One caveat so this is not oversold: the HTTPS case will not turn red if followSslRedirects(false) is removed, because followRedirects(false) covers it. Add it for realistic transport coverage, not as justification for the flag.

The redirect rejection itself is correctly enforced. Flipping followRedirects to true turns the test red on all four status codes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Codex working with Aaron: The followSslRedirects point is correct, but the proposed TLS setup will fail. heldCertificate(...) sets server identity; it does not make the client trust the self-signed certificate, which also needs a localhost SAN. Use separate server/client HandshakeCertificates, addSubjectAlternativeName("localhost"), and client.addTrustedCertificate(serverCertificate.certificate()). The HTTPS test is optional because followRedirects(false) already covers the behavior.

}

private static HttpUrl buildDirectIntakeUrl(Intake intake, Config config) {
if (intake != Intake.EVENT_PLATFORM) {
return HttpUrl.get(intake.getAgentlessUrl(config));
}
return buildEventPlatformIntakeUrl(config.getSite());
}

static HttpUrl buildEventPlatformIntakeUrl(String site) {
if (site == null || site.isEmpty()) {
throw new IllegalArgumentException("Invalid Datadog site");
}

String expectedHost = Intake.EVENT_PLATFORM.getUrlPrefix() + "." + site;
HttpUrl url =
new HttpUrl.Builder()
.scheme("https")
.host(expectedHost)
.addPathSegment("api")
.addPathSegment(Intake.EVENT_PLATFORM.getVersion())
.addPathSegment("")
.build();
if (!url.isHttps()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude working with Aaron:

Seven of these eight conditions cannot fire. Please keep only the host comparison.

This method builds the URL, then validates the URL it just built. HttpUrl.Builder.host() already throws for userinfo, colons, slashes, spaces, backslashes, queries and fragments.

I ran the 12 non-null inputs from eventPlatformDirectIntakeRejectsUnsafeSite, plus four more. Every one is rejected by host() before this block runs. null and the empty string are caught earlier, by the guard on line 90. The only condition that ever fires is host().equalsIgnoreCase, for a non-ASCII site such as dätadoghq.com whose punycode form differs from the input.

Deleting lines 103-112 entirely left BackendApiFactoryTest green.

// HttpUrl.Builder.host() already rejects userinfo, ports, paths, queries and
// fragments, so only the host itself still needs checking. This catches an IDN
// site whose punycode form differs from the configured value.
if (!url.host().equalsIgnoreCase(expectedHost)) {
  throw new IllegalArgumentException("Invalid Datadog site");
}

To be fair to the current code: these are defensive postconditions, not a hole. The cost is that seven unreachable clauses hide the one check that does the work, so a future reader cannot tell which line is load-bearing.

Two things I confirmed while here, both good. The site validation genuinely closes the userinfo trick, since the previous HttpUrl.get("https://event-platform-intake." + site + "/api/v2/") would have parsed datadoghq.com@evil.example as host evil.example and sent DD-API-KEY there. And the new builder is reachable only from the feature-flag path, because Intake.EVENT_PLATFORM has no custom-URL override.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Codex working with Aaron: The reachability analysis is correct, but I would not request this change. These are cheap defensive checks around API-key egress and make the security invariant explicit. Treat removal as optional cleanup.

|| !url.username().isEmpty()
|| !url.password().isEmpty()
|| !url.host().equalsIgnoreCase(expectedHost)
|| url.port() != 443
|| !url.encodedPath().equals("/api/" + Intake.EVENT_PLATFORM.getVersion() + "/")
|| url.encodedQuery() != null
|| url.encodedFragment() != null) {
throw new IllegalArgumentException("Invalid Datadog site");
}
return url;
}

/** Creates an API client that uses the specified retry policy with a compatible local proxy. */
public @Nullable BackendApi createEvpProxyApi(Intake intake) {
return createEvpProxyApi(intake, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@
import datadog.remoteconfig.DefaultConfigurationPoller;
import datadog.trace.api.Config;
import datadog.trace.api.civisibility.config.BazelMode;
import datadog.trace.util.AgentProxySelector;
import datadog.trace.util.AgentTaskScheduler;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.security.Security;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
Expand All @@ -44,6 +56,9 @@ public class SharedCommunicationObjects {
*/
private volatile OkHttpClient intakeHttpClient;

private volatile HttpUrl intakeHttpsProxy;
private volatile Set<String> intakeNoProxyHosts = Collections.emptySet();

@SuppressFBWarnings("PA_PUBLIC_PRIMITIVE_ATTRIBUTE")
public long httpClientTimeout;

Expand Down Expand Up @@ -78,6 +93,8 @@ public void createRemaining(Config config) {
: TimeUnit.SECONDS.toMillis(config.getAgentTimeout());

forceClearTextHttpForIntakeClient = config.isForceClearTextHttpForIntakeClient();
intakeHttpsProxy = parseHttpsProxy(config.getHttpsProxy());
intakeNoProxyHosts = config.getNoProxyHosts();

if (agentUrl == null) {
agentUrl = parseAgentUrl(config);
Expand Down Expand Up @@ -269,11 +286,94 @@ public OkHttpClient getIntakeHttpClient() {

synchronized (this) {
if (this.intakeHttpClient == null) {
this.intakeHttpClient =
OkHttpClient intakeClient =
OkHttpUtils.buildHttpClient(
forceClearTextHttpForIntakeClient, null, null, httpClientTimeout);
if (intakeHttpsProxy != null) {
final Proxy proxy =
new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress(intakeHttpsProxy.host(), intakeHttpsProxy.port()));
final OkHttpClient.Builder builder =
intakeClient
.newBuilder()
.proxySelector(new IntakeProxySelector(proxy, intakeNoProxyHosts));
if (!intakeHttpsProxy.username().isEmpty()) {
final String credential =
Credentials.basic(intakeHttpsProxy.username(), intakeHttpsProxy.password());
builder.proxyAuthenticator(
(route, response) ->
response
.request()
.newBuilder()
.header("Proxy-Authorization", credential)
.build());
}
intakeClient = builder.build();
}
this.intakeHttpClient = intakeClient;
}
return this.intakeHttpClient;
}
}

@Nullable
static HttpUrl parseHttpsProxy(@Nullable final String configuredProxy) {
if (configuredProxy == null || configuredProxy.trim().isEmpty()) {
return null;
}
final String candidate =
configuredProxy.contains("://") ? configuredProxy : "http://" + configuredProxy;
final HttpUrl proxy = HttpUrl.parse(candidate);
if (proxy == null || !"http".equalsIgnoreCase(proxy.scheme())) {
log.warn("Ignoring invalid HTTPS proxy configuration");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude working with Aaron: (nit)

Falling back here is fine. Please make the warning say what was ignored and what happens next.

An operator who sets an https:// proxy URL currently gets one line with no setting name and no consequence.

Two accuracy points for the new message. The value can arrive from DD_PROXY_HTTPS, HTTPS_PROXY, https_proxy or a system property, so naming one of them may be wrong. And returning null here does not mean traffic goes direct: the client keeps the default AgentProxySelector, which delegates to ProxySelector.getDefault() and may still apply a JVM-configured proxy.

if (proxy == null || !"http".equalsIgnoreCase(proxy.scheme())) {
  // Only an http:// proxy URL is supported. Falling back is intentional; the
  // client keeps the default selector, which may still apply a JVM proxy.
  log.warn(
      "Ignoring configured HTTPS proxy: only an http:// proxy URL is supported. "
          + "Intake traffic will fall back to the default proxy selection.");
  return null;
}

Keeping the configured value out of the message is right, since it can contain credentials. Worth a short comment saying so, to stop someone adding it back for debuggability.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Codex working with Aaron: The warning should be clearer, but the suggested text is slightly inaccurate because host:port without a scheme is accepted. Prefer: “Ignoring configured intake proxy: expected host:port or an http:// URL; using default proxy selection instead.” Keep the configured value out because it may contain credentials.

return null;
}
return proxy;
}

static final class IntakeProxySelector extends ProxySelector {
private static final List<Proxy> DIRECT = Collections.singletonList(Proxy.NO_PROXY);

private final Proxy proxy;
private final Set<String> noProxyHosts;

IntakeProxySelector(final Proxy proxy, final Set<String> noProxyHosts) {
this.proxy = proxy;
this.noProxyHosts = noProxyHosts;
}

@Override
public List<Proxy> select(final URI uri) {
final String host = uri.getHost();
if (host != null && shouldBypassProxy(host)) {
return DIRECT;
}
if ("https".equalsIgnoreCase(uri.getScheme())) {
return Collections.singletonList(proxy);
}
return AgentProxySelector.INSTANCE.select(uri);
}

@Override
public void connectFailed(
final URI uri, final SocketAddress address, final IOException failure) {
AgentProxySelector.INSTANCE.connectFailed(uri, address, failure);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude working with Aaron: (nit)

This reports our own proxy's failure to a selector that never returned it. Please make that explicit.

The failed address comes from IntakeProxySelector. Delegating to AgentProxySelector forwards it to the JVM default selector, which has no knowledge of this proxy.

@Override
public void connectFailed(
    final URI uri, final SocketAddress address, final IOException failure) {
  if (address.equals(proxy.address())) {
    // This proxy came from our own configuration, not from the default
    // selector, so reporting the failure there would be meaningless.
    log.debug("Intake proxy connection failed for {}", uri, failure);
    return;
  }
  AgentProxySelector.INSTANCE.connectFailed(uri, address, failure);
}

}

private boolean shouldBypassProxy(final String host) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude working with Aaron:

Please move this matcher into one shared helper and have AgentProxySelector use it too.

One config value now behaves two ways in one JVM. AgentProxySelector.java:24 matches exactly and case-sensitively with noProxyHosts.contains(uri.getHost()). This matcher lowercases, honours *, and does leading-dot suffix matching.

I ran both against DD_PROXY_NO_PROXY=direct.example,.internal.example,EXAMPLE.COM:

host                       IntakeProxySelector   AgentProxySelector
direct.example             BYPASS                BYPASS
service.internal.example   BYPASS                via proxy   <-- diverges
internal.example           BYPASS                via proxy   <-- diverges
example.com                BYPASS                via proxy   <-- diverges

We would like the case-insensitive behaviour to win, so please conform the agent selector to it.

// datadog.trace.util.NoProxyHosts (new) - one matcher, used by both selectors
public static boolean matches(final String host, final Set<String> noProxyHosts) {
  // body moved verbatim from shouldBypassProxy
}

// AgentProxySelector.select
if (uri.getHost() != null && NoProxyHosts.matches(uri.getHost(), noProxyHosts)) {
  return DIRECT;
}
return defaultProxySelector.select(uri);

One shared helper is also the natural home for the bare-domain suffix fix that both bots raised on line 372.

To be explicit about the risk: conforming makes the agent selector bypass the proxy more often than it does today. For someone who relies on a JVM-configured proxy to reach the Agent, that means new direct egress. It is a deliberate compatibility change, not a free one, so it belongs in the release notes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Codex working with Aaron: The divergence is real, but changing AgentProxySelector here is not a safe default. It broadens existing DD_PROXY_NO_PROXY behavior and can bypass a JVM proxy outside Feature Flags. Treat this as a separate compatibility decision with tests and release notes; it should not block this PR.

final String normalizedHost = host.toLowerCase(Locale.ROOT);
for (final String configuredHost : noProxyHosts) {
final String normalized = configuredHost.trim().toLowerCase(Locale.ROOT);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude working with Aaron: (nit)

Please normalize the no-proxy set once, in the constructor.

trim().toLowerCase(Locale.ROOT) currently runs for every configured host on every select() call. The set is fixed at startup. This is a tracer on a customer host, so the repeated allocation is worth removing.

Both halves of the change, since normalizing in the constructor only helps if the loop stops re-normalizing:

// needs java.util.HashSet
IntakeProxySelector(final Proxy proxy, final Set<String> noProxyHosts) {
  this.proxy = proxy;
  final Set<String> normalized = new HashSet<>(noProxyHosts.size());
  for (final String host : noProxyHosts) {
    normalized.add(host.trim().toLowerCase(Locale.ROOT));
  }
  this.noProxyHosts = normalized;
}

private boolean shouldBypassProxy(final String host) {
  final String normalizedHost = host.toLowerCase(Locale.ROOT);
  for (final String normalized : noProxyHosts) {   // already normalized
    if ("*".equals(normalized)
        || normalizedHost.equals(normalized)
        || (normalized.startsWith(".")
            && (normalizedHost.equals(normalized.substring(1))
                || normalizedHost.endsWith(normalized)))) {
      return true;
    }
  }
  return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Codex working with Aaron: The repeated normalization is real, but it runs per outbound batch, not per span, and the list is normally small. Keep this as an optional micro-optimization, not requested review work. It becomes reasonable if a shared matcher is introduced separately.

if ("*".equals(normalized)
|| normalizedHost.equals(normalized)
|| (normalized.startsWith(".")
&& (normalizedHost.equals(normalized.substring(1))
|| normalizedHost.endsWith(normalized)))) {
Comment on lines +369 to +372

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match bare NO_PROXY domains against subdomains

When a standard domain entry such as NO_PROXY=datadoghq.com is used with HTTPS_PROXY, this matcher bypasses only the exact host because suffix matching is restricted to entries starting with a dot. Consequently, event-platform-intake.datadoghq.com is still sent through the proxy, even though bare domain entries in no-proxy lists are expected to cover that domain and its subdomains. Apply boundary-aware suffix matching to bare domain entries as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Codex working with Aaron: This is correct only if this PR keeps standard NO_PROXY support. If the ambient standard-variable reads are removed as suggested elsewhere, this case is out of scope and should move with that support to a follow-up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Plain NO_PROXY domains do not match subdomains

Direct Feature Flags intake can fail when the proxy blocks Datadog traffic.

Assertion details
  • Input: Set DD_PROXY_HTTPS and set NO_PROXY=datadoghq.com. Then send direct intake traffic to event-platform-intake.datadoghq.com.
  • Expected: A plain NO_PROXY domain must bypass the proxy for that domain and its subdomains.
  • Actual: The selector uses the configured HTTPS proxy because suffix matching only applies to entries that start with a dot.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

return true;
}
}
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class SharedCommunicationsObjectsSpecification extends DDSpecification {
1 * config.isCiVisibilityEnabled()
1 * config.getAgentTimeout()
1 * config.isForceClearTextHttpForIntakeClient()
1 * config.getHttpsProxy()
1 * config.getNoProxyHosts()
0 * _
sco.agentUrl.is(url)
sco.agentHttpClient.is(okHttpClient)
Expand Down Expand Up @@ -136,4 +138,55 @@ class SharedCommunicationsObjectsSpecification extends DDSpecification {
then:
client != null
}

void 'configures standard HTTPS proxy for intake while preserving no-proxy hosts'() {
given:
Config config = Mock()
sco.agentUrl = HttpUrl.get("http://example.com")
sco.agentHttpClient = Mock(OkHttpClient)
sco.monitoring = Monitoring.DISABLED
sco.featuresDiscovery = Mock(DDAgentFeaturesDiscovery)

when:
sco.createRemaining(config)
def selector = sco.getIntakeHttpClient().proxySelector()

then:
1 * config.isCiVisibilityEnabled() >> false
1 * config.getAgentTimeout() >> 1
1 * config.isForceClearTextHttpForIntakeClient() >> false
1 * config.getHttpsProxy() >> "http://proxy.example:8181"
1 * config.getNoProxyHosts() >> (["direct.example", ".internal.example"] as Set)
0 * _

and:
def selected = selector.select(new URI("https://event-platform-intake.datadoghq.com"))
selected.size() == 1
selected[0].type() == Proxy.Type.HTTP
selected[0].address() == new InetSocketAddress("proxy.example", 8181)
selector.select(new URI("https://direct.example")) == [Proxy.NO_PROXY]
selector.select(new URI("https://service.internal.example")) == [Proxy.NO_PROXY]
}

void 'parses supported HTTPS proxy forms without logging credentials'() {
expect:
SharedCommunicationObjects.parseHttpsProxy(configured)?.toString() == expected

where:
configured | expected
null | null
"" | null
"proxy.example:8080" | "http://proxy.example:8080/"
"http://user:pass@proxy:3128" | "http://user:pass@proxy:3128/"
"https://unsupported.example" | null
}

void 'wildcard no-proxy host bypasses configured HTTPS proxy'() {
given:
def proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example", 8181))
def selector = new SharedCommunicationObjects.IntakeProxySelector(proxy, ["*"] as Set)

expect:
selector.select(new URI("https://event-platform-intake.datadoghq.com")) == [Proxy.NO_PROXY]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import datadog.trace.api.Config;
import datadog.trace.api.ProtocolVersion;
import datadog.trace.api.intake.Intake;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
Expand All @@ -22,11 +24,98 @@
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

class BackendApiFactoryTest {

private static final MediaType JSON = MediaType.parse("application/json");

@ParameterizedTest
@ValueSource(strings = {"datadoghq.com", "custom.example", "DATADOGHQ.EU"})
void eventPlatformDirectIntakeUsesExactHttpsHost(String site) {
final HttpUrl url = BackendApiFactory.buildEventPlatformIntakeUrl(site);

assertEquals("https", url.scheme());
assertEquals("event-platform-intake." + site.toLowerCase(Locale.ROOT), url.host());
assertEquals(443, url.port());
assertEquals("/api/v2/", url.encodedPath());
assertEquals("", url.username());
assertEquals("", url.password());
assertNull(url.encodedQuery());
assertNull(url.encodedFragment());
}

@ParameterizedTest
@NullAndEmptySource
@ValueSource(
strings = {
"datadoghq.com@evil.example",
"datadoghq.com:password@evil.example",
"https://datadoghq.com",
"datadoghq.com:443",
"datadoghq.com:8443",
"datadoghq.com/path",
"datadoghq.com?query=value",
"datadoghq.com#fragment",
"data doghq.com",
" datadoghq.com",
"datadoghq.com ",
"datadoghq.com\\evil.example"
})
void eventPlatformDirectIntakeRejectsUnsafeSite(String site) {
assertThrows(
IllegalArgumentException.class, () -> BackendApiFactory.buildEventPlatformIntakeUrl(site));
}

@ParameterizedTest
@ValueSource(ints = {301, 302, 307, 308})
void featureFlagDirectIntakeDoesNotFollowRedirects(final int statusCode) throws Exception {
final MockWebServer intake = new MockWebServer();
final MockWebServer redirectTarget = new MockWebServer();
final OkHttpClient sharedClient = new OkHttpClient.Builder().build();
final OkHttpClient directClient = BackendApiFactory.directIntakeHttpClient(sharedClient, false);
redirectTarget.start();
intake.enqueue(
new MockResponse()
.setResponseCode(statusCode)
.setHeader("Location", redirectTarget.url("/redirected")));
intake.start();
try {
final IntakeApi api =
new IntakeApi(
intake.url("/api/v2/"),
"api-key",
"123",
HttpRetryPolicy.Factory.NEVER_RETRY,
directClient,
false);

assertThrows(
IOException.class,
() ->
api.post(
"flagevaluation",
RequestBody.create(JSON, "{}".getBytes(StandardCharsets.UTF_8)),
stream -> null,
null,
false));

final RecordedRequest request = intake.takeRequest();
assertEquals("api-key", request.getHeader("DD-API-KEY"));
assertEquals(1, intake.getRequestCount());
assertEquals(0, redirectTarget.getRequestCount());
} finally {
directClient.dispatcher().executorService().shutdownNow();
directClient.connectionPool().evictAll();
sharedClient.dispatcher().executorService().shutdownNow();
sharedClient.connectionPool().evictAll();
intake.shutdown();
redirectTarget.shutdown();
}
}

@Test
void noBackendApiWhenAgentDoesNotAdvertiseEvpProxy() {
final FakeFeaturesDiscovery discovery = new FakeFeaturesDiscovery(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public final class TracerConfig {
public static final String AGENT_TIMEOUT = "trace.agent.timeout";
public static final String FORCE_CLEAR_TEXT_HTTP_FOR_INTAKE_CLIENT =
"force.clear.text.http.for.intake.client";
public static final String PROXY_HTTPS = "proxy.https";
public static final String PROXY_NO_PROXY = "proxy.no_proxy";
public static final String TRACE_AGENT_PATH = "trace.agent.path";
public static final String TRACE_AGENT_ARGS = "trace.agent.args";
Expand Down
Loading