From a6463df5bbf82c738d414c514a3a3148ba64a475 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 12 Feb 2014 14:39:45 -0800 Subject: [PATCH] Make WebSocket optional, disabled by default. Add tests. --- .../WhisperServerConfiguration.java | 9 + .../textsecuregcm/WhisperServerService.java | 9 +- .../configuration/WebsocketConfiguration.java | 14 ++ .../entities/MismatchedDevices.java | 4 + .../textsecuregcm/storage/Account.java | 8 + .../controllers/MessageControllerTest.java | 154 ++++++++++++++++++ .../textsecuregcm/tests/util/AuthHelper.java | 4 + .../current_message_extra_device.json | 14 ++ .../current_message_multi_device.json | 14 ++ .../current_message_single_device.json | 8 + .../legacy_message_single_device.json | 8 + .../fixtures/missing_device_response.json | 4 + .../fixtures/missing_device_response2.json | 4 + 13 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/configuration/WebsocketConfiguration.java create mode 100644 src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java create mode 100644 src/test/resources/fixtures/current_message_extra_device.json create mode 100644 src/test/resources/fixtures/current_message_multi_device.json create mode 100644 src/test/resources/fixtures/current_message_single_device.json create mode 100644 src/test/resources/fixtures/legacy_message_single_device.json create mode 100644 src/test/resources/fixtures/missing_device_response.json create mode 100644 src/test/resources/fixtures/missing_device_response2.json diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index 7bf5e32e7..0b81581e4 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -29,6 +29,7 @@ import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration; import org.whispersystems.textsecuregcm.configuration.RedisConfiguration; import org.whispersystems.textsecuregcm.configuration.S3Configuration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; +import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration; import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -83,6 +84,14 @@ public class WhisperServerConfiguration extends Configuration { @JsonProperty private GraphiteConfiguration graphite = new GraphiteConfiguration(); + @Valid + @JsonProperty + private WebsocketConfiguration websocket = new WebsocketConfiguration(); + + public WebsocketConfiguration getWebsocketConfiguration() { + return websocket; + } + public TwilioConfiguration getTwilioConfiguration() { return twilio; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 66db201d7..0a37eb297 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -147,8 +147,11 @@ public class WhisperServerService extends Service { environment.addResource(keysController); environment.addResource(messageController); - environment.addServlet(new WebsocketControllerFactory(deviceAuthenticator, storedMessageManager, pubSubManager), - "/v1/websocket/"); + if (config.getWebsocketConfiguration().isEnabled()) { + environment.addServlet(new WebsocketControllerFactory(deviceAuthenticator, storedMessageManager, pubSubManager), + "/v1/websocket/"); + environment.addFilter(new CORSHeaderFilter(), "/*"); + } environment.addHealthCheck(new RedisHealthCheck(redisClient)); environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient)); @@ -156,8 +159,6 @@ public class WhisperServerService extends Service { environment.addProvider(new IOExceptionMapper()); environment.addProvider(new RateLimitExceededExceptionMapper()); - environment.addFilter(new CORSHeaderFilter(), "/*"); - if (config.getGraphiteConfiguration().isEnabled()) { GraphiteReporter.enable(15, TimeUnit.SECONDS, config.getGraphiteConfiguration().getHost(), diff --git a/src/main/java/org/whispersystems/textsecuregcm/configuration/WebsocketConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/configuration/WebsocketConfiguration.java new file mode 100644 index 000000000..4cea807ec --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/configuration/WebsocketConfiguration.java @@ -0,0 +1,14 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class WebsocketConfiguration { + + @JsonProperty + private boolean enabled = false; + + public boolean isEnabled() { + return enabled; + } + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/MismatchedDevices.java b/src/main/java/org/whispersystems/textsecuregcm/entities/MismatchedDevices.java index 20df30c1d..23d74dfb5 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/MismatchedDevices.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/MismatchedDevices.java @@ -1,6 +1,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; import java.util.List; @@ -12,6 +13,9 @@ public class MismatchedDevices { @JsonProperty public List extraDevices; + @VisibleForTesting + public MismatchedDevices() {} + public MismatchedDevices(List missingDevices, List extraDevices) { this.missingDevices = missingDevices; this.extraDevices = extraDevices; diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index bd8738239..ab2971d97 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -19,6 +19,7 @@ package org.whispersystems.textsecuregcm.storage; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import java.io.Serializable; @@ -51,6 +52,13 @@ public class Account implements Serializable { this.supportsSms = supportsSms; } + @VisibleForTesting + public Account(String number, boolean supportsSms, List devices) { + this.number = number; + this.supportsSms = supportsSms; + this.devices = devices; + } + public long getId() { return id; } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java new file mode 100644 index 000000000..9d44d2e84 --- /dev/null +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java @@ -0,0 +1,154 @@ +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.MessageController; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.IncomingMessageList; +import org.whispersystems.textsecuregcm.entities.MessageProtos; +import org.whispersystems.textsecuregcm.entities.MismatchedDevices; +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.asJson; +import static com.yammer.dropwizard.testing.JsonHelpers.jsonFixture; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +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.*; + +public class MessageControllerTest 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)); + }}; + + List multiDeviceList = new LinkedList() {{ + add(new Device(1, "foo", "bar", "baz", "isgcm", null, false)); + add(new Device(2, "foo", "bar", "baz", "isgcm", null, false)); + }}; + + 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); + + addResource(new MessageController(rateLimiters, pushSender, accountsManager, + federatedClientManager)); + } + + @Test + public void testSingleDeviceLegacy() throws Exception { + ClientResponse response = + client().resource("/v1/messages/") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .entity(mapper.readValue(jsonFixture("fixtures/legacy_message_single_device.json"), IncomingMessageList.class)) + .type(MediaType.APPLICATION_JSON_TYPE) + .post(ClientResponse.class); + + assertThat("Good Response", response.getStatus(), is(equalTo(200))); + + verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); + } + + @Test + public void testSingleDeviceCurrent() throws Exception { + ClientResponse response = + client().resource(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .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)); + } + + @Test + public void testMultiDeviceMissing() throws Exception { + ClientResponse response = + client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class)) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(409))); + + assertThat("Good Response Body", + asJson(response.getEntity(MismatchedDevices.class)), + is(equalTo(jsonFixture("fixtures/missing_device_response.json")))); + + verifyNoMoreInteractions(pushSender); + } + + @Test + public void testMultiDeviceExtra() throws Exception { + ClientResponse response = + client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .entity(mapper.readValue(jsonFixture("fixtures/current_message_extra_device.json"), IncomingMessageList.class)) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(409))); + + assertThat("Good Response Body", + asJson(response.getEntity(MismatchedDevices.class)), + is(equalTo(jsonFixture("fixtures/missing_device_response2.json")))); + + verifyNoMoreInteractions(pushSender); + } + + @Test + public void testMultiDevice() throws Exception { + ClientResponse response = + client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .entity(mapper.readValue(jsonFixture("fixtures/current_message_multi_device.json"), IncomingMessageList.class)) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class); + + assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); + + verify(pushSender, times(2)).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 14d3728e8..775ee6189 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -33,7 +33,11 @@ public class AuthHelper { when(credentials.verify("foo")).thenReturn(true); when(device.getAuthenticationCredentials()).thenReturn(credentials); + when(device.getId()).thenReturn(1L); when(account.getDevice(anyLong())).thenReturn(Optional.of(device)); + when(account.getNumber()).thenReturn(VALID_NUMBER); + when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device)); + when(account.getRelay()).thenReturn(Optional.absent()); when(accounts.get(VALID_NUMBER)).thenReturn(Optional.of(account)); return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(new FederationConfiguration()), diff --git a/src/test/resources/fixtures/current_message_extra_device.json b/src/test/resources/fixtures/current_message_extra_device.json new file mode 100644 index 000000000..aed97de3a --- /dev/null +++ b/src/test/resources/fixtures/current_message_extra_device.json @@ -0,0 +1,14 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "body" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 3, + "body" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} \ No newline at end of file diff --git a/src/test/resources/fixtures/current_message_multi_device.json b/src/test/resources/fixtures/current_message_multi_device.json new file mode 100644 index 000000000..7061d7f3c --- /dev/null +++ b/src/test/resources/fixtures/current_message_multi_device.json @@ -0,0 +1,14 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "body" : "Zm9vYmFyego", + "timestamp" : 1234 + }, + { + "type" : 1, + "destinationDeviceId" : 2, + "body" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} \ No newline at end of file diff --git a/src/test/resources/fixtures/current_message_single_device.json b/src/test/resources/fixtures/current_message_single_device.json new file mode 100644 index 000000000..7425c1c5c --- /dev/null +++ b/src/test/resources/fixtures/current_message_single_device.json @@ -0,0 +1,8 @@ +{ + "messages" : [{ + "type" : 1, + "destinationDeviceId" : 1, + "body" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +} \ No newline at end of file diff --git a/src/test/resources/fixtures/legacy_message_single_device.json b/src/test/resources/fixtures/legacy_message_single_device.json new file mode 100644 index 000000000..3480af2e8 --- /dev/null +++ b/src/test/resources/fixtures/legacy_message_single_device.json @@ -0,0 +1,8 @@ +{ + "messages" : [{ + "type": 1, + "destination": "+14151111111", + "body": "Zm9vYmFyego", + "timestamp": "1234" + }] +} \ No newline at end of file diff --git a/src/test/resources/fixtures/missing_device_response.json b/src/test/resources/fixtures/missing_device_response.json new file mode 100644 index 000000000..a0ad77d79 --- /dev/null +++ b/src/test/resources/fixtures/missing_device_response.json @@ -0,0 +1,4 @@ +{ + "missingDevices" : [2], + "extraDevices" : [] +} \ No newline at end of file diff --git a/src/test/resources/fixtures/missing_device_response2.json b/src/test/resources/fixtures/missing_device_response2.json new file mode 100644 index 000000000..960900cc5 --- /dev/null +++ b/src/test/resources/fixtures/missing_device_response2.json @@ -0,0 +1,4 @@ +{ + "missingDevices" : [2], + "extraDevices" : [3] +} \ No newline at end of file