diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java index 2bda8ef56..a2d7bf9fb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/CloudflareTurnCredentialsManager.java @@ -16,6 +16,7 @@ import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; @@ -126,8 +127,12 @@ public class CloudflareTurnCredentialsManager { final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper() .readValue(response.body(), CloudflareTurnResponse.class); - return new TurnToken(cloudflareTurnResponse.iceServers().username(), + return TurnTokenGenerator.from( + cloudflareTurnResponse.iceServers().username(), cloudflareTurnResponse.iceServers().credential(), - cloudflareTurnUrls, cloudflareTurnComposedUrls, cloudflareTurnHostname); + Optional.ofNullable(cloudflareTurnUrls), + Optional.ofNullable(cloudflareTurnComposedUrls), + cloudflareTurnHostname + ); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java index 738e0810d..98ddace09 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java @@ -5,9 +5,14 @@ package org.whispersystems.textsecuregcm.auth; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.List; -public record TurnToken(String username, String password, List urls, @Nullable List urlsWithIps, - @Nullable String hostname) { +public record TurnToken( + String username, + String password, + @Nonnull List urls, + @Nonnull List urlsWithIps, + @Nullable String hostname) { } 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 a004f429c..d757c9fde 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java @@ -11,19 +11,13 @@ import java.security.SecureRandom; import java.time.Duration; import java.time.Instant; import java.util.Base64; +import java.util.Collections; 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.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 { @@ -43,23 +37,51 @@ public class TurnTokenGenerator { return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname()); } - private TurnToken generateToken(String hostname, List urlsWithIps, List urlsWithHostname) { + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private TurnToken generateToken( + String hostname, + Optional> urlsWithIps, + Optional> urlsWithHostname + ) { try { final Mac mac = Mac.getInstance(ALGORITHM); final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond(); final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt()); final String userTime = validUntilSeconds + ":" + user; - final String protocol = urlsWithIps != null && !urlsWithIps.isEmpty() - ? WithIpsProtocol - : WithUrlsProtocol; + final String protocol = urlsWithIps.isEmpty() || urlsWithIps.get().isEmpty() + ? WithUrlsProtocol + : WithIpsProtocol; final String protocolUserTime = userTime + "#" + protocol; mac.init(new SecretKeySpec(turnSecret, ALGORITHM)); 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) { throw new AssertionError(e); } } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static TurnToken from( + String username, + String password, + Optional> urls, + Optional> urlsWithIps, + String hostname + ) { + return new TurnToken( + username, + password, + urls.orElse(Collections.emptyList()), + urlsWithIps.orElse(Collections.emptyList()), + hostname + ); + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java index 91a98c606..4367d755c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java @@ -77,7 +77,7 @@ public class TurnCallRouter { return getRoutingForInner(aci, clientAddress, instanceLimit); } catch(Exception 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 targetedUrls = this.configTurnRouter.targetedUrls(aci); 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) { - return new TurnServerOptions(hostname, null, this.configTurnRouter.randomUrls()); + return new TurnServerOptions(hostname, Optional.empty(), Optional.ofNullable(this.configTurnRouter.randomUrls())); } CityResponse geoInfo; @@ -128,7 +128,7 @@ public class TurnCallRouter { datacenters, 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 diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnServerOptions.java b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnServerOptions.java index a1381ea22..0003cc99e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnServerOptions.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnServerOptions.java @@ -6,6 +6,11 @@ package org.whispersystems.textsecuregcm.calls.routing; import java.util.List; +import java.util.Optional; -public record TurnServerOptions(String hostname, List urlsWithIps, List urlsWithHostname) { +public record TurnServerOptions( + String hostname, + Optional> urlsWithIps, + Optional> urlsWithHostname +) { } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java index 58d5676cb..0d4687347 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java @@ -127,8 +127,8 @@ public class TurnCallRouterTest { TurnServerOptions optionsWithUrls(List urls) { return new TurnServerOptions( TEST_HOSTNAME, - urls, - EXPECTED_TEST_URLS_WITH_HOSTS + Optional.of(urls), + 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)) .isEqualTo(new TurnServerOptions( TEST_HOSTNAME, - null, - targetedUrls + Optional.empty(), + Optional.of(targetedUrls) )); } @@ -157,8 +157,8 @@ public class TurnCallRouterTest { assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10)) .isEqualTo(new TurnServerOptions( TEST_HOSTNAME, - null, - TEST_URLS_WITH_HOSTS + Optional.empty(), + 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)) .isEqualTo(new TurnServerOptions( TEST_HOSTNAME, - null, - TEST_URLS_WITH_HOSTS + Optional.empty(), + Optional.of(TEST_URLS_WITH_HOSTS) )); } 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 e90db20b9..bc57929e6 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.List; import java.util.Optional; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; @@ -84,8 +85,8 @@ class CallRoutingControllerTest { void testGetTurnEndpointsSuccess() throws UnknownHostException { TurnServerOptions options = new TurnServerOptions( "example.domain.org", - List.of("stun:12.34.56.78"), - List.of("stun:example.domain.org") + Optional.of(List.of("stun:12.34.56.78")), + Optional.of(List.of("stun:example.domain.org")) ); when(turnCallRouter.getRoutingFor( @@ -104,8 +105,8 @@ class CallRoutingControllerTest { assertThat(token.username()).isNotEmpty(); assertThat(token.password()).isNotEmpty(); assertThat(token.hostname()).isEqualTo(options.hostname()); - assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); - assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); + assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get()); + assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get()); } } @@ -115,7 +116,7 @@ class CallRoutingControllerTest { .thenReturn(true); 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")); try (Response response = resources.getJerseyTest() @@ -129,7 +130,7 @@ class CallRoutingControllerTest { assertThat(token.username()).isEqualTo("ABC"); assertThat(token.password()).isEqualTo("XYZ"); 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")); } } @@ -138,8 +139,8 @@ class CallRoutingControllerTest { void testGetTurnEndpointsInvalidIpSuccess() throws UnknownHostException { TurnServerOptions options = new TurnServerOptions( "example.domain.org", - List.of(), - List.of("stun:example.domain.org") + Optional.of(List.of()), + Optional.of(List.of("stun:example.domain.org")) ); when(turnCallRouter.getRoutingFor( @@ -158,8 +159,8 @@ class CallRoutingControllerTest { assertThat(token.username()).isNotEmpty(); assertThat(token.password()).isNotEmpty(); assertThat(token.hostname()).isEqualTo(options.hostname()); - assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); - assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); + assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get()); + assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get()); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java index dc0e410a1..1936fc816 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java @@ -49,8 +49,8 @@ class CallRoutingControllerV2Test { private static final String REMOTE_ADDRESS = "123.123.123.1"; private static final TurnServerOptions TURN_SERVER_OPTIONS = new TurnServerOptions( "example.domain.org", - List.of("stun:12.34.56.78"), - List.of("stun:example.domain.org") + Optional.of(List.of("stun:12.34.56.78")), + Optional.of(List.of("stun:example.domain.org")) ); private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken( "ABC", @@ -129,8 +129,8 @@ class CallRoutingControllerV2Test { assertThat(relays.getFirst().username()).isNotEmpty(); assertThat(relays.getFirst().password()).isNotEmpty(); assertThat(relays.getFirst().hostname()).isEqualTo(options.hostname()); - assertThat(relays.getFirst().urlsWithIps()).isEqualTo(options.urlsWithIps()); - assertThat(relays.getFirst().urls()).isEqualTo(options.urlsWithHostname()); + assertThat(relays.getFirst().urlsWithIps()).isEqualTo(options.urlsWithIps().get()); + assertThat(relays.getFirst().urls()).isEqualTo(options.urlsWithHostname().get()); } } @@ -159,8 +159,8 @@ class CallRoutingControllerV2Test { assertThat(token.username()).isNotEmpty(); assertThat(token.password()).isNotEmpty(); assertThat(token.hostname()).isEqualTo(options.hostname()); - assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); - assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); + assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get()); + assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get()); } } @@ -168,8 +168,8 @@ class CallRoutingControllerV2Test { void testGetRelaysInvalidIpSuccess() throws UnknownHostException { TurnServerOptions options = new TurnServerOptions( "example.domain.org", - List.of(), - List.of("stun:example.domain.org") + Optional.of(List.of()), + Optional.of(List.of("stun:example.domain.org")) ); when(turnCallRouter.getRoutingFor( @@ -192,8 +192,8 @@ class CallRoutingControllerV2Test { assertThat(token.username()).isNotEmpty(); assertThat(token.password()).isNotEmpty(); assertThat(token.hostname()).isEqualTo(options.hostname()); - assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps()); - assertThat(token.urls()).isEqualTo(options.urlsWithHostname()); + assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get()); + assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get()); } }