Server side support for delivery receipts.

This commit is contained in:
Moxie Marlinspike 2014-07-25 15:48:34 -07:00
parent 160c0bfe14
commit 4eb88a3e02
23 changed files with 1259 additions and 558 deletions

View File

@ -20,6 +20,15 @@ option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos";
message OutgoingMessageSignal {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
optional uint32 type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;

View File

@ -37,6 +37,7 @@ import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@ -176,6 +177,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
environment.jersey().register(new ReceiptController(accountsManager, federatedClientManager, pushSender));
environment.jersey().register(attachmentController);
environment.jersey().register(keysControllerV1);
environment.jersey().register(keysControllerV2);
@ -183,6 +185,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
if (config.getWebsocketConfiguration().isEnabled()) {
WebsocketControllerFactory servlet = new WebsocketControllerFactory(deviceAuthenticator,
accountsManager,
pushSender,
storedMessages,
pubSubManager);

View File

@ -142,7 +142,7 @@ public class MessageController {
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
if (destinationDevice.isPresent()) {
sendLocalMessage(source, destination, destinationDevice.get(), incomingMessage);
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
}
}
}
@ -150,6 +150,7 @@ public class MessageController {
private void sendLocalMessage(Account source,
Account destinationAccount,
Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage)
throws NoSuchUserException, IOException
{
@ -159,7 +160,7 @@ public class MessageController {
messageBuilder.setType(incomingMessage.getType())
.setSource(source.getNumber())
.setTimestamp(System.currentTimeMillis())
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
if (messageBody.isPresent()) {

View File

@ -0,0 +1,108 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
import io.dropwizard.auth.Auth;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
@Path("/v1/receipt")
public class ReceiptController {
private final AccountsManager accountManager;
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
public ReceiptController(AccountsManager accountManager,
FederatedClientManager federatedClientManager,
PushSender pushSender)
{
this.accountManager = accountManager;
this.federatedClientManager = federatedClientManager;
this.pushSender = pushSender;
}
@Timed
@PUT
@Path("/{destination}/{messageId}")
public void sendDeliveryReceipt(@Auth Account source,
@PathParam("destination") String destination,
@PathParam("messageId") long messageId,
@QueryParam("relay") Optional<String> relay)
throws IOException
{
try {
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
else sendDirectReceipt(source, destination, messageId);
} catch (NoSuchUserException | NotPushRegisteredException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
} catch (TransientPushFailureException e) {
throw new IOException(e);
}
}
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
throws NoSuchUserException, IOException
{
try {
federatedClientManager.getClient(relay)
.sendDeliveryReceipt(source.getNumber(),
source.getAuthenticatedDevice().get().getId(),
destination, messageId);
} catch (NoSuchPeerException e) {
throw new NoSuchUserException(e);
}
}
private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{
Account destinationAccount = getDestinationAccount(destination);
List<Device> destinationDevices = destinationAccount.getDevices();
OutgoingMessageSignal.Builder message =
OutgoingMessageSignal.newBuilder()
.setSource(source.getNumber())
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
.setTimestamp(messageId)
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
if (source.getRelay().isPresent()) {
message.setRelay(source.getRelay().get());
}
for (Device destinationDevice : destinationDevices) {
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
}
}
private Account getDestinationAccount(String destination)
throws NoSuchUserException
{
Optional<Account> account = accountManager.get(destination);
if (!account.isPresent()) {
throw new NoSuchUserException(destination);
}
return account.get();
}
}

View File

@ -16,11 +16,13 @@ import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.textsecuregcm.websocket.WebsocketMessage;
@ -33,14 +35,16 @@ import java.util.Map;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.basic.BasicCredentials;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
public class WebsocketController implements WebSocketListener, PubSubListener {
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
private static final ObjectMapper mapper = new ObjectMapper();
private static final ObjectMapper mapper = SystemMapper.getMapper();
private static final Map<Long, PendingMessage> pendingMessages = new HashMap<>();
private final AccountAuthenticator accountAuthenticator;
private final AccountsManager accountsManager;
private final PubSubManager pubSubManager;
private final StoredMessages storedMessages;
private final PushSender pushSender;
@ -53,11 +57,13 @@ public class WebsocketController implements WebSocketListener, PubSubListener {
private long pendingMessageSequence;
public WebsocketController(AccountAuthenticator accountAuthenticator,
AccountsManager accountsManager,
PushSender pushSender,
PubSubManager pubSubManager,
StoredMessages storedMessages)
{
this.accountAuthenticator = accountAuthenticator;
this.accountsManager = accountsManager;
this.pushSender = pushSender;
this.pubSubManager = pubSubManager;
this.storedMessages = storedMessages;
@ -186,10 +192,16 @@ public class WebsocketController implements WebSocketListener, PubSubListener {
private void handleMessageAck(String message) {
try {
AcknowledgeWebsocketMessage ack = mapper.readValue(message, AcknowledgeWebsocketMessage.class);
PendingMessage acknowledgedMessage;
synchronized (pendingMessages) {
pendingMessages.remove(ack.getId());
acknowledgedMessage = pendingMessages.remove(ack.getId());
}
if (acknowledgedMessage != null && !acknowledgedMessage.isReceipt()) {
sendDeliveryReceipt(acknowledgedMessage);
}
} catch (IOException e) {
logger.warn("Mapping", e);
}
@ -203,6 +215,30 @@ public class WebsocketController implements WebSocketListener, PubSubListener {
}
}
private void sendDeliveryReceipt(PendingMessage acknowledgedMessage) {
try {
Optional<Account> source = accountsManager.get(acknowledgedMessage.getSender());
if (!source.isPresent()) {
logger.warn("Source account disappeared? (%s)", acknowledgedMessage.getSender());
return;
}
OutgoingMessageSignal.Builder receipt =
OutgoingMessageSignal.newBuilder()
.setSource(account.getNumber())
.setSourceDevice((int) device.getId())
.setTimestamp(acknowledgedMessage.getMessageId())
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
for (Device device : source.get().getDevices()) {
pushSender.sendMessage(source.get(), device, receipt.build());
}
} catch (NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("Websocket", "Delivery receipet", e);
}
}
@Override
public void onWebSocketBinary(byte[] bytes, int i, int i2) {
logger.info("Received binary message!");

View File

@ -41,7 +41,7 @@ public class IncomingMessage {
private String relay;
@JsonProperty
private long timestamp;
private long timestamp; // deprecated
public String getDestination() {

View File

@ -32,6 +32,9 @@ public class IncomingMessageList {
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
public IncomingMessageList() {}
public List<IncomingMessage> getMessages() {
@ -45,4 +48,8 @@ public class IncomingMessageList {
public void setRelay(String relay) {
this.relay = relay;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@ -13,11 +13,15 @@ public class PendingMessage {
@JsonProperty
private String encryptedOutgoingMessage;
@JsonProperty
private boolean receipt;
public PendingMessage() {}
public PendingMessage(String sender, long messageId, String encryptedOutgoingMessage) {
public PendingMessage(String sender, long messageId, boolean receipt, String encryptedOutgoingMessage) {
this.sender = sender;
this.messageId = messageId;
this.receipt = receipt;
this.encryptedOutgoingMessage = encryptedOutgoingMessage;
}
@ -33,6 +37,10 @@ public class PendingMessage {
return sender;
}
public boolean isReceipt() {
return receipt;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof PendingMessage)) return false;
@ -41,11 +49,12 @@ public class PendingMessage {
return
this.sender.equals(that.sender) &&
this.messageId == that.messageId &&
this.receipt == that.receipt &&
this.encryptedOutgoingMessage.equals(that.encryptedOutgoingMessage);
}
@Override
public int hashCode() {
return this.sender.hashCode() ^ (int)this.messageId ^ this.encryptedOutgoingMessage.hashCode();
return this.sender.hashCode() ^ (int)this.messageId ^ this.encryptedOutgoingMessage.hashCode() ^ (receipt ? 1 : 0);
}
}

View File

@ -69,6 +69,7 @@ public class FederatedClient {
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
private final FederatedPeer peer;
private final Client client;
@ -197,6 +198,25 @@ public class FederatedClient {
}
}
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException
{
try {
String path = String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId);
WebResource resource = client.resource(peer.getUrl()).path(path);
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.put(ClientResponse.class);
if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new WebApplicationException(clientResponseToResponse(response));
}
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
}
}
private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
}

View File

@ -19,13 +19,13 @@ package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsService;
import com.notnoop.exceptions.NetworkIOException;
import net.spy.memcached.MemcachedClient;
import org.bouncycastle.openssl.PEMReader;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
@ -36,6 +36,7 @@ import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
@ -69,7 +70,7 @@ public class APNSender implements Managed {
private static final String MESSAGE_BODY = "m";
private static final ObjectMapper mapper = new ObjectMapper();
private static final ObjectMapper mapper = SystemMapper.getMapper();
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
@ -112,7 +113,10 @@ public class APNSender implements Managed {
} else {
memcacheSet(registrationId, account.getNumber());
storedMessages.insert(account.getId(), device.getId(), message);
sendPush(registrationId, serializedPendingMessage);
if (!message.isReceipt()) {
sendPush(registrationId, serializedPendingMessage);
}
}
} catch (IOException e) {
throw new TransientPushFailureException(e);

View File

@ -87,9 +87,11 @@ public class GCMSender implements Managed, PacketListener {
public void sendMessage(String messageId, UnacknowledgedMessage message) {
try {
boolean isReceipt = message.getPendingMessage().isReceipt();
Map<String, String> dataObject = new HashMap<>();
dataObject.put("type", "message");
dataObject.put("message", message.getPendingMessage().getEncryptedOutgoingMessage());
dataObject.put(isReceipt ? "receipt" : "message", message.getPendingMessage().getEncryptedOutgoingMessage());
Map<String, Object> messageObject = new HashMap<>();
messageObject.put("to", message.getRegistrationId());

View File

@ -25,6 +25,8 @@ import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
public class PushSender {
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
@ -42,13 +44,17 @@ public class PushSender {
this.webSocketSender = websocketSender;
}
public void sendMessage(Account account, Device device, MessageProtos.OutgoingMessageSignal message)
public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
throws NotPushRegisteredException, TransientPushFailureException
{
try {
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
String signalingKey = device.getSignalingKey();
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, signalingKey);
PendingMessage pendingMessage = new PendingMessage(message.getSource(), message.getTimestamp(), encryptedMessage.serialize());
PendingMessage pendingMessage = new PendingMessage(message.getSource(),
message.getTimestamp(),
isReceipt,
encryptedMessage.serialize());
sendMessage(account, device, pendingMessage);
} catch (CryptoEncodingException e) {

View File

@ -31,6 +31,7 @@ import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static com.codahale.metrics.MetricRegistry.name;
@ -43,7 +44,7 @@ public class WebsocketSender {
private final Meter onlineMeter = metricRegistry.meter(name(getClass(), "online"));
private final Meter offlineMeter = metricRegistry.meter(name(getClass(), "offline"));
private static final ObjectMapper mapper = new ObjectMapper();
private static final ObjectMapper mapper = SystemMapper.getMapper();
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;

View File

@ -33,6 +33,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.SystemMapper;
import java.io.IOException;
import java.lang.annotation.Annotation;
@ -51,12 +52,7 @@ public abstract class Accounts {
private static final String NUMBER = "number";
private static final String DATA = "data";
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}
private static final ObjectMapper mapper = SystemMapper.getMapper();
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DATA + ") VALUES (:number, CAST(:data AS json))")
@GetGeneratedKeys

View File

@ -17,6 +17,8 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
@ -24,6 +26,7 @@ import net.spy.memcached.MemcachedClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
@ -46,7 +49,7 @@ public class AccountsManager {
this.accounts = accounts;
this.directory = directory;
this.memcachedClient = memcachedClient;
this.mapper = new ObjectMapper();
this.mapper = SystemMapper.getMapper();
}
public long getCount() {

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
@ -18,7 +19,7 @@ import redis.clients.jedis.JedisPubSub;
public class PubSubManager {
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final ObjectMapper mapper = new ObjectMapper();
private final ObjectMapper mapper = SystemMapper.getMapper();
private final SubscriptionListener baseListener = new SubscriptionListener();
private final Map<WebsocketAddress, PubSubListener> listeners = new HashMap<>();

View File

@ -26,6 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import java.util.LinkedList;
@ -43,7 +44,7 @@ public class StoredMessages {
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
private static final ObjectMapper mapper = new ObjectMapper();
private static final ObjectMapper mapper = SystemMapper.getMapper();
private static final String QUEUE_PREFIX = "msgs";
private final JedisPool jedisPool;

View File

@ -1,41 +0,0 @@
/**
* Copyright (C) 2014 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.util;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CORSHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
((HttpServletResponse) response).addHeader("Access-Control-Allow-Origin", "*");
((HttpServletResponse) response).addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
((HttpServletResponse) response).addHeader("Access-Control-Allow-Headers", "Authorization, Content-type");
}
chain.doFilter(request, response);
}
@Override public void init(FilterConfig filterConfig) throws ServletException { }
@Override public void destroy() { }
}

View File

@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SystemMapper {
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}
public static ObjectMapper getMapper() {
return mapper;
}
}

View File

@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.controllers.WebsocketController;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
@ -23,13 +23,16 @@ public class WebsocketControllerFactory extends WebSocketServlet implements WebS
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
private final AccountAuthenticator accountAuthenticator;
private final AccountsManager accounts;
public WebsocketControllerFactory(AccountAuthenticator accountAuthenticator,
AccountsManager accounts,
PushSender pushSender,
StoredMessages storedMessages,
PubSubManager pubSubManager)
{
this.accountAuthenticator = accountAuthenticator;
this.accounts = accounts;
this.pushSender = pushSender;
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
@ -42,6 +45,6 @@ public class WebsocketControllerFactory extends WebSocketServlet implements WebS
@Override
public Object createWebSocket(UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse) {
return new WebsocketController(accountAuthenticator, pushSender, pubSubManager, storedMessages);
return new WebsocketController(accountAuthenticator, accounts, pushSender, pubSubManager, storedMessages);
}
}

View File

@ -0,0 +1,91 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
public class ReceiptControllerTest {
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
private final PushSender pushSender = mock(PushSender.class );
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
private final AccountsManager accountsManager = mock(AccountsManager.class );
private final ObjectMapper mapper = new ObjectMapper();
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator())
.addResource(new ReceiptController(accountsManager, federatedClientManager, pushSender))
.build();
@Before
public void setup() throws Exception {
List<Device> singleDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
}};
List<Device> multiDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, false, multiDeviceList);
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
}
@Test
public synchronized void testSingleDeviceCurrent() throws Exception {
ClientResponse response =
resources.client().resource(String.format("/v1/receipt/%s/%d", SINGLE_DEVICE_RECIPIENT, 1234))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class);
assertThat(response.getStatus() == 204);
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
}
@Test
public synchronized void testMultiDeviceCurrent() throws Exception {
ClientResponse response =
resources.client().resource(String.format("/v1/receipt/%s/%d", MULTI_DEVICE_RECIPIENT, 12345))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class);
assertThat(response.getStatus() == 204);
verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
}
}

View File

@ -11,9 +11,11 @@ import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.controllers.WebsocketController;
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
@ -40,6 +42,7 @@ public class WebsocketControllerTest {
private static final StoredMessages storedMessages = mock(StoredMessages.class);
private static final AccountAuthenticator accountAuthenticator = mock(AccountAuthenticator.class);
private static final AccountsManager accountsManager = mock(AccountsManager.class);
private static final PubSubManager pubSubManager = mock(PubSubManager.class );
private static final Account account = mock(Account.class );
private static final Device device = mock(Device.class );
@ -57,7 +60,7 @@ public class WebsocketControllerTest {
when(session.getUpgradeRequest()).thenReturn(upgradeRequest);
WebsocketController controller = new WebsocketController(accountAuthenticator, pushSender, pubSubManager, storedMessages);
WebsocketController controller = new WebsocketController(accountAuthenticator, accountsManager, pushSender, pubSubManager, storedMessages);
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
put("login", new String[] {VALID_USER});
@ -85,17 +88,30 @@ public class WebsocketControllerTest {
RemoteEndpoint remote = mock(RemoteEndpoint.class);
List<PendingMessage> outgoingMessages = new LinkedList<PendingMessage>() {{
add(new PendingMessage("sender1", 1111, "first"));
add(new PendingMessage("sender1", 2222, "second"));
add(new PendingMessage("sender2", 3333, "third"));
add(new PendingMessage("sender1", 1111, false, "first"));
add(new PendingMessage("sender1", 2222, false, "second"));
add(new PendingMessage("sender2", 3333, false, "third"));
}};
when(device.getId()).thenReturn(2L);
when(account.getId()).thenReturn(31337L);
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
when(account.getNumber()).thenReturn("+14152222222");
when(session.getRemote()).thenReturn(remote);
when(session.getUpgradeRequest()).thenReturn(upgradeRequest);
final Device sender1device = mock(Device.class);
List<Device> sender1devices = new LinkedList<Device>() {{
add(sender1device);
}};
Account sender1 = mock(Account.class);
when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
put("login", new String[] {VALID_USER});
put("password", new String[] {VALID_PASSWORD});
@ -107,7 +123,7 @@ public class WebsocketControllerTest {
when(storedMessages.getMessagesForDevice(account.getId(), device.getId()))
.thenReturn(outgoingMessages);
WebsocketControllerFactory factory = new WebsocketControllerFactory(accountAuthenticator, pushSender, storedMessages, pubSubManager);
WebsocketControllerFactory factory = new WebsocketControllerFactory(accountAuthenticator, accountsManager, pushSender, storedMessages, pubSubManager);
WebsocketController controller = (WebsocketController) factory.createWebSocket(null, null);
controller.onWebSocketConnect(session);
@ -119,12 +135,12 @@ public class WebsocketControllerTest {
controller.onWebSocketClose(1000, "Closed");
List<PendingMessage> pending = new LinkedList<PendingMessage>() {{
add(new PendingMessage("sender1", 1111, "first"));
add(new PendingMessage("sender2", 3333, "third"));
add(new PendingMessage("sender1", 1111, false, "first"));
add(new PendingMessage("sender2", 3333, false, "third"));
}};
verify(pushSender, times(2)).sendMessage(eq(account), eq(device), any(PendingMessage.class));
verify(pushSender, times(1)).sendMessage(eq(sender1), eq(sender1device), any(MessageProtos.OutgoingMessageSignal.class));
}
}