diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 951be7e3b..1f3f35ada 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -32,7 +32,8 @@ import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AttachmentController; import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DirectoryController; -import org.whispersystems.textsecuregcm.controllers.FederationController; +import org.whispersystems.textsecuregcm.controllers.FederationControllerV1; +import org.whispersystems.textsecuregcm.controllers.FederationControllerV2; import org.whispersystems.textsecuregcm.controllers.KeysControllerV1; import org.whispersystems.textsecuregcm.controllers.KeysControllerV2; import org.whispersystems.textsecuregcm.controllers.MessageController; @@ -160,7 +161,8 @@ public class WhisperServerService extends Application. - */ package org.whispersystems.textsecuregcm.controllers; -import com.codahale.metrics.annotation.Timed; -import com.google.common.base.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.whispersystems.textsecuregcm.entities.AccountCount; -import org.whispersystems.textsecuregcm.entities.AttachmentUri; -import org.whispersystems.textsecuregcm.entities.ClientContact; -import org.whispersystems.textsecuregcm.entities.ClientContacts; -import org.whispersystems.textsecuregcm.entities.IncomingMessageList; -import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2; -import org.whispersystems.textsecuregcm.entities.PreKeyV1; -import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1; -import org.whispersystems.textsecuregcm.federation.FederatedPeer; -import org.whispersystems.textsecuregcm.federation.NonLimitedAccount; -import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; -import org.whispersystems.textsecuregcm.util.Util; -import javax.validation.Valid; -import javax.ws.rs.GET; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; - -import io.dropwizard.auth.Auth; - -@Path("/") public class FederationController { - private final Logger logger = LoggerFactory.getLogger(FederationController.class); + protected final AccountsManager accounts; + protected final AttachmentController attachmentController; + protected final MessageController messageController; - private static final int ACCOUNT_CHUNK_SIZE = 10000; - - private final AccountsManager accounts; - private final AttachmentController attachmentController; - private final KeysControllerV1 keysControllerV1; - private final KeysControllerV2 keysControllerV2; - private final MessageController messageController; - - public FederationController(AccountsManager accounts, + public FederationController(AccountsManager accounts, AttachmentController attachmentController, - KeysControllerV1 keysControllerV1, - KeysControllerV2 keysControllerV2, - MessageController messageController) + MessageController messageController) { this.accounts = accounts; this.attachmentController = attachmentController; - this.keysControllerV1 = keysControllerV1; - this.keysControllerV2 = keysControllerV2; this.messageController = messageController; } - - @Timed - @GET - @Path("/v1/federation/attachment/{attachmentId}") - @Produces(MediaType.APPLICATION_JSON) - public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer, - @PathParam("attachmentId") long attachmentId) - throws IOException - { - return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()), - attachmentId, Optional.absent()); - } - - @Timed - @GET - @Path("/v1/federation/key/{number}") - @Produces(MediaType.APPLICATION_JSON) - public Optional getKey(@Auth FederatedPeer peer, - @PathParam("number") String number) - throws IOException - { - try { - return keysControllerV1.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); - } - } - - @Timed - @GET - @Path("/v1/federation/key/{number}/{device}") - @Produces(MediaType.APPLICATION_JSON) - public Optional getKeysV1(@Auth FederatedPeer peer, - @PathParam("number") String number, - @PathParam("device") String device) - throws IOException - { - try { - return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()), - number, device, Optional.absent()); - } catch (RateLimitExceededException e) { - logger.warn("Rate limiting on federated channel", e); - throw new IOException(e); - } - } - - @Timed - @GET - @Path("/v2/federation/key/{number}/{device}") - @Produces(MediaType.APPLICATION_JSON) - public Optional getKeysV2(@Auth FederatedPeer peer, - @PathParam("number") String number, - @PathParam("device") String device) - throws IOException - { - try { - return keysControllerV2.getDeviceKeys(new NonLimitedAccount("Unknown", -1, peer.getName()), - number, device, Optional.absent()); - } catch (RateLimitExceededException e) { - logger.warn("Rate limiting on federated channel", e); - throw new IOException(e); - } - } - - @Timed - @PUT - @Path("/v1/federation/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, sourceDeviceId, peer.getName()), destination, messages); - } catch (RateLimitExceededException e) { - logger.warn("Rate limiting on federated channel", e); - throw new IOException(e); - } - } - - @Timed - @GET - @Path("/v1/federation/user_count") - @Produces(MediaType.APPLICATION_JSON) - public AccountCount getUserCount(@Auth FederatedPeer peer) { - return new AccountCount((int)accounts.getCount()); - } - - @Timed - @GET - @Path("/v1/federation/user_tokens/{offset}") - @Produces(MediaType.APPLICATION_JSON) - public ClientContacts getUserTokens(@Auth FederatedPeer peer, - @PathParam("offset") int offset) - { - List accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE); - List clientContacts = new LinkedList<>(); - - for (Account account : accountList) { - byte[] token = Util.getContactToken(account.getNumber()); - ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); - - if (!account.isActive()) { - clientContact.setInactive(true); - } - - clientContacts.add(clientContact); - } - - return new ClientContacts(clientContacts); - } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationControllerV1.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationControllerV1.java new file mode 100644 index 000000000..64fd31b2f --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationControllerV1.java @@ -0,0 +1,164 @@ +/** + * Copyright (C) 2013 Open WhisperSystems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.whispersystems.textsecuregcm.controllers; + +import com.codahale.metrics.annotation.Timed; +import com.google.common.base.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.AccountCount; +import org.whispersystems.textsecuregcm.entities.AttachmentUri; +import org.whispersystems.textsecuregcm.entities.ClientContact; +import org.whispersystems.textsecuregcm.entities.ClientContacts; +import org.whispersystems.textsecuregcm.entities.IncomingMessageList; +import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1; +import org.whispersystems.textsecuregcm.entities.PreKeyV1; +import org.whispersystems.textsecuregcm.federation.FederatedPeer; +import org.whispersystems.textsecuregcm.federation.NonLimitedAccount; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.util.Util; + +import javax.validation.Valid; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import io.dropwizard.auth.Auth; + +@Path("/v1/federation") +public class FederationControllerV1 extends FederationController { + + private final Logger logger = LoggerFactory.getLogger(FederationControllerV1.class); + + private static final int ACCOUNT_CHUNK_SIZE = 10000; + + private final KeysControllerV1 keysControllerV1; + + public FederationControllerV1(AccountsManager accounts, + AttachmentController attachmentController, + MessageController messageController, + KeysControllerV1 keysControllerV1) + { + super(accounts, attachmentController, messageController); + this.keysControllerV1 = keysControllerV1; + } + + @Timed + @GET + @Path("/attachment/{attachmentId}") + @Produces(MediaType.APPLICATION_JSON) + public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer, + @PathParam("attachmentId") long attachmentId) + throws IOException + { + return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()), + attachmentId, Optional.absent()); + } + + @Timed + @GET + @Path("/key/{number}") + @Produces(MediaType.APPLICATION_JSON) + public Optional getKey(@Auth FederatedPeer peer, + @PathParam("number") String number) + throws IOException + { + try { + return keysControllerV1.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); + } + } + + @Timed + @GET + @Path("/key/{number}/{device}") + @Produces(MediaType.APPLICATION_JSON) + public Optional getKeysV1(@Auth FederatedPeer peer, + @PathParam("number") String number, + @PathParam("device") String device) + throws IOException + { + try { + return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()), + number, device, Optional.absent()); + } catch (RateLimitExceededException e) { + logger.warn("Rate limiting on federated channel", e); + throw new IOException(e); + } + } + + @Timed + @PUT + @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, sourceDeviceId, peer.getName()), destination, messages); + } catch (RateLimitExceededException e) { + logger.warn("Rate limiting on federated channel", e); + throw new IOException(e); + } + } + + @Timed + @GET + @Path("/user_count") + @Produces(MediaType.APPLICATION_JSON) + public AccountCount getUserCount(@Auth FederatedPeer peer) { + return new AccountCount((int)accounts.getCount()); + } + + @Timed + @GET + @Path("/user_tokens/{offset}") + @Produces(MediaType.APPLICATION_JSON) + public ClientContacts getUserTokens(@Auth FederatedPeer peer, + @PathParam("offset") int offset) + { + List accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE); + List clientContacts = new LinkedList<>(); + + for (Account account : accountList) { + byte[] token = Util.getContactToken(account.getNumber()); + ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); + + if (!account.isActive()) { + clientContact.setInactive(true); + } + + clientContacts.add(clientContact); + } + + return new ClientContacts(clientContacts); + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationControllerV2.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationControllerV2.java new file mode 100644 index 000000000..85bee34fb --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationControllerV2.java @@ -0,0 +1,51 @@ +package org.whispersystems.textsecuregcm.controllers; + +import com.codahale.metrics.annotation.Timed; +import com.google.common.base.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2; +import org.whispersystems.textsecuregcm.federation.FederatedPeer; +import org.whispersystems.textsecuregcm.federation.NonLimitedAccount; +import org.whispersystems.textsecuregcm.storage.AccountsManager; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.io.IOException; + +import io.dropwizard.auth.Auth; + +@Path("/v2/federation") +public class FederationControllerV2 extends FederationController { + + private final Logger logger = LoggerFactory.getLogger(FederationControllerV2.class); + + private final KeysControllerV2 keysControllerV2; + + public FederationControllerV2(AccountsManager accounts, AttachmentController attachmentController, MessageController messageController, KeysControllerV2 keysControllerV2) { + super(accounts, attachmentController, messageController); + this.keysControllerV2 = keysControllerV2; + } + + @Timed + @GET + @Path("/key/{number}/{device}") + @Produces(MediaType.APPLICATION_JSON) + public Optional getKeysV2(@Auth FederatedPeer peer, + @PathParam("number") String number, + @PathParam("device") String device) + throws IOException + { + try { + return keysControllerV2.getDeviceKeys(new NonLimitedAccount("Unknown", -1, peer.getName()), + number, device, Optional.absent()); + } catch (RateLimitExceededException e) { + logger.warn("Rate limiting on federated channel", e); + throw new IOException(e); + } + } + +} diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java index b43d7a7d9..0d8a60499 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java @@ -4,13 +4,19 @@ 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.hamcrest.CoreMatchers; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.whispersystems.textsecuregcm.controllers.FederationController; +import org.whispersystems.textsecuregcm.controllers.FederationControllerV1; +import org.whispersystems.textsecuregcm.controllers.FederationControllerV2; +import org.whispersystems.textsecuregcm.controllers.KeysControllerV2; import org.whispersystems.textsecuregcm.controllers.MessageController; import org.whispersystems.textsecuregcm.entities.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.entities.PreKeyResponseItemV2; +import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2; +import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; @@ -44,16 +50,19 @@ public class FederatedControllerTest { private RateLimiters rateLimiters = mock(RateLimiters.class ); private RateLimiter rateLimiter = mock(RateLimiter.class ); + private final SignedPreKey signedPreKey = new SignedPreKey(3333, "foo", "baar"); + private final PreKeyResponseV2 preKeyResponseV2 = new PreKeyResponseV2("foo", new LinkedList()); + private final ObjectMapper mapper = new ObjectMapper(); private final MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager); + private final KeysControllerV2 keysControllerV2 = mock(KeysControllerV2.class); @Rule public final ResourceTestRule resources = ResourceTestRule.builder() .addProvider(AuthHelper.getAuthenticator()) - .addResource(new FederationController(accountsManager, - null, null, null, - messageController)) + .addResource(new FederationControllerV1(accountsManager, null, messageController, null)) + .addResource(new FederationControllerV2(accountsManager, null, messageController, keysControllerV2)) .build(); @@ -76,6 +85,10 @@ public class FederatedControllerTest { when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount)); when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter); + + when(keysControllerV2.getSignedKey(any(Account.class))).thenReturn(Optional.of(signedPreKey)); + when(keysControllerV2.getDeviceKeys(any(Account.class), anyString(), anyString(), any(Optional.class))) + .thenReturn(Optional.of(preKeyResponseV2)); } @Test @@ -92,5 +105,14 @@ public class FederatedControllerTest { verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); } + @Test + public void testSignedPreKeyV2() throws Exception { + PreKeyResponseV2 response = + resources.client().resource("/v2/federation/key/+14152223333/1") + .header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo")) + .get(PreKeyResponseV2.class); + + assertThat("good response", response.getIdentityKey().equals(preKeyResponseV2.getIdentityKey())); + } }