New API to support multiple accounts per # (FREEBIE)
This commit is contained in:
parent
4cd1082a4a
commit
ef1160eda8
2
pom.xml
2
pom.xml
|
@ -56,7 +56,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.protobuf</groupId>
|
<groupId>com.google.protobuf</groupId>
|
||||||
<artifactId>protobuf-java</artifactId>
|
<artifactId>protobuf-java</artifactId>
|
||||||
<version>2.4.1</version>
|
<version>2.5.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -24,6 +24,7 @@ message OutgoingMessageSignal {
|
||||||
optional string source = 2;
|
optional string source = 2;
|
||||||
optional string relay = 3;
|
optional string relay = 3;
|
||||||
repeated string destinations = 4;
|
repeated string destinations = 4;
|
||||||
|
repeated uint64 destinationDeviceIds = 7;
|
||||||
optional uint64 timestamp = 5;
|
optional uint64 timestamp = 5;
|
||||||
optional bytes message = 6;
|
optional bytes message = 6;
|
||||||
}
|
}
|
|
@ -57,6 +57,10 @@ import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
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.util.UrlSigner;
|
||||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||||
|
|
||||||
|
@ -92,16 +96,20 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
||||||
|
|
||||||
Accounts accounts = jdbi.onDemand(Accounts.class);
|
Accounts accounts = jdbi.onDemand(Accounts.class);
|
||||||
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
|
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
|
||||||
|
PendingDeviceRegistrations pendingDevices = jdbi.onDemand(PendingDeviceRegistrations.class);
|
||||||
Keys keys = jdbi.onDemand(Keys.class);
|
Keys keys = jdbi.onDemand(Keys.class);
|
||||||
|
StoredMessages storedMessages = jdbi.onDemand(StoredMessages.class);
|
||||||
|
|
||||||
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
|
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
|
||||||
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
|
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
|
||||||
|
|
||||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
DirectoryManager directory = new DirectoryManager(redisClient);
|
||||||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
|
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
|
||||||
|
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, memcachedClient);
|
||||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
||||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager );
|
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager );
|
||||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||||
|
StoredMessageManager storedMessageManager = new StoredMessageManager(storedMessages);
|
||||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
||||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||||
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
||||||
|
@ -109,6 +117,7 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
||||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||||
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
|
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
|
||||||
config.getApnConfiguration(),
|
config.getApnConfiguration(),
|
||||||
|
storedMessageManager,
|
||||||
accountsManager, directory);
|
accountsManager, directory);
|
||||||
|
|
||||||
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||||
|
@ -116,10 +125,11 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
||||||
accountAuthenticator,
|
accountAuthenticator,
|
||||||
Account.class, "WhisperServer"));
|
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 DirectoryController(rateLimiters, directory));
|
||||||
environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner));
|
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.addResource(new FederationController(keys, accountsManager, pushSender, urlSigner));
|
||||||
|
|
||||||
environment.addServlet(new MessageController(rateLimiters, accountAuthenticator,
|
environment.addServlet(new MessageController(rateLimiters, accountAuthenticator,
|
||||||
|
|
|
@ -51,7 +51,13 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
|
||||||
public Optional<Account> authenticate(BasicCredentials basicCredentials)
|
public Optional<Account> authenticate(BasicCredentials basicCredentials)
|
||||||
throws AuthenticationException
|
throws AuthenticationException
|
||||||
{
|
{
|
||||||
Optional<Account> account = accountsManager.get(basicCredentials.getUsername());
|
AuthorizationHeader authorizationHeader;
|
||||||
|
try {
|
||||||
|
authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
|
||||||
|
} catch (InvalidAuthorizationHeaderException iahe) {
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber(), authorizationHeader.getDeviceId());
|
||||||
|
|
||||||
if (!account.isPresent()) {
|
if (!account.isPresent()) {
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
|
|
|
@ -24,10 +24,28 @@ import java.io.IOException;
|
||||||
|
|
||||||
public class AuthorizationHeader {
|
public class AuthorizationHeader {
|
||||||
|
|
||||||
private final String user;
|
private final String number;
|
||||||
|
private final long accountId;
|
||||||
private final String password;
|
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 {
|
try {
|
||||||
if (header == null) {
|
if (header == null) {
|
||||||
throw new InvalidAuthorizationHeaderException("Null header");
|
throw new InvalidAuthorizationHeaderException("Null header");
|
||||||
|
@ -55,16 +73,18 @@ public class AuthorizationHeader {
|
||||||
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
|
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.user = credentialParts[0];
|
return fromUserAndPassword(credentialParts[0], credentialParts[1]);
|
||||||
this.password = credentialParts[1];
|
|
||||||
|
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
throw new InvalidAuthorizationHeaderException(ioe);
|
throw new InvalidAuthorizationHeaderException(ioe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserName() {
|
public String getNumber() {
|
||||||
return user;
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDeviceId() {
|
||||||
|
return accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.auth.Auth;
|
import com.yammer.dropwizard.auth.Auth;
|
||||||
import com.yammer.metrics.annotation.Timed;
|
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.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||||
|
|
||||||
|
@ -59,16 +61,19 @@ public class AccountController {
|
||||||
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
||||||
|
|
||||||
private final PendingAccountsManager pendingAccounts;
|
private final PendingAccountsManager pendingAccounts;
|
||||||
|
private final PendingDevicesManager pendingDevices;
|
||||||
private final AccountsManager accounts;
|
private final AccountsManager accounts;
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final SmsSender smsSender;
|
private final SmsSender smsSender;
|
||||||
|
|
||||||
public AccountController(PendingAccountsManager pendingAccounts,
|
public AccountController(PendingAccountsManager pendingAccounts,
|
||||||
|
PendingDevicesManager pendingDevices,
|
||||||
AccountsManager accounts,
|
AccountsManager accounts,
|
||||||
RateLimiters rateLimiters,
|
RateLimiters rateLimiters,
|
||||||
SmsSender smsSenderFactory)
|
SmsSender smsSenderFactory)
|
||||||
{
|
{
|
||||||
this.pendingAccounts = pendingAccounts;
|
this.pendingAccounts = pendingAccounts;
|
||||||
|
this.pendingDevices = pendingDevices;
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.smsSender = smsSenderFactory;
|
this.smsSender = smsSenderFactory;
|
||||||
|
@ -119,8 +124,8 @@ public class AccountController {
|
||||||
throws RateLimitExceededException
|
throws RateLimitExceededException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
AuthorizationHeader header = new AuthorizationHeader(authorizationHeader);
|
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||||
String number = header.getUserName();
|
String number = header.getNumber();
|
||||||
String password = header.getPassword();
|
String password = header.getPassword();
|
||||||
|
|
||||||
rateLimiters.getVerifyLimiter().validate(number);
|
rateLimiters.getVerifyLimiter().validate(number);
|
||||||
|
@ -138,16 +143,22 @@ public class AccountController {
|
||||||
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||||
account.setSignalingKey(accountAttributes.getSignalingKey());
|
account.setSignalingKey(accountAttributes.getSignalingKey());
|
||||||
account.setSupportsSms(accountAttributes.getSupportsSms());
|
account.setSupportsSms(accountAttributes.getSupportsSms());
|
||||||
|
account.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||||
|
account.setDeviceId(0);
|
||||||
|
|
||||||
|
accounts.createResetNumber(account);
|
||||||
|
|
||||||
|
pendingAccounts.remove(number);
|
||||||
|
|
||||||
accounts.create(account);
|
|
||||||
logger.debug("Stored account...");
|
logger.debug("Stored account...");
|
||||||
|
|
||||||
} catch (InvalidAuthorizationHeaderException e) {
|
} catch (InvalidAuthorizationHeaderException e) {
|
||||||
logger.info("Bad Authorization Header", e);
|
logger.info("Bad Authorization Header", e);
|
||||||
throw new WebApplicationException(Response.status(401).build());
|
throw new WebApplicationException(Response.status(401).build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@PUT
|
@PUT
|
||||||
@Path("/gcm/")
|
@Path("/gcm/")
|
||||||
|
@ -193,7 +204,7 @@ public class AccountController {
|
||||||
encodedVerificationText)).build();
|
encodedVerificationText)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private VerificationCode generateVerificationCode() {
|
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||||
try {
|
try {
|
||||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||||
int randomInt = 100000 + random.nextInt(900000);
|
int randomInt = 100000 + random.nextInt(900000);
|
||||||
|
@ -203,4 +214,64 @@ public class AccountController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/registerdevice")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public VerificationCode createDeviceToken(@Auth Account account)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
rateLimiters.getVerifyLimiter().validate(account.getNumber()); //TODO: New limiter?
|
||||||
|
|
||||||
|
VerificationCode verificationCode = generateVerificationCode();
|
||||||
|
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
|
||||||
|
|
||||||
|
return verificationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/device/{verification_code}")
|
||||||
|
public long verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
||||||
|
@HeaderParam("Authorization") String authorizationHeader,
|
||||||
|
@Valid AccountAttributes accountAttributes)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
Account account;
|
||||||
|
try {
|
||||||
|
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||||
|
String number = header.getNumber();
|
||||||
|
String password = header.getPassword();
|
||||||
|
|
||||||
|
rateLimiters.getVerifyLimiter().validate(number); //TODO: New limiter?
|
||||||
|
|
||||||
|
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
|
||||||
|
|
||||||
|
if (!storedVerificationCode.isPresent() ||
|
||||||
|
!verificationCode.equals(storedVerificationCode.get()))
|
||||||
|
{
|
||||||
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
account = new Account();
|
||||||
|
account.setNumber(number);
|
||||||
|
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||||
|
account.setSignalingKey(accountAttributes.getSignalingKey());
|
||||||
|
account.setSupportsSms(accountAttributes.getSupportsSms());
|
||||||
|
account.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||||
|
|
||||||
|
accounts.createAccountOnExistingNumber(account);
|
||||||
|
|
||||||
|
pendingDevices.remove(number);
|
||||||
|
|
||||||
|
logger.debug("Stored new device account...");
|
||||||
|
} catch (InvalidAuthorizationHeaderException e) {
|
||||||
|
logger.info("Bad Authorization Header", e);
|
||||||
|
throw new WebApplicationException(Response.status(401).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.getDeviceId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,11 +29,13 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
import org.whispersystems.textsecuregcm.util.NumberData;
|
||||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
@ -86,16 +88,16 @@ public class FederationController {
|
||||||
@GET
|
@GET
|
||||||
@Path("/key/{number}")
|
@Path("/key/{number}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public PreKey getKey(@Auth FederatedPeer peer,
|
public UnstructuredPreKeyList getKey(@Auth FederatedPeer peer,
|
||||||
@PathParam("number") String number)
|
@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());
|
throw new WebApplicationException(Response.status(404).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
return preKey;
|
return preKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
|
@ -111,7 +113,7 @@ public class FederationController {
|
||||||
.setRelay(peer.getName())
|
.setRelay(peer.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
pushSender.sendMessage(message.getDestination(), signal);
|
pushSender.sendMessage(message.getDestination(), message.getDestinationDeviceId(), signal);
|
||||||
} catch (InvalidProtocolBufferException ipe) {
|
} catch (InvalidProtocolBufferException ipe) {
|
||||||
logger.warn("ProtoBuf", ipe);
|
logger.warn("ProtoBuf", ipe);
|
||||||
throw new WebApplicationException(Response.status(400).build());
|
throw new WebApplicationException(Response.status(400).build());
|
||||||
|
@ -136,18 +138,15 @@ public class FederationController {
|
||||||
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
|
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
|
||||||
@PathParam("offset") int offset)
|
@PathParam("offset") int offset)
|
||||||
{
|
{
|
||||||
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
|
List<NumberData> numberList = accounts.getAllNumbers(offset, ACCOUNT_CHUNK_SIZE);
|
||||||
List<ClientContact> clientContacts = new LinkedList<>();
|
List<ClientContact> clientContacts = new LinkedList<>();
|
||||||
|
|
||||||
for (Account account : accountList) {
|
for (NumberData number : numberList) {
|
||||||
byte[] token = Util.getContactToken(account.getNumber());
|
byte[] token = Util.getContactToken(number.getNumber());
|
||||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
ClientContact clientContact = new ClientContact(token, null, number.isSupportsSms());
|
||||||
|
|
||||||
if (Util.isEmpty(account.getApnRegistrationId()) &&
|
if (!number.isActive())
|
||||||
Util.isEmpty(account.getGcmRegistrationId()))
|
|
||||||
{
|
|
||||||
clientContact.setInactive(true);
|
clientContact.setInactive(true);
|
||||||
}
|
|
||||||
|
|
||||||
clientContacts.add(clientContact);
|
clientContacts.add(clientContact);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,12 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKeyList;
|
import org.whispersystems.textsecuregcm.entities.PreKeyList;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
@ -39,21 +41,24 @@ import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Path("/v1/keys")
|
@Path("/v1/keys")
|
||||||
public class KeysController {
|
public abstract class KeysController {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
||||||
|
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final Keys keys;
|
private final Keys keys;
|
||||||
|
private final AccountsManager accountsManager;
|
||||||
private final FederatedClientManager federatedClientManager;
|
private final FederatedClientManager federatedClientManager;
|
||||||
|
|
||||||
public KeysController(RateLimiters rateLimiters, Keys keys,
|
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager,
|
||||||
FederatedClientManager federatedClientManager)
|
FederatedClientManager federatedClientManager)
|
||||||
{
|
{
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
|
this.accountsManager = accountsManager;
|
||||||
this.federatedClientManager = federatedClientManager;
|
this.federatedClientManager = federatedClientManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +66,35 @@ public class KeysController {
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PreKey> getKeys(Account account, String number, String relay) throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number);
|
||||||
|
|
||||||
|
try {
|
||||||
|
UnstructuredPreKeyList keyList;
|
||||||
|
|
||||||
|
if (relay == null) {
|
||||||
|
keyList = keys.get(number, accountsManager.getAllByNumber(number));
|
||||||
|
} else {
|
||||||
|
keyList = federatedClientManager.getClient(relay).getKeys(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@Timed
|
||||||
|
@ -73,20 +106,27 @@ public class KeysController {
|
||||||
@QueryParam("relay") String relay)
|
@QueryParam("relay") String relay)
|
||||||
throws RateLimitExceededException
|
throws RateLimitExceededException
|
||||||
{
|
{
|
||||||
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number);
|
return super.getKeys(account, number, relay).get(0);
|
||||||
|
|
||||||
try {
|
|
||||||
PreKey key;
|
|
||||||
|
|
||||||
if (relay == null) key = keys.get(number);
|
|
||||||
else key = federatedClientManager.getClient(relay).getKey(number);
|
|
||||||
|
|
||||||
if (key == null) throw new WebApplicationException(Response.status(404).build());
|
|
||||||
else return key;
|
|
||||||
} catch (NoSuchPeerException e) {
|
|
||||||
logger.info("No peer: " + relay);
|
|
||||||
throw new WebApplicationException(Response.status(404).build());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("/v2/keys")
|
||||||
|
public static class V2 extends KeysController {
|
||||||
|
public V2(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager, FederatedClientManager federatedClientManager)
|
||||||
|
{
|
||||||
|
super(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/{number}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public List<PreKey> get(@Auth Account account,
|
||||||
|
@PathParam("number") String number,
|
||||||
|
@QueryParam("relay") String relay)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
return super.getKeys(account, number, relay);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,11 +139,12 @@ public class MessageController extends HttpServlet {
|
||||||
try {
|
try {
|
||||||
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
|
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
|
||||||
String destination = messagePair.first().getDestination();
|
String destination = messagePair.first().getDestination();
|
||||||
|
long destinationDeviceId = messagePair.first().getDestinationDeviceId();
|
||||||
String relay = messagePair.first().getRelay();
|
String relay = messagePair.first().getRelay();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
|
if (Util.isEmpty(relay)) sendLocalMessage(destination, destinationDeviceId, messagePair.second());
|
||||||
else sendRelayMessage(relay, destination, messagePair.second());
|
else sendRelayMessage(relay, destination, destinationDeviceId, messagePair.second());
|
||||||
success.add(destination);
|
success.add(destination);
|
||||||
} catch (NoSuchUserException e) {
|
} catch (NoSuchUserException e) {
|
||||||
logger.debug("No such user", 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
|
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
|
throws IOException, NoSuchUserException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
FederatedClient client = federatedClientManager.getClient(relay);
|
FederatedClient client = federatedClientManager.getClient(relay);
|
||||||
client.sendMessage(destination, outgoingMessage);
|
client.sendMessage(destination, destinationDeviceId, outgoingMessage);
|
||||||
} catch (NoSuchPeerException e) {
|
} catch (NoSuchPeerException e) {
|
||||||
logger.info("No such peer", e);
|
logger.info("No such peer", e);
|
||||||
throw new NoSuchUserException(e);
|
throw new NoSuchUserException(e);
|
||||||
|
@ -208,6 +209,7 @@ public class MessageController extends HttpServlet {
|
||||||
|
|
||||||
for (IncomingMessage sub : incomingMessages) {
|
for (IncomingMessage sub : incomingMessages) {
|
||||||
if (sub != incoming) {
|
if (sub != incoming) {
|
||||||
|
outgoingMessage.setDestinationDeviceIds(index, sub.getDestinationDeviceId());
|
||||||
outgoingMessage.setDestinations(index++, sub.getDestination());
|
outgoingMessage.setDestinations(index++, sub.getDestination());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,8 +265,8 @@ public class MessageController extends HttpServlet {
|
||||||
|
|
||||||
private Account authenticate(HttpServletRequest request) throws AuthenticationException {
|
private Account authenticate(HttpServletRequest request) throws AuthenticationException {
|
||||||
try {
|
try {
|
||||||
AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization"));
|
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromFullHeader(request.getHeader("Authorization"));
|
||||||
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(),
|
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getNumber() + "." + authorizationHeader.getDeviceId(),
|
||||||
authorizationHeader.getPassword() );
|
authorizationHeader.getPassword() );
|
||||||
|
|
||||||
Optional<Account> account = accountAuthenticator.authenticate(credentials);
|
Optional<Account> account = accountAuthenticator.authenticate(credentials);
|
||||||
|
|
|
@ -28,11 +28,15 @@ public class AccountAttributes {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private boolean supportsSms;
|
private boolean supportsSms;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean fetchesMessages;
|
||||||
|
|
||||||
public AccountAttributes() {}
|
public AccountAttributes() {}
|
||||||
|
|
||||||
public AccountAttributes(String signalingKey, boolean supportsSms) {
|
public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages) {
|
||||||
this.signalingKey = signalingKey;
|
this.signalingKey = signalingKey;
|
||||||
this.supportsSms = supportsSms;
|
this.supportsSms = supportsSms;
|
||||||
|
this.fetchesMessages = fetchesMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSignalingKey() {
|
public String getSignalingKey() {
|
||||||
|
@ -43,4 +47,8 @@ public class AccountAttributes {
|
||||||
return supportsSms;
|
return supportsSms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getFetchesMessages() {
|
||||||
|
return fetchesMessages;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,9 @@ public class IncomingMessage {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private long timestamp;
|
private long timestamp;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long destinationDeviceId = 1;
|
||||||
|
|
||||||
public String getDestination() {
|
public String getDestination() {
|
||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
|
@ -53,4 +56,12 @@ public class IncomingMessage {
|
||||||
public String getRelay() {
|
public String getRelay() {
|
||||||
return relay;
|
return relay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDestinationDeviceId() {
|
||||||
|
return destinationDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDestinationDeviceId(long destinationDeviceId) {
|
||||||
|
this.destinationDeviceId = destinationDeviceId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -34,6 +34,10 @@ public class PreKey {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private String number;
|
private String number;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private long deviceId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotNull
|
@NotNull
|
||||||
private long keyId;
|
private long keyId;
|
||||||
|
@ -51,12 +55,13 @@ public class PreKey {
|
||||||
|
|
||||||
public 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,
|
String publicKey, String identityKey,
|
||||||
boolean lastResort)
|
boolean lastResort)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.number = number;
|
this.number = number;
|
||||||
|
this.deviceId = deviceId;
|
||||||
this.keyId = keyId;
|
this.keyId = keyId;
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
this.identityKey = identityKey;
|
this.identityKey = identityKey;
|
||||||
|
@ -113,4 +118,12 @@ public class PreKey {
|
||||||
public void setLastResort(boolean lastResort) {
|
public void setLastResort(boolean lastResort) {
|
||||||
this.lastResort = lastResort;
|
this.lastResort = lastResort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDeviceId(long deviceId) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,10 @@ public class RelayMessage {
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String destination;
|
private String destination;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private long destinationDeviceId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||||
|
@ -40,7 +44,7 @@ public class RelayMessage {
|
||||||
|
|
||||||
public RelayMessage() {}
|
public RelayMessage() {}
|
||||||
|
|
||||||
public RelayMessage(String destination, byte[] outgoingMessageSignal) {
|
public RelayMessage(String destination, long destinationDeviceId, byte[] outgoingMessageSignal) {
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
this.outgoingMessageSignal = outgoingMessageSignal;
|
this.outgoingMessageSignal = outgoingMessageSignal;
|
||||||
}
|
}
|
||||||
|
@ -49,6 +53,10 @@ public class RelayMessage {
|
||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDestinationDeviceId() {
|
||||||
|
return destinationDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getOutgoingMessageSignal() {
|
public byte[] getOutgoingMessageSignal() {
|
||||||
return outgoingMessageSignal;
|
return outgoingMessageSignal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<PreKey> keys;
|
||||||
|
|
||||||
|
public UnstructuredPreKeyList(List<PreKey> preKeys) {
|
||||||
|
this.keys = preKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PreKey> getKeys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof UnstructuredPreKeyList) ||
|
||||||
|
((UnstructuredPreKeyList) o).keys.size() != keys.size())
|
||||||
|
return false;
|
||||||
|
Iterator<PreKey> otherKeys = ((UnstructuredPreKeyList) o).keys.iterator();
|
||||||
|
for (PreKey key : keys) {
|
||||||
|
if (!otherKeys.next().equals(key))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||||
import org.whispersystems.textsecuregcm.util.Base64;
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
|
@ -99,12 +100,12 @@ public class FederatedClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreKey getKey(String destination) {
|
public UnstructuredPreKeyList getKeys(String destination) {
|
||||||
try {
|
try {
|
||||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination));
|
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination));
|
||||||
return resource.accept(MediaType.APPLICATION_JSON)
|
return resource.accept(MediaType.APPLICATION_JSON)
|
||||||
.header("Authorization", authorizationHeader)
|
.header("Authorization", authorizationHeader)
|
||||||
.get(PreKey.class);
|
.get(UnstructuredPreKeyList.class);
|
||||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||||
logger.warn("PreKey", e);
|
logger.warn("PreKey", e);
|
||||||
return null;
|
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
|
throws IOException, NoSuchUserException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH);
|
WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH);
|
||||||
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
||||||
.header("Authorization", authorizationHeader)
|
.header("Authorization", authorizationHeader)
|
||||||
.entity(new RelayMessage(destination, message.toByteArray()))
|
.entity(new RelayMessage(destination, destinationDeviceId, message.toByteArray()))
|
||||||
.put(ClientResponse.class);
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
if (response.getStatus() == 404) {
|
if (response.getStatus() == 404) {
|
||||||
|
|
|
@ -27,11 +27,13 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class PushSender {
|
public class PushSender {
|
||||||
|
|
||||||
|
@ -42,9 +44,11 @@ public class PushSender {
|
||||||
|
|
||||||
private final GCMSender gcmSender;
|
private final GCMSender gcmSender;
|
||||||
private final APNSender apnSender;
|
private final APNSender apnSender;
|
||||||
|
private final StoredMessageManager storedMessageManager;
|
||||||
|
|
||||||
public PushSender(GcmConfiguration gcmConfiguration,
|
public PushSender(GcmConfiguration gcmConfiguration,
|
||||||
ApnConfiguration apnConfiguration,
|
ApnConfiguration apnConfiguration,
|
||||||
|
StoredMessageManager storedMessageManager,
|
||||||
AccountsManager accounts,
|
AccountsManager accounts,
|
||||||
DirectoryManager directory)
|
DirectoryManager directory)
|
||||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
||||||
|
@ -52,25 +56,27 @@ public class PushSender {
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
|
|
||||||
|
this.storedMessageManager = storedMessageManager;
|
||||||
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
|
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
|
||||||
this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
|
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
|
throws IOException, NoSuchUserException
|
||||||
{
|
{
|
||||||
Optional<Account> account = accounts.get(destination);
|
Optional<Account> accountOptional = accounts.get(destination, destinationDeviceId);
|
||||||
|
|
||||||
if (!account.isPresent()) {
|
if (!accountOptional.isPresent()) {
|
||||||
directory.remove(destination);
|
|
||||||
throw new NoSuchUserException("No such local destination: " + destination);
|
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);
|
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
|
||||||
|
|
||||||
if (account.get().getGcmRegistrationId() != null) sendGcmMessage(account.get(), message);
|
if (account.getGcmRegistrationId() != null) sendGcmMessage(account, message);
|
||||||
else if (account.get().getApnRegistrationId() != null) sendApnMessage(account.get(), message);
|
else if (account.getApnRegistrationId() != null) sendApnMessage(account, message);
|
||||||
|
else if (account.getFetchesMessages()) storeFetchedMessage(account, message);
|
||||||
else throw new NoSuchUserException("No push identifier!");
|
else throw new NoSuchUserException("No push identifier!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,4 +106,7 @@ public class PushSender {
|
||||||
apnSender.sendMessage(account.getApnRegistrationId(), outgoingMessage);
|
apnSender.sendMessage(account.getApnRegistrationId(), outgoingMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void storeFetchedMessage(Account account, EncryptedOutgoingMessage outgoingMessage) {
|
||||||
|
storedMessageManager.storeMessage(account, outgoingMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,27 +27,36 @@ public class Account implements Serializable {
|
||||||
|
|
||||||
private long id;
|
private long id;
|
||||||
private String number;
|
private String number;
|
||||||
|
private long deviceId;
|
||||||
private String hashedAuthenticationToken;
|
private String hashedAuthenticationToken;
|
||||||
private String salt;
|
private String salt;
|
||||||
private String signalingKey;
|
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 gcmRegistrationId;
|
||||||
private String apnRegistrationId;
|
private String apnRegistrationId;
|
||||||
private boolean supportsSms;
|
private boolean supportsSms;
|
||||||
|
private boolean fetchesMessages;
|
||||||
|
|
||||||
public Account() {}
|
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,
|
String signalingKey, String gcmRegistrationId, String apnRegistrationId,
|
||||||
boolean supportsSms)
|
boolean supportsSms, boolean fetchesMessages)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.number = number;
|
this.number = number;
|
||||||
|
this.deviceId = deviceId;
|
||||||
this.hashedAuthenticationToken = hashedAuthenticationToken;
|
this.hashedAuthenticationToken = hashedAuthenticationToken;
|
||||||
this.salt = salt;
|
this.salt = salt;
|
||||||
this.signalingKey = signalingKey;
|
this.signalingKey = signalingKey;
|
||||||
this.gcmRegistrationId = gcmRegistrationId;
|
this.gcmRegistrationId = gcmRegistrationId;
|
||||||
this.apnRegistrationId = apnRegistrationId;
|
this.apnRegistrationId = apnRegistrationId;
|
||||||
this.supportsSms = supportsSms;
|
this.supportsSms = supportsSms;
|
||||||
|
this.fetchesMessages = fetchesMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getApnRegistrationId() {
|
public String getApnRegistrationId() {
|
||||||
|
@ -74,6 +83,14 @@ public class Account implements Serializable {
|
||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeviceId(long deviceId) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
|
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
|
||||||
this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken();
|
this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken();
|
||||||
this.salt = credentials.getSalt();
|
this.salt = credentials.getSalt();
|
||||||
|
@ -106,4 +123,12 @@ public class Account implements Serializable {
|
||||||
public void setId(long id) {
|
public void setId(long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFetchesMessages(boolean fetchesMessages) {
|
||||||
|
this.fetchesMessages = fetchesMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getFetchesMessages() {
|
||||||
|
return fetchesMessages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
|
||||||
import org.skife.jdbi.v2.sqlobject.Transaction;
|
import org.skife.jdbi.v2.sqlobject.Transaction;
|
||||||
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
||||||
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.NumberData;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
|
@ -44,48 +45,74 @@ public abstract class Accounts {
|
||||||
|
|
||||||
public static final String ID = "id";
|
public static final String ID = "id";
|
||||||
public static final String NUMBER = "number";
|
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 AUTH_TOKEN = "auth_token";
|
||||||
public static final String SALT = "salt";
|
public static final String SALT = "salt";
|
||||||
public static final String SIGNALING_KEY = "signaling_key";
|
public static final String SIGNALING_KEY = "signaling_key";
|
||||||
public static final String GCM_ID = "gcm_id";
|
public static final String GCM_ID = "gcm_id";
|
||||||
public static final String APN_ID = "apn_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";
|
public static final String SUPPORTS_SMS = "supports_sms";
|
||||||
|
|
||||||
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + AUTH_TOKEN + ", " +
|
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DEVICE_ID + ", " + AUTH_TOKEN + ", " +
|
||||||
SALT + ", " + SIGNALING_KEY + ", " + GCM_ID + ", " +
|
SALT + ", " + SIGNALING_KEY + ", " + FETCHES_MESSAGES + ", " +
|
||||||
APN_ID + ", " + SUPPORTS_SMS + ") " +
|
GCM_ID + ", " + APN_ID + ", " + SUPPORTS_SMS + ") " +
|
||||||
"VALUES (:number, :auth_token, :salt, :signaling_key, :gcm_id, :apn_id, :supports_sms)")
|
"VALUES (:number, :device_id, :auth_token, :salt, :signaling_key, :fetches_messages, :gcm_id, :apn_id, :supports_sms)")
|
||||||
@GetGeneratedKeys
|
@GetGeneratedKeys
|
||||||
abstract long createStep(@AccountBinder Account account);
|
abstract long insertStep(@AccountBinder Account account);
|
||||||
|
|
||||||
@SqlUpdate("DELETE FROM accounts WHERE number = :number")
|
@SqlQuery("SELECT " + DEVICE_ID + " FROM accounts WHERE " + NUMBER + " = :number ORDER BY " + DEVICE_ID + " DESC LIMIT 1 FOR UPDATE")
|
||||||
abstract void removeStep(@Bind("number") String number);
|
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, " +
|
@SqlUpdate("UPDATE accounts SET " + AUTH_TOKEN + " = :auth_token, " + SALT + " = :salt, " +
|
||||||
SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " +
|
SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " + APN_ID + " = :apn_id, " +
|
||||||
APN_ID + " = :apn_id, " + SUPPORTS_SMS + " = :supports_sms " +
|
FETCHES_MESSAGES + " = :fetches_messages, " + SUPPORTS_SMS + " = :supports_sms " +
|
||||||
"WHERE " + NUMBER + " = :number")
|
"WHERE " + NUMBER + " = :number AND " + DEVICE_ID + " = :device_id")
|
||||||
abstract void update(@AccountBinder Account account);
|
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<NumberData> getAllNumbers(@Bind("offset") int offset, @Bind("limit") int length);
|
||||||
|
|
||||||
|
@Mapper(NumberDataMapper.class)
|
||||||
|
@SqlQuery(NUMBER_DATA_QUERY + " GROUP BY " + NUMBER)
|
||||||
|
public abstract Iterator<NumberData> getAllNumbers();
|
||||||
|
|
||||||
|
@Mapper(NumberDataMapper.class)
|
||||||
|
@SqlQuery(NUMBER_DATA_QUERY + " WHERE " + NUMBER + " = :number GROUP BY " + NUMBER)
|
||||||
|
abstract NumberData getNumberData(@Bind("number") String number);
|
||||||
|
|
||||||
@Mapper(AccountMapper.class)
|
@Mapper(AccountMapper.class)
|
||||||
@SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number")
|
@SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number")
|
||||||
abstract Account get(@Bind("number") String number);
|
public abstract List<Account> getAllByNumber(@Bind("number") String number);
|
||||||
|
|
||||||
@SqlQuery("SELECT COUNT(*) from accounts")
|
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||||
abstract long getCount();
|
public long insertClearingNumber(Account account) {
|
||||||
|
removeAccountsByNumber(account.getNumber());
|
||||||
@Mapper(AccountMapper.class)
|
account.setDeviceId(getHighestDeviceId(account.getNumber()) + 1);
|
||||||
@SqlQuery("SELECT * FROM accounts OFFSET :offset LIMIT :limit")
|
return insertStep(account);
|
||||||
abstract List<Account> getAll(@Bind("offset") int offset, @Bind("limit") int length);
|
|
||||||
|
|
||||||
@Mapper(AccountMapper.class)
|
|
||||||
@SqlQuery("SELECT * FROM accounts")
|
|
||||||
abstract Iterator<Account> getAll();
|
|
||||||
|
|
||||||
@Transaction(TransactionIsolationLevel.REPEATABLE_READ)
|
|
||||||
public long create(Account account) {
|
|
||||||
removeStep(account.getNumber());
|
|
||||||
return createStep(account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AccountMapper implements ResultSetMapper<Account> {
|
public static class AccountMapper implements ResultSetMapper<Account> {
|
||||||
|
@ -94,11 +121,21 @@ public abstract class Accounts {
|
||||||
public Account map(int i, ResultSet resultSet, StatementContext statementContext)
|
public Account map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||||
throws SQLException
|
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(AUTH_TOKEN), resultSet.getString(SALT),
|
||||||
resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID),
|
resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID),
|
||||||
resultSet.getString(APN_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<NumberData> {
|
||||||
|
|
||||||
|
@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(ID, account.getId());
|
||||||
sql.bind(NUMBER, account.getNumber());
|
sql.bind(NUMBER, account.getNumber());
|
||||||
|
sql.bind(DEVICE_ID, account.getDeviceId());
|
||||||
sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials()
|
sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials()
|
||||||
.getHashedAuthenticationToken());
|
.getHashedAuthenticationToken());
|
||||||
sql.bind(SALT, account.getAuthenticationCredentials().getSalt());
|
sql.bind(SALT, account.getAuthenticationCredentials().getSalt());
|
||||||
|
@ -124,6 +162,7 @@ public abstract class Accounts {
|
||||||
sql.bind(GCM_ID, account.getGcmRegistrationId());
|
sql.bind(GCM_ID, account.getGcmRegistrationId());
|
||||||
sql.bind(APN_ID, account.getApnRegistrationId());
|
sql.bind(APN_ID, account.getApnRegistrationId());
|
||||||
sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0);
|
sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0);
|
||||||
|
sql.bind(FETCHES_MESSAGES, account.getFetchesMessages() ? 1 : 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.whispersystems.textsecuregcm.storage;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import net.spy.memcached.MemcachedClient;
|
import net.spy.memcached.MemcachedClient;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||||
|
import org.whispersystems.textsecuregcm.util.NumberData;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -41,24 +42,36 @@ public class AccountsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getCount() {
|
public long getCount() {
|
||||||
return accounts.getCount();
|
return accounts.getNumberCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Account> getAll(int offset, int length) {
|
public List<NumberData> getAllNumbers(int offset, int length) {
|
||||||
return accounts.getAll(offset, length);
|
return accounts.getAllNumbers(offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Iterator<Account> getAll() {
|
public Iterator<NumberData> getAllNumbers() {
|
||||||
return accounts.getAll();
|
return accounts.getAllNumbers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void create(Account account) {
|
/** Creates a new Account and NumberData, clearing all existing accounts/data on the given number */
|
||||||
long id = accounts.create(account);
|
public void createResetNumber(Account account) {
|
||||||
|
long id = accounts.insertClearingNumber(account);
|
||||||
account.setId(id);
|
account.setId(id);
|
||||||
|
|
||||||
if (memcachedClient != null) {
|
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);
|
updateDirectory(account);
|
||||||
|
@ -66,25 +79,25 @@ public class AccountsManager {
|
||||||
|
|
||||||
public void update(Account account) {
|
public void update(Account account) {
|
||||||
if (memcachedClient != null) {
|
if (memcachedClient != null) {
|
||||||
memcachedClient.set(getKey(account.getNumber()), 0, account);
|
memcachedClient.set(getKey(account.getNumber(), account.getDeviceId()), 0, account);
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
updateDirectory(account);
|
updateDirectory(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Account> get(String number) {
|
public Optional<Account> get(String number, long deviceId) {
|
||||||
Account account = null;
|
Account account = null;
|
||||||
|
|
||||||
if (memcachedClient != null) {
|
if (memcachedClient != null) {
|
||||||
account = (Account)memcachedClient.get(getKey(number));
|
account = (Account)memcachedClient.get(getKey(number, deviceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
account = accounts.get(number);
|
account = accounts.get(number, deviceId);
|
||||||
|
|
||||||
if (account != null && memcachedClient != null) {
|
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();
|
else return Optional.absent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Account> getAllByNumber(String number) {
|
||||||
|
return accounts.getAllByNumber(number);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateDirectory(Account account) {
|
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());
|
byte[] token = Util.getContactToken(account.getNumber());
|
||||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
ClientContact clientContact = new ClientContact(token, null, supportsSms);
|
||||||
directory.add(clientContact);
|
directory.add(clientContact);
|
||||||
} else {
|
} else {
|
||||||
directory.remove(account.getNumber());
|
directory.remove(account.getNumber());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getKey(String number) {
|
private String getKey(String number, long accountId) {
|
||||||
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
|
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number + accountId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.skife.jdbi.v2.sqlobject.Transaction;
|
||||||
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
||||||
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
|
@ -38,48 +39,60 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class Keys {
|
public abstract class Keys {
|
||||||
|
|
||||||
@SqlUpdate("DELETE FROM keys WHERE number = :number")
|
@SqlUpdate("DELETE FROM keys WHERE number = :number AND device_id = :device_id")
|
||||||
abstract void removeKeys(@Bind("number") String number);
|
abstract void removeKeys(@Bind("number") String number, @Bind("device_id") long deviceId);
|
||||||
|
|
||||||
@SqlUpdate("DELETE FROM keys WHERE id = :id")
|
@SqlUpdate("DELETE FROM keys WHERE id = :id")
|
||||||
abstract void removeKey(@Bind("id") long 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<PreKey> preKeys);
|
abstract void append(@PreKeyBinder List<PreKey> 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);
|
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)
|
@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)
|
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||||
public void store(String number, PreKey lastResortKey, List<PreKey> keys) {
|
public void store(String number, long deviceId, PreKey lastResortKey, List<PreKey> keys) {
|
||||||
for (PreKey key : keys) {
|
for (PreKey key : keys) {
|
||||||
key.setNumber(number);
|
key.setNumber(number);
|
||||||
|
key.setDeviceId(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastResortKey.setNumber(number);
|
lastResortKey.setNumber(number);
|
||||||
|
lastResortKey.setDeviceId(deviceId);
|
||||||
|
lastResortKey.setLastResort(true);
|
||||||
|
|
||||||
removeKeys(number);
|
removeKeys(number, deviceId);
|
||||||
append(keys);
|
append(keys);
|
||||||
append(lastResortKey);
|
append(lastResortKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||||
public PreKey get(String number) {
|
public UnstructuredPreKeyList get(String number, List<Account> accounts) {
|
||||||
PreKey preKey = retrieveFirst(number);
|
List<PreKey> preKeys = new LinkedList<>();
|
||||||
|
for (Account account : accounts) {
|
||||||
|
PreKey preKey = retrieveFirst(number, account.getDeviceId());
|
||||||
|
if (preKey != null)
|
||||||
|
preKeys.add(preKey);
|
||||||
|
}
|
||||||
|
|
||||||
if (preKey != null && !preKey.isLastResort()) {
|
for (PreKey preKey : preKeys) {
|
||||||
|
if (!preKey.isLastResort())
|
||||||
removeKey(preKey.getId());
|
removeKey(preKey.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return preKey;
|
return new UnstructuredPreKeyList(preKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
|
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
|
||||||
|
@ -95,6 +108,7 @@ public abstract class Keys {
|
||||||
{
|
{
|
||||||
sql.bind("id", preKey.getId());
|
sql.bind("id", preKey.getId());
|
||||||
sql.bind("number", preKey.getNumber());
|
sql.bind("number", preKey.getNumber());
|
||||||
|
sql.bind("device_id", preKey.getDeviceId());
|
||||||
sql.bind("key_id", preKey.getKeyId());
|
sql.bind("key_id", preKey.getKeyId());
|
||||||
sql.bind("public_key", preKey.getPublicKey());
|
sql.bind("public_key", preKey.getPublicKey());
|
||||||
sql.bind("identity_key", preKey.getIdentityKey());
|
sql.bind("identity_key", preKey.getIdentityKey());
|
||||||
|
@ -111,7 +125,7 @@ public abstract class Keys {
|
||||||
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext)
|
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||||
throws SQLException
|
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.getLong("key_id"), resultSet.getString("public_key"),
|
||||||
resultSet.getString("identity_key"),
|
resultSet.getString("identity_key"),
|
||||||
resultSet.getInt("last_resort") == 1);
|
resultSet.getInt("last_resort") == 1);
|
||||||
|
|
|
@ -29,4 +29,6 @@ public interface PendingAccounts {
|
||||||
@SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number")
|
@SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number")
|
||||||
String getCodeForNumber(@Bind("number") String number);
|
String getCodeForNumber(@Bind("number") String number);
|
||||||
|
|
||||||
|
@SqlUpdate("DELETE FROM pending_accounts WHERE number = :number")
|
||||||
|
void remove(@Bind("number") String number);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,12 @@ public class PendingAccountsManager {
|
||||||
pendingAccounts.insert(number, code);
|
pendingAccounts.insert(number, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void remove(String number) {
|
||||||
|
if (memcachedClient != null)
|
||||||
|
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
||||||
|
pendingAccounts.remove(number);
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<String> getCodeForNumber(String number) {
|
public Optional<String> getCodeForNumber(String number) {
|
||||||
String code = null;
|
String code = null;
|
||||||
|
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<String> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<EncryptedOutgoingMessage> getMessagesForAccountId(@Bind("account_id") long accountId);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,12 +16,24 @@
|
||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.util;
|
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 {
|
public class VerificationCode {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
private String verificationCode;
|
private String verificationCode;
|
||||||
|
@JsonIgnore
|
||||||
private String verificationCodeDisplay;
|
private String verificationCodeDisplay;
|
||||||
|
@JsonIgnore
|
||||||
private String verificationCodeSpeech;
|
private String verificationCodeSpeech;
|
||||||
|
|
||||||
|
@VisibleForTesting VerificationCode() {}
|
||||||
|
|
||||||
public VerificationCode(int verificationCode) {
|
public VerificationCode(int verificationCode) {
|
||||||
this.verificationCode = verificationCode + "";
|
this.verificationCode = verificationCode + "";
|
||||||
this.verificationCodeDisplay = this.verificationCode.substring(0, 3) + "-" +
|
this.verificationCodeDisplay = this.verificationCode.substring(0, 3) + "-" +
|
||||||
|
@ -54,4 +66,7 @@ public class VerificationCode {
|
||||||
return delimited;
|
return delimited;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting public boolean equals(Object o) {
|
||||||
|
return o instanceof VerificationCode && verificationCode.equals(((VerificationCode) o).verificationCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
|
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
|
||||||
import org.whispersystems.textsecuregcm.util.Base64;
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
import org.whispersystems.textsecuregcm.util.NumberData;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -53,22 +54,22 @@ public class DirectoryUpdater {
|
||||||
BatchOperationHandle batchOperation = directory.startBatchOperation();
|
BatchOperationHandle batchOperation = directory.startBatchOperation();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Iterator<Account> accounts = accountsManager.getAll();
|
Iterator<NumberData> numbers = accountsManager.getAllNumbers();
|
||||||
|
|
||||||
if (accounts == null)
|
if (numbers == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
while (accounts.hasNext()) {
|
while (numbers.hasNext()) {
|
||||||
Account account = accounts.next();
|
NumberData number = numbers.next();
|
||||||
if (account.getApnRegistrationId() != null || account.getGcmRegistrationId() != null) {
|
if (number.isActive()) {
|
||||||
byte[] token = Util.getContactToken(account.getNumber());
|
byte[] token = Util.getContactToken(number.getNumber());
|
||||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
ClientContact clientContact = new ClientContact(token, null, number.isSupportsSms());
|
||||||
|
|
||||||
directory.add(batchOperation, clientContact);
|
directory.add(batchOperation, clientContact);
|
||||||
|
|
||||||
logger.debug("Adding local token: " + Base64.encodeBytesWithoutPadding(token));
|
logger.debug("Adding local token: " + Base64.encodeBytesWithoutPadding(token));
|
||||||
} else {
|
} else {
|
||||||
directory.remove(batchOperation, account.getNumber());
|
directory.remove(batchOperation, number.getNumber());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -75,4 +75,51 @@
|
||||||
<column name="number"/>
|
<column name="number"/>
|
||||||
</createIndex>
|
</createIndex>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="2" author="matt">
|
||||||
|
<addColumn tableName="accounts">
|
||||||
|
<column name="device_id" type="bigint">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="fetches_messages" type="smallint" defaultValue="0"/>
|
||||||
|
</addColumn>
|
||||||
|
|
||||||
|
<dropUniqueConstraint tableName="accounts" constraintName="accounts_number_key" />
|
||||||
|
<addUniqueConstraint constraintName="account_number_device_unique" tableName="accounts" columnNames="number, device_id" />
|
||||||
|
|
||||||
|
<addColumn tableName="keys">
|
||||||
|
<column name="device_id" type="bigint" >
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
|
||||||
|
<createTable tableName="pending_devices">
|
||||||
|
<column name="id" type="bigint" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="number" type="varchar(255)">
|
||||||
|
<constraints unique="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="verification_code" type="varchar(255)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<createTable tableName="stored_messages">
|
||||||
|
<column name="id" type="bigint" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="destination_id" type="bigint">
|
||||||
|
<constraints nullable="false" foreignKeyName="destination_fk" references="accounts(id)"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="encrypted_message" type="text">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.sun.jersey.api.client.ClientResponse;
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
import com.yammer.dropwizard.testing.ResourceTest;
|
import com.yammer.dropwizard.testing.ResourceTest;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
import org.junit.Test;
|
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.controllers.AccountController;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
|
@ -12,19 +18,50 @@ import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
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 javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
import static org.fest.assertions.api.Assertions.assertThat;
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Matchers.anyString;
|
import static org.mockito.Matchers.anyString;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
public class AccountControllerTest extends ResourceTest {
|
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 static final String SENDER = "+14152222222";
|
||||||
|
|
||||||
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
|
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
|
||||||
|
private PendingDevicesManager pendingDevicesManager = mock(PendingDevicesManager.class);
|
||||||
private AccountsManager accountsManager = mock(AccountsManager.class );
|
private AccountsManager accountsManager = mock(AccountsManager.class );
|
||||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
|
@ -40,10 +77,17 @@ public class AccountControllerTest extends ResourceTest {
|
||||||
|
|
||||||
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
|
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
|
||||||
|
|
||||||
addResource(new AccountController(pendingAccountsManager,
|
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901"));
|
||||||
accountsManager,
|
|
||||||
rateLimiters,
|
Mockito.doAnswer(new Answer() {
|
||||||
smsSender));
|
@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
|
@Test
|
||||||
|
@ -62,13 +106,17 @@ public class AccountControllerTest extends ResourceTest {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/accounts/code/%s", "1234"))
|
client().resource(String.format("/v1/accounts/code/%s", "1234"))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||||
.entity(new AccountAttributes("keykeykeykey", false))
|
.entity(new V1AccountAttributes("keykeykeykey", false))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
.put(ClientResponse.class);
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(204);
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
verify(accountsManager).create(isA(Account.class));
|
verify(accountsManager).createResetNumber(isA(Account.class));
|
||||||
|
|
||||||
|
ArgumentCaptor<String> number = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(pendingAccountsManager).remove(number.capture());
|
||||||
|
assertThat(number.getValue()).isEqualTo(SENDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -76,7 +124,7 @@ public class AccountControllerTest extends ResourceTest {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/accounts/code/%s", "1111"))
|
client().resource(String.format("/v1/accounts/code/%s", "1111"))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||||
.entity(new AccountAttributes("keykeykeykey", false))
|
.entity(new V1AccountAttributes("keykeykeykey", false))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
.put(ClientResponse.class);
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
|
@ -85,4 +133,28 @@ public class AccountControllerTest extends ResourceTest {
|
||||||
verifyNoMoreInteractions(accountsManager);
|
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<Account> newAccount = ArgumentCaptor.forClass(Account.class);
|
||||||
|
verify(accountsManager).createAccountOnExistingNumber(newAccount.capture());
|
||||||
|
assertThat(deviceId).isEqualTo(newAccount.getValue().getDeviceId());
|
||||||
|
|
||||||
|
ArgumentCaptor<String> number = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(pendingDevicesManager).remove(number.capture());
|
||||||
|
assertThat(number.getValue()).isEqualTo(AuthHelper.VALID_NUMBER);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
import com.sun.jersey.api.client.ClientResponse;
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
|
import com.sun.jersey.api.client.GenericType;
|
||||||
import com.yammer.dropwizard.testing.ResourceTest;
|
import com.yammer.dropwizard.testing.ResourceTest;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
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.storage.Keys;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
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.fest.assertions.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ -18,26 +27,41 @@ public class KeyControllerTest extends ResourceTest {
|
||||||
private final String EXISTS_NUMBER = "+14152222222";
|
private final String EXISTS_NUMBER = "+14152222222";
|
||||||
private final String NOT_EXISTS_NUMBER = "+14152222220";
|
private final String NOT_EXISTS_NUMBER = "+14152222220";
|
||||||
|
|
||||||
private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, 1234, "test1", "test2", false);
|
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);
|
private final Keys keys = mock(Keys.class);
|
||||||
|
|
||||||
|
Account[] fakeAccount;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUpResources() {
|
protected void setUpResources() {
|
||||||
addProvider(AuthHelper.getAuthenticator());
|
addProvider(AuthHelper.getAuthenticator());
|
||||||
|
|
||||||
RateLimiters rateLimiters = mock(RateLimiters.class);
|
RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
RateLimiter rateLimiter = mock(RateLimiter.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(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
|
||||||
|
|
||||||
when(keys.get(EXISTS_NUMBER)).thenReturn(SAMPLE_KEY);
|
when(keys.get(eq(EXISTS_NUMBER), anyList())).thenReturn(new UnstructuredPreKeyList(Arrays.asList(SAMPLE_KEY, SAMPLE_KEY2)));
|
||||||
when(keys.get(NOT_EXISTS_NUMBER)).thenReturn(null);
|
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<Account>());
|
||||||
|
|
||||||
|
addResource(new KeysController.V1(rateLimiters, keys, accounts, null));
|
||||||
|
addResource(new KeysController.V2(rateLimiters, keys, accounts, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validRequestTest() throws Exception {
|
public void validRequestsTest() throws Exception {
|
||||||
PreKey result = client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER))
|
PreKey result = client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(PreKey.class);
|
.get(PreKey.class);
|
||||||
|
@ -49,7 +73,32 @@ public class KeyControllerTest extends ResourceTest {
|
||||||
assertThat(result.getId() == 0);
|
assertThat(result.getId() == 0);
|
||||||
assertThat(result.getNumber() == null);
|
assertThat(result.getNumber() == null);
|
||||||
|
|
||||||
verify(keys).get(EXISTS_NUMBER);
|
verify(keys).get(eq(EXISTS_NUMBER), eq(Arrays.asList(fakeAccount)));
|
||||||
|
verifyNoMoreInteractions(keys);
|
||||||
|
|
||||||
|
List<PreKey> results = client().resource(String.format("/v2/keys/%s", EXISTS_NUMBER))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(new GenericType<List<PreKey>>(){});
|
||||||
|
|
||||||
|
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
|
@Test
|
||||||
|
@ -60,7 +109,7 @@ public class KeyControllerTest extends ResourceTest {
|
||||||
|
|
||||||
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(404);
|
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(404);
|
||||||
|
|
||||||
verify(keys).get(NOT_EXISTS_NUMBER);
|
verify(keys).get(NOT_EXISTS_NUMBER, new LinkedList<Account>());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -14,7 +14,7 @@ public class PreKeyTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serializeToJSON() throws Exception {
|
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",
|
assertThat("Basic Contact Serialization works",
|
||||||
asJson(preKey),
|
asJson(preKey),
|
||||||
|
|
|
@ -15,6 +15,7 @@ import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class AuthHelper {
|
public class AuthHelper {
|
||||||
|
public static final long DEFAULT_DEVICE_ID = 1;
|
||||||
|
|
||||||
public static final String VALID_NUMBER = "+14150000000";
|
public static final String VALID_NUMBER = "+14150000000";
|
||||||
public static final String VALID_PASSWORD = "foo";
|
public static final String VALID_PASSWORD = "foo";
|
||||||
|
@ -29,7 +30,7 @@ public class AuthHelper {
|
||||||
|
|
||||||
when(credentials.verify("foo")).thenReturn(true);
|
when(credentials.verify("foo")).thenReturn(true);
|
||||||
when(account.getAuthenticationCredentials()).thenReturn(credentials);
|
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()),
|
return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(new FederationConfiguration()),
|
||||||
FederatedPeer.class,
|
FederatedPeer.class,
|
||||||
|
@ -41,4 +42,7 @@ public class AuthHelper {
|
||||||
return "Basic " + Base64.encodeBytes((number + ":" + password).getBytes());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue