New API to support multiple accounts per # (FREEBIE)

This commit is contained in:
Matt Corallo
2014-01-07 13:35:58 -10:00
parent 4cd1082a4a
commit ef1160eda8
35 changed files with 1591 additions and 388 deletions

View File

@@ -16,6 +16,7 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
@@ -33,6 +34,7 @@ import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@@ -58,17 +60,20 @@ public class AccountController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final PendingAccountsManager pendingAccounts;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final PendingAccountsManager pendingAccounts;
private final PendingDevicesManager pendingDevices;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
SmsSender smsSenderFactory)
PendingDevicesManager pendingDevices,
AccountsManager accounts,
RateLimiters rateLimiters,
SmsSender smsSenderFactory)
{
this.pendingAccounts = pendingAccounts;
this.pendingDevices = pendingDevices;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
@@ -119,8 +124,8 @@ public class AccountController {
throws RateLimitExceededException
{
try {
AuthorizationHeader header = new AuthorizationHeader(authorizationHeader);
String number = header.getUserName();
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getNumber();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
@@ -138,16 +143,22 @@ public class AccountController {
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
account.setSignalingKey(accountAttributes.getSignalingKey());
account.setSupportsSms(accountAttributes.getSupportsSms());
account.setFetchesMessages(accountAttributes.getFetchesMessages());
account.setDeviceId(0);
accounts.createResetNumber(account);
pendingAccounts.remove(number);
accounts.create(account);
logger.debug("Stored account...");
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@Path("/gcm/")
@@ -190,10 +201,10 @@ public class AccountController {
@Produces(MediaType.APPLICATION_XML)
public Response getTwiml(@PathParam("code") String encodedVerificationText) {
return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML,
encodedVerificationText)).build();
encodedVerificationText)).build();
}
private VerificationCode generateVerificationCode() {
@VisibleForTesting protected VerificationCode generateVerificationCode() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int randomInt = 100000 + random.nextInt(900000);
@@ -203,4 +214,64 @@ public class AccountController {
}
}
@Timed
@GET
@Path("/registerdevice")
@Produces(MediaType.APPLICATION_JSON)
public VerificationCode createDeviceToken(@Auth Account account)
throws RateLimitExceededException
{
rateLimiters.getVerifyLimiter().validate(account.getNumber()); //TODO: New limiter?
VerificationCode verificationCode = generateVerificationCode();
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
return verificationCode;
}
@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("/device/{verification_code}")
public long verifyDeviceToken(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
Account account;
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getNumber();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number); //TODO: New limiter?
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
if (!storedVerificationCode.isPresent() ||
!verificationCode.equals(storedVerificationCode.get()))
{
throw new WebApplicationException(Response.status(403).build());
}
account = new Account();
account.setNumber(number);
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
account.setSignalingKey(accountAttributes.getSignalingKey());
account.setSupportsSms(accountAttributes.getSupportsSms());
account.setFetchesMessages(accountAttributes.getFetchesMessages());
accounts.createAccountOnExistingNumber(account);
pendingDevices.remove(number);
logger.debug("Stored new device account...");
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
}
return account.getDeviceId();
}
}

View File

