mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-26 16:43:24 +01:00
New API to support multiple accounts per # (FREEBIE)
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user