Remove /v1/calling/relays API endpoint

This commit is contained in:
Adel Lahlou 2025-02-07 10:58:10 -08:00 committed by ravi-signal
parent 2dfd17af4a
commit 09ce79bd43
4 changed files with 1 additions and 287 deletions

View File

@ -110,7 +110,6 @@ import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
import org.whispersystems.textsecuregcm.controllers.ArchiveController; import org.whispersystems.textsecuregcm.controllers.ArchiveController;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4; import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
import org.whispersystems.textsecuregcm.controllers.CallLinkController; import org.whispersystems.textsecuregcm.controllers.CallLinkController;
import org.whispersystems.textsecuregcm.controllers.CallRoutingController;
import org.whispersystems.textsecuregcm.controllers.CallRoutingControllerV2; import org.whispersystems.textsecuregcm.controllers.CallRoutingControllerV2;
import org.whispersystems.textsecuregcm.controllers.CertificateController; import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.ChallengeController; import org.whispersystems.textsecuregcm.controllers.ChallengeController;
@ -1116,7 +1115,6 @@ 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, experimentEnrollmentManager, cloudflareTurnCredentialsManager),
new CallRoutingControllerV2(rateLimiters, callRouter, turnTokenGenerator, experimentEnrollmentManager, cloudflareTurnCredentialsManager), new CallRoutingControllerV2(rateLimiters, callRouter, turnTokenGenerator, experimentEnrollmentManager, cloudflareTurnCredentialsManager),
new CallLinkController(rateLimiters, callingGenericZkSecretParams), new CallLinkController(rateLimiters, callingGenericZkSecretParams),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),

View File

@ -1,103 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
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.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.websocket.auth.ReadOnly;
@Path("/v1/calling")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Calling")
public class CallRoutingController {
private static final int TURN_INSTANCE_LIMIT = 2;
private static final Counter INVALID_IP_COUNTER = Metrics.counter(name(CallRoutingController.class, "invalidIP"));
private static final Logger log = LoggerFactory.getLogger(CallRoutingController.class);
private final RateLimiters rateLimiters;
private final TurnCallRouter turnCallRouter;
private final TurnTokenGenerator tokenGenerator;
private final ExperimentEnrollmentManager experimentEnrollmentManager;
private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
public CallRoutingController(
final RateLimiters rateLimiters,
final TurnCallRouter turnCallRouter,
final TurnTokenGenerator tokenGenerator,
final ExperimentEnrollmentManager experimentEnrollmentManager,
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager
) {
this.rateLimiters = rateLimiters;
this.turnCallRouter = turnCallRouter;
this.tokenGenerator = tokenGenerator;
this.experimentEnrollmentManager = experimentEnrollmentManager;
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
}
@GET
@Path("/relays")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Get 1:1 calling relay options for the client",
description = """
Get 1:1 relay addresses in IpV4, Ipv6, and URL formats.
"""
)
@ApiResponse(responseCode = "200", description = "`JSON` with call endpoints.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "400", description = "Invalid get call endpoint request.")
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
@ApiResponse(responseCode = "422", description = "Invalid request format.")
@ApiResponse(responseCode = "429", description = "Rate limited.")
public TurnToken getCallingRelays(
final @ReadOnly @Auth AuthenticatedDevice auth,
@Context ContainerRequestContext requestContext
) throws RateLimitExceededException, IOException {
UUID aci = auth.getAccount().getUuid();
rateLimiters.getCallEndpointLimiter().validate(aci);
if (experimentEnrollmentManager.isEnrolled(auth.getAccount().getNumber(), aci, "cloudflareTurn")) {
return cloudflareTurnCredentialsManager.retrieveFromCloudflare();
}
Optional<InetAddress> address = Optional.empty();
try {
final String remoteAddress = (String) requestContext.getProperty(
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
address = Optional.of(InetAddress.getByName(remoteAddress));
} catch (UnknownHostException e) {
INVALID_IP_COUNTER.increment();
}
TurnServerOptions options = turnCallRouter.getRoutingFor(aci, address, TURN_INSTANCE_LIMIT);
return tokenGenerator.generateWithTurnServerOptions(options);
}
}

View File

@ -40,7 +40,7 @@ import org.whispersystems.websocket.auth.ReadOnly;
public class CallRoutingControllerV2 { public class CallRoutingControllerV2 {
private static final Counter INVALID_IP_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "invalidIP")); private static final Counter INVALID_IP_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "invalidIP"));
private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER = Metrics.counter(name(CallRoutingController.class, "cloudflareTurnError")); private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "cloudflareTurnError"));
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final TurnCallRouter turnCallRouter; private final TurnCallRouter turnCallRouter;
private final TurnTokenGenerator tokenGenerator; private final TurnTokenGenerator tokenGenerator;

