Squashed History

This commit is contained in:
Moxie Marlinspike
2013-12-08 23:11:09 -08:00
commit 4ad0dad3d9
103 changed files with 9737 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
/**
* 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.controllers;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.sms.SenderFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@Path("/v1/accounts")
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 SenderFactory senderFactory;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
SenderFactory smsSenderFactory)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.senderFactory = smsSenderFactory;
}
@Timed
@GET
@Path("/{transport}/code/{number}")
public Response createAccount(@PathParam("transport") String transport,
@PathParam("number") String number)
throws IOException, RateLimitExceededException
{
if (!Util.isValidNumber(number)) {
logger.debug("Invalid number: " + number);
throw new WebApplicationException(Response.status(400).build());
}
switch (transport) {
case "sms":
rateLimiters.getSmsDestinationLimiter().validate(number);
break;
case "voice":
rateLimiters.getVoiceDestinationLimiter().validate(number);
break;
default:
throw new WebApplicationException(Response.status(415).build());
}
VerificationCode verificationCode = generateVerificationCode();
pendingAccounts.store(number, verificationCode.getVerificationCode());
if (transport.equals("sms")) {
senderFactory.getSmsSender(number).deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
} else if (transport.equals("voice")) {
senderFactory.getVoxSender(number).deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
}
return Response.ok().build();
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Path("/code/{verification_code}")
public void verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = new AuthorizationHeader(authorizationHeader);
String number = header.getUserName();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
Optional<String> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
if (!storedVerificationCode.isPresent() ||
!verificationCode.equals(storedVerificationCode.get()))
{
throw new WebApplicationException(Response.status(403).build());
}
Account account = new Account();
account.setNumber(number);
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
account.setSignalingKey(accountAttributes.getSignalingKey());
account.setSupportsSms(accountAttributes.getSupportsSms());
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/")
@Consumes(MediaType.APPLICATION_JSON)
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
account.setApnRegistrationId(null);
account.setGcmRegistrationId(registrationId.getGcmRegistrationId());
accounts.update(account);
}
@Timed
@DELETE
@Path("/gcm/")
public void deleteGcmRegistrationId(@Auth Account account) {
account.setGcmRegistrationId(null);
accounts.update(account);
}
@Timed
@PUT
@Path("/apn/")
@Consumes(MediaType.APPLICATION_JSON)
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
account.setApnRegistrationId(registrationId.getApnRegistrationId());
account.setGcmRegistrationId(null);
accounts.update(account);
}
@Timed
@DELETE
@Path("/apn/")
public void deleteApnRegistrationId(@Auth Account account) {
account.setApnRegistrationId(null);
accounts.update(account);
}
private VerificationCode generateVerificationCode() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int randomInt = 100000 + random.nextInt(900000);
return new VerificationCode(randomInt);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,113 @@
/**
* 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.controllers;
import com.amazonaws.HttpMethod;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
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.util.Conversions;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@Path("/v1/attachments")
public class AttachmentController {
private final Logger logger = LoggerFactory.getLogger(AttachmentController.class);
private final RateLimiters rateLimiters;
private final FederatedClientManager federatedClientManager;
private final UrlSigner urlSigner;
public AttachmentController(RateLimiters rateLimiters,
FederatedClientManager federatedClientManager,
UrlSigner urlSigner)
{
this.rateLimiters = rateLimiters;
this.federatedClientManager = federatedClientManager;
this.urlSigner = urlSigner;
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response allocateAttachment(@Auth Account account) throws RateLimitExceededException {
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
long attachmentId = generateAttachmentId();
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);
AttachmentDescriptor descriptor = new AttachmentDescriptor(attachmentId, url.toExternalForm());
return Response.ok().entity(descriptor).build();
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{attachmentId}")
public Response redirectToAttachment(@Auth Account account,
@PathParam("attachmentId") long attachmentId,
@QueryParam("relay") String relay)
{
try {
URL url;
if (relay == null) url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
else url = federatedClientManager.getClient(relay).getSignedAttachmentUri(attachmentId);
return Response.ok().entity(new AttachmentUri(url)).build();
} catch (IOException e) {
logger.warn("No conectivity", e);
return Response.status(500).build();
} catch (NoSuchPeerException e) {
logger.info("No such peer: " + relay);
return Response.status(404).build();
}
}
private long generateAttachmentId() {
try {
byte[] attachmentBytes = new byte[8];
SecureRandom.getInstance("SHA1PRNG").nextBytes(attachmentBytes);
attachmentBytes[0] = (byte)(attachmentBytes[0] & 0x7F);
return Conversions.byteArrayToLong(attachmentBytes);
} catch (NoSuchAlgorithmException nsae) {
throw new AssertionError(nsae);
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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.controllers;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Metered;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
@Path("/v1/directory")
public class DirectoryController {
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
private final RateLimiters rateLimiters;
private final DirectoryManager directory;
public DirectoryController(RateLimiters rateLimiters, DirectoryManager directory) {
this.directory = directory;
this.rateLimiters = rateLimiters;
}
@Timed()
@GET
@Path("/{token}")
@Produces(MediaType.APPLICATION_JSON)
public Response getTokenPresence(@Auth Account account, @PathParam("token") String token)
throws RateLimitExceededException
{
rateLimiters.getContactsLimiter().validate(account.getNumber());
try {
Optional<ClientContact> contact = directory.get(Base64.decodeWithoutPadding(token));
if (contact.isPresent()) return Response.ok().entity(contact.get()).build();
else return Response.status(404).build();
} catch (IOException e) {
logger.info("Bad token", e);
return Response.status(404).build();
}
}
@Timed()
@PUT
@Path("/tokens")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public ClientContacts getContactIntersection(@Auth Account account, @Valid ClientContactTokens contacts)
throws RateLimitExceededException
{
rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size());
try {
List<byte[]> tokens = new LinkedList<>();
for (String encodedContact : contacts.getContacts()) {
tokens.add(Base64.decodeWithoutPadding(encodedContact));
}
List<ClientContact> intersection = directory.get(tokens);
return new ClientContacts(intersection);
} catch (IOException e) {
logger.info("Bad token", e);
throw new WebApplicationException(Response.status(400).build());
}
}
}

View File

@@ -0,0 +1,157 @@
/**
* 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.controllers;
import com.amazonaws.HttpMethod;
import com.google.protobuf.InvalidProtocolBufferException;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
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.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.UrlSigner;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
@Path("/v1/federation")
public class FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationController.class);
private static final int ACCOUNT_CHUNK_SIZE = 10000;
private final PushSender pushSender;
private final Keys keys;
private final AccountsManager accounts;
private final UrlSigner urlSigner;
public FederationController(Keys keys, AccountsManager accounts, PushSender pushSender, UrlSigner urlSigner) {
this.keys = keys;
this.accounts = accounts;
this.pushSender = pushSender;
this.urlSigner = urlSigner;
}
@Timed
@GET
@Path("/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId)
{
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
return new AttachmentUri(url);
}
@Timed
@GET
@Path("/key/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
{
PreKey preKey = keys.get(number);
if (preKey == null) {
throw new WebApplicationException(Response.status(404).build());
}
return preKey;
}
@Timed
@PUT
@Path("/message")
@Consumes(MediaType.APPLICATION_JSON)
public void relayMessage(@Auth FederatedPeer peer, @Valid RelayMessage message)
throws IOException
{
try {
OutgoingMessageSignal signal = OutgoingMessageSignal.parseFrom(message.getOutgoingMessageSignal())
.toBuilder()
.setRelay(peer.getName())
.build();
pushSender.sendMessage(message.getDestination(), signal);
} catch (InvalidProtocolBufferException ipe) {
logger.warn("ProtoBuf", ipe);
throw new WebApplicationException(Response.status(400).build());
} catch (NoSuchUserException e) {
logger.debug("No User", e);
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/user_count")
@Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount());
}
@Timed
@GET
@Path("/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(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());
if (Util.isEmpty(account.getApnRegistrationId()) &&
Util.isEmpty(account.getGcmRegistrationId()))
{
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}
return new ClientContacts(clientContacts);
}
}

View File

@@ -0,0 +1,92 @@
/**
* 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.controllers;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyList;
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.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/v1/keys")
public class KeysController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final RateLimiters rateLimiters;
private final Keys keys;
private final FederatedClientManager federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys,
FederatedClientManager federatedClientManager)
{
this.rateLimiters = rateLimiters;
this.keys = keys;
this.federatedClientManager = federatedClientManager;
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
keys.store(account.getNumber(), 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
{
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number);
try {
PreKey key;
if (relay == null) key = keys.get(number);
else key = federatedClientManager.getClient(relay).getKey(number);
if (key == null) throw new WebApplicationException(Response.status(404).build());
else return key;
} catch (NoSuchPeerException e) {
logger.info("No peer: " + relay);
throw new WebApplicationException(Response.status(404).build());
}
}
}

View File

@@ -0,0 +1,313 @@
/**
* 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.controllers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.google.protobuf.ByteString;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.core.TimerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.MessageResponse;
import org.whispersystems.textsecuregcm.federation.FederatedClient;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.IterablePair.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MessageController extends HttpServlet {
public static final String PATH = "/v1/messages/";
private final Meter successMeter = Metrics.newMeter(MessageController.class, "deliver_message", "success", TimeUnit.MINUTES);
private final Meter failureMeter = Metrics.newMeter(MessageController.class, "deliver_message", "failure", TimeUnit.MINUTES);
private final Timer timer = Metrics.newTimer(MessageController.class, "deliver_message_time", TimeUnit.MILLISECONDS, TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
private final RateLimiters rateLimiters;
private final AccountAuthenticator accountAuthenticator;
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
private final ObjectMapper objectMapper;
private final ExecutorService executor;
public MessageController(RateLimiters rateLimiters,
AccountAuthenticator accountAuthenticator,
PushSender pushSender,
FederatedClientManager federatedClientManager)
{
this.rateLimiters = rateLimiters;
this.accountAuthenticator = accountAuthenticator;
this.pushSender = pushSender;
this.federatedClientManager = federatedClientManager;
this.objectMapper = new ObjectMapper();
this.executor = Executors.newFixedThreadPool(10);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
TimerContext timerContext = timer.time();
try {
Account sender = authenticate(req);
IncomingMessageList messages = parseIncomingMessages(req);
rateLimiters.getMessagesLimiter().validate(sender.getNumber());
List<IncomingMessage> incomingMessages = messages.getMessages();
List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(),
incomingMessages);
IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages,
outgoingMessages);
handleAsyncDelivery(timerContext, req.startAsync(), listPair);
} catch (AuthenticationException e) {
failureMeter.mark();
timerContext.stop();
resp.setStatus(401);
} catch (ValidationException e) {
failureMeter.mark();
timerContext.stop();
resp.setStatus(415);
} catch (IOException e) {
logger.warn("IOE", e);
failureMeter.mark();
timerContext.stop();
resp.setStatus(501);
} catch (RateLimitExceededException e) {
timerContext.stop();
failureMeter.mark();
resp.setStatus(413);
}
}
private void handleAsyncDelivery(final TimerContext timerContext,
final AsyncContext context,
final IterablePair<IncomingMessage, OutgoingMessageSignal> listPair)
{
executor.submit(new Runnable() {
@Override
public void run() {
List<String> success = new LinkedList<>();
List<String> failure = new LinkedList<>();
HttpServletResponse response = (HttpServletResponse) context.getResponse();
try {
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
String destination = messagePair.first().getDestination();
String relay = messagePair.first().getRelay();
try {
if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
else sendRelayMessage(relay, destination, messagePair.second());
success.add(destination);
} catch (NoSuchUserException e) {
logger.debug("No such user", e);
failure.add(destination);
}
}
byte[] responseData = serializeResponse(new MessageResponse(success, failure));
response.setContentLength(responseData.length);
response.getOutputStream().write(responseData);
context.complete();
successMeter.mark();
} catch (IOException e) {
logger.warn("Async Handler", e);
failureMeter.mark();
response.setStatus(501);
context.complete();
}
timerContext.stop();
}
});
}
private void sendLocalMessage(String destination, OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
pushSender.sendMessage(destination, outgoingMessage);
}
private void sendRelayMessage(String relay, String destination, OutgoingMessageSignal outgoingMessage)
throws IOException, NoSuchUserException
{
try {
FederatedClient client = federatedClientManager.getClient(relay);
client.sendMessage(destination, outgoingMessage);
} catch (NoSuchPeerException e) {
logger.info("No such peer", e);
throw new NoSuchUserException(e);
}
}
private List<OutgoingMessageSignal> getOutgoingMessageSignals(String number,
List<IncomingMessage> incomingMessages)
{
List<OutgoingMessageSignal> outgoingMessages = new LinkedList<>();
for (IncomingMessage incoming : incomingMessages) {
OutgoingMessageSignal.Builder outgoingMessage = OutgoingMessageSignal.newBuilder();
outgoingMessage.setType(incoming.getType());
outgoingMessage.setSource(number);
byte[] messageBody = getMessageBody(incoming);
if (messageBody != null) {
outgoingMessage.setMessage(ByteString.copyFrom(messageBody));
}
outgoingMessage.setTimestamp(System.currentTimeMillis());
int index = 0;
for (IncomingMessage sub : incomingMessages) {
if (sub != incoming) {
outgoingMessage.setDestinations(index++, sub.getDestination());
}
}
outgoingMessages.add(outgoingMessage.build());
}
return outgoingMessages;
}
private byte[] getMessageBody(IncomingMessage message) {
try {
return Base64.decode(message.getBody());
} catch (IOException ioe) {
ioe.printStackTrace();
return null;
}
}
private byte[] serializeResponse(MessageResponse response) throws IOException {
try {
return objectMapper.writeValueAsBytes(response);
} catch (JsonProcessingException e) {
throw new IOException(e);
}
}
private IncomingMessageList parseIncomingMessages(HttpServletRequest request)
throws IOException, ValidationException
{
BufferedReader reader = request.getReader();
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
}
IncomingMessageList messages = objectMapper.readValue(content.toString(),
IncomingMessageList.class);
if (messages.getMessages() == null) {
throw new ValidationException();
}
for (IncomingMessage message : messages.getMessages()) {
if (message.getBody() == null) throw new ValidationException();
if (message.getDestination() == null) throw new ValidationException();
}
return messages;
}
private Account authenticate(HttpServletRequest request) throws AuthenticationException {
try {
AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization"));
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(),
authorizationHeader.getPassword() );
Optional<Account> account = accountAuthenticator.authenticate(credentials);
if (account.isPresent()) return account.get();
else throw new AuthenticationException("Bad credentials");
} catch (InvalidAuthorizationHeaderException e) {
throw new AuthenticationException(e);
}
}
// @Timed
// @POST
// @Consumes(MediaType.APPLICATION_JSON)
// @Produces(MediaType.APPLICATION_JSON)
// public MessageResponse sendMessage(@Auth Account sender, IncomingMessageList messages)
// throws IOException
// {
// List<String> success = new LinkedList<>();
// List<String> failure = new LinkedList<>();
// List<IncomingMessage> incomingMessages = messages.getMessages();
// List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(), incomingMessages);
//
// IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages, outgoingMessages);
//
// for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
// String destination = messagePair.first().getDestination();
// String relay = messagePair.first().getRelay();
//
// try {
// if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
// else sendRelayMessage(relay, destination, messagePair.second());
// success.add(destination);
// } catch (NoSuchUserException e) {
// logger.debug("No such user", e);
// failure.add(destination);
// }
// }
//
// return new MessageResponse(success, failure);
// }
}

View File

@@ -0,0 +1,45 @@
/**
* 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.controllers;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import java.util.LinkedList;
import java.util.List;
public class NoSuchUserException extends Exception {
private List<String> missing;
public NoSuchUserException(String user) {
super(user);
missing = new LinkedList<String>();
missing.add(user);
}
public NoSuchUserException(List<String> missing) {
this.missing = missing;
}
public NoSuchUserException(Exception e) {
super(e);
}
public List<String> getMissing() {
return missing;
}
}

View File

@@ -0,0 +1,23 @@
/**
* 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.controllers;
public class RateLimitExceededException extends Exception {
public RateLimitExceededException(String number) {
super(number);
}
}

View File

@@ -0,0 +1,21 @@
/**
* 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.controllers;
public class ValidationException extends Exception {
}