Server side support for delivery receipts.
This commit is contained in:
parent
160c0bfe14
commit
4eb88a3e02
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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!");
|
||||
|
|
|
@ -41,7 +41,7 @@ public class IncomingMessage {
|
|||
private String relay;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
private long timestamp; // deprecated
|
||||
|
||||
|
||||
public String getDestination() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() { }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue