From 4a28ab6317dfcc3556137b59a03db93c9235f403 Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Wed, 24 Apr 2024 18:49:53 -0500 Subject: [PATCH] Add support to trial Cloudflare TURN beta --- service/config/sample-secrets-bundle.yml | 2 + service/config/sample.yml | 5 ++ .../WhisperServerConfiguration.java | 6 +- .../textsecuregcm/WhisperServerService.java | 4 +- .../textsecuregcm/auth/TurnToken.java | 4 +- .../auth/TurnTokenGenerator.java | 37 ++++++---- .../CloudflareTurnConfiguration.java | 17 +++++ ...figuration.java => TurnConfiguration.java} | 2 +- .../controllers/AccountController.java | 3 +- .../controllers/CallRoutingController.java | 19 +++-- .../auth/TurnTokenGeneratorTest.java | 38 ++++++---- .../CallRoutingControllerTest.java | 72 ++++++++++++++----- .../resources/config/test-secrets-bundle.yml | 2 + service/src/test/resources/config/test.yml | 5 ++ 14 files changed, 158 insertions(+), 58 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/CloudflareTurnConfiguration.java rename service/src/main/java/org/whispersystems/textsecuregcm/configuration/{TurnSecretConfiguration.java => TurnConfiguration.java} (70%) diff --git a/service/config/sample-secrets-bundle.yml b/service/config/sample-secrets-bundle.yml index a3c9addd..933fa014 100644 --- a/service/config/sample-secrets-bundle.yml +++ b/service/config/sample-secrets-bundle.yml @@ -91,6 +91,8 @@ currentReportingKey.secret: AAAAAAAAAAA= currentReportingKey.salt: AAAAAAAAAAA= turn.secret: AAAAAAAAAAA= +turn.cloudflare.username: ABCDEFGHIJKLM +turn.cloudflare.password: NOPQRSTUVWXYZ linkDevice.secret: AAAAAAAAAAA= diff --git a/service/config/sample.yml b/service/config/sample.yml index 3bddf548..f680be17 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -452,6 +452,11 @@ registrationService: turn: secret: secret://turn.secret + cloudflare: + username: secret://turn.cloudflare.username + password: secret://turn.cloudflare.password + urls: + - turns:turn.cloudflare.example.com:443?transport=tcp linkDevice: secret: secret://linkDevice.secret diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index dd17ed00..07cc6b90 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -57,7 +57,7 @@ import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration; import org.whispersystems.textsecuregcm.configuration.StripeConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.TlsKeyStoreConfiguration; -import org.whispersystems.textsecuregcm.configuration.TurnSecretConfiguration; +import org.whispersystems.textsecuregcm.configuration.TurnConfiguration; import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration; import org.whispersystems.textsecuregcm.configuration.VirtualThreadConfiguration; import org.whispersystems.textsecuregcm.configuration.ZkConfig; @@ -288,7 +288,7 @@ public class WhisperServerConfiguration extends Configuration { @Valid @NotNull @JsonProperty - private TurnSecretConfiguration turn; + private TurnConfiguration turn; @Valid @NotNull @@ -529,7 +529,7 @@ public class WhisperServerConfiguration extends Configuration { return registrationService; } - public TurnSecretConfiguration getTurnSecretConfiguration() { + public TurnConfiguration getTurnConfiguration() { return turn; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 539d8a6b..9340647a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -605,7 +605,7 @@ public class WhisperServerService extends Application urls, List urlsWithIps, String hostname) { +public record TurnToken(String username, String password, List urls, @Nullable List urlsWithIps, + @Nullable String hostname) { public TurnToken(String username, String password, List urls) { this(username, password, urls, null, null); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java index ddd37ffe..48e57727 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java @@ -5,17 +5,6 @@ package org.whispersystems.textsecuregcm.auth; -import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions; -import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import org.whispersystems.textsecuregcm.util.Pair; -import org.whispersystems.textsecuregcm.util.Util; -import org.whispersystems.textsecuregcm.util.WeightedRandomSelect; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -25,6 +14,17 @@ import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.UUID; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions; +import org.whispersystems.textsecuregcm.configuration.CloudflareTurnConfiguration; +import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.Util; +import org.whispersystems.textsecuregcm.util.WeightedRandomSelect; public class TurnTokenGenerator { @@ -38,13 +38,22 @@ public class TurnTokenGenerator { private static final String WithIpsProtocol = "01"; + private final String cloudflareTurnUsername; + private final String cloudflareTurnPassword; + private final List cloudflareTurnUrls; + public TurnTokenGenerator(final DynamicConfigurationManager dynamicConfigurationManager, - final byte[] turnSecret) { + final byte[] turnSecret, final CloudflareTurnConfiguration cloudflareTurnConfiguration) { this.dynamicConfigurationManager = dynamicConfigurationManager; this.turnSecret = turnSecret; + + this.cloudflareTurnUsername = cloudflareTurnConfiguration.username().value(); + this.cloudflareTurnPassword = cloudflareTurnConfiguration.password().value(); + this.cloudflareTurnUrls = cloudflareTurnConfiguration.urls(); } + @Deprecated public TurnToken generate(final UUID aci) { return generateToken(null, null, urls(aci)); } @@ -53,6 +62,10 @@ public class TurnTokenGenerator { return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname()); } + public TurnToken generateForCloudflareBeta() { + return new TurnToken(cloudflareTurnUsername, cloudflareTurnPassword, cloudflareTurnUrls); + } + private TurnToken generateToken(String hostname, List urlsWithIps, List urlsWithHostname) { try { final Mac mac = Mac.getInstance(ALGORITHM); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CloudflareTurnConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CloudflareTurnConfiguration.java new file mode 100644 index 00000000..7dd5a80d --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/CloudflareTurnConfiguration.java @@ -0,0 +1,17 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration; + +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +public record CloudflareTurnConfiguration(@NotNull SecretString username, @NotNull SecretString password, + @Valid @NotNull List<@NotBlank String> urls) { + +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnSecretConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java similarity index 70% rename from service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnSecretConfiguration.java rename to service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java index 496780e9..6637503b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnSecretConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java @@ -7,5 +7,5 @@ package org.whispersystems.textsecuregcm.configuration; import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; -public record TurnSecretConfiguration(SecretBytes secret) { +public record TurnConfiguration(SecretBytes secret, CloudflareTurnConfiguration cloudflare) { } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 4df2024e..2e505fa4 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -95,7 +95,8 @@ public class AccountController { this.usernameHashZkProofVerifier = usernameHashZkProofVerifier; } - @Deprecated + // may be removed after 2024-07-16 + @Deprecated(forRemoval = true) @GET @Path("/turn/") @Produces(MediaType.APPLICATION_JSON) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingController.java index 8d69706e..e042bd68 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingController.java @@ -1,11 +1,12 @@ package org.whispersystems.textsecuregcm.controllers; +import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; + import io.dropwizard.auth.Auth; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Metrics; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; - import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Optional; @@ -21,14 +22,13 @@ import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.TurnToken; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; -import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions; import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter; +import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.websocket.auth.ReadOnly; -import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; - @Path("/v1/calling") @io.swagger.v3.oas.annotations.tags.Tag(name = "Calling") public class CallRoutingController { @@ -39,15 +39,18 @@ public class CallRoutingController { private final RateLimiters rateLimiters; private final TurnCallRouter turnCallRouter; private final TurnTokenGenerator tokenGenerator; + private final ExperimentEnrollmentManager experimentEnrollmentManager; public CallRoutingController( final RateLimiters rateLimiters, final TurnCallRouter turnCallRouter, - final TurnTokenGenerator tokenGenerator + final TurnTokenGenerator tokenGenerator, + final ExperimentEnrollmentManager experimentEnrollmentManager ) { this.rateLimiters = rateLimiters; this.turnCallRouter = turnCallRouter; this.tokenGenerator = tokenGenerator; + this.experimentEnrollmentManager = experimentEnrollmentManager; } @GET @@ -63,7 +66,7 @@ public class CallRoutingController { @ApiResponse(responseCode = "400", description = "Invalid get call endpoint request.") @ApiResponse(responseCode = "401", description = "Account authentication check failed.") @ApiResponse(responseCode = "422", description = "Invalid request format.") - @ApiResponse(responseCode = "429", description = "Ratelimited.") + @ApiResponse(responseCode = "429", description = "Rate limited.") public TurnToken getCallingRelays( final @ReadOnly @Auth AuthenticatedAccount auth, @Context ContainerRequestContext requestContext @@ -71,6 +74,10 @@ public class CallRoutingController { UUID aci = auth.getAccount().getUuid(); rateLimiters.getCallEndpointLimiter().validate(aci); + if (experimentEnrollmentManager.isEnrolled(aci, "cloudflareTurn")) { + return tokenGenerator.generateForCloudflareBeta(); + } + Optional address = Optional.empty(); try { final String remoteAddress = (String) requestContext.getProperty( diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/auth/TurnTokenGeneratorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/auth/TurnTokenGeneratorTest.java index 4c0599bf..fb94e273 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/auth/TurnTokenGeneratorTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/auth/TurnTokenGeneratorTest.java @@ -1,22 +1,27 @@ package org.whispersystems.textsecuregcm.auth; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Test; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; - -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.core.JsonProcessingException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.configuration.CloudflareTurnConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; + public class TurnTokenGeneratorTest { + private static final CloudflareTurnConfiguration CLOUDFLARE_TURN_CONFIGURATION = new CloudflareTurnConfiguration( + new SecretString("cf_username"), new SecretString("cf_password"), List.of("turn:cloudflare.example.com")); + @Test public void testAlwaysSelectFirst() throws JsonProcessingException { final String configString = """ @@ -30,7 +35,7 @@ public class TurnTokenGeneratorTest { - uris: - never.org weight: 0 - """; + """; DynamicConfiguration config = DynamicConfigurationManager .parseConfiguration(configString, DynamicConfiguration.class) .orElseThrow(); @@ -42,7 +47,8 @@ public class TurnTokenGeneratorTest { when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); final TurnTokenGenerator turnTokenGenerator = - new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8)); + new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8), + CLOUDFLARE_TURN_CONFIGURATION); final long COUNT = 1000; @@ -83,7 +89,8 @@ public class TurnTokenGeneratorTest { when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); final TurnTokenGenerator turnTokenGenerator = - new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8)); + new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8), + CLOUDFLARE_TURN_CONFIGURATION); final long COUNT = 1000; @@ -126,7 +133,8 @@ public class TurnTokenGeneratorTest { when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); final TurnTokenGenerator turnTokenGenerator = - new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8)); + new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8), + CLOUDFLARE_TURN_CONFIGURATION); TurnToken token = turnTokenGenerator.generate(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6")); assertThat(token.urls().get(0)).isEqualTo("enrolled.org"); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerTest.java index 8565cf36..59ecb1cd 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerTest.java @@ -6,14 +6,13 @@ package org.whispersystems.textsecuregcm.controllers; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; -import com.google.common.net.HttpHeaders; import io.dropwizard.auth.AuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; @@ -24,6 +23,7 @@ import java.util.List; import java.util.Optional; import javax.ws.rs.core.Response; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,7 +32,10 @@ import org.whispersystems.textsecuregcm.auth.TurnToken; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter; import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions; +import org.whispersystems.textsecuregcm.configuration.CloudflareTurnConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.secrets.SecretString; +import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; @@ -43,30 +46,44 @@ import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider; @ExtendWith(DropwizardExtensionsSupport.class) class CallRoutingControllerTest { - private static final RateLimiters rateLimiters = mock(RateLimiters.class); - private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class); - private static final DynamicConfigurationManager configManager = mock(DynamicConfigurationManager.class); - private static final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(configManager, "bloop".getBytes( - StandardCharsets.UTF_8)); - private static final TurnCallRouter turnCallRouter = mock(TurnCallRouter.class); + private static final String GET_CALL_ENDPOINTS_PATH = "v1/calling/relays"; private static final String REMOTE_ADDRESS = "123.123.123.1"; - private static final ResourceExtension resources = ResourceExtension.builder() - .addProvider(AuthHelper.getAuthFilter()) - .addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class)) - .addProvider(new RateLimitExceededExceptionMapper()) - .addProvider(new TestRemoteAddressFilterProvider(REMOTE_ADDRESS)) - .setMapper(SystemMapper.jsonMapper()) - .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) - .addResource(new CallRoutingController(rateLimiters, turnCallRouter, turnTokenGenerator)) - .build(); + private static final RateLimiters rateLimiters = mock(RateLimiters.class); + private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class); + private static final DynamicConfigurationManager dynamicConfigurationManager = mock( + DynamicConfigurationManager.class); + private static final ExperimentEnrollmentManager experimentEnrollmentManager = mock( + ExperimentEnrollmentManager.class); + private static final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager, + "bloop".getBytes(StandardCharsets.UTF_8), + new CloudflareTurnConfiguration(new SecretString("cf_username"), new SecretString("cf_password"), + List.of("turn:cf.example.com"))); + private static final TurnCallRouter turnCallRouter = mock(TurnCallRouter.class); + + private static final ResourceExtension resources = ResourceExtension.builder() + .addProvider(AuthHelper.getAuthFilter()) + .addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class)) + .addProvider(new RateLimitExceededExceptionMapper()) + .addProvider(new TestRemoteAddressFilterProvider(REMOTE_ADDRESS)) + .setMapper(SystemMapper.jsonMapper()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new CallRoutingController(rateLimiters, turnCallRouter, turnTokenGenerator, + experimentEnrollmentManager)) + .build(); @BeforeEach void setup() { when(rateLimiters.getCallEndpointLimiter()).thenReturn(getCallEndpointLimiter); } + @AfterEach + void tearDown() { + reset(experimentEnrollmentManager, dynamicConfigurationManager, rateLimiters, getCallEndpointLimiter, + turnCallRouter); + } + @Test void testGetTurnEndpointsSuccess() throws UnknownHostException { TurnServerOptions options = new TurnServerOptions( @@ -96,6 +113,27 @@ class CallRoutingControllerTest { } } + @Test + void testGetTurnEndpointsCloudflare() { + when(experimentEnrollmentManager.isEnrolled(AuthHelper.VALID_UUID, "cloudflareTurn")) + .thenReturn(true); + + try (Response response = resources.getJerseyTest() + .target(GET_CALL_ENDPOINTS_PATH) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get()) { + + assertThat(response.getStatus()).isEqualTo(200); + TurnToken token = response.readEntity(TurnToken.class); + assertThat(token.username()).isNotEmpty(); + assertThat(token.password()).isNotEmpty(); + assertThat(token.hostname()).isNull(); + assertThat(token.urlsWithIps()).isNull(); + assertThat(token.urls()).isEqualTo(List.of("turn:cf.example.com")); + } + } + @Test void testGetTurnEndpointsInvalidIpSuccess() throws UnknownHostException { TurnServerOptions options = new TurnServerOptions( diff --git a/service/src/test/resources/config/test-secrets-bundle.yml b/service/src/test/resources/config/test-secrets-bundle.yml index 9299d449..d8cfdc93 100644 --- a/service/src/test/resources/config/test-secrets-bundle.yml +++ b/service/src/test/resources/config/test-secrets-bundle.yml @@ -124,6 +124,8 @@ currentReportingKey.secret: AAAAAAAAAAA= currentReportingKey.salt: AAAAAAAAAAA= turn.secret: AAAAAAAAAAA= +turn.cloudflare.username: ABCDEFGHIJKLM +turn.cloudflare.password: NOPQRSTUVWXYZ linkDevice.secret: AAAAAAAAAAA= diff --git a/service/src/test/resources/config/test.yml b/service/src/test/resources/config/test.yml index 5a945b6f..2e92b7fb 100644 --- a/service/src/test/resources/config/test.yml +++ b/service/src/test/resources/config/test.yml @@ -443,6 +443,11 @@ registrationService: turn: secret: secret://turn.secret + cloudflare: + username: secret://turn.cloudflare.username + password: secret://turn.cloudflare.password + urls: + - turns:turn.cloudflare.example.com:443?transport=tcp linkDevice: secret: secret://linkDevice.secret