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

View File

@ -17,7 +17,6 @@
package org.whispersystems.textsecuregcm; package org.whispersystems.textsecuregcm;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature; 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.dropwizard.simpleauth.BasicCredentialAuthFilter;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator; import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController; import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DeviceController;
@ -100,14 +100,12 @@ import javax.servlet.ServletRegistration;
import javax.ws.rs.client.Client; import javax.ws.rs.client.Client;
import java.security.Security; import java.security.Security;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.Application; import io.dropwizard.Application;
import io.dropwizard.client.JerseyClientBuilder; import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory; import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment; import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool; 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()); PushSender pushSender = new PushSender(apnFallbackManager, pushServiceClient, websocketSender, config.getPushConfiguration().getQueueSize());
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager); ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager); FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey(); Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
environment.lifecycle().manage(apnFallbackManager); environment.lifecycle().manage(apnFallbackManager);
@ -212,7 +211,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.buildAuthFilter())); .buildAuthFilter()));
environment.jersey().register(new AuthValueFactoryProvider.Binder()); 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 DeviceController(pendingDevicesManager, accountsManager, messagesManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory)); environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1)); 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 @JsonProperty
private RateLimitConfiguration verifyDevice = new RateLimitConfiguration(2, 2); private RateLimitConfiguration verifyDevice = new RateLimitConfiguration(2, 2);
@JsonProperty
private RateLimitConfiguration turnAllocations = new RateLimitConfiguration(60, 60);
public RateLimitConfiguration getAllocateDevice() { public RateLimitConfiguration getAllocateDevice() {
return allocateDevice; return allocateDevice;
} }
@ -90,6 +93,10 @@ public class RateLimitsConfiguration {
return verifyNumber; return verifyNumber;
} }
public RateLimitConfiguration getTurnAllocations() {
return turnAllocations;
}
public static class RateLimitConfiguration { public static class RateLimitConfiguration {
@JsonProperty @JsonProperty
private int bucketSize; 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.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.codahale.metrics.annotation.Timed; import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional; 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.AuthorizationToken;
import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator; import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; 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.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; 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.Constants;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode; import org.whispersystems.textsecuregcm.util.VerificationCode;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
import javax.validation.Valid; import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
@ -83,6 +83,7 @@ public class AccountController {
private final MessagesManager messagesManager; private final MessagesManager messagesManager;
private final TimeProvider timeProvider; private final TimeProvider timeProvider;
private final Optional<AuthorizationTokenGenerator> tokenGenerator; private final Optional<AuthorizationTokenGenerator> tokenGenerator;
private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices; private final Map<String, Integer> testDevices;
public AccountController(PendingAccountsManager pendingAccounts, public AccountController(PendingAccountsManager pendingAccounts,
@ -92,6 +93,7 @@ public class AccountController {
MessagesManager messagesManager, MessagesManager messagesManager,
TimeProvider timeProvider, TimeProvider timeProvider,
Optional<byte[]> authorizationKey, Optional<byte[]> authorizationKey,
TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices) Map<String, Integer> testDevices)
{ {
this.pendingAccounts = pendingAccounts; this.pendingAccounts = pendingAccounts;
@ -101,6 +103,7 @@ public class AccountController {
this.messagesManager = messagesManager; this.messagesManager = messagesManager;
this.timeProvider = timeProvider; this.timeProvider = timeProvider;
this.testDevices = testDevices; this.testDevices = testDevices;
this.turnTokenGenerator = turnTokenGenerator;
if (authorizationKey.isPresent()) { if (authorizationKey.isPresent()) {
tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get())); tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get()));
@ -232,6 +235,15 @@ public class AccountController {
return tokenGenerator.get().generateFor(account.getNumber()); 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 @Timed
@PUT @PUT
@Path("/gcm/") @Path("/gcm/")

View File

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

View File

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