Add support to trial Cloudflare TURN beta

This commit is contained in:
Chris Eager 2024-04-24 18:49:53 -05:00 committed by Chris Eager
parent 0986ce12e6
commit 4a28ab6317
14 changed files with 158 additions and 58 deletions

View File

@ -91,6 +91,8 @@ currentReportingKey.secret: AAAAAAAAAAA=
currentReportingKey.salt: AAAAAAAAAAA= currentReportingKey.salt: AAAAAAAAAAA=
turn.secret: AAAAAAAAAAA= turn.secret: AAAAAAAAAAA=
turn.cloudflare.username: ABCDEFGHIJKLM
turn.cloudflare.password: NOPQRSTUVWXYZ
linkDevice.secret: AAAAAAAAAAA= linkDevice.secret: AAAAAAAAAAA=

View File

@ -452,6 +452,11 @@ registrationService:
turn: turn:
secret: secret://turn.secret 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: linkDevice:
secret: secret://linkDevice.secret secret: secret://linkDevice.secret

View File

@ -57,7 +57,7 @@ import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration; import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.TlsKeyStoreConfiguration; 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.UnidentifiedDeliveryConfiguration;
import org.whispersystems.textsecuregcm.configuration.VirtualThreadConfiguration; import org.whispersystems.textsecuregcm.configuration.VirtualThreadConfiguration;
import org.whispersystems.textsecuregcm.configuration.ZkConfig; import org.whispersystems.textsecuregcm.configuration.ZkConfig;
@ -288,7 +288,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid @Valid
@NotNull @NotNull
@JsonProperty @JsonProperty
private TurnSecretConfiguration turn; private TurnConfiguration turn;
@Valid @Valid
@NotNull @NotNull
@ -529,7 +529,7 @@ public class WhisperServerConfiguration extends Configuration {
return registrationService; return registrationService;
} }
public TurnSecretConfiguration getTurnSecretConfiguration() { public TurnConfiguration getTurnConfiguration() {
return turn; return turn;
} }

View File

@ -605,7 +605,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
pushLatencyManager); pushLatencyManager);
final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor); final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager, final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
config.getTurnSecretConfiguration().secret().value()); config.getTurnConfiguration().secret().value(), config.getTurnConfiguration().cloudflare());
final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator( final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator(
rateLimitersCluster, rateLimitersCluster,
@ -938,7 +938,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator, new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator,
experimentEnrollmentManager), experimentEnrollmentManager),
new ArchiveController(backupAuthManager, backupManager), new ArchiveController(backupAuthManager, backupManager),
new CallRoutingController(rateLimiters, callRouter, turnTokenGenerator), new CallRoutingController(rateLimiters, callRouter, turnTokenGenerator, experimentEnrollmentManager),
new CallLinkController(rateLimiters, callingGenericZkSecretParams), new CallLinkController(rateLimiters, callingGenericZkSecretParams),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()), config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),

View File

