Compare commits

..

90 Commits
v0.46 ... v0.93

Author SHA1 Message Date
Moxie Marlinspike
c410348278 Bump version to 0.93
// FREEBIE
2016-03-06 13:59:36 -08:00
Moxie Marlinspike
d8a758211f Make push sender queue depth configurable or disable-able.
// FREEBIE
2016-03-06 13:58:30 -08:00
Moxie Marlinspike
33e60f2527 Bump version to 0.92
// FREEBIE
2016-02-02 15:31:10 -08:00
Moxie Marlinspike
fb705eee23 Switch PushSender queue depth metrics to gauge
// FREEBIE
2016-02-02 15:21:15 -08:00
Moxie Marlinspike
635e16e934 Bump version to 0.91
// FREEBIE
2016-02-02 14:43:34 -08:00
Moxie Marlinspike
1deb3ae67f Asynchronous processing queue for incoming messages
// FREEBIE
2016-02-02 14:42:47 -08:00
Moxie Marlinspike
16ff40f420 Bump version to 0.90
// FREEBIE
2016-01-20 18:41:22 -08:00
Moxie Marlinspike
a8b5cb23fe Reduce pending max queue size to 1000 messages
// FREEBIE
2016-01-20 18:41:01 -08:00
Moxie Marlinspike
82f88d04ad Fuzz GCM write timestmap by 10 seconds
// FREEBIE
2015-12-21 16:59:54 -08:00
Moxie Marlinspike
0e1091e0ea Bump version to 0.89
// FREEBIE
2015-12-21 12:36:07 -08:00
Moxie Marlinspike
7b48f10cc9 Filter message deletes by device
// FREEBIE
2015-12-21 12:35:45 -08:00
Moxie Marlinspike
0be34b1135 Bump version to 0.88
// FREEBIE
2015-12-04 12:59:22 -08:00
Moxie Marlinspike
fb5e0242d0 Adjust requeue message logic to avoid redis assumptions
// FREEBIE
2015-12-04 11:41:23 -08:00
Moxie Marlinspike
d376035557 Bump version to 0.87 2015-12-03 16:40:23 -08:00
Moxie Marlinspike
747b2dc7c5 Jedis sanity checks
// FREEBIE
2015-12-03 16:40:04 -08:00
Moxie Marlinspike
fac2f1bee3 Bump version to 0.86
// FREEBIE
2015-12-02 15:06:54 -08:00
Moxie Marlinspike
a211f6aed9 Delete pending messages for an unlinked device
// FREEBIE
2015-12-02 15:06:09 -08:00
Moxie Marlinspike
0bc494245d Fix for broken string format
// FREEBIE
2015-12-01 11:54:50 -08:00
Frederic Jacobs
b31a88043e Adding Signal SMS verification strings.
- Changes the voice verification string.
- Keeps the TextSecure SMS String for matching in Signal for Android.
- Changes TextSecure to Signal for iOS, adding tap to verify link.
- Added test for iOS query parameter.
2015-12-01 11:54:14 -08:00
Moxie Marlinspike
85509c6d8b Don't need to send payload GCM messages any longer.
// FREEBIE
2015-12-01 10:58:43 -08:00
Moxie Marlinspike
2dd131cf79 Bump verison to 0.85
// FREEBIE
2015-11-12 10:42:28 -08:00
Moxie Marlinspike
51990d0b33 Lower chunk size
// FREEBIE
2015-11-12 10:42:16 -08:00
Moxie Marlinspike
00a49afc30 Bump version to 0.84
// FREEBIE
2015-11-09 17:20:28 -08:00
Moxie Marlinspike
faa0630851 Fix up MX numbers for SMS delivery
// FREEBIE
2015-11-09 17:18:59 -08:00
Moxie Marlinspike
aac3fc68fc Bump version to 0.83
// FREEBIE
2015-11-06 11:24:53 -08:00
Moxie Marlinspike
9c08b96b50 Bump version to 0.82
// FREEBIE
2015-11-04 11:20:39 -08:00
Moxie Marlinspike
15ddde1df4 Adjust log levels on delivery receipt failure.
// FREEBIE
2015-11-04 11:20:09 -08:00
Moxie Marlinspike
f2a9de3ba8 Retry serializable transaction.
// FREEBIE
2015-11-04 11:19:54 -08:00
Moxie Marlinspike
fd725206e2 Bump version to 0.81
// FREEBIE
2015-11-03 08:12:31 -08:00
Moxie Marlinspike
6368b9383a Stripe SMS/Vox across multiple numbers
// FREEBIE
2015-11-03 08:12:18 -08:00
Moxie Marlinspike
2b8a11b001 Bump version to 0.80
// FREEBIE
2015-09-30 17:53:47 -07:00
Moxie Marlinspike
c9e0339a30 Specify media type on attributes put
// FREEBIE
2015-09-30 17:53:09 -07:00
Moxie Marlinspike
8d11595290 Bump version to 0.79 2015-09-21 14:09:28 -07:00
Moxie Marlinspike
2fe9f3effa Generate as well as consume auth tokens. Also user agents.
// FREEBIE
2015-09-21 14:09:03 -07:00
Moxie Marlinspike
ae122ff8a2 Bump version to 0.76
// FREEBIE
2015-08-18 11:11:58 -07:00
Moxie Marlinspike
8b941ddd33 Make the messagedb a bounded queue at 5000 msgs/device
// FREEBIE
2015-08-18 11:10:42 -07:00
Moxie Marlinspike
2902ea6689 Get rid of deprecated API
// FREEBIE
2015-08-18 11:10:34 -07:00
Moxie Marlinspike
5ccbf355bd Chunk sending pending message queues > a chunk size.
// FREEBIE
2015-08-17 17:12:36 -07:00
Moxie Marlinspike
62d8f635b0 Track voice support on TS server.
// FREEBIE
2015-08-13 11:43:49 -07:00
Moxie Marlinspike
4c3aae63d3 Trim old messages
// FREEBIE
2015-08-11 20:15:05 -07:00
Moxie Marlinspike
8f94aa0c0d Actually vacuum messages
// FREEBIE
2015-08-11 20:00:11 -07:00
Moxie Marlinspike
0370306bb6 Map 411 to 413
// FREEBIE
2015-08-01 10:09:15 -07:00
Moxie Marlinspike
c9176efe6f Bump version to 0.71
// FREEBIE
2015-07-30 17:31:19 -07:00
Moxie Marlinspike
a3fd08b7ef Add gauge for reporting number of open fds
// FREEBIE
2015-07-30 16:55:19 -07:00
Moxie Marlinspike
83a9e36ef1 Update logging levels.
// FREEBIE
2015-07-30 16:39:55 -07:00
Moxie Marlinspike
9668decc84 Bump version to 0.70
// FREEBIE
2015-07-30 14:17:24 -07:00
Moxie Marlinspike
328bb47d44 Only handle dead letters to addresses, not connection info.
// FREEBIE
2015-07-30 14:16:39 -07:00
Moxie Marlinspike
c74e0b9eab Bump version to 0.69
// FREEBIE
2015-07-30 13:25:40 -07:00
Moxie Marlinspike
20dc32413f Soften some logging.
// FREEBIE
2015-07-30 13:25:29 -07:00
Moxie Marlinspike
d4e618893c Make APN fallback behave well in multi-server environments.
// FREEBIE
2015-07-30 13:18:22 -07:00
Moxie Marlinspike
8d0d934249 Bump version to 0.68
// FREEBIE
2015-07-29 15:19:09 -07:00
Moxie Marlinspike
ef2441ad82 Don't pass response objects back from federated client.
// FREEBIE
2015-07-29 15:18:40 -07:00
Moxie Marlinspike
bb7859138c Bump version to 0.67
// FREEBIE
2015-07-29 14:23:05 -07:00
Moxie Marlinspike
ebc4570941 Fix federated client connection leak.
// FREEBIE
2015-07-29 12:36:03 -07:00
Moxie Marlinspike
d04baed38b Bump version to 0.65
// FREEBIE
2015-07-28 15:23:49 -07:00
Moxie Marlinspike
001c81f797 Try to make JerseyClient put() include a content-length of 0.
// FREEBIE
2015-07-28 15:23:21 -07:00
Moxie Marlinspike
3327bf4788 Add provisioning keepalive endpoint.
// FREEBIE
2015-07-28 15:22:51 -07:00
Moxie Marlinspike
e0b480e232 Bump version to 0.64
// FREEBIE
2015-07-27 22:47:16 -07:00
Moxie Marlinspike
b328d85230 Increase timeout on push service socket.
// FREEBIE
2015-07-27 22:46:38 -07:00
Moxie Marlinspike
3afaa5c1e6 Fix bug with federated delivery receipts.
// FREEBIE
2015-07-27 22:46:18 -07:00
Moxie Marlinspike
f2c8699823 Remove unused provider.
// FREEBIE
2015-07-27 17:58:38 -07:00
Moxie Marlinspike
4c11315a3c Bump version to 0.63
// FREEBIE
2015-07-27 17:04:07 -07:00
Moxie Marlinspike
0e3a347d6b Disable FAIL_ON_UNKNOWN_PROPERTIES for directory command.
// FREEBIE
2015-07-27 17:03:39 -07:00
Moxie Marlinspike
dc723fadaa Bump version to 0.62
// FREEBIE
2015-07-27 16:41:23 -07:00
Moxie Marlinspike
6396958a31 Bump up FederatedClient timeouts.
// FREEBIE
2015-07-27 16:40:42 -07:00
Moxie Marlinspike
1fe57e4841 Bump version to 0.61
// FREEBIE
2015-07-27 14:03:42 -07:00
Moxie Marlinspike
3885ae6337 Dropwizard 9 compatibility!
// FREEBIE
2015-07-27 14:02:44 -07:00
Moxie Marlinspike
39e3366b3b Bump version to 0.54
// FREEBIE
2015-06-25 11:02:00 -07:00
Moxie Marlinspike
a5ffd47935 Gotta stub out message field for delivery receipts w/ old clients
// FREEBIE
2015-06-25 11:00:59 -07:00
Moxie Marlinspike
18a96a445b Bump version to 0.53 2015-06-25 08:51:09 -07:00
Moxie Marlinspike
de366b976e Ignore unknown properties from federated responses.
// FREEBIE
2015-06-25 08:50:10 -07:00
Moxie Marlinspike
8f6aff3a7e Bump version to 0.52
// FREEBIE
2015-06-24 13:46:08 -07:00
Moxie Marlinspike
fb411b20cc Make adding and removing master device operations.
// FREEBIE
2015-06-22 11:01:08 -07:00
Moxie Marlinspike
52ce7d6935 Enhance device management API.
1. Put a limit on the number of registered devices per account.

2. Support removing devices.

3. Support device names and created dates.

4. Support enumerating devices.

// FREEBIE
2015-06-19 21:41:22 -07:00
Moxie Marlinspike
75ee398633 Remove server-side tracking of "supports SMS." Nobody does!
// FREEBIE
2015-06-17 16:45:23 -07:00
Moxie Marlinspike
53bdd946d6 Update TextSecure envelope protobuf.
Support envelope 'content' field.

// FREEBIE
2015-06-17 16:29:07 -07:00
Moxie Marlinspike
79f36664ef Bump version to 0.50
// FREEBIE
2015-06-06 21:04:53 -07:00
Moxie Marlinspike
83078a48ab Support for expiration on APN messages.
// FREEBIE
2015-06-06 21:04:08 -07:00
Moxie Marlinspike
6f67a812dc Make APN fallback 30 seconds.
// FREEBIE
2015-05-27 16:25:31 -07:00
Moxie Marlinspike
6ad705b40e Fall back straight to APN.
// FREEBIE
2015-05-27 16:19:56 -07:00
Moxie Marlinspike
4cb43415a1 Track APN fallback deliverability metrics.
// FREEBIE
2015-05-15 17:01:23 -07:00
Moxie Marlinspike
bbb09b558c Support for APN fallback retries.
// FREEBIE
2015-05-15 16:04:27 -07:00
Moxie Marlinspike
6363be81e0 Support for configured test devices with hardcoded verification.
Closes #40

// FREEBIE
2015-05-13 15:35:59 -07:00
Moxie Marlinspike
c6810d7460 Bump version to 0.49
// FREEBIE
2015-05-13 14:31:38 -07:00
Moxie Marlinspike
4c1e7e7c2f Rate limit messages on source+destination rather than just src.
// FREEBIE
2015-04-24 16:25:59 -07:00
Moxie Marlinspike
931081752a Updated sample config
// FREEBIE

Closes #38
2015-04-21 19:45:31 -07:00
Moxie Marlinspike
424e98e67e Bump version to 0.48
// FREEBIE
2015-04-21 19:44:20 -07:00
Moxie Marlinspike
7cfa93f5f8 Tone down websocket logging for bad federated responses.
// FREEBIE
2015-04-21 19:44:02 -07:00
Moxie Marlinspike
fd8e8d1475 Catch WebApplicationException inside WebsocketConnection.
// FREEBIE
2015-04-16 11:33:16 -07:00
Moxie Marlinspike
7ed5eb22ec Additional WebsocketConnection test.
// FREEBIE
2015-04-16 10:45:24 -07:00
94 changed files with 2845 additions and 1499 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ config/production.yml
config/federated.yml config/federated.yml
config/staging.yml config/staging.yml
.opsmanage .opsmanage
put.sh

View File

@@ -1,32 +1,16 @@
twilio: twilio: # Twilio SMS gateway configuration
accountId: accountId:
accountToken: accountToken:
number: number:
localDomain: # The domain Twilio can call back to. localDomain: # The domain Twilio can call back to.
international: # Boolean specifying Twilio for international delivery
# Optional. If specified, Nexmo will be used for non-US SMS and push: # GCM/APN push server configuration
# voice verification if twilio.international is false. Otherwise, host:
# Nexmo, if specified, Nexmo will only be used as a fallback port:
# for failed Twilio deliveries. username:
nexmo: password:
apiKey:
apiSecret:
number:
gcm: s3: # AWS S3 configuration
senderId:
apiKey:
# Optional. Only if iOS clients are supported.
apn:
# In PEM format.
certificate:
# In PEM format.
key:
s3:
accessKey: accessKey:
accessSecret: accessSecret:
@@ -35,29 +19,22 @@ s3:
# correct permissions. # correct permissions.
attachmentsBucket: attachmentsBucket:
memcache: directory: # Redis server configuration for TS directory
servers:
user:
password:
redis:
url: url:
federation: cache: # Redis server configuration for general purpose caching
name: url:
peers:
-
name: somepeer
url: https://foo.com
authenticationToken: foo
certificate: in pem format
# Optional address of graphite server to report metrics websocket:
graphite: enabled: true
host:
port:
database: messageStore: # Postgres database configuration for message store
driverClass: org.postgresql.Driver
user:
password:
url:
database: # Postgres database configuration for account store
# the name of your JDBC driver # the name of your JDBC driver
driverClass: org.postgresql.Driver driverClass: org.postgresql.Driver
@@ -73,3 +50,13 @@ database:
# any properties specific to your JDBC driver: # any properties specific to your JDBC driver:
properties: properties:
charSet: UTF-8 charSet: UTF-8
federation:
name:
peers:
-
name: somepeer
url: https://foo.com
authenticationToken: foo
certificate: in pem format

93
pom.xml
View File

@@ -9,12 +9,10 @@
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<version>0.46</version> <version>0.93</version>
<properties> <properties>
<dropwizard.version>0.7.1</dropwizard.version> <dropwizard.version>0.9.0-rc3</dropwizard.version>
<jackson.api.version>2.3.3</jackson.api.version>
<commons-codec.version>1.6</commons-codec.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -58,78 +56,67 @@
<artifactId>dropwizard-papertrail</artifactId> <artifactId>dropwizard-papertrail</artifactId>
<version>1.1</version> <version>1.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.sun.jersey</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>jersey-json</artifactId>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>9.0.7.v20131107</version>
</dependency>
<dependency>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId> <artifactId>bcprov-jdk16</artifactId>
<version>140</version> <version>1.46</version>
</dependency>
<dependency>
<groupId>com.google.android.gcm</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.notnoop.apns</groupId>
<artifactId>apns</artifactId>
<version>0.2.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId> <artifactId>aws-java-sdk-s3</artifactId>
<version>1.4.1</version> <version>1.10.6</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.protobuf</groupId> <groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId> <artifactId>protobuf-java</artifactId>
<version>2.5.0</version> <version>2.6.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>redis.clients</groupId> <groupId>redis.clients</groupId>
<artifactId>jedis</artifactId> <artifactId>jedis</artifactId>
<version>2.6.2</version> <version>2.7.3</version>
<type>jar</type> <type>jar</type>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.twilio.sdk</groupId> <groupId>com.twilio.sdk</groupId>
<artifactId>twilio-java-sdk</artifactId> <artifactId>twilio-java-sdk</artifactId>
<version>3.4.5</version> <version>4.4.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
<version>9.1-901.jdbc4</version> <version>9.4-1201-jdbc41</version>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-tcp</artifactId>
<version>4.0.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.whispersystems</groupId> <groupId>org.whispersystems</groupId>
<artifactId>websocket-resources</artifactId> <artifactId>websocket-resources</artifactId>
<version>0.2.3</version> <version>0.3.2</version>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>dropwizard-simpleauth</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>2.19</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -137,14 +124,14 @@
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>httpclient</artifactId>
<version>${jackson.api.version}</version> <version>4.4.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>commons-codec</artifactId> <artifactId>httpcore</artifactId>
<version>${commons-codec.version}</version> <version>4.4.1</version>
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@@ -1,3 +1,3 @@
all: all:
protoc --java_out=../src/main/java/ OutgoingMessageSignal.proto PubSubMessage.proto protoc --java_out=../src/main/java/ TextSecure.proto PubSubMessage.proto

View File

@@ -26,6 +26,7 @@ message PubSubMessage {
DELIVER = 2; DELIVER = 2;
KEEPALIVE = 3; KEEPALIVE = 3;
CLOSE = 4; CLOSE = 4;
CONNECTED = 5;
} }
optional Type type = 1; optional Type type = 1;

View File

@@ -19,23 +19,22 @@ package textsecure;
option java_package = "org.whispersystems.textsecuregcm.entities"; option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos"; option java_outer_classname = "MessageProtos";
message OutgoingMessageSignal { message Envelope {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0;
CIPHERTEXT = 1; CIPHERTEXT = 1;
KEY_EXCHANGE = 2; KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3; PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5; RECEIPT = 5;
} }
optional uint32 type = 1; optional Type type = 1;
optional string source = 2; optional string source = 2;
optional uint32 sourceDevice = 7; optional uint32 sourceDevice = 7;
optional string relay = 3; optional string relay = 3;
// repeated string destinations = 4; optional uint64 timestamp = 5;
optional uint64 timestamp = 5; optional bytes legacyMessage = 6; // Contains an encrypted DataMessage XXX -- Remove after 10/01/15
optional bytes message = 6; optional bytes content = 8; // Contains an encrypted Content
} }
message ProvisioningUuid { message ProvisioningUuid {

View File

@@ -110,7 +110,7 @@ public class DispatchManager extends Thread {
if (subscription.isPresent()) { if (subscription.isPresent()) {
dispatchSubscription(reply.getChannel(), subscription.get()); dispatchSubscription(reply.getChannel(), subscription.get());
} else { } else {
logger.warn("Received subscribe event for non-existing channel: " + reply.getChannel()); logger.info("Received subscribe event for non-existing channel: " + reply.getChannel());
} }
} }

View File

@@ -19,17 +19,21 @@ package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration; import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration; import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration; import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration; import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration; import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration; import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.S3Configuration; import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration; import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import io.dropwizard.Configuration; import io.dropwizard.Configuration;
import io.dropwizard.client.JerseyClientConfiguration; import io.dropwizard.client.JerseyClientConfiguration;
@@ -42,9 +46,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private TwilioConfiguration twilio; private TwilioConfiguration twilio;
@JsonProperty
private NexmoConfiguration nexmo;
@NotNull @NotNull
@Valid @Valid
@JsonProperty @JsonProperty
@@ -70,6 +71,10 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private DataSourceFactory messageStore; private DataSourceFactory messageStore;
@Valid
@NotNull
@JsonProperty
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();
@Valid @Valid
@JsonProperty @JsonProperty
@@ -110,10 +115,6 @@ public class WhisperServerConfiguration extends Configuration {
return twilio; return twilio;
} }
public NexmoConfiguration getNexmoConfiguration() {
return nexmo;
}
public PushConfiguration getPushConfiguration() { public PushConfiguration getPushConfiguration() {
return push; return push;
} }
@@ -157,4 +158,15 @@ public class WhisperServerConfiguration extends Configuration {
public RedPhoneConfiguration getRedphoneConfiguration() { public RedPhoneConfiguration getRedphoneConfiguration() {
return redphone; return redphone;
} }
public Map<String, Integer> getTestDevices() {
Map<String, Integer> results = new HashMap<>();
for (TestDeviceConfiguration testDeviceConfiguration : testDevices) {
results.put(testDeviceConfiguration.getNumber(),
testDeviceConfiguration.getCode());
}
return results;
}
} }

View File

@@ -18,18 +18,21 @@ package org.whispersystems.textsecuregcm;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter; import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.Client;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter; import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.client.ClientProperties;
import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.DBI;
import org.whispersystems.dispatch.DispatchChannel; import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.dispatch.DispatchManager; import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator; import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController; import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DeviceController;
@@ -46,26 +49,29 @@ import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle; import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper; import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge; import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge;
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge; import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge; import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge; import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory; import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck; import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
import org.whispersystems.textsecuregcm.providers.TimeProvider; import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.FeedbackHandler; import org.whispersystems.textsecuregcm.push.FeedbackHandler;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.PushServiceClient; import org.whispersystems.textsecuregcm.push.PushServiceClient;
import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender; import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts; import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.Keys; import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.Messages; import org.whispersystems.textsecuregcm.storage.Messages;
@@ -82,6 +88,7 @@ import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener; import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator; import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.DirectoryCommand; import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
import org.whispersystems.textsecuregcm.workers.TrimMessagesCommand;
import org.whispersystems.textsecuregcm.workers.VacuumCommand; import org.whispersystems.textsecuregcm.workers.VacuumCommand;
import org.whispersystems.websocket.WebSocketResourceProviderFactory; import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment; import org.whispersystems.websocket.setup.WebSocketEnvironment;
@@ -89,6 +96,7 @@ import org.whispersystems.websocket.setup.WebSocketEnvironment;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration; import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration; import javax.servlet.ServletRegistration;
import javax.ws.rs.client.Client;
import java.security.Security; import java.security.Security;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -113,6 +121,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) { public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
bootstrap.addCommand(new DirectoryCommand()); bootstrap.addCommand(new DirectoryCommand());
bootstrap.addCommand(new VacuumCommand()); bootstrap.addCommand(new VacuumCommand());
bootstrap.addCommand(new TrimMessagesCommand());
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") { bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
@Override @Override
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) { public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
@@ -139,6 +148,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
{ {
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics()); SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
DBIFactory dbiFactory = new DBIFactory(); DBIFactory dbiFactory = new DBIFactory();
DBI database = dbiFactory.build(environment, config.getDataSourceFactory(), "accountdb"); DBI database = dbiFactory.build(environment, config.getDataSourceFactory(), "accountdb");
@@ -153,47 +164,54 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
RedisClientFactory cacheClientFactory = new RedisClientFactory(config.getCacheConfiguration().getUrl()); RedisClientFactory cacheClientFactory = new RedisClientFactory(config.getCacheConfiguration().getUrl());
JedisPool cacheClient = cacheClientFactory.getRedisClientPool(); JedisPool cacheClient = cacheClientFactory.getRedisClientPool();
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool(); JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration()) Client httpClient = initializeHttpClient(environment, config);
.build(getName());
DirectoryManager directory = new DirectoryManager(directoryClient); DirectoryManager directory = new DirectoryManager(directoryClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient); PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient); PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration()); FederatedClientManager federatedClientManager = new FederatedClientManager(environment, config.getJerseyClientConfiguration(), config.getFederationConfiguration());
MessagesManager messagesManager = new MessagesManager(messages); MessagesManager messagesManager = new MessagesManager(messages);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager); DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.<DispatchChannel>of(deadLetterHandler)); DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.<DispatchChannel>of(deadLetterHandler));
PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager); PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager);
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration()); PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager); WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager); AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager );
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient); FederatedPeerAuthenticator federatedPeerAuthenticator = new FederatedPeerAuthenticator(config.getFederationConfiguration());
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient, pubSubManager);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration()); TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration()); SmsSender smsSender = new SmsSender(twilioSmsSender);
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration()); UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(pushServiceClient, websocketSender); PushSender pushSender = new PushSender(apnFallbackManager, pushServiceClient, websocketSender, config.getPushConfiguration().getQueueSize());
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager); ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager); FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey(); Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
environment.lifecycle().manage(apnFallbackManager);
environment.lifecycle().manage(pubSubManager); environment.lifecycle().manage(pubSubManager);
environment.lifecycle().manage(feedbackHandler); environment.lifecycle().manage(feedbackHandler);
environment.lifecycle().manage(pushSender);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner); AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager); KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager); KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager); MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager);
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()), environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
FederatedPeer.class, .setAuthenticator(deviceAuthenticator)
deviceAuthenticator, .setPrincipal(Account.class)
Device.class, "WhisperServer")); .buildAuthFilter(),
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
.setAuthenticator(federatedPeerAuthenticator)
.setPrincipal(FederatedPeer.class)
.buildAuthFilter()));
environment.jersey().register(new AuthValueFactoryProvider.Binder());
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey)); environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, config.getTestDevices()));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters)); environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory)); environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1)); environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2)); environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
@@ -207,7 +225,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
if (config.getWebsocketConfiguration().isEnabled()) { if (config.getWebsocketConfiguration().isEnabled()) {
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000); WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000);
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator)); webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager)); webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager, apnFallbackManager));
webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager)); webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager));
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config); WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config);
@@ -232,7 +250,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class); FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
filter.setInitParameter("allowedOrigins", "*"); filter.setInitParameter("allowedOrigins", "*");
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin"); filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,X-Signal-Agent");
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS"); filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
filter.setInitParameter("preflightMaxAge", "5184000"); filter.setInitParameter("preflightMaxAge", "5184000");
filter.setInitParameter("allowCredentials", "true"); filter.setInitParameter("allowCredentials", "true");
@@ -243,11 +261,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new IOExceptionMapper()); environment.jersey().register(new IOExceptionMapper());
environment.jersey().register(new RateLimitExceededExceptionMapper()); environment.jersey().register(new RateLimitExceededExceptionMapper());
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
environment.jersey().register(new DeviceLimitExceededExceptionMapper());
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge()); environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge()); environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge()); environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge()); environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
environment.metrics().register(name(FileDescriptorGauge.class, "fd_count"), new FileDescriptorGauge());
if (config.getGraphiteConfiguration().isEnabled()) { if (config.getGraphiteConfiguration().isEnabled()) {
GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory(); GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory();
@@ -259,12 +280,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
} }
} }
private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) { private Client initializeHttpClient(Environment environment, WhisperServerConfiguration config) {
if (configuration == null) { Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
return Optional.absent(); .build(getName());
} else {
return Optional.of(new NexmoSmsSender(configuration)); httpClient.property(ClientProperties.CONNECT_TIMEOUT, 1000);
} httpClient.property(ClientProperties.READ_TIMEOUT, 1000);
return httpClient;
} }
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {

View File

@@ -22,6 +22,7 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.dropwizard.simpleauth.Authenticator;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
@@ -29,7 +30,6 @@ import org.whispersystems.textsecuregcm.util.Constants;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.auth.basic.BasicCredentials;
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> { public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {

View File

@@ -1,6 +1,7 @@
package org.whispersystems.textsecuregcm.auth; package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -16,61 +17,13 @@ import java.util.concurrent.TimeUnit;
public class AuthorizationToken { public class AuthorizationToken {
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class); @JsonProperty
private String token;
private final String token; public AuthorizationToken(String token) {
private final byte[] key;
public AuthorizationToken(String token, byte[] key) {
this.token = token; this.token = token;
this.key = key;
} }
public boolean isValid(String number, long currentTimeMillis) { public AuthorizationToken() {}
String[] parts = token.split(":");
if (parts.length != 3) {
return false;
}
if (!number.equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2]);
}
private boolean isValidTime(String timeString, long currentTimeMillis) {
try {
long tokenTime = Long.parseLong(timeString);
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
} catch (NumberFormatException e) {
logger.warn("Number Format", e);
return false;
}
}
private boolean isValidSignature(String prefix, String suffix) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] ourSuffix = Util.truncate(hmac.doFinal(prefix.getBytes()), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (DecoderException e) {
logger.warn("Authorizationtoken", e);
return false;
}
}
} }

