Retrieve Cloudflare Turn Credentials from Cloudflare
This commit is contained in:
parent
01743e5c88
commit
ffb81e4ff7
|
@ -91,8 +91,7 @@ currentReportingKey.secret: AAAAAAAAAAA=
|
||||||
currentReportingKey.salt: AAAAAAAAAAA=
|
currentReportingKey.salt: AAAAAAAAAAA=
|
||||||
|
|
||||||
turn.secret: AAAAAAAAAAA=
|
turn.secret: AAAAAAAAAAA=
|
||||||
turn.cloudflare.username: ABCDEFGHIJKLM
|
turn.cloudflare.apiToken: ABCDEFGHIJKLM
|
||||||
turn.cloudflare.password: NOPQRSTUVWXYZ
|
|
||||||
|
|
||||||
linkDevice.secret: AAAAAAAAAAA=
|
linkDevice.secret: AAAAAAAAAAA=
|
||||||
|
|
||||||
|
|
|
@ -429,10 +429,15 @@ registrationService:
|
||||||
turn:
|
turn:
|
||||||
secret: secret://turn.secret
|
secret: secret://turn.secret
|
||||||
cloudflare:
|
cloudflare:
|
||||||
username: secret://turn.cloudflare.username
|
apiToken: secret://turn.cloudflare.apiToken
|
||||||
password: secret://turn.cloudflare.password
|
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
||||||
urls:
|
urls:
|
||||||
- turns:turn.cloudflare.example.com:443?transport=tcp
|
- turn:turn.example.com:80
|
||||||
|
urlsWithIps:
|
||||||
|
- turn:%s
|
||||||
|
- turn:%s:80?transport=tcp
|
||||||
|
- turns:%s:443?transport=tcp
|
||||||
|
ttl: 86400
|
||||||
hostname: turn.cloudflare.example.com
|
hostname: turn.cloudflare.example.com
|
||||||
|
|
||||||
linkDevice:
|
linkDevice:
|
||||||
|
|
|
@ -28,6 +28,11 @@ import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor;
|
import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor;
|
||||||
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
||||||
import io.netty.channel.local.LocalAddress;
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.resolver.ResolvedAddressTypes;
|
||||||
|
import io.netty.resolver.dns.DnsNameResolver;
|
||||||
|
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
|
@ -73,6 +78,7 @@ import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
||||||
|
@ -539,9 +545,24 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
.workQueue(new SynchronousQueue<>())
|
.workQueue(new SynchronousQueue<>())
|
||||||
.keepAliveTime(io.dropwizard.util.Duration.seconds(60L))
|
.keepAliveTime(io.dropwizard.util.Duration.seconds(60L))
|
||||||
.build();
|
.build();
|
||||||
|
ExecutorService cloudflareTurnHttpExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "cloudflareTurn-%d"))
|
||||||
|
.maxThreads(2)
|
||||||
|
.minThreads(2)
|
||||||
|
.build();
|
||||||
|
|
||||||
ScheduledExecutorService subscriptionProcessorRetryExecutor = environment.lifecycle()
|
ScheduledExecutorService subscriptionProcessorRetryExecutor = environment.lifecycle()
|
||||||
.scheduledExecutorService(name(getClass(), "subscriptionProcessorRetry-%d")).threads(1).build();
|
.scheduledExecutorService(name(getClass(), "subscriptionProcessorRetry-%d")).threads(1).build();
|
||||||
|
ScheduledExecutorService cloudflareTurnRetryExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "cloudflareTurnRetry-%d")).threads(1).build();
|
||||||
|
|
||||||
|
final ManagedNioEventLoopGroup dnsResolutionEventLoopGroup = new ManagedNioEventLoopGroup();
|
||||||
|
final DnsNameResolver cloudflareDnsResolver = new DnsNameResolverBuilder(dnsResolutionEventLoopGroup.next())
|
||||||
|
.resolvedAddressTypes(ResolvedAddressTypes.IPV6_PREFERRED)
|
||||||
|
.completeOncePreferredResolved(false)
|
||||||
|
.channelType(NioDatagramChannel.class)
|
||||||
|
.socketChannelType(NioSocketChannel.class)
|
||||||
|
.build();
|
||||||
|
|
||||||
ExternalServiceCredentialsGenerator directoryV2CredentialsGenerator = DirectoryV2Controller.credentialsGenerator(
|
ExternalServiceCredentialsGenerator directoryV2CredentialsGenerator = DirectoryV2Controller.credentialsGenerator(
|
||||||
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration());
|
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration());
|
||||||
|
@ -635,7 +656,20 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
pushLatencyManager);
|
pushLatencyManager);
|
||||||
final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
||||||
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
|
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
|
||||||
config.getTurnConfiguration().secret().value(), config.getTurnConfiguration().cloudflare());
|
config.getTurnConfiguration().secret().value());
|
||||||
|
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
||||||
|
config.getTurnConfiguration().cloudflare().apiToken().value(),
|
||||||
|
config.getTurnConfiguration().cloudflare().endpoint(),
|
||||||
|
config.getTurnConfiguration().cloudflare().ttl(),
|
||||||
|
config.getTurnConfiguration().cloudflare().urls(),
|
||||||
|
config.getTurnConfiguration().cloudflare().urlsWithIps(),
|
||||||
|
config.getTurnConfiguration().cloudflare().hostname(),
|
||||||
|
config.getTurnConfiguration().cloudflare().circuitBreaker(),
|
||||||
|
cloudflareTurnHttpExecutor,
|
||||||
|
config.getTurnConfiguration().cloudflare().retry(),
|
||||||
|
cloudflareTurnRetryExecutor,
|
||||||
|
cloudflareDnsResolver
|
||||||
|
);
|
||||||
|
|
||||||
final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator(
|
final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator(
|
||||||
rateLimitersCluster,
|
rateLimitersCluster,
|
||||||
|
@ -887,6 +921,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getNoiseWebSocketTunnelConfiguration().recognizedProxySecret().value());
|
config.getNoiseWebSocketTunnelConfiguration().recognizedProxySecret().value());
|
||||||
|
|
||||||
environment.lifecycle().manage(localEventLoopGroup);
|
environment.lifecycle().manage(localEventLoopGroup);
|
||||||
|
environment.lifecycle().manage(dnsResolutionEventLoopGroup);
|
||||||
environment.lifecycle().manage(anonymousGrpcServer);
|
environment.lifecycle().manage(anonymousGrpcServer);
|
||||||
environment.lifecycle().manage(authenticatedGrpcServer);
|
environment.lifecycle().manage(authenticatedGrpcServer);
|
||||||
environment.lifecycle().manage(noiseWebSocketEventLoopGroup);
|
environment.lifecycle().manage(noiseWebSocketEventLoopGroup);
|
||||||
|
@ -1018,7 +1053,7 @@ 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),
|
new CallRoutingController(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(),
|
||||||
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),
|
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import io.netty.resolver.dns.DnsNameResolver;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Inet6Address;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
|
public class CloudflareTurnCredentialsManager {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CloudflareTurnCredentialsManager.class);
|
||||||
|
|
||||||
|
private final List<String> cloudflareTurnUrls;
|
||||||
|
private final List<String> cloudflareTurnUrlsWithIps;
|
||||||
|
private final String cloudflareTurnHostname;
|
||||||
|
private final HttpRequest request;
|
||||||
|
|
||||||
|
private final FaultTolerantHttpClient cloudflareTurnClient;
|
||||||
|
private final DnsNameResolver dnsNameResolver;
|
||||||
|
|
||||||
|
record CredentialRequest(long ttl) {}
|
||||||
|
|
||||||
|
record CloudflareTurnResponse(IceServer iceServers) {
|
||||||
|
|
||||||
|
record IceServer(
|
||||||
|
String username,
|
||||||
|
String credential,
|
||||||
|
List<String> urls) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CloudflareTurnCredentialsManager(final String cloudflareTurnApiToken,
|
||||||
|
final String cloudflareTurnEndpoint, final long cloudflareTurnTtl, final List<String> cloudflareTurnUrls,
|
||||||
|
final List<String> cloudflareTurnUrlsWithIps, final String cloudflareTurnHostname,
|
||||||
|
final CircuitBreakerConfiguration circuitBreaker, final ExecutorService executor, final RetryConfiguration retry,
|
||||||
|
final ScheduledExecutorService retryExecutor, final DnsNameResolver dnsNameResolver) {
|
||||||
|
|
||||||
|
this.cloudflareTurnClient = FaultTolerantHttpClient.newBuilder()
|
||||||
|
.withName("cloudflare-turn")
|
||||||
|
.withCircuitBreaker(circuitBreaker)
|
||||||
|
.withExecutor(executor)
|
||||||
|
.withRetry(retry)
|
||||||
|
.withRetryExecutor(retryExecutor)
|
||||||
|
.build();
|
||||||
|
this.cloudflareTurnUrls = cloudflareTurnUrls;
|
||||||
|
this.cloudflareTurnUrlsWithIps = cloudflareTurnUrlsWithIps;
|
||||||
|
this.cloudflareTurnHostname = cloudflareTurnHostname;
|
||||||
|
this.dnsNameResolver = dnsNameResolver;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final String body = SystemMapper.jsonMapper().writeValueAsString(new CredentialRequest(cloudflareTurnTtl));
|
||||||
|
this.request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(cloudflareTurnEndpoint))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", String.format("Bearer %s", cloudflareTurnApiToken))
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||||
|
.build();
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TurnToken retrieveFromCloudflare() throws IOException {
|
||||||
|
final List<String> cloudflareTurnComposedUrls;
|
||||||
|
try {
|
||||||
|
cloudflareTurnComposedUrls = dnsNameResolver.resolveAll(cloudflareTurnHostname).get().stream()
|
||||||
|
.map(i -> switch (i) {
|
||||||
|
case Inet6Address i6 -> "[" + i6.getHostAddress() + "]";
|
||||||
|
default -> i.getHostAddress();
|
||||||
|
})
|
||||||
|
.flatMap(i -> cloudflareTurnUrlsWithIps.stream().map(u -> u.formatted(i)))
|
||||||
|
.toList();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final HttpResponse<String> response;
|
||||||
|
try {
|
||||||
|
response = cloudflareTurnClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join();
|
||||||
|
} catch (CompletionException e) {
|
||||||
|
logger.warn("failed to make http request to Cloudflare Turn: {}", e.getMessage());
|
||||||
|
throw new IOException(ExceptionUtils.unwrap(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode() != Response.Status.CREATED.getStatusCode()) {
|
||||||
|
logger.warn("failure request credentials from Cloudflare Turn (code={}): {}", response.statusCode(), response);
|
||||||
|
throw new IOException("Cloudflare Turn http failure : " + response.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper()
|
||||||
|
.readValue(response.body(), CloudflareTurnResponse.class);
|
||||||
|
|
||||||
|
return new TurnToken(cloudflareTurnResponse.iceServers().username(),
|
||||||
|
cloudflareTurnResponse.iceServers().credential(),
|
||||||
|
cloudflareTurnUrls, cloudflareTurnComposedUrls, cloudflareTurnHostname);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ 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.CloudflareTurnConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration;
|
||||||
|
@ -38,21 +37,10 @@ public class TurnTokenGenerator {
|
||||||
|
|
||||||
private static final String WithIpsProtocol = "01";
|
private static final String WithIpsProtocol = "01";
|
||||||
|
|
||||||
private final String cloudflareTurnUsername;
|
|
||||||
private final String cloudflareTurnPassword;
|
|
||||||
private final List<String> cloudflareTurnUrls;
|
|
||||||
private final String cloudflareTurnHostname;
|
|
||||||
|
|
||||||
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||||
final byte[] turnSecret, final CloudflareTurnConfiguration cloudflareTurnConfiguration) {
|
final byte[] turnSecret) {
|
||||||
|
|
||||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
this.turnSecret = turnSecret;
|
this.turnSecret = turnSecret;
|
||||||
|
|
||||||
this.cloudflareTurnUsername = cloudflareTurnConfiguration.username().value();
|
|
||||||
this.cloudflareTurnPassword = cloudflareTurnConfiguration.password().value();
|
|
||||||
this.cloudflareTurnUrls = cloudflareTurnConfiguration.urls();
|
|
||||||
this.cloudflareTurnHostname = cloudflareTurnConfiguration.hostname();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
|
@ -64,10 +52,6 @@ public class TurnTokenGenerator {
|
||||||
return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname());
|
return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname());
|
||||||
}
|
}
|
||||||
|
|
||||||
public TurnToken generateForCloudflareBeta() {
|
|
||||||
return new TurnToken(cloudflareTurnUsername, cloudflareTurnPassword, cloudflareTurnUrls, null, cloudflareTurnHostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TurnToken generateToken(String hostname, List<String> urlsWithIps, List<String> urlsWithHostname) {
|
private TurnToken generateToken(String hostname, List<String> urlsWithIps, List<String> urlsWithHostname) {
|
||||||
try {
|
try {
|
||||||
final Mac mac = Mac.getInstance(ALGORITHM);
|
final Mac mac = Mac.getInstance(ALGORITHM);
|
||||||
|
|
|
@ -11,7 +11,25 @@ import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
public record CloudflareTurnConfiguration(@NotNull SecretString username, @NotNull SecretString password,
|
public record CloudflareTurnConfiguration(@NotNull SecretString apiToken,
|
||||||
@Valid @NotNull List<@NotBlank String> urls, @NotBlank String hostname) {
|
@NotBlank String endpoint,
|
||||||
|
@NotBlank long ttl,
|
||||||
|
@NotBlank List<String> urls,
|
||||||
|
@NotBlank List<String> urlsWithIps,
|
||||||
|
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
|
||||||
|
@NotNull @Valid RetryConfiguration retry,
|
||||||
|
@NotBlank String hostname) {
|
||||||
|
|
||||||
|
public CloudflareTurnConfiguration {
|
||||||
|
if (circuitBreaker == null) {
|
||||||
|
// It’s a little counter-intuitive, but this compact constructor allows a default value
|
||||||
|
// to be used when one isn’t specified (e.g. in YAML), allowing the field to still be
|
||||||
|
// validated as @NotNull
|
||||||
|
circuitBreaker = new CircuitBreakerConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retry == null) {
|
||||||
|
retry = new RetryConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import io.micrometer.core.instrument.Counter;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -20,6 +21,7 @@ import javax.ws.rs.core.MediaType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||||
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
|
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
|
||||||
|
@ -40,17 +42,20 @@ public class CallRoutingController {
|
||||||
private final TurnCallRouter turnCallRouter;
|
private final TurnCallRouter turnCallRouter;
|
||||||
private final TurnTokenGenerator tokenGenerator;
|
private final TurnTokenGenerator tokenGenerator;
|
||||||
private final ExperimentEnrollmentManager experimentEnrollmentManager;
|
private final ExperimentEnrollmentManager experimentEnrollmentManager;
|
||||||
|
private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
|
||||||
|
|
||||||
public CallRoutingController(
|
public CallRoutingController(
|
||||||
final RateLimiters rateLimiters,
|
final RateLimiters rateLimiters,
|
||||||
final TurnCallRouter turnCallRouter,
|
final TurnCallRouter turnCallRouter,
|
||||||
final TurnTokenGenerator tokenGenerator,
|
final TurnTokenGenerator tokenGenerator,
|
||||||
final ExperimentEnrollmentManager experimentEnrollmentManager
|
final ExperimentEnrollmentManager experimentEnrollmentManager,
|
||||||
|
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager
|
||||||
) {
|
) {
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.turnCallRouter = turnCallRouter;
|
this.turnCallRouter = turnCallRouter;
|
||||||
this.tokenGenerator = tokenGenerator;
|
this.tokenGenerator = tokenGenerator;
|
||||||
this.experimentEnrollmentManager = experimentEnrollmentManager;
|
this.experimentEnrollmentManager = experimentEnrollmentManager;
|
||||||
|
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -70,12 +75,12 @@ public class CallRoutingController {
|
||||||
public TurnToken getCallingRelays(
|
public TurnToken getCallingRelays(
|
||||||
final @ReadOnly @Auth AuthenticatedAccount auth,
|
final @ReadOnly @Auth AuthenticatedAccount auth,
|
||||||
@Context ContainerRequestContext requestContext
|
@Context ContainerRequestContext requestContext
|
||||||
) throws RateLimitExceededException {
|
) throws RateLimitExceededException, IOException {
|
||||||
UUID aci = auth.getAccount().getUuid();
|
UUID aci = auth.getAccount().getUuid();
|
||||||
rateLimiters.getCallEndpointLimiter().validate(aci);
|
rateLimiters.getCallEndpointLimiter().validate(aci);
|
||||||
|
|
||||||
if (experimentEnrollmentManager.isEnrolled(aci, "cloudflareTurn")) {
|
if (experimentEnrollmentManager.isEnrolled(aci, "cloudflareTurn")) {
|
||||||
return tokenGenerator.generateForCloudflareBeta();
|
return cloudflareTurnCredentialsManager.retrieveFromCloudflare();
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<InetAddress> address = Optional.empty();
|
Optional<InetAddress> address = Optional.empty();
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||||
|
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;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
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 java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
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.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||||
|
|
||||||
|
public class CloudflareTurnCredentialsManagerTest {
|
||||||
|
@RegisterExtension
|
||||||
|
private 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;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws CertificateException {
|
||||||
|
httpExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
dnsResolver = mock(DnsNameResolver.class);
|
||||||
|
dnsResult = mock(Future.class);
|
||||||
|
cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
||||||
|
"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"),
|
||||||
|
TURN_HOSTNAME,
|
||||||
|
new CircuitBreakerConfiguration(),
|
||||||
|
httpExecutor,
|
||||||
|
new RetryConfiguration(),
|
||||||
|
retryExecutor,
|
||||||
|
dnsResolver
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws InterruptedException {
|
||||||
|
httpExecutor.shutdown();
|
||||||
|
httpExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||||
|
retryExecutor.shutdown();
|
||||||
|
retryExecutor.awaitTermination(1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess() throws IOException, CancellationException, ExecutionException, InterruptedException {
|
||||||
|
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")));
|
||||||
|
when(dnsResolver.resolveAll(TURN_HOSTNAME))
|
||||||
|
.thenReturn(dnsResult);
|
||||||
|
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,22 +6,15 @@ import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.whispersystems.textsecuregcm.configuration.CloudflareTurnConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
|
||||||
public class TurnTokenGeneratorTest {
|
public class TurnTokenGeneratorTest {
|
||||||
|
|
||||||
private static final CloudflareTurnConfiguration CLOUDFLARE_TURN_CONFIGURATION = new CloudflareTurnConfiguration(
|
|
||||||
new SecretString("cf_username"), new SecretString("cf_password"), List.of("turn:cloudflare.example.com"), "cloudflare.example.com");
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAlwaysSelectFirst() throws JsonProcessingException {
|
public void testAlwaysSelectFirst() throws JsonProcessingException {
|
||||||
final String configString = """
|
final String configString = """
|
||||||
|
@ -47,8 +40,7 @@ public class TurnTokenGeneratorTest {
|
||||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||||
|
|
||||||
final TurnTokenGenerator turnTokenGenerator =
|
final TurnTokenGenerator turnTokenGenerator =
|
||||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8),
|
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8));
|
||||||
CLOUDFLARE_TURN_CONFIGURATION);
|
|
||||||
|
|
||||||
final long COUNT = 1000;
|
final long COUNT = 1000;
|
||||||
|
|
||||||
|
@ -88,9 +80,9 @@ public class TurnTokenGeneratorTest {
|
||||||
DynamicConfigurationManager.class);
|
DynamicConfigurationManager.class);
|
||||||
|
|
||||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||||
|
|
||||||
final TurnTokenGenerator turnTokenGenerator =
|
final TurnTokenGenerator turnTokenGenerator =
|
||||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8),
|
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8));
|
||||||
CLOUDFLARE_TURN_CONFIGURATION);
|
|
||||||
|
|
||||||
final long COUNT = 1000;
|
final long COUNT = 1000;
|
||||||
|
|
||||||
|
@ -133,8 +125,7 @@ public class TurnTokenGeneratorTest {
|
||||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||||
|
|
||||||
final TurnTokenGenerator turnTokenGenerator =
|
final TurnTokenGenerator turnTokenGenerator =
|
||||||
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8),
|
new TurnTokenGenerator(mockDynamicConfigManager, "bloop".getBytes(StandardCharsets.UTF_8));
|
||||||
CLOUDFLARE_TURN_CONFIGURATION);
|
|
||||||
|
|
||||||
TurnToken token = turnTokenGenerator.generate(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6"));
|
TurnToken token = turnTokenGenerator.generate(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6"));
|
||||||
assertThat(token.urls().get(0)).isEqualTo("enrolled.org");
|
assertThat(token.urls().get(0)).isEqualTo("enrolled.org");
|
||||||
|
|
|
@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
|
||||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||||
|
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;
|
||||||
|
@ -28,13 +29,12 @@ import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||||
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
|
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
|
||||||
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
|
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
|
||||||
import org.whispersystems.textsecuregcm.configuration.CloudflareTurnConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
|
||||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
@ -57,9 +57,10 @@ class CallRoutingControllerTest {
|
||||||
private static final ExperimentEnrollmentManager experimentEnrollmentManager = mock(
|
private static final ExperimentEnrollmentManager experimentEnrollmentManager = mock(
|
||||||
ExperimentEnrollmentManager.class);
|
ExperimentEnrollmentManager.class);
|
||||||
private static final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
|
private static final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
|
||||||
"bloop".getBytes(StandardCharsets.UTF_8),
|
"bloop".getBytes(StandardCharsets.UTF_8));
|
||||||
new CloudflareTurnConfiguration(new SecretString("cf_username"), new SecretString("cf_password"),
|
private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = mock(
|
||||||
List.of("turn:cf.example.com"), "cf.example.com"));
|
CloudflareTurnCredentialsManager.class);
|
||||||
|
|
||||||
private static final TurnCallRouter turnCallRouter = mock(TurnCallRouter.class);
|
private static final TurnCallRouter turnCallRouter = mock(TurnCallRouter.class);
|
||||||
|
|
||||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||||
|
@ -70,7 +71,7 @@ class CallRoutingControllerTest {
|
||||||
.setMapper(SystemMapper.jsonMapper())
|
.setMapper(SystemMapper.jsonMapper())
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
.addResource(new CallRoutingController(rateLimiters, turnCallRouter, turnTokenGenerator,
|
.addResource(new CallRoutingController(rateLimiters, turnCallRouter, turnTokenGenerator,
|
||||||
experimentEnrollmentManager))
|
experimentEnrollmentManager, cloudflareTurnCredentialsManager))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -97,7 +98,7 @@ class CallRoutingControllerTest {
|
||||||
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
|
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
|
||||||
anyInt())
|
anyInt())
|
||||||
).thenReturn(options);
|
).thenReturn(options);
|
||||||
try(Response response = resources.getJerseyTest()
|
try (Response response = resources.getJerseyTest()
|
||||||
.target(GET_CALL_ENDPOINTS_PATH)
|
.target(GET_CALL_ENDPOINTS_PATH)
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||||
|
@ -114,10 +115,14 @@ class CallRoutingControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetTurnEndpointsCloudflare() {
|
void testGetTurnEndpointsCloudflare() throws IOException {
|
||||||
when(experimentEnrollmentManager.isEnrolled(AuthHelper.VALID_UUID, "cloudflareTurn"))
|
when(experimentEnrollmentManager.isEnrolled(AuthHelper.VALID_UUID, "cloudflareTurn"))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
|
|
||||||
|
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(new TurnToken("ABC", "XYZ",
|
||||||
|
List.of("turn:cloudflare.example.com:3478?transport=udp"), null,
|
||||||
|
"cf.example.com"));
|
||||||
|
|
||||||
try (Response response = resources.getJerseyTest()
|
try (Response response = resources.getJerseyTest()
|
||||||
.target(GET_CALL_ENDPOINTS_PATH)
|
.target(GET_CALL_ENDPOINTS_PATH)
|
||||||
.request()
|
.request()
|
||||||
|
@ -126,11 +131,11 @@ class CallRoutingControllerTest {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
TurnToken token = response.readEntity(TurnToken.class);
|
TurnToken token = response.readEntity(TurnToken.class);
|
||||||
assertThat(token.username()).isNotEmpty();
|
assertThat(token.username()).isEqualTo("ABC");
|
||||||
assertThat(token.password()).isNotEmpty();
|
assertThat(token.password()).isEqualTo("XYZ");
|
||||||
assertThat(token.hostname()).isNotEmpty();
|
assertThat(token.hostname()).isEqualTo("cf.example.com");
|
||||||
assertThat(token.urlsWithIps()).isNull();
|
assertThat(token.urlsWithIps()).isNull();
|
||||||
assertThat(token.urls()).isEqualTo(List.of("turn:cf.example.com"));
|
assertThat(token.urls()).isEqualTo(List.of("turn:cloudflare.example.com:3478?transport=udp"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +152,7 @@ class CallRoutingControllerTest {
|
||||||
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
|
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))),
|
||||||
anyInt())
|
anyInt())
|
||||||
).thenReturn(options);
|
).thenReturn(options);
|
||||||
try(Response response = resources.getJerseyTest()
|
try (Response response = resources.getJerseyTest()
|
||||||
.target(GET_CALL_ENDPOINTS_PATH)
|
.target(GET_CALL_ENDPOINTS_PATH)
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||||
|
@ -168,7 +173,7 @@ class CallRoutingControllerTest {
|
||||||
doThrow(new RateLimitExceededException(null, false))
|
doThrow(new RateLimitExceededException(null, false))
|
||||||
.when(getCallEndpointLimiter).validate(AuthHelper.VALID_UUID);
|
.when(getCallEndpointLimiter).validate(AuthHelper.VALID_UUID);
|
||||||
|
|
||||||
try(final Response response = resources.getJerseyTest()
|
try (final Response response = resources.getJerseyTest()
|
||||||
.target(GET_CALL_ENDPOINTS_PATH)
|
.target(GET_CALL_ENDPOINTS_PATH)
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||||
|
|
|
@ -124,8 +124,7 @@ currentReportingKey.secret: AAAAAAAAAAA=
|
||||||
currentReportingKey.salt: AAAAAAAAAAA=
|
currentReportingKey.salt: AAAAAAAAAAA=
|
||||||
|
|
||||||
turn.secret: AAAAAAAAAAA=
|
turn.secret: AAAAAAAAAAA=
|
||||||
turn.cloudflare.username: ABCDEFGHIJKLM
|
turn.cloudflare.apiToken: ABCDEFGHIJKLM
|
||||||
turn.cloudflare.password: NOPQRSTUVWXYZ
|
|
||||||
|
|
||||||
linkDevice.secret: AAAAAAAAAAA=
|
linkDevice.secret: AAAAAAAAAAA=
|
||||||
|
|
||||||
|
|
|
@ -418,10 +418,15 @@ registrationService:
|
||||||
turn:
|
turn:
|
||||||
secret: secret://turn.secret
|
secret: secret://turn.secret
|
||||||
cloudflare:
|
cloudflare:
|
||||||
username: secret://turn.cloudflare.username
|
apiToken: secret://turn.cloudflare.apiToken
|
||||||
password: secret://turn.cloudflare.password
|
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
||||||
|
ttl: 86400
|
||||||
urls:
|
urls:
|
||||||
- turns:turn.cloudflare.example.com:443?transport=tcp
|
- turn:turn.example.com:80
|
||||||
|
urlsWithIps:
|
||||||
|
- turn:%s
|
||||||
|
- turn:%s:80?transport=tcp
|
||||||
|
- turns:%s:443?transport=tcp
|
||||||
hostname: turn.cloudflare.example.com
|
hostname: turn.cloudflare.example.com
|
||||||
|
|
||||||
linkDevice:
|
linkDevice:
|
||||||
|
|
Loading…
Reference in New Issue