Support for turn allocations

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2016-11-01 16:27:34 -07:00
parent 8eed2329bc
commit 19a4c7253a
9 changed files with 141 additions and 13 deletions

View File

@ -25,6 +25,7 @@ import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
@ -105,6 +106,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
@Valid
@NotNull
@JsonProperty
private TurnConfiguration turn;
public WebsocketConfiguration getWebsocketConfiguration() {
return websocket;
@ -158,6 +164,10 @@ public class WhisperServerConfiguration extends Configuration {
return redphone;
}
public TurnConfiguration getTurnConfiguration() {
return turn;
}
public Map<String, Integer> getTestDevices() {
Map<String, Integer> results = new HashMap<>();

View File

@ -17,7 +17,6 @@
package org.whispersystems.textsecuregcm;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
@ -33,6 +32,7 @@ import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
@ -100,14 +100,12 @@ import javax.servlet.ServletRegistration;
import javax.ws.rs.client.Client;
import java.security.Security;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.Application;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool;
@ -190,6 +188,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
PushSender pushSender = new PushSender(apnFallbackManager, pushServiceClient, websocketSender, config.getPushConfiguration().getQueueSize());
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
environment.lifecycle().manage(apnFallbackManager);
@ -212,7 +211,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.buildAuthFilter()));
environment.jersey().register(new AuthValueFactoryProvider.Binder());
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, config.getTestDevices()));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, turnTokenGenerator, config.getTestDevices()));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));

View File

@ -0,0 +1,23 @@
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class TurnToken {
@JsonProperty
private String username;
@JsonProperty
private String password;
@JsonProperty
private List<String> urls;
public TurnToken(String username, String password, List<String> urls) {
this.username = username;
this.password = password;
this.urls = urls;
}
}

View File

@ -0,0 +1,39 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class TurnTokenGenerator {
private final byte[] key;
private final List<String> urls;
public TurnTokenGenerator(TurnConfiguration configuration) {
this.key = configuration.getSecret().getBytes();
this.urls = configuration.getUris();
}
public TurnToken generate() {
try {
Mac mac = Mac.getInstance("HmacSHA1");
long validUntilSeconds = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) / 1000;
long user = Math.abs(SecureRandom.getInstance("SHA1PRNG").nextInt());
String userTime = validUntilSeconds + ":" + user;
mac.init(new SecretKeySpec(key, "HmacSHA1"));
String password = Base64.encodeBytes(mac.doFinal(userTime.getBytes()));
return new TurnToken(userTime, password, urls);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@ -50,6 +50,9 @@ public class RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration verifyDevice = new RateLimitConfiguration(2, 2);
@JsonProperty
private RateLimitConfiguration turnAllocations = new RateLimitConfiguration(60, 60);
public RateLimitConfiguration getAllocateDevice() {
return allocateDevice;
}
@ -90,6 +93,10 @@ public class RateLimitsConfiguration {
return verifyNumber;
}
public RateLimitConfiguration getTurnAllocations() {
return turnAllocations;
}
public static class RateLimitConfiguration {
@JsonProperty
private int bucketSize;

View File

@ -0,0 +1,26 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
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;
}
}

View File

@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
@ -30,6 +29,8 @@ import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
@ -45,7 +46,6 @@ import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
@ -83,6 +83,7 @@ public class AccountController {
private final MessagesManager messagesManager;
private final TimeProvider timeProvider;
private final Optional<AuthorizationTokenGenerator> tokenGenerator;
private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices;
public AccountController(PendingAccountsManager pendingAccounts,
@ -92,15 +93,17 @@ public class AccountController {
MessagesManager messagesManager,
TimeProvider timeProvider,
Optional<byte[]> authorizationKey,
TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.messagesManager = messagesManager;
this.timeProvider = timeProvider;
this.testDevices = testDevices;
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.messagesManager = messagesManager;
this.timeProvider = timeProvider;
this.testDevices = testDevices;
this.turnTokenGenerator = turnTokenGenerator;
if (authorizationKey.isPresent()) {
tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get()));
@ -232,6 +235,15 @@ public class AccountController {
return tokenGenerator.get().generateFor(account.getNumber());
}
@Timed
@GET
@Path("/turn/")
@Produces(MediaType.APPLICATION_JSON)
public TurnToken getTurnToken(@Auth Account account) throws RateLimitExceededException {
rateLimiters.getTurnLimiter().validate(account.getNumber());
return turnTokenGenerator.generate();
}
@Timed
@PUT
@Path("/gcm/")

View File

@ -36,6 +36,8 @@ public class RateLimiters {
private final RateLimiter allocateDeviceLimiter;
private final RateLimiter verifyDeviceLimiter;
private final RateLimiter turnLimiter;
public RateLimiters(RateLimitsConfiguration config, JedisPool cacheClient) {
this.smsDestinationLimiter = new RateLimiter(cacheClient, "smsDestination",
config.getSmsDestination().getBucketSize(),
@ -77,6 +79,9 @@ public class RateLimiters {
config.getVerifyDevice().getBucketSize(),
config.getVerifyDevice().getLeakRatePerMinute());
this.turnLimiter = new RateLimiter(cacheClient, "turnAllocate",
config.getTurnAllocations().getBucketSize(),
config.getTurnAllocations().getLeakRatePerMinute());
}
public RateLimiter getAllocateDeviceLimiter() {
@ -119,4 +124,8 @@ public class RateLimiters {
return verifyLimiter;
}
public RateLimiter getTurnLimiter() {
return turnLimiter;
}
}

View File

@ -8,6 +8,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
@ -42,6 +43,7 @@ public class AccountControllerTest {
private SmsSender smsSender = mock(SmsSender.class );
private MessagesManager storedMessages = mock(MessagesManager.class );
private TimeProvider timeProvider = mock(TimeProvider.class );
private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f");
@Rule
@ -57,6 +59,7 @@ public class AccountControllerTest {
storedMessages,
timeProvider,
Optional.of(authorizationKey),
turnTokenGenerator,
new HashMap<String, Integer>()))
.build();