View File

@@ -0,0 +1,90 @@
package org.whispersystems.textsecuregcm.auth;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
public class AuthorizationTokenGenerator {
private final Logger logger = LoggerFactory.getLogger(AuthorizationTokenGenerator.class);
private final byte[] key;
public AuthorizationTokenGenerator(byte[] key) {
this.key = key;
}
public AuthorizationToken generateFor(String number) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
long currentTimeSeconds = System.currentTimeMillis() / 1000;
String prefix = number + ":" + currentTimeSeconds;
mac.init(new SecretKeySpec(key, "HmacSHA256"));
String output = Hex.encodeHexString(Util.truncate(mac.doFinal(prefix.getBytes()), 10));
String token = prefix + ":" + output;
return new AuthorizationToken(token);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public boolean isValid(String token, String number, long currentTimeMillis) {
String[] parts = token.split(":");
if (parts.length != 3) {
return false;
}
if (!number.equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2]);
}
private boolean isValidTime(String timeString, long currentTimeMillis) {
try {
long tokenTime = Long.parseLong(timeString);
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
} catch (NumberFormatException e) {
logger.warn("Number Format", e);
return false;
}
}
private boolean isValidSignature(String prefix, String suffix) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] ourSuffix = Util.truncate(hmac.doFinal(prefix.getBytes()), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (DecoderException e) {
logger.warn("Authorizationtoken", e);
return false;
}
}
}

View File

@@ -22,6 +22,7 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.dropwizard.simpleauth.Authenticator;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration; import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
@@ -30,7 +31,6 @@ import java.util.List;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.auth.basic.BasicCredentials;

View File

@@ -1,66 +0,0 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.auth;
import com.sun.jersey.api.model.Parameter;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
import io.dropwizard.auth.Auth;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicAuthProvider;
import io.dropwizard.auth.basic.BasicCredentials;
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
private final BasicAuthProvider<T1> provider1;
private final BasicAuthProvider<T2> provider2;
private final Class<?> clazz1;
private final Class<?> clazz2;
public MultiBasicAuthProvider(Authenticator<BasicCredentials, T1> authenticator1,
Class<?> clazz1,
Authenticator<BasicCredentials, T2> authenticator2,
Class<?> clazz2,
String realm)
{
this.provider1 = new BasicAuthProvider<>(authenticator1, realm);
this.provider2 = new BasicAuthProvider<>(authenticator2, realm);
this.clazz1 = clazz1;
this.clazz2 = clazz2;
}
@Override
public ComponentScope getScope() {
return ComponentScope.PerRequest;
}
@Override
public Injectable<?> getInjectable(ComponentContext componentContext,
Auth auth, Parameter parameter)
{
if (parameter.getParameterClass().equals(clazz1)) {
return this.provider1.getInjectable(componentContext, auth, parameter);
} else {
return this.provider2.getInjectable(componentContext, auth, parameter);
}
}
}

View File

@@ -1,43 +0,0 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class NexmoConfiguration {
@JsonProperty
private String apiKey;
@JsonProperty
private String apiSecret;
@JsonProperty
private String number;
public String getApiKey() {
return apiKey;
}
public String getApiSecret() {
return apiSecret;
}
public String getNumber() {
return number;
}
}

View File

@@ -22,6 +22,10 @@ public class PushConfiguration {
@NotEmpty @NotEmpty
private String password; private String password;
@JsonProperty
@Min(0)
private int queueSize = 200;
public String getHost() { public String getHost() {
return host; return host;
} }
@@ -37,4 +41,8 @@ public class PushConfiguration {
public String getPassword() { public String getPassword() {
return password; return password;
} }
public int getQueueSize() {
return queueSize;
}
} }

View File

@@ -0,0 +1,25 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class TestDeviceConfiguration {
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@NotNull
private int code;
public String getNumber() {
return number;
}
public int getCode() {
return code;
}
}

View File

@@ -19,6 +19,9 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
public class TwilioConfiguration { public class TwilioConfiguration {
@NotEmpty @NotEmpty
@@ -29,17 +32,14 @@ public class TwilioConfiguration {
@JsonProperty @JsonProperty
private String accountToken; private String accountToken;
@NotEmpty @NotNull
@JsonProperty @JsonProperty
private String number; private List<String> numbers;
@NotEmpty @NotEmpty
@JsonProperty @JsonProperty
private String localDomain; private String localDomain;
@JsonProperty
private boolean international;
public String getAccountId() { public String getAccountId() {
return accountId; return accountId;
} }
@@ -48,15 +48,11 @@ public class TwilioConfiguration {
return accountToken; return accountToken;
} }
public String getNumber() { public List<String> getNumbers() {
return number; return numbers;
} }
public String getLocalDomain() { public String getLocalDomain() {
return localDomain; return localDomain;
} }
public boolean isInternational() {
return international;
}
} }

View File

@@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.AuthorizationToken; import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
@@ -50,12 +51,14 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Map;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
@@ -64,13 +67,14 @@ public class AccountController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class); private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final PendingAccountsManager pendingAccounts; private final PendingAccountsManager pendingAccounts;
private final AccountsManager accounts; private final AccountsManager accounts;
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final SmsSender smsSender; private final SmsSender smsSender;
private final MessagesManager messagesManager; private final MessagesManager messagesManager;
private final TimeProvider timeProvider; private final TimeProvider timeProvider;
private final Optional<byte[]> authorizationKey; private final Optional<AuthorizationTokenGenerator> tokenGenerator;
private final Map<String, Integer> testDevices;
public AccountController(PendingAccountsManager pendingAccounts, public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts, AccountsManager accounts,
@@ -78,7 +82,8 @@ public class AccountController {
SmsSender smsSenderFactory, SmsSender smsSenderFactory,
MessagesManager messagesManager, MessagesManager messagesManager,
TimeProvider timeProvider, TimeProvider timeProvider,
Optional<byte[]> authorizationKey) Optional<byte[]> authorizationKey,
Map<String, Integer> testDevices)
{ {
this.pendingAccounts = pendingAccounts; this.pendingAccounts = pendingAccounts;
this.accounts = accounts; this.accounts = accounts;
@@ -86,14 +91,21 @@ public class AccountController {
this.smsSender = smsSenderFactory; this.smsSender = smsSenderFactory;
this.messagesManager = messagesManager; this.messagesManager = messagesManager;
this.timeProvider = timeProvider; this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey; this.testDevices = testDevices;
if (authorizationKey.isPresent()) {
tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get()));
} else {
tokenGenerator = Optional.absent();
}
} }
@Timed @Timed
@GET @GET
@Path("/{transport}/code/{number}") @Path("/{transport}/code/{number}")
public Response createAccount(@PathParam("transport") String transport, public Response createAccount(@PathParam("transport") String transport,
@PathParam("number") String number) @PathParam("number") String number,
@QueryParam("client") Optional<String> client)
throws IOException, RateLimitExceededException throws IOException, RateLimitExceededException
{ {
if (!Util.isValidNumber(number)) { if (!Util.isValidNumber(number)) {
@@ -112,11 +124,13 @@ public class AccountController {
throw new WebApplicationException(Response.status(422).build()); throw new WebApplicationException(Response.status(422).build());
} }
VerificationCode verificationCode = generateVerificationCode(); VerificationCode verificationCode = generateVerificationCode(number);
pendingAccounts.store(number, verificationCode.getVerificationCode()); pendingAccounts.store(number, verificationCode.getVerificationCode());
if (transport.equals("sms")) { if (testDevices.containsKey(number)) {
smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay()); // noop
} else if (transport.equals("sms")) {
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
} else if (transport.equals("voice")) { } else if (transport.equals("voice")) {
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech()); smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
} }
@@ -130,6 +144,7 @@ public class AccountController {
@Path("/code/{verification_code}") @Path("/code/{verification_code}")
public void verifyAccount(@PathParam("verification_code") String verificationCode, public void verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader, @HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes accountAttributes) @Valid AccountAttributes accountAttributes)
throws RateLimitExceededException throws RateLimitExceededException
{ {
@@ -152,7 +167,7 @@ public class AccountController {
throw new WebApplicationException(Response.status(417).build()); throw new WebApplicationException(Response.status(417).build());
} }
createAccount(number, password, accountAttributes); createAccount(number, password, userAgent, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) { } catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e); logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build()); throw new WebApplicationException(Response.status(401).build());
@@ -165,6 +180,7 @@ public class AccountController {
@Path("/token/{verification_token}") @Path("/token/{verification_token}")
public void verifyToken(@PathParam("verification_token") String verificationToken, public void verifyToken(@PathParam("verification_token") String verificationToken,
@HeaderParam("Authorization") String authorizationHeader, @HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes accountAttributes) @Valid AccountAttributes accountAttributes)
throws RateLimitExceededException throws RateLimitExceededException
{ {
@@ -175,24 +191,37 @@ public class AccountController {
rateLimiters.getVerifyLimiter().validate(number); rateLimiters.getVerifyLimiter().validate(number);
if (!authorizationKey.isPresent()) { if (!tokenGenerator.isPresent()) {
logger.debug("Attempt to authorize with key but not configured..."); logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());
} }
AuthorizationToken token = new AuthorizationToken(verificationToken, authorizationKey.get()); if (!tokenGenerator.get().isValid(verificationToken, number, timeProvider.getCurrentTimeMillis())) {
if (!token.isValid(number, timeProvider.getCurrentTimeMillis())) {
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());
} }
createAccount(number, password, accountAttributes); createAccount(number, password, userAgent, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) { } catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad authorization header", e); logger.info("Bad authorization header", e);
throw new WebApplicationException(Response.status(401).build()); throw new WebApplicationException(Response.status(401).build());
} }
} }
@Timed
@GET
@Path("/token/")
@Produces(MediaType.APPLICATION_JSON)
public AuthorizationToken verifyToken(@Auth Account account)
throws RateLimitExceededException
{
if (!tokenGenerator.isPresent()) {
logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(404).build());
}
return tokenGenerator.get().generateFor(account.getNumber());
}
@Timed @Timed
@PUT @PUT
@Path("/gcm/") @Path("/gcm/")
@@ -244,19 +273,22 @@ public class AccountController {
@Timed @Timed
@PUT @PUT
@Path("/wsc/") @Path("/attributes/")
public void setWebSocketChannelSupported(@Auth Account account) { @Consumes(MediaType.APPLICATION_JSON)
public void setAccountAttributes(@Auth Account account,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes attributes)
{
Device device = account.getAuthenticatedDevice().get(); Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(true);
accounts.update(account);
}
@Timed device.setFetchesMessages(attributes.getFetchesMessages());
@DELETE device.setName(attributes.getName());
@Path("/wsc/") device.setLastSeen(Util.todayInMillis());
public void deleteWebSocketChannel(@Auth Account account) { device.setVoiceSupported(attributes.getVoice());
Device device = account.getAuthenticatedDevice().get(); device.setRegistrationId(attributes.getRegistrationId());
device.setFetchesMessages(false); device.setSignalingKey(attributes.getSignalingKey());
device.setUserAgent(userAgent);
accounts.update(account); accounts.update(account);
} }
@@ -269,28 +301,34 @@ public class AccountController {
encodedVerificationText)).build(); encodedVerificationText)).build();
} }
private void createAccount(String number, String password, AccountAttributes accountAttributes) { private void createAccount(String number, String password, String userAgent, AccountAttributes accountAttributes) {
Device device = new Device(); Device device = new Device();
device.setId(Device.MASTER_ID); device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password)); device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey()); device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages()); device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId()); device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setVoiceSupported(accountAttributes.getVoice());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(userAgent);
Account account = new Account(); Account account = new Account();
account.setNumber(number); account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device); account.addDevice(device);
accounts.create(account); accounts.create(account);
messagesManager.clear(number); messagesManager.clear(number);
pendingAccounts.remove(number); pendingAccounts.remove(number);
logger.debug("Stored device...");
} }
@VisibleForTesting protected VerificationCode generateVerificationCode() { @VisibleForTesting protected VerificationCode generateVerificationCode(String number) {
try { try {
if (testDevices.containsKey(number)) {
return new VerificationCode(testDevices.get(number));
}
SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int randomInt = 100000 + random.nextInt(900000); int randomInt = 100000 + random.nextInt(900000);
return new VerificationCode(randomInt); return new VerificationCode(randomInt);

View File

@@ -25,17 +25,21 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
import org.whispersystems.textsecuregcm.entities.DeviceResponse; import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode; import org.whispersystems.textsecuregcm.util.VerificationCode;
import javax.validation.Valid; import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam; import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
@@ -48,6 +52,8 @@ import javax.ws.rs.core.Response;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
@@ -56,28 +62,68 @@ public class DeviceController {
private final Logger logger = LoggerFactory.getLogger(DeviceController.class); private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
private static final int MAX_DEVICES = 3;
private final PendingDevicesManager pendingDevices; private final PendingDevicesManager pendingDevices;
private final AccountsManager accounts; private final AccountsManager accounts;
private final MessagesManager messages;
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
public DeviceController(PendingDevicesManager pendingDevices, public DeviceController(PendingDevicesManager pendingDevices,
AccountsManager accounts, AccountsManager accounts,
MessagesManager messages,
RateLimiters rateLimiters) RateLimiters rateLimiters)
{ {
this.pendingDevices = pendingDevices; this.pendingDevices = pendingDevices;
this.accounts = accounts; this.accounts = accounts;
this.messages = messages;
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
} }
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
public DeviceInfoList getDevices(@Auth Account account) {
List<DeviceInfo> devices = new LinkedList<>();
for (Device device : account.getDevices()) {
devices.add(new DeviceInfo(device.getId(), device.getName(),
device.getLastSeen(), device.getCreated()));
}
return new DeviceInfoList(devices);
}
@Timed
@DELETE
@Path("/{device_id}")
public void removeDevice(@Auth Account account, @PathParam("device_id") long deviceId) {
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
account.removeDevice(deviceId);
accounts.update(account);
messages.clear(account.getNumber(), deviceId);
}
@Timed @Timed
@GET @GET
@Path("/provisioning/code") @Path("/provisioning/code")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public VerificationCode createDeviceToken(@Auth Account account) public VerificationCode createDeviceToken(@Auth Account account)
throws RateLimitExceededException throws RateLimitExceededException, DeviceLimitExceededException
{ {
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber()); rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
if (account.getActiveDeviceCount() >= MAX_DEVICES) {
throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES);
}
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
VerificationCode verificationCode = generateVerificationCode(); VerificationCode verificationCode = generateVerificationCode();
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode()); pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
@@ -92,7 +138,7 @@ public class DeviceController {
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode, public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader, @HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes) @Valid AccountAttributes accountAttributes)
throws RateLimitExceededException throws RateLimitExceededException, DeviceLimitExceededException
{ {
try { try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader); AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
@@ -115,13 +161,19 @@ public class DeviceController {
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());
} }
if (account.get().getActiveDeviceCount() >= MAX_DEVICES) {
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
}
Device device = new Device(); Device device = new Device();
device.setName(accountAttributes.getName());
device.setAuthenticationCredentials(new AuthenticationCredentials(password)); device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey()); device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages()); device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setId(account.get().getNextDeviceId()); device.setId(account.get().getNextDeviceId());
device.setRegistrationId(accountAttributes.getRegistrationId()); device.setRegistrationId(accountAttributes.getRegistrationId());
device.setLastSeen(Util.todayInMillis()); device.setLastSeen(Util.todayInMillis());
device.setCreated(System.currentTimeMillis());
account.get().addDevice(device); account.get().addDevice(device);
accounts.update(account.get()); accounts.update(account.get());

View File

@@ -0,0 +1,21 @@
package org.whispersystems.textsecuregcm.controllers;
public class DeviceLimitExceededException extends Exception {
private final int currentDevices;
private final int maxDevices;
public DeviceLimitExceededException(int currentDevices, int maxDevices) {
this.currentDevices = currentDevices;
this.maxDevices = maxDevices;
}
public int getCurrentDevices() {
return currentDevices;
}
public int getMaxDevices() {
return maxDevices;
}
}

View File

@@ -150,7 +150,7 @@ public class FederationControllerV1 extends FederationController {
for (Account account : accountList) { for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber()); byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
if (!account.isActive()) { if (!account.isActive()) {
clientContact.setInactive(true); clientContact.setInactive(true);

View File

@@ -29,8 +29,8 @@ public class KeepAliveController {
@Timed @Timed
@GET @GET
public Response getKeepAlive(@Auth(required = false) Account account, public Response getKeepAlive(@Auth Account account,
@WebSocketSession WebSocketSessionContext context) @WebSocketSession WebSocketSessionContext context)
{ {
if (account != null) { if (account != null) {
WebsocketAddress address = new WebsocketAddress(account.getNumber(), WebsocketAddress address = new WebsocketAddress(account.getNumber(),
@@ -45,4 +45,11 @@ public class KeepAliveController {
return Response.ok().build(); return Response.ok().build();
} }
@Timed
@GET
@Path("/provisioning")
public Response getProvisioningKeepAlive() {
return Response.ok().build();
}
} }

View File

@@ -16,8 +16,10 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyCount; import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
@@ -38,6 +40,8 @@ import io.dropwizard.auth.Auth;
public class KeysController { public class KeysController {
private static final Logger logger = LoggerFactory.getLogger(KeysController.class);
protected final RateLimiters rateLimiters; protected final RateLimiters rateLimiters;
protected final Keys keys; protected final Keys keys;
protected final AccountsManager accounts; protected final AccountsManager accounts;
@@ -52,7 +56,6 @@ public class KeysController {
this.federatedClientManager = federatedClientManager; this.federatedClientManager = federatedClientManager;
} }
@Timed
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public PreKeyCount getStatus(@Auth Account account) { public PreKeyCount getStatus(@Auth Account account) {
@@ -87,8 +90,16 @@ public class KeysController {
throw new NoSuchUserException("Target device is inactive."); throw new NoSuchUserException("Target device is inactive.");
} }
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId); for (int i=0;i<20;i++) {
return new TargetKeys(destination.get(), preKeys); try {
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
return new TargetKeys(destination.get(), preKeys);
} catch (UnableToExecuteStatementException e) {
logger.info(e.getMessage());
}
}
throw new WebApplicationException(Response.status(500).build());
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build()); throw new WebApplicationException(Response.status(422).build());
} }

View File

@@ -23,8 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.IncomingMessage; import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.MessageResponse;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices; import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
@@ -49,7 +48,6 @@ import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
@@ -102,7 +100,7 @@ public class MessageController {
@Valid IncomingMessageList messages) @Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException throws IOException, RateLimitExceededException
{ {
rateLimiters.getMessagesLimiter().validate(source.getNumber()); rateLimiters.getMessagesLimiter().validate(source.getNumber() + "__" + destinationName);
try { try {
boolean isSyncMessage = source.getNumber().equals(destinationName); boolean isSyncMessage = source.getNumber().equals(destinationName);
@@ -129,33 +127,12 @@ public class MessageController {
} }
} }
@Timed
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public MessageResponse sendMessageLegacy(@Auth Account source, @Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException
{
try {
List<IncomingMessage> incomingMessages = messages.getMessages();
validateLegacyDestinations(incomingMessages);
messages.setRelay(incomingMessages.get(0).getRelay());
sendMessage(source, incomingMessages.get(0).getDestination(), messages);
return new MessageResponse(new LinkedList<String>(), new LinkedList<String>());
} catch (ValidationException e) {
throw new WebApplicationException(Response.status(422).build());
}
}
@Timed @Timed
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public OutgoingMessageEntityList getPendingMessages(@Auth Account account) { public OutgoingMessageEntityList getPendingMessages(@Auth Account account) {
return new OutgoingMessageEntityList(messagesManager.getMessagesForDevice(account.getNumber(), return messagesManager.getMessagesForDevice(account.getNumber(),
account.getAuthenticatedDevice() account.getAuthenticatedDevice().get().getId());
.get().getId()));
} }
@Timed @Timed
@@ -167,15 +144,19 @@ public class MessageController {
throws IOException throws IOException
{ {
try { try {
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(), source, timestamp); Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(),
account.getAuthenticatedDevice().get().getId(),
source, timestamp);
if (message.isPresent() && message.get().getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) { if (message.isPresent() && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
receiptSender.sendReceipt(account, receiptSender.sendReceipt(account,
message.get().getSource(), message.get().getSource(),
message.get().getTimestamp(), message.get().getTimestamp(),
Optional.fromNullable(message.get().getRelay())); Optional.fromNullable(message.get().getRelay()));
} }
} catch (NoSuchUserException | NotPushRegisteredException | TransientPushFailureException e) { } catch (NotPushRegisteredException e) {
logger.info("User no longer push registered for delivery receipt: " + e.getMessage());
} catch (NoSuchUserException | TransientPushFailureException e) {
logger.warn("Sending delivery receipt", e); logger.warn("Sending delivery receipt", e);
} }
} }
@@ -185,7 +166,7 @@ public class MessageController {
String destinationName, String destinationName,
IncomingMessageList messages, IncomingMessageList messages,
boolean isSyncMessage) boolean isSyncMessage)
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException throws NoSuchUserException, MismatchedDevicesException, StaleDevicesException
{ {
Account destination; Account destination;
@@ -209,19 +190,24 @@ public class MessageController {
Device destinationDevice, Device destinationDevice,
long timestamp, long timestamp,
IncomingMessage incomingMessage) IncomingMessage incomingMessage)
throws NoSuchUserException, IOException throws NoSuchUserException
{ {
try { try {
Optional<byte[]> messageBody = getMessageBody(incomingMessage); Optional<byte[]> messageBody = getMessageBody(incomingMessage);
OutgoingMessageSignal.Builder messageBuilder = OutgoingMessageSignal.newBuilder(); Optional<byte[]> messageContent = getMessageContent(incomingMessage);
Envelope.Builder messageBuilder = Envelope.newBuilder();
messageBuilder.setType(incomingMessage.getType()) messageBuilder.setType(Envelope.Type.valueOf(incomingMessage.getType()))
.setSource(source.getNumber()) .setSource(source.getNumber())
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp) .setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId()); .setSourceDevice((int) source.getAuthenticatedDevice().get().getId());
if (messageBody.isPresent()) { if (messageBody.isPresent()) {
messageBuilder.setMessage(ByteString.copyFrom(messageBody.get())); messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
}
if (messageContent.isPresent()) {
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
} }
if (source.getRelay().isPresent()) { if (source.getRelay().isPresent()) {
@@ -232,9 +218,6 @@ public class MessageController {
} catch (NotPushRegisteredException e) { } catch (NotPushRegisteredException e) {
if (destinationDevice.isMaster()) throw new NoSuchUserException(e); if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
else logger.debug("Not registered", e); else logger.debug("Not registered", e);
} catch (TransientPushFailureException e) {
if (destinationDevice.isMaster()) throw new IOException(e);
else logger.debug("Transient failure", e);
} }
} }
@@ -326,23 +309,9 @@ public class MessageController {
} }
} }
private void validateLegacyDestinations(List<IncomingMessage> messages)
throws ValidationException
{
String destination = null;
for (IncomingMessage message : messages) {
if ((message.getDestination() == null) ||
(destination != null && !destination.equals(message.getDestination())))
{
throw new ValidationException("Multiple account destinations!");
}
destination = message.getDestination();
}
}
private Optional<byte[]> getMessageBody(IncomingMessage message) { private Optional<byte[]> getMessageBody(IncomingMessage message) {
if (Util.isEmpty(message.getBody())) return Optional.absent();
try { try {
return Optional.of(Base64.decode(message.getBody())); return Optional.of(Base64.decode(message.getBody()));
} catch (IOException ioe) { } catch (IOException ioe) {
@@ -350,4 +319,15 @@ public class MessageController {
return Optional.absent(); return Optional.absent();
} }
} }
private Optional<byte[]> getMessageContent(IncomingMessage message) {
if (Util.isEmpty(message.getContent())) return Optional.absent();
try {
return Optional.of(Base64.decode(message.getContent()));
} catch (IOException ioe) {
logger.debug("Bad B64", ioe);
return Optional.absent();
}
}
} }

View File

@@ -17,6 +17,8 @@
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.NotEmpty;
public class AccountAttributes { public class AccountAttributes {
@@ -25,32 +27,39 @@ public class AccountAttributes {
@NotEmpty @NotEmpty
private String signalingKey; private String signalingKey;
@JsonProperty
private boolean supportsSms;
@JsonProperty @JsonProperty
private boolean fetchesMessages; private boolean fetchesMessages;
@JsonProperty @JsonProperty
private int registrationId; private int registrationId;
@JsonProperty
@Length(max = 50, message = "This field must be less than 50 characters")
private String name;
@JsonProperty
private boolean voice;
public AccountAttributes() {} public AccountAttributes() {}
public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages, int registrationId) { @VisibleForTesting
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) {
this(signalingKey, fetchesMessages, registrationId, null, false);
}
@VisibleForTesting
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice) {
this.signalingKey = signalingKey; this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
this.fetchesMessages = fetchesMessages; this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId; this.registrationId = registrationId;
this.name = name;
this.voice = voice;
} }
public String getSignalingKey() { public String getSignalingKey() {
return signalingKey; return signalingKey;
} }
public boolean getSupportsSms() {
return supportsSms;
}
public boolean getFetchesMessages() { public boolean getFetchesMessages() {
return fetchesMessages; return fetchesMessages;
} }
@@ -58,4 +67,13 @@ public class AccountAttributes {
public int getRegistrationId() { public int getRegistrationId() {
return registrationId; return registrationId;
} }
public String getName() {
return name;
}
public boolean getVoice() {
return voice;
}
} }

View File

@@ -8,6 +8,9 @@ import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
public class ApnMessage { public class ApnMessage {
public static long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L;
@JsonProperty @JsonProperty
@NotEmpty @NotEmpty
private String apnId; private String apnId;
@@ -28,13 +31,46 @@ public class ApnMessage {
@NotNull @NotNull
private boolean voip; private boolean voip;
@JsonProperty
private long expirationTime;
public ApnMessage() {} public ApnMessage() {}
public ApnMessage(String apnId, String number, int deviceId, String message, boolean voip) { public ApnMessage(String apnId, String number, int deviceId, String message, boolean voip, long expirationTime) {
this.apnId = apnId; this.apnId = apnId;
this.number = number; this.number = number;
this.deviceId = deviceId; this.deviceId = deviceId;
this.message = message; this.message = message;
this.voip = voip; this.voip = voip;
this.expirationTime = expirationTime;
}
public ApnMessage(ApnMessage copy, String apnId, boolean voip, long expirationTime) {
this.apnId = apnId;
this.number = copy.number;
this.deviceId = copy.deviceId;
this.message = copy.message;
this.voip = voip;
this.expirationTime = expirationTime;
}
@VisibleForTesting
public String getApnId() {
return apnId;
}
@VisibleForTesting
public boolean isVoip() {
return voip;
}
@VisibleForTesting
public String getMessage() {
return message;
}
@VisibleForTesting
public long getExpirationTime() {
return expirationTime;
} }
} }

View File

@@ -32,14 +32,16 @@ public class ClientContact {
@JsonProperty @JsonProperty
private byte[] token; private byte[] token;
@JsonProperty
private boolean voice;
private String relay; private String relay;
private boolean inactive; private boolean inactive;
private boolean supportsSms;
public ClientContact(byte[] token, String relay, boolean supportsSms) { public ClientContact(byte[] token, String relay, boolean voice) {
this.token = token; this.token = token;
this.relay = relay; this.relay = relay;
this.supportsSms = supportsSms; this.voice = voice;
} }
public ClientContact() {} public ClientContact() {}
@@ -56,10 +58,6 @@ public class ClientContact {
this.relay = relay; this.relay = relay;
} }
public boolean isSupportsSms() {
return supportsSms;
}
public boolean isInactive() { public boolean isInactive() {
return inactive; return inactive;
} }
@@ -68,9 +66,13 @@ public class ClientContact {
this.inactive = inactive; this.inactive = inactive;
} }
// public String toString() { public boolean isVoice() {
// return new Gson().toJson(this); return voice;
// } }
public void setVoice(boolean voice) {
this.voice = voice;
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
@@ -81,8 +83,8 @@ public class ClientContact {
return return
Arrays.equals(this.token, that.token) && Arrays.equals(this.token, that.token) &&
this.supportsSms == that.supportsSms &&
this.inactive == that.inactive && this.inactive == that.inactive &&
this.voice == that.voice &&
(this.relay == null ? (that.relay == null) : this.relay.equals(that.relay)); (this.relay == null ? (that.relay == null) : this.relay.equals(that.relay));
} }

View File

@@ -0,0 +1,24 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeviceInfo {
@JsonProperty
private long id;
@JsonProperty
private String name;
@JsonProperty
private long lastSeen;
@JsonProperty
private long created;
public DeviceInfo(long id, String name, long lastSeen, long created) {
this.id = id;
this.name = name;
this.lastSeen = lastSeen;
this.created = created;
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class DeviceInfoList {
@JsonProperty
private List<DeviceInfo> devices;
public DeviceInfoList(List<DeviceInfo> devices) {
this.devices = devices;
}
}

View File

@@ -18,7 +18,7 @@ package org.whispersystems.textsecuregcm.entities;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
@@ -44,8 +44,7 @@ public class EncryptedOutgoingMessage {
private final byte[] serialized; private final byte[] serialized;
private final String serializedAndEncoded; private final String serializedAndEncoded;
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage, public EncryptedOutgoingMessage(Envelope outgoingMessage, String signalingKey)
String signalingKey)
throws CryptoEncodingException throws CryptoEncodingException
{ {
byte[] plaintext = outgoingMessage.toByteArray(); byte[] plaintext = outgoingMessage.toByteArray();

View File

@@ -34,9 +34,11 @@ public class IncomingMessage {
private int destinationRegistrationId; private int destinationRegistrationId;
@JsonProperty @JsonProperty
@NotEmpty
private String body; private String body;
@JsonProperty
private String content;
@JsonProperty @JsonProperty
private String relay; private String relay;
@@ -67,4 +69,8 @@ public class IncomingMessage {
public int getDestinationRegistrationId() { public int getDestinationRegistrationId() {
return destinationRegistrationId; return destinationRegistrationId;
} }
public String getContent() {
return content;
}
} }

View File

@@ -1,5 +1,5 @@
// Generated by the protocol buffer compiler. DO NOT EDIT! // Generated by the protocol buffer compiler. DO NOT EDIT!
// source: OutgoingMessageSignal.proto // source: TextSecure.proto
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
@@ -8,18 +8,18 @@ public final class MessageProtos {
public static void registerAllExtensions( public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) { com.google.protobuf.ExtensionRegistry registry) {
} }
public interface OutgoingMessageSignalOrBuilder public interface EnvelopeOrBuilder
extends com.google.protobuf.MessageOrBuilder { extends com.google.protobuf.MessageOrBuilder {
// optional uint32 type = 1; // optional .textsecure.Envelope.Type type = 1;
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
boolean hasType(); boolean hasType();
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
int getType(); org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type getType();
// optional string source = 2; // optional string source = 2;
/** /**
@@ -64,50 +64,68 @@ public final class MessageProtos {
// optional uint64 timestamp = 5; // optional uint64 timestamp = 5;
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
boolean hasTimestamp(); boolean hasTimestamp();
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
long getTimestamp(); long getTimestamp();
// optional bytes message = 6; // optional bytes legacyMessage = 6;
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
boolean hasMessage(); boolean hasLegacyMessage();
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
com.google.protobuf.ByteString getMessage(); com.google.protobuf.ByteString getLegacyMessage();
// optional bytes content = 8;
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
boolean hasContent();
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
com.google.protobuf.ByteString getContent();
} }
/** /**
* Protobuf type {@code textsecure.OutgoingMessageSignal} * Protobuf type {@code textsecure.Envelope}
*/ */
public static final class OutgoingMessageSignal extends public static final class Envelope extends
com.google.protobuf.GeneratedMessage com.google.protobuf.GeneratedMessage
implements OutgoingMessageSignalOrBuilder { implements EnvelopeOrBuilder {
// Use OutgoingMessageSignal.newBuilder() to construct. // Use Envelope.newBuilder() to construct.
private OutgoingMessageSignal(com.google.protobuf.GeneratedMessage.Builder<?> builder) { private Envelope(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
super(builder); super(builder);
this.unknownFields = builder.getUnknownFields(); this.unknownFields = builder.getUnknownFields();
} }
private OutgoingMessageSignal(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); } private Envelope(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
private static final OutgoingMessageSignal defaultInstance; private static final Envelope defaultInstance;
public static OutgoingMessageSignal getDefaultInstance() { public static Envelope getDefaultInstance() {
return defaultInstance; return defaultInstance;
} }
public OutgoingMessageSignal getDefaultInstanceForType() { public Envelope getDefaultInstanceForType() {
return defaultInstance; return defaultInstance;
} }
@@ -117,7 +135,7 @@ public final class MessageProtos {
getUnknownFields() { getUnknownFields() {
return this.unknownFields; return this.unknownFields;
} }
private OutgoingMessageSignal( private Envelope(
com.google.protobuf.CodedInputStream input, com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException { throws com.google.protobuf.InvalidProtocolBufferException {
@@ -141,8 +159,14 @@ public final class MessageProtos {
break; break;
} }
case 8: { case 8: {
bitField0_ |= 0x00000001; int rawValue = input.readEnum();
type_ = input.readUInt32(); org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type value = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.valueOf(rawValue);
if (value == null) {
unknownFields.mergeVarintField(1, rawValue);
} else {
bitField0_ |= 0x00000001;
type_ = value;
}
break; break;
} }
case 18: { case 18: {
@@ -162,7 +186,7 @@ public final class MessageProtos {
} }
case 50: { case 50: {
bitField0_ |= 0x00000020; bitField0_ |= 0x00000020;
message_ = input.readBytes(); legacyMessage_ = input.readBytes();
break; break;
} }
case 56: { case 56: {
@@ -170,6 +194,11 @@ public final class MessageProtos {
sourceDevice_ = input.readUInt32(); sourceDevice_ = input.readUInt32();
break; break;
} }
case 66: {
bitField0_ |= 0x00000040;
content_ = input.readBytes();
break;
}
} }
} }
} catch (com.google.protobuf.InvalidProtocolBufferException e) { } catch (com.google.protobuf.InvalidProtocolBufferException e) {
@@ -184,33 +213,33 @@ public final class MessageProtos {
} }
public static final com.google.protobuf.Descriptors.Descriptor public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() { getDescriptor() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor; return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_descriptor;
} }
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() { internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_fieldAccessorTable
.ensureFieldAccessorsInitialized( .ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class); org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.class, org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Builder.class);
} }
public static com.google.protobuf.Parser<OutgoingMessageSignal> PARSER = public static com.google.protobuf.Parser<Envelope> PARSER =
new com.google.protobuf.AbstractParser<OutgoingMessageSignal>() { new com.google.protobuf.AbstractParser<Envelope>() {
public OutgoingMessageSignal parsePartialFrom( public Envelope parsePartialFrom(
com.google.protobuf.CodedInputStream input, com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException { throws com.google.protobuf.InvalidProtocolBufferException {
return new OutgoingMessageSignal(input, extensionRegistry); return new Envelope(input, extensionRegistry);
} }
}; };
@java.lang.Override @java.lang.Override
public com.google.protobuf.Parser<OutgoingMessageSignal> getParserForType() { public com.google.protobuf.Parser<Envelope> getParserForType() {
return PARSER; return PARSER;
} }
/** /**
* Protobuf enum {@code textsecure.OutgoingMessageSignal.Type} * Protobuf enum {@code textsecure.Envelope.Type}
*/ */
public enum Type public enum Type
implements com.google.protobuf.ProtocolMessageEnum { implements com.google.protobuf.ProtocolMessageEnum {
@@ -230,14 +259,10 @@ public final class MessageProtos {
* <code>PREKEY_BUNDLE = 3;</code> * <code>PREKEY_BUNDLE = 3;</code>
*/ */
PREKEY_BUNDLE(3, 3), PREKEY_BUNDLE(3, 3),
/**
* <code>PLAINTEXT = 4;</code>
*/
PLAINTEXT(4, 4),
/** /**
* <code>RECEIPT = 5;</code> * <code>RECEIPT = 5;</code>
*/ */
RECEIPT(5, 5), RECEIPT(4, 5),
; ;
/** /**
@@ -256,10 +281,6 @@ public final class MessageProtos {
* <code>PREKEY_BUNDLE = 3;</code> * <code>PREKEY_BUNDLE = 3;</code>
*/ */
public static final int PREKEY_BUNDLE_VALUE = 3; public static final int PREKEY_BUNDLE_VALUE = 3;
/**
* <code>PLAINTEXT = 4;</code>
*/
public static final int PLAINTEXT_VALUE = 4;
/** /**
* <code>RECEIPT = 5;</code> * <code>RECEIPT = 5;</code>
*/ */
@@ -274,7 +295,6 @@ public final class MessageProtos {
case 1: return CIPHERTEXT; case 1: return CIPHERTEXT;
case 2: return KEY_EXCHANGE; case 2: return KEY_EXCHANGE;
case 3: return PREKEY_BUNDLE; case 3: return PREKEY_BUNDLE;
case 4: return PLAINTEXT;
case 5: return RECEIPT; case 5: return RECEIPT;
default: return null; default: return null;
} }
@@ -302,7 +322,7 @@ public final class MessageProtos {
} }
public static final com.google.protobuf.Descriptors.EnumDescriptor public static final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptor() { getDescriptor() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDescriptor().getEnumTypes().get(0); return org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.getDescriptor().getEnumTypes().get(0);
} }
private static final Type[] VALUES = values(); private static final Type[] VALUES = values();
@@ -324,23 +344,23 @@ public final class MessageProtos {
this.value = value; this.value = value;
} }
// @@protoc_insertion_point(enum_scope:textsecure.OutgoingMessageSignal.Type) // @@protoc_insertion_point(enum_scope:textsecure.Envelope.Type)
} }
private int bitField0_; private int bitField0_;
// optional uint32 type = 1; // optional .textsecure.Envelope.Type type = 1;
public static final int TYPE_FIELD_NUMBER = 1; public static final int TYPE_FIELD_NUMBER = 1;
private int type_; private org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type type_;
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
public boolean hasType() { public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001); return ((bitField0_ & 0x00000001) == 0x00000001);
} }
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
public int getType() { public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type getType() {
return type_; return type_;
} }
@@ -451,48 +471,73 @@ public final class MessageProtos {
private long timestamp_; private long timestamp_;
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
public boolean hasTimestamp() { public boolean hasTimestamp() {
return ((bitField0_ & 0x00000010) == 0x00000010); return ((bitField0_ & 0x00000010) == 0x00000010);
} }
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
public long getTimestamp() { public long getTimestamp() {
return timestamp_; return timestamp_;
} }
// optional bytes message = 6; // optional bytes legacyMessage = 6;
public static final int MESSAGE_FIELD_NUMBER = 6; public static final int LEGACYMESSAGE_FIELD_NUMBER = 6;
private com.google.protobuf.ByteString message_; private com.google.protobuf.ByteString legacyMessage_;
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
public boolean hasMessage() { public boolean hasLegacyMessage() {
return ((bitField0_ & 0x00000020) == 0x00000020); return ((bitField0_ & 0x00000020) == 0x00000020);
} }
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
public com.google.protobuf.ByteString getMessage() { public com.google.protobuf.ByteString getLegacyMessage() {
return message_; return legacyMessage_;
}
// optional bytes content = 8;
public static final int CONTENT_FIELD_NUMBER = 8;
private com.google.protobuf.ByteString content_;
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
} }
private void initFields() { private void initFields() {
type_ = 0; type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
source_ = ""; source_ = "";
sourceDevice_ = 0; sourceDevice_ = 0;
relay_ = ""; relay_ = "";
timestamp_ = 0L; timestamp_ = 0L;
message_ = com.google.protobuf.ByteString.EMPTY; legacyMessage_ = com.google.protobuf.ByteString.EMPTY;
content_ = com.google.protobuf.ByteString.EMPTY;
} }
private byte memoizedIsInitialized = -1; private byte memoizedIsInitialized = -1;
public final boolean isInitialized() { public final boolean isInitialized() {
@@ -507,7 +552,7 @@ public final class MessageProtos {
throws java.io.IOException { throws java.io.IOException {
getSerializedSize(); getSerializedSize();
if (((bitField0_ & 0x00000001) == 0x00000001)) { if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeUInt32(1, type_); output.writeEnum(1, type_.getNumber());
} }
if (((bitField0_ & 0x00000002) == 0x00000002)) { if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeBytes(2, getSourceBytes()); output.writeBytes(2, getSourceBytes());
@@ -519,11 +564,14 @@ public final class MessageProtos {
output.writeUInt64(5, timestamp_); output.writeUInt64(5, timestamp_);
} }
if (((bitField0_ & 0x00000020) == 0x00000020)) { if (((bitField0_ & 0x00000020) == 0x00000020)) {
output.writeBytes(6, message_); output.writeBytes(6, legacyMessage_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) { if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeUInt32(7, sourceDevice_); output.writeUInt32(7, sourceDevice_);
} }
if (((bitField0_ & 0x00000040) == 0x00000040)) {
output.writeBytes(8, content_);
}
getUnknownFields().writeTo(output); getUnknownFields().writeTo(output);
} }
@@ -535,7 +583,7 @@ public final class MessageProtos {
size = 0; size = 0;
if (((bitField0_ & 0x00000001) == 0x00000001)) { if (((bitField0_ & 0x00000001) == 0x00000001)) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(1, type_); .computeEnumSize(1, type_.getNumber());
} }
if (((bitField0_ & 0x00000002) == 0x00000002)) { if (((bitField0_ & 0x00000002) == 0x00000002)) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
@@ -551,12 +599,16 @@ public final class MessageProtos {
} }
if (((bitField0_ & 0x00000020) == 0x00000020)) { if (((bitField0_ & 0x00000020) == 0x00000020)) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeBytesSize(6, message_); .computeBytesSize(6, legacyMessage_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) { if (((bitField0_ & 0x00000004) == 0x00000004)) {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(7, sourceDevice_); .computeUInt32Size(7, sourceDevice_);
} }
if (((bitField0_ & 0x00000040) == 0x00000040)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(8, content_);
}
size += getUnknownFields().getSerializedSize(); size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size; memoizedSerializedSize = size;
return size; return size;
@@ -569,53 +621,53 @@ public final class MessageProtos {
return super.writeReplace(); return super.writeReplace();
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
com.google.protobuf.ByteString data) com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException { throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data); return PARSER.parseFrom(data);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
com.google.protobuf.ByteString data, com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException { throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry); return PARSER.parseFrom(data, extensionRegistry);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(byte[] data) public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException { throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data); return PARSER.parseFrom(data);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
byte[] data, byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException { throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry); return PARSER.parseFrom(data, extensionRegistry);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(java.io.InputStream input) public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(java.io.InputStream input)
throws java.io.IOException { throws java.io.IOException {
return PARSER.parseFrom(input); return PARSER.parseFrom(input);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
java.io.InputStream input, java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException { throws java.io.IOException {
return PARSER.parseFrom(input, extensionRegistry); return PARSER.parseFrom(input, extensionRegistry);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(java.io.InputStream input) public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException { throws java.io.IOException {
return PARSER.parseDelimitedFrom(input); return PARSER.parseDelimitedFrom(input);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom( public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseDelimitedFrom(
java.io.InputStream input, java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException { throws java.io.IOException {
return PARSER.parseDelimitedFrom(input, extensionRegistry); return PARSER.parseDelimitedFrom(input, extensionRegistry);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
com.google.protobuf.CodedInputStream input) com.google.protobuf.CodedInputStream input)
throws java.io.IOException { throws java.io.IOException {
return PARSER.parseFrom(input); return PARSER.parseFrom(input);
} }
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
com.google.protobuf.CodedInputStream input, com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException { throws java.io.IOException {
@@ -624,7 +676,7 @@ public final class MessageProtos {
public static Builder newBuilder() { return Builder.create(); } public static Builder newBuilder() { return Builder.create(); }
public Builder newBuilderForType() { return newBuilder(); } public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal prototype) { public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope prototype) {
return newBuilder().mergeFrom(prototype); return newBuilder().mergeFrom(prototype);
} }
public Builder toBuilder() { return newBuilder(this); } public Builder toBuilder() { return newBuilder(this); }
@@ -636,24 +688,24 @@ public final class MessageProtos {
return builder; return builder;
} }
/** /**
* Protobuf type {@code textsecure.OutgoingMessageSignal} * Protobuf type {@code textsecure.Envelope}
*/ */
public static final class Builder extends public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder> com.google.protobuf.GeneratedMessage.Builder<Builder>
implements org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignalOrBuilder { implements org.whispersystems.textsecuregcm.entities.MessageProtos.EnvelopeOrBuilder {
public static final com.google.protobuf.Descriptors.Descriptor public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() { getDescriptor() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor; return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_descriptor;
} }
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() { internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_fieldAccessorTable
.ensureFieldAccessorsInitialized( .ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class); org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.class, org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Builder.class);
} }
// Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.newBuilder() // Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.newBuilder()
private Builder() { private Builder() {
maybeForceBuilderInitialization(); maybeForceBuilderInitialization();
} }
@@ -673,7 +725,7 @@ public final class MessageProtos {
public Builder clear() { public Builder clear() {
super.clear(); super.clear();
type_ = 0; type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
bitField0_ = (bitField0_ & ~0x00000001); bitField0_ = (bitField0_ & ~0x00000001);
source_ = ""; source_ = "";
bitField0_ = (bitField0_ & ~0x00000002); bitField0_ = (bitField0_ & ~0x00000002);
@@ -683,8 +735,10 @@ public final class MessageProtos {
bitField0_ = (bitField0_ & ~0x00000008); bitField0_ = (bitField0_ & ~0x00000008);
timestamp_ = 0L; timestamp_ = 0L;
bitField0_ = (bitField0_ & ~0x00000010); bitField0_ = (bitField0_ & ~0x00000010);
message_ = com.google.protobuf.ByteString.EMPTY; legacyMessage_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000020); bitField0_ = (bitField0_ & ~0x00000020);
content_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000040);
return this; return this;
} }
@@ -694,23 +748,23 @@ public final class MessageProtos {
public com.google.protobuf.Descriptors.Descriptor public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() { getDescriptorForType() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor; return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_descriptor;
} }
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal getDefaultInstanceForType() { public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope getDefaultInstanceForType() {
return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance(); return org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.getDefaultInstance();
} }
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal build() { public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope build() {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial(); org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope result = buildPartial();
if (!result.isInitialized()) { if (!result.isInitialized()) {
throw newUninitializedMessageException(result); throw newUninitializedMessageException(result);
} }
return result; return result;
} }
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildPartial() { public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope buildPartial() {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = new org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal(this); org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope result = new org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope(this);
int from_bitField0_ = bitField0_; int from_bitField0_ = bitField0_;
int to_bitField0_ = 0; int to_bitField0_ = 0;
if (((from_bitField0_ & 0x00000001) == 0x00000001)) { if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
@@ -736,23 +790,27 @@ public final class MessageProtos {
if (((from_bitField0_ & 0x00000020) == 0x00000020)) { if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
to_bitField0_ |= 0x00000020; to_bitField0_ |= 0x00000020;
} }
result.message_ = message_; result.legacyMessage_ = legacyMessage_;
if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
to_bitField0_ |= 0x00000040;
}
result.content_ = content_;
result.bitField0_ = to_bitField0_; result.bitField0_ = to_bitField0_;
onBuilt(); onBuilt();
return result; return result;
} }
public Builder mergeFrom(com.google.protobuf.Message other) { public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) { if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope) {
return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal)other); return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope)other);
} else { } else {
super.mergeFrom(other); super.mergeFrom(other);
return this; return this;
} }
} }
public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal other) { public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope other) {
if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance()) return this; if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.getDefaultInstance()) return this;
if (other.hasType()) { if (other.hasType()) {
setType(other.getType()); setType(other.getType());
} }
@@ -772,8 +830,11 @@ public final class MessageProtos {
if (other.hasTimestamp()) { if (other.hasTimestamp()) {
setTimestamp(other.getTimestamp()); setTimestamp(other.getTimestamp());
} }
if (other.hasMessage()) { if (other.hasLegacyMessage()) {
setMessage(other.getMessage()); setLegacyMessage(other.getLegacyMessage());
}
if (other.hasContent()) {
setContent(other.getContent());
} }
this.mergeUnknownFields(other.getUnknownFields()); this.mergeUnknownFields(other.getUnknownFields());
return this; return this;
@@ -787,11 +848,11 @@ public final class MessageProtos {
com.google.protobuf.CodedInputStream input, com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry) com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException { throws java.io.IOException {
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parsedMessage = null; org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parsedMessage = null;
try { try {
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
} catch (com.google.protobuf.InvalidProtocolBufferException e) { } catch (com.google.protobuf.InvalidProtocolBufferException e) {
parsedMessage = (org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) e.getUnfinishedMessage(); parsedMessage = (org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope) e.getUnfinishedMessage();
throw e; throw e;
} finally { } finally {
if (parsedMessage != null) { if (parsedMessage != null) {
@@ -802,35 +863,38 @@ public final class MessageProtos {
} }
private int bitField0_; private int bitField0_;
// optional uint32 type = 1; // optional .textsecure.Envelope.Type type = 1;
private int type_ ; private org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
public boolean hasType() { public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001); return ((bitField0_ & 0x00000001) == 0x00000001);
} }
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
public int getType() { public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type getType() {
return type_; return type_;
} }
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
public Builder setType(int value) { public Builder setType(org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000001; bitField0_ |= 0x00000001;
type_ = value; type_ = value;
onChanged(); onChanged();
return this; return this;
} }
/** /**
* <code>optional uint32 type = 1;</code> * <code>optional .textsecure.Envelope.Type type = 1;</code>
*/ */
public Builder clearType() { public Builder clearType() {
bitField0_ = (bitField0_ & ~0x00000001); bitField0_ = (bitField0_ & ~0x00000001);
type_ = 0; type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
onChanged(); onChanged();
return this; return this;
} }
@@ -1020,30 +1084,18 @@ public final class MessageProtos {
private long timestamp_ ; private long timestamp_ ;
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
public boolean hasTimestamp() { public boolean hasTimestamp() {
return ((bitField0_ & 0x00000010) == 0x00000010); return ((bitField0_ & 0x00000010) == 0x00000010);
} }
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
public long getTimestamp() { public long getTimestamp() {
return timestamp_; return timestamp_;
} }
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
public Builder setTimestamp(long value) { public Builder setTimestamp(long value) {
bitField0_ |= 0x00000010; bitField0_ |= 0x00000010;
@@ -1053,10 +1105,6 @@ public final class MessageProtos {
} }
/** /**
* <code>optional uint64 timestamp = 5;</code> * <code>optional uint64 timestamp = 5;</code>
*
* <pre>
* repeated string destinations = 4;
* </pre>
*/ */
public Builder clearTimestamp() { public Builder clearTimestamp() {
bitField0_ = (bitField0_ & ~0x00000010); bitField0_ = (bitField0_ & ~0x00000010);
@@ -1065,51 +1113,119 @@ public final class MessageProtos {
return this; return this;
} }
// optional bytes message = 6; // optional bytes legacyMessage = 6;
private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY; private com.google.protobuf.ByteString legacyMessage_ = com.google.protobuf.ByteString.EMPTY;
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
public boolean hasMessage() { public boolean hasLegacyMessage() {
return ((bitField0_ & 0x00000020) == 0x00000020); return ((bitField0_ & 0x00000020) == 0x00000020);
} }
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
public com.google.protobuf.ByteString getMessage() { public com.google.protobuf.ByteString getLegacyMessage() {
return message_; return legacyMessage_;
} }
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
public Builder setMessage(com.google.protobuf.ByteString value) { public Builder setLegacyMessage(com.google.protobuf.ByteString value) {
if (value == null) { if (value == null) {
throw new NullPointerException(); throw new NullPointerException();
} }
bitField0_ |= 0x00000020; bitField0_ |= 0x00000020;
message_ = value; legacyMessage_ = value;
onChanged(); onChanged();
return this; return this;
} }
/** /**
* <code>optional bytes message = 6;</code> * <code>optional bytes legacyMessage = 6;</code>
*
* <pre>
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
* </pre>
*/ */
public Builder clearMessage() { public Builder clearLegacyMessage() {
bitField0_ = (bitField0_ & ~0x00000020); bitField0_ = (bitField0_ & ~0x00000020);
message_ = getDefaultInstance().getMessage(); legacyMessage_ = getDefaultInstance().getLegacyMessage();
onChanged(); onChanged();
return this; return this;
} }
// @@protoc_insertion_point(builder_scope:textsecure.OutgoingMessageSignal) // optional bytes content = 8;
private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
}
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
public Builder setContent(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000040;
content_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes content = 8;</code>
*
* <pre>
* Contains an encrypted Content
* </pre>
*/
public Builder clearContent() {
bitField0_ = (bitField0_ & ~0x00000040);
content_ = getDefaultInstance().getContent();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.Envelope)
} }
static { static {
defaultInstance = new OutgoingMessageSignal(true); defaultInstance = new Envelope(true);
defaultInstance.initFields(); defaultInstance.initFields();
} }
// @@protoc_insertion_point(class_scope:textsecure.OutgoingMessageSignal) // @@protoc_insertion_point(class_scope:textsecure.Envelope)
} }
public interface ProvisioningUuidOrBuilder public interface ProvisioningUuidOrBuilder
@@ -1584,10 +1700,10 @@ public final class MessageProtos {
} }
private static com.google.protobuf.Descriptors.Descriptor private static com.google.protobuf.Descriptors.Descriptor
internal_static_textsecure_OutgoingMessageSignal_descriptor; internal_static_textsecure_Envelope_descriptor;
private static private static
com.google.protobuf.GeneratedMessage.FieldAccessorTable com.google.protobuf.GeneratedMessage.FieldAccessorTable
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable; internal_static_textsecure_Envelope_fieldAccessorTable;
private static com.google.protobuf.Descriptors.Descriptor private static com.google.protobuf.Descriptors.Descriptor
internal_static_textsecure_ProvisioningUuid_descriptor; internal_static_textsecure_ProvisioningUuid_descriptor;
private static private static
@@ -1602,28 +1718,28 @@ public final class MessageProtos {
descriptor; descriptor;
static { static {
java.lang.String[] descriptorData = { java.lang.String[] descriptorData = {
"\n\033OutgoingMessageSignal.proto\022\ntextsecur" + "\n\020TextSecure.proto\022\ntextsecure\"\372\001\n\010Envel" +
"e\"\344\001\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(" + "ope\022\'\n\004type\030\001 \001(\0162\031.textsecure.Envelope." +
"\r\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceDevice\030\007 \001(\r\022" + "Type\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceDevice\030\007 \001" +
"\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mes" + "(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(\004\022\025\n\r" +
"sage\030\006 \001(\014\"d\n\004Type\022\013\n\007UNKNOWN\020\000\022\016\n\nCIPHE" + "legacyMessage\030\006 \001(\014\022\017\n\007content\030\010 \001(\014\"U\n\004" +
"RTEXT\020\001\022\020\n\014KEY_EXCHANGE\020\002\022\021\n\rPREKEY_BUND" + "Type\022\013\n\007UNKNOWN\020\000\022\016\n\nCIPHERTEXT\020\001\022\020\n\014KEY" +
"LE\020\003\022\r\n\tPLAINTEXT\020\004\022\013\n\007RECEIPT\020\005\" \n\020Prov" + "_EXCHANGE\020\002\022\021\n\rPREKEY_BUNDLE\020\003\022\013\n\007RECEIP" +
"isioningUuid\022\014\n\004uuid\030\001 \001(\tB:\n)org.whispe" + "T\020\005\" \n\020ProvisioningUuid\022\014\n\004uuid\030\001 \001(\tB:\n" +
"rsystems.textsecuregcm.entitiesB\rMessage" + ")org.whispersystems.textsecuregcm.entiti" +
"Protos" "esB\rMessageProtos"
}; };
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
public com.google.protobuf.ExtensionRegistry assignDescriptors( public com.google.protobuf.ExtensionRegistry assignDescriptors(
com.google.protobuf.Descriptors.FileDescriptor root) { com.google.protobuf.Descriptors.FileDescriptor root) {
descriptor = root; descriptor = root;
internal_static_textsecure_OutgoingMessageSignal_descriptor = internal_static_textsecure_Envelope_descriptor =
getDescriptor().getMessageTypes().get(0); getDescriptor().getMessageTypes().get(0);
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable = new internal_static_textsecure_Envelope_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable( com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_OutgoingMessageSignal_descriptor, internal_static_textsecure_Envelope_descriptor,
new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "Message", }); new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "LegacyMessage", "Content", });
internal_static_textsecure_ProvisioningUuid_descriptor = internal_static_textsecure_ProvisioningUuid_descriptor =
getDescriptor().getMessageTypes().get(1); getDescriptor().getMessageTypes().get(1);
internal_static_textsecure_ProvisioningUuid_fieldAccessorTable = new internal_static_textsecure_ProvisioningUuid_fieldAccessorTable = new

View File

@@ -26,10 +26,14 @@ public class OutgoingMessageEntity {
@JsonProperty @JsonProperty
private byte[] message; private byte[] message;
@JsonProperty
private byte[] content;
public OutgoingMessageEntity() {} public OutgoingMessageEntity() {}
public OutgoingMessageEntity(long id, int type, String relay, long timestamp, public OutgoingMessageEntity(long id, int type, String relay, long timestamp,
String source, int sourceDevice, byte[] message) String source, int sourceDevice, byte[] message,
byte[] content)
{ {
this.id = id; this.id = id;
this.type = type; this.type = type;
@@ -38,6 +42,7 @@ public class OutgoingMessageEntity {
this.source = source; this.source = source;
this.sourceDevice = sourceDevice; this.sourceDevice = sourceDevice;
this.message = message; this.message = message;
this.content = content;
} }
public int getType() { public int getType() {
@@ -64,7 +69,12 @@ public class OutgoingMessageEntity {
return message; return message;
} }
public byte[] getContent() {
return content;
}
public long getId() { public long getId() {
return id; return id;
} }
} }

View File

@@ -10,14 +10,21 @@ public class OutgoingMessageEntityList {
@JsonProperty @JsonProperty
private List<OutgoingMessageEntity> messages; private List<OutgoingMessageEntity> messages;
@JsonProperty
private boolean more;
public OutgoingMessageEntityList() {} public OutgoingMessageEntityList() {}
public OutgoingMessageEntityList(List<OutgoingMessageEntity> messages) { public OutgoingMessageEntityList(List<OutgoingMessageEntity> messages, boolean more) {
this.messages = messages; this.messages = messages;
this.more = more;
} }
@VisibleForTesting
public List<OutgoingMessageEntity> getMessages() { public List<OutgoingMessageEntity> getMessages() {
return messages; return messages;
} }
public boolean hasMore() {
return more;
}
} }

View File

@@ -18,17 +18,15 @@ package org.whispersystems.textsecuregcm.federation;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.Client; import org.apache.http.config.Registry;
import com.sun.jersey.api.client.ClientHandlerException; import org.apache.http.config.RegistryBuilder;
import com.sun.jersey.api.client.ClientResponse; import org.apache.http.conn.socket.ConnectionSocketFactory;
import com.sun.jersey.api.client.UniformInterfaceException; import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import com.sun.jersey.api.client.WebResource; import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.client.urlconnection.HTTPSProperties;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.bouncycastle.openssl.PEMReader; import org.bouncycastle.openssl.PEMReader;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount; import org.whispersystems.textsecuregcm.entities.AccountCount;
@@ -38,11 +36,13 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -57,7 +57,10 @@ import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.List; import java.util.List;
import java.util.Map;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.setup.Environment;
public class FederatedClient { public class FederatedClient {
@@ -73,15 +76,14 @@ public class FederatedClient {
private final FederatedPeer peer; private final FederatedPeer peer;
private final Client client; private final Client client;
private final String authorizationHeader;
public FederatedClient(String federationName, FederatedPeer peer) public FederatedClient(Environment environment, JerseyClientConfiguration configuration,
String federationName, FederatedPeer peer)
throws IOException throws IOException
{ {
try { try {
this.client = Client.create(getClientConfig(peer)); this.client = createClient(environment, configuration, federationName, peer);
this.peer = peer; this.peer = peer;
this.authorizationHeader = getAuthorizationHeader(federationName, peer);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
throw new AssertionError(e); throw new AssertionError(e);
} catch (KeyStoreException | KeyManagementException | CertificateException e) { } catch (KeyStoreException | KeyManagementException | CertificateException e) {
@@ -91,20 +93,14 @@ public class FederatedClient {
public URL getSignedAttachmentUri(long attachmentId) throws IOException { public URL getSignedAttachmentUri(long attachmentId) throws IOException {
try { try {
WebResource resource = client.resource(peer.getUrl()) AttachmentUri response = client.target(peer.getUrl())
.path(String.format(ATTACHMENT_URI_PATH, attachmentId)); .path(String.format(ATTACHMENT_URI_PATH, attachmentId))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(AttachmentUri.class);
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON) return response.getLocation();
.header("Authorization", authorizationHeader) } catch (ProcessingException e) {
.get(ClientResponse.class);
if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new WebApplicationException(clientResponseToResponse(response));
}
return response.getEntity(AttachmentUri.class).getLocation();
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("Bad URI", e); logger.warn("Bad URI", e);
throw new IOException(e); throw new IOException(e);
} }
@@ -112,19 +108,14 @@ public class FederatedClient {
public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) { public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device)); PreKeyResponseV1 response = client.target(peer.getUrl())
.path(String.format(PREKEY_PATH_DEVICE_V1, destination, device))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(PreKeyResponseV1.class);
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON) return Optional.of(response);
.header("Authorization", authorizationHeader) } catch (ProcessingException e) {
.get(ClientResponse.class);
if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new WebApplicationException(clientResponseToResponse(response));
}
return Optional.of(response.getEntity(PreKeyResponseV1.class));
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e); logger.warn("PreKey", e);
return Optional.absent(); return Optional.absent();
} }
@@ -132,34 +123,29 @@ public class FederatedClient {
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) { public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device)); PreKeyResponseV2 response = client.target(peer.getUrl())
.path(String.format(PREKEY_PATH_DEVICE_V2, destination, device))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(PreKeyResponseV2.class);
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON) return Optional.of(response);
.header("Authorization", authorizationHeader) } catch (ProcessingException e) {
.get(ClientResponse.class);
if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new WebApplicationException(clientResponseToResponse(response));
}
return Optional.of(response.getEntity(PreKeyResponseV2.class));
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e); logger.warn("PreKey", e);
return Optional.absent(); return Optional.absent();
} }
} }
public int getUserCount() { public int getUserCount() {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH); AccountCount count = client.target(peer.getUrl())
AccountCount count = resource.accept(MediaType.APPLICATION_JSON) .path(USER_COUNT_PATH)
.header("Authorization", authorizationHeader) .request()
.get(AccountCount.class); .accept(MediaType.APPLICATION_JSON_TYPE)
.get(AccountCount.class);
return count.getCount(); return count.getCount();
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (ProcessingException e) {
logger.warn("User Count", e); logger.warn("User Count", e);
return 0; return 0;
} }
@@ -167,13 +153,14 @@ public class FederatedClient {
public List<ClientContact> getUserTokens(int offset) { public List<ClientContact> getUserTokens(int offset) {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(USER_TOKENS_PATH, offset)); ClientContacts contacts = client.target(peer.getUrl())
ClientContacts contacts = resource.accept(MediaType.APPLICATION_JSON) .path(String.format(USER_TOKENS_PATH, offset))
.header("Authorization", authorizationHeader) .request()
.get(ClientContacts.class); .accept(MediaType.APPLICATION_JSON_TYPE)
.get(ClientContacts.class);
return contacts.getContacts(); return contacts.getContacts();
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (ProcessingException e) {
logger.warn("User Tokens", e); logger.warn("User Tokens", e);
return null; return null;
} }
@@ -182,46 +169,53 @@ public class FederatedClient {
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages) public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
throws IOException throws IOException
{ {
Response response = null;
try { try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination)); response = client.target(peer.getUrl())
ClientResponse response = resource.type(MediaType.APPLICATION_JSON) .path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination))
.header("Authorization", authorizationHeader) .request()
.entity(messages) .put(Entity.json(messages));
.put(ClientResponse.class);
if (response.getStatus() != 200 && response.getStatus() != 204) { if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new WebApplicationException(clientResponseToResponse(response)); if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
} }
} catch (UniformInterfaceException | ClientHandlerException e) {
} catch (ProcessingException e) {
logger.warn("sendMessage", e); logger.warn("sendMessage", e);
throw new IOException(e); throw new IOException(e);
} finally {
if (response != null) response.close();
} }
} }
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId) public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException throws IOException
{ {
Response response = null;
try { try {
String path = String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId); response = client.target(peer.getUrl())
WebResource resource = client.resource(peer.getUrl()).path(path); .path(String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId))
ClientResponse response = resource.type(MediaType.APPLICATION_JSON) .request()
.header("Authorization", authorizationHeader) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
.put(ClientResponse.class); .put(Entity.entity("", MediaType.APPLICATION_JSON_TYPE));
if (response.getStatus() != 200 && response.getStatus() != 204) { if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new WebApplicationException(clientResponseToResponse(response)); if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
} }
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (ProcessingException e) {
logger.warn("sendMessage", e); logger.warn("sendMessage", e);
throw new IOException(e); throw new IOException(e);
} finally {
if (response != null) response.close();
} }
} }
private String getAuthorizationHeader(String federationName, FederatedPeer peer) { private Client createClient(Environment environment, JerseyClientConfiguration configuration,
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes()); String federationName, FederatedPeer peer)
}
private ClientConfig getClientConfig(FederatedPeer peer)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, CertificateException throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, CertificateException
{ {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
@@ -230,12 +224,19 @@ public class FederatedClient {
SSLContext sslContext = SSLContext.getInstance("TLS"); SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), SecureRandom.getInstance("SHA1PRNG")); sslContext.init(null, trustManagerFactory.getTrustManagers(), SecureRandom.getInstance("SHA1PRNG"));
ClientConfig config = new DefaultClientConfig(); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier());
config.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionSocketFactory).build();
new HTTPSProperties(new StrictHostnameVerifier(), sslContext));
config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
return config; Client client = new JerseyClientBuilder(environment).using(configuration)
.using(registry)
.build("FederatedClient");
client.property(ClientProperties.CONNECT_TIMEOUT, 5000);
client.property(ClientProperties.READ_TIMEOUT, 10000);
client.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
client.register(HttpAuthenticationFeature.basic(federationName, peer.getAuthenticationToken()));
return client;
} }
private KeyStore initializeTrustStore(String name, String pemCertificate) private KeyStore initializeTrustStore(String name, String pemCertificate)
@@ -261,19 +262,6 @@ public class FederatedClient {
} }
} }
private Response clientResponseToResponse(ClientResponse r) {
Response.ResponseBuilder rb = Response.status(r.getStatus());
for (Map.Entry<String, List<String>> entry : r.getHeaders().entrySet()) {
for (String value : entry.getValue()) {
rb.header(entry.getKey(), value);
}
}
rb.entity(r.getEntityInputStream());
return rb.build();
}
public String getPeerName() { public String getPeerName() {
return peer.getName(); return peer.getName();
} }

View File

@@ -25,13 +25,18 @@ import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.setup.Environment;
public class FederatedClientManager { public class FederatedClientManager {
private final Logger logger = LoggerFactory.getLogger(FederatedClientManager.class); private final Logger logger = LoggerFactory.getLogger(FederatedClientManager.class);
private final HashMap<String, FederatedClient> clients = new HashMap<>(); private final HashMap<String, FederatedClient> clients = new HashMap<>();
public FederatedClientManager(FederationConfiguration federationConfig) public FederatedClientManager(Environment environment,
JerseyClientConfiguration clientConfig,
FederationConfiguration federationConfig)
throws IOException throws IOException
{ {
List<FederatedPeer> peers = federationConfig.getPeers(); List<FederatedPeer> peers = federationConfig.getPeers();
@@ -40,7 +45,7 @@ public class FederatedClientManager {
if (peers != null) { if (peers != null) {
for (FederatedPeer peer : peers) { for (FederatedPeer peer : peers) {
logger.info("Adding peer: " + peer.getName()); logger.info("Adding peer: " + peer.getName());
clients.put(peer.getName(), new FederatedClient(identity, peer)); clients.put(peer.getName(), new FederatedClient(environment, clientConfig, identity, peer));
} }
} }
} }

View File

@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
@Override @Override
public Optional<Device> getAuthenticatedDevice() { public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis())); return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "NA"));
} }
} }

View File

@@ -7,9 +7,9 @@ import java.sql.SQLException;
import io.dropwizard.Configuration; import io.dropwizard.Configuration;
import io.dropwizard.cli.ConfiguredCommand; import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.db.DatabaseConfiguration; import io.dropwizard.db.DatabaseConfiguration;
import io.dropwizard.db.ManagedDataSource; import io.dropwizard.db.ManagedDataSource;
import io.dropwizard.db.PooledDataSourceFactory;
import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Bootstrap;
import liquibase.Liquibase; import liquibase.Liquibase;
import liquibase.exception.LiquibaseException; import liquibase.exception.LiquibaseException;
@@ -40,10 +40,8 @@ public abstract class AbstractLiquibaseCommand<T extends Configuration> extends
@Override @Override
@SuppressWarnings("UseOfSystemOutOrSystemErr") @SuppressWarnings("UseOfSystemOutOrSystemErr")
protected void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception { protected void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception {
final DataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration); final PooledDataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration);
dbConfig.setMaxSize(1); dbConfig.asSingleConnectionPool();
dbConfig.setMinSize(1);
dbConfig.setInitialSize(1);
try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) { try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) {
run(namespace, liquibase); run(namespace, liquibase);
@@ -53,7 +51,7 @@ public abstract class AbstractLiquibaseCommand<T extends Configuration> extends
} }
} }
private CloseableLiquibase openLiquibase(final DataSourceFactory dataSourceFactory, final Namespace namespace) private CloseableLiquibase openLiquibase(final PooledDataSourceFactory dataSourceFactory, final Namespace namespace)
throws ClassNotFoundException, SQLException, LiquibaseException throws ClassNotFoundException, SQLException, LiquibaseException
{ {
final ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(), "liquibase"); final ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(), "liquibase");

View File

@@ -0,0 +1,33 @@
package org.whispersystems.textsecuregcm.mappers;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.controllers.DeviceLimitExceededException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class DeviceLimitExceededExceptionMapper implements ExceptionMapper<DeviceLimitExceededException> {
@Override
public Response toResponse(DeviceLimitExceededException exception) {
return Response.status(411)
.entity(new DeviceLimitExceededDetails(exception.getCurrentDevices(),
exception.getMaxDevices()))
.build();
}
private static class DeviceLimitExceededDetails {
@JsonProperty
private int current;
@JsonProperty
private int max;
public DeviceLimitExceededDetails(int current, int max) {
this.current = current;
this.max = max;
}
}
}

View File

@@ -0,0 +1,19 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import java.io.File;
public class FileDescriptorGauge implements Gauge<Integer> {
@Override
public Integer getValue() {
File file = new File("/proc/self/fd");
if (file.isDirectory() && file.exists()) {
return file.list().length;
}
return 0;
}
}

View File

@@ -31,9 +31,7 @@ public class RedisHealthCheck extends HealthCheck {
@Override @Override
protected Result check() throws Exception { protected Result check() throws Exception {
Jedis client = clientPool.getResource(); try (Jedis client = clientPool.getResource()) {
try {
client.set("HEALTH", "test"); client.set("HEALTH", "test");
if (!"test".equals(client.get("HEALTH"))) { if (!"test".equals(client.get("HEALTH"))) {
@@ -41,8 +39,6 @@ public class RedisHealthCheck extends HealthCheck {
} }
return Result.healthy(); return Result.healthy();
} finally {
clientPool.returnResource(client);
} }
} }
} }

View File

@@ -0,0 +1,220 @@
package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.RatioGauge;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnectionInfo;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
public class ApnFallbackManager implements Managed, Runnable, DispatchChannel {
private static final Logger logger = LoggerFactory.getLogger(ApnFallbackManager.class);
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private static final Meter voipOneSuccess = metricRegistry.meter(name(ApnFallbackManager.class, "voip_one_success"));
private static final Meter voipOneDelivery = metricRegistry.meter(name(ApnFallbackManager.class, "voip_one_failure"));
private static final Histogram voipOneSuccessHistogram = metricRegistry.histogram(name(ApnFallbackManager.class, "voip_one_success_histogram"));
static {
metricRegistry.register(name(ApnFallbackManager.class, "voip_one_success_ratio"), new VoipRatioGauge(voipOneSuccess, voipOneDelivery));
}
private final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
private final PushServiceClient pushServiceClient;
private final PubSubManager pubSubManager;
public ApnFallbackManager(PushServiceClient pushServiceClient, PubSubManager pubSubManager) {
this.pushServiceClient = pushServiceClient;
this.pubSubManager = pubSubManager;
}
public void schedule(final WebsocketAddress address, ApnFallbackTask task) {
voipOneDelivery.mark();
if (taskQueue.put(address, task)) {
pubSubManager.subscribe(new WebSocketConnectionInfo(address), this);
}
}
private void cancel(WebsocketAddress address) {
ApnFallbackTask task = taskQueue.remove(address);
if (task != null) {
pubSubManager.unsubscribe(new WebSocketConnectionInfo(address), this);
voipOneSuccess.mark();
voipOneSuccessHistogram.update(System.currentTimeMillis() - task.getScheduledTime());
}
}
@Override
public void start() throws Exception {
new Thread(this).start();
}
@Override
public void stop() throws Exception {
}
@Override
public void run() {
while (true) {
try {
Entry<WebsocketAddress, ApnFallbackTask> taskEntry = taskQueue.get();
ApnFallbackTask task = taskEntry.getValue();
pubSubManager.unsubscribe(new WebSocketConnectionInfo(taskEntry.getKey()), this);
pushServiceClient.send(new ApnMessage(task.getMessage(), task.getApnId(),
false, ApnMessage.MAX_EXPIRATION));
} catch (Throwable e) {
logger.warn("ApnFallbackThread", e);
}
}
}
@Override
public void onDispatchMessage(String channel, byte[] message) {
try {
PubSubMessage notification = PubSubMessage.parseFrom(message);
if (notification.getType().getNumber() == PubSubMessage.Type.CONNECTED_VALUE) {
WebSocketConnectionInfo address = new WebSocketConnectionInfo(channel);
cancel(address.getWebsocketAddress());
} else {
logger.warn("Got strange pubsub type: " + notification.getType().getNumber());
}
} catch (WebSocketConnectionInfo.FormattingException e) {
logger.warn("Bad formatting?", e);
} catch (InvalidProtocolBufferException e) {
logger.warn("Bad protobuf", e);
}
}
@Override
public void onDispatchSubscribed(String channel) {}
@Override
public void onDispatchUnsubscribed(String channel) {}
public static class ApnFallbackTask {
private final long delay;
private final long scheduledTime;
private final String apnId;
private final ApnMessage message;
public ApnFallbackTask(String apnId, ApnMessage message) {
this(apnId, message, TimeUnit.SECONDS.toMillis(30));
}
@VisibleForTesting
public ApnFallbackTask(String apnId, ApnMessage message, long delay) {
this.scheduledTime = System.currentTimeMillis();
this.delay = delay;
this.apnId = apnId;
this.message = message;
}
public String getApnId() {
return apnId;
}
public ApnMessage getMessage() {
return message;
}
public long getScheduledTime() {
return scheduledTime;
}
public long getExecutionTime() {
return scheduledTime + delay;
}
public long getDelay() {
return delay;
}
}
@VisibleForTesting
public static class ApnFallbackTaskQueue {
private final LinkedHashMap<WebsocketAddress, ApnFallbackTask> tasks = new LinkedHashMap<>();
public Entry<WebsocketAddress, ApnFallbackTask> get() {
while (true) {
long timeDelta;
synchronized (tasks) {
while (tasks.isEmpty()) Util.wait(tasks);
Iterator<Entry<WebsocketAddress, ApnFallbackTask>> iterator = tasks.entrySet().iterator();
Entry<WebsocketAddress, ApnFallbackTask> nextTask = iterator.next();
timeDelta = nextTask.getValue().getExecutionTime() - System.currentTimeMillis();
if (timeDelta <= 0) {
iterator.remove();
return nextTask;
}
}
Util.sleep(timeDelta);
}
}
public boolean put(WebsocketAddress address, ApnFallbackTask task) {
synchronized (tasks) {
ApnFallbackTask previous = tasks.put(address, task);
tasks.notifyAll();
return previous == null;
}
}
public ApnFallbackTask remove(WebsocketAddress address) {
synchronized (tasks) {
return tasks.remove(address);
}
}
}
private static class VoipRatioGauge extends RatioGauge {
private final Meter success;
private final Meter attempts;
private VoipRatioGauge(Meter success, Meter attempts) {
this.success = success;
this.attempts = attempts;
}
@Override
protected Ratio getRatio() {
return Ratio.of(success.getFiveMinuteRate(), attempts.getFiveMinuteRate());
}
}
}

View File

@@ -73,7 +73,7 @@ public class FeedbackHandler implements Managed, Runnable {
if (event.getRegistrationId().equals(device.get().getGcmId())) { if (event.getRegistrationId().equals(device.get().getGcmId())) {
logger.info("GCM Unregister GCM ID matches!"); logger.info("GCM Unregister GCM ID matches!");
if (device.get().getPushTimestamp() == 0 || if (device.get().getPushTimestamp() == 0 ||
event.getTimestamp() > device.get().getPushTimestamp()) event.getTimestamp() > (device.get().getPushTimestamp() + TimeUnit.SECONDS.toMillis(10)))
{ {
logger.info("GCM Unregister Timestamp matches!"); logger.info("GCM Unregister Timestamp matches!");
@@ -82,6 +82,7 @@ public class FeedbackHandler implements Managed, Runnable {
device.get().setGcmId(event.getCanonicalId()); device.get().setGcmId(event.getCanonicalId());
} else { } else {
device.get().setGcmId(null); device.get().setGcmId(null);
device.get().setFetchesMessages(false);
} }
accountsManager.update(account.get()); accountsManager.update(account.get());
} }

View File

@@ -16,102 +16,159 @@
*/ */
package org.whispersystems.textsecuregcm.push; package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.SharedMetricRegistries;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ApnMessage; import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.GcmMessage; import org.whispersystems.textsecuregcm.entities.GcmMessage;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask;
import org.whispersystems.textsecuregcm.push.WebsocketSender.DeliveryStatus; import org.whispersystems.textsecuregcm.push.WebsocketSender.DeliveryStatus;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.BlockingThreadPoolExecutor;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import java.util.concurrent.TimeUnit;
public class PushSender { import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
public class PushSender implements Managed {
private final Logger logger = LoggerFactory.getLogger(PushSender.class); private final Logger logger = LoggerFactory.getLogger(PushSender.class);
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"badge\":%d,\"alert\":{\"loc-key\":\"APN_Message\"}}}"; private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"badge\":%d,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
private final PushServiceClient pushServiceClient; private final ApnFallbackManager apnFallbackManager;
private final WebsocketSender webSocketSender; private final PushServiceClient pushServiceClient;
private final WebsocketSender webSocketSender;
private final BlockingThreadPoolExecutor executor;
private final int queueSize;
public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) { public PushSender(ApnFallbackManager apnFallbackManager, PushServiceClient pushServiceClient,
this.pushServiceClient = pushServiceClient; WebsocketSender websocketSender, int queueSize)
this.webSocketSender = websocketSender; {
this.apnFallbackManager = apnFallbackManager;
this.pushServiceClient = pushServiceClient;
this.webSocketSender = websocketSender;
this.queueSize = queueSize;
this.executor = new BlockingThreadPoolExecutor(50, queueSize);
SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME)
.register(name(PushSender.class, "send_queue_depth"),
new Gauge<Integer>() {
@Override
public Integer getValue() {
return executor.getSize();
}
});
} }
public void sendMessage(Account account, Device device, OutgoingMessageSignal message) public void sendMessage(final Account account, final Device device, final Envelope message)
throws NotPushRegisteredException
{
if (device.getGcmId() == null && device.getApnId() == null && !device.getFetchesMessages()) {
throw new NotPushRegisteredException("No delivery possible!");
}
if (queueSize > 0) {
executor.execute(new Runnable() {
@Override
public void run() {
sendSynchronousMessage(account, device, message);
}
});
} else {
sendSynchronousMessage(account, device, message);
}
}
public void sendQueuedNotification(Account account, Device device, int messageQueueDepth)
throws NotPushRegisteredException, TransientPushFailureException throws NotPushRegisteredException, TransientPushFailureException
{ {
if (device.getGcmId() != null) sendGcmMessage(account, device, message); if (device.getGcmId() != null) sendGcmNotification(account, device);
else if (device.getApnId() != null) sendApnMessage(account, device, message); else if (device.getApnId() != null) sendApnNotification(account, device, messageQueueDepth);
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message); else if (!device.getFetchesMessages()) throw new NotPushRegisteredException("No notification possible!");
else throw new NotPushRegisteredException("No delivery possible!");
} }
public WebsocketSender getWebSocketSender() { public WebsocketSender getWebSocketSender() {
return webSocketSender; return webSocketSender;
} }
private void sendGcmMessage(Account account, Device device, OutgoingMessageSignal message) private void sendSynchronousMessage(Account account, Device device, Envelope message) {
throws TransientPushFailureException, NotPushRegisteredException if (device.getGcmId() != null) sendGcmMessage(account, device, message);
{ else if (device.getApnId() != null) sendApnMessage(account, device, message);
if (device.getFetchesMessages()) sendNotificationGcmMessage(account, device, message); else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message);
else sendPayloadGcmMessage(account, device, message); else throw new AssertionError();
} }
private void sendPayloadGcmMessage(Account account, Device device, OutgoingMessageSignal message) private void sendGcmMessage(Account account, Device device, Envelope message) {
throws TransientPushFailureException, NotPushRegisteredException
{
try {
String number = account.getNumber();
long deviceId = device.getId();
String registrationId = device.getGcmId();
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int) deviceId,
encryptedMessage.toEncodedString(), isReceipt, false);
pushServiceClient.send(gcmMessage);
} catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e);
}
}
private void sendNotificationGcmMessage(Account account, Device device, OutgoingMessageSignal message)
throws TransientPushFailureException
{
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, message, WebsocketSender.Type.GCM); DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, message, WebsocketSender.Type.GCM);
if (!deliveryStatus.isDelivered()) { if (!deliveryStatus.isDelivered()) {
sendGcmNotification(account, device);
}
}
private void sendGcmNotification(Account account, Device device) {
try {
GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(), GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(),
(int)device.getId(), "", false, true); (int)device.getId(), "", false, true);
pushServiceClient.send(gcmMessage); pushServiceClient.send(gcmMessage);
} catch (TransientPushFailureException e) {
logger.warn("SILENT PUSH LOSS", e);
} }
} }
private void sendApnMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage) private void sendApnMessage(Account account, Device device, Envelope outgoingMessage) {
throws TransientPushFailureException
{
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.APN); DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.APN);
if (!deliveryStatus.isDelivered() && outgoingMessage.getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) { if (!deliveryStatus.isDelivered() && outgoingMessage.getType() != Envelope.Type.RECEIPT) {
String apnId = Util.isEmpty(device.getVoipApnId()) ? device.getApnId() : device.getVoipApnId(); sendApnNotification(account, device, deliveryStatus.getMessageQueueDepth());
boolean isVoip = !Util.isEmpty(device.getVoipApnId());
ApnMessage apnMessage = new ApnMessage(apnId, account.getNumber(), (int)device.getId(),
String.format(APN_PAYLOAD, deliveryStatus.getMessageQueueDepth()),
isVoip);
pushServiceClient.send(apnMessage);
} }
} }
private void sendWebSocketMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage) private void sendApnNotification(Account account, Device device, int messageQueueDepth) {
ApnMessage apnMessage;
if (!Util.isEmpty(device.getVoipApnId())) {
apnMessage = new ApnMessage(device.getVoipApnId(), account.getNumber(), (int)device.getId(),
String.format(APN_PAYLOAD, messageQueueDepth),
true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30));
apnFallbackManager.schedule(new WebsocketAddress(account.getNumber(), device.getId()),
new ApnFallbackTask(device.getApnId(), apnMessage));
} else {
apnMessage = new ApnMessage(device.getApnId(), account.getNumber(), (int)device.getId(),
String.format(APN_PAYLOAD, messageQueueDepth),
false, ApnMessage.MAX_EXPIRATION);
}
try {
pushServiceClient.send(apnMessage);
} catch (TransientPushFailureException e) {
logger.warn("SILENT PUSH LOSS", e);
}
}
private void sendWebSocketMessage(Account account, Device device, Envelope outgoingMessage)
{ {
webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.WEB); webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.WEB);
} }
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
executor.shutdown();
executor.awaitTermination(5, TimeUnit.MINUTES);
}
} }

View File

@@ -1,9 +1,5 @@
package org.whispersystems.textsecuregcm.push; package org.whispersystems.textsecuregcm.push;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration; import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
@@ -13,7 +9,11 @@ import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
import org.whispersystems.textsecuregcm.entities.UnregisteredEventList; import org.whispersystems.textsecuregcm.entities.UnregisteredEventList;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@@ -57,16 +57,17 @@ public class PushServiceClient {
private void sendPush(String path, Object entity) throws TransientPushFailureException { private void sendPush(String path, Object entity) throws TransientPushFailureException {
try { try {
ClientResponse response = client.resource("http://" + host + ":" + port + path) Response response = client.target("http://" + host + ":" + port)
.header("Authorization", authorization) .path(path)
.entity(entity, MediaType.APPLICATION_JSON) .request()
.put(ClientResponse.class); .header("Authorization", authorization)
.put(Entity.entity(entity, MediaType.APPLICATION_JSON_TYPE));
if (response.getStatus() != 204 && response.getStatus() != 200) { if (response.getStatus() != 204 && response.getStatus() != 200) {
logger.warn("PushServer response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase()); logger.warn("PushServer response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase());
throw new TransientPushFailureException("Bad response: " + response.getStatus()); throw new TransientPushFailureException("Bad response: " + response.getStatus());
} }
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (ProcessingException e) {
logger.warn("Push error: ", e); logger.warn("Push error: ", e);
throw new TransientPushFailureException(e); throw new TransientPushFailureException(e);
} }
@@ -74,12 +75,14 @@ public class PushServiceClient {
private List<UnregisteredEvent> getFeedback(String path) throws IOException { private List<UnregisteredEvent> getFeedback(String path) throws IOException {
try { try {
UnregisteredEventList unregisteredEvents = client.resource("http://" + host + ":" + port + path) UnregisteredEventList unregisteredEvents = client.target("http://" + host + ":" + port)
.path(path)
.request()
.header("Authorization", authorization) .header("Authorization", authorization)
.get(UnregisteredEventList.class); .get(UnregisteredEventList.class);
return unregisteredEvents.getDevices(); return unregisteredEvents.getDevices();
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (ProcessingException e) {
logger.warn("Request error:", e); logger.warn("Request error:", e);
throw new IOException(e); throw new IOException(e);
} }

View File

@@ -2,7 +2,7 @@ package org.whispersystems.textsecuregcm.push;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException; import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException; import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
@@ -55,15 +55,13 @@ public class ReceiptSender {
private void sendDirectReceipt(Account source, String destination, long messageId) private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{ {
Account destinationAccount = getDestinationAccount(destination); Account destinationAccount = getDestinationAccount(destination);
Set<Device> destinationDevices = destinationAccount.getDevices(); Set<Device> destinationDevices = destinationAccount.getDevices();
Envelope.Builder message = Envelope.newBuilder()
MessageProtos.OutgoingMessageSignal.Builder message = .setSource(source.getNumber())
MessageProtos.OutgoingMessageSignal.newBuilder() .setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
.setSource(source.getNumber()) .setTimestamp(messageId)
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId()) .setType(Envelope.Type.RECEIPT);
.setTimestamp(messageId)
.setType(MessageProtos.OutgoingMessageSignal.Type.RECEIPT_VALUE);
if (source.getRelay().isPresent()) { if (source.getRelay().isPresent()) {
message.setRelay(source.getRelay().get()); message.setRelay(source.getRelay().get());

View File

@@ -31,7 +31,7 @@ import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress; import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage; import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public class WebsocketSender { public class WebsocketSender {
@@ -46,6 +46,7 @@ public class WebsocketSender {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter websocketRequeueMeter = metricRegistry.meter(name(getClass(), "ws_requeue"));
private final Meter websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" )); private final Meter websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" ));
private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" )); private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" ));
@@ -66,7 +67,7 @@ public class WebsocketSender {
this.pubSubManager = pubSubManager; this.pubSubManager = pubSubManager;
} }
public DeliveryStatus sendMessage(Account account, Device device, OutgoingMessageSignal message, Type channel) { public DeliveryStatus sendMessage(Account account, Device device, Envelope message, Type channel) {
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId()); WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
PubSubMessage pubSubMessage = PubSubMessage.newBuilder() PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.DELIVER) .setType(PubSubMessage.Type.DELIVER)
@@ -84,15 +85,24 @@ public class WebsocketSender {
else if (channel == Type.GCM) gcmOfflineMeter.mark(); else if (channel == Type.GCM) gcmOfflineMeter.mark();
else websocketOfflineMeter.mark(); else websocketOfflineMeter.mark();
int queueDepth = messagesManager.insert(account.getNumber(), device.getId(), message); int queueDepth = queueMessage(account, device, message);
pubSubManager.publish(address, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.QUERY_DB)
.build());
return new DeliveryStatus(false, queueDepth); return new DeliveryStatus(false, queueDepth);
} }
} }
public int queueMessage(Account account, Device device, Envelope message) {
websocketRequeueMeter.mark();
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
int queueDepth = messagesManager.insert(account.getNumber(), device.getId(), message);
pubSubManager.publish(address, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.QUERY_DB)
.build());
return queueDepth;
}
public boolean sendProvisioningMessage(ProvisioningAddress address, byte[] body) { public boolean sendProvisioningMessage(ProvisioningAddress address, byte[] body) {
PubSubMessage pubSubMessage = PubSubMessage.newBuilder() PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.DELIVER) .setType(PubSubMessage.Type.DELIVER)

View File

@@ -1,91 +0,0 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.sms;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.util.Constants;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import static com.codahale.metrics.MetricRegistry.name;
public class NexmoSmsSender {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
private final Logger logger = LoggerFactory.getLogger(NexmoSmsSender.class);
private static final String NEXMO_SMS_URL =
"https://rest.nexmo.com/sms/json?api_key=%s&api_secret=%s&from=%s&to=%s&text=%s";
private static final String NEXMO_VOX_URL =
"https://rest.nexmo.com/tts/json?api_key=%s&api_secret=%s&to=%s&text=%s";
private final String apiKey;
private final String apiSecret;
private final String number;
public NexmoSmsSender(NexmoConfiguration config) {
this.apiKey = config.getApiKey();
this.apiSecret = config.getApiSecret();
this.number = config.getNumber();
}
public void deliverSmsVerification(String destination, String verificationCode) throws IOException {
URL url = new URL(String.format(NEXMO_SMS_URL, apiKey, apiSecret, number, destination,
URLEncoder.encode(SmsSender.SMS_VERIFICATION_TEXT + verificationCode, "UTF-8")));
URLConnection connection = url.openConnection();
connection.setDoInput(true);
connection.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
while (reader.readLine() != null) {}
reader.close();
smsMeter.mark();
}
public void deliverVoxVerification(String destination, String message) throws IOException {
URL url = new URL(String.format(NEXMO_VOX_URL, apiKey, apiSecret, destination,
URLEncoder.encode(SmsSender.VOX_VERIFICATION_TEXT + message, "UTF-8")));
URLConnection connection = url.openConnection();
connection.setDoInput(true);
connection.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
logger.debug(line);
}
reader.close();
voxMeter.mark();
}
}

View File

@@ -26,59 +26,41 @@ import java.io.IOException;
public class SmsSender { public class SmsSender {
static final String SMS_VERIFICATION_TEXT = "Your TextSecure verification code: "; static final String SMS_IOS_VERIFICATION_TEXT = "Your Signal verification code: %s\n\nOr tap: sgnl://verify/%s";
static final String VOX_VERIFICATION_TEXT = "Your TextSecure verification code is: "; static final String SMS_VERIFICATION_TEXT = "Your TextSecure verification code: %s";
static final String VOX_VERIFICATION_TEXT = "Your Signal verification code is: ";
private final Logger logger = LoggerFactory.getLogger(SmsSender.class); private final Logger logger = LoggerFactory.getLogger(SmsSender.class);
private final TwilioSmsSender twilioSender; private final TwilioSmsSender twilioSender;
private final Optional<NexmoSmsSender> nexmoSender;
private final boolean isTwilioInternational;
public SmsSender(TwilioSmsSender twilioSender, public SmsSender(TwilioSmsSender twilioSender)
Optional<NexmoSmsSender> nexmoSender,
boolean isTwilioInternational)
{ {
this.isTwilioInternational = isTwilioInternational; this.twilioSender = twilioSender;
this.twilioSender = twilioSender;
this.nexmoSender = nexmoSender;
} }
public void deliverSmsVerification(String destination, String verificationCode) public void deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode)
throws IOException throws IOException
{ {
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) { // Fix up mexico numbers to 'mobile' format just for SMS delivery.
nexmoSender.get().deliverSmsVerification(destination, verificationCode); if (destination.startsWith("+42") && !destination.startsWith("+421")) {
} else { destination = "+421" + destination.substring(3);
try { }
twilioSender.deliverSmsVerification(destination, verificationCode);
} catch (TwilioRestException e) { try {
logger.info("Twilio SMS Failed: " + e.getErrorMessage()); twilioSender.deliverSmsVerification(destination, clientType, verificationCode);
if (nexmoSender.isPresent()) { } catch (TwilioRestException e) {
nexmoSender.get().deliverSmsVerification(destination, verificationCode); logger.info("Twilio SMS Failed: " + e.getErrorMessage());
}
}
} }
} }
public void deliverVoxVerification(String destination, String verificationCode) public void deliverVoxVerification(String destination, String verificationCode)
throws IOException throws IOException
{ {
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) { try {
nexmoSender.get().deliverVoxVerification(destination, verificationCode); twilioSender.deliverVoxVerification(destination, verificationCode);
} else { } catch (TwilioRestException e) {
try { logger.info("Twilio Vox Failed: " + e.getErrorMessage());
twilioSender.deliverVoxVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio Vox Failed: " + e.getErrorMessage());
if (nexmoSender.isPresent()) {
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
}
}
} }
} }
private boolean isTwilioDestination(String number) {
return isTwilioInternational || number.length() == 12 && number.startsWith("+1");
}
} }

View File

@@ -19,6 +19,7 @@ package org.whispersystems.textsecuregcm.sms;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import com.twilio.sdk.TwilioRestClient; import com.twilio.sdk.TwilioRestClient;
import com.twilio.sdk.TwilioRestException; import com.twilio.sdk.TwilioRestException;
import com.twilio.sdk.resource.factory.CallFactory; import com.twilio.sdk.resource.factory.CallFactory;
@@ -29,10 +30,12 @@ import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
@@ -47,27 +50,34 @@ public class TwilioSmsSender {
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered")); private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered")); private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
private final String accountId; private final String accountId;
private final String accountToken; private final String accountToken;
private final String number; private final ArrayList<String> numbers;
private final String localDomain; private final String localDomain;
private final Random random;
public TwilioSmsSender(TwilioConfiguration config) { public TwilioSmsSender(TwilioConfiguration config) {
this.accountId = config.getAccountId(); this.accountId = config.getAccountId();
this.accountToken = config.getAccountToken(); this.accountToken = config.getAccountToken();
this.number = config.getNumber(); this.numbers = new ArrayList<>(config.getNumbers());
this.localDomain = config.getLocalDomain(); this.localDomain = config.getLocalDomain();
this.random = new Random(System.currentTimeMillis());
} }
public void deliverSmsVerification(String destination, String verificationCode) public void deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode)
throws IOException, TwilioRestException throws IOException, TwilioRestException
{ {
TwilioRestClient client = new TwilioRestClient(accountId, accountToken); TwilioRestClient client = new TwilioRestClient(accountId, accountToken);
MessageFactory messageFactory = client.getAccount().getMessageFactory(); MessageFactory messageFactory = client.getAccount().getMessageFactory();
List<NameValuePair> messageParams = new LinkedList<>(); List<NameValuePair> messageParams = new LinkedList<>();
messageParams.add(new BasicNameValuePair("To", destination)); messageParams.add(new BasicNameValuePair("To", destination));
messageParams.add(new BasicNameValuePair("From", number)); messageParams.add(new BasicNameValuePair("From", getRandom(random, numbers)));
messageParams.add(new BasicNameValuePair("Body", SmsSender.SMS_VERIFICATION_TEXT + verificationCode));
if ("ios".equals(clientType.orNull())) {
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_IOS_VERIFICATION_TEXT, verificationCode, verificationCode)));
} else {
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_VERIFICATION_TEXT, verificationCode)));
}
try { try {
messageFactory.create(messageParams); messageFactory.create(messageParams);
@@ -85,7 +95,7 @@ public class TwilioSmsSender {
CallFactory callFactory = client.getAccount().getCallFactory(); CallFactory callFactory = client.getAccount().getCallFactory();
Map<String, String> callParams = new HashMap<>(); Map<String, String> callParams = new HashMap<>();
callParams.put("To", destination); callParams.put("To", destination);
callParams.put("From", number); callParams.put("From", getRandom(random, numbers));
callParams.put("Url", "https://" + localDomain + "/v1/accounts/voice/twiml/" + verificationCode); callParams.put("Url", "https://" + localDomain + "/v1/accounts/voice/twiml/" + verificationCode);
try { try {
@@ -96,4 +106,9 @@ public class TwilioSmsSender {
voxMeter.mark(); voxMeter.mark();
} }
private String getRandom(Random random, ArrayList<String> elements) {
return elements.get(random.nextInt(elements.size()));
}
} }

View File

@@ -32,9 +32,6 @@ public class Account {
@JsonProperty @JsonProperty
private String number; private String number;
@JsonProperty
private boolean supportsSms;
@JsonProperty @JsonProperty
private Set<Device> devices = new HashSet<>(); private Set<Device> devices = new HashSet<>();
@@ -47,10 +44,9 @@ public class Account {
public Account() {} public Account() {}
@VisibleForTesting @VisibleForTesting
public Account(String number, boolean supportsSms, Set<Device> devices) { public Account(String number, Set<Device> devices) {
this.number = number; this.number = number;
this.supportsSms = supportsSms; this.devices = devices;
this.devices = devices;
} }
public Optional<Device> getAuthenticatedDevice() { public Optional<Device> getAuthenticatedDevice() {
@@ -69,19 +65,15 @@ public class Account {
return number; return number;
} }
public boolean getSupportsSms() {
return supportsSms;
}
public void setSupportsSms(boolean supportsSms) {
this.supportsSms = supportsSms;
}
public void addDevice(Device device) { public void addDevice(Device device) {
this.devices.remove(device); this.devices.remove(device);
this.devices.add(device); this.devices.add(device);
} }
public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, false, "NA"));
}
public Set<Device> getDevices() { public Set<Device> getDevices() {
return devices; return devices;
} }
@@ -100,6 +92,16 @@ public class Account {
return Optional.absent(); return Optional.absent();
} }
public boolean isVoiceSupported() {
for (Device device : devices) {
if (device.isActive() && device.isVoiceSupported()) {
return true;
}
}
return false;
}
public boolean isActive() { public boolean isActive() {
return return
getMasterDevice().isPresent() && getMasterDevice().isPresent() &&

View File

@@ -16,8 +16,6 @@
*/ */
package org.whispersystems.textsecuregcm.storage; 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.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.skife.jdbi.v2.SQLStatement; import org.skife.jdbi.v2.SQLStatement;

View File

@@ -100,7 +100,7 @@ public class AccountsManager {
private void updateDirectory(Account account) { private void updateDirectory(Account account) {
if (account.isActive()) { if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber()); byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
directory.add(clientContact); directory.add(clientContact);
} else { } else {
directory.remove(account.getNumber()); directory.remove(account.getNumber());

View File

@@ -31,6 +31,9 @@ public class Device {
@JsonProperty @JsonProperty
private long id; private long id;
@JsonProperty
private String name;
@JsonProperty @JsonProperty
private String authToken; private String authToken;
@@ -64,14 +67,26 @@ public class Device {
@JsonProperty @JsonProperty
private long lastSeen; private long lastSeen;
@JsonProperty
private long created;
@JsonProperty
private boolean voice;
@JsonProperty
private String userAgent;
public Device() {} public Device() {}
public Device(long id, String authToken, String salt, public Device(long id, String name, String authToken, String salt,
String signalingKey, String gcmId, String apnId, String signalingKey, String gcmId, String apnId,
String voipApnId, boolean fetchesMessages, String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey, long lastSeen) int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created, boolean voice,
String userAgent)
{ {
this.id = id; this.id = id;
this.name = name;
this.authToken = authToken; this.authToken = authToken;
this.salt = salt; this.salt = salt;
this.signalingKey = signalingKey; this.signalingKey = signalingKey;
@@ -82,6 +97,9 @@ public class Device {
this.registrationId = registrationId; this.registrationId = registrationId;
this.signedPreKey = signedPreKey; this.signedPreKey = signedPreKey;
this.lastSeen = lastSeen; this.lastSeen = lastSeen;
this.created = created;
this.voice = voice;
this.userAgent = userAgent;
} }
public String getApnId() { public String getApnId() {
@@ -112,6 +130,14 @@ public class Device {
return lastSeen; return lastSeen;
} }
public void setCreated(long created) {
this.created = created;
}
public long getCreated() {
return this.created;
}
public String getGcmId() { public String getGcmId() {
return gcmId; return gcmId;
} }
@@ -132,6 +158,22 @@ public class Device {
this.id = id; this.id = id;
} }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isVoiceSupported() {
return voice;
}
public void setVoiceSupported(boolean voice) {
this.voice = voice;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) { public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.authToken = credentials.getHashedAuthenticationToken(); this.authToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt(); this.salt = credentials.getSalt();
@@ -188,6 +230,14 @@ public class Device {
return pushTimestamp; return pushTimestamp;
} }
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public String getUserAgent() {
return this.userAgent;
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
if (other == null || !(other instanceof Device)) return false; if (other == null || !(other instanceof Device)) return false;

View File

@@ -61,9 +61,9 @@ public class DirectoryManager {
} }
public void remove(byte[] token) { public void remove(byte[] token) {
Jedis jedis = redisPool.getResource(); try (Jedis jedis = redisPool.getResource()) {
jedis.hdel(DIRECTORY_KEY, token); jedis.hdel(DIRECTORY_KEY, token);
redisPool.returnResource(jedis); }
} }
public void remove(BatchOperationHandle handle, byte[] token) { public void remove(BatchOperationHandle handle, byte[] token) {
@@ -72,7 +72,7 @@ public class DirectoryManager {
} }
public void add(ClientContact contact) { public void add(ClientContact contact) {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms()); TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice());
try (Jedis jedis = redisPool.getResource()) { try (Jedis jedis = redisPool.getResource()) {
jedis.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue)); jedis.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
@@ -84,7 +84,7 @@ public class DirectoryManager {
public void add(BatchOperationHandle handle, ClientContact contact) { public void add(BatchOperationHandle handle, ClientContact contact) {
try { try {
Pipeline pipeline = handle.pipeline; Pipeline pipeline = handle.pipeline;
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms()); TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice());
pipeline.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue)); pipeline.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@@ -106,7 +106,7 @@ public class DirectoryManager {
} }
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class); TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms)); return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice));
} catch (IOException e) { } catch (IOException e) {
logger.warn("JSON Error", e); logger.warn("JSON Error", e);
return Optional.absent(); return Optional.absent();
@@ -133,7 +133,7 @@ public class DirectoryManager {
try { try {
if (pair.second().get() != null) { if (pair.second().get() != null) {
TokenValue tokenValue = objectMapper.readValue(pair.second().get(), TokenValue.class); TokenValue tokenValue = objectMapper.readValue(pair.second().get(), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms); ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.voice);
results.add(clientContact); results.add(clientContact);
} }
@@ -175,14 +175,14 @@ public class DirectoryManager {
@JsonProperty(value = "r") @JsonProperty(value = "r")
private String relay; private String relay;
@JsonProperty(value = "s") @JsonProperty(value = "v")
private boolean supportsSms; private boolean voice;
public TokenValue() {} public TokenValue() {}
public TokenValue(String relay, boolean supportsSms) { public TokenValue(String relay, boolean voice) {
this.relay = relay; this.relay = relay;
this.supportsSms = supportsSms; this.voice = voice;
} }
} }
@@ -205,7 +205,7 @@ public class DirectoryManager {
} }
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class); TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms)); return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice));
} }
} }

View File

@@ -10,7 +10,7 @@ import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate; import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper; import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper; import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
@@ -24,6 +24,8 @@ import java.util.List;
public abstract class Messages { public abstract class Messages {
public static final int RESULT_SET_CHUNK_SIZE = 100;
private static final String ID = "id"; private static final String ID = "id";
private static final String TYPE = "type"; private static final String TYPE = "type";
private static final String RELAY = "relay"; private static final String RELAY = "relay";
@@ -33,22 +35,26 @@ public abstract class Messages {
private static final String DESTINATION = "destination"; private static final String DESTINATION = "destination";
private static final String DESTINATION_DEVICE = "destination_device"; private static final String DESTINATION_DEVICE = "destination_device";
private static final String MESSAGE = "message"; private static final String MESSAGE = "message";
private static final String CONTENT = "content";
@SqlQuery("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ") " + @SqlQuery("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ", " + CONTENT + ") " +
"VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message) " + "VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message, :content) " +
"RETURNING (SELECT COUNT(id) FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + TYPE + " != " + OutgoingMessageSignal.Type.RECEIPT_VALUE + ")") "RETURNING (SELECT COUNT(id) FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + TYPE + " != " + Envelope.Type.RECEIPT_VALUE + ")")
abstract int store(@MessageBinder OutgoingMessageSignal message, abstract int store(@MessageBinder Envelope message,
@Bind("destination") String destination, @Bind("destination") String destination,
@Bind("destination_device") long destinationDevice); @Bind("destination_device") long destinationDevice);
@Mapper(MessageMapper.class) @Mapper(MessageMapper.class)
@SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC") @SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC LIMIT " + RESULT_SET_CHUNK_SIZE)
abstract List<OutgoingMessageEntity> load(@Bind("destination") String destination, abstract List<OutgoingMessageEntity> load(@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice); @Bind("destination_device") long destinationDevice);
@Mapper(MessageMapper.class) @Mapper(MessageMapper.class)
@SqlQuery("DELETE FROM messages WHERE " + ID + " IN (SELECT " + ID + " FROM messages WHERE " + DESTINATION + " = :destination AND " + SOURCE + " = :source AND " + TIMESTAMP + " = :timestamp ORDER BY " + ID + " LIMIT 1) RETURNING *") @SqlQuery("DELETE FROM messages WHERE " + ID + " IN (SELECT " + ID + " FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + SOURCE + " = :source AND " + TIMESTAMP + " = :timestamp ORDER BY " + ID + " LIMIT 1) RETURNING *")
abstract OutgoingMessageEntity remove(@Bind("destination") String destination, @Bind("source") String source, @Bind("timestamp") long timestamp); abstract OutgoingMessageEntity remove(@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice,
@Bind("source") String source,
@Bind("timestamp") long timestamp);
@Mapper(MessageMapper.class) @Mapper(MessageMapper.class)
@SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id AND " + DESTINATION + " = :destination") @SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id AND " + DESTINATION + " = :destination")
@@ -57,6 +63,12 @@ public abstract class Messages {
@SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination") @SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination")
abstract void clear(@Bind("destination") String destination); abstract void clear(@Bind("destination") String destination);
@SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device")
abstract void clear(@Bind("destination") String destination, @Bind("destination_device") long destinationDevice);
@SqlUpdate("DELETE FROM messages WHERE " + TIMESTAMP + " < :timestamp")
public abstract void removeOld(@Bind("timestamp") long timestamp);
@SqlUpdate("VACUUM messages") @SqlUpdate("VACUUM messages")
public abstract void vacuum(); public abstract void vacuum();
@@ -65,13 +77,23 @@ public abstract class Messages {
public OutgoingMessageEntity map(int i, ResultSet resultSet, StatementContext statementContext) public OutgoingMessageEntity map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException throws SQLException
{ {
int type = resultSet.getInt(TYPE);
byte[] legacyMessage = resultSet.getBytes(MESSAGE);
if (type == Envelope.Type.RECEIPT_VALUE && legacyMessage == null) {
/// XXX - REMOVE AFTER 10/01/15
legacyMessage = new byte[0];
}
return new OutgoingMessageEntity(resultSet.getLong(ID), return new OutgoingMessageEntity(resultSet.getLong(ID),
resultSet.getInt(TYPE), type,
resultSet.getString(RELAY), resultSet.getString(RELAY),
resultSet.getLong(TIMESTAMP), resultSet.getLong(TIMESTAMP),
resultSet.getString(SOURCE), resultSet.getString(SOURCE),
resultSet.getInt(SOURCE_DEVICE), resultSet.getInt(SOURCE_DEVICE),
resultSet.getBytes(MESSAGE)); legacyMessage,
resultSet.getBytes(CONTENT));
} }
} }
@@ -82,18 +104,19 @@ public abstract class Messages {
public static class AccountBinderFactory implements BinderFactory { public static class AccountBinderFactory implements BinderFactory {
@Override @Override
public Binder build(Annotation annotation) { public Binder build(Annotation annotation) {
return new Binder<MessageBinder, OutgoingMessageSignal>() { return new Binder<MessageBinder, Envelope>() {
@Override @Override
public void bind(SQLStatement<?> sql, public void bind(SQLStatement<?> sql,
MessageBinder accountBinder, MessageBinder accountBinder,
OutgoingMessageSignal message) Envelope message)
{ {
sql.bind(TYPE, message.getType()); sql.bind(TYPE, message.getType().getNumber());
sql.bind(RELAY, message.getRelay()); sql.bind(RELAY, message.getRelay());
sql.bind(TIMESTAMP, message.getTimestamp()); sql.bind(TIMESTAMP, message.getTimestamp());
sql.bind(SOURCE, message.getSource()); sql.bind(SOURCE, message.getSource());
sql.bind(SOURCE_DEVICE, message.getSourceDevice()); sql.bind(SOURCE_DEVICE, message.getSourceDevice());
sql.bind(MESSAGE, message.getMessage().toByteArray()); sql.bind(MESSAGE, message.hasLegacyMessage() ? message.getLegacyMessage().toByteArray() : null);
sql.bind(CONTENT, message.hasContent() ? message.getContent().toByteArray() : null);
} }
}; };
} }

View File

@@ -2,9 +2,9 @@ package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import java.util.List; import java.util.List;
@@ -16,20 +16,26 @@ public class MessagesManager {
this.messages = messages; this.messages = messages;
} }
public int insert(String destination, long destinationDevice, OutgoingMessageSignal message) { public int insert(String destination, long destinationDevice, Envelope message) {
return this.messages.store(message, destination, destinationDevice) + 1; return this.messages.store(message, destination, destinationDevice) + 1;
} }
public List<OutgoingMessageEntity> getMessagesForDevice(String destination, long destinationDevice) { public OutgoingMessageEntityList getMessagesForDevice(String destination, long destinationDevice) {
return this.messages.load(destination, destinationDevice); List<OutgoingMessageEntity> messages = this.messages.load(destination, destinationDevice);
return new OutgoingMessageEntityList(messages, messages.size() >= Messages.RESULT_SET_CHUNK_SIZE);
} }
public void clear(String destination) { public void clear(String destination) {
this.messages.clear(destination); this.messages.clear(destination);
} }
public Optional<OutgoingMessageEntity> delete(String destination, String source, long timestamp) { public void clear(String destination, long deviceId) {
return Optional.fromNullable(this.messages.remove(destination, source, timestamp)); this.messages.clear(destination, deviceId);
}
public Optional<OutgoingMessageEntity> delete(String destination, long destinationDevice, String source, long timestamp)
{
return Optional.fromNullable(this.messages.remove(destination, destinationDevice, source, timestamp));
} }
public void delete(String destination, long id) { public void delete(String destination, long id) {

View File

@@ -0,0 +1,5 @@
package org.whispersystems.textsecuregcm.storage;
public interface PubSubAddress {
public String serialize();
}

View File

@@ -1,13 +1,11 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.google.protobuf.ByteString;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.DispatchChannel; import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.dispatch.DispatchManager; import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.util.concurrent.atomic.AtomicInteger; import java.util.Arrays;
import io.dropwizard.lifecycle.Managed; import io.dropwizard.lifecycle.Managed;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage; import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
@@ -21,7 +19,7 @@ public class PubSubManager implements Managed {
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class); private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final DispatchManager dispatchManager; private final DispatchManager dispatchManager;
private final JedisPool jedisPool; private final JedisPool jedisPool;
private boolean subscribed = false; private boolean subscribed = false;
@@ -49,27 +47,31 @@ public class PubSubManager implements Managed {
dispatchManager.shutdown(); dispatchManager.shutdown();
} }
public void subscribe(WebsocketAddress address, DispatchChannel channel) { public void subscribe(PubSubAddress address, DispatchChannel channel) {
String serializedAddress = address.serialize(); dispatchManager.subscribe(address.serialize(), channel);
dispatchManager.subscribe(serializedAddress, channel);
} }
public void unsubscribe(WebsocketAddress address, DispatchChannel dispatchChannel) { public void unsubscribe(PubSubAddress address, DispatchChannel dispatchChannel) {
String serializedAddress = address.serialize(); dispatchManager.unsubscribe(address.serialize(), dispatchChannel);
dispatchManager.unsubscribe(serializedAddress, dispatchChannel);
} }
public boolean hasLocalSubscription(WebsocketAddress address) { public boolean hasLocalSubscription(PubSubAddress address) {
return dispatchManager.hasSubscription(address.serialize()); return dispatchManager.hasSubscription(address.serialize());
} }
public boolean publish(WebsocketAddress address, PubSubMessage message) { public boolean publish(PubSubAddress address, PubSubMessage message) {
return publish(address.serialize().getBytes(), message); return publish(address.serialize().getBytes(), message);
} }
private boolean publish(byte[] channel, PubSubMessage message) { private boolean publish(byte[] channel, PubSubMessage message) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
return jedis.publish(channel, message.toByteArray()) != 0; long result = jedis.publish(channel, message.toByteArray());
if (result < 0) {
logger.warn("**** Jedis publish result < 0");
}
return result > 0;
} }
} }

