Make TURN configuration dynamic
Also enables conditionally including more TURN servers for gradual rollouts
This commit is contained in:
parent
8541360bf3
commit
c70d7535b9
|
@ -78,14 +78,6 @@ twilio: # Twilio gateway configuration
|
||||||
androidAppHash: example # Hash appended to Android
|
androidAppHash: example # Hash appended to Android
|
||||||
verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable
|
verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable
|
||||||
|
|
||||||
turn: # TURN server configuration
|
|
||||||
secret: example # TURN server secret
|
|
||||||
uris:
|
|
||||||
- stun:example.com:80
|
|
||||||
- stun:another.example.com:443
|
|
||||||
- turn:example.com:443?transport=udp
|
|
||||||
- turn:ya.example.com:80?transport=udp
|
|
||||||
|
|
||||||
cacheCluster: # Redis server configuration for cache cluster
|
cacheCluster: # Redis server configuration for cache cluster
|
||||||
configurationUri: redis://redis.example.com:6379/
|
configurationUri: redis://redis.example.com:6379/
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@ import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfig
|
||||||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
|
||||||
|
@ -168,11 +167,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private WebSocketConfiguration webSocket = new WebSocketConfiguration();
|
private WebSocketConfiguration webSocket = new WebSocketConfiguration();
|
||||||
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty
|
|
||||||
private TurnConfiguration turn;
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -345,10 +339,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return limits;
|
return limits;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TurnConfiguration getTurnConfiguration() {
|
|
||||||
return turn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GcmConfiguration getGcmConfiguration() {
|
public GcmConfiguration getGcmConfiguration() {
|
||||||
return gcm;
|
return gcm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -484,7 +484,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||||
MessageSender messageSender = new MessageSender(apnFallbackManager, clientPresenceManager, messagesManager, gcmSender, apnSender, pushLatencyManager);
|
MessageSender messageSender = new MessageSender(apnFallbackManager, clientPresenceManager, messagesManager, gcmSender, apnSender, pushLatencyManager);
|
||||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
||||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager);
|
||||||
RecaptchaClient recaptchaClient = new RecaptchaClient(
|
RecaptchaClient recaptchaClient = new RecaptchaClient(
|
||||||
config.getRecaptchaConfiguration().getProjectPath(),
|
config.getRecaptchaConfiguration().getProjectPath(),
|
||||||
config.getRecaptchaConfiguration().getCredentialConfigurationJson(),
|
config.getRecaptchaConfiguration().getCredentialConfigurationJson(),
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
package org.whispersystems.textsecuregcm.auth;
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -25,4 +26,9 @@ public class TurnToken {
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.urls = urls;
|
this.urls = urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
List<String> getUrls() {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,12 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.auth;
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
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.WeightedRandomSelect;
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
@ -14,20 +19,21 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class TurnTokenGenerator {
|
public class TurnTokenGenerator {
|
||||||
|
|
||||||
private final byte[] key;
|
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfiguration;
|
||||||
private final List<String> urls;
|
|
||||||
|
|
||||||
public TurnTokenGenerator(TurnConfiguration configuration) {
|
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> config) {
|
||||||
this.key = configuration.getSecret().getBytes();
|
this.dynamicConfiguration = config;
|
||||||
this.urls = configuration.getUris();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TurnToken generate() {
|
public TurnToken generate(final String e164) {
|
||||||
try {
|
try {
|
||||||
|
byte[] key = dynamicConfiguration.getConfiguration().getTurnConfiguration().getSecret().getBytes();
|
||||||
|
List<String> urls = urls(e164);
|
||||||
Mac mac = Mac.getInstance("HmacSHA1");
|
Mac mac = Mac.getInstance("HmacSHA1");
|
||||||
long validUntilSeconds = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) / 1000;
|
long validUntilSeconds = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) / 1000;
|
||||||
long user = Math.abs(new SecureRandom().nextInt());
|
long user = Math.abs(new SecureRandom().nextInt());
|
||||||
|
@ -41,4 +47,22 @@ public class TurnTokenGenerator {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> urls(final String e164) {
|
||||||
|
final DynamicTurnConfiguration turnConfig = dynamicConfiguration.getConfiguration().getTurnConfiguration();
|
||||||
|
|
||||||
|
// Check if number is enrolled to test out specific turn servers
|
||||||
|
final Optional<TurnUriConfiguration> enrolled = turnConfig.getUriConfigs().stream()
|
||||||
|
.filter(config -> config.getEnrolledNumbers().contains(e164))
|
||||||
|
.findFirst();
|
||||||
|
if (enrolled.isPresent()) {
|
||||||
|
return enrolled.get().getUris();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, select from turn server sets by weighted choice
|
||||||
|
return WeightedRandomSelect.select(turnConfig
|
||||||
|
.getUriConfigs()
|
||||||
|
.stream()
|
||||||
|
.map(c -> new Pair<List<String>, Long>(c.getUris(), c.getWeight())).toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.validation.constraints.NotEmpty;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
public class TurnConfiguration {
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
@NotEmpty
|
|
||||||
private String secret;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
@NotNull
|
|
||||||
private List<String> uris;
|
|
||||||
|
|
||||||
public List<String> getUris() {
|
|
||||||
return uris;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSecret() {
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class TurnUriConfiguration {
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private List<String> uris;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The weight of this entry for weighted random selection
|
||||||
|
*/
|
||||||
|
@JsonProperty
|
||||||
|
@Min(0)
|
||||||
|
private long weight = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enrolled numbers will always get this uri list
|
||||||
|
*/
|
||||||
|
private Set<String> enrolledNumbers = Collections.emptySet();
|
||||||
|
|
||||||
|
public List<String> getUris() {
|
||||||
|
return uris;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getWeight() {
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getEnrolledNumbers() {
|
||||||
|
return Collections.unmodifiableSet(enrolledNumbers);
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,10 @@ public class DynamicConfiguration {
|
||||||
@Valid
|
@Valid
|
||||||
private DynamicUakMigrationConfiguration uakMigrationConfiguration = new DynamicUakMigrationConfiguration();
|
private DynamicUakMigrationConfiguration uakMigrationConfiguration = new DynamicUakMigrationConfiguration();
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Valid
|
||||||
|
private DynamicTurnConfiguration turn = new DynamicTurnConfiguration();
|
||||||
|
|
||||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||||
final String experimentName) {
|
final String experimentName) {
|
||||||
return Optional.ofNullable(experiments.get(experimentName));
|
return Optional.ofNullable(experiments.get(experimentName));
|
||||||
|
@ -109,4 +113,8 @@ public class DynamicConfiguration {
|
||||||
|
|
||||||
public DynamicUakMigrationConfiguration getUakMigrationConfiguration() { return uakMigrationConfiguration; }
|
public DynamicUakMigrationConfiguration getUakMigrationConfiguration() { return uakMigrationConfiguration; }
|
||||||
|
|
||||||
|
public DynamicTurnConfiguration getTurnConfiguration() {
|
||||||
|
return turn;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public class DynamicTurnConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String secret;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<@Valid TurnUriConfiguration> uriConfigs = Collections.emptyList();
|
||||||
|
|
||||||
|
public List<TurnUriConfiguration> getUriConfigs() {
|
||||||
|
return uriConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSecret() {
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
}
|
|
@ -449,7 +449,7 @@ public class AccountController {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public TurnToken getTurnToken(@Auth AuthenticatedAccount auth) throws RateLimitExceededException {
|
public TurnToken getTurnToken(@Auth AuthenticatedAccount auth) throws RateLimitExceededException {
|
||||||
rateLimiters.getTurnLimiter().validate(auth.getAccount().getUuid());
|
rateLimiters.getTurnLimiter().validate(auth.getAccount().getUuid());
|
||||||
return turnTokenGenerator.generate();
|
return turnTokenGenerator.generate(auth.getAccount().getNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a random item according to its weight
|
||||||
|
*
|
||||||
|
* @param <T> the type of the objects to select from
|
||||||
|
*/
|
||||||
|
public class WeightedRandomSelect<T> {
|
||||||
|
|
||||||
|
List<Pair<T, Long>> weightedItems;
|
||||||
|
long totalWeight;
|
||||||
|
|
||||||
|
public WeightedRandomSelect(List<Pair<T, Long>> weightedItems) throws IllegalArgumentException {
|
||||||
|
this.weightedItems = weightedItems;
|
||||||
|
this.totalWeight = weightedItems.stream().mapToLong(Pair::second).sum();
|
||||||
|
|
||||||
|
weightedItems.stream().map(Pair::second).filter(w -> w < 0).findFirst().ifPresent(invalid -> {
|
||||||
|
throw new IllegalArgumentException("Illegal selection weight " + invalid);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (weightedItems.isEmpty() || totalWeight == 0) {
|
||||||
|
throw new IllegalArgumentException("Cannot create an empty weighted random selector");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T select() {
|
||||||
|
if (weightedItems.size() == 1) {
|
||||||
|
return weightedItems.get(0).first();
|
||||||
|
}
|
||||||
|
long select = ThreadLocalRandom.current().nextLong(0, totalWeight);
|
||||||
|
long current = 0;
|
||||||
|
for (Pair<T, Long> item : weightedItems) {
|
||||||
|
/*
|
||||||
|
Accumulate weights for each item and select the first item whose
|
||||||
|
cumulative weight exceeds the selected value. nextLong() is exclusive,
|
||||||
|
so by the last item we're guaranteed to find a value as the
|
||||||
|
last item's weight is one more than the maximum value of select.
|
||||||
|
*/
|
||||||
|
current += item.second();
|
||||||
|
if (current > select) {
|
||||||
|
return item.first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("totalWeight " + totalWeight + " exceeds item weights");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T select(List<Pair<T, Long>> weightedItems) {
|
||||||
|
return new WeightedRandomSelect<T>(weightedItems).select();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class TurnTokenGeneratorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlwaysSelectFirst() throws JsonProcessingException {
|
||||||
|
final String configString = """
|
||||||
|
captcha:
|
||||||
|
scoreFloor: 1.0
|
||||||
|
turn:
|
||||||
|
secret: bloop
|
||||||
|
uriConfigs:
|
||||||
|
- uris:
|
||||||
|
- always1.org
|
||||||
|
- always2.org
|
||||||
|
- uris:
|
||||||
|
- never.org
|
||||||
|
weight: 0
|
||||||
|
""";
|
||||||
|
DynamicConfiguration config = DynamicConfigurationManager
|
||||||
|
.parseConfiguration(configString, DynamicConfiguration.class)
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
DynamicConfigurationManager<DynamicConfiguration> mockDynamicConfigManager = mock(
|
||||||
|
DynamicConfigurationManager.class);
|
||||||
|
|
||||||
|
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||||
|
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(mockDynamicConfigManager);
|
||||||
|
|
||||||
|
final long COUNT = 1000;
|
||||||
|
|
||||||
|
final Map<String, Long> urlCounts = Stream
|
||||||
|
.generate(() -> turnTokenGenerator.generate(""))
|
||||||
|
.limit(COUNT)
|
||||||
|
.flatMap(token -> token.getUrls().stream())
|
||||||
|
.collect(Collectors.groupingBy(i -> i, Collectors.counting()));
|
||||||
|
|
||||||
|
assertThat(urlCounts.get("always1.org")).isEqualTo(COUNT);
|
||||||
|
assertThat(urlCounts.get("always2.org")).isEqualTo(COUNT);
|
||||||
|
assertThat(urlCounts).doesNotContainKey("never.org");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProbabilisticUrls() throws JsonProcessingException {
|
||||||
|
final String configString = """
|
||||||
|
captcha:
|
||||||
|
scoreFloor: 1.0
|
||||||
|
turn:
|
||||||
|
secret: bloop
|
||||||
|
uriConfigs:
|
||||||
|
- uris:
|
||||||
|
- always.org
|
||||||
|
- sometimes1.org
|
||||||
|
weight: 5
|
||||||
|
- uris:
|
||||||
|
- always.org
|
||||||
|
- sometimes2.org
|
||||||
|
weight: 5
|
||||||
|
""";
|
||||||
|
DynamicConfiguration config = DynamicConfigurationManager
|
||||||
|
.parseConfiguration(configString, DynamicConfiguration.class)
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
DynamicConfigurationManager<DynamicConfiguration> mockDynamicConfigManager = mock(
|
||||||
|
DynamicConfigurationManager.class);
|
||||||
|
|
||||||
|
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||||
|
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(mockDynamicConfigManager);
|
||||||
|
|
||||||
|
final long COUNT = 1000;
|
||||||
|
|
||||||
|
final Map<String, Long> urlCounts = Stream
|
||||||
|
.generate(() -> turnTokenGenerator.generate(""))
|
||||||
|
.limit(COUNT)
|
||||||
|
.flatMap(token -> token.getUrls().stream())
|
||||||
|
.collect(Collectors.groupingBy(i -> i, Collectors.counting()));
|
||||||
|
|
||||||
|
assertThat(urlCounts.get("always.org")).isEqualTo(COUNT);
|
||||||
|
assertThat(urlCounts.get("sometimes1.org")).isGreaterThan(0);
|
||||||
|
assertThat(urlCounts.get("sometimes2.org")).isGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExplicitEnrollment() throws JsonProcessingException {
|
||||||
|
final String configString = """
|
||||||
|
captcha:
|
||||||
|
scoreFloor: 1.0
|
||||||
|
turn:
|
||||||
|
secret: bloop
|
||||||
|
uriConfigs:
|
||||||
|
- uris:
|
||||||
|
- enrolled.org
|
||||||
|
weight: 0
|
||||||
|
enrolledNumbers:
|
||||||
|
- +15555555555
|
||||||
|
- uris:
|
||||||
|
- unenrolled.org
|
||||||
|
weight: 1
|
||||||
|
""";
|
||||||
|
DynamicConfiguration config = DynamicConfigurationManager
|
||||||
|
.parseConfiguration(configString, DynamicConfiguration.class)
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
DynamicConfigurationManager<DynamicConfiguration> mockDynamicConfigManager = mock(
|
||||||
|
DynamicConfigurationManager.class);
|
||||||
|
|
||||||
|
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
||||||
|
|
||||||
|
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(mockDynamicConfigManager);
|
||||||
|
TurnToken token = turnTokenGenerator.generate("+15555555555");
|
||||||
|
assertThat(token.getUrls().get(0)).isEqualTo("enrolled.org");
|
||||||
|
token = turnTokenGenerator.generate("+15555555556");
|
||||||
|
assertThat(token.getUrls().get(0)).isEqualTo("unenrolled.org");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -395,4 +395,46 @@ class DynamicConfigurationTest {
|
||||||
assertThat(directoryReconcilerConfiguration.isEnabled()).isFalse();
|
assertThat(directoryReconcilerConfiguration.isEnabled()).isFalse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseTurnConfig() throws JsonProcessingException {
|
||||||
|
{
|
||||||
|
final String config = REQUIRED_CONFIG.concat("""
|
||||||
|
turn:
|
||||||
|
secret: bloop
|
||||||
|
uriConfigs:
|
||||||
|
- uris:
|
||||||
|
- turn:test.org
|
||||||
|
weight: -1
|
||||||
|
""");
|
||||||
|
assertThat(DynamicConfigurationManager.parseConfiguration(config, DynamicConfiguration.class)).isEmpty();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
final String config = REQUIRED_CONFIG.concat("""
|
||||||
|
turn:
|
||||||
|
secret: bloop
|
||||||
|
uriConfigs:
|
||||||
|
- uris:
|
||||||
|
- turn:test0.org
|
||||||
|
- turn:test1.org
|
||||||
|
- uris:
|
||||||
|
- turn:test2.org
|
||||||
|
weight: 2
|
||||||
|
enrolledNumbers:
|
||||||
|
- +15555555555
|
||||||
|
""");
|
||||||
|
DynamicTurnConfiguration turnConfiguration = DynamicConfigurationManager
|
||||||
|
.parseConfiguration(config, DynamicConfiguration.class)
|
||||||
|
.orElseThrow()
|
||||||
|
.getTurnConfiguration();
|
||||||
|
assertThat(turnConfiguration.getSecret()).isEqualTo("bloop");
|
||||||
|
assertThat(turnConfiguration.getUriConfigs().get(0).getUris()).hasSize(2);
|
||||||
|
assertThat(turnConfiguration.getUriConfigs().get(1).getUris()).hasSize(1);
|
||||||
|
assertThat(turnConfiguration.getUriConfigs().get(0).getWeight()).isEqualTo(1);
|
||||||
|
assertThat(turnConfiguration.getUriConfigs().get(1).getWeight()).isEqualTo(2);
|
||||||
|
assertThat(turnConfiguration.getUriConfigs().get(1).getEnrolledNumbers()).containsExactly("+15555555555");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class WeightedRandomSelectTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test5050() {
|
||||||
|
final WeightedRandomSelect<String> selector = new WeightedRandomSelect<>(
|
||||||
|
List.of(new Pair<>("a", 1L), new Pair<>("b", 1L)));
|
||||||
|
final Map<String, Long> counts = Stream.generate(selector::select)
|
||||||
|
.limit(1000)
|
||||||
|
.collect(Collectors.groupingBy(s -> s, Collectors.counting()));
|
||||||
|
assertThat(counts.get("a")).isGreaterThan(1);
|
||||||
|
assertThat(counts.get("b")).isGreaterThan(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlways() {
|
||||||
|
final WeightedRandomSelect<String> selector = new WeightedRandomSelect<>(
|
||||||
|
List.of(new Pair<>("a", 1L), new Pair<>("b", 0L)));
|
||||||
|
final Map<String, Long> counts = Stream.generate(selector::select)
|
||||||
|
.limit(1000)
|
||||||
|
.collect(Collectors.groupingBy(s -> s, Collectors.counting()));
|
||||||
|
assertThat(counts.get("a")).isEqualTo(1000);
|
||||||
|
assertThat(counts).doesNotContainKey("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testThree() {
|
||||||
|
final WeightedRandomSelect<String> selector = new WeightedRandomSelect<>(
|
||||||
|
List.of(new Pair<>("a", 33L), new Pair<>("b", 33L), new Pair<>("c", 33L)));
|
||||||
|
final Map<String, Long> counts = Stream.generate(selector::select)
|
||||||
|
.limit(1000)
|
||||||
|
.collect(Collectors.groupingBy(s -> s, Collectors.counting()));
|
||||||
|
assertThat(counts.get("a")).isGreaterThan(1);
|
||||||
|
assertThat(counts.get("b")).isGreaterThan(1);
|
||||||
|
assertThat(counts.get("c")).isGreaterThan(1);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue