mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 11:18:03 +01:00
Retrieve Cloudflare Turn Credentials from Cloudflare
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
||||
import io.netty.resolver.dns.DnsNameResolver;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||
|
||||
public class CloudflareTurnCredentialsManagerTest {
|
||||
@RegisterExtension
|
||||
private final WireMockExtension wireMock = WireMockExtension.newInstance()
|
||||
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
|
||||
.build();
|
||||
|
||||
private static final String GET_CREDENTIALS_PATH = "/v1/turn/keys/LMNOP/credentials/generate";
|
||||
private static final String TURN_HOSTNAME = "localhost";
|
||||
private ExecutorService httpExecutor;
|
||||
private ScheduledExecutorService retryExecutor;
|
||||
private DnsNameResolver dnsResolver;
|
||||
private Future<List<InetAddress>> dnsResult;
|
||||
|
||||
private CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = null;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws CertificateException {
|
||||
httpExecutor = Executors.newSingleThreadExecutor();
|
||||
retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
dnsResolver = mock(DnsNameResolver.class);
|
||||
dnsResult = mock(Future.class);
|
||||
cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
||||
"API_TOKEN",
|
||||
"http://localhost:" + wireMock.getPort() + GET_CREDENTIALS_PATH,
|
||||
100,
|
||||
List.of("turn:cf.example.com"),
|
||||
List.of("turn:%s", "turn:%s:80?transport=tcp", "turns:%s:443?transport=tcp"),
|
||||
TURN_HOSTNAME,
|
||||
new CircuitBreakerConfiguration(),
|
||||
httpExecutor,
|
||||
new RetryConfiguration(),
|
||||
retryExecutor,
|
||||
dnsResolver
|
||||
);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws InterruptedException {
|
||||
httpExecutor.shutdown();
|
||||
httpExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
retryExecutor.shutdown();
|
||||
retryExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess() throws IOException, CancellationException, ExecutionException, InterruptedException {
|
||||
wireMock.stubFor(post(urlEqualTo(GET_CREDENTIALS_PATH))
|
||||
.willReturn(aResponse().withStatus(201).withHeader("Content-Type", new String[]{"application/json"}).withBody("{\"iceServers\":{\"urls\":[\"turn:cloudflare.example.com:3478?transport=udp\"],\"username\":\"ABC\",\"credential\":\"XYZ\"}}")));
|
||||
when(dnsResult.get())
|
||||
.thenReturn(List.of(InetAddress.getByName("127.0.0.1"), InetAddress.getByName("::1")));
|
||||
when(dnsResolver.resolveAll(TURN_HOSTNAME))
|
||||
.thenReturn(dnsResult);
|
||||
|
||||
TurnToken token = cloudflareTurnCredentialsManager.retrieveFromCloudflare();
|
||||
|
||||
assertThat(token.username()).isEqualTo("ABC");
|
||||
assertThat(token.password()).isEqualTo("XYZ");
|
||||
assertThat(token.hostname()).isEqualTo("localhost");
|
||||
assertThat(token.urlsWithIps()).containsAll(List.of("turn:127.0.0.1", "turn:127.0.0.1:80?transport=tcp", "turns:127.0.0.1:443?transport=tcp", "turn:[0:0:0:0:0:0:0:1]", "turn:[0:0:0:0:0:0:0:1]:80?transport=tcp", "turns:[0:0:0:0:0:0:0:1]:443?transport=tcp"));;
|
||||
assertThat(token.urls()).isEqualTo(List.of("turn:cf.example.com"));
|
||||
}
|
||||
}
|
||||
@@ -6,22 +6,15 @@ 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"), "cloudflare.example.com");
|
||||
|
||||
@Test
|
||||
public void testAlwaysSelectFirst() throws JsonProcessingException {
|
||||
final String configString = """
|
||||
@@ -47,8 +40,7 @@ public class TurnTokenGeneratorTest {
|
||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||
|
||||
final TurnTokenGenerator turnTokenGenerator =
|
||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8),
|
||||
CLOUDFLARE_TURN_CONFIGURATION);
|
||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final long COUNT = 1000;
|
||||
|
||||
@@ -88,9 +80,9 @@ public class TurnTokenGeneratorTest {
|
||||
DynamicConfigurationManager.class);
|
||||
|
||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||
|
||||
final TurnTokenGenerator turnTokenGenerator =
|
||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8),
|
||||
CLOUDFLARE_TURN_CONFIGURATION);
|
||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final long COUNT = 1000;
|
||||
|
||||
@@ -133,8 +125,7 @@ public class TurnTokenGeneratorTest {
|
||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||
|
||||
final TurnTokenGenerator turnTokenGenerator =
|
||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8),
|
||||
CLOUDFLARE_TURN_CONFIGURATION);
|
||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
TurnToken token = turnTokenGenerator.generate(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6"));
|
||||
assertThat(token.urls().get(0)).isEqualTo("enrolled.org");
|
||||
|
||||
@@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -28,13 +29,12 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
||||
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;
|
||||
@@ -57,9 +57,10 @@ class CallRoutingControllerTest {
|
||||
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"), "cf.example.com"));
|
||||
"bloop".getBytes(StandardCharsets.UTF_8));
|
||||
private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = mock(
|
||||
CloudflareTurnCredentialsManager.class);
|
||||
|
||||
private static final TurnCallRouter turnCallRouter = mock(TurnCallRouter.class);
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
@@ -70,7 +71,7 @@ class CallRoutingControllerTest {
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new CallRoutingController(rateLimiters, turnCallRouter, turnTokenGenerator,
|
||||
experimentEnrollmentManager))
|
||||
experimentEnrollmentManager, cloudflareTurnCredentialsManager))
|
||||
.build();
|
||||
|
||||
@BeforeEach
|
||||
@@ -97,7 +98,7 @@ class CallRoutingControllerTest {
|
||||
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
|
||||
anyInt())
|
||||
).thenReturn(options);
|
||||
try(Response response = resources.getJerseyTest()
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target(GET_CALL_ENDPOINTS_PATH)
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
@@ -114,10 +115,14 @@ class CallRoutingControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTurnEndpointsCloudflare() {
|
||||
void testGetTurnEndpointsCloudflare() throws IOException {
|
||||
when(experimentEnrollmentManager.isEnrolled(AuthHelper.VALID_UUID, "cloudflareTurn"))
|
||||
.thenReturn(true);
|
||||
|
||||
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(new TurnToken("ABC", "XYZ",
|
||||
List.of("turn:cloudflare.example.com:3478?transport=udp"), null,
|
||||
"cf.example.com"));
|
||||
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target(GET_CALL_ENDPOINTS_PATH)
|
||||
.request()
|
||||
@@ -126,11 +131,11 @@ class CallRoutingControllerTest {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
TurnToken token = response.readEntity(TurnToken.class);
|
||||
assertThat(token.username()).isNotEmpty();
|
||||
assertThat(token.password()).isNotEmpty();
|
||||
assertThat(token.hostname()).isNotEmpty();
|
||||
assertThat(token.username()).isEqualTo("ABC");
|
||||
assertThat(token.password()).isEqualTo("XYZ");
|
||||
assertThat(token.hostname()).isEqualTo("cf.example.com");
|
||||
assertThat(token.urlsWithIps()).isNull();
|
||||
assertThat(token.urls()).isEqualTo(List.of("turn:cf.example.com"));
|
||||
assertThat(token.urls()).isEqualTo(List.of("turn:cloudflare.example.com:3478?transport=udp"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +152,7 @@ class CallRoutingControllerTest {
|
||||
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
|
||||
anyInt())
|
||||
).thenReturn(options);
|
||||
try(Response response = resources.getJerseyTest()
|
||||
try (Response response = resources.getJerseyTest()
|
||||
.target(GET_CALL_ENDPOINTS_PATH)
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
@@ -168,7 +173,7 @@ class CallRoutingControllerTest {
|
||||
doThrow(new RateLimitExceededException(null, false))
|
||||
.when(getCallEndpointLimiter).validate(AuthHelper.VALID_UUID);
|
||||
|
||||
try(final Response response = resources.getJerseyTest()
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target(GET_CALL_ENDPOINTS_PATH)
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
|
||||
Reference in New Issue
Block a user