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

@@ -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!");