View File

@ -1,181 +0,0 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import jakarta.ws.rs.core.Response;
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;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
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.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider;
@ExtendWith(DropwizardExtensionsSupport.class)
class CallRoutingControllerTest {
private static final String GET_CALL_ENDPOINTS_PATH = "v1/calling/relays";
private static final String REMOTE_ADDRESS = "123.123.123.1";
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class);
private static final ExperimentEnrollmentManager experimentEnrollmentManager = mock(
ExperimentEnrollmentManager.class);
private static final TurnTokenGenerator turnTokenGenerator =
new TurnTokenGenerator("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()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
.addProvider(new RateLimitExceededExceptionMapper())
.addProvider(new TestRemoteAddressFilterProvider(REMOTE_ADDRESS))
.setMapper(SystemMapper.jsonMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new CallRoutingController(rateLimiters, turnCallRouter, turnTokenGenerator,
experimentEnrollmentManager, cloudflareTurnCredentialsManager))
.build();
@BeforeEach
void setup() {
when(rateLimiters.getCallEndpointLimiter()).thenReturn(getCallEndpointLimiter);
}
@AfterEach
void tearDown() {
reset(experimentEnrollmentManager, rateLimiters, getCallEndpointLimiter, turnCallRouter);
}
@Test
void testGetTurnEndpointsSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions(
"example.domain.org",
Optional.of(List.of("stun:12.34.56.78")),
Optional.of(List.of("stun:example.domain.org"))
);
when(turnCallRouter.getRoutingFor(
eq(AuthHelper.VALID_UUID),
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
anyInt())
).thenReturn(options);
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()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
}
}
@Test
void testGetTurnEndpointsCloudflare() throws IOException {
when(experimentEnrollmentManager.isEnrolled(AuthHelper.VALID_NUMBER, AuthHelper.VALID_UUID, "cloudflareTurn"))
.thenReturn(true);
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(new TurnToken("ABC", "XYZ",
List.of("turn:cloudflare.example.com:3478?transport=udp"), Collections.emptyList(),
"cf.example.com"));
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()).isEqualTo("ABC");
assertThat(token.password()).isEqualTo("XYZ");
assertThat(token.hostname()).isEqualTo("cf.example.com");
assertThat(token.urlsWithIps()).isEmpty();
assertThat(token.urls()).isEqualTo(List.of("turn:cloudflare.example.com:3478?transport=udp"));
}
}
@Test
void testGetTurnEndpointsInvalidIpSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions(
"example.domain.org",
Optional.of(List.of()),
Optional.of(List.of("stun:example.domain.org"))
);
when(turnCallRouter.getRoutingFor(
eq(AuthHelper.VALID_UUID),
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
anyInt())
).thenReturn(options);
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()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
}
}
@Test
void testGetTurnEndpointRateLimited() throws RateLimitExceededException {
doThrow(new RateLimitExceededException(null))
.when(getCallEndpointLimiter).validate(AuthHelper.VALID_UUID);
try (final 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(429);
}
}
}