@@ -29,11 +29,13 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.RelayMessage;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.util.NumberData;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.util.Util;
@@ -86,16 +88,16 @@ public class FederationController {
@GET
@Path("/key/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey getKey(@Auth FederatedPeer peer,
public UnstructuredPreKeyList getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
{
PreKey preKey = keys.get(number);
UnstructuredPreKeyList preKeys = keys.get(number, accounts.getAllByNumber(number));
if (preKey == null) {
if (preKeys == null) {
throw new WebApplicationException(Response.status(404).build());
}
return preKey;
return preKeys;
}
@Timed
@@ -111,7 +113,7 @@ public class FederationController {
.setRelay(peer.getName())
.build();
pushSender.sendMessage(message.getDestination(), signal);
pushSender.sendMessage(message.getDestination(), message.getDestinationDeviceId(), signal);
} catch (InvalidProtocolBufferException ipe) {
logger.warn("ProtoBuf", ipe);
throw new WebApplicationException(Response.status(400).build());
@@ -136,18 +138,15 @@ public class FederationController {
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<NumberData> numberList = accounts.getAllNumbers(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> clientContacts = new LinkedList<>();
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
for (NumberData number : numberList) {
byte[] token = Util.getContactToken(number.getNumber());
ClientContact clientContact = new ClientContact(token, null, number.isSupportsSms());
if (Util.isEmpty(account.getApnRegistrationId()) &&
Util.isEmpty(account.getGcmRegistrationId()))
{
if (!number.isActive())
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}

View File

@@ -22,10 +22,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyList;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
@@ -39,21 +41,24 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
@Path("/v1/keys")
public class KeysController {
public abstract class KeysController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final RateLimiters rateLimiters;
private final Keys keys;
private final AccountsManager accountsManager;
private final FederatedClientManager federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys,
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager,
FederatedClientManager federatedClientManager)
{
this.rateLimiters = rateLimiters;
this.keys = keys;
this.accountsManager = accountsManager;
this.federatedClientManager = federatedClientManager;
}
@@ -61,32 +66,67 @@ public class KeysController {
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
keys.store(account.getNumber(), preKeys.getLastResortKey(), preKeys.getKeys());
keys.store(account.getNumber(), account.getDeviceId(), preKeys.getLastResortKey(), preKeys.getKeys());
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") String relay)
throws RateLimitExceededException
public List<PreKey> getKeys(Account account, String number, String relay) throws RateLimitExceededException
{
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number);
try {
PreKey key;
UnstructuredPreKeyList keyList;
if (relay == null) key = keys.get(number);
else key = federatedClientManager.getClient(relay).getKey(number);
if (relay == null) {
keyList = keys.get(number, accountsManager.getAllByNumber(number));
} else {
keyList = federatedClientManager.getClient(relay).getKeys(number);
}
if (key == null) throw new WebApplicationException(Response.status(404).build());
else return key;
if (keyList == null || keyList.getKeys().isEmpty()) throw new WebApplicationException(Response.status(404).build());
else return keyList.getKeys();
} catch (NoSuchPeerException e) {
logger.info("No peer: " + relay);
throw new WebApplicationException(Response.status(404).build());
}
}
@Path("/v1/keys")
public static class V1 extends KeysController {
public V1(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager, FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accountsManager, federatedClientManager);
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") String relay)
throws RateLimitExceededException
{
return super.getKeys(account, number, relay).get(0);
}
}
@Path("/v2/keys")
public static class V2 extends KeysController {
public V2(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager, FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accountsManager, federatedClientManager);
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public List<PreKey> get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") String relay)
throws RateLimitExceededException
{
return super.getKeys(account, number, relay);
}
}
}

View File

@@ -138,12 +138,13 @@ public class MessageController extends HttpServlet {
try {
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
String destination = messagePair.first().getDestination();
String relay = messagePair.first().getRelay();
String destination = messagePair.first().getDestination();
long destinationDeviceId = messagePair.first().getDestinationDeviceId();
String relay = messagePair.first().getRelay();
try {
if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
else sendRelayMessage(relay, destination, messagePair.second());
if (Util.isEmpty(relay)) sendLocalMessage(destination, destinationDeviceId, messagePair.second());
else sendRelayMessage(relay, destination, destinationDeviceId, messagePair.second());
success.add(destination);
} catch (NoSuchUserException e) {
logger.debug("No such user", e);
@@ -168,18 +169,18 @@ public class MessageController extends HttpServlet {
});
}
private void sendLocalMessage(String destination, OutgoingMessageSignal outgoingMessage)
private void sendLocalMessage(String destination, long destinationDeviceId, OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
pushSender.sendMessage(destination, outgoingMessage);
pushSender.sendMessage(destination, destinationDeviceId, outgoingMessage);
}
private void sendRelayMessage(String relay, String destination, OutgoingMessageSignal outgoingMessage)
private void sendRelayMessage(String relay, String destination, long destinationDeviceId, OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
try {
FederatedClient client = federatedClientManager.getClient(relay);
client.sendMessage(destination, outgoingMessage);
client.sendMessage(destination, destinationDeviceId, outgoingMessage);
} catch (NoSuchPeerException e) {
logger.info("No such peer", e);
throw new NoSuchUserException(e);
@@ -208,6 +209,7 @@ public class MessageController extends HttpServlet {
for (IncomingMessage sub : incomingMessages) {
if (sub != incoming) {
outgoingMessage.setDestinationDeviceIds(index, sub.getDestinationDeviceId());
outgoingMessage.setDestinations(index++, sub.getDestination());
}
}
@@ -263,8 +265,8 @@ public class MessageController extends HttpServlet {
private Account authenticate(HttpServletRequest request) throws AuthenticationException {
try {
AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization"));
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(),
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromFullHeader(request.getHeader("Authorization"));
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getNumber() + "." + authorizationHeader.getDeviceId(),
authorizationHeader.getPassword() );
Optional<Account> account = accountAuthenticator.authenticate(credentials);