diff --git a/pom.xml b/pom.xml
index c22e45e02..1ad6ab003 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,7 +56,7 @@
com.google.protobuf
protobuf-java
- 2.4.1
+ 2.5.0
diff --git a/protobuf/OutgoingMessageSignal.proto b/protobuf/OutgoingMessageSignal.proto
index 06cf14600..f89daf3bd 100644
--- a/protobuf/OutgoingMessageSignal.proto
+++ b/protobuf/OutgoingMessageSignal.proto
@@ -24,6 +24,7 @@ message OutgoingMessageSignal {
optional string source = 2;
optional string relay = 3;
repeated string destinations = 4;
+ repeated uint64 destinationDeviceIds = 7;
optional uint64 timestamp = 5;
optional bytes message = 6;
}
\ No newline at end of file
diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index f6b2992b6..bbe0cdc27 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -57,6 +57,10 @@ import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
+import org.whispersystems.textsecuregcm.storage.PendingDeviceRegistrations;
+import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
+import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
+import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
@@ -90,18 +94,22 @@ public class WhisperServerService extends Service {
DBIFactory dbiFactory = new DBIFactory();
DBI jdbi = dbiFactory.build(environment, config.getDatabaseConfiguration(), "postgresql");
- Accounts accounts = jdbi.onDemand(Accounts.class);
- PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
- Keys keys = jdbi.onDemand(Keys.class);
+ Accounts accounts = jdbi.onDemand(Accounts.class);
+ PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
+ PendingDeviceRegistrations pendingDevices = jdbi.onDemand(PendingDeviceRegistrations.class);
+ Keys keys = jdbi.onDemand(Keys.class);
+ StoredMessages storedMessages = jdbi.onDemand(StoredMessages.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
+ PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, memcachedClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager );
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
+ StoredMessageManager storedMessageManager = new StoredMessageManager(storedMessages);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
@@ -109,6 +117,7 @@ public class WhisperServerService extends Service {
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
config.getApnConfiguration(),
+ storedMessageManager,
accountsManager, directory);
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
@@ -116,10 +125,11 @@ public class WhisperServerService extends Service {
accountAuthenticator,
Account.class, "WhisperServer"));
- environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender));
+ environment.addResource(new AccountController(pendingAccountsManager, pendingDevicesManager, accountsManager, rateLimiters, smsSender));
environment.addResource(new DirectoryController(rateLimiters, directory));
environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner));
- environment.addResource(new KeysController(rateLimiters, keys, federatedClientManager));
+ environment.addResource(new KeysController.V1(rateLimiters, keys, accountsManager, federatedClientManager));
+ environment.addResource(new KeysController.V2(rateLimiters, keys, accountsManager, federatedClientManager));
environment.addResource(new FederationController(keys, accountsManager, pushSender, urlSigner));
environment.addServlet(new MessageController(rateLimiters, accountAuthenticator,
diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java b/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java
index bb5bfb473..58b517de6 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java
@@ -51,7 +51,13 @@ public class AccountAuthenticator implements Authenticator authenticate(BasicCredentials basicCredentials)
throws AuthenticationException
{
- Optional account = accountsManager.get(basicCredentials.getUsername());
+ AuthorizationHeader authorizationHeader;
+ try {
+ authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
+ } catch (InvalidAuthorizationHeaderException iahe) {
+ return Optional.absent();
+ }
+ Optional account = accountsManager.get(authorizationHeader.getNumber(), authorizationHeader.getDeviceId());
if (!account.isPresent()) {
return Optional.absent();
diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java
index d0771dd44..f102f2ea5 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java
@@ -24,10 +24,28 @@ import java.io.IOException;
public class AuthorizationHeader {
- private final String user;
+ private final String number;
+ private final long accountId;
private final String password;
- public AuthorizationHeader(String header) throws InvalidAuthorizationHeaderException {
+ private AuthorizationHeader(String number, long accountId, String password) {
+ this.number = number;
+ this.accountId = accountId;
+ this.password = password;
+ }
+
+ public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {
+ try {
+ String[] numberAndId = user.split("\\.");
+ return new AuthorizationHeader(numberAndId[0],
+ numberAndId.length > 1 ? Long.parseLong(numberAndId[1]) : 1,
+ password);
+ } catch (NumberFormatException nfe) {
+ throw new InvalidAuthorizationHeaderException(nfe);
+ }
+ }
+
+ public static AuthorizationHeader fromFullHeader(String header) throws InvalidAuthorizationHeaderException {
try {
if (header == null) {
throw new InvalidAuthorizationHeaderException("Null header");
@@ -55,16 +73,18 @@ public class AuthorizationHeader {
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
}
- this.user = credentialParts[0];
- this.password = credentialParts[1];
-
+ return fromUserAndPassword(credentialParts[0], credentialParts[1]);
} catch (IOException ioe) {
throw new InvalidAuthorizationHeaderException(ioe);
}
}
- public String getUserName() {
- return user;
+ public String getNumber() {
+ return number;
+ }
+
+ public long getDeviceId() {
+ return accountId;
}
public String getPassword() {
diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
index cb2ad62ff..3a6364486 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
@@ -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 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();
+ }
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java
index e7dd510e3..bc31b4121 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java
@@ -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 accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
+ List numberList = accounts.getAllNumbers(offset, ACCOUNT_CHUNK_SIZE);
List 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);
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java
index ad836afcd..6ff6fae36 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java
@@ -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 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 get(@Auth Account account,
+ @PathParam("number") String number,
+ @QueryParam("relay") String relay)
+ throws RateLimitExceededException
+ {
+ return super.getKeys(account, number, relay);
+ }
+ }
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
index 4c6601a6a..ea56815eb 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
@@ -138,12 +138,13 @@ public class MessageController extends HttpServlet {
try {
for (Pair 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 = accountAuthenticator.authenticate(credentials);
diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java
index 1d1d2aa7d..18db726f9 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java
@@ -28,11 +28,15 @@ public class AccountAttributes {
@JsonProperty
private boolean supportsSms;
+ @JsonProperty
+ private boolean fetchesMessages;
+
public AccountAttributes() {}
- public AccountAttributes(String signalingKey, boolean supportsSms) {
+ public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
+ this.fetchesMessages = fetchesMessages;
}
public String getSignalingKey() {
@@ -43,4 +47,8 @@ public class AccountAttributes {
return supportsSms;
}
+ public boolean getFetchesMessages() {
+ return fetchesMessages;
+ }
+
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java b/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java
index f5cbfb5c2..85b23a7e8 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java
@@ -38,6 +38,9 @@ public class IncomingMessage {
@JsonProperty
private long timestamp;
+ @JsonProperty
+ private long destinationDeviceId = 1;
+
public String getDestination() {
return destination;
}
@@ -53,4 +56,12 @@ public class IncomingMessage {
public String getRelay() {
return relay;
}
+
+ public long getDestinationDeviceId() {
+ return destinationDeviceId;
+ }
+
+ public void setDestinationDeviceId(long destinationDeviceId) {
+ this.destinationDeviceId = destinationDeviceId;
+ }
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java b/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java
index 60e63a559..b308aea51 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java
@@ -10,174 +10,445 @@ public final class MessageProtos {
}
public interface OutgoingMessageSignalOrBuilder
extends com.google.protobuf.MessageOrBuilder {
-
+
// optional uint32 type = 1;
+ /**
+ * optional uint32 type = 1;
+ */
boolean hasType();
+ /**
+ * optional uint32 type = 1;
+ */
int getType();
-
+
// optional string source = 2;
+ /**
+ * optional string source = 2;
+ */
boolean hasSource();
- String getSource();
-
+ /**
+ * optional string source = 2;
+ */
+ java.lang.String getSource();
+ /**
+ * optional string source = 2;
+ */
+ com.google.protobuf.ByteString
+ getSourceBytes();
+
// optional string relay = 3;
+ /**
+ * optional string relay = 3;
+ */
boolean hasRelay();
- String getRelay();
-
+ /**
+ * optional string relay = 3;
+ */
+ java.lang.String getRelay();
+ /**
+ * optional string relay = 3;
+ */
+ com.google.protobuf.ByteString
+ getRelayBytes();
+
// repeated string destinations = 4;
- java.util.List getDestinationsList();
+ /**
+ * repeated string destinations = 4;
+ */
+ java.util.List
+ getDestinationsList();
+ /**
+ * repeated string destinations = 4;
+ */
int getDestinationsCount();
- String getDestinations(int index);
-
+ /**
+ * repeated string destinations = 4;
+ */
+ java.lang.String getDestinations(int index);
+ /**
+ * repeated string destinations = 4;
+ */
+ com.google.protobuf.ByteString
+ getDestinationsBytes(int index);
+
+ // repeated uint64 destinationDeviceIds = 7;
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ java.util.List getDestinationDeviceIdsList();
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ int getDestinationDeviceIdsCount();
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ long getDestinationDeviceIds(int index);
+
// optional uint64 timestamp = 5;
+ /**
+ * optional uint64 timestamp = 5;
+ */
boolean hasTimestamp();
+ /**
+ * optional uint64 timestamp = 5;
+ */
long getTimestamp();
-
+
// optional bytes message = 6;
+ /**
+ * optional bytes message = 6;
+ */
boolean hasMessage();
+ /**
+ * optional bytes message = 6;
+ */
com.google.protobuf.ByteString getMessage();
}
+ /**
+ * Protobuf type {@code textsecure.OutgoingMessageSignal}
+ */
public static final class OutgoingMessageSignal extends
com.google.protobuf.GeneratedMessage
implements OutgoingMessageSignalOrBuilder {
// Use OutgoingMessageSignal.newBuilder() to construct.
- private OutgoingMessageSignal(Builder builder) {
+ private OutgoingMessageSignal(com.google.protobuf.GeneratedMessage.Builder> builder) {
super(builder);
+ this.unknownFields = builder.getUnknownFields();
}
- private OutgoingMessageSignal(boolean noInit) {}
-
+ private OutgoingMessageSignal(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
private static final OutgoingMessageSignal defaultInstance;
public static OutgoingMessageSignal getDefaultInstance() {
return defaultInstance;
}
-
+
public OutgoingMessageSignal getDefaultInstanceForType() {
return defaultInstance;
}
-
+
+ private final com.google.protobuf.UnknownFieldSet unknownFields;
+ @java.lang.Override
+ public final com.google.protobuf.UnknownFieldSet
+ getUnknownFields() {
+ return this.unknownFields;
+ }
+ private OutgoingMessageSignal(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ initFields();
+ int mutable_bitField0_ = 0;
+ com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+ com.google.protobuf.UnknownFieldSet.newBuilder();
+ try {
+ boolean done = false;
+ while (!done) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ done = true;
+ break;
+ default: {
+ if (!parseUnknownField(input, unknownFields,
+ extensionRegistry, tag)) {
+ done = true;
+ }
+ break;
+ }
+ case 8: {
+ bitField0_ |= 0x00000001;
+ type_ = input.readUInt32();
+ break;
+ }
+ case 18: {
+ bitField0_ |= 0x00000002;
+ source_ = input.readBytes();
+ break;
+ }
+ case 26: {
+ bitField0_ |= 0x00000004;
+ relay_ = input.readBytes();
+ break;
+ }
+ case 34: {
+ if (!((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+ destinations_ = new com.google.protobuf.LazyStringArrayList();
+ mutable_bitField0_ |= 0x00000008;
+ }
+ destinations_.add(input.readBytes());
+ break;
+ }
+ case 40: {
+ bitField0_ |= 0x00000008;
+ timestamp_ = input.readUInt64();
+ break;
+ }
+ case 50: {
+ bitField0_ |= 0x00000010;
+ message_ = input.readBytes();
+ break;
+ }
+ case 56: {
+ if (!((mutable_bitField0_ & 0x00000010) == 0x00000010)) {
+ destinationDeviceIds_ = new java.util.ArrayList();
+ mutable_bitField0_ |= 0x00000010;
+ }
+ destinationDeviceIds_.add(input.readUInt64());
+ break;
+ }
+ case 58: {
+ int length = input.readRawVarint32();
+ int limit = input.pushLimit(length);
+ if (!((mutable_bitField0_ & 0x00000010) == 0x00000010) && input.getBytesUntilLimit() > 0) {
+ destinationDeviceIds_ = new java.util.ArrayList();
+ mutable_bitField0_ |= 0x00000010;
+ }
+ while (input.getBytesUntilLimit() > 0) {
+ destinationDeviceIds_.add(input.readUInt64());
+ }
+ input.popLimit(limit);
+ break;
+ }
+ }
+ }
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw e.setUnfinishedMessage(this);
+ } catch (java.io.IOException e) {
+ throw new com.google.protobuf.InvalidProtocolBufferException(
+ e.getMessage()).setUnfinishedMessage(this);
+ } finally {
+ if (((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+ destinations_ = new com.google.protobuf.UnmodifiableLazyStringList(destinations_);
+ }
+ if (((mutable_bitField0_ & 0x00000010) == 0x00000010)) {
+ destinationDeviceIds_ = java.util.Collections.unmodifiableList(destinationDeviceIds_);
+ }
+ this.unknownFields = unknownFields.build();
+ makeExtensionsImmutable();
+ }
+ }
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
}
-
+
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
- return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
+ return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable
+ .ensureFieldAccessorsInitialized(
+ org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class);
}
-
+
+ public static com.google.protobuf.Parser PARSER =
+ new com.google.protobuf.AbstractParser() {
+ public OutgoingMessageSignal parsePartialFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return new OutgoingMessageSignal(input, extensionRegistry);
+ }
+ };
+
+ @java.lang.Override
+ public com.google.protobuf.Parser getParserForType() {
+ return PARSER;
+ }
+
private int bitField0_;
// optional uint32 type = 1;
public static final int TYPE_FIELD_NUMBER = 1;
private int type_;
+ /**
+ * optional uint32 type = 1;
+ */
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
+ /**
+ * optional uint32 type = 1;
+ */
public int getType() {
return type_;
}
-
+
// optional string source = 2;
public static final int SOURCE_FIELD_NUMBER = 2;
private java.lang.Object source_;
+ /**
+ * optional string source = 2;
+ */
public boolean hasSource() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
- public String getSource() {
+ /**
+ * optional string source = 2;
+ */
+ public java.lang.String getSource() {
java.lang.Object ref = source_;
- if (ref instanceof String) {
- return (String) ref;
+ if (ref instanceof java.lang.String) {
+ return (java.lang.String) ref;
} else {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
- String s = bs.toStringUtf8();
- if (com.google.protobuf.Internal.isValidUtf8(bs)) {
+ java.lang.String s = bs.toStringUtf8();
+ if (bs.isValidUtf8()) {
source_ = s;
}
return s;
}
}
- private com.google.protobuf.ByteString getSourceBytes() {
+ /**
+ * optional string source = 2;
+ */
+ public com.google.protobuf.ByteString
+ getSourceBytes() {
java.lang.Object ref = source_;
- if (ref instanceof String) {
+ if (ref instanceof java.lang.String) {
com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8((String) ref);
+ com.google.protobuf.ByteString.copyFromUtf8(
+ (java.lang.String) ref);
source_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
-
+
// optional string relay = 3;
public static final int RELAY_FIELD_NUMBER = 3;
private java.lang.Object relay_;
+ /**
+ * optional string relay = 3;
+ */
public boolean hasRelay() {
return ((bitField0_ & 0x00000004) == 0x00000004);
}
- public String getRelay() {
+ /**
+ * optional string relay = 3;
+ */
+ public java.lang.String getRelay() {
java.lang.Object ref = relay_;
- if (ref instanceof String) {
- return (String) ref;
+ if (ref instanceof java.lang.String) {
+ return (java.lang.String) ref;
} else {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
- String s = bs.toStringUtf8();
- if (com.google.protobuf.Internal.isValidUtf8(bs)) {
+ java.lang.String s = bs.toStringUtf8();
+ if (bs.isValidUtf8()) {
relay_ = s;
}
return s;
}
}
- private com.google.protobuf.ByteString getRelayBytes() {
+ /**
+ * optional string relay = 3;
+ */
+ public com.google.protobuf.ByteString
+ getRelayBytes() {
java.lang.Object ref = relay_;
- if (ref instanceof String) {
+ if (ref instanceof java.lang.String) {
com.google.protobuf.ByteString b =
- com.google.protobuf.ByteString.copyFromUtf8((String) ref);
+ com.google.protobuf.ByteString.copyFromUtf8(
+ (java.lang.String) ref);
relay_ = b;
return b;
} else {
return (com.google.protobuf.ByteString) ref;
}
}
-
+
// repeated string destinations = 4;
public static final int DESTINATIONS_FIELD_NUMBER = 4;
private com.google.protobuf.LazyStringList destinations_;
- public java.util.List
+ /**
+ * repeated string destinations = 4;
+ */
+ public java.util.List
getDestinationsList() {
return destinations_;
}
+ /**
+ * repeated string destinations = 4;
+ */
public int getDestinationsCount() {
return destinations_.size();
}
- public String getDestinations(int index) {
+ /**
+ * repeated string destinations = 4;
+ */
+ public java.lang.String getDestinations(int index) {
return destinations_.get(index);
}
-
+ /**
+ * repeated string destinations = 4;
+ */
+ public com.google.protobuf.ByteString
+ getDestinationsBytes(int index) {
+ return destinations_.getByteString(index);
+ }
+
+ // repeated uint64 destinationDeviceIds = 7;
+ public static final int DESTINATIONDEVICEIDS_FIELD_NUMBER = 7;
+ private java.util.List destinationDeviceIds_;
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public java.util.List
+ getDestinationDeviceIdsList() {
+ return destinationDeviceIds_;
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public int getDestinationDeviceIdsCount() {
+ return destinationDeviceIds_.size();
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public long getDestinationDeviceIds(int index) {
+ return destinationDeviceIds_.get(index);
+ }
+
// optional uint64 timestamp = 5;
public static final int TIMESTAMP_FIELD_NUMBER = 5;
private long timestamp_;
+ /**
+ * optional uint64 timestamp = 5;
+ */
public boolean hasTimestamp() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
+ /**
+ * optional uint64 timestamp = 5;
+ */
public long getTimestamp() {
return timestamp_;
}
-
+
// optional bytes message = 6;
public static final int MESSAGE_FIELD_NUMBER = 6;
private com.google.protobuf.ByteString message_;
+ /**
+ * optional bytes message = 6;
+ */
public boolean hasMessage() {
return ((bitField0_ & 0x00000010) == 0x00000010);
}
+ /**
+ * optional bytes message = 6;
+ */
public com.google.protobuf.ByteString getMessage() {
return message_;
}
-
+
private void initFields() {
type_ = 0;
source_ = "";
relay_ = "";
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
+ destinationDeviceIds_ = java.util.Collections.emptyList();
timestamp_ = 0L;
message_ = com.google.protobuf.ByteString.EMPTY;
}
@@ -185,11 +456,11 @@ public final class MessageProtos {
public final boolean isInitialized() {
byte isInitialized = memoizedIsInitialized;
if (isInitialized != -1) return isInitialized == 1;
-
+
memoizedIsInitialized = 1;
return true;
}
-
+
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
getSerializedSize();
@@ -211,14 +482,17 @@ public final class MessageProtos {
if (((bitField0_ & 0x00000010) == 0x00000010)) {
output.writeBytes(6, message_);
}
+ for (int i = 0; i < destinationDeviceIds_.size(); i++) {
+ output.writeUInt64(7, destinationDeviceIds_.get(i));
+ }
getUnknownFields().writeTo(output);
}
-
+
private int memoizedSerializedSize = -1;
public int getSerializedSize() {
int size = memoizedSerializedSize;
if (size != -1) return size;
-
+
size = 0;
if (((bitField0_ & 0x00000001) == 0x00000001)) {
size += com.google.protobuf.CodedOutputStream
@@ -249,98 +523,96 @@ public final class MessageProtos {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(6, message_);
}
+ {
+ int dataSize = 0;
+ for (int i = 0; i < destinationDeviceIds_.size(); i++) {
+ dataSize += com.google.protobuf.CodedOutputStream
+ .computeUInt64SizeNoTag(destinationDeviceIds_.get(i));
+ }
+ size += dataSize;
+ size += 1 * getDestinationDeviceIdsList().size();
+ }
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
}
-
+
private static final long serialVersionUID = 0L;
@java.lang.Override
protected java.lang.Object writeReplace()
throws java.io.ObjectStreamException {
return super.writeReplace();
}
-
+
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException {
- return newBuilder().mergeFrom(data).buildParsed();
+ return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
- return newBuilder().mergeFrom(data, extensionRegistry)
- .buildParsed();
+ return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
- return newBuilder().mergeFrom(data).buildParsed();
+ return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
- return newBuilder().mergeFrom(data, extensionRegistry)
- .buildParsed();
+ return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(java.io.InputStream input)
throws java.io.IOException {
- return newBuilder().mergeFrom(input).buildParsed();
+ return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
- return newBuilder().mergeFrom(input, extensionRegistry)
- .buildParsed();
+ return PARSER.parseFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException {
- Builder builder = newBuilder();
- if (builder.mergeDelimitedFrom(input)) {
- return builder.buildParsed();
- } else {
- return null;
- }
+ return PARSER.parseDelimitedFrom(input);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
- Builder builder = newBuilder();
- if (builder.mergeDelimitedFrom(input, extensionRegistry)) {
- return builder.buildParsed();
- } else {
- return null;
- }
+ return PARSER.parseDelimitedFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.CodedInputStream input)
throws java.io.IOException {
- return newBuilder().mergeFrom(input).buildParsed();
+ return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
- return newBuilder().mergeFrom(input, extensionRegistry)
- .buildParsed();
+ return PARSER.parseFrom(input, extensionRegistry);
}
-
+
public static Builder newBuilder() { return Builder.create(); }
public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal prototype) {
return newBuilder().mergeFrom(prototype);
}
public Builder toBuilder() { return newBuilder(this); }
-
+
@java.lang.Override
protected Builder newBuilderForType(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
+ /**
+ * Protobuf type {@code textsecure.OutgoingMessageSignal}
+ */
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder
implements org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignalOrBuilder {
@@ -348,18 +620,21 @@ public final class MessageProtos {
getDescriptor() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
}
-
+
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
- return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
+ return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable
+ .ensureFieldAccessorsInitialized(
+ org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class);
}
-
+
// Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.newBuilder()
private Builder() {
maybeForceBuilderInitialization();
}
-
- private Builder(BuilderParent parent) {
+
+ private Builder(
+ com.google.protobuf.GeneratedMessage.BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
@@ -370,7 +645,7 @@ public final class MessageProtos {
private static Builder create() {
return new Builder();
}
-
+
public Builder clear() {
super.clear();
type_ = 0;
@@ -381,26 +656,28 @@ public final class MessageProtos {
bitField0_ = (bitField0_ & ~0x00000004);
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
bitField0_ = (bitField0_ & ~0x00000008);
- timestamp_ = 0L;
+ destinationDeviceIds_ = java.util.Collections.emptyList();
bitField0_ = (bitField0_ & ~0x00000010);
- message_ = com.google.protobuf.ByteString.EMPTY;
+ timestamp_ = 0L;
bitField0_ = (bitField0_ & ~0x00000020);
+ message_ = com.google.protobuf.ByteString.EMPTY;
+ bitField0_ = (bitField0_ & ~0x00000040);
return this;
}
-
+
public Builder clone() {
return create().mergeFrom(buildPartial());
}
-
+
public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() {
- return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDescriptor();
+ return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
}
-
+
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal getDefaultInstanceForType() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance();
}
-
+
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal build() {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial();
if (!result.isInitialized()) {
@@ -408,17 +685,7 @@ public final class MessageProtos {
}
return result;
}
-
- private org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildParsed()
- throws com.google.protobuf.InvalidProtocolBufferException {
- org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial();
- if (!result.isInitialized()) {
- throw newUninitializedMessageException(
- result).asInvalidProtocolBufferException();
- }
- return result;
- }
-
+
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildPartial() {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = new org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal(this);
int from_bitField0_ = bitField0_;
@@ -441,11 +708,16 @@ public final class MessageProtos {
bitField0_ = (bitField0_ & ~0x00000008);
}
result.destinations_ = destinations_;
- if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
+ if (((bitField0_ & 0x00000010) == 0x00000010)) {
+ destinationDeviceIds_ = java.util.Collections.unmodifiableList(destinationDeviceIds_);
+ bitField0_ = (bitField0_ & ~0x00000010);
+ }
+ result.destinationDeviceIds_ = destinationDeviceIds_;
+ if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
to_bitField0_ |= 0x00000008;
}
result.timestamp_ = timestamp_;
- if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
+ if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
to_bitField0_ |= 0x00000010;
}
result.message_ = message_;
@@ -453,7 +725,7 @@ public final class MessageProtos {
onBuilt();
return result;
}
-
+
public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) {
return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal)other);
@@ -462,17 +734,21 @@ public final class MessageProtos {
return this;
}
}
-
+
public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal other) {
if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance()) return this;
if (other.hasType()) {
setType(other.getType());
}
if (other.hasSource()) {
- setSource(other.getSource());
+ bitField0_ |= 0x00000002;
+ source_ = other.source_;
+ onChanged();
}
if (other.hasRelay()) {
- setRelay(other.getRelay());
+ bitField0_ |= 0x00000004;
+ relay_ = other.relay_;
+ onChanged();
}
if (!other.destinations_.isEmpty()) {
if (destinations_.isEmpty()) {
@@ -484,6 +760,16 @@ public final class MessageProtos {
}
onChanged();
}
+ if (!other.destinationDeviceIds_.isEmpty()) {
+ if (destinationDeviceIds_.isEmpty()) {
+ destinationDeviceIds_ = other.destinationDeviceIds_;
+ bitField0_ = (bitField0_ & ~0x00000010);
+ } else {
+ ensureDestinationDeviceIdsIsMutable();
+ destinationDeviceIds_.addAll(other.destinationDeviceIds_);
+ }
+ onChanged();
+ }
if (other.hasTimestamp()) {
setTimestamp(other.getTimestamp());
}
@@ -493,107 +779,106 @@ public final class MessageProtos {
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
-
+
public final boolean isInitialized() {
return true;
}
-
+
public Builder mergeFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
- com.google.protobuf.UnknownFieldSet.Builder unknownFields =
- com.google.protobuf.UnknownFieldSet.newBuilder(
- this.getUnknownFields());
- while (true) {
- int tag = input.readTag();
- switch (tag) {
- case 0:
- this.setUnknownFields(unknownFields.build());
- onChanged();
- return this;
- default: {
- if (!parseUnknownField(input, unknownFields,
- extensionRegistry, tag)) {
- this.setUnknownFields(unknownFields.build());
- onChanged();
- return this;
- }
- break;
- }
- case 8: {
- bitField0_ |= 0x00000001;
- type_ = input.readUInt32();
- break;
- }
- case 18: {
- bitField0_ |= 0x00000002;
- source_ = input.readBytes();
- break;
- }
- case 26: {
- bitField0_ |= 0x00000004;
- relay_ = input.readBytes();
- break;
- }
- case 34: {
- ensureDestinationsIsMutable();
- destinations_.add(input.readBytes());
- break;
- }
- case 40: {
- bitField0_ |= 0x00000010;
- timestamp_ = input.readUInt64();
- break;
- }
- case 50: {
- bitField0_ |= 0x00000020;
- message_ = input.readBytes();
- break;
- }
+ org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parsedMessage = null;
+ try {
+ parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ parsedMessage = (org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) e.getUnfinishedMessage();
+ throw e;
+ } finally {
+ if (parsedMessage != null) {
+ mergeFrom(parsedMessage);
}
}
+ return this;
}
-
private int bitField0_;
-
+
// optional uint32 type = 1;
private int type_ ;
+ /**
+ * optional uint32 type = 1;
+ */
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
+ /**
+ * optional uint32 type = 1;
+ */
public int getType() {
return type_;
}
+ /**
+ * optional uint32 type = 1;
+ */
public Builder setType(int value) {
bitField0_ |= 0x00000001;
type_ = value;
onChanged();
return this;
}
+ /**
+ * optional uint32 type = 1;
+ */
public Builder clearType() {
bitField0_ = (bitField0_ & ~0x00000001);
type_ = 0;
onChanged();
return this;
}
-
+
// optional string source = 2;
private java.lang.Object source_ = "";
+ /**
+ * optional string source = 2;
+ */
public boolean hasSource() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
- public String getSource() {
+ /**
+ * optional string source = 2;
+ */
+ public java.lang.String getSource() {
java.lang.Object ref = source_;
- if (!(ref instanceof String)) {
- String s = ((com.google.protobuf.ByteString) ref).toStringUtf8();
+ if (!(ref instanceof java.lang.String)) {
+ java.lang.String s = ((com.google.protobuf.ByteString) ref)
+ .toStringUtf8();
source_ = s;
return s;
} else {
- return (String) ref;
+ return (java.lang.String) ref;
}
}
- public Builder setSource(String value) {
+ /**
+ * optional string source = 2;
+ */
+ public com.google.protobuf.ByteString
+ getSourceBytes() {
+ java.lang.Object ref = source_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8(
+ (java.lang.String) ref);
+ source_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+ /**
+ * optional string source = 2;
+ */
+ public Builder setSource(
+ java.lang.String value) {
if (value == null) {
throw new NullPointerException();
}
@@ -602,34 +887,72 @@ public final class MessageProtos {
onChanged();
return this;
}
+ /**
+ * optional string source = 2;
+ */
public Builder clearSource() {
bitField0_ = (bitField0_ & ~0x00000002);
source_ = getDefaultInstance().getSource();
onChanged();
return this;
}
- void setSource(com.google.protobuf.ByteString value) {
- bitField0_ |= 0x00000002;
+ /**
+ * optional string source = 2;
+ */
+ public Builder setSourceBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ bitField0_ |= 0x00000002;
source_ = value;
onChanged();
+ return this;
}
-
+
// optional string relay = 3;
private java.lang.Object relay_ = "";
+ /**
+ * optional string relay = 3;
+ */
public boolean hasRelay() {
return ((bitField0_ & 0x00000004) == 0x00000004);
}
- public String getRelay() {
+ /**
+ * optional string relay = 3;
+ */
+ public java.lang.String getRelay() {
java.lang.Object ref = relay_;
- if (!(ref instanceof String)) {
- String s = ((com.google.protobuf.ByteString) ref).toStringUtf8();
+ if (!(ref instanceof java.lang.String)) {
+ java.lang.String s = ((com.google.protobuf.ByteString) ref)
+ .toStringUtf8();
relay_ = s;
return s;
} else {
- return (String) ref;
+ return (java.lang.String) ref;
}
}
- public Builder setRelay(String value) {
+ /**
+ * optional string relay = 3;
+ */
+ public com.google.protobuf.ByteString
+ getRelayBytes() {
+ java.lang.Object ref = relay_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8(
+ (java.lang.String) ref);
+ relay_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+ /**
+ * optional string relay = 3;
+ */
+ public Builder setRelay(
+ java.lang.String value) {
if (value == null) {
throw new NullPointerException();
}
@@ -638,18 +961,29 @@ public final class MessageProtos {
onChanged();
return this;
}
+ /**
+ * optional string relay = 3;
+ */
public Builder clearRelay() {
bitField0_ = (bitField0_ & ~0x00000004);
relay_ = getDefaultInstance().getRelay();
onChanged();
return this;
}
- void setRelay(com.google.protobuf.ByteString value) {
- bitField0_ |= 0x00000004;
+ /**
+ * optional string relay = 3;
+ */
+ public Builder setRelayBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ bitField0_ |= 0x00000004;
relay_ = value;
onChanged();
+ return this;
}
-
+
// repeated string destinations = 4;
private com.google.protobuf.LazyStringList destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
private void ensureDestinationsIsMutable() {
@@ -658,18 +992,37 @@ public final class MessageProtos {
bitField0_ |= 0x00000008;
}
}
- public java.util.List
+ /**
+ * repeated string destinations = 4;
+ */
+ public java.util.List
getDestinationsList() {
return java.util.Collections.unmodifiableList(destinations_);
}
+ /**
+ * repeated string destinations = 4;
+ */
public int getDestinationsCount() {
return destinations_.size();
}
- public String getDestinations(int index) {
+ /**
+ * repeated string destinations = 4;
+ */
+ public java.lang.String getDestinations(int index) {
return destinations_.get(index);
}
+ /**
+ * repeated string destinations = 4;
+ */
+ public com.google.protobuf.ByteString
+ getDestinationsBytes(int index) {
+ return destinations_.getByteString(index);
+ }
+ /**
+ * repeated string destinations = 4;
+ */
public Builder setDestinations(
- int index, String value) {
+ int index, java.lang.String value) {
if (value == null) {
throw new NullPointerException();
}
@@ -678,7 +1031,11 @@ public final class MessageProtos {
onChanged();
return this;
}
- public Builder addDestinations(String value) {
+ /**
+ * repeated string destinations = 4;
+ */
+ public Builder addDestinations(
+ java.lang.String value) {
if (value == null) {
throw new NullPointerException();
}
@@ -687,87 +1044,191 @@ public final class MessageProtos {
onChanged();
return this;
}
+ /**
+ * repeated string destinations = 4;
+ */
public Builder addAllDestinations(
- java.lang.Iterable values) {
+ java.lang.Iterable values) {
ensureDestinationsIsMutable();
super.addAll(values, destinations_);
onChanged();
return this;
}
+ /**
+ * repeated string destinations = 4;
+ */
public Builder clearDestinations() {
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
bitField0_ = (bitField0_ & ~0x00000008);
onChanged();
return this;
}
- void addDestinations(com.google.protobuf.ByteString value) {
- ensureDestinationsIsMutable();
+ /**
+ * repeated string destinations = 4;
+ */
+ public Builder addDestinationsBytes(
+ com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureDestinationsIsMutable();
destinations_.add(value);
onChanged();
+ return this;
}
-
+
+ // repeated uint64 destinationDeviceIds = 7;
+ private java.util.List destinationDeviceIds_ = java.util.Collections.emptyList();
+ private void ensureDestinationDeviceIdsIsMutable() {
+ if (!((bitField0_ & 0x00000010) == 0x00000010)) {
+ destinationDeviceIds_ = new java.util.ArrayList(destinationDeviceIds_);
+ bitField0_ |= 0x00000010;
+ }
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public java.util.List
+ getDestinationDeviceIdsList() {
+ return java.util.Collections.unmodifiableList(destinationDeviceIds_);
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public int getDestinationDeviceIdsCount() {
+ return destinationDeviceIds_.size();
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public long getDestinationDeviceIds(int index) {
+ return destinationDeviceIds_.get(index);
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public Builder setDestinationDeviceIds(
+ int index, long value) {
+ ensureDestinationDeviceIdsIsMutable();
+ destinationDeviceIds_.set(index, value);
+ onChanged();
+ return this;
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public Builder addDestinationDeviceIds(long value) {
+ ensureDestinationDeviceIdsIsMutable();
+ destinationDeviceIds_.add(value);
+ onChanged();
+ return this;
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public Builder addAllDestinationDeviceIds(
+ java.lang.Iterable extends java.lang.Long> values) {
+ ensureDestinationDeviceIdsIsMutable();
+ super.addAll(values, destinationDeviceIds_);
+ onChanged();
+ return this;
+ }
+ /**
+ * repeated uint64 destinationDeviceIds = 7;
+ */
+ public Builder clearDestinationDeviceIds() {
+ destinationDeviceIds_ = java.util.Collections.emptyList();
+ bitField0_ = (bitField0_ & ~0x00000010);
+ onChanged();
+ return this;
+ }
+
// optional uint64 timestamp = 5;
private long timestamp_ ;
+ /**
+ * optional uint64 timestamp = 5;
+ */
public boolean hasTimestamp() {
- return ((bitField0_ & 0x00000010) == 0x00000010);
+ return ((bitField0_ & 0x00000020) == 0x00000020);
}
+ /**
+ * optional uint64 timestamp = 5;
+ */
public long getTimestamp() {
return timestamp_;
}
+ /**
+ * optional uint64 timestamp = 5;
+ */
public Builder setTimestamp(long value) {
- bitField0_ |= 0x00000010;
+ bitField0_ |= 0x00000020;
timestamp_ = value;
onChanged();
return this;
}
+ /**
+ * optional uint64 timestamp = 5;
+ */
public Builder clearTimestamp() {
- bitField0_ = (bitField0_ & ~0x00000010);
+ bitField0_ = (bitField0_ & ~0x00000020);
timestamp_ = 0L;
onChanged();
return this;
}
-
+
// optional bytes message = 6;
private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY;
+ /**
+ * optional bytes message = 6;
+ */
public boolean hasMessage() {
- return ((bitField0_ & 0x00000020) == 0x00000020);
+ return ((bitField0_ & 0x00000040) == 0x00000040);
}
+ /**
+ * optional bytes message = 6;
+ */
public com.google.protobuf.ByteString getMessage() {
return message_;
}
+ /**
+ * optional bytes message = 6;
+ */
public Builder setMessage(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
- bitField0_ |= 0x00000020;
+ bitField0_ |= 0x00000040;
message_ = value;
onChanged();
return this;
}
+ /**
+ * optional bytes message = 6;
+ */
public Builder clearMessage() {
- bitField0_ = (bitField0_ & ~0x00000020);
+ bitField0_ = (bitField0_ & ~0x00000040);
message_ = getDefaultInstance().getMessage();
onChanged();
return this;
}
-
+
// @@protoc_insertion_point(builder_scope:textsecure.OutgoingMessageSignal)
}
-
+
static {
defaultInstance = new OutgoingMessageSignal(true);
defaultInstance.initFields();
}
-
+
// @@protoc_insertion_point(class_scope:textsecure.OutgoingMessageSignal)
}
-
+
private static com.google.protobuf.Descriptors.Descriptor
internal_static_textsecure_OutgoingMessageSignal_descriptor;
private static
com.google.protobuf.GeneratedMessage.FieldAccessorTable
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
-
+
public static com.google.protobuf.Descriptors.FileDescriptor
getDescriptor() {
return descriptor;
@@ -777,11 +1238,12 @@ public final class MessageProtos {
static {
java.lang.String[] descriptorData = {
"\n\033OutgoingMessageSignal.proto\022\ntextsecur" +
- "e\"~\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(\r" +
- "\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022\024\n\014desti" +
- "nations\030\004 \003(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mess" +
- "age\030\006 \001(\014B:\n)org.whispersystems.textsecu" +
- "regcm.entitiesB\rMessageProtos"
+ "e\"\234\001\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(" +
+ "\r\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022\024\n\014dest" +
+ "inations\030\004 \003(\t\022\034\n\024destinationDeviceIds\030\007" +
+ " \003(\004\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007message\030\006 \001(\014" +
+ "B:\n)org.whispersystems.textsecuregcm.ent" +
+ "itiesB\rMessageProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@@ -793,9 +1255,7 @@ public final class MessageProtos {
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_OutgoingMessageSignal_descriptor,
- new java.lang.String[] { "Type", "Source", "Relay", "Destinations", "Timestamp", "Message", },
- org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class,
- org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class);
+ new java.lang.String[] { "Type", "Source", "Relay", "Destinations", "DestinationDeviceIds", "Timestamp", "Message", });
return null;
}
};
@@ -804,6 +1264,6 @@ public final class MessageProtos {
new com.google.protobuf.Descriptors.FileDescriptor[] {
}, assigner);
}
-
+
// @@protoc_insertion_point(outer_class_scope)
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java
index 69a0cae56..7c589b4fc 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java
@@ -34,6 +34,10 @@ public class PreKey {
@JsonIgnore
private String number;
+ @JsonProperty
+ @NotNull
+ private long deviceId;
+
@JsonProperty
@NotNull
private long keyId;
@@ -51,12 +55,13 @@ public class PreKey {
public PreKey() {}
- public PreKey(long id, String number, long keyId,
+ public PreKey(long id, String number, long deviceId, long keyId,
String publicKey, String identityKey,
boolean lastResort)
{
this.id = id;
this.number = number;
+ this.deviceId = deviceId;
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
@@ -113,4 +118,12 @@ public class PreKey {
public void setLastResort(boolean lastResort) {
this.lastResort = lastResort;
}
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ public long getDeviceId() {
+ return deviceId;
+ }
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java b/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java
index 4a8ccb2f8..411eb5d8f 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java
@@ -32,6 +32,10 @@ public class RelayMessage {
@NotEmpty
private String destination;
+ @JsonProperty
+ @NotEmpty
+ private long destinationDeviceId;
+
@JsonProperty
@NotNull
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@@ -40,7 +44,7 @@ public class RelayMessage {
public RelayMessage() {}
- public RelayMessage(String destination, byte[] outgoingMessageSignal) {
+ public RelayMessage(String destination, long destinationDeviceId, byte[] outgoingMessageSignal) {
this.destination = destination;
this.outgoingMessageSignal = outgoingMessageSignal;
}
@@ -49,6 +53,10 @@ public class RelayMessage {
return destination;
}
+ public long getDestinationDeviceId() {
+ return destinationDeviceId;
+ }
+
public byte[] getOutgoingMessageSignal() {
return outgoingMessageSignal;
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java b/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java
new file mode 100644
index 000000000..016d431cd
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (C) 2013 Open WhisperSystems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.whispersystems.textsecuregcm.entities;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.annotations.VisibleForTesting;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.Iterator;
+import java.util.List;
+
+public class UnstructuredPreKeyList {
+ @JsonProperty
+ @NotNull
+ @Valid
+ private List keys;
+
+ public UnstructuredPreKeyList(List preKeys) {
+ this.keys = preKeys;
+ }
+
+ public List getKeys() {
+ return keys;
+ }
+
+ @VisibleForTesting public boolean equals(Object o) {
+ if (!(o instanceof UnstructuredPreKeyList) ||
+ ((UnstructuredPreKeyList) o).keys.size() != keys.size())
+ return false;
+ Iterator otherKeys = ((UnstructuredPreKeyList) o).keys.iterator();
+ for (PreKey key : keys) {
+ if (!otherKeys.next().equals(key))
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java
index 19a75a132..a008e43e3 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java
@@ -38,6 +38,7 @@ 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.util.Base64;
import javax.net.ssl.SSLContext;
@@ -99,12 +100,12 @@ public class FederatedClient {
}
}
- public PreKey getKey(String destination) {
+ public UnstructuredPreKeyList getKeys(String destination) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination));
return resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
- .get(PreKey.class);
+ .get(UnstructuredPreKeyList.class);
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e);
return null;
@@ -139,14 +140,14 @@ public class FederatedClient {
}
}
- public void sendMessage(String destination, OutgoingMessageSignal message)
+ public void sendMessage(String destination, long destinationDeviceId, OutgoingMessageSignal message)
throws IOException, NoSuchUserException
{
try {
WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH);
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
- .entity(new RelayMessage(destination, message.toByteArray()))
+ .entity(new RelayMessage(destination, destinationDeviceId, message.toByteArray()))
.put(ClientResponse.class);
if (response.getStatus() == 404) {
diff --git a/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java b/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java
index a440f6c0d..f0054baf2 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java
@@ -27,11 +27,13 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
+import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
+import java.util.List;
public class PushSender {
@@ -42,9 +44,11 @@ public class PushSender {
private final GCMSender gcmSender;
private final APNSender apnSender;
+ private final StoredMessageManager storedMessageManager;
public PushSender(GcmConfiguration gcmConfiguration,
ApnConfiguration apnConfiguration,
+ StoredMessageManager storedMessageManager,
AccountsManager accounts,
DirectoryManager directory)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
@@ -52,25 +56,27 @@ public class PushSender {
this.accounts = accounts;
this.directory = directory;
- this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
- this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
+ this.storedMessageManager = storedMessageManager;
+ this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
+ this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
}
- public void sendMessage(String destination, MessageProtos.OutgoingMessageSignal outgoingMessage)
+ public void sendMessage(String destination, long destinationDeviceId, MessageProtos.OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
- Optional account = accounts.get(destination);
+ Optional accountOptional = accounts.get(destination, destinationDeviceId);
- if (!account.isPresent()) {
- directory.remove(destination);
+ if (!accountOptional.isPresent()) {
throw new NoSuchUserException("No such local destination: " + destination);
}
+ Account account = accountOptional.get();
- String signalingKey = account.get().getSignalingKey();
+ String signalingKey = account.getSignalingKey();
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
- if (account.get().getGcmRegistrationId() != null) sendGcmMessage(account.get(), message);
- else if (account.get().getApnRegistrationId() != null) sendApnMessage(account.get(), message);
+ if (account.getGcmRegistrationId() != null) sendGcmMessage(account, message);
+ else if (account.getApnRegistrationId() != null) sendApnMessage(account, message);
+ else if (account.getFetchesMessages()) storeFetchedMessage(account, message);
else throw new NoSuchUserException("No push identifier!");
}
@@ -100,4 +106,7 @@ public class PushSender {
apnSender.sendMessage(account.getApnRegistrationId(), outgoingMessage);
}
+ private void storeFetchedMessage(Account account, EncryptedOutgoingMessage outgoingMessage) {
+ storedMessageManager.storeMessage(account, outgoingMessage);
+ }
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java
index 41ff1c428..f16bc1d23 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java
@@ -27,27 +27,36 @@ public class Account implements Serializable {
private long id;
private String number;
+ private long deviceId;
private String hashedAuthenticationToken;
private String salt;
private String signalingKey;
+ /**
+ * In order for us to tell a client that an account is "inactive" (ie go use SMS for transport), we check that all
+ * non-fetching Accounts don't have push registrations. In this way, we can ensure that we have some form of transport
+ * available for all Accounts on all "active" numbers.
+ */
private String gcmRegistrationId;
private String apnRegistrationId;
private boolean supportsSms;
+ private boolean fetchesMessages;
public Account() {}
- public Account(long id, String number, String hashedAuthenticationToken, String salt,
+ public Account(long id, String number, long deviceId, String hashedAuthenticationToken, String salt,
String signalingKey, String gcmRegistrationId, String apnRegistrationId,
- boolean supportsSms)
+ boolean supportsSms, boolean fetchesMessages)
{
this.id = id;
this.number = number;
+ this.deviceId = deviceId;
this.hashedAuthenticationToken = hashedAuthenticationToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmRegistrationId = gcmRegistrationId;
this.apnRegistrationId = apnRegistrationId;
this.supportsSms = supportsSms;
+ this.fetchesMessages = fetchesMessages;
}
public String getApnRegistrationId() {
@@ -74,6 +83,14 @@ public class Account implements Serializable {
return number;
}
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
@@ -106,4 +123,12 @@ public class Account implements Serializable {
public void setId(long id) {
this.id = id;
}
+
+ public void setFetchesMessages(boolean fetchesMessages) {
+ this.fetchesMessages = fetchesMessages;
+ }
+
+ public boolean getFetchesMessages() {
+ return fetchesMessages;
+ }
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java
index ba941846f..3ace80c17 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java
@@ -29,6 +29,7 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
+import org.whispersystems.textsecuregcm.util.NumberData;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
@@ -42,50 +43,76 @@ import java.util.List;
public abstract class Accounts {
- public static final String ID = "id";
- public static final String NUMBER = "number";
- public static final String AUTH_TOKEN = "auth_token";
- public static final String SALT = "salt";
- public static final String SIGNALING_KEY = "signaling_key";
- public static final String GCM_ID = "gcm_id";
- public static final String APN_ID = "apn_id";
- public static final String SUPPORTS_SMS = "supports_sms";
+ public static final String ID = "id";
+ public static final String NUMBER = "number";
+ public static final String DEVICE_ID = "device_id";
+ public static final String AUTH_TOKEN = "auth_token";
+ public static final String SALT = "salt";
+ public static final String SIGNALING_KEY = "signaling_key";
+ public static final String GCM_ID = "gcm_id";
+ public static final String APN_ID = "apn_id";
+ public static final String FETCHES_MESSAGES = "fetches_messages";
+ public static final String SUPPORTS_SMS = "supports_sms";
- @SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + AUTH_TOKEN + ", " +
- SALT + ", " + SIGNALING_KEY + ", " + GCM_ID + ", " +
- APN_ID + ", " + SUPPORTS_SMS + ") " +
- "VALUES (:number, :auth_token, :salt, :signaling_key, :gcm_id, :apn_id, :supports_sms)")
+ @SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DEVICE_ID + ", " + AUTH_TOKEN + ", " +
+ SALT + ", " + SIGNALING_KEY + ", " + FETCHES_MESSAGES + ", " +
+ GCM_ID + ", " + APN_ID + ", " + SUPPORTS_SMS + ") " +
+ "VALUES (:number, :device_id, :auth_token, :salt, :signaling_key, :fetches_messages, :gcm_id, :apn_id, :supports_sms)")
@GetGeneratedKeys
- abstract long createStep(@AccountBinder Account account);
+ abstract long insertStep(@AccountBinder Account account);
- @SqlUpdate("DELETE FROM accounts WHERE number = :number")
- abstract void removeStep(@Bind("number") String number);
+ @SqlQuery("SELECT " + DEVICE_ID + " FROM accounts WHERE " + NUMBER + " = :number ORDER BY " + DEVICE_ID + " DESC LIMIT 1 FOR UPDATE")
+ abstract long getHighestDeviceId(@Bind("number") String number);
+
+ @Transaction(TransactionIsolationLevel.SERIALIZABLE)
+ public long insert(@AccountBinder Account account) {
+ account.setDeviceId(getHighestDeviceId(account.getNumber()) + 1);
+ return insertStep(account);
+ }
+
+ @SqlUpdate("DELETE FROM accounts WHERE " + NUMBER + " = :number RETURNING id")
+ abstract void removeAccountsByNumber(@Bind("number") String number);
@SqlUpdate("UPDATE accounts SET " + AUTH_TOKEN + " = :auth_token, " + SALT + " = :salt, " +
- SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " +
- APN_ID + " = :apn_id, " + SUPPORTS_SMS + " = :supports_sms " +
- "WHERE " + NUMBER + " = :number")
+ SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " + APN_ID + " = :apn_id, " +
+ FETCHES_MESSAGES + " = :fetches_messages, " + SUPPORTS_SMS + " = :supports_sms " +
+ "WHERE " + NUMBER + " = :number AND " + DEVICE_ID + " = :device_id")
abstract void update(@AccountBinder Account account);
+ @Mapper(AccountMapper.class)
+ @SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number AND " + DEVICE_ID + " = :device_id")
+ abstract Account get(@Bind("number") String number, @Bind("device_id") long deviceId);
+
+ @SqlQuery("SELECT COUNT(DISTINCT " + NUMBER + ") from accounts")
+ abstract long getNumberCount();
+
+ private static final String NUMBER_DATA_QUERY = "SELECT number, COUNT(" +
+ "CASE WHEN (" + GCM_ID + " IS NOT NULL OR " + APN_ID + " IS NOT NULL OR " + FETCHES_MESSAGES + " = 1) " +
+ "THEN 1 ELSE 0 END) AS active, COUNT(" +
+ "CASE WHEN " + SUPPORTS_SMS + " = 1 THEN 1 ELSE 0 END) AS " + SUPPORTS_SMS + " " +
+ "FROM accounts";
+
+ @Mapper(NumberDataMapper.class)
+ @SqlQuery(NUMBER_DATA_QUERY + " GROUP BY " + NUMBER + " OFFSET :offset LIMIT :limit")
+ abstract List getAllNumbers(@Bind("offset") int offset, @Bind("limit") int length);
+
+ @Mapper(NumberDataMapper.class)
+ @SqlQuery(NUMBER_DATA_QUERY + " GROUP BY " + NUMBER)
+ public abstract Iterator getAllNumbers();
+
+ @Mapper(NumberDataMapper.class)
+ @SqlQuery(NUMBER_DATA_QUERY + " WHERE " + NUMBER + " = :number GROUP BY " + NUMBER)
+ abstract NumberData getNumberData(@Bind("number") String number);
+
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number")
- abstract Account get(@Bind("number") String number);
+ public abstract List getAllByNumber(@Bind("number") String number);
- @SqlQuery("SELECT COUNT(*) from accounts")
- abstract long getCount();
-
- @Mapper(AccountMapper.class)
- @SqlQuery("SELECT * FROM accounts OFFSET :offset LIMIT :limit")
- abstract List getAll(@Bind("offset") int offset, @Bind("limit") int length);
-
- @Mapper(AccountMapper.class)
- @SqlQuery("SELECT * FROM accounts")
- abstract Iterator getAll();
-
- @Transaction(TransactionIsolationLevel.REPEATABLE_READ)
- public long create(Account account) {
- removeStep(account.getNumber());
- return createStep(account);
+ @Transaction(TransactionIsolationLevel.SERIALIZABLE)
+ public long insertClearingNumber(Account account) {
+ removeAccountsByNumber(account.getNumber());
+ account.setDeviceId(getHighestDeviceId(account.getNumber()) + 1);
+ return insertStep(account);
}
public static class AccountMapper implements ResultSetMapper {
@@ -94,11 +121,21 @@ public abstract class Accounts {
public Account map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
- return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER),
+ return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER), resultSet.getLong(DEVICE_ID),
resultSet.getString(AUTH_TOKEN), resultSet.getString(SALT),
resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID),
resultSet.getString(APN_ID),
- resultSet.getInt(SUPPORTS_SMS) == 1);
+ resultSet.getInt(SUPPORTS_SMS) == 1, resultSet.getInt(FETCHES_MESSAGES) == 1);
+ }
+ }
+
+ public static class NumberDataMapper implements ResultSetMapper {
+
+ @Override
+ public NumberData map(int i, ResultSet resultSet, StatementContext statementContext)
+ throws SQLException
+ {
+ return new NumberData(resultSet.getString("number"), resultSet.getInt("active") != 0, resultSet.getInt(SUPPORTS_SMS) != 0);
}
}
@@ -117,6 +154,7 @@ public abstract class Accounts {
{
sql.bind(ID, account.getId());
sql.bind(NUMBER, account.getNumber());
+ sql.bind(DEVICE_ID, account.getDeviceId());
sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials()
.getHashedAuthenticationToken());
sql.bind(SALT, account.getAuthenticationCredentials().getSalt());
@@ -124,6 +162,7 @@ public abstract class Accounts {
sql.bind(GCM_ID, account.getGcmRegistrationId());
sql.bind(APN_ID, account.getApnRegistrationId());
sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0);
+ sql.bind(FETCHES_MESSAGES, account.getFetchesMessages() ? 1 : 0);
}
};
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java
index adfc87285..19ec7d78b 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java
@@ -20,6 +20,7 @@ package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.entities.ClientContact;
+import org.whispersystems.textsecuregcm.util.NumberData;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Iterator;
@@ -41,24 +42,36 @@ public class AccountsManager {
}
public long getCount() {
- return accounts.getCount();
+ return accounts.getNumberCount();
}
- public List getAll(int offset, int length) {
- return accounts.getAll(offset, length);
+ public List getAllNumbers(int offset, int length) {
+ return accounts.getAllNumbers(offset, length);
}
- public Iterator getAll() {
- return accounts.getAll();
+ public Iterator getAllNumbers() {
+ return accounts.getAllNumbers();
}
- public void create(Account account) {
- long id = accounts.create(account);
-
+ /** Creates a new Account and NumberData, clearing all existing accounts/data on the given number */
+ public void createResetNumber(Account account) {
+ long id = accounts.insertClearingNumber(account);
account.setId(id);
if (memcachedClient != null) {
- memcachedClient.set(getKey(account.getNumber()), 0, account);
+ memcachedClient.set(getKey(account.getNumber(), account.getDeviceId()), 0, account);
+ }
+
+ updateDirectory(account);
+ }
+
+ /** Creates a new Account for an existing NumberData (setting the deviceId) */
+ public void createAccountOnExistingNumber(Account account) {
+ long id = accounts.insert(account);
+ account.setId(id);
+
+ if (memcachedClient != null) {
+ memcachedClient.set(getKey(account.getNumber(), account.getDeviceId()), 0, account);
}
updateDirectory(account);
@@ -66,25 +79,25 @@ public class AccountsManager {
public void update(Account account) {
if (memcachedClient != null) {
- memcachedClient.set(getKey(account.getNumber()), 0, account);
+ memcachedClient.set(getKey(account.getNumber(), account.getDeviceId()), 0, account);
}
accounts.update(account);
updateDirectory(account);
}
- public Optional get(String number) {
+ public Optional get(String number, long deviceId) {
Account account = null;
if (memcachedClient != null) {
- account = (Account)memcachedClient.get(getKey(number));
+ account = (Account)memcachedClient.get(getKey(number, deviceId));
}
if (account == null) {
- account = accounts.get(number);
+ account = accounts.get(number, deviceId);
if (account != null && memcachedClient != null) {
- memcachedClient.set(getKey(number), 0, account);
+ memcachedClient.set(getKey(number, deviceId), 0, account);
}
}
@@ -92,17 +105,31 @@ public class AccountsManager {
else return Optional.absent();
}
+ public List getAllByNumber(String number) {
+ return accounts.getAllByNumber(number);
+ }
+
private void updateDirectory(Account account) {
- if (account.getGcmRegistrationId() != null || account.getApnRegistrationId() != null) {
+ boolean active = account.getFetchesMessages() ||
+ !Util.isEmpty(account.getApnRegistrationId()) || !Util.isEmpty(account.getGcmRegistrationId());
+ boolean supportsSms = account.getSupportsSms();
+
+ if (!active || !supportsSms) {
+ NumberData numberData = accounts.getNumberData(account.getNumber());
+ active = numberData.isActive();
+ supportsSms = numberData.isSupportsSms();
+ }
+
+ if (active) {
byte[] token = Util.getContactToken(account.getNumber());
- ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
+ ClientContact clientContact = new ClientContact(token, null, supportsSms);
directory.add(clientContact);
} else {
directory.remove(account.getNumber());
}
}
- private String getKey(String number) {
- return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
+ private String getKey(String number, long accountId) {
+ return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number + accountId;
}
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java
index 16cf88d2b..772a66eab 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java
@@ -30,6 +30,7 @@ import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.PreKey;
+import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
@@ -38,48 +39,60 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.LinkedList;
import java.util.List;
public abstract class Keys {
- @SqlUpdate("DELETE FROM keys WHERE number = :number")
- abstract void removeKeys(@Bind("number") String number);
+ @SqlUpdate("DELETE FROM keys WHERE number = :number AND device_id = :device_id")
+ abstract void removeKeys(@Bind("number") String number, @Bind("device_id") long deviceId);
@SqlUpdate("DELETE FROM keys WHERE id = :id")
abstract void removeKey(@Bind("id") long id);
- @SqlBatch("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
+ @SqlBatch("INSERT INTO keys (number, device_id, key_id, public_key, identity_key, last_resort) VALUES " +
+ "(:number, :device_id, :key_id, :public_key, :identity_key, :last_resort)")
abstract void append(@PreKeyBinder List preKeys);
- @SqlUpdate("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
+ @SqlUpdate("INSERT INTO keys (number, device_id, key_id, public_key, identity_key, last_resort) VALUES " +
+ "(:number, :device_id, :key_id, :public_key, :identity_key, :last_resort)")
abstract void append(@PreKeyBinder PreKey preKey);
- @SqlQuery("SELECT * FROM keys WHERE number = :number ORDER BY id LIMIT 1 FOR UPDATE")
+ @SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE")
@Mapper(PreKeyMapper.class)
- abstract PreKey retrieveFirst(@Bind("number") String number);
+ abstract PreKey retrieveFirst(@Bind("number") String number, @Bind("device_id") long deviceId);
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
- public void store(String number, PreKey lastResortKey, List keys) {
+ public void store(String number, long deviceId, PreKey lastResortKey, List keys) {
for (PreKey key : keys) {
key.setNumber(number);
+ key.setDeviceId(deviceId);
}
lastResortKey.setNumber(number);
+ lastResortKey.setDeviceId(deviceId);
+ lastResortKey.setLastResort(true);
- removeKeys(number);
+ removeKeys(number, deviceId);
append(keys);
append(lastResortKey);
}
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
- public PreKey get(String number) {
- PreKey preKey = retrieveFirst(number);
-
- if (preKey != null && !preKey.isLastResort()) {
- removeKey(preKey.getId());
+ public UnstructuredPreKeyList get(String number, List accounts) {
+ List preKeys = new LinkedList<>();
+ for (Account account : accounts) {
+ PreKey preKey = retrieveFirst(number, account.getDeviceId());
+ if (preKey != null)
+ preKeys.add(preKey);
}
- return preKey;
+ for (PreKey preKey : preKeys) {
+ if (!preKey.isLastResort())
+ removeKey(preKey.getId());
+ }
+
+ return new UnstructuredPreKeyList(preKeys);
}
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
@@ -95,6 +108,7 @@ public abstract class Keys {
{
sql.bind("id", preKey.getId());
sql.bind("number", preKey.getNumber());
+ sql.bind("device_id", preKey.getDeviceId());
sql.bind("key_id", preKey.getKeyId());
sql.bind("public_key", preKey.getPublicKey());
sql.bind("identity_key", preKey.getIdentityKey());
@@ -111,7 +125,7 @@ public abstract class Keys {
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
- return new PreKey(resultSet.getLong("id"), resultSet.getString("number"),
+ return new PreKey(resultSet.getLong("id"), resultSet.getString("number"), resultSet.getLong("device_id"),
resultSet.getLong("key_id"), resultSet.getString("public_key"),
resultSet.getString("identity_key"),
resultSet.getInt("last_resort") == 1);
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java
index f54c165e7..bd1a374e5 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java
@@ -29,4 +29,6 @@ public interface PendingAccounts {
@SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number")
String getCodeForNumber(@Bind("number") String number);
+ @SqlUpdate("DELETE FROM pending_accounts WHERE number = :number")
+ void remove(@Bind("number") String number);
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java
index 306fda280..a6eddf026 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java
@@ -41,6 +41,12 @@ public class PendingAccountsManager {
pendingAccounts.insert(number, code);
}
+ public void remove(String number) {
+ if (memcachedClient != null)
+ memcachedClient.delete(MEMCACHE_PREFIX + number);
+ pendingAccounts.remove(number);
+ }
+
public Optional getCodeForNumber(String number) {
String code = null;
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDeviceRegistrations.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDeviceRegistrations.java
new file mode 100644
index 000000000..1db4db6cb
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDeviceRegistrations.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2013 Open WhisperSystems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.whispersystems.textsecuregcm.storage;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+public interface PendingDeviceRegistrations {
+
+ @SqlUpdate("WITH upsert AS (UPDATE pending_devices SET verification_code = :verification_code WHERE number = :number RETURNING *) " +
+ "INSERT INTO pending_devices (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)")
+ void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode);
+
+ @SqlQuery("SELECT verification_code FROM pending_devices WHERE number = :number")
+ String getCodeForNumber(@Bind("number") String number);
+
+ @SqlUpdate("DELETE FROM pending_devices WHERE number = :number")
+ void remove(@Bind("number") String number);
+}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java
new file mode 100644
index 000000000..d67924c16
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (C) 2013 Open WhisperSystems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.whispersystems.textsecuregcm.storage;
+
+import com.google.common.base.Optional;
+import net.spy.memcached.MemcachedClient;
+
+public class PendingDevicesManager {
+
+ private static final String MEMCACHE_PREFIX = "pending_devices";
+
+ private final PendingDeviceRegistrations pendingDevices;
+ private final MemcachedClient memcachedClient;
+
+ public PendingDevicesManager(PendingDeviceRegistrations pendingDevices,
+ MemcachedClient memcachedClient)
+ {
+ this.pendingDevices = pendingDevices;
+ this.memcachedClient = memcachedClient;
+ }
+
+ public void store(String number, String code) {
+ if (memcachedClient != null) {
+ memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
+ }
+
+ pendingDevices.insert(number, code);
+ }
+
+ public void remove(String number) {
+ if (memcachedClient != null)
+ memcachedClient.delete(MEMCACHE_PREFIX + number);
+ pendingDevices.remove(number);
+ }
+
+ public Optional getCodeForNumber(String number) {
+ String code = null;
+
+ if (memcachedClient != null) {
+ code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
+ }
+
+ if (code == null) {
+ code = pendingDevices.getCodeForNumber(number);
+
+ if (code != null && memcachedClient != null) {
+ memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
+ }
+ }
+
+ if (code != null) return Optional.of(code);
+ else return Optional.absent();
+ }
+}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessageManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessageManager.java
new file mode 100644
index 000000000..d9d563bac
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessageManager.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (C) 2013 Open WhisperSystems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.whispersystems.textsecuregcm.storage;
+
+import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
+
+public class StoredMessageManager {
+ StoredMessages storedMessages;
+ public StoredMessageManager(StoredMessages storedMessages) {
+ this.storedMessages = storedMessages;
+ }
+
+ public void storeMessage(Account account, EncryptedOutgoingMessage outgoingMessage) {
+ storedMessages.insert(account.getId(), outgoingMessage);
+ }
+}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessages.java b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessages.java
new file mode 100644
index 000000000..361771b63
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessages.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (C) 2013 Open WhisperSystems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.whispersystems.textsecuregcm.storage;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
+
+import java.util.List;
+
+public interface StoredMessages {
+
+ @SqlUpdate("INSERT INTO stored_messages (destination_id, encrypted_message) VALUES :destination_id, :encrypted_message")
+ void insert(@Bind("destination_id") long destinationAccountId, @Bind("encrypted_message") EncryptedOutgoingMessage encryptedOutgoingMessage);
+
+ @SqlQuery("SELECT encrypted_message FROM stored_messages WHERE destination_id = :account_id")
+ List getMessagesForAccountId(@Bind("account_id") long accountId);
+}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/util/NumberData.java b/src/main/java/org/whispersystems/textsecuregcm/util/NumberData.java
new file mode 100644
index 000000000..8fd446e61
--- /dev/null
+++ b/src/main/java/org/whispersystems/textsecuregcm/util/NumberData.java
@@ -0,0 +1,25 @@
+package org.whispersystems.textsecuregcm.util;
+
+public class NumberData {
+ private String number;
+ private boolean active;
+ private boolean supportsSms;
+
+ public NumberData(String number, boolean active, boolean supportsSms) {
+ this.number = number;
+ this.active = active;
+ this.supportsSms = supportsSms;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public boolean isSupportsSms() {
+ return supportsSms;
+ }
+
+ public String getNumber() {
+ return number;
+ }
+}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java b/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java
index e77b38770..41d56cad9 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java
@@ -16,12 +16,24 @@
*/
package org.whispersystems.textsecuregcm.util;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.google.common.annotations.VisibleForTesting;
+
public class VerificationCode {
+ @JsonProperty
private String verificationCode;
+ @JsonIgnore
private String verificationCodeDisplay;
+ @JsonIgnore
private String verificationCodeSpeech;
+ @VisibleForTesting VerificationCode() {}
+
public VerificationCode(int verificationCode) {
this.verificationCode = verificationCode + "";
this.verificationCodeDisplay = this.verificationCode.substring(0, 3) + "-" +
@@ -54,4 +66,7 @@ public class VerificationCode {
return delimited;
}
+ @VisibleForTesting public boolean equals(Object o) {
+ return o instanceof VerificationCode && verificationCode.equals(((VerificationCode) o).verificationCode);
+ }
}
diff --git a/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java b/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java
index b489b2a59..916d70104 100644
--- a/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java
+++ b/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java
@@ -27,6 +27,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.util.Base64;
+import org.whispersystems.textsecuregcm.util.NumberData;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Iterator;
@@ -53,22 +54,22 @@ public class DirectoryUpdater {
BatchOperationHandle batchOperation = directory.startBatchOperation();
try {
- Iterator accounts = accountsManager.getAll();
+ Iterator numbers = accountsManager.getAllNumbers();
- if (accounts == null)
+ if (numbers == null)
return;
- while (accounts.hasNext()) {
- Account account = accounts.next();
- if (account.getApnRegistrationId() != null || account.getGcmRegistrationId() != null) {
- byte[] token = Util.getContactToken(account.getNumber());
- ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
+ while (numbers.hasNext()) {
+ NumberData number = numbers.next();
+ if (number.isActive()) {
+ byte[] token = Util.getContactToken(number.getNumber());
+ ClientContact clientContact = new ClientContact(token, null, number.isSupportsSms());
directory.add(batchOperation, clientContact);
logger.debug("Adding local token: " + Base64.encodeBytesWithoutPadding(token));
} else {
- directory.remove(batchOperation, account.getNumber());
+ directory.remove(batchOperation, number.getNumber());
}
}
} finally {
diff --git a/src/main/resources/migrations.xml b/src/main/resources/migrations.xml
index 8f4ddf51a..cf9e162aa 100644
--- a/src/main/resources/migrations.xml
+++ b/src/main/resources/migrations.xml
@@ -75,4 +75,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java
index fee459848..38ce3dab3 100644
--- a/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java
+++ b/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java
@@ -1,9 +1,15 @@
package org.whispersystems.textsecuregcm.tests.controllers;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse;
import com.yammer.dropwizard.testing.ResourceTest;
+import org.hibernate.validator.constraints.NotEmpty;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
@@ -12,23 +18,54 @@ import org.whispersystems.textsecuregcm.sms.SmsSender;
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.tests.util.AuthHelper;
+import org.whispersystems.textsecuregcm.util.VerificationCode;
+import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import static org.fest.assertions.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
public class AccountControllerTest extends ResourceTest {
+ /** The AccountAttributes used in protocol v1 (no fetchesMessages) */
+ static class V1AccountAttributes {
+ @JsonProperty
+ @NotEmpty
+ private String signalingKey;
+
+ @JsonProperty
+ private boolean supportsSms;
+
+ public V1AccountAttributes(String signalingKey, boolean supportsSms) {
+ this.signalingKey = signalingKey;
+ this.supportsSms = supportsSms;
+ }
+ }
+
+ @Path("/v1/accounts")
+ static class DumbVerificationAccountController extends AccountController {
+ public DumbVerificationAccountController(PendingAccountsManager pendingAccounts, PendingDevicesManager pendingDevices, AccountsManager accounts, RateLimiters rateLimiters, SmsSender smsSenderFactory) {
+ super(pendingAccounts, pendingDevices, accounts, rateLimiters, smsSenderFactory);
+ }
+
+ @Override
+ protected VerificationCode generateVerificationCode() {
+ return new VerificationCode(5678901);
+ }
+ }
private static final String SENDER = "+14152222222";
- private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
- private AccountsManager accountsManager = mock(AccountsManager.class );
- private RateLimiters rateLimiters = mock(RateLimiters.class );
- private RateLimiter rateLimiter = mock(RateLimiter.class );
- private SmsSender smsSender = mock(SmsSender.class );
+ private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
+ private PendingDevicesManager pendingDevicesManager = mock(PendingDevicesManager.class);
+ private AccountsManager accountsManager = mock(AccountsManager.class );
+ private RateLimiters rateLimiters = mock(RateLimiters.class );
+ private RateLimiter rateLimiter = mock(RateLimiter.class );
+ private SmsSender smsSender = mock(SmsSender.class );
@Override
protected void setUpResources() throws Exception {
@@ -40,10 +77,17 @@ public class AccountControllerTest extends ResourceTest {
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
- addResource(new AccountController(pendingAccountsManager,
- accountsManager,
- rateLimiters,
- smsSender));
+ when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901"));
+
+ Mockito.doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ ((Account)invocation.getArguments()[0]).setDeviceId(2);
+ return null;
+ }
+ }).when(accountsManager).createAccountOnExistingNumber(any(Account.class));
+
+ addResource(new DumbVerificationAccountController(pendingAccountsManager, pendingDevicesManager, accountsManager, rateLimiters, smsSender));
}
@Test
@@ -62,13 +106,17 @@ public class AccountControllerTest extends ResourceTest {
ClientResponse response =
client().resource(String.format("/v1/accounts/code/%s", "1234"))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
- .entity(new AccountAttributes("keykeykeykey", false))
+ .entity(new V1AccountAttributes("keykeykeykey", false))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(204);
- verify(accountsManager).create(isA(Account.class));
+ verify(accountsManager).createResetNumber(isA(Account.class));
+
+ ArgumentCaptor number = ArgumentCaptor.forClass(String.class);
+ verify(pendingAccountsManager).remove(number.capture());
+ assertThat(number.getValue()).isEqualTo(SENDER);
}
@Test
@@ -76,7 +124,7 @@ public class AccountControllerTest extends ResourceTest {
ClientResponse response =
client().resource(String.format("/v1/accounts/code/%s", "1111"))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
- .entity(new AccountAttributes("keykeykeykey", false))
+ .entity(new V1AccountAttributes("keykeykeykey", false))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
@@ -85,4 +133,28 @@ public class AccountControllerTest extends ResourceTest {
verifyNoMoreInteractions(accountsManager);
}
+ @Test
+ public void validDeviceRegisterTest() throws Exception {
+ VerificationCode deviceCode = client().resource("/v1/accounts/registerdevice")
+ .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
+ .get(VerificationCode.class);
+
+ assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
+
+ Long deviceId = client().resource(String.format("/v1/accounts/device/5678901"))
+ .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
+ .entity(new AccountAttributes("keykeykeykey", false, true))
+ .type(MediaType.APPLICATION_JSON_TYPE)
+ .put(Long.class);
+ assertThat(deviceId).isNotEqualTo(AuthHelper.DEFAULT_DEVICE_ID);
+
+ ArgumentCaptor newAccount = ArgumentCaptor.forClass(Account.class);
+ verify(accountsManager).createAccountOnExistingNumber(newAccount.capture());
+ assertThat(deviceId).isEqualTo(newAccount.getValue().getDeviceId());
+
+ ArgumentCaptor number = ArgumentCaptor.forClass(String.class);
+ verify(pendingDevicesManager).remove(number.capture());
+ assertThat(number.getValue()).isEqualTo(AuthHelper.VALID_NUMBER);
+ }
+
}
diff --git a/src/test/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java b/src/test/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java
index 3b8bd886c..a41e7fa16 100644
--- a/src/test/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java
+++ b/src/test/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java
@@ -1,15 +1,24 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.GenericType;
import com.yammer.dropwizard.testing.ResourceTest;
import org.junit.Test;
import org.whispersystems.textsecuregcm.controllers.KeysController;
import org.whispersystems.textsecuregcm.entities.PreKey;
+import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
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 org.whispersystems.textsecuregcm.tests.util.AuthHelper;
+import javax.jws.WebResult;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@@ -18,26 +27,41 @@ public class KeyControllerTest extends ResourceTest {
private final String EXISTS_NUMBER = "+14152222222";
private final String NOT_EXISTS_NUMBER = "+14152222220";
- private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, 1234, "test1", "test2", false);
- private final Keys keys = mock(Keys.class);
+ private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, AuthHelper.DEFAULT_DEVICE_ID, 1234, "test1", "test2", false);
+ private final PreKey SAMPLE_KEY2 = new PreKey(2, EXISTS_NUMBER, 2, 5667, "test3", "test4", false);
+ private final Keys keys = mock(Keys.class);
+
+ Account[] fakeAccount;
@Override
protected void setUpResources() {
addProvider(AuthHelper.getAuthenticator());
- RateLimiters rateLimiters = mock(RateLimiters.class);
- RateLimiter rateLimiter = mock(RateLimiter.class );
+ RateLimiters rateLimiters = mock(RateLimiters.class);
+ RateLimiter rateLimiter = mock(RateLimiter.class );
+ AccountsManager accounts = mock(AccountsManager.class);
+
+ fakeAccount = new Account[2];
+ fakeAccount[0] = mock(Account.class);
+ fakeAccount[1] = mock(Account.class);
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
- when(keys.get(EXISTS_NUMBER)).thenReturn(SAMPLE_KEY);
- when(keys.get(NOT_EXISTS_NUMBER)).thenReturn(null);
+ when(keys.get(eq(EXISTS_NUMBER), anyList())).thenReturn(new UnstructuredPreKeyList(Arrays.asList(SAMPLE_KEY, SAMPLE_KEY2)));
+ when(keys.get(eq(NOT_EXISTS_NUMBER), anyList())).thenReturn(null);
- addResource(new KeysController(rateLimiters, keys, null));
+ when(fakeAccount[0].getDeviceId()).thenReturn(AuthHelper.DEFAULT_DEVICE_ID);
+ when(fakeAccount[1].getDeviceId()).thenReturn((long) 2);
+
+ when(accounts.getAllByNumber(EXISTS_NUMBER)).thenReturn(Arrays.asList(fakeAccount[0], fakeAccount[1]));
+ when(accounts.getAllByNumber(NOT_EXISTS_NUMBER)).thenReturn(new LinkedList());
+
+ addResource(new KeysController.V1(rateLimiters, keys, accounts, null));
+ addResource(new KeysController.V2(rateLimiters, keys, accounts, null));
}
@Test
- public void validRequestTest() throws Exception {
+ public void validRequestsTest() throws Exception {
PreKey result = client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKey.class);
@@ -49,7 +73,32 @@ public class KeyControllerTest extends ResourceTest {
assertThat(result.getId() == 0);
assertThat(result.getNumber() == null);
- verify(keys).get(EXISTS_NUMBER);
+ verify(keys).get(eq(EXISTS_NUMBER), eq(Arrays.asList(fakeAccount)));
+ verifyNoMoreInteractions(keys);
+
+ List results = client().resource(String.format("/v2/keys/%s", EXISTS_NUMBER))
+ .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
+ .get(new GenericType>(){});
+
+ assertThat(results.size()).isEqualTo(2);
+ result = results.get(0);
+ assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
+ assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
+ assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY.getIdentityKey());
+
+ assertThat(result.getId() == 0);
+ assertThat(result.getNumber() == null);
+
+ result = results.get(1);
+ assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
+ assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
+ assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY2.getIdentityKey());
+
+ assertThat(result.getId() == 1);
+ assertThat(result.getNumber() == null);
+
+ verify(keys, times(2)).get(eq(EXISTS_NUMBER), eq(Arrays.asList(fakeAccount[0], fakeAccount[1])));
+ verifyNoMoreInteractions(keys);
}
@Test
@@ -60,7 +109,7 @@ public class KeyControllerTest extends ResourceTest {
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(404);
- verify(keys).get(NOT_EXISTS_NUMBER);
+ verify(keys).get(NOT_EXISTS_NUMBER, new LinkedList());
}
@Test
diff --git a/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java b/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java
index 5295afc77..a912d5c6a 100644
--- a/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java
+++ b/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java
@@ -14,7 +14,7 @@ public class PreKeyTest {
@Test
public void serializeToJSON() throws Exception {
- PreKey preKey = new PreKey(1, "+14152222222", 1234, "test", "identityTest", false);
+ PreKey preKey = new PreKey(1, "+14152222222", 0, 1234, "test", "identityTest", false);
assertThat("Basic Contact Serialization works",
asJson(preKey),
diff --git a/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java
index 636a01f73..deb9182dc 100644
--- a/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java
+++ b/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java
@@ -15,6 +15,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AuthHelper {
+ public static final long DEFAULT_DEVICE_ID = 1;
public static final String VALID_NUMBER = "+14150000000";
public static final String VALID_PASSWORD = "foo";
@@ -29,7 +30,7 @@ public class AuthHelper {
when(credentials.verify("foo")).thenReturn(true);
when(account.getAuthenticationCredentials()).thenReturn(credentials);
- when(accounts.get(VALID_NUMBER)).thenReturn(Optional.of(account));
+ when(accounts.get(VALID_NUMBER, DEFAULT_DEVICE_ID)).thenReturn(Optional.of(account));
return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(new FederationConfiguration()),
FederatedPeer.class,
@@ -41,4 +42,7 @@ public class AuthHelper {
return "Basic " + Base64.encodeBytes((number + ":" + password).getBytes());
}
+ public static String getV2AuthHeader(String number, long deviceId, String password) {
+ return "Basic " + Base64.encodeBytes((number + "." + deviceId + ":" + password).getBytes());
+ }
}