Return empty lists instead of null in GetCallingRelaysV2

This commit is contained in:
adel-signal 2025-01-24 14:33:45 -08:00 committed by GitHub
parent 7e616a4056
commit ae1e7fbaa0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 87 additions and 49 deletions

View File

@ -16,6 +16,7 @@ import java.net.URI;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -126,8 +127,12 @@ public class CloudflareTurnCredentialsManager {
final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper() final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper()
.readValue(response.body(), CloudflareTurnResponse.class); .readValue(response.body(), CloudflareTurnResponse.class);
return new TurnToken(cloudflareTurnResponse.iceServers().username(), return TurnTokenGenerator.from(
cloudflareTurnResponse.iceServers().username(),
cloudflareTurnResponse.iceServers().credential(), cloudflareTurnResponse.iceServers().credential(),
cloudflareTurnUrls, cloudflareTurnComposedUrls, cloudflareTurnHostname); Optional.ofNullable(cloudflareTurnUrls),
Optional.ofNullable(cloudflareTurnComposedUrls),
cloudflareTurnHostname
);
} }
} }

View File

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

View File

@ -11,19 +11,13 @@ import java.security.SecureRandom;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Base64; import java.util.Base64;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions; 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.Util;
import org.whispersystems.textsecuregcm.util.WeightedRandomSelect;
public class TurnTokenGenerator { public class TurnTokenGenerator {
@ -43,23 +37,51 @@ public class TurnTokenGenerator {
return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname()); return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname());
} }
private TurnToken generateToken(String hostname, List<String> urlsWithIps, List<String> urlsWithHostname) { @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private TurnToken generateToken(
String hostname,
Optional<List<String>> urlsWithIps,
Optional<List<String>> urlsWithHostname
) {
try { try {
final Mac mac = Mac.getInstance(ALGORITHM); final Mac mac = Mac.getInstance(ALGORITHM);
final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond(); final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond();
final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt()); final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt());
final String userTime = validUntilSeconds + ":" + user; final String userTime = validUntilSeconds + ":" + user;
final String protocol = urlsWithIps != null && !urlsWithIps.isEmpty() final String protocol = urlsWithIps.isEmpty() || urlsWithIps.get().isEmpty()
? WithIpsProtocol ? WithUrlsProtocol
: WithUrlsProtocol; : WithIpsProtocol;
final String protocolUserTime = userTime + "#" + protocol; final String protocolUserTime = userTime + "#" + protocol;
mac.init(new SecretKeySpec(turnSecret, ALGORITHM)); mac.init(new SecretKeySpec(turnSecret, ALGORITHM));
final String password = Base64.getEncoder().encodeToString(mac.doFinal(protocolUserTime.getBytes())); final String password = Base64.getEncoder().encodeToString(mac.doFinal(protocolUserTime.getBytes()));
return new TurnToken(protocolUserTime, password, urlsWithHostname, urlsWithIps, hostname); return from(
protocolUserTime,
password,
urlsWithHostname,
urlsWithIps,
hostname
);
} catch (final NoSuchAlgorithmException | InvalidKeyException e) { } catch (final NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public static TurnToken from(
String username,
String password,
Optional<List<String>> urls,
Optional<List<String>> urlsWithIps,
String hostname
) {
return new TurnToken(
username,
password,
urls.orElse(Collections.emptyList()),
urlsWithIps.orElse(Collections.emptyList()),
hostname
);
}
} }

View File

@ -77,7 +77,7 @@ public class TurnCallRouter {
return getRoutingForInner(aci, clientAddress, instanceLimit); return getRoutingForInner(aci, clientAddress, instanceLimit);
} catch(Exception e) { } catch(Exception e) {
logger.error("Failed to perform routing", e); logger.error("Failed to perform routing", e);
return new TurnServerOptions(this.configTurnRouter.getHostname(), null, this.configTurnRouter.randomUrls()); return new TurnServerOptions(this.configTurnRouter.getHostname(), null, Optional.of(this.configTurnRouter.randomUrls()));
} }
} }
@ -90,11 +90,11 @@ public class TurnCallRouter {
List<String> targetedUrls = this.configTurnRouter.targetedUrls(aci); List<String> targetedUrls = this.configTurnRouter.targetedUrls(aci);
if(!targetedUrls.isEmpty()) { if(!targetedUrls.isEmpty()) {
return new TurnServerOptions(hostname, null, targetedUrls); return new TurnServerOptions(hostname, Optional.empty(), Optional.ofNullable(targetedUrls));
} }
if(clientAddress.isEmpty() || this.configTurnRouter.shouldRandomize() || instanceLimit < 1) { if(clientAddress.isEmpty() || this.configTurnRouter.shouldRandomize() || instanceLimit < 1) {
return new TurnServerOptions(hostname, null, this.configTurnRouter.randomUrls()); return new TurnServerOptions(hostname, Optional.empty(), Optional.ofNullable(this.configTurnRouter.randomUrls()));
} }
CityResponse geoInfo; CityResponse geoInfo;
@ -128,7 +128,7 @@ public class TurnCallRouter {
datacenters, datacenters,
instanceLimit instanceLimit
)); ));
return new TurnServerOptions(hostname, urlsWithIps, minimalRandomUrls()); return new TurnServerOptions(hostname, Optional.of(urlsWithIps), Optional.of(minimalRandomUrls()));
} }
// Includes only the udp options in the randomUrls // Includes only the udp options in the randomUrls

