Port CallRoutingControllerV2 to gRPC
This commit is contained in:
parent
ae9f43bb3c
commit
82e3c16fba
@ -152,6 +152,7 @@ import org.whispersystems.textsecuregcm.grpc.AttachmentsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.BackupsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.BackupsGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.CallQualitySurveyGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.CallingGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.ChallengeGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.CredentialsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.CredentialsGrpcService;
|
||||
@ -996,6 +997,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
final List<ServerServiceDefinition> authenticatedServices = Stream.of(
|
||||
new AccountsGrpcService(accountsManager, rateLimiters, usernameHashZkProofVerifier, registrationRecoveryPasswordsManager),
|
||||
new CallingGrpcService(cloudflareTurnCredentialsManager, rateLimiters),
|
||||
new CredentialsGrpcService(accountsManager, certificateGenerator, zkAuthOperations, callingGenericZkSecretParams, rateLimiters, Clock.systemUTC(), ExternalServiceDefinitions.createExternalServiceList(config, Clock.systemUTC())),
|
||||
new KeysGrpcService(accountsManager, keysManager, rateLimiters),
|
||||
new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, config.getBadges(), profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters),
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.signal.chat.calling.GetCallingRelaysRequest;
|
||||
import org.signal.chat.calling.GetCallingRelaysResponse;
|
||||
import org.signal.chat.calling.SimpleCallingGrpc;
|
||||
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
|
||||
public class CallingGrpcService extends SimpleCallingGrpc.CallingImplBase {
|
||||
|
||||
private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
|
||||
private final RateLimiters rateLimiters;
|
||||
|
||||
public CallingGrpcService(final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager,
|
||||
final RateLimiters rateLimiters) {
|
||||
|
||||
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetCallingRelaysResponse getCallingRelays(final GetCallingRelaysRequest request)
|
||||
throws RateLimitExceededException, IOException {
|
||||
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
rateLimiters.getCallEndpointLimiter().validate(authenticatedDevice.accountIdentifier());
|
||||
|
||||
final TurnToken turnToken =
|
||||
cloudflareTurnCredentialsManager.retrieveFromCloudflare(authenticatedDevice.accountIdentifier());
|
||||
|
||||
final GetCallingRelaysResponse.Relay.Builder relayBuilder = GetCallingRelaysResponse.Relay.newBuilder()
|
||||
.setUsername(turnToken.username())
|
||||
.setPassword(turnToken.password())
|
||||
.setCredentialTtlSeconds(turnToken.ttlSeconds());
|
||||
|
||||
if (!turnToken.urls().isEmpty()) {
|
||||
relayBuilder.setHostnameUrls(turnToken.urls().stream()
|
||||
.collect(GetCallingRelaysResponse.HostnameUrlList::newBuilder,
|
||||
GetCallingRelaysResponse.HostnameUrlList.Builder::addUrls,
|
||||
(a, b) -> a.mergeFrom(b.build())));
|
||||
}
|
||||
|
||||
if (!turnToken.urlsWithIps().isEmpty()) {
|
||||
relayBuilder.setIpUrls(turnToken.urlsWithIps().stream()
|
||||
.collect(() -> {
|
||||
final GetCallingRelaysResponse.IpUrlList.Builder builder =
|
||||
GetCallingRelaysResponse.IpUrlList.newBuilder();
|
||||
|
||||
if (StringUtils.isNotBlank(turnToken.hostname())) {
|
||||
builder.setHostname(turnToken.hostname());
|
||||
}
|
||||
|
||||
return builder;
|
||||
},
|
||||
GetCallingRelaysResponse.IpUrlList.Builder::addUrls,
|
||||
(a, b) -> a.mergeFrom(b.build())));
|
||||
}
|
||||
|
||||
return GetCallingRelaysResponse.newBuilder()
|
||||
.addRelays(relayBuilder.build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -9,23 +9,53 @@ option java_multiple_files = true;
|
||||
|
||||
package org.signal.chat.calling;
|
||||
|
||||
// Provides methods for getting credentials for one-on-one and group calls.
|
||||
// Provides methods for getting credentials and relay options for one-on-one
|
||||
// calls.
|
||||
service Calling {
|
||||
|
||||
// Generates and returns TURN credentials for the caller.
|
||||
rpc GetTurnCredentials(GetTurnCredentialsRequest) returns (GetTurnCredentialsResponse) {}
|
||||
// Retrieves TURN credentials and relay options for one-on-one calls.
|
||||
rpc GetCallingRelays(GetCallingRelaysRequest) returns (GetCallingRelaysResponse) {}
|
||||
}
|
||||
|
||||
message GetTurnCredentialsRequest {}
|
||||
message GetCallingRelaysRequest {}
|
||||
|
||||
message GetTurnCredentialsResponse {
|
||||
// A username that can be presented to authenticate with a TURN server.
|
||||
string username = 1;
|
||||
message GetCallingRelaysResponse {
|
||||
message HostnameUrlList {
|
||||
// A collection of hostname-based TURN, TURNS, or STUN URLs a client can use
|
||||
// to connect to a relay.
|
||||
repeated string urls = 1;
|
||||
}
|
||||
|
||||
// A password that can be presented to authenticate with a TURN server.
|
||||
string password = 2;
|
||||
message IpUrlList {
|
||||
// A collection of IP-based TURN, TURNS, or STUN URLs a client can use to
|
||||
// connect to a relay.
|
||||
repeated string urls = 1;
|
||||
|
||||
// A list of TURN (or TURNS or STUN) servers where the provided credentials
|
||||
// may be used.
|
||||
repeated string urls = 3;
|
||||
// A hostname clients must use to validate the relay's TLS certificate when
|
||||
// connecting via an IP-based TURNS URL. May not be specified if `urls`
|
||||
// contains no TURNS URLs.
|
||||
optional string hostname = 2;
|
||||
}
|
||||
|
||||
message Relay {
|
||||
// A username that can be presented to authenticate with a TURN server.
|
||||
string username = 1;
|
||||
|
||||
// A password that can be presented to authenticate with a TURN server.
|
||||
string password = 2;
|
||||
|
||||
// The duration, in seconds, after which the included username and password
|
||||
// will no longer be valid.
|
||||
uint64 credential_ttl_seconds = 3;
|
||||
|
||||
// A collection of hostname-based URLs clients may use to connect to this
|
||||
// relay.
|
||||
optional HostnameUrlList hostname_urls = 4;
|
||||
|
||||
// A collection of IP-based URLs clients may use to connect to this relay.
|
||||
optional IpUrlList ip_urls = 5;
|
||||
}
|
||||
|
||||
// A collection of calling relays a client may use for one-on-one calls.
|
||||
repeated Relay relays = 1;
|
||||
}
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.signal.chat.calling.CallingGrpc;
|
||||
import org.signal.chat.calling.GetCallingRelaysRequest;
|
||||
import org.signal.chat.calling.GetCallingRelaysResponse;
|
||||
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
|
||||
class CallingGrpcServiceTest extends SimpleBaseGrpcTest<CallingGrpcService, CallingGrpc.CallingBlockingStub> {
|
||||
|
||||
@Mock
|
||||
private CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
|
||||
|
||||
@Mock
|
||||
private RateLimiter rateLimiter;
|
||||
|
||||
@Override
|
||||
protected CallingGrpcService createServiceBeforeEachTest() {
|
||||
final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
when(rateLimiters.getCallEndpointLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
return new CallingGrpcService(cloudflareTurnCredentialsManager, rateLimiters);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCallingRelays() throws IOException {
|
||||
final String username = "username";
|
||||
final String password = "password";
|
||||
final long credentialTtlSeconds = 73;
|
||||
final String hostnameTurnUrl = "turn:example.com:443";
|
||||
final String ipTurnUrl = "turn:127.0.0.1:80";
|
||||
final String ipTurnsUrl = "turns:127.0.0.1:443";
|
||||
final String hostname = "example.com";
|
||||
|
||||
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare(any()))
|
||||
.thenReturn(new TurnToken(username,
|
||||
password,
|
||||
credentialTtlSeconds,
|
||||
List.of(hostnameTurnUrl),
|
||||
List.of(ipTurnUrl, ipTurnsUrl),
|
||||
hostname));
|
||||
|
||||
assertEquals(GetCallingRelaysResponse.newBuilder()
|
||||
.addRelays(GetCallingRelaysResponse.Relay.newBuilder()
|
||||
.setUsername(username)
|
||||
.setPassword(password)
|
||||
.setCredentialTtlSeconds(credentialTtlSeconds)
|
||||
.setHostnameUrls(GetCallingRelaysResponse.HostnameUrlList.newBuilder()
|
||||
.addUrls(hostnameTurnUrl)
|
||||
.build())
|
||||
.setIpUrls(GetCallingRelaysResponse.IpUrlList.newBuilder()
|
||||
.addUrls(ipTurnUrl)
|
||||
.addUrls(ipTurnsUrl)
|
||||
.setHostname(hostname)
|
||||
.build())
|
||||
.build())
|
||||
.build(),
|
||||
authenticatedServiceStub().getCallingRelays(GetCallingRelaysRequest.getDefaultInstance()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCallingRelaysIoException() throws IOException {
|
||||
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare(any()))
|
||||
.thenThrow(IOException.class);
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored,ThrowableNotThrown
|
||||
GrpcTestUtils.assertStatusException(Status.UNAVAILABLE,
|
||||
() -> authenticatedServiceStub().getCallingRelays(GetCallingRelaysRequest.getDefaultInstance()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCallingRelaysRateLimited() throws RateLimitExceededException {
|
||||
final Duration retryAfter = Duration.ofSeconds(19);
|
||||
|
||||
doThrow(new RateLimitExceededException(retryAfter))
|
||||
.when(rateLimiter).validate(any(UUID.class));
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
|
||||
() -> authenticatedServiceStub().getCallingRelays(GetCallingRelaysRequest.getDefaultInstance()));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user