View File

@@ -162,6 +162,10 @@ public final class PubSubProtos {
* <code>CLOSE = 4;</code> * <code>CLOSE = 4;</code>
*/ */
CLOSE(4, 4), CLOSE(4, 4),
/**
* <code>CONNECTED = 5;</code>
*/
CONNECTED(5, 5),
; ;
/** /**
@@ -184,6 +188,10 @@ public final class PubSubProtos {
* <code>CLOSE = 4;</code> * <code>CLOSE = 4;</code>
*/ */
public static final int CLOSE_VALUE = 4; public static final int CLOSE_VALUE = 4;
/**
* <code>CONNECTED = 5;</code>
*/
public static final int CONNECTED_VALUE = 5;
public final int getNumber() { return value; } public final int getNumber() { return value; }
@@ -195,6 +203,7 @@ public final class PubSubProtos {
case 2: return DELIVER; case 2: return DELIVER;
case 3: return KEEPALIVE; case 3: return KEEPALIVE;
case 4: return CLOSE; case 4: return CLOSE;
case 5: return CONNECTED;
default: return null; default: return null;
} }
} }
@@ -620,13 +629,13 @@ public final class PubSubProtos {
descriptor; descriptor;
static { static {
java.lang.String[] descriptorData = { java.lang.String[] descriptorData = {
"\n\023PubSubMessage.proto\022\ntextsecure\"\230\001\n\rPu" + "\n\023PubSubMessage.proto\022\ntextsecure\"\247\001\n\rPu" +
"bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" + "bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" +
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"H\n\004Ty" + "ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"W\n\004Ty" +
"pe\022\013\n\007UNKNOWN\020\000\022\014\n\010QUERY_DB\020\001\022\013\n\007DELIVER" + "pe\022\013\n\007UNKNOWN\020\000\022\014\n\010QUERY_DB\020\001\022\013\n\007DELIVER" +
"\020\002\022\r\n\tKEEPALIVE\020\003\022\t\n\005CLOSE\020\004B8\n(org.whis" + "\020\002\022\r\n\tKEEPALIVE\020\003\022\t\n\005CLOSE\020\004\022\r\n\tCONNECTE" +
"persystems.textsecuregcm.storageB\014PubSub" + "D\020\005B8\n(org.whispersystems.textsecuregcm." +
"Protos" "storageB\014PubSubProtos"
}; };
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {

View File

@@ -0,0 +1,37 @@
package org.whispersystems.textsecuregcm.util;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {
private final Semaphore semaphore;
public BlockingThreadPoolExecutor(int threads, int bound) {
super(threads, threads, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
this.semaphore = new Semaphore(bound);
}
@Override
public void execute(Runnable task) {
semaphore.acquireUninterruptibly();
try {
super.execute(task);
} catch (Throwable t) {
semaphore.release();
throw new RuntimeException(t);
}
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
semaphore.release();
}
public int getSize() {
return ((LinkedBlockingQueue)getQueue()).size();
}
}

View File

@@ -2,6 +2,7 @@ package org.whispersystems.textsecuregcm.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
public class SystemMapper { public class SystemMapper {
@@ -11,6 +12,7 @@ public class SystemMapper {
static { static {
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
} }
public static ObjectMapper getMapper() { public static ObjectMapper getMapper() {

View File

@@ -20,6 +20,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -115,12 +116,24 @@ public class Util {
return parts; return parts;
} }
public static void sleep(int i) { public static void sleep(long i) {
try { try {
Thread.sleep(i); Thread.sleep(i);
} catch (InterruptedException ie) {} } catch (InterruptedException ie) {}
} }
public static void wait(Object object) {
try {
object.wait();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
public static int hashCode(Object... objects) {
return Arrays.hashCode(objects);
}
public static long todayInMillis() { public static long todayInMillis() {
return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis())); return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
} }

View File

@@ -5,6 +5,7 @@ import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
@@ -12,6 +13,8 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.websocket.session.WebSocketSessionContext; import org.whispersystems.websocket.session.WebSocketSessionContext;
@@ -25,34 +28,37 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private static final Histogram durationHistogram = metricRegistry.histogram(name(WebSocketConnection.class, "connected_duration")); private static final Histogram durationHistogram = metricRegistry.histogram(name(WebSocketConnection.class, "connected_duration"));
private final ApnFallbackManager apnFallbackManager;
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final PushSender pushSender; private final PushSender pushSender;
private final ReceiptSender receiptSender; private final ReceiptSender receiptSender;
private final MessagesManager messagesManager; private final MessagesManager messagesManager;
private final PubSubManager pubSubManager; private final PubSubManager pubSubManager;
public AuthenticatedConnectListener(AccountsManager accountsManager, PushSender pushSender, public AuthenticatedConnectListener(AccountsManager accountsManager, PushSender pushSender,
ReceiptSender receiptSender, MessagesManager messagesManager, ReceiptSender receiptSender, MessagesManager messagesManager,
PubSubManager pubSubManager) PubSubManager pubSubManager, ApnFallbackManager apnFallbackManager)
{ {
this.accountsManager = accountsManager; this.accountsManager = accountsManager;
this.pushSender = pushSender; this.pushSender = pushSender;
this.receiptSender = receiptSender; this.receiptSender = receiptSender;
this.messagesManager = messagesManager; this.messagesManager = messagesManager;
this.pubSubManager = pubSubManager; this.pubSubManager = pubSubManager;
this.apnFallbackManager = apnFallbackManager;
} }
@Override @Override
public void onWebSocketConnect(WebSocketSessionContext context) { public void onWebSocketConnect(WebSocketSessionContext context) {
final Account account = context.getAuthenticated(Account.class).get(); final Account account = context.getAuthenticated(Account.class);
final Device device = account.getAuthenticatedDevice().get(); final Device device = account.getAuthenticatedDevice().get();
final long connectTime = System.currentTimeMillis(); final long connectTime = System.currentTimeMillis();
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId()); final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender, final WebSocketConnectionInfo info = new WebSocketConnectionInfo(address);
messagesManager, account, device, final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
context.getClient()); messagesManager, account, device,
context.getClient());
pubSubManager.publish(info, PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED).build());
updateLastSeen(account, device); updateLastSeen(account, device);
pubSubManager.subscribe(address, connection); pubSubManager.subscribe(address, connection);

View File

@@ -4,9 +4,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.DispatchChannel; import org.whispersystems.dispatch.DispatchChannel;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage; import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public class DeadLetterHandler implements DispatchChannel { public class DeadLetterHandler implements DispatchChannel {
@@ -21,22 +20,24 @@ public class DeadLetterHandler implements DispatchChannel {
@Override @Override
public void onDispatchMessage(String channel, byte[] data) { public void onDispatchMessage(String channel, byte[] data) {
try { if (!WebSocketConnectionInfo.isType(channel)) {
logger.warn("Handling dead letter to: " + channel); try {
logger.info("Handling dead letter to: " + channel);
WebsocketAddress address = new WebsocketAddress(channel); WebsocketAddress address = new WebsocketAddress(channel);
PubSubMessage pubSubMessage = PubSubMessage.parseFrom(data); PubSubMessage pubSubMessage = PubSubMessage.parseFrom(data);
switch (pubSubMessage.getType().getNumber()) { switch (pubSubMessage.getType().getNumber()) {
case PubSubMessage.Type.DELIVER_VALUE: case PubSubMessage.Type.DELIVER_VALUE:
OutgoingMessageSignal message = OutgoingMessageSignal.parseFrom(pubSubMessage.getContent()); Envelope message = Envelope.parseFrom(pubSubMessage.getContent());
messagesManager.insert(address.getNumber(), address.getDeviceId(), message); messagesManager.insert(address.getNumber(), address.getDeviceId(), message);
break; break;
}
} catch (InvalidProtocolBufferException e) {
logger.warn("Bad pubsub message", e);
} catch (InvalidWebsocketAddressException e) {
logger.warn("Invalid websocket address", e);
} }
} catch (InvalidProtocolBufferException e) {
logger.warn("Bad pubsub message", e);
} catch (InvalidWebsocketAddressException e) {
logger.warn("Invalid websocket address", e);
} }
} }

View File

@@ -8,6 +8,7 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.websocket.auth.AuthenticationException; import org.whispersystems.websocket.auth.AuthenticationException;
import org.whispersystems.websocket.auth.WebSocketAuthenticator; import org.whispersystems.websocket.auth.WebSocketAuthenticator;
import java.util.List;
import java.util.Map; import java.util.Map;
import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.auth.basic.BasicCredentials;
@@ -24,18 +25,18 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Acc
@Override @Override
public Optional<Account> authenticate(UpgradeRequest request) throws AuthenticationException { public Optional<Account> authenticate(UpgradeRequest request) throws AuthenticationException {
try { try {
Map<String, String[]> parameters = request.getParameterMap(); Map<String, List<String>> parameters = request.getParameterMap();
String[] usernames = parameters.get("login"); List<String> usernames = parameters.get("login");
String[] passwords = parameters.get("password"); List<String> passwords = parameters.get("password");
if (usernames == null || usernames.length == 0 || if (usernames == null || usernames.size() == 0 ||
passwords == null || passwords.length == 0) passwords == null || passwords.size() == 0)
{ {
return Optional.absent(); return Optional.absent();
} }
BasicCredentials credentials = new BasicCredentials(usernames[0].replace(" ", "+"), BasicCredentials credentials = new BasicCredentials(usernames.get(0).replace(" ", "+"),
passwords[0].replace(" ", "+")); passwords.get(0).replace(" ", "+"));
return accountAuthenticator.authenticate(credentials); return accountAuthenticator.authenticate(credentials);
} catch (io.dropwizard.auth.AuthenticationException e) { } catch (io.dropwizard.auth.AuthenticationException e) {

View File

@@ -13,6 +13,7 @@ import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException; import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.ReceiptSender;
@@ -25,10 +26,11 @@ import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.ws.rs.WebApplicationException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.Iterator;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage; import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public class WebSocketConnection implements DispatchChannel { public class WebSocketConnection implements DispatchChannel {
@@ -68,7 +70,7 @@ public class WebSocketConnection implements DispatchChannel {
processStoredMessages(); processStoredMessages();
break; break;
case PubSubMessage.Type.DELIVER_VALUE: case PubSubMessage.Type.DELIVER_VALUE:
sendMessage(OutgoingMessageSignal.parseFrom(pubSubMessage.getContent()), Optional.<Long>absent()); sendMessage(Envelope.parseFrom(pubSubMessage.getContent()), Optional.<Long>absent(), false);
break; break;
default: default:
logger.warn("Unknown pubsub message: " + pubSubMessage.getType().getNumber()); logger.warn("Unknown pubsub message: " + pubSubMessage.getType().getNumber());
@@ -87,8 +89,9 @@ public class WebSocketConnection implements DispatchChannel {
processStoredMessages(); processStoredMessages();
} }
private void sendMessage(final OutgoingMessageSignal message, private void sendMessage(final Envelope message,
final Optional<Long> storedMessageId) final Optional<Long> storedMessageId,
final boolean requery)
{ {
try { try {
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey()); EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
@@ -98,11 +101,12 @@ public class WebSocketConnection implements DispatchChannel {
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() { Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
@Override @Override
public void onSuccess(@Nullable WebSocketResponseMessage response) { public void onSuccess(@Nullable WebSocketResponseMessage response) {
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE; boolean isReceipt = message.getType() == Envelope.Type.RECEIPT;
if (isSuccessResponse(response)) { if (isSuccessResponse(response)) {
if (storedMessageId.isPresent()) messagesManager.delete(account.getNumber(), storedMessageId.get()); if (storedMessageId.isPresent()) messagesManager.delete(account.getNumber(), storedMessageId.get());
if (!isReceipt) sendDeliveryReceiptFor(message); if (!isReceipt) sendDeliveryReceiptFor(message);
if (requery) processStoredMessages();
} else if (!isSuccessResponse(response) && !storedMessageId.isPresent()) { } else if (!isSuccessResponse(response) && !storedMessageId.isPresent()) {
requeueMessage(message); requeueMessage(message);
} }
@@ -122,43 +126,55 @@ public class WebSocketConnection implements DispatchChannel {
} }
} }
private void requeueMessage(OutgoingMessageSignal message) { private void requeueMessage(Envelope message) {
int queueDepth = pushSender.getWebSocketSender().queueMessage(account, device, message);
try { try {
pushSender.sendMessage(account, device, message); pushSender.sendQueuedNotification(account, device, queueDepth);
} catch (NotPushRegisteredException | TransientPushFailureException e) { } catch (NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("requeueMessage", e); logger.warn("requeueMessage", e);
messagesManager.insert(account.getNumber(), device.getId(), message);
} }
} }
private void sendDeliveryReceiptFor(OutgoingMessageSignal message) { private void sendDeliveryReceiptFor(Envelope message) {
try { try {
receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp(), receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp(),
message.hasRelay() ? Optional.of(message.getRelay()) : message.hasRelay() ? Optional.of(message.getRelay()) :
Optional.<String>absent()); Optional.<String>absent());
} catch (IOException | NoSuchUserException | TransientPushFailureException | NotPushRegisteredException e) { } catch (NoSuchUserException | NotPushRegisteredException e) {
logger.warn("sendDeliveryReceiptFor", e); logger.info("No longer registered " + e.getMessage());
} catch(IOException | TransientPushFailureException e) {
logger.warn("Something wrong while sending receipt", e);
} catch (WebApplicationException e) {
logger.warn("Bad federated response for receipt: " + e.getResponse().getStatus());
} }
} }
private void processStoredMessages() { private void processStoredMessages() {
List<OutgoingMessageEntity> messages = messagesManager.getMessagesForDevice(account.getNumber(), device.getId()); OutgoingMessageEntityList messages = messagesManager.getMessagesForDevice(account.getNumber(), device.getId());
Iterator<OutgoingMessageEntity> iterator = messages.getMessages().iterator();
for (OutgoingMessageEntity message : messages) { while (iterator.hasNext()) {
OutgoingMessageSignal.Builder builder = OutgoingMessageSignal.newBuilder() OutgoingMessageEntity message = iterator.next();
.setType(message.getType()) Envelope.Builder builder = Envelope.newBuilder()
.setMessage(ByteString.copyFrom(message.getMessage())) .setType(Envelope.Type.valueOf(message.getType()))
.setSourceDevice(message.getSourceDevice()) .setSourceDevice(message.getSourceDevice())
.setSource(message.getSource()) .setSource(message.getSource())
.setTimestamp(message.getTimestamp()); .setTimestamp(message.getTimestamp());
if (message.getMessage() != null) {
builder.setLegacyMessage(ByteString.copyFrom(message.getMessage()));
}
if (message.getContent() != null) {
builder.setContent(ByteString.copyFrom(message.getContent()));
}
if (message.getRelay() != null && !message.getRelay().isEmpty()) { if (message.getRelay() != null && !message.getRelay().isEmpty()) {
builder.setRelay(message.getRelay()); builder.setRelay(message.getRelay());
} }
sendMessage(builder.build(), Optional.of(message.getId())); sendMessage(builder.build(), Optional.of(message.getId()), !iterator.hasNext() && messages.hasMore());
} }
} }
} }

View File

@@ -0,0 +1,62 @@
package org.whispersystems.textsecuregcm.websocket;
import org.whispersystems.textsecuregcm.storage.PubSubAddress;
import org.whispersystems.textsecuregcm.util.Util;
public class WebSocketConnectionInfo implements PubSubAddress {
private final WebsocketAddress address;
public WebSocketConnectionInfo(WebsocketAddress address) {
this.address = address;
}
public WebSocketConnectionInfo(String serialized) throws FormattingException {
String[] parts = serialized.split("[:]", 3);
if (parts.length != 3 || !"c".equals(parts[2])) {
throw new FormattingException("Bad address: " + serialized);
}
try {
this.address = new WebsocketAddress(parts[0], Long.parseLong(parts[1]));
} catch (NumberFormatException e) {
throw new FormattingException(e);
}
}
public String serialize() {
return address.serialize() + ":c";
}
public WebsocketAddress getWebsocketAddress() {
return address;
}
public static boolean isType(String address) {
return address.endsWith(":c");
}
@Override
public boolean equals(Object other) {
return
other != null &&
other instanceof WebSocketConnectionInfo
&& ((WebSocketConnectionInfo)other).address.equals(address);
}
@Override
public int hashCode() {
return Util.hashCode(address, "c");
}
public static class FormattingException extends Exception {
public FormattingException(String message) {
super(message);
}
public FormattingException(Exception e) {
super(e);
}
}
}

View File

@@ -1,6 +1,8 @@
package org.whispersystems.textsecuregcm.websocket; package org.whispersystems.textsecuregcm.websocket;
public class WebsocketAddress { import org.whispersystems.textsecuregcm.storage.PubSubAddress;
public class WebsocketAddress implements PubSubAddress {
private final String number; private final String number;
private final long deviceId; private final long deviceId;

View File

@@ -16,6 +16,7 @@
*/ */
package org.whispersystems.textsecuregcm.workers; package org.whispersystems.textsecuregcm.workers;
import com.fasterxml.jackson.databind.DeserializationFeature;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.DBI;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -27,31 +28,42 @@ import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import io.dropwizard.Application;
import io.dropwizard.cli.ConfiguredCommand; import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.cli.EnvironmentCommand;
import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.ImmutableListContainerFactory; import io.dropwizard.jdbi.ImmutableListContainerFactory;
import io.dropwizard.jdbi.ImmutableSetContainerFactory; import io.dropwizard.jdbi.ImmutableSetContainerFactory;
import io.dropwizard.jdbi.OptionalContainerFactory; import io.dropwizard.jdbi.OptionalContainerFactory;
import io.dropwizard.jdbi.args.OptionalArgumentFactory; import io.dropwizard.jdbi.args.OptionalArgumentFactory;
import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfiguration> { public class DirectoryCommand extends EnvironmentCommand<WhisperServerConfiguration> {
private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class); private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class);
public DirectoryCommand() { public DirectoryCommand() {
super("directory", "Update directory from DB and peers."); super(new Application<WhisperServerConfiguration>() {
@Override
public void run(WhisperServerConfiguration configuration, Environment environment)
throws Exception
{
}
}, "directory", "Update directory from DB and peers.");
} }
@Override @Override
protected void run(Bootstrap<WhisperServerConfiguration> bootstrap, protected void run(Environment environment, Namespace namespace,
Namespace namespace, WhisperServerConfiguration configuration)
WhisperServerConfiguration config)
throws Exception throws Exception
{ {
try { try {
DataSourceFactory dbConfig = config.getDataSourceFactory(); environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
DataSourceFactory dbConfig = configuration.getDataSourceFactory();
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword()); DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass())); dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
@@ -60,11 +72,13 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
dbi.registerContainerFactory(new OptionalContainerFactory()); dbi.registerContainerFactory(new OptionalContainerFactory());
Accounts accounts = dbi.onDemand(Accounts.class); Accounts accounts = dbi.onDemand(Accounts.class);
JedisPool cacheClient = new RedisClientFactory(config.getCacheConfiguration().getUrl()).getRedisClientPool(); JedisPool cacheClient = new RedisClientFactory(configuration.getCacheConfiguration().getUrl()).getRedisClientPool();
JedisPool redisClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool(); JedisPool redisClient = new RedisClientFactory(configuration.getDirectoryConfiguration().getUrl()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient); DirectoryManager directory = new DirectoryManager(redisClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration()); FederatedClientManager federatedClientManager = new FederatedClientManager(environment,
configuration.getJerseyClientConfiguration(),
configuration.getFederationConfiguration());
DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory); DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory);

View File

@@ -26,7 +26,6 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle; import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException; import java.io.IOException;
@@ -73,7 +72,7 @@ public class DirectoryUpdater {
for (Account account : accounts) { for (Account account : accounts) {
if (account.isActive()) { if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber()); byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
directory.add(batchOperation, clientContact); directory.add(batchOperation, clientContact);
contactsAdded++; contactsAdded++;

View File

@@ -0,0 +1,50 @@
package org.whispersystems.textsecuregcm.workers;
import net.sourceforge.argparse4j.inf.Namespace;
import org.skife.jdbi.v2.DBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.storage.Messages;
import java.util.concurrent.TimeUnit;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.ImmutableListContainerFactory;
import io.dropwizard.jdbi.ImmutableSetContainerFactory;
import io.dropwizard.jdbi.OptionalContainerFactory;
import io.dropwizard.jdbi.args.OptionalArgumentFactory;
import io.dropwizard.setup.Bootstrap;
public class TrimMessagesCommand extends ConfiguredCommand<WhisperServerConfiguration> {
private final Logger logger = LoggerFactory.getLogger(VacuumCommand.class);
public TrimMessagesCommand() {
super("trim", "Trim Messages Database");
}
@Override
protected void run(Bootstrap<WhisperServerConfiguration> bootstrap,
Namespace namespace,
WhisperServerConfiguration config)
throws Exception
{
DataSourceFactory messageDbConfig = config.getMessageStoreConfiguration();
DBI messageDbi = new DBI(messageDbConfig.getUrl(), messageDbConfig.getUser(), messageDbConfig.getPassword());
messageDbi.registerArgumentFactory(new OptionalArgumentFactory(messageDbConfig.getDriverClass()));
messageDbi.registerContainerFactory(new ImmutableListContainerFactory());
messageDbi.registerContainerFactory(new ImmutableSetContainerFactory());
messageDbi.registerContainerFactory(new OptionalContainerFactory());
Messages messages = messageDbi.onDemand(Messages.class);
long timestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60);
logger.info("Trimming old messages: " + timestamp + "...");
messages.removeOld(timestamp);
Thread.sleep(3000);
System.exit(0);
}
}

View File

@@ -21,7 +21,7 @@ import io.dropwizard.setup.Bootstrap;
public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration> { public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration> {
private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class); private final Logger logger = LoggerFactory.getLogger(VacuumCommand.class);
public VacuumCommand() { public VacuumCommand() {
super("vacuum", "Vacuum Postgres Tables"); super("vacuum", "Vacuum Postgres Tables");
@@ -51,18 +51,18 @@ public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration>
Accounts accounts = dbi.onDemand(Accounts.class ); Accounts accounts = dbi.onDemand(Accounts.class );
Keys keys = dbi.onDemand(Keys.class ); Keys keys = dbi.onDemand(Keys.class );
PendingAccounts pendingAccounts = dbi.onDemand(PendingAccounts.class); PendingAccounts pendingAccounts = dbi.onDemand(PendingAccounts.class);
Messages messages = dbi.onDemand(Messages.class ); Messages messages = messageDbi.onDemand(Messages.class );
logger.warn("Vacuuming accounts..."); logger.info("Vacuuming accounts...");
accounts.vacuum(); accounts.vacuum();
logger.warn("Vacuuming pending_accounts..."); logger.info("Vacuuming pending_accounts...");
pendingAccounts.vacuum(); pendingAccounts.vacuum();
logger.warn("Vacuuming keys..."); logger.info("Vacuuming keys...");
keys.vacuum(); keys.vacuum();
logger.warn("Vacuuming messages..."); logger.info("Vacuuming messages...");
messages.vacuum(); messages.vacuum();
Thread.sleep(3000); Thread.sleep(3000);

View File

@@ -57,4 +57,22 @@
</createIndex> </createIndex>
</changeSet> </changeSet>
<changeSet id="2" author="moxie">
<addColumn tableName="messages">
<column name="content" type="bytea"/>
</addColumn>
<dropNotNullConstraint tableName="messages" columnName="message"/>
</changeSet>
<changeSet id="3" author="moxie">
<sql>CREATE RULE bounded_message_queue AS ON INSERT TO messages DO ALSO DELETE FROM messages WHERE id IN (SELECT id FROM messages WHERE destination = NEW.destination AND destination_device = NEW.destination_device ORDER BY timestamp DESC OFFSET 5000);</sql>
</changeSet>
<changeSet id="4" author="moxie">
<sql>DROP RULE bounded_message_queue ON messages;</sql>
<sql>CREATE RULE bounded_message_queue AS ON INSERT TO messages DO ALSO DELETE FROM messages WHERE id IN (SELECT id FROM messages WHERE destination = NEW.destination AND destination_device = NEW.destination_device ORDER BY timestamp DESC OFFSET 1000);</sql>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -1,12 +1,13 @@
package org.whispersystems.textsecuregcm.tests.controllers; package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse;
import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
@@ -17,13 +18,16 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
//import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import io.dropwizard.testing.junit.ResourceTestRule; import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -42,14 +46,18 @@ public class AccountControllerTest {
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new AccountController(pendingAccountsManager, .addResource(new AccountController(pendingAccountsManager,
accountsManager, accountsManager,
rateLimiters, rateLimiters,
smsSender, smsSender,
storedMessages, storedMessages,
timeProvider, timeProvider,
Optional.of(authorizationKey))) Optional.of(authorizationKey),
new HashMap<String, Integer>()))
.build(); .build();
@@ -66,23 +74,40 @@ public class AccountControllerTest {
@Test @Test
public void testSendCode() throws Exception { public void testSendCode() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/accounts/sms/code/%s", SENDER)) resources.getJerseyTest()
.get(ClientResponse.class); .target(String.format("/v1/accounts/sms/code/%s", SENDER))
.request()
.get();
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
verify(smsSender).deliverSmsVerification(eq(SENDER), anyString()); verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.<String>absent()), anyString());
}
@Test
public void testSendiOSCode() throws Exception {
Response response =
resources.getJerseyTest()
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
.queryParam("client", "ios")
.request()
.get();
assertThat(response.getStatus()).isEqualTo(200);
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("ios")), anyString());
} }
@Test @Test
public void testVerifyCode() throws Exception { public void testVerifyCode() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/accounts/code/%s", "1234")) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) .target(String.format("/v1/accounts/code/%s", "1234"))
.entity(new AccountAttributes("keykeykeykey", false, false, 2222)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.put(ClientResponse.class); .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
@@ -91,12 +116,13 @@ public class AccountControllerTest {
@Test @Test
public void testVerifyBadCode() throws Exception { public void testVerifyBadCode() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/accounts/code/%s", "1111")) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) .target(String.format("/v1/accounts/code/%s", "1111"))
.entity(new AccountAttributes("keykeykeykey", false, false, 3333)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.put(ClientResponse.class); .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
@@ -109,12 +135,13 @@ public class AccountControllerTest {
String token = SENDER + ":1415906573:af4f046107c21721224a"; String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/accounts/token/%s", token)) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) .target(String.format("/v1/accounts/token/%s", token))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.put(ClientResponse.class); .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
@@ -127,12 +154,13 @@ public class AccountControllerTest {
String token = SENDER + ":1415906574:af4f046107c21721224a"; String token = SENDER + ":1415906574:af4f046107c21721224a";
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/accounts/token/%s", token)) resources.getJerseyTest()
.target(String.format("/v1/accounts/token/%s", token))
.request()
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444)) .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
.type(MediaType.APPLICATION_JSON_TYPE) MediaType.APPLICATION_JSON_TYPE));
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
@@ -145,12 +173,13 @@ public class AccountControllerTest {
String token = SENDER + ":1415906573:af4f046107c21721224a"; String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/accounts/token/%s", token)) resources.getJerseyTest()
.target(String.format("/v1/accounts/token/%s", token))
.request()
.header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar")) .header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444)) .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
.type(MediaType.APPLICATION_JSON_TYPE) MediaType.APPLICATION_JSON_TYPE));
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);
@@ -163,12 +192,13 @@ public class AccountControllerTest {
String token = SENDER + ":1415906573:af4f046107c21721224a"; String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/accounts/token/%s", token)) resources.getJerseyTest()
.target(String.format("/v1/accounts/token/%s", token))
.request()
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444)) .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
.type(MediaType.APPLICATION_JSON_TYPE) MediaType.APPLICATION_JSON_TYPE));
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403); assertThat(response.getStatus()).isEqualTo(403);

View File

@@ -17,32 +17,44 @@
package org.whispersystems.textsecuregcm.tests.controllers; package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceResponse; import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.VerificationCode; import org.whispersystems.textsecuregcm.util.VerificationCode;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import io.dropwizard.jersey.validation.ConstraintViolationExceptionMapper;
import io.dropwizard.testing.junit.ResourceTestRule; import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public class DeviceControllerTest { public class DeviceControllerTest {
@Path("/v1/devices") @Path("/v1/devices")
static class DumbVerificationDeviceController extends DeviceController { static class DumbVerificationDeviceController extends DeviceController {
public DumbVerificationDeviceController(PendingDevicesManager pendingDevices, AccountsManager accounts, RateLimiters rateLimiters) { public DumbVerificationDeviceController(PendingDevicesManager pendingDevices,
super(pendingDevices, accounts, rateLimiters); AccountsManager accounts,
MessagesManager messages,
RateLimiters rateLimiters)
{
super(pendingDevices, accounts, messages, rateLimiters);
} }
@Override @Override
@@ -53,15 +65,22 @@ public class DeviceControllerTest {
private PendingDevicesManager pendingDevicesManager = mock(PendingDevicesManager.class); private PendingDevicesManager pendingDevicesManager = mock(PendingDevicesManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class ); private AccountsManager accountsManager = mock(AccountsManager.class );
private MessagesManager messagesManager = mock(MessagesManager.class);
private RateLimiters rateLimiters = mock(RateLimiters.class ); private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class ); private RateLimiter rateLimiter = mock(RateLimiter.class );
private Account account = mock(Account.class ); private Account account = mock(Account.class );
private Account maxedAccount = mock(Account.class);
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new DeviceLimitExceededExceptionMapper())
.addProvider(new ConstraintViolationExceptionMapper())
.addResource(new DumbVerificationDeviceController(pendingDevicesManager, .addResource(new DumbVerificationDeviceController(pendingDevicesManager,
accountsManager, accountsManager,
messagesManager,
rateLimiters)) rateLimiters))
.build(); .build();
@@ -75,27 +94,57 @@ public class DeviceControllerTest {
when(rateLimiters.getVerifyDeviceLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVerifyDeviceLimiter()).thenReturn(rateLimiter);
when(account.getNextDeviceId()).thenReturn(42L); when(account.getNextDeviceId()).thenReturn(42L);
when(maxedAccount.getActiveDeviceCount()).thenReturn(3);
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901")); when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901"));
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of("1112223"));
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account)); when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount));
} }
@Test @Test
public void validDeviceRegisterTest() throws Exception { public void validDeviceRegisterTest() throws Exception {
VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning/code") VerificationCode deviceCode = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target("/v1/devices/provisioning/code")
.get(VerificationCode.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(VerificationCode.class);
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901)); assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
DeviceResponse response = resources.client().resource("/v1/devices/5678901") DeviceResponse response = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) .target("/v1/devices/5678901")
.entity(new AccountAttributes("keykeykeykey", false, true, 1234)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
.put(DeviceResponse.class); .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234),
MediaType.APPLICATION_JSON_TYPE),
DeviceResponse.class);
assertThat(response.getDeviceId()).isEqualTo(42L); assertThat(response.getDeviceId()).isEqualTo(42L);
verify(pendingDevicesManager).remove(AuthHelper.VALID_NUMBER); verify(pendingDevicesManager).remove(AuthHelper.VALID_NUMBER);
} }
@Test
public void maxDevicesTest() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/devices/provisioning/code")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
.get();
assertEquals(response.getStatus(), 411);
}
@Test
public void longNameTest() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/devices/5678901")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters", true),
MediaType.APPLICATION_JSON_TYPE));
assertEquals(response.getStatus(), 422);
}
} }

