mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 01:18:35 +01:00
Include a TURN credential TTL for clients in GetCallingRelaysResponse
This commit is contained in:
@@ -5,8 +5,11 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.created;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
|
||||
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;
|
||||
@@ -15,17 +18,19 @@ 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 io.netty.util.concurrent.GlobalEventExecutor;
|
||||
import io.netty.util.concurrent.SucceededFuture;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
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.apache.commons.lang3.RandomStringUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -35,31 +40,41 @@ import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||
|
||||
public class CloudflareTurnCredentialsManagerTest {
|
||||
@RegisterExtension
|
||||
private final WireMockExtension wireMock = WireMockExtension.newInstance()
|
||||
private static 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;
|
||||
private CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
|
||||
|
||||
private static final String GET_CREDENTIALS_PATH = "/v1/turn/keys/LMNOP/credentials/generate";
|
||||
private static final String TURN_HOSTNAME = "localhost";
|
||||
|
||||
private static final String API_TOKEN = RandomStringUtils.insecure().nextAlphanumeric(16);
|
||||
private static final String USERNAME = RandomStringUtils.insecure().nextAlphanumeric(16);
|
||||
private static final String CREDENTIAL = RandomStringUtils.insecure().nextAlphanumeric(16);
|
||||
private static final List<String> CLOUDFLARE_TURN_URLS = List.of("turn:cf.example.com");
|
||||
private static final Duration REQUESTED_CREDENTIAL_TTL = Duration.ofSeconds(100);
|
||||
private static final Duration CLIENT_CREDENTIAL_TTL = REQUESTED_CREDENTIAL_TTL.dividedBy(2);
|
||||
private static final List<String> IP_URL_PATTERNS = List.of("turn:%s", "turn:%s:80?transport=tcp", "turns:%s:443?transport=tcp");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws CertificateException {
|
||||
void setUp() {
|
||||
httpExecutor = Executors.newSingleThreadExecutor();
|
||||
retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
dnsResolver = mock(DnsNameResolver.class);
|
||||
dnsResult = mock(Future.class);
|
||||
|
||||
cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
||||
"API_TOKEN",
|
||||
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"),
|
||||
REQUESTED_CREDENTIAL_TTL,
|
||||
CLIENT_CREDENTIAL_TTL,
|
||||
CLOUDFLARE_TURN_URLS,
|
||||
IP_URL_PATTERNS,
|
||||
TURN_HOSTNAME,
|
||||
2,
|
||||
new CircuitBreakerConfiguration(),
|
||||
@@ -73,26 +88,61 @@ public class CloudflareTurnCredentialsManagerTest {
|
||||
@AfterEach
|
||||
void tearDown() throws InterruptedException {
|
||||
httpExecutor.shutdown();
|
||||
httpExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
retryExecutor.shutdown();
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
httpExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
retryExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess() throws IOException, CancellationException, ExecutionException, InterruptedException {
|
||||
public void testSuccess() throws IOException, CancellationException {
|
||||
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")));
|
||||
.willReturn(created()
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withBody("""
|
||||
{
|
||||
"iceServers": {
|
||||
"urls": [
|
||||
"turn:cloudflare.example.com:3478?transport=udp"
|
||||
],
|
||||
"username": "%s",
|
||||
"credential": "%s"
|
||||
}
|
||||
}
|
||||
""".formatted(USERNAME, CREDENTIAL))));
|
||||
|
||||
when(dnsResolver.resolveAll(TURN_HOSTNAME))
|
||||
.thenReturn(dnsResult);
|
||||
.thenReturn(new SucceededFuture<>(GlobalEventExecutor.INSTANCE,
|
||||
List.of(InetAddress.getByName("127.0.0.1"), InetAddress.getByName("::1"))));
|
||||
|
||||
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"));
|
||||
wireMock.verify(postRequestedFor(urlEqualTo(GET_CREDENTIALS_PATH))
|
||||
.withHeader("Content-Type", equalTo("application/json"))
|
||||
.withHeader("Authorization", equalTo("Bearer " + API_TOKEN))
|
||||
.withRequestBody(equalToJson("""
|
||||
{
|
||||
"ttl": %d
|
||||
}
|
||||
""".formatted(REQUESTED_CREDENTIAL_TTL.toSeconds()))));
|
||||
|
||||
assertThat(token.username()).isEqualTo(USERNAME);
|
||||
assertThat(token.password()).isEqualTo(CREDENTIAL);
|
||||
assertThat(token.hostname()).isEqualTo(TURN_HOSTNAME);
|
||||
assertThat(token.urls()).isEqualTo(CLOUDFLARE_TURN_URLS);
|
||||
assertThat(token.ttlSeconds()).isEqualTo(CLIENT_CREDENTIAL_TTL.toSeconds());
|
||||
|
||||
final List<String> expectedUrlsWithIps = new ArrayList<>();
|
||||
|
||||
for (final String ip : new String[] {"127.0.0.1", "[0:0:0:0:0:0:0:1]"}) {
|
||||
for (final String pattern : IP_URL_PATTERNS) {
|
||||
expectedUrlsWithIps.add(pattern.formatted(ip));
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(token.urlsWithIps()).containsExactlyElementsOf(expectedUrlsWithIps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,14 +40,15 @@ class CallRoutingControllerV2Test {
|
||||
private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken(
|
||||
"ABC",
|
||||
"XYZ",
|
||||
43_200,
|
||||
List.of("turn:cloudflare.example.com:3478?transport=udp"),
|
||||
null,
|
||||
"cf.example.com");
|
||||
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class);
|
||||
private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = mock(
|
||||
CloudflareTurnCredentialsManager.class);
|
||||
private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager =
|
||||
mock(CloudflareTurnCredentialsManager.class);
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
@@ -66,21 +67,14 @@ class CallRoutingControllerV2Test {
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
reset( rateLimiters, getCallEndpointLimiter);
|
||||
}
|
||||
|
||||
void initializeMocksWith(TurnToken cloudflareToken) {
|
||||
try {
|
||||
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(cloudflareToken);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
reset(rateLimiters, getCallEndpointLimiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRelaysBothRouting() {
|
||||
initializeMocksWith(CLOUDFLARE_TURN_TOKEN);
|
||||
void testGetRelaysBothRouting() throws IOException {
|
||||
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(CLOUDFLARE_TURN_TOKEN);
|
||||
|
||||
try (Response rawResponse = resources.getJerseyTest()
|
||||
try (final Response rawResponse = resources.getJerseyTest()
|
||||
.target(GET_CALL_RELAYS_PATH)
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
@@ -88,11 +82,8 @@ class CallRoutingControllerV2Test {
|
||||
|
||||
assertThat(rawResponse.getStatus()).isEqualTo(200);
|
||||
|
||||
CallRoutingControllerV2.GetCallingRelaysResponse response = rawResponse.readEntity(
|
||||
CallRoutingControllerV2.GetCallingRelaysResponse.class);
|
||||
|
||||
List<TurnToken> relays = response.relays();
|
||||
assertThat(relays).isEqualTo(List.of(CLOUDFLARE_TURN_TOKEN));
|
||||
assertThat(rawResponse.readEntity(GetCallingRelaysResponse.class).relays())
|
||||
.isEqualTo(List.of(CLOUDFLARE_TURN_TOKEN));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user