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