diff --git a/pom.xml b/pom.xml index a45c3b3e6..740ddb7d9 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ org.whispersystems.textsecure TextSecureServer - 0.4 + 0.5 diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java index a6ce2e785..e9714720f 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java @@ -76,7 +76,7 @@ public class FederationController { @PathParam("attachmentId") long attachmentId) throws IOException { - return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", peer.getName()), + return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()), attachmentId, Optional.absent()); } @@ -89,7 +89,7 @@ public class FederationController { throws IOException { try { - return keysController.get(new NonLimitedAccount("Unknown", peer.getName()), number, Optional.absent()); + return keysController.get(new NonLimitedAccount("Unknown", -1, peer.getName()), number, Optional.absent()); } catch (RateLimitExceededException e) { logger.warn("Rate limiting on federated channel", e); throw new IOException(e); @@ -106,7 +106,7 @@ public class FederationController { throws IOException { try { - return keysController.getDeviceKey(new NonLimitedAccount("Unknown", peer.getName()), + return keysController.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()), number, device, Optional.absent()); } catch (RateLimitExceededException e) { logger.warn("Rate limiting on federated channel", e); @@ -116,16 +116,17 @@ public class FederationController { @Timed @PUT - @Path("/messages/{source}/{destination}") - public void sendMessages(@Auth FederatedPeer peer, - @PathParam("source") String source, - @PathParam("destination") String destination, - @Valid IncomingMessageList messages) + @Path("/messages/{source}/{sourceDeviceId}/{destination}") + public void sendMessages(@Auth FederatedPeer peer, + @PathParam("source") String source, + @PathParam("sourceDeviceId") long sourceDeviceId, + @PathParam("destination") String destination, + @Valid IncomingMessageList messages) throws IOException { try { messages.setRelay(null); - messageController.sendMessage(new NonLimitedAccount(source, peer.getName()), destination, messages); + messageController.sendMessage(new NonLimitedAccount(source, sourceDeviceId, peer.getName()), destination, messages); } catch (RateLimitExceededException e) { logger.warn("Rate limiting on federated channel", e); throw new IOException(e); diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java index 7a2ee06ba..551437693 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java @@ -186,7 +186,8 @@ public class MessageController { { try { FederatedClient client = federatedClientManager.getClient(messages.getRelay()); - client.sendMessages(source.getNumber(), destinationName, messages); + client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(), + destinationName, messages); } catch (NoSuchPeerException e) { throw new NoSuchUserException(e); } diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java index b8909c2dd..8168285e8 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java +++ b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java @@ -64,7 +64,7 @@ public class FederatedClient { private static final String USER_COUNT_PATH = "/v1/federation/user_count"; private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d"; - private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%s"; + private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s"; private static final String PREKEY_PATH_DEVICE = "/v1/federation/key/%s/%s"; private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d"; @@ -155,11 +155,11 @@ public class FederatedClient { } } - public void sendMessages(String source, String destination, IncomingMessageList messages) + public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages) throws IOException { try { - WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, destination)); + WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination)); ClientResponse response = resource.type(MediaType.APPLICATION_JSON) .header("Authorization", authorizationHeader) .entity(messages) diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java index 09a2986bf..6b3090adf 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java +++ b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java @@ -4,6 +4,7 @@ package org.whispersystems.textsecuregcm.federation; import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.Optional; import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; public class NonLimitedAccount extends Account { @@ -13,20 +14,32 @@ public class NonLimitedAccount extends Account { @JsonIgnore private final String relay; - public NonLimitedAccount(String number, String relay) { - this.number = number; - this.relay = relay; + @JsonIgnore + private final long deviceId; + + public NonLimitedAccount(String number, long deviceId, String relay) { + this.number = number; + this.deviceId = deviceId; + this.relay = relay; } + @Override public String getNumber() { return number; } + @Override public boolean isRateLimited() { return false; } + @Override public Optional getRelay() { return Optional.of(relay); } + + @Override + public Optional getAuthenticatedDevice() { + return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0)); + } } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java new file mode 100644 index 000000000..c00d396a1 --- /dev/null +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java @@ -0,0 +1,89 @@ +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 com.yammer.dropwizard.testing.ResourceTest; +import org.junit.Test; +import org.whispersystems.textsecuregcm.controllers.FederationController; +import org.whispersystems.textsecuregcm.controllers.MessageController; +import org.whispersystems.textsecuregcm.entities.IncomingMessageList; +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 javax.ws.rs.core.MediaType; +import java.util.LinkedList; +import java.util.List; + +import static com.yammer.dropwizard.testing.JsonHelpers.jsonFixture; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FederatedControllerTest extends ResourceTest { + + private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111"; + private static final String MULTI_DEVICE_RECIPIENT = "+14152222222"; + + private PushSender pushSender = mock(PushSender.class ); + private FederatedClientManager federatedClientManager = mock(FederatedClientManager.class); + private AccountsManager accountsManager = mock(AccountsManager.class ); + private RateLimiters rateLimiters = mock(RateLimiters.class ); + private RateLimiter rateLimiter = mock(RateLimiter.class ); + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + protected void setUpResources() throws Exception { + addProvider(AuthHelper.getAuthenticator()); + + List singleDeviceList = new LinkedList() {{ + add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111)); + }}; + + List multiDeviceList = new LinkedList() {{ + add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222)); + add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333)); + }}; + + 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)); + + when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter); + + MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager); + addResource(new FederationController(accountsManager, null, null, messageController)); + } + + @Test + public void testSingleDeviceCurrent() throws Exception { + ClientResponse response = + client().resource(String.format("/v1/federation/messages/+14152223333/1/%s", SINGLE_DEVICE_RECIPIENT)) + .header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo")) + .entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class)) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class); + + assertThat("Good Response", response.getStatus(), is(equalTo(204))); + + verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); + } + + +} diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java index 775ee6189..ba34e9854 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -13,6 +13,8 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.util.Base64; import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.mock; @@ -40,7 +42,14 @@ public class AuthHelper { when(account.getRelay()).thenReturn(Optional.absent()); when(accounts.get(VALID_NUMBER)).thenReturn(Optional.of(account)); - return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(new FederationConfiguration()), + List peer = new LinkedList() {{ + add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz")); + }}; + + FederationConfiguration federationConfiguration = mock(FederationConfiguration.class); + when(federationConfiguration.getPeers()).thenReturn(peer); + + return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(federationConfiguration), FederatedPeer.class, new AccountAuthenticator(accounts), Account.class, "WhisperServer");