@ -5,9 +5,11 @@
package org.whispersystems.textsecuregcm.auth; package org.whispersystems.textsecuregcm.auth;
import javax.annotation.Nullable;
import java.util.List; import java.util.List;
public record TurnToken(String username, String password, List<String> urls, List<String> urlsWithIps, String hostname) { public record TurnToken(String username, String password, List<String> urls, @Nullable List<String> urlsWithIps,
@Nullable String hostname) {
public TurnToken(String username, String password, List<String> urls) { public TurnToken(String username, String password, List<String> urls) {
this(username, password, urls, null, null); this(username, password, urls, null, null);
} }

View File

@ -5,17 +5,6 @@
package org.whispersystems.textsecuregcm.auth; 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.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -25,6 +14,17 @@ import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; 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 { public class TurnTokenGenerator {
@ -38,13 +38,22 @@ public class TurnTokenGenerator {
private static final String WithIpsProtocol = "01"; private static final String WithIpsProtocol = "01";
private final String cloudflareTurnUsername;
private final String cloudflareTurnPassword;
private final List<String> cloudflareTurnUrls;
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager, public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final byte[] turnSecret) { final byte[] turnSecret, final CloudflareTurnConfiguration cloudflareTurnConfiguration) {
this.dynamicConfigurationManager = dynamicConfigurationManager; this.dynamicConfigurationManager = dynamicConfigurationManager;
this.turnSecret = turnSecret; this.turnSecret = turnSecret;
this.cloudflareTurnUsername = cloudflareTurnConfiguration.username().value();
this.cloudflareTurnPassword = cloudflareTurnConfiguration.password().value();
this.cloudflareTurnUrls = cloudflareTurnConfiguration.urls();
} }
@Deprecated
public TurnToken generate(final UUID aci) { public TurnToken generate(final UUID aci) {
return generateToken(null, null, urls(aci)); return generateToken(null, null, urls(aci));
} }
@ -53,6 +62,10 @@ public class TurnTokenGenerator {
return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname()); return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname());
} }
public TurnToken generateForCloudflareBeta() {
return new TurnToken(cloudflareTurnUsername, cloudflareTurnPassword, cloudflareTurnUrls);
}
private TurnToken generateToken(String hostname, List<String> urlsWithIps, List<String> urlsWithHostname) { private TurnToken generateToken(String hostname, List<String> urlsWithIps, List<String> urlsWithHostname) {
try { try {
final Mac mac = Mac.getInstance(ALGORITHM); final Mac mac = Mac.getInstance(ALGORITHM);

View File

@ -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) {
}

View File

@ -7,5 +7,5 @@ package org.whispersystems.textsecuregcm.configuration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes; import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public record TurnSecretConfiguration(SecretBytes secret) { public record TurnConfiguration(SecretBytes secret, CloudflareTurnConfiguration cloudflare) {
} }

View File

@ -95,7 +95,8 @@ public class AccountController {
this.usernameHashZkProofVerifier = usernameHashZkProofVerifier; this.usernameHashZkProofVerifier = usernameHashZkProofVerifier;
} }
@Deprecated // may be removed after 2024-07-16
@Deprecated(forRemoval = true)
@GET @GET
@Path("/turn/") @Path("/turn/")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)

View File

@ -1,11 +1,12 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Optional; import java.util.Optional;
@ -21,14 +22,13 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.TurnToken; import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; 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.TurnCallRouter;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter; import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.websocket.auth.ReadOnly; import org.whispersystems.websocket.auth.ReadOnly;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
@Path("/v1/calling") @Path("/v1/calling")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Calling") @io.swagger.v3.oas.annotations.tags.Tag(name = "Calling")
public class CallRoutingController { public class CallRoutingController {
@ -39,15 +39,18 @@ public class CallRoutingController {
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final TurnCallRouter turnCallRouter; private final TurnCallRouter turnCallRouter;
private final TurnTokenGenerator tokenGenerator; private final TurnTokenGenerator tokenGenerator;
private final ExperimentEnrollmentManager experimentEnrollmentManager;
public CallRoutingController( public CallRoutingController(
final RateLimiters rateLimiters, final RateLimiters rateLimiters,
final TurnCallRouter turnCallRouter, final TurnCallRouter turnCallRouter,
final TurnTokenGenerator tokenGenerator final TurnTokenGenerator tokenGenerator,
final ExperimentEnrollmentManager experimentEnrollmentManager
) { ) {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.turnCallRouter = turnCallRouter; this.turnCallRouter = turnCallRouter;
this.tokenGenerator = tokenGenerator; this.tokenGenerator = tokenGenerator;
this.experimentEnrollmentManager = experimentEnrollmentManager;
} }
@GET @GET
@ -63,7 +66,7 @@ public class CallRoutingController {
@ApiResponse(responseCode = "400", description = "Invalid get call endpoint request.") @ApiResponse(responseCode = "400", description = "Invalid get call endpoint request.")
@ApiResponse(responseCode = "401", description = "Account authentication check failed.") @ApiResponse(responseCode = "401", description = "Account authentication check failed.")
@ApiResponse(responseCode = "422", description = "Invalid request format.") @ApiResponse(responseCode = "422", description = "Invalid request format.")
@ApiResponse(responseCode = "429", description = "Ratelimited.") @ApiResponse(responseCode = "429", description = "Rate limited.")
public TurnToken getCallingRelays( public TurnToken getCallingRelays(
final @ReadOnly @Auth AuthenticatedAccount auth, final @ReadOnly @Auth AuthenticatedAccount auth,
@Context ContainerRequestContext requestContext @Context ContainerRequestContext requestContext
@ -71,6 +74,10 @@ public class CallRoutingController {
UUID aci = auth.getAccount().getUuid(); UUID aci = auth.getAccount().getUuid();
rateLimiters.getCallEndpointLimiter().validate(aci); rateLimiters.getCallEndpointLimiter().validate(aci);
if (experimentEnrollmentManager.isEnrolled(aci, "cloudflareTurn")) {
return tokenGenerator.generateForCloudflareBeta();
}
Optional<InetAddress> address = Optional.empty(); Optional<InetAddress> address = Optional.empty();
try { try {
final String remoteAddress = (String) requestContext.getProperty( final String remoteAddress = (String) requestContext.getProperty(

View File

@ -1,22 +1,27 @@
package org.whispersystems.textsecuregcm.auth; 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.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; 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 { 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 @Test
public void testAlwaysSelectFirst() throws JsonProcessingException { public void testAlwaysSelectFirst() throws JsonProcessingException {
final String configString = """ final String configString = """
@ -30,7 +35,7 @@ public class TurnTokenGeneratorTest {
- uris: - uris:
- never.org - never.org
weight: 0 weight: 0
"""; """;
DynamicConfiguration config = DynamicConfigurationManager DynamicConfiguration config = DynamicConfigurationManager
.parseConfiguration(configString, DynamicConfiguration.class) .parseConfiguration(configString, DynamicConfiguration.class)
.orElseThrow(); .orElseThrow();
@ -42,7 +47,8 @@ public class TurnTokenGeneratorTest {
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
final TurnTokenGenerator turnTokenGenerator = 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; final long COUNT = 1000;
@ -83,7 +89,8 @@ public class TurnTokenGeneratorTest {
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
final TurnTokenGenerator turnTokenGenerator = 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; final long COUNT = 1000;
@ -126,7 +133,8 @@ public class TurnTokenGeneratorTest {
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config); when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
final TurnTokenGenerator turnTokenGenerator = 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")); TurnToken token = turnTokenGenerator.generate(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6"));
assertThat(token.urls().get(0)).isEqualTo("enrolled.org"); assertThat(token.urls().get(0)).isEqualTo("enrolled.org");

View File

@ -6,14 +6,13 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat; 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.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.common.net.HttpHeaders;
import io.dropwizard.auth.AuthValueFactoryProvider; import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension; import io.dropwizard.testing.junit5.ResourceExtension;
@ -24,6 +23,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; 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.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter; import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions; 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.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.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
@ -43,30 +46,44 @@ import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider;
@ExtendWith(DropwizardExtensionsSupport.class) @ExtendWith(DropwizardExtensionsSupport.class)
class CallRoutingControllerTest { class CallRoutingControllerTest {
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class);
private static final DynamicConfigurationManager<DynamicConfiguration> 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 GET_CALL_ENDPOINTS_PATH = "v1/calling/relays";
private static final String REMOTE_ADDRESS = "123.123.123.1"; private static final String REMOTE_ADDRESS = "123.123.123.1";
private static final ResourceExtension resources = ResourceExtension.builder() private static final RateLimiters rateLimiters = mock(RateLimiters.class);
.addProvider(AuthHelper.getAuthFilter()) private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class);
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class)) private static final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(
.addProvider(new RateLimitExceededExceptionMapper()) DynamicConfigurationManager.class);
.addProvider(new TestRemoteAddressFilterProvider(REMOTE_ADDRESS)) private static final ExperimentEnrollmentManager experimentEnrollmentManager = mock(
.setMapper(SystemMapper.jsonMapper()) ExperimentEnrollmentManager.class);
.setTestContainerFactory(new GrizzlyWebTestContainerFactory()) private static final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
.addResource(new CallRoutingController(rateLimiters, turnCallRouter, turnTokenGenerator)) "bloop".getBytes(StandardCharsets.UTF_8),
.build(); 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 @BeforeEach
void setup() { void setup() {
when(rateLimiters.getCallEndpointLimiter()).thenReturn(getCallEndpointLimiter); when(rateLimiters.getCallEndpointLimiter()).thenReturn(getCallEndpointLimiter);
} }
@AfterEach
void tearDown() {
reset(experimentEnrollmentManager, dynamicConfigurationManager, rateLimiters, getCallEndpointLimiter,
turnCallRouter);
}
@Test @Test
void testGetTurnEndpointsSuccess() throws UnknownHostException { void testGetTurnEndpointsSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions( 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 @Test
void testGetTurnEndpointsInvalidIpSuccess() throws UnknownHostException { void testGetTurnEndpointsInvalidIpSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions( TurnServerOptions options = new TurnServerOptions(

View File

@ -124,6 +124,8 @@ currentReportingKey.secret: AAAAAAAAAAA=
currentReportingKey.salt: AAAAAAAAAAA= currentReportingKey.salt: AAAAAAAAAAA=
turn.secret: AAAAAAAAAAA= turn.secret: AAAAAAAAAAA=
turn.cloudflare.username: ABCDEFGHIJKLM
turn.cloudflare.password: NOPQRSTUVWXYZ
linkDevice.secret: AAAAAAAAAAA= linkDevice.secret: AAAAAAAAAAA=

View File

@ -443,6 +443,11 @@ registrationService:
turn: turn:
secret: secret://turn.secret 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: linkDevice:
secret: secret://linkDevice.secret secret: secret://linkDevice.secret