View File

@@ -1,12 +1,12 @@
package org.whispersystems.textsecuregcm.tests.controllers; package org.whispersystems.textsecuregcm.tests.controllers;
import com.sun.jersey.api.client.ClientResponse; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.DirectoryController; import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens; import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
@@ -15,12 +15,14 @@ import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import io.dropwizard.testing.junit.ResourceTestRule; import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyList;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -33,7 +35,9 @@ public class DirectoryControllerTest {
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new DirectoryController(rateLimiters, .addResource(new DirectoryController(rateLimiters,
directoryManager)) directoryManager))
.build(); .build();
@@ -64,17 +68,17 @@ public class DirectoryControllerTest {
List<String> expectedResponse = new LinkedList<>(tokens); List<String> expectedResponse = new LinkedList<>(tokens);
expectedResponse.remove(0); expectedResponse.remove(0);
ClientResponse response = Response response =
resources.client().resource("/v1/directory/tokens/") resources.getJerseyTest()
.entity(new ClientContactTokens(tokens)) .target("/v1/directory/tokens/")
.type(MediaType.APPLICATION_JSON_TYPE) .request()
.header("Authorization", .header("Authorization",
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER,
AuthHelper.VALID_PASSWORD)) AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class); .put(Entity.entity(new ClientContactTokens(tokens), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getEntity(ClientContactTokens.class).getContacts()).isEqualTo(expectedResponse); assertThat(response.readEntity(ClientContactTokens.class).getContacts()).isEqualTo(expectedResponse);
} }
} }

View File