View File

@ -6,6 +6,11 @@
package org.whispersystems.textsecuregcm.calls.routing; package org.whispersystems.textsecuregcm.calls.routing;
import java.util.List; import java.util.List;
import java.util.Optional;
public record TurnServerOptions(String hostname, List<String> urlsWithIps, List<String> urlsWithHostname) { public record TurnServerOptions(
String hostname,
Optional<List<String>> urlsWithIps,
Optional<List<String>> urlsWithHostname
) {
} }

View File

@ -127,8 +127,8 @@ public class TurnCallRouterTest {
TurnServerOptions optionsWithUrls(List<String> urls) { TurnServerOptions optionsWithUrls(List<String> urls) {
return new TurnServerOptions( return new TurnServerOptions(
TEST_HOSTNAME, TEST_HOSTNAME,
urls, Optional.of(urls),
EXPECTED_TEST_URLS_WITH_HOSTS Optional.of(EXPECTED_TEST_URLS_WITH_HOSTS)
); );
} }
@ -144,8 +144,8 @@ public class TurnCallRouterTest {
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10)) assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
.isEqualTo(new TurnServerOptions( .isEqualTo(new TurnServerOptions(
TEST_HOSTNAME, TEST_HOSTNAME,
null, Optional.empty(),
targetedUrls Optional.of(targetedUrls)
)); ));
} }
@ -157,8 +157,8 @@ public class TurnCallRouterTest {
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10)) assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
.isEqualTo(new TurnServerOptions( .isEqualTo(new TurnServerOptions(
TEST_HOSTNAME, TEST_HOSTNAME,
null, Optional.empty(),
TEST_URLS_WITH_HOSTS Optional.of(TEST_URLS_WITH_HOSTS)
)); ));
} }
@ -172,8 +172,8 @@ public class TurnCallRouterTest {
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 0)) assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 0))
.isEqualTo(new TurnServerOptions( .isEqualTo(new TurnServerOptions(
TEST_HOSTNAME, TEST_HOSTNAME,
null, Optional.empty(),
TEST_URLS_WITH_HOSTS Optional.of(TEST_URLS_WITH_HOSTS)
)); ));
} }

View File

