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,45 @@ 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).build();
}

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.host().equalsIgnoreCase(expectedHost)) {
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 @@ -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,99 @@
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",
"dätadoghq.com",
"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 @@ -90,7 +90,7 @@ private BackendApi createDirectApi() {
}
try {
return backendApiFactory.createDirectIntakeApi(
Intake.EVENT_PLATFORM, eventType.responseCompressionEnabled());
Intake.EVENT_PLATFORM, eventType.responseCompressionEnabled(), false);
} catch (final IllegalArgumentException exception) {
LOGGER.debug(
"Cannot configure direct Feature Flagging {} delivery", eventType.logName(), exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ void testAgentlessExposureEventWritesDirectlyWithApiKey() throws Exception {
new OkHttpClient.Builder().build(),
false);
when(backendApiFactory.createDirectIntakeApi(
datadog.trace.api.intake.Intake.EVENT_PLATFORM, true))
eq(datadog.trace.api.intake.Intake.EVENT_PLATFORM), eq(true), eq(false)))
.thenReturn(directApi);
FeatureFlagBackendApiFactory exposureBackendApiFactory =
new FeatureFlagBackendApiFactory(config, backendApiFactory, FeatureFlagEventType.EXPOSURE);
Expand Down Expand Up @@ -311,7 +311,7 @@ void testAmbiguousExposureBatchIsNotRetriedOrReplayedDirectly() throws Exception
when(backendApiFactory.createEvpProxyApi(
Intake.EVENT_PLATFORM, true, HttpRetryPolicy.Factory.NEVER_RETRY))
.thenReturn(proxyApi);
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, true))
when(backendApiFactory.createDirectIntakeApi(eq(Intake.EVENT_PLATFORM), eq(true), eq(false)))
.thenReturn(directApi);
when(proxyApi.post(eq("exposures"), any(RequestBody.class), any(), any(), eq(false)))
.thenThrow(new SocketTimeoutException("ambiguous timeout"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ void remoteConfigUsesOnlyLocalEvpProxy() {
new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create();

assertSame(proxyApi, selected);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false);
}

@Test
Expand All @@ -45,7 +45,7 @@ void remoteConfigDisablesDeliveryWhenLocalEvpProxyIsUnavailable() {

assertNull(selected);
verify(backendApiFactory).createEvpProxyApi(Intake.EVENT_PLATFORM, true);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, true);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, true, false);
}

@Test
Expand All @@ -55,7 +55,7 @@ void agentlessPrefersLocalEvpProxyWithDirectFallback() {
when(backendApiFactory.createEvpProxyApi(
Intake.EVENT_PLATFORM, false, HttpRetryPolicy.Factory.NEVER_RETRY))
.thenReturn(mock(BackendApi.class));
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false))
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false))
.thenReturn(mock(BackendApi.class));

final BackendApi selected =
Expand All @@ -64,15 +64,15 @@ void agentlessPrefersLocalEvpProxyWithDirectFallback() {
assertInstanceOf(AgentlessFeatureFlagBackendApi.class, selected);
verify(backendApiFactory)
.createEvpProxyApi(Intake.EVENT_PLATFORM, false, HttpRetryPolicy.Factory.NEVER_RETRY);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false);
}

@Test
void agentlessUsesDirectIntakeWhenLocalEvpProxyIsUnavailable() {
final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key");
final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class);
final BackendApi directApi = mock(BackendApi.class);
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false))
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false))
.thenReturn(directApi);

final BackendApi selected =
Expand All @@ -92,7 +92,7 @@ void agentlessUsesLocalEvpProxyWhenApiKeyIsUnavailable() {
new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create();

assertSame(proxyApi, selected);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false);
}

@Test
Expand All @@ -115,7 +115,7 @@ void agentlessDisablesDeliveryWhenApiKeyIsEmpty() {
new FeatureFlagBackendApiFactory(config, backendApiFactory, EXPOSURE).create();

assertNull(selected);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, true);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, true, false);
}

@Test
Expand All @@ -126,21 +126,21 @@ void agentlessDoesNotValidateDirectUrlWhileLocalRouteIsAvailable() {
when(backendApiFactory.createEvpProxyApi(
Intake.EVENT_PLATFORM, false, HttpRetryPolicy.Factory.NEVER_RETRY))
.thenReturn(proxyApi);
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false))
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false))
.thenThrow(new IllegalArgumentException("invalid URL"));

final BackendApi selected =
new FeatureFlagBackendApiFactory(config, backendApiFactory, FLAG_EVALUATION).create();

assertInstanceOf(AgentlessFeatureFlagBackendApi.class, selected);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false);
verify(backendApiFactory, never()).createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false);
}

@Test
void agentlessDisablesDeliveryWhenDirectUrlIsInvalidAndLocalRouteIsUnavailable() {
final Config config = config(CONFIGURATION_SOURCE_AGENTLESS, "api-key");
final BackendApiFactory backendApiFactory = mock(BackendApiFactory.class);
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false))
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false, false))
.thenThrow(new IllegalArgumentException("invalid URL"));

final BackendApi selected =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,8 @@ void agentlessWritesFlagEvaluationsDirectlyWhenLocalProxyIsUnavailable() throws
HttpRetryPolicy.Factory.NEVER_RETRY,
client,
false);
when(backendApiFactory.createDirectIntakeApi(Intake.EVENT_PLATFORM, false))
when(backendApiFactory.createDirectIntakeApi(
eq(Intake.EVENT_PLATFORM), eq(false), eq(false)))
.thenReturn(directApi);
final FeatureFlagBackendApiFactory featureFlagBackendApiFactory =
new FeatureFlagBackendApiFactory(
Expand Down