@@ -3,10 +3,11 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1; import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2; import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2; import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
@@ -27,7 +28,9 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Set; import java.util.Set;
@@ -64,7 +67,9 @@ public class FederatedControllerTest {
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new FederationControllerV1(accountsManager, null, messageController, null)) .addResource(new FederationControllerV1(accountsManager, null, messageController, null))
.addResource(new FederationControllerV2(accountsManager, null, messageController, keysControllerV2)) .addResource(new FederationControllerV2(accountsManager, null, messageController, keysControllerV2))
.build(); .build();
@@ -74,16 +79,16 @@ public class FederatedControllerTest {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{ Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis())); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}}; }};
Set<Device> multiDeviceList = new HashSet<Device>() {{ Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis())); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis())); add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}}; }};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList); Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, false, multiDeviceList); Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList);
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount)); when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount)); when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
@@ -97,22 +102,25 @@ public class FederatedControllerTest {
@Test @Test
public void testSingleDeviceCurrent() throws Exception { public void testSingleDeviceCurrent() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/federation/messages/+14152223333/1/%s", SINGLE_DEVICE_RECIPIENT)) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo")) .target(String.format("/v1/federation/messages/+14152223333/1/%s", SINGLE_DEVICE_RECIPIENT))
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
.put(ClientResponse.class); .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response", response.getStatus(), is(equalTo(204))); assertThat("Good Response", response.getStatus(), is(equalTo(204)));
verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.Envelope.class));
} }
@Test @Test
public void testSignedPreKeyV2() throws Exception { public void testSignedPreKeyV2() throws Exception {
PreKeyResponseV2 response = PreKeyResponseV2 response =
resources.client().resource("/v2/federation/key/+14152223333/1") resources.getJerseyTest()
.target("/v2/federation/key/+14152223333/1")
.request()
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo")) .header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
.get(PreKeyResponseV2.class); .get(PreKeyResponseV2.class);

View File

@@ -1,14 +1,14 @@
package org.whispersystems.textsecuregcm.tests.controllers; package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1; import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2; import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyCount; import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
@@ -16,6 +16,7 @@ import org.whispersystems.textsecuregcm.entities.PreKeyStateV1;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV2; import org.whispersystems.textsecuregcm.entities.PreKeyStateV2;
import org.whispersystems.textsecuregcm.entities.PreKeyV1; import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV2; import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
@@ -25,14 +26,16 @@ import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys; import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import io.dropwizard.testing.junit.ResourceTestRule; import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public class KeyControllerTest { public class KeyControllerTest {
@@ -63,7 +66,9 @@ public class KeyControllerTest {
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new KeysControllerV1(rateLimiters, keys, accounts, null)) .addResource(new KeysControllerV1(rateLimiters, keys, accounts, null))
.addResource(new KeysControllerV2(rateLimiters, keys, accounts, null)) .addResource(new KeysControllerV2(rateLimiters, keys, accounts, null))
.build(); .build();
@@ -134,10 +139,12 @@ public class KeyControllerTest {
@Test @Test
public void validKeyStatusTestV1() throws Exception { public void validKeyStatusTestV1() throws Exception {
PreKeyCount result = resources.client().resource("/v1/keys") PreKeyCount result = resources.getJerseyTest()
.header("Authorization", .target("/v1/keys")
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .request()
.get(PreKeyCount.class); .header("Authorization",
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyCount.class);
assertThat(result.getCount() == 4); assertThat(result.getCount() == 4);
@@ -146,7 +153,9 @@ public class KeyControllerTest {
@Test @Test
public void validKeyStatusTestV2() throws Exception { public void validKeyStatusTestV2() throws Exception {
PreKeyCount result = resources.client().resource("/v2/keys") PreKeyCount result = resources.getJerseyTest()
.target("/v2/keys")
.request()
.header("Authorization", .header("Authorization",
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyCount.class); .get(PreKeyCount.class);
@@ -158,7 +167,9 @@ public class KeyControllerTest {
@Test @Test
public void getSignedPreKeyV2() throws Exception { public void getSignedPreKeyV2() throws Exception {
SignedPreKey result = resources.client().resource("/v2/keys/signed") SignedPreKey result = resources.getJerseyTest()
.target("/v2/keys/signed")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(SignedPreKey.class); .get(SignedPreKey.class);
@@ -168,10 +179,11 @@ public class KeyControllerTest {
@Test @Test
public void putSignedPreKeyV2() throws Exception { public void putSignedPreKeyV2() throws Exception {
SignedPreKey test = new SignedPreKey(9999, "fooozzz", "baaarzzz"); SignedPreKey test = new SignedPreKey(9999, "fooozzz", "baaarzzz");
ClientResponse response = resources.client().resource("/v2/keys/signed") Response response = resources.getJerseyTest()
.target("/v2/keys/signed")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.type(MediaType.APPLICATION_JSON_TYPE) .put(Entity.entity(test, MediaType.APPLICATION_JSON_TYPE));
.put(ClientResponse.class, test);
assertThat(response.getStatus() == 204); assertThat(response.getStatus() == 204);
@@ -181,9 +193,11 @@ public class KeyControllerTest {
@Test @Test
public void validLegacyRequestTest() throws Exception { public void validLegacyRequestTest() throws Exception {
PreKeyV1 result = resources.client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER)) PreKeyV1 result = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/keys/%s", EXISTS_NUMBER))
.get(PreKeyV1.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyV1.class);
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
@@ -195,7 +209,9 @@ public class KeyControllerTest {
@Test @Test
public void validSingleRequestTestV2() throws Exception { public void validSingleRequestTestV2() throws Exception {
PreKeyResponseV2 result = resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER)) PreKeyResponseV2 result = resources.getJerseyTest()
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponseV2.class); .get(PreKeyResponseV2.class);
@@ -212,9 +228,11 @@ public class KeyControllerTest {
@Test @Test
public void validMultiRequestTestV1() throws Exception { public void validMultiRequestTestV1() throws Exception {
PreKeyResponseV1 results = resources.client().resource(String.format("/v1/keys/%s/*", EXISTS_NUMBER)) PreKeyResponseV1 results = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/keys/%s/*", EXISTS_NUMBER))
.get(PreKeyResponseV1.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponseV1.class);
assertThat(results.getKeys().size()).isEqualTo(3); assertThat(results.getKeys().size()).isEqualTo(3);
@@ -243,7 +261,9 @@ public class KeyControllerTest {
@Test @Test
public void validMultiRequestTestV2() throws Exception { public void validMultiRequestTestV2() throws Exception {
PreKeyResponseV2 results = resources.client().resource(String.format("/v2/keys/%s/*", EXISTS_NUMBER)) PreKeyResponseV2 results = resources.getJerseyTest()
.target(String.format("/v2/keys/%s/*", EXISTS_NUMBER))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponseV2.class); .get(PreKeyResponseV2.class);
@@ -292,59 +312,73 @@ public class KeyControllerTest {
@Test @Test
public void invalidRequestTestV1() throws Exception { public void invalidRequestTestV1() throws Exception {
ClientResponse response = resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER)) Response response = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.get(ClientResponse.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get();
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404); assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
} }
@Test @Test
public void invalidRequestTestV2() throws Exception { public void invalidRequestTestV2() throws Exception {
ClientResponse response = resources.client().resource(String.format("/v2/keys/%s", NOT_EXISTS_NUMBER)) Response response = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v2/keys/%s", NOT_EXISTS_NUMBER))
.get(ClientResponse.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get();
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404); assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
} }
@Test @Test
public void anotherInvalidRequestTestV2() throws Exception { public void anotherInvalidRequestTestV2() throws Exception {
ClientResponse response = resources.client().resource(String.format("/v2/keys/%s/22", EXISTS_NUMBER)) Response response = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v2/keys/%s/22", EXISTS_NUMBER))
.get(ClientResponse.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get();
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404); assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
} }
@Test @Test
public void unauthorizedRequestTestV1() throws Exception { public void unauthorizedRequestTestV1() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER)) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD)) .target(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.get(ClientResponse.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get();
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
response = response =
resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER)) resources.getJerseyTest()
.get(ClientResponse.class); .target(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.request()
.get();
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
} }
@Test @Test
public void unauthorizedRequestTestV2() throws Exception { public void unauthorizedRequestTestV2() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER)) resources.getJerseyTest()
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get(ClientResponse.class); .get();
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
response = response =
resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER)) resources.getJerseyTest()
.get(ClientResponse.class); .target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.request()
.get();
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
} }
@@ -362,13 +396,14 @@ public class KeyControllerTest {
preKeyList.setKeys(preKeys); preKeyList.setKeys(preKeys);
preKeyList.setLastResortKey(lastResortKey); preKeyList.setLastResortKey(lastResortKey);
ClientResponse response = Response response =
resources.client().resource("/v1/keys") resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target("/v1/keys")
.type(MediaType.APPLICATION_JSON_TYPE) .request()
.put(ClientResponse.class, preKeyList); .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(preKeyList, MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class ); ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class );
ArgumentCaptor<PreKeyV1> lastResortCaptor = ArgumentCaptor.forClass(PreKeyV1.class); ArgumentCaptor<PreKeyV1> lastResortCaptor = ArgumentCaptor.forClass(PreKeyV1.class);
@@ -400,13 +435,14 @@ public class KeyControllerTest {
PreKeyStateV2 preKeyState = new PreKeyStateV2(identityKey, signedPreKey, preKeys, lastResortKey); PreKeyStateV2 preKeyState = new PreKeyStateV2(identityKey, signedPreKey, preKeys, lastResortKey);
ClientResponse response = Response response =
resources.client().resource("/v2/keys") resources.getJerseyTest()
.target("/v2/keys")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.type(MediaType.APPLICATION_JSON_TYPE) .put(Entity.entity(preKeyState, MediaType.APPLICATION_JSON_TYPE));
.put(ClientResponse.class, preKeyState);
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class); ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class);
verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), eq(lastResortKey)); verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), eq(lastResortKey));

View File

@@ -2,14 +2,14 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.hamcrest.CoreMatchers;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.MessageController; import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices; import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
@@ -25,9 +25,10 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -62,7 +63,9 @@ public class MessageControllerTest {
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, .addResource(new MessageController(rateLimiters, pushSender, receiptSender, accountsManager,
messagesManager, federatedClientManager)) messagesManager, federatedClientManager))
.build(); .build();
@@ -71,17 +74,17 @@ public class MessageControllerTest {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{ Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis())); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}}; }};
Set<Device> multiDeviceList = new HashSet<Device>() {{ Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis())); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis())); add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(3, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31))); add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), false, "Test"));
}}; }};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList); Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, false, multiDeviceList); Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList);
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount)); when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount)); when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
@@ -89,47 +92,35 @@ public class MessageControllerTest {
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter); when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
} }
@Test
public synchronized void testSingleDeviceLegacy() throws Exception {
ClientResponse response =
resources.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, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
}
@Test @Test
public synchronized void testSingleDeviceCurrent() throws Exception { public synchronized void testSingleDeviceCurrent() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT)) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT))
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class); .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response", response.getStatus(), is(equalTo(200))); assertThat("Good Response", response.getStatus(), is(equalTo(200)));
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class));
} }
@Test @Test
public synchronized void testMultiDeviceMissing() throws Exception { public synchronized void testMultiDeviceMissing() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT)) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class); .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response Code", response.getStatus(), is(equalTo(409))); assertThat("Good Response Code", response.getStatus(), is(equalTo(409)));
assertThat("Good Response Body", assertThat("Good Response Body",
asJson(response.getEntity(MismatchedDevices.class)), asJson(response.readEntity(MismatchedDevices.class)),
is(equalTo(jsonFixture("fixtures/missing_device_response.json")))); is(equalTo(jsonFixture("fixtures/missing_device_response.json"))));
verifyNoMoreInteractions(pushSender); verifyNoMoreInteractions(pushSender);
@@ -137,17 +128,18 @@ public class MessageControllerTest {
@Test @Test
public synchronized void testMultiDeviceExtra() throws Exception { public synchronized void testMultiDeviceExtra() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT)) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
.entity(mapper.readValue(jsonFixture("fixtures/current_message_extra_device.json"), IncomingMessageList.class)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class); .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_extra_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response Code", response.getStatus(), is(equalTo(409))); assertThat("Good Response Code", response.getStatus(), is(equalTo(409)));
assertThat("Good Response Body", assertThat("Good Response Body",
asJson(response.getEntity(MismatchedDevices.class)), asJson(response.readEntity(MismatchedDevices.class)),
is(equalTo(jsonFixture("fixtures/missing_device_response2.json")))); is(equalTo(jsonFixture("fixtures/missing_device_response2.json"))));
verifyNoMoreInteractions(pushSender); verifyNoMoreInteractions(pushSender);
@@ -155,31 +147,32 @@ public class MessageControllerTest {
@Test @Test
public synchronized void testMultiDevice() throws Exception { public synchronized void testMultiDevice() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT)) resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
.entity(mapper.readValue(jsonFixture("fixtures/current_message_multi_device.json"), IncomingMessageList.class)) .request()
.type(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class); .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_multi_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response Code", response.getStatus(), is(equalTo(200))); assertThat("Good Response Code", response.getStatus(), is(equalTo(200)));
verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class));
} }
@Test @Test
public synchronized void testRegistrationIdMismatch() throws Exception { public synchronized void testRegistrationIdMismatch() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT)) resources.getJerseyTest().target(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .request()
.entity(mapper.readValue(jsonFixture("fixtures/current_message_registration_id.json"), IncomingMessageList.class)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.type(MediaType.APPLICATION_JSON_TYPE) .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_registration_id.json"), IncomingMessageList.class),
.put(ClientResponse.class); MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response Code", response.getStatus(), is(equalTo(410))); assertThat("Good Response Code", response.getStatus(), is(equalTo(410)));
assertThat("Good Response Body", assertThat("Good Response Body",
asJson(response.getEntity(StaleDevices.class)), asJson(response.readEntity(StaleDevices.class)),
is(equalTo(jsonFixture("fixtures/mismatched_registration_id.json")))); is(equalTo(jsonFixture("fixtures/mismatched_registration_id.json"))));
verifyNoMoreInteractions(pushSender); verifyNoMoreInteractions(pushSender);
@@ -193,14 +186,17 @@ public class MessageControllerTest {
final long timestampTwo = 313388; final long timestampTwo = 313388;
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{ List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
add(new OutgoingMessageEntity(1L, MessageProtos.OutgoingMessageSignal.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes())); add(new OutgoingMessageEntity(1L, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null));
add(new OutgoingMessageEntity(2L, MessageProtos.OutgoingMessageSignal.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null)); add(new OutgoingMessageEntity(2L, Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null));
}}; }};
when(messagesManager.getMessagesForDevice(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(messages); OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
when(messagesManager.getMessagesForDevice(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(messagesList);
OutgoingMessageEntityList response = OutgoingMessageEntityList response =
resources.client().resource("/v1/messages/") resources.getJerseyTest().target("/v1/messages/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.accept(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE)
.get(OutgoingMessageEntityList.class); .get(OutgoingMessageEntityList.class);
@@ -218,39 +214,45 @@ public class MessageControllerTest {
@Test @Test
public synchronized void testDeleteMessages() throws Exception { public synchronized void testDeleteMessages() throws Exception {
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
when(messagesManager.delete(AuthHelper.VALID_NUMBER, "+14152222222", 31337)) when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31337))
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, .thenReturn(Optional.of(new OutgoingMessageEntity(31337L,
MessageProtos.OutgoingMessageSignal.Type.CIPHERTEXT_VALUE, Envelope.Type.CIPHERTEXT_VALUE,
null, timestamp, null, timestamp,
"+14152222222", 1, "hi".getBytes()))); "+14152222222", 1, "hi".getBytes(), null)));
when(messagesManager.delete(AuthHelper.VALID_NUMBER, "+14152222222", 31338)) when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31338))
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, .thenReturn(Optional.of(new OutgoingMessageEntity(31337L,
MessageProtos.OutgoingMessageSignal.Type.RECEIPT_VALUE, Envelope.Type.RECEIPT_VALUE,
null, System.currentTimeMillis(), null, System.currentTimeMillis(),
"+14152222222", 1, null))); "+14152222222", 1, null, null)));
when(messagesManager.delete(AuthHelper.VALID_NUMBER, "+14152222222", 31339)) when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31339))
.thenReturn(Optional.<OutgoingMessageEntity>absent()); .thenReturn(Optional.<OutgoingMessageEntity>absent());
ClientResponse response = resources.client().resource(String.format("/v1/messages/%s/%d", "+14152222222", 31337)) Response response = resources.getJerseyTest()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .target(String.format("/v1/messages/%s/%d", "+14152222222", 31337))
.delete(ClientResponse.class); .request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete();
assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp), eq(Optional.<String>absent())); verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp), eq(Optional.<String>absent()));
response = resources.client().resource(String.format("/v1/messages/%s/%d", "+14152222222", 31338)) response = resources.getJerseyTest()
.target(String.format("/v1/messages/%s/%d", "+14152222222", 31338))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete(ClientResponse.class); .delete();
assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verifyNoMoreInteractions(receiptSender); verifyNoMoreInteractions(receiptSender);
response = resources.client().resource(String.format("/v1/messages/%s/%d", "+14152222222", 31339)) response = resources.getJerseyTest()
.target(String.format("/v1/messages/%s/%d", "+14152222222", 31339))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete(ClientResponse.class); .delete();
assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verifyNoMoreInteractions(receiptSender); verifyNoMoreInteractions(receiptSender);

View File

@@ -2,12 +2,14 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse; import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.ReceiptController; import org.whispersystems.textsecuregcm.controllers.ReceiptController;
import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.ReceiptSender;
@@ -16,11 +18,14 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import io.dropwizard.testing.junit.ResourceTestRule; import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -39,23 +44,25 @@ public class ReceiptControllerTest {
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new ReceiptController(receiptSender)) .addResource(new ReceiptController(receiptSender))
.build(); .build();
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{ Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis())); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}}; }};
Set<Device> multiDeviceList = new HashSet<Device>() {{ Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis())); add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis())); add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}}; }};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList); Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, false, multiDeviceList); Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList);
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount)); when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount)); when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
@@ -63,26 +70,32 @@ public class ReceiptControllerTest {
@Test @Test
public synchronized void testSingleDeviceCurrent() throws Exception { public synchronized void testSingleDeviceCurrent() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/receipt/%s/%d", SINGLE_DEVICE_RECIPIENT, 1234)) resources.getJerseyTest()
.target(String.format("/v1/receipt/%s/%d", SINGLE_DEVICE_RECIPIENT, 1234))
.request()
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class); .put(null);
assertThat(response.getStatus() == 204); assertThat(response.getStatus() == 204);
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class));
} }
@Test @Test
public synchronized void testMultiDeviceCurrent() throws Exception { public synchronized void testMultiDeviceCurrent() throws Exception {
ClientResponse response = Response response =
resources.client().resource(String.format("/v1/receipt/%s/%d", MULTI_DEVICE_RECIPIENT, 12345)) resources.getJerseyTest()
.target(String.format("/v1/receipt/%s/%d", MULTI_DEVICE_RECIPIENT, 12345))
.request()
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(ClientResponse.class); .put(null);
assertThat(response.getStatus() == 204); assertThat(response.getStatus() == 204);
verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class));
} }

View File

@@ -1,6 +1,5 @@
package org.whispersystems.textsecuregcm.tests.entities; package org.whispersystems.textsecuregcm.tests.entities;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
@@ -8,9 +7,7 @@ import org.whispersystems.textsecuregcm.util.Util;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.asJson; import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.fromJson;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture;
public class ClientContactTest { public class ClientContactTest {
@@ -19,7 +16,7 @@ public class ClientContactTest {
byte[] token = Util.getContactToken("+14152222222"); byte[] token = Util.getContactToken("+14152222222");
ClientContact contact = new ClientContact(token, null, false); ClientContact contact = new ClientContact(token, null, false);
ClientContact contactWithRelay = new ClientContact(token, "whisper", false); ClientContact contactWithRelay = new ClientContact(token, "whisper", false);
ClientContact contactWithRelaySms = new ClientContact(token, "whisper", true ); ClientContact contactWithRelayVox = new ClientContact(token, "whisper", true);
assertThat("Basic Contact Serialization works", assertThat("Basic Contact Serialization works",
asJson(contact), asJson(contact),
@@ -29,18 +26,18 @@ public class ClientContactTest {
asJson(contactWithRelay), asJson(contactWithRelay),
is(equalTo(jsonFixture("fixtures/contact.relay.json")))); is(equalTo(jsonFixture("fixtures/contact.relay.json"))));
assertThat("Contact Relay+SMS Serialization works", assertThat("Contact Relay Vox Serializaton works",
asJson(contactWithRelaySms), asJson(contactWithRelayVox),
is(equalTo(jsonFixture("fixtures/contact.relay.sms.json")))); is(equalTo(jsonFixture("fixtures/contact.relay.voice.json"))));
} }
@Test @Test
public void deserializeFromJSON() throws Exception { public void deserializeFromJSON() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"), ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", true); "whisper", false);
assertThat("a ClientContact can be deserialized from JSON", assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.sms.json"), ClientContact.class), fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),
is(contact)); is(contact));
} }

View File

@@ -26,10 +26,10 @@ public class PreKeyTest {
@Test @Test
public void deserializeFromJSONV() throws Exception { public void deserializeFromJSONV() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"), ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", true); "whisper", false);
assertThat("a ClientContact can be deserialized from JSON", assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.sms.json"), ClientContact.class), fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),
is(contact)); is(contact));
} }

View File

@@ -0,0 +1,72 @@
package org.whispersystems.textsecuregcm.tests.push;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask;
import org.whispersystems.textsecuregcm.push.PushServiceClient;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnectionInfo;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.*;
public class ApnFallbackManagerTest {
@Test
public void testFullFallback() throws Exception {
PushServiceClient pushServiceClient = mock(PushServiceClient.class);
PubSubManager pubSubManager = mock(PubSubManager.class);
WebsocketAddress address = new WebsocketAddress("+14152222223", 1L);
WebSocketConnectionInfo info = new WebSocketConnectionInfo(address);
ApnMessage message = new ApnMessage("bar", "123", 1, "hmm", true, 1111);
ApnFallbackTask task = new ApnFallbackTask("foo", message, 500);
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient, pubSubManager);
apnFallbackManager.start();
apnFallbackManager.schedule(address, task);
Util.sleep(1100);
ArgumentCaptor<ApnMessage> captor = ArgumentCaptor.forClass(ApnMessage.class);
verify(pushServiceClient, times(1)).send(captor.capture());
verify(pubSubManager).unsubscribe(eq(info), eq(apnFallbackManager));
assertEquals(captor.getValue().getMessage(), message.getMessage());
assertEquals(captor.getValue().getApnId(), task.getApnId());
assertFalse(captor.getValue().isVoip());
assertEquals(captor.getValue().getExpirationTime(), Integer.MAX_VALUE * 1000L);
}
@Test
public void testNoFallback() throws Exception {
PushServiceClient pushServiceClient = mock(PushServiceClient.class);
PubSubManager pubSubManager = mock(PubSubManager.class);
WebsocketAddress address = new WebsocketAddress("+14152222222", 1);
WebSocketConnectionInfo info = new WebSocketConnectionInfo(address);
ApnMessage message = new ApnMessage("bar", "123", 1, "hmm", true, 5555);
ApnFallbackTask task = new ApnFallbackTask ("foo", message, 500);
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient, pubSubManager);
apnFallbackManager.start();
apnFallbackManager.schedule(address, task);
apnFallbackManager.onDispatchMessage(info.serialize(),
PubSubProtos.PubSubMessage.newBuilder()
.setType(PubSubProtos.PubSubMessage.Type.CONNECTED)
.build().toByteArray());
verify(pubSubManager).unsubscribe(eq(info), eq(apnFallbackManager));
Util.sleep(1100);
verifyNoMoreInteractions(pushServiceClient);
}
}

View File

@@ -0,0 +1,92 @@
package org.whispersystems.textsecuregcm.tests.push;
import org.junit.Test;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTaskQueue;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ApnFallbackTaskQueueTest {
@Test
public void testBlocking() {
final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
final WebsocketAddress address = mock(WebsocketAddress.class);
final ApnFallbackTask task = mock(ApnFallbackTask.class );
when(task.getExecutionTime()).thenReturn(System.currentTimeMillis() - 1000);
new Thread() {
@Override
public void run() {
Util.sleep(500);
taskQueue.put(address, task);
}
}.start();
Map.Entry<WebsocketAddress, ApnFallbackTask> result = taskQueue.get();
assertEquals(result.getKey(), address);
assertEquals(result.getValue(), task);
}
@Test
public void testElapsedTime() {
final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
final WebsocketAddress address = mock(WebsocketAddress.class);
final ApnFallbackTask task = mock(ApnFallbackTask.class );
long currentTime = System.currentTimeMillis();
when(task.getExecutionTime()).thenReturn(currentTime + 1000);
taskQueue.put(address, task);
Map.Entry<WebsocketAddress, ApnFallbackTask> result = taskQueue.get();
assertTrue(System.currentTimeMillis() >= currentTime + 1000);
assertEquals(result.getKey(), address);
assertEquals(result.getValue(), task);
}
@Test
public void testCanceled() {
final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
final WebsocketAddress addressOne = mock(WebsocketAddress.class);
final ApnFallbackTask taskOne = mock(ApnFallbackTask.class );
final WebsocketAddress addressTwo = mock(WebsocketAddress.class);
final ApnFallbackTask taskTwo = mock(ApnFallbackTask.class );
long currentTime = System.currentTimeMillis();
when(taskOne.getExecutionTime()).thenReturn(currentTime + 1000);
when(taskTwo.getExecutionTime()).thenReturn(currentTime + 2000);
taskQueue.put(addressOne, taskOne);
taskQueue.put(addressTwo, taskTwo);
new Thread() {
@Override
public void run() {
Util.sleep(300);
taskQueue.remove(addressOne);
}
}.start();
Map.Entry<WebsocketAddress, ApnFallbackTask> result = taskQueue.get();
assertTrue(System.currentTimeMillis() >= currentTime + 2000);
assertEquals(result.getKey(), addressTwo);
assertEquals(result.getValue(), taskTwo);
}
}

View File

@@ -1,36 +0,0 @@
package org.whispersystems.textsecuregcm.tests.sms;
import com.google.common.base.Optional;
import com.twilio.sdk.TwilioRestException;
import junit.framework.TestCase;
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import java.io.IOException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
public class DeliveryPreferenceTest extends TestCase {
private TwilioSmsSender twilioSender = mock(TwilioSmsSender.class);
private NexmoSmsSender nexmoSender = mock(NexmoSmsSender.class);
public void testInternationalPreferenceOff() throws IOException, TwilioRestException {
SmsSender smsSender = new SmsSender(twilioSender, Optional.of(nexmoSender), false);
smsSender.deliverSmsVerification("+441112223333", "123-456");
verify(nexmoSender).deliverSmsVerification("+441112223333", "123-456");
verifyNoMoreInteractions(twilioSender);
}
public void testInternationalPreferenceOn() throws IOException, TwilioRestException {
SmsSender smsSender = new SmsSender(twilioSender, Optional.of(nexmoSender), true);
smsSender.deliverSmsVerification("+441112223333", "123-456");
verify(twilioSender).deliverSmsVerification("+441112223333", "123-456");
verifyNoMoreInteractions(nexmoSender);
}
}

View File

@@ -1,43 +0,0 @@
package org.whispersystems.textsecuregcm.tests.sms;
import com.google.common.base.Optional;
import com.twilio.sdk.TwilioRestException;
import junit.framework.TestCase;
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import java.io.IOException;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
public class TwilioFallbackTest extends TestCase {
private NexmoSmsSender nexmoSender = mock(NexmoSmsSender.class );
private TwilioSmsSender twilioSender = mock(TwilioSmsSender.class);
@Override
protected void setUp() throws IOException, TwilioRestException {
doThrow(new TwilioRestException("foo", 404)).when(twilioSender).deliverSmsVerification(anyString(), anyString());
doThrow(new TwilioRestException("bar", 405)).when(twilioSender).deliverVoxVerification(anyString(), anyString());
}
public void testNexmoSmsFallback() throws IOException, TwilioRestException {
SmsSender smsSender = new SmsSender(twilioSender, Optional.of(nexmoSender), true);
smsSender.deliverSmsVerification("+442223334444", "123-456");
verify(nexmoSender).deliverSmsVerification("+442223334444", "123-456");
verify(twilioSender).deliverSmsVerification("+442223334444", "123-456");
}
public void testNexmoVoxFallback() throws IOException, TwilioRestException {
SmsSender smsSender = new SmsSender(twilioSender, Optional.of(nexmoSender), true);
smsSender.deliverVoxVerification("+442223334444", "123-456");
verify(nexmoSender).deliverVoxVerification("+442223334444", "123-456");
verify(twilioSender).deliverVoxVerification("+442223334444", "123-456");
}
}

View File

@@ -1,22 +1,23 @@
package org.whispersystems.textsecuregcm.tests.util; package org.whispersystems.textsecuregcm.tests.util;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator; import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration; import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import java.util.Arrays;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -24,23 +25,38 @@ public class AuthHelper {
public static final String VALID_NUMBER = "+14150000000"; public static final String VALID_NUMBER = "+14150000000";
public static final String VALID_PASSWORD = "foo"; public static final String VALID_PASSWORD = "foo";
public static final String VALID_NUMBER_TWO = "+14151111111";
public static final String VALID_PASSWORD_TWO = "baz";
public static final String INVVALID_NUMBER = "+14151111111"; public static final String INVVALID_NUMBER = "+14151111111";
public static final String INVALID_PASSWORD = "bar"; public static final String INVALID_PASSWORD = "bar";
public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class ); public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class );
public static Account VALID_ACCOUNT = mock(Account.class ); public static Account VALID_ACCOUNT = mock(Account.class );
public static Account VALID_ACCOUNT_TWO = mock(Account.class);
public static Device VALID_DEVICE = mock(Device.class ); public static Device VALID_DEVICE = mock(Device.class );
public static AuthenticationCredentials VALID_CREDENTIALS = mock(AuthenticationCredentials.class); public static Device VALID_DEVICE_TWO = mock(Device.class);
private static AuthenticationCredentials VALID_CREDENTIALS = mock(AuthenticationCredentials.class);
private static AuthenticationCredentials VALID_CREDENTIALS_TWO = mock(AuthenticationCredentials.class);
public static MultiBasicAuthProvider<FederatedPeer, Account> getAuthenticator() { public static AuthDynamicFeature getAuthFilter() {
when(VALID_CREDENTIALS.verify("foo")).thenReturn(true); when(VALID_CREDENTIALS.verify("foo")).thenReturn(true);
when(VALID_CREDENTIALS_TWO.verify("baz")).thenReturn(true);
when(VALID_DEVICE.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS); when(VALID_DEVICE.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS);
when(VALID_DEVICE_TWO.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS_TWO);
when(VALID_DEVICE.getId()).thenReturn(1L); when(VALID_DEVICE.getId()).thenReturn(1L);
when(VALID_DEVICE_TWO.getId()).thenReturn(1L);
when(VALID_ACCOUNT.getDevice(anyLong())).thenReturn(Optional.of(VALID_DEVICE)); when(VALID_ACCOUNT.getDevice(anyLong())).thenReturn(Optional.of(VALID_DEVICE));
when(VALID_ACCOUNT_TWO.getDevice(eq(1L))).thenReturn(Optional.of(VALID_DEVICE_TWO));
when(VALID_ACCOUNT_TWO.getActiveDeviceCount()).thenReturn(3);
when(VALID_ACCOUNT.getNumber()).thenReturn(VALID_NUMBER); when(VALID_ACCOUNT.getNumber()).thenReturn(VALID_NUMBER);
when(VALID_ACCOUNT_TWO.getNumber()).thenReturn(VALID_NUMBER_TWO);
when(VALID_ACCOUNT.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE)); when(VALID_ACCOUNT.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE));
when(VALID_ACCOUNT_TWO.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE_TWO));
when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.<String>absent()); when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.<String>absent());
when(VALID_ACCOUNT_TWO.getRelay()).thenReturn(Optional.<String>absent());
when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT)); when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT));
when(ACCOUNTS_MANAGER.get(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO));
List<FederatedPeer> peer = new LinkedList<FederatedPeer>() {{ List<FederatedPeer> peer = new LinkedList<FederatedPeer>() {{
add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz")); add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz"));
@@ -49,10 +65,14 @@ public class AuthHelper {
FederationConfiguration federationConfiguration = mock(FederationConfiguration.class); FederationConfiguration federationConfiguration = mock(FederationConfiguration.class);
when(federationConfiguration.getPeers()).thenReturn(peer); when(federationConfiguration.getPeers()).thenReturn(peer);
return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(federationConfiguration), return new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
FederatedPeer.class, .setAuthenticator(new AccountAuthenticator(ACCOUNTS_MANAGER))
new AccountAuthenticator(ACCOUNTS_MANAGER), .setPrincipal(Account.class)
Account.class, "WhisperServer"); .buildAuthFilter(),
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
.setAuthenticator(new FederatedPeerAuthenticator(federationConfiguration))
.setPrincipal(FederatedPeer.class)
.buildAuthFilter());
} }
public static String getAuthHeader(String number, String password) { public static String getAuthHeader(String number, String password) {

View File

@@ -0,0 +1,58 @@
package org.whispersystems.textsecuregcm.tests.util;
import org.junit.Test;
import org.whispersystems.textsecuregcm.util.BlockingThreadPoolExecutor;
import org.whispersystems.textsecuregcm.util.Util;
import static org.junit.Assert.assertTrue;
public class BlockingThreadPoolExecutorTest {
@Test
public void testBlocking() {
BlockingThreadPoolExecutor executor = new BlockingThreadPoolExecutor(1, 3);
long start = System.currentTimeMillis();
executor.execute(new Runnable() {
@Override
public void run() {
Util.sleep(1000);
}
});
assertTrue(System.currentTimeMillis() - start < 500);
start = System.currentTimeMillis();
executor.execute(new Runnable() {
@Override
public void run() {
Util.sleep(1000);
}
});
assertTrue(System.currentTimeMillis() - start < 500);
start = System.currentTimeMillis();
executor.execute(new Runnable() {
@Override
public void run() {
Util.sleep(1000);
}
});
assertTrue(System.currentTimeMillis() - start < 500);
start = System.currentTimeMillis();
executor.execute(new Runnable() {
@Override
public void run() {
Util.sleep(1000);
}
});
assertTrue(System.currentTimeMillis() - start > 500);
}
}

View File

@@ -5,13 +5,15 @@ import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
@@ -19,7 +21,6 @@ import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos; import org.whispersystems.textsecuregcm.storage.PubSubProtos;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener; import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator; import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection; import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
@@ -27,7 +28,6 @@ import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.websocket.WebSocketClient; import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.messages.WebSocketResponseMessage; import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import org.whispersystems.websocket.session.WebSocketSessionContext; import org.whispersystems.websocket.session.WebSocketSessionContext;
import org.whispersystems.websocket.setup.WebSocketConnectListener;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@@ -37,12 +37,10 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.auth.basic.BasicCredentials;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
public class WebSocketConnectionTest { public class WebSocketConnectionTest {
@@ -60,12 +58,13 @@ public class WebSocketConnectionTest {
private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class ); private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class );
private static final PushSender pushSender = mock(PushSender.class); private static final PushSender pushSender = mock(PushSender.class);
private static final ReceiptSender receiptSender = mock(ReceiptSender.class); private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
@Test @Test
public void testCredentials() throws Exception { public void testCredentials() throws Exception {
MessagesManager storedMessages = mock(MessagesManager.class); MessagesManager storedMessages = mock(MessagesManager.class);
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator); WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, storedMessages, pubSubManager); AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, storedMessages, pubSubManager, apnFallbackManager);
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class); WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD)))) when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
@@ -76,21 +75,21 @@ public class WebSocketConnectionTest {
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device)); when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{ when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, List<String>>() {{
put("login", new String[] {VALID_USER}); put("login", new LinkedList<String>() {{add(VALID_USER);}});
put("password", new String[] {VALID_PASSWORD}); put("password", new LinkedList<String>() {{add(VALID_PASSWORD);}});
}}); }});
Optional<Account> account = webSocketAuthenticator.authenticate(upgradeRequest); Optional<Account> account = webSocketAuthenticator.authenticate(upgradeRequest);
when(sessionContext.getAuthenticated(Account.class)).thenReturn(account); when(sessionContext.getAuthenticated(Account.class)).thenReturn(account.get());
connectListener.onWebSocketConnect(sessionContext); connectListener.onWebSocketConnect(sessionContext);
verify(sessionContext).addListener(any(WebSocketSessionContext.WebSocketEventListener.class)); verify(sessionContext).addListener(any(WebSocketSessionContext.WebSocketEventListener.class));
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{ when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, List<String>>() {{
put("login", new String[] {INVALID_USER}); put("login", new LinkedList<String>() {{add(INVALID_USER);}});
put("password", new String[] {INVALID_PASSWORD}); put("password", new LinkedList<String>() {{add(INVALID_PASSWORD);}});
}}); }});
account = webSocketAuthenticator.authenticate(upgradeRequest); account = webSocketAuthenticator.authenticate(upgradeRequest);
@@ -107,6 +106,8 @@ public class WebSocketConnectionTest {
add(createMessage(3L, "sender2", 3333, false, "third")); add(createMessage(3L, "sender2", 3333, false, "third"));
}}; }};
OutgoingMessageEntityList outgoingMessagesList = new OutgoingMessageEntityList(outgoingMessages, false);
when(device.getId()).thenReturn(2L); when(device.getId()).thenReturn(2L);
when(device.getSignalingKey()).thenReturn(Base64.encodeBytes(new byte[52])); when(device.getSignalingKey()).thenReturn(Base64.encodeBytes(new byte[52]));
@@ -126,7 +127,7 @@ public class WebSocketConnectionTest {
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent()); when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId())) when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
.thenReturn(outgoingMessages); .thenReturn(outgoingMessagesList);
final List<SettableFuture<WebSocketResponseMessage>> futures = new LinkedList<>(); final List<SettableFuture<WebSocketResponseMessage>> futures = new LinkedList<>();
final WebSocketClient client = mock(WebSocketClient.class); final WebSocketClient client = mock(WebSocketClient.class);
@@ -157,29 +158,207 @@ public class WebSocketConnectionTest {
futures.get(0).setException(new IOException()); futures.get(0).setException(new IOException());
futures.get(2).setException(new IOException()); futures.get(2).setException(new IOException());
// List<OutgoingMessageSignal> pending = new LinkedList<OutgoingMessageSignal>() {{
// add(createMessage("sender1", 1111, false, "first"));
// add(createMessage("sender2", 3333, false, "third"));
// }};
verify(storedMessages, times(1)).delete(eq(account.getNumber()), eq(2L)); verify(storedMessages, times(1)).delete(eq(account.getNumber()), eq(2L));
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender1"), eq(2222L), eq(Optional.<String>absent()));
// verify(pushSender, times(1)).sendMessage(eq(sender1), eq(sender1device), any(OutgoingMessageSignal.class));
connection.onDispatchUnsubscribed(websocketAddress.serialize()); connection.onDispatchUnsubscribed(websocketAddress.serialize());
verify(client).close(anyInt(), anyString()); verify(client).close(anyInt(), anyString());
} }
@Test
public void testOnlineSend() throws Exception {
MessagesManager storedMessages = mock(MessagesManager.class);
WebsocketSender websocketSender = mock(WebsocketSender.class);
when(pushSender.getWebSocketSender()).thenReturn(websocketSender);
when(websocketSender.queueMessage(any(Account.class), any(Device.class), any(Envelope.class))).thenReturn(10);
Envelope firstMessage = Envelope.newBuilder()
.setLegacyMessage(ByteString.copyFrom("first".getBytes()))
.setSource("sender1")
.setTimestamp(System.currentTimeMillis())
.setSourceDevice(1)
.setType(Envelope.Type.CIPHERTEXT)
.build();
Envelope secondMessage = Envelope.newBuilder()
.setLegacyMessage(ByteString.copyFrom("second".getBytes()))
.setSource("sender2")
.setTimestamp(System.currentTimeMillis())
.setSourceDevice(2)
.setType(Envelope.Type.CIPHERTEXT)
.build();
List<OutgoingMessageEntity> pendingMessages = new LinkedList<>();
OutgoingMessageEntityList pendingMessagesList = new OutgoingMessageEntityList(pendingMessages, false);
when(device.getId()).thenReturn(2L);
when(device.getSignalingKey()).thenReturn(Base64.encodeBytes(new byte[52]));
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
when(account.getNumber()).thenReturn("+14152222222");
final Device sender1device = mock(Device.class);
Set<Device> sender1devices = new HashSet<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(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
.thenReturn(pendingMessagesList);
final List<SettableFuture<WebSocketResponseMessage>> futures = new LinkedList<>();
final WebSocketClient client = mock(WebSocketClient.class);
when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class)))
.thenAnswer(new Answer<SettableFuture<WebSocketResponseMessage>>() {
@Override
public SettableFuture<WebSocketResponseMessage> answer(InvocationOnMock invocationOnMock) throws Throwable {
SettableFuture<WebSocketResponseMessage> future = SettableFuture.create();
futures.add(future);
return future;
}
});
WebsocketAddress websocketAddress = new WebsocketAddress(account.getNumber(), device.getId());
WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender, storedMessages,
account, device, client);
connection.onDispatchSubscribed(websocketAddress.serialize());
connection.onDispatchMessage(websocketAddress.serialize(), PubSubProtos.PubSubMessage.newBuilder()
.setType(PubSubProtos.PubSubMessage.Type.DELIVER)
.setContent(ByteString.copyFrom(firstMessage.toByteArray()))
.build().toByteArray());
connection.onDispatchMessage(websocketAddress.serialize(), PubSubProtos.PubSubMessage.newBuilder()
.setType(PubSubProtos.PubSubMessage.Type.DELIVER)
.setContent(ByteString.copyFrom(secondMessage.toByteArray()))
.build().toByteArray());
verify(client, times(2)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class));
assertEquals(futures.size(), 2);
WebSocketResponseMessage response = mock(WebSocketResponseMessage.class);
when(response.getStatus()).thenReturn(200);
futures.get(1).set(response);
futures.get(0).setException(new IOException());
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()), eq(Optional.<String>absent()));
verify(websocketSender, times(1)).queueMessage(eq(account), eq(device), any(Envelope.class));
verify(pushSender, times(1)).sendQueuedNotification(eq(account), eq(device), eq(10));
connection.onDispatchUnsubscribed(websocketAddress.serialize());
verify(client).close(anyInt(), anyString());
}
@Test
public void testPendingSend() throws Exception {
MessagesManager storedMessages = mock(MessagesManager.class);
WebsocketSender websocketSender = mock(WebsocketSender.class);
reset(websocketSender);
reset(pushSender);
when(pushSender.getWebSocketSender()).thenReturn(websocketSender);
when(websocketSender.queueMessage(any(Account.class), any(Device.class), any(Envelope.class))).thenReturn(10);
final Envelope firstMessage = Envelope.newBuilder()
.setLegacyMessage(ByteString.copyFrom("first".getBytes()))
.setSource("sender1")
.setTimestamp(System.currentTimeMillis())
.setSourceDevice(1)
.setType(Envelope.Type.CIPHERTEXT)
.build();
final Envelope secondMessage = Envelope.newBuilder()
.setLegacyMessage(ByteString.copyFrom("second".getBytes()))
.setSource("sender2")
.setTimestamp(System.currentTimeMillis())
.setSourceDevice(2)
.setType(Envelope.Type.CIPHERTEXT)
.build();
List<OutgoingMessageEntity> pendingMessages = new LinkedList<OutgoingMessageEntity>() {{
add(new OutgoingMessageEntity(1, firstMessage.getType().getNumber(), firstMessage.getRelay(),
firstMessage.getTimestamp(), firstMessage.getSource(),
firstMessage.getSourceDevice(), firstMessage.getLegacyMessage().toByteArray(),
firstMessage.getContent().toByteArray()));
add(new OutgoingMessageEntity(2, secondMessage.getType().getNumber(), secondMessage.getRelay(),
secondMessage.getTimestamp(), secondMessage.getSource(),
secondMessage.getSourceDevice(), secondMessage.getLegacyMessage().toByteArray(),
secondMessage.getContent().toByteArray()));
}};
OutgoingMessageEntityList pendingMessagesList = new OutgoingMessageEntityList(pendingMessages, false);
when(device.getId()).thenReturn(2L);
when(device.getSignalingKey()).thenReturn(Base64.encodeBytes(new byte[52]));
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
when(account.getNumber()).thenReturn("+14152222222");
final Device sender1device = mock(Device.class);
Set<Device> sender1devices = new HashSet<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(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
.thenReturn(pendingMessagesList);
final List<SettableFuture<WebSocketResponseMessage>> futures = new LinkedList<>();
final WebSocketClient client = mock(WebSocketClient.class);
when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class)))
.thenAnswer(new Answer<SettableFuture<WebSocketResponseMessage>>() {
@Override
public SettableFuture<WebSocketResponseMessage> answer(InvocationOnMock invocationOnMock) throws Throwable {
SettableFuture<WebSocketResponseMessage> future = SettableFuture.create();
futures.add(future);
return future;
}
});
WebsocketAddress websocketAddress = new WebsocketAddress(account.getNumber(), device.getId());
WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender, storedMessages,
account, device, client);
connection.onDispatchSubscribed(websocketAddress.serialize());
verify(client, times(2)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class));
assertEquals(futures.size(), 2);
WebSocketResponseMessage response = mock(WebSocketResponseMessage.class);
when(response.getStatus()).thenReturn(200);
futures.get(1).set(response);
futures.get(0).setException(new IOException());
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()), eq(Optional.<String>absent()));
verifyNoMoreInteractions(websocketSender);
verifyNoMoreInteractions(pushSender);
connection.onDispatchUnsubscribed(websocketAddress.serialize());
verify(client).close(anyInt(), anyString());
}
private OutgoingMessageEntity createMessage(long id, String sender, long timestamp, boolean receipt, String content) { private OutgoingMessageEntity createMessage(long id, String sender, long timestamp, boolean receipt, String content) {
return new OutgoingMessageEntity(id, receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE, return new OutgoingMessageEntity(id, receipt ? Envelope.Type.RECEIPT_VALUE : Envelope.Type.CIPHERTEXT_VALUE,
null, timestamp, sender, 1, content.getBytes()); null, timestamp, sender, 1, content.getBytes(), null);
// return OutgoingMessageSignal.newBuilder()
// .setSource(sender)
// .setSourceDevice(1)
// .setType(receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE)
// .setTimestamp(timestamp)
// .setMessage(ByteString.copyFrom(content.getBytes()))
// .build();
} }
} }

View File

@@ -1,4 +1,4 @@
{ {
"relay" : "whisper", "token" : "BQVVHxMt5zAFXA",
"token" : "BQVVHxMt5zAFXA" "relay" : "whisper"
} }

View File

@@ -1,5 +0,0 @@
{
"relay" : "whisper",
"supportsSms" : true,
"token" : "BQVVHxMt5zAFXA"
}

View File

@@ -0,0 +1,5 @@
{
"token" : "BQVVHxMt5zAFXA",
"voice" : true,
"relay" : "whisper"
}