@ -21,6 +21,7 @@ import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
@ -84,8 +85,8 @@ class CallRoutingControllerTest {
void testGetTurnEndpointsSuccess() throws UnknownHostException { void testGetTurnEndpointsSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions( TurnServerOptions options = new TurnServerOptions(
"example.domain.org", "example.domain.org",
List.of("stun:12.34.56.78"), Optional.of(List.of("stun:12.34.56.78")),
List.of("stun:example.domain.org") Optional.of(List.of("stun:example.domain.org"))
); );
when(turnCallRouter.getRoutingFor( when(turnCallRouter.getRoutingFor(
@ -104,8 +105,8 @@ class CallRoutingControllerTest {
assertThat(token.username()).isNotEmpty(); assertThat(token.username()).isNotEmpty();
assertThat(token.password()).isNotEmpty(); assertThat(token.password()).isNotEmpty();
assertThat(token.hostname()).isEqualTo(options.hostname()); assertThat(token.hostname()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
} }
} }
@ -115,7 +116,7 @@ class CallRoutingControllerTest {
.thenReturn(true); .thenReturn(true);
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(new TurnToken("ABC", "XYZ", when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(new TurnToken("ABC", "XYZ",
List.of("turn:cloudflare.example.com:3478?transport=udp"), null, List.of("turn:cloudflare.example.com:3478?transport=udp"), Collections.emptyList(),
"cf.example.com")); "cf.example.com"));
try (Response response = resources.getJerseyTest() try (Response response = resources.getJerseyTest()
@ -129,7 +130,7 @@ class CallRoutingControllerTest {
assertThat(token.username()).isEqualTo("ABC"); assertThat(token.username()).isEqualTo("ABC");
assertThat(token.password()).isEqualTo("XYZ"); assertThat(token.password()).isEqualTo("XYZ");
assertThat(token.hostname()).isEqualTo("cf.example.com"); assertThat(token.hostname()).isEqualTo("cf.example.com");
assertThat(token.urlsWithIps()).isNull(); assertThat(token.urlsWithIps()).isEmpty();
assertThat(token.urls()).isEqualTo(List.of("turn:cloudflare.example.com:3478?transport=udp")); assertThat(token.urls()).isEqualTo(List.of("turn:cloudflare.example.com:3478?transport=udp"));
} }
} }
@ -138,8 +139,8 @@ class CallRoutingControllerTest {
void testGetTurnEndpointsInvalidIpSuccess() throws UnknownHostException { void testGetTurnEndpointsInvalidIpSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions( TurnServerOptions options = new TurnServerOptions(
"example.domain.org", "example.domain.org",
List.of(), Optional.of(List.of()),
List.of("stun:example.domain.org") Optional.of(List.of("stun:example.domain.org"))
); );
when(turnCallRouter.getRoutingFor( when(turnCallRouter.getRoutingFor(
@ -158,8 +159,8 @@ class CallRoutingControllerTest {
assertThat(token.username()).isNotEmpty(); assertThat(token.username()).isNotEmpty();
assertThat(token.password()).isNotEmpty(); assertThat(token.password()).isNotEmpty();
assertThat(token.hostname()).isEqualTo(options.hostname()); assertThat(token.hostname()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
} }
} }

View File

@ -49,8 +49,8 @@ class CallRoutingControllerV2Test {
private static final String REMOTE_ADDRESS = "123.123.123.1"; private static final String REMOTE_ADDRESS = "123.123.123.1";
private static final TurnServerOptions TURN_SERVER_OPTIONS = new TurnServerOptions( private static final TurnServerOptions TURN_SERVER_OPTIONS = new TurnServerOptions(
"example.domain.org", "example.domain.org",
List.of("stun:12.34.56.78"), Optional.of(List.of("stun:12.34.56.78")),
List.of("stun:example.domain.org") Optional.of(List.of("stun:example.domain.org"))
); );
private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken( private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken(
"ABC", "ABC",
@ -129,8 +129,8 @@ class CallRoutingControllerV2Test {
assertThat(relays.getFirst().username()).isNotEmpty(); assertThat(relays.getFirst().username()).isNotEmpty();
assertThat(relays.getFirst().password()).isNotEmpty(); assertThat(relays.getFirst().password()).isNotEmpty();
assertThat(relays.getFirst().hostname()).isEqualTo(options.hostname()); assertThat(relays.getFirst().hostname()).isEqualTo(options.hostname());
assertThat(relays.getFirst().urlsWithIps()).isEqualTo(options.urlsWithIps()); assertThat(relays.getFirst().urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(relays.getFirst().urls()).isEqualTo(options.urlsWithHostname()); assertThat(relays.getFirst().urls()).isEqualTo(options.urlsWithHostname().get());
} }
} }
@ -159,8 +159,8 @@ class CallRoutingControllerV2Test {
assertThat(token.username()).isNotEmpty(); assertThat(token.username()).isNotEmpty();
assertThat(token.password()).isNotEmpty(); assertThat(token.password()).isNotEmpty();
assertThat(token.hostname()).isEqualTo(options.hostname()); assertThat(token.hostname()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
} }
} }
@ -168,8 +168,8 @@ class CallRoutingControllerV2Test {
void testGetRelaysInvalidIpSuccess() throws UnknownHostException { void testGetRelaysInvalidIpSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions( TurnServerOptions options = new TurnServerOptions(
"example.domain.org", "example.domain.org",
List.of(), Optional.of(List.of()),
List.of("stun:example.domain.org") Optional.of(List.of("stun:example.domain.org"))
); );
when(turnCallRouter.getRoutingFor( when(turnCallRouter.getRoutingFor(
@ -192,8 +192,8 @@ class CallRoutingControllerV2Test {
assertThat(token.username()).isNotEmpty(); assertThat(token.username()).isNotEmpty();
assertThat(token.password()).isNotEmpty(); assertThat(token.password()).isNotEmpty();
assertThat(token.hostname()).isEqualTo(options.hostname()); assertThat(token.hostname()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
} }
} }