Compare commits

...

95 Commits
v0.2 ... v0.24

Author SHA1 Message Date
Moxie Marlinspike
8a2131416d Bump version to 0.24
// FREEBIE
2014-11-27 16:24:27 -08:00
Moxie Marlinspike
2525304215 Account for websocket-resources changes.
// FREEBIE
2014-11-15 09:48:09 -08:00
Moxie Marlinspike
fdb35d4f77 Switch to WebSocket-Resources
// FREEBIE
2014-11-14 17:59:50 -08:00
Moxie Marlinspike
222c7ea641 Support for signature token based account verification. 2014-11-13 14:56:24 -08:00
Moxie Marlinspike
8f2722263f Bump version to 0.23 2014-11-04 19:33:07 -08:00
Moxie Marlinspike
fd662e3401 Add vacuum command.
// FREEBIE
2014-11-04 19:32:35 -08:00
Moxie Marlinspike
bc65461ecb Bump version to 0.22 2014-10-01 15:03:25 -07:00
Moxie Marlinspike
30017371df Reconnect even when Smack thinks it doesn't need to. 2014-10-01 14:07:12 -07:00
Moxie Marlinspike
b944b86bf8 Bump version to 0.21
// FREEBIE
2014-07-30 11:45:45 -07:00
Moxie Marlinspike
6ba8352fa6 Update sample config to include GCM senderId
// FREEBIE
2014-07-30 11:38:23 -07:00
Moxie Marlinspike
aadf76692e Bump version to 0.20
// FREEBIE
2014-07-30 11:36:54 -07:00
Moxie Marlinspike
c9a1386a55 Fix for PubSub channel.
1) Create channels based on numbers rather than DB row ids.

2) Ensure that stored messages are cleared at reregistration
   time.
2014-07-26 20:41:25 -07:00
Moxie Marlinspike
4eb88a3e02 Server side support for delivery receipts. 2014-07-25 15:48:34 -07:00
Moxie Marlinspike
160c0bfe14 Switch from Java serialization to JSON for memcache storage. 2014-07-23 18:02:35 -07:00
Moxie Marlinspike
4cd098af1d Switch to GCM CCS and add support for APN feedback processing. 2014-07-23 18:00:49 -07:00
Moxie Marlinspike
362abd618f Bump version to 0.19
// FREEBIE
2014-07-21 01:20:57 -07:00
Moxie Marlinspike
69de9f6684 Fix stored message retrieval.
// FREEBIE
2014-07-21 01:20:14 -07:00
Moxie Marlinspike
2aa379bf21 Bumping version to 0.18
// FREEBIE
2014-07-17 11:05:38 -07:00
Moxie Marlinspike
820a2f1a63 Break FederationController into V1 and V2 2014-07-16 17:24:01 -07:00
Moxie Marlinspike
6fac7614f5 Allow device to query their currently stored signed prekey. 2014-07-16 14:44:00 -07:00
Moxie Marlinspike
b724ea8d3b Renamed 'device key' to 'signed prekey'. 2014-07-11 10:37:19 -07:00
Moxie Marlinspike
06f80c320d Introduce V2 API for PreKey updates and requests.
1) A /v2/keys controller.

2) Separate wire protocol PreKey POJOs from database PreKey
   objects.

3) Separate wire protocol PreKey submission and response POJOs.

4) Introduce a new update/response JSON format for /v2/keys.
2014-07-10 18:06:45 -07:00
Moxie Marlinspike
d9de015eab Bump version to 0.17 2014-07-10 17:45:11 -07:00
Moxie Marlinspike
dd36c861ba Pipeline directory update redis flow for a 10x speedup. 2014-07-10 17:31:39 -07:00
Moxie Marlinspike
b34e46af93 Bump version to 0.16 2014-06-30 12:18:39 -07:00
Moxie Marlinspike
405802c492 Get JSON metrics response code. 2014-06-30 12:18:16 -07:00
Moxie Marlinspike
e15f3c9d2b By default, dont try to gunzip 2014-06-29 19:48:47 -07:00
Moxie Marlinspike
885af064c9 Support unrecognized properties. 2014-06-29 18:16:43 -07:00
Moxie Marlinspike
40529dc41f Fix JSON reporter. 2014-06-27 19:49:21 -07:00
Moxie Marlinspike
2452f6ef8a Fix dependency conflicts. 2014-06-27 19:48:49 -07:00
Moxie Marlinspike
4c543e6f06 Update websocket close codes to comply with RFC 2014-06-26 16:08:29 -07:00
Moxie Marlinspike
bc5fd5d441 Update sample config 2014-06-26 16:08:29 -07:00
Moxie Marlinspike
7a33cef27e Updated iOS message delivery.
1) Use WebSockets for delivery if a client is connected.

2) If a client isn't connected, write to a redis queue and send
   an APN push.
2014-06-26 16:08:29 -07:00
Moxie Marlinspike
b433b9c879 Upgrade to dropwizard 0.7. 2014-06-26 16:08:29 -07:00
Moxie Marlinspike
5d169c523f Bump version to 0.13 2014-06-25 21:52:07 -07:00
Moxie Marlinspike
98d277368f Final migration step, remove identity_key column from keys table. 2014-06-25 21:51:22 -07:00
Moxie Marlinspike
3bd58bf25e Bumping version to 0.12 2014-06-25 21:27:00 -07:00
Moxie Marlinspike
ba05e577ae Treat account object as authoritative source for identity keys.
Step 3 in migration.
2014-06-25 21:26:25 -07:00
Moxie Marlinspike
4206f6af45 Bumping version to 0.11 2014-06-25 18:55:54 -07:00
Moxie Marlinspike
0c5da1cc47 Schema migration for identity keys. 2014-06-25 18:55:26 -07:00
Moxie Marlinspike
d9bd1c679e Bump version to 0.10 2014-06-25 11:36:12 -07:00
Moxie Marlinspike
437eb8de37 Write identity key into 'account' object.
This is the beginning of a migration to storing one identity
key per account, instead of the braindead duplication we're
doing now.  Part one of a two-part deployment in the schema
migration process.
2014-06-25 11:34:54 -07:00
Moxie Marlinspike
f14c181840 Add host system metrics. 2014-04-12 14:14:18 -07:00
Moxie Marlinspike
d46c9fb157 Bump version to 0.9 2014-04-04 21:14:53 -07:00
Moxie Marlinspike
6913e4dfd2 Add contacts histogram and directory controller test. 2014-04-04 20:19:12 -07:00
Moxie Marlinspike
aea3f299a0 JSON metrics reporting. 2014-03-19 14:31:31 -07:00
Moxie Marlinspike
5667476780 Bump version to 0.7 2014-03-19 10:02:46 -07:00
Moxie Marlinspike
b263f47826 Support for querying PreKey meta-information. 2014-03-18 18:46:00 -07:00
Moxie Marlinspike
21723d6313 Bump version to 0.6 2014-03-06 22:53:43 -08:00
Moxie Marlinspike
a63cdc76b0 Disallow registration from clients registered on another relay. 2014-02-25 17:04:46 -08:00
Moxie Marlinspike
129e372613 Fix for federated message flow to support source IDs. 2014-02-23 18:24:48 -08:00
Moxie Marlinspike
53de38fc06 Directory update bug fix. 2014-02-21 11:34:43 -08:00
Moxie Marlinspike
67e5794722 Support DataDog Reporting. 2014-02-21 09:14:05 -08:00
Moxie Marlinspike
6aaca59020 Add registrationId tests. 2014-02-20 09:44:31 -08:00
Moxie Marlinspike
f4ecb5d7be Add support for "registrationId" session enforcement. 2014-02-20 09:32:42 -08:00
Moxie Marlinspike
35e212a30f Make migration more sane. 2014-02-13 16:56:08 -08:00
Moxie Marlinspike
a6463df5bb Make WebSocket optional, disabled by default. Add tests. 2014-02-12 14:39:45 -08:00
Moxie Marlinspike
a9994ef5aa Fix IncomingMessage requirements. 2014-02-03 11:51:22 -08:00
Moxie Marlinspike
6e0ae70f02 Fixes for some multi-device bugs. 2014-02-02 16:44:02 -08:00
Moxie Marlinspike
a0889130e5 Include device source and destination 2014-02-02 12:10:25 -08:00
Moxie Marlinspike
8e763f62f5 Require exact device id match on message deliver. 2014-01-24 16:44:31 -08:00
Moxie Marlinspike
866f8bf1ef basic websocket test 2014-01-24 16:07:32 -08:00
Moxie Marlinspike
7bb505db4c Refactor WebSocket support to use Redis for pubsub communication. 2014-01-24 12:33:40 -08:00
Moxie Marlinspike
519f982604 Add device limiters 2014-01-19 09:37:55 -08:00
Moxie Marlinspike
2f85cd214e Pass non-success response codes through federated client. 2014-01-19 09:32:45 -08:00
Moxie Marlinspike
74f71fd8a6 Initial multi device support refactoring.
1) Store account data as a json type, which includes all
   devices in a single object.

2) Simplify message delivery logic.

3) Make federated calls a pass through to standard controllers.

4) Simplify key retrieval logic.
2014-01-18 23:45:07 -08:00
Matt Corallo
6f9226dcf9 One query to get set of accounts and long-existing logic error. 2014-01-11 17:40:00 -10:00
Matt Corallo
eedaa8b3f4 Simplify message handling by returning early and throwing out maps 2014-01-11 16:30:37 -10:00
Matt Corallo
7af3c51cc4 FederateionControllerTest 2014-01-11 15:12:18 -10:00
Matt Corallo
d3830a7fd4 Split Account into Device and Account definitions. 2014-01-11 13:59:49 -10:00
Matt Corallo
ce9d3548e4 s/accountCache/deviceCache/g 2014-01-11 10:57:50 -10:00
Matt Corallo
0bd82784a0 Add missing file 2014-01-11 10:56:59 -10:00
Matt Corallo
542bf73a75 Fix some whitespace 2014-01-11 10:56:50 -10:00
Matt Corallo
bd6cf10402 Auto refactor Only: s/Account/Device/g 2014-01-11 10:56:29 -10:00
Matt Corallo
5a837d4481 Fix jersey warning 2014-01-10 22:16:34 -10:00
Matt Corallo
b08eb0df5c Clean up whitespace/copyright/includes + minor test tweak. 2014-01-10 22:16:34 -10:00
Matt Corallo
e39016ad35 Add CORS header to allow any origin.
We don't need CORS protection because we don't use cookies at all
(so a different origin cant exploit cookie saving to steal our
session).
2014-01-10 22:16:34 -10:00
Matt Corallo
8c74ad073b Rework messages API to fail if you miss some deviceIds per number 2014-01-09 15:20:06 -10:00
Matt Corallo
918ef4a7ca s/IterablePair.Pair/Pair/ 2014-01-09 12:15:35 -10:00
Matt Corallo
2473505d4e Make first account "master" for directory purposes 2014-01-09 11:54:48 -10:00
Matt Corallo
591d26981e Remove a DB query for resetting a number. 2014-01-09 11:01:44 -10:00
Matt Corallo
605e88d4bf Remove all differences in url parameters. 2014-01-09 11:01:44 -10:00
Matt Corallo
48fe609d53 Fix logging init 2014-01-09 11:01:44 -10:00
Matt Corallo
a0768e219a Fix account deletion for fetch'd messages 2014-01-09 09:26:44 -10:00
Matt Corallo
40a988c0cd /v1/devices 2014-01-08 17:29:57 -10:00
Matt Corallo
5845d2dedd Move /v2/keys/{number} to /v1/keys/multikeys/{number} 2014-01-08 17:14:01 -10:00
Matt Corallo
cb185a6552 Remove very overzealous protobuf change. 2014-01-08 16:45:37 -10:00
Matt Corallo
2dc5857645 Add PreKeyList hashCode 2014-01-08 16:10:18 -10:00
Matt Corallo
7d8336fd30 Remove useless setter 2014-01-08 16:06:47 -10:00
Matt Corallo
f9d7c1de57 Fix StoredMessages calls (now all api calls have at least been tested on a running server...) 2014-01-08 16:04:03 -10:00
Matt Corallo
648812a267 AUthHeader indentation fix 2014-01-08 15:34:34 -10:00
Matt Corallo
ef1160eda8 New API to support multiple accounts per # (FREEBIE) 2014-01-08 14:46:33 -10:00
Moxie Marlinspike
4cd1082a4a Add BitHub payment slug 2013-12-16 10:14:09 -08:00
Moxie Marlinspike
cae5cf7024 Bump version to 0.3 2013-12-10 16:37:22 -08:00
Moxie Marlinspike
96435648d3 Change SMS/Voice code delivery priorities.
1) Honor a twilio.international configuration boolean that
   specifies whether to use twilio for international destinations.

2) If a nexmo configuration is specified, and Twilio fails to
   deliver, fall back and attempt delivery again with nexmo.
2013-12-10 16:35:25 -08:00
142 changed files with 7735 additions and 1748 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ run.sh
local.yml local.yml
config/production.yml config/production.yml
config/federated.yml config/federated.yml
config/staging.yml
.opsmanage

View File

@@ -30,6 +30,10 @@ whispersystems@lists.riseup.net
https://lists.riseup.net/www/info/whispersystems https://lists.riseup.net/www/info/whispersystems
Current BitHub Payment Per Commit:
=================
![Current Price](https://bithub.herokuapp.com/v1/status/payment/commit)
Cryptography Notice Cryptography Notice
------------ ------------

View File

@@ -3,15 +3,19 @@ twilio:
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 # Optional. If specified, Nexmo will be used for non-US SMS and
# voice verification. # voice verification if twilio.international is false. Otherwise,
# Nexmo, if specified, Nexmo will only be used as a fallback
# for failed Twilio deliveries.
nexmo: nexmo:
apiKey: apiKey:
apiSecret: apiSecret:
number: number:
gcm: gcm:
senderId:
apiKey: apiKey:
# Optional. Only if iOS clients are supported. # Optional. Only if iOS clients are supported.
@@ -53,9 +57,6 @@ graphite:
host: host:
port: port:
http:
shutdownGracePeriod: 0s
database: database:
# the name of your JDBC driver # the name of your JDBC driver
driverClass: org.postgresql.Driver driverClass: org.postgresql.Driver
@@ -72,24 +73,3 @@ database:
# any properties specific to your JDBC driver: # any properties specific to your JDBC driver:
properties: properties:
charSet: UTF-8 charSet: UTF-8
# the maximum amount of time to wait on an empty pool before throwing an exception
maxWaitForConnection: 1s
# the SQL query to run when validating a connection's liveness
validationQuery: "/* MyService Health Check */ SELECT 1"
# the minimum number of connections to keep open
minSize: 8
# the maximum number of connections to keep open
maxSize: 32
# whether or not idle connections should be validated
checkConnectionWhileIdle: false
# how long a connection must be held before it can be validated
checkConnectionHealthWhenIdleFor: 10s
# the maximum lifetime of an idle connection
closeConnectionIfIdleFor: 1 minute

127
pom.xml
View File

@@ -9,24 +9,72 @@
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<version>0.2</version> <version>0.24</version>
<properties>
<dropwizard.version>0.7.0</dropwizard.version>
<jackson.api.version>2.3.3</jackson.api.version>
<commons-codec.version>1.6</commons-codec.version>
</properties>
<dependencies> <dependencies>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-jdbi</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-metrics-graphite</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</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> <dependency>
<groupId>bouncycastle</groupId> <groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId> <artifactId>bcprov-jdk16</artifactId>
<version>140</version> <version>140</version>
</dependency> </dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>2.2.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.google.android.gcm</groupId> <groupId>com.google.android.gcm</groupId>
<artifactId>gcm-server</artifactId> <artifactId>gcm-server</artifactId>
@@ -56,7 +104,7 @@
<dependency> <dependency>
<groupId>com.google.protobuf</groupId> <groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId> <artifactId>protobuf-java</artifactId>
<version>2.4.1</version> <version>2.5.0</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -66,35 +114,10 @@
<type>jar</type> <type>jar</type>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-jdbi</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>0.6.2</version>
</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.1</version> <version>3.4.5</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -102,15 +125,35 @@
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
<version>9.1-901.jdbc4</version> <version>9.1-901.jdbc4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.sun.jersey</groupId> <groupId>org.igniterealtime.smack</groupId>
<artifactId>jersey-json</artifactId> <artifactId>smack-tcp</artifactId>
<version>1.17.1</version> <version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.whispersystems.websocket</groupId>
<artifactId>websocket-resources</artifactId>
<version>0.1-SNAPSHOT</version>
</dependency> </dependency>
</dependencies> </dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.api.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>

View File

@@ -20,10 +20,20 @@ option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos"; option java_outer_classname = "MessageProtos";
message OutgoingMessageSignal { message OutgoingMessageSignal {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
optional uint32 type = 1; optional uint32 type = 1;
optional string source = 2; optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3; optional string relay = 3;
repeated string destinations = 4; // repeated string destinations = 4;
optional uint64 timestamp = 5; optional uint64 timestamp = 5;
optional bytes message = 6; optional bytes message = 6;
} }

View File

@@ -17,22 +17,26 @@
package org.whispersystems.textsecuregcm; package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.yammer.dropwizard.config.Configuration;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration; import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration; import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration; import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration; import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration; import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MetricsConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration; import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration; import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
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.TwilioConfiguration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
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 io.dropwizard.Configuration;
import io.dropwizard.db.DataSourceFactory;
public class WhisperServerConfiguration extends Configuration { public class WhisperServerConfiguration extends Configuration {
@NotNull @NotNull
@@ -44,6 +48,7 @@ public class WhisperServerConfiguration extends Configuration {
private NexmoConfiguration nexmo; private NexmoConfiguration nexmo;
@NotNull @NotNull
@Valid
@JsonProperty @JsonProperty
private GcmConfiguration gcm; private GcmConfiguration gcm;
@@ -72,7 +77,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid @Valid
@NotNull @NotNull
@JsonProperty @JsonProperty
private DatabaseConfiguration database = new DatabaseConfiguration(); private DataSourceFactory database = new DataSourceFactory();
@Valid @Valid
@NotNull @NotNull
@@ -83,6 +88,21 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private GraphiteConfiguration graphite = new GraphiteConfiguration(); private GraphiteConfiguration graphite = new GraphiteConfiguration();
@Valid
@JsonProperty
private MetricsConfiguration viz = new MetricsConfiguration();
@Valid
@JsonProperty
private WebsocketConfiguration websocket = new WebsocketConfiguration();
@JsonProperty
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
public WebsocketConfiguration getWebsocketConfiguration() {
return websocket;
}
public TwilioConfiguration getTwilioConfiguration() { public TwilioConfiguration getTwilioConfiguration() {
return twilio; return twilio;
} }
@@ -111,7 +131,7 @@ public class WhisperServerConfiguration extends Configuration {
return redis; return redis;
} }
public DatabaseConfiguration getDatabaseConfiguration() { public DataSourceFactory getDataSourceFactory() {
return database; return database;
} }
@@ -126,4 +146,12 @@ public class WhisperServerConfiguration extends Configuration {
public GraphiteConfiguration getGraphiteConfiguration() { public GraphiteConfiguration getGraphiteConfiguration() {
return graphite; return graphite;
} }
public MetricsConfiguration getMetricsConfiguration() {
return viz;
}
public RedPhoneConfiguration getRedphoneConfiguration() {
return redphone;
}
} }

View File

@@ -16,52 +16,88 @@
*/ */
package org.whispersystems.textsecuregcm; package org.whispersystems.textsecuregcm;
import com.yammer.dropwizard.Service; import com.codahale.metrics.SharedMetricRegistries;
import com.yammer.dropwizard.config.Bootstrap; import com.codahale.metrics.graphite.GraphiteReporter;
import com.yammer.dropwizard.config.Environment; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.yammer.dropwizard.db.DatabaseConfiguration; import com.google.common.base.Optional;
import com.yammer.dropwizard.jdbi.DBIFactory;
import com.yammer.dropwizard.migrations.MigrationsBundle;
import com.yammer.metrics.reporting.GraphiteReporter;
import net.spy.memcached.MemcachedClient; import net.spy.memcached.MemcachedClient;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.DBI;
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.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.DirectoryController; import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.FederationController; import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysController; import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.controllers.MessageController; import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager; 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.mappers.IOExceptionMapper; import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.JsonMetricsReporter;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck; import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory; import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
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.push.APNSender;
import org.whispersystems.textsecuregcm.push.GCMSender;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.sms.SenderFactory; import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
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.PendingAccounts; import org.whispersystems.textsecuregcm.storage.PendingAccounts;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingDevices;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.UrlSigner; import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.websocket.ConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.DirectoryCommand; import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
import java.security.Security; import java.security.Security;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.Application;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
public class WhisperServerService extends Service<WhisperServerConfiguration> { public class WhisperServerService extends Application<WhisperServerConfiguration> {
static { static {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
@@ -69,25 +105,34 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
@Override @Override
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) { public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
bootstrap.setName("whisper-server");
bootstrap.addCommand(new DirectoryCommand()); bootstrap.addCommand(new DirectoryCommand());
bootstrap.addCommand(new VacuumCommand());
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() { bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
@Override @Override
public DatabaseConfiguration getDatabaseConfiguration(WhisperServerConfiguration configuration) { public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
return configuration.getDatabaseConfiguration(); return configuration.getDataSourceFactory();
} }
}); });
} }
@Override
public String getName() {
return "whisper-server";
}
@Override @Override
public void run(WhisperServerConfiguration config, Environment environment) public void run(WhisperServerConfiguration config, Environment environment)
throws Exception throws Exception
{ {
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
DBIFactory dbiFactory = new DBIFactory(); DBIFactory dbiFactory = new DBIFactory();
DBI jdbi = dbiFactory.build(environment, config.getDatabaseConfiguration(), "postgresql"); DBI jdbi = dbiFactory.build(environment, config.getDataSourceFactory(), "postgresql");
Accounts accounts = jdbi.onDemand(Accounts.class); Accounts accounts = jdbi.onDemand(Accounts.class);
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class); PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class);
Keys keys = jdbi.onDemand(Keys.class); Keys keys = jdbi.onDemand(Keys.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient(); MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
@@ -95,41 +140,109 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
DirectoryManager directory = new DirectoryManager(redisClient); DirectoryManager directory = new DirectoryManager(redisClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient); PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, memcachedClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration()); FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient); StoredMessages storedMessages = new StoredMessages(redisClient);
SenderFactory senderFactory = new SenderFactory(config.getTwilioConfiguration(), config.getNexmoConfiguration()); PubSubManager pubSubManager = new PubSubManager(redisClient);
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
config.getApnConfiguration(),
accountsManager, directory);
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()), APNSender apnSender = new APNSender(accountsManager, pubSubManager, storedMessages, memcachedClient,
FederatedPeer.class, config.getApnConfiguration().getCertificate(),
accountAuthenticator, config.getApnConfiguration().getKey());
Account.class, "WhisperServer"));
environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, senderFactory)); GCMSender gcmSender = new GCMSender(accountsManager,
environment.addResource(new DirectoryController(rateLimiters, directory)); config.getGcmConfiguration().getSenderId(),
environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner)); config.getGcmConfiguration().getApiKey());
environment.addResource(new KeysController(rateLimiters, keys, federatedClientManager));
environment.addResource(new FederationController(keys, accountsManager, pushSender, urlSigner));
environment.addServlet(new MessageController(rateLimiters, accountAuthenticator, WebsocketSender websocketSender = new WebsocketSender(storedMessages, pubSubManager);
pushSender, federatedClientManager),
MessageController.PATH);
environment.addHealthCheck(new RedisHealthCheck(redisClient)); environment.lifecycle().manage(apnSender);
environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient)); environment.lifecycle().manage(gcmSender);
environment.addProvider(new IOExceptionMapper()); AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
environment.addProvider(new RateLimitExceededExceptionMapper()); RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(gcmSender, apnSender, websocketSender);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
FederatedPeer.class,
deviceAuthenticator,
Device.class, "WhisperServer"));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, storedMessages, new TimeProvider(), authorizationKey));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
environment.jersey().register(new ReceiptController(accountsManager, federatedClientManager, pushSender));
environment.jersey().register(attachmentController);
environment.jersey().register(keysControllerV1);
environment.jersey().register(keysControllerV2);
environment.jersey().register(messageController);
if (config.getWebsocketConfiguration().isEnabled()) {
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment);
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
webSocketEnvironment.setConnectListener(new ConnectListener(accountsManager, pushSender, storedMessages, pubSubManager));
WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment);
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet);
websocket.addMapping("/v1/websocket/*");
websocket.setAsyncSupported(true);
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
filter.setInitParameter("allowedOrigins", "*");
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin");
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
filter.setInitParameter("preflightMaxAge", "5184000");
filter.setInitParameter("allowCredentials", "true");
}
environment.healthChecks().register("redis", new RedisHealthCheck(redisClient));
environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
environment.jersey().register(new IOExceptionMapper());
environment.jersey().register(new RateLimitExceededExceptionMapper());
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
if (config.getGraphiteConfiguration().isEnabled()) { if (config.getGraphiteConfiguration().isEnabled()) {
GraphiteReporter.enable(15, TimeUnit.SECONDS, GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory();
config.getGraphiteConfiguration().getHost(), graphiteReporterFactory.setHost(config.getGraphiteConfiguration().getHost());
config.getGraphiteConfiguration().getPort()); graphiteReporterFactory.setPort(config.getGraphiteConfiguration().getPort());
GraphiteReporter graphiteReporter = (GraphiteReporter) graphiteReporterFactory.build(environment.metrics());
graphiteReporter.start(15, TimeUnit.SECONDS);
}
if (config.getMetricsConfiguration().isEnabled()) {
new JsonMetricsReporter(environment.metrics(),
config.getMetricsConfiguration().getToken(),
config.getMetricsConfiguration().getHost())
.start(60, TimeUnit.SECONDS);
}
}
private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) {
if (configuration == null) {
return Optional.absent();
} else {
return Optional.of(new NexmoSmsSender(configuration));
} }
} }

View File

@@ -16,28 +16,27 @@
*/ */
package org.whispersystems.textsecuregcm.auth; package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
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.util.Constants;
import java.util.concurrent.TimeUnit; import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> { public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
private final Meter authenticationFailedMeter = Metrics.newMeter(AccountAuthenticator.class, private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
"authentication", "failed", private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
TimeUnit.MINUTES); private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded"));
private final Meter authenticationSucceededMeter = Metrics.newMeter(AccountAuthenticator.class,
"authentication", "succeeded",
TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class); private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
@@ -51,18 +50,30 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
public Optional<Account> authenticate(BasicCredentials basicCredentials) public Optional<Account> authenticate(BasicCredentials basicCredentials)
throws AuthenticationException throws AuthenticationException
{ {
Optional<Account> account = accountsManager.get(basicCredentials.getUsername()); try {
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber());
if (!account.isPresent()) { if (!account.isPresent()) {
return Optional.absent();
}
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
if (!device.isPresent()) {
return Optional.absent();
}
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
authenticationSucceededMeter.mark();
account.get().setAuthenticatedDevice(device.get());
return account;
}
authenticationFailedMeter.mark();
return Optional.absent();
} catch (InvalidAuthorizationHeaderException iahe) {
return Optional.absent(); return Optional.absent();
} }
if (account.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
authenticationSucceededMeter.mark();
return account;
}
authenticationFailedMeter.mark();
return Optional.absent();
} }
} }

View File

@@ -24,10 +24,28 @@ import java.io.IOException;
public class AuthorizationHeader { public class AuthorizationHeader {
private final String user; private final String number;
private final long accountId;
private final String password; private final String password;
public AuthorizationHeader(String header) throws InvalidAuthorizationHeaderException { private AuthorizationHeader(String number, long accountId, String password) {
this.number = number;
this.accountId = accountId;
this.password = password;
}
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {
try {
String[] numberAndId = user.split("\\.");
return new AuthorizationHeader(numberAndId[0],
numberAndId.length > 1 ? Long.parseLong(numberAndId[1]) : 1,
password);
} catch (NumberFormatException nfe) {
throw new InvalidAuthorizationHeaderException(nfe);
}
}
public static AuthorizationHeader fromFullHeader(String header) throws InvalidAuthorizationHeaderException {
try { try {
if (header == null) { if (header == null) {
throw new InvalidAuthorizationHeaderException("Null header"); throw new InvalidAuthorizationHeaderException("Null header");
@@ -55,16 +73,18 @@ public class AuthorizationHeader {
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues); throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
} }
this.user = credentialParts[0]; return fromUserAndPassword(credentialParts[0], credentialParts[1]);
this.password = credentialParts[1];
} catch (IOException ioe) { } catch (IOException ioe) {
throw new InvalidAuthorizationHeaderException(ioe); throw new InvalidAuthorizationHeaderException(ioe);
} }
} }
public String getUserName() { public String getNumber() {
return user; return number;
}
public long getDeviceId() {
return accountId;
} }
public String getPassword() { public String getPassword() {

View File

@@ -0,0 +1,76 @@
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 AuthorizationToken {
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
private final String token;
private final byte[] key;
public AuthorizationToken(String token, byte[] key) {
this.token = token;
this.key = key;
}
public boolean isValid(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

@@ -16,30 +16,35 @@
*/ */
package org.whispersystems.textsecuregcm.auth; package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
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 java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> { public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> {
private final Meter authenticationFailedMeter = Metrics.newMeter(FederatedPeerAuthenticator.class, private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
"authentication", "failed",
TimeUnit.MINUTES);
private final Meter authenticationSucceededMeter = Metrics.newMeter(FederatedPeerAuthenticator.class, private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(),
"authentication", "succeeded", "authentication",
TimeUnit.MINUTES); "failed"));
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(),
"authentication",
"succeeded"));
private final Logger logger = LoggerFactory.getLogger(FederatedPeerAuthenticator.class); private final Logger logger = LoggerFactory.getLogger(FederatedPeerAuthenticator.class);

View File

@@ -21,17 +21,14 @@ import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope; import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable; import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider; import com.sun.jersey.spi.inject.InjectableProvider;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.dropwizard.auth.Authenticator; import io.dropwizard.auth.Auth;
import com.yammer.dropwizard.auth.basic.BasicAuthProvider; import io.dropwizard.auth.Authenticator;
import com.yammer.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.auth.basic.BasicAuthProvider;
import org.slf4j.Logger; import io.dropwizard.auth.basic.BasicCredentials;
import org.slf4j.LoggerFactory;
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> { public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
private final Logger logger = LoggerFactory.getLogger(MultiBasicAuthProvider.class);
private final BasicAuthProvider<T1> provider1; private final BasicAuthProvider<T1> provider1;
private final BasicAuthProvider<T2> provider2; private final BasicAuthProvider<T2> provider2;
@@ -44,8 +41,8 @@ public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, P
Class<?> clazz2, Class<?> clazz2,
String realm) String realm)
{ {
this.provider1 = new BasicAuthProvider<T1>(authenticator1, realm); this.provider1 = new BasicAuthProvider<>(authenticator1, realm);
this.provider2 = new BasicAuthProvider<T2>(authenticator2, realm); this.provider2 = new BasicAuthProvider<>(authenticator2, realm);
this.clazz1 = clazz1; this.clazz1 = clazz1;
this.clazz2 = clazz2; this.clazz2 = clazz2;
} }

View File

@@ -19,8 +19,14 @@ 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;
public class GcmConfiguration { public class GcmConfiguration {
@NotNull
@JsonProperty
private long senderId;
@NotEmpty @NotEmpty
@JsonProperty @JsonProperty
private String apiKey; private String apiKey;
@@ -28,4 +34,8 @@ public class GcmConfiguration {
public String getApiKey() { public String getApiKey() {
return apiKey; return apiKey;
} }
public long getSenderId() {
return senderId;
}
} }

View File

@@ -0,0 +1,27 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class MetricsConfiguration {
@JsonProperty
private String token;
@JsonProperty
private String host;
@JsonProperty
private boolean enabled = false;
public String getHost() {
return host;
}
public String getToken() {
return token;
}
public boolean isEnabled() {
return enabled && token != null && host != null;
}
}

View File

@@ -41,6 +41,20 @@ public class RateLimitsConfiguration {
@JsonProperty @JsonProperty
private RateLimitConfiguration messages = new RateLimitConfiguration(60, 60); private RateLimitConfiguration messages = new RateLimitConfiguration(60, 60);
@JsonProperty
private RateLimitConfiguration allocateDevice = new RateLimitConfiguration(2, 1.0 / 2.0);
@JsonProperty
private RateLimitConfiguration verifyDevice = new RateLimitConfiguration(2, 2);
public RateLimitConfiguration getAllocateDevice() {
return allocateDevice;
}
public RateLimitConfiguration getVerifyDevice() {
return verifyDevice;
}
public RateLimitConfiguration getMessages() { public RateLimitConfiguration getMessages() {
return messages; return messages;
} }

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Optional;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
public class RedPhoneConfiguration {
@JsonProperty
private String authKey;
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
if (authKey == null || authKey.trim().length() == 0) {
return Optional.absent();
}
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
}
}

View File

@@ -37,6 +37,9 @@ public class TwilioConfiguration {
@JsonProperty @JsonProperty
private String localDomain; private String localDomain;
@JsonProperty
private boolean international;
public String getAccountId() { public String getAccountId() {
return accountId; return accountId;
} }
@@ -52,4 +55,8 @@ public class TwilioConfiguration {
public String getLocalDomain() { public String getLocalDomain() {
return localDomain; return localDomain;
} }
public boolean isInternational() {
return international;
}
} }

View File

@@ -0,0 +1,14 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class WebsocketConfiguration {
@JsonProperty
private boolean enabled = false;
public boolean isEnabled() {
return enabled;
}
}

View File

@@ -16,9 +16,9 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
@@ -27,14 +27,19 @@ 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;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.sms.SenderFactory; import org.whispersystems.textsecuregcm.providers.TimeProvider;
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.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
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 org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import javax.validation.Valid; import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
@@ -53,6 +58,8 @@ import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import io.dropwizard.auth.Auth;
@Path("/v1/accounts") @Path("/v1/accounts")
public class AccountController { public class AccountController {
@@ -61,17 +68,26 @@ public class AccountController {
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 SenderFactory senderFactory; private final SmsSender smsSender;
private final StoredMessages storedMessages;
private final TimeProvider timeProvider;
private final Optional<byte[]> authorizationKey;
public AccountController(PendingAccountsManager pendingAccounts, public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts, AccountsManager accounts,
RateLimiters rateLimiters, RateLimiters rateLimiters,
SenderFactory smsSenderFactory) SmsSender smsSenderFactory,
StoredMessages storedMessages,
TimeProvider timeProvider,
Optional<byte[]> authorizationKey)
{ {
this.pendingAccounts = pendingAccounts; this.pendingAccounts = pendingAccounts;
this.accounts = accounts; this.accounts = accounts;
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.senderFactory = smsSenderFactory; this.smsSender = smsSenderFactory;
this.storedMessages = storedMessages;
this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey;
} }
@Timed @Timed
@@ -94,16 +110,16 @@ public class AccountController {
rateLimiters.getVoiceDestinationLimiter().validate(number); rateLimiters.getVoiceDestinationLimiter().validate(number);
break; break;
default: default:
throw new WebApplicationException(Response.status(415).build()); throw new WebApplicationException(Response.status(422).build());
} }
VerificationCode verificationCode = generateVerificationCode(); VerificationCode verificationCode = generateVerificationCode();
pendingAccounts.store(number, verificationCode.getVerificationCode()); pendingAccounts.store(number, verificationCode.getVerificationCode());
if (transport.equals("sms")) { if (transport.equals("sms")) {
senderFactory.getSmsSender(number).deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay()); smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
} else if (transport.equals("voice")) { } else if (transport.equals("voice")) {
senderFactory.getVoxSender(number).deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech()); smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
} }
return Response.ok().build(); return Response.ok().build();
@@ -119,8 +135,8 @@ public class AccountController {
throws RateLimitExceededException throws RateLimitExceededException
{ {
try { try {
AuthorizationHeader header = new AuthorizationHeader(authorizationHeader); AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getUserName(); String number = header.getNumber();
String password = header.getPassword(); String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number); rateLimiters.getVerifyLimiter().validate(number);
@@ -133,28 +149,59 @@ public class AccountController {
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());
} }
Account account = new Account(); if (accounts.isRelayListed(number)) {
account.setNumber(number); throw new WebApplicationException(Response.status(417).build());
account.setAuthenticationCredentials(new AuthenticationCredentials(password)); }
account.setSignalingKey(accountAttributes.getSignalingKey());
account.setSupportsSms(accountAttributes.getSupportsSms());
accounts.create(account);
logger.debug("Stored account...");
createAccount(number, password, 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
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Path("/token/{verification_token}")
public void verifyToken(@PathParam("verification_token") String verificationToken,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getNumber();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
if (!authorizationKey.isPresent()) {
logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(403).build());
}
AuthorizationToken token = new AuthorizationToken(verificationToken, authorizationKey.get());
if (!token.isValid(number, timeProvider.getCurrentTimeMillis())) {
throw new WebApplicationException(Response.status(403).build());
}
createAccount(number, password, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad authorization header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed @Timed
@PUT @PUT
@Path("/gcm/") @Path("/gcm/")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) { public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
account.setApnRegistrationId(null); Device device = account.getAuthenticatedDevice().get();
account.setGcmRegistrationId(registrationId.getGcmRegistrationId()); device.setApnId(null);
device.setGcmId(registrationId.getGcmRegistrationId());
accounts.update(account); accounts.update(account);
} }
@@ -162,7 +209,8 @@ public class AccountController {
@DELETE @DELETE
@Path("/gcm/") @Path("/gcm/")
public void deleteGcmRegistrationId(@Auth Account account) { public void deleteGcmRegistrationId(@Auth Account account) {
account.setGcmRegistrationId(null); Device device = account.getAuthenticatedDevice().get();
device.setGcmId(null);
accounts.update(account); accounts.update(account);
} }
@@ -171,8 +219,9 @@ public class AccountController {
@Path("/apn/") @Path("/apn/")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) { public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
account.setApnRegistrationId(registrationId.getApnRegistrationId()); Device device = account.getAuthenticatedDevice().get();
account.setGcmRegistrationId(null); device.setApnId(registrationId.getApnRegistrationId());
device.setGcmId(null);
accounts.update(account); accounts.update(account);
} }
@@ -180,7 +229,8 @@ public class AccountController {
@DELETE @DELETE
@Path("/apn/") @Path("/apn/")
public void deleteApnRegistrationId(@Auth Account account) { public void deleteApnRegistrationId(@Auth Account account) {
account.setApnRegistrationId(null); Device device = account.getAuthenticatedDevice().get();
device.setApnId(null);
accounts.update(account); accounts.update(account);
} }
@@ -190,11 +240,30 @@ public class AccountController {
@Produces(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML)
public Response getTwiml(@PathParam("code") String encodedVerificationText) { public Response getTwiml(@PathParam("code") String encodedVerificationText) {
return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML, return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML,
SenderFactory.VoxSender.VERIFICATION_TEXT + encodedVerificationText)).build();
encodedVerificationText)).build();
} }
private VerificationCode generateVerificationCode() { private void createAccount(String number, String password, AccountAttributes accountAttributes) {
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
Account account = new Account();
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
storedMessages.clear(new WebsocketAddress(number, Device.MASTER_ID));
pendingAccounts.remove(number);
logger.debug("Stored device...");
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
try { try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int randomInt = 100000 + random.nextInt(900000); int randomInt = 100000 + random.nextInt(900000);
@@ -203,5 +272,4 @@ public class AccountController {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
} }

View File

@@ -17,8 +17,8 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod; import com.amazonaws.HttpMethod;
import com.yammer.dropwizard.auth.Auth; import com.codahale.metrics.annotation.Timed;
import com.yammer.metrics.annotation.Timed; 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.textsecuregcm.entities.AttachmentDescriptor; import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
@@ -35,14 +35,16 @@ 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.QueryParam;
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.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import io.dropwizard.auth.Auth;
@Path("/v1/attachments") @Path("/v1/attachments")
public class AttachmentController { public class AttachmentController {
@@ -65,37 +67,38 @@ public class AttachmentController {
@Timed @Timed
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response allocateAttachment(@Auth Account account) throws RateLimitExceededException { public AttachmentDescriptor allocateAttachment(@Auth Account account)
rateLimiters.getAttachmentLimiter().validate(account.getNumber()); throws RateLimitExceededException
{
if (account.isRateLimited()) {
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
}
long attachmentId = generateAttachmentId(); long attachmentId = generateAttachmentId();
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT); URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);
AttachmentDescriptor descriptor = new AttachmentDescriptor(attachmentId, url.toExternalForm());
return new AttachmentDescriptor(attachmentId, url.toExternalForm());
return Response.ok().entity(descriptor).build();
} }
@Timed @Timed
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/{attachmentId}") @Path("/{attachmentId}")
public Response redirectToAttachment(@Auth Account account, public AttachmentUri redirectToAttachment(@Auth Account account,
@PathParam("attachmentId") long attachmentId, @PathParam("attachmentId") long attachmentId,
@QueryParam("relay") String relay) @QueryParam("relay") Optional<String> relay)
throws IOException
{ {
try { try {
URL url; if (!relay.isPresent()) {
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET));
if (relay == null) url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET); } else {
else url = federatedClientManager.getClient(relay).getSignedAttachmentUri(attachmentId); return new AttachmentUri(federatedClientManager.getClient(relay.get()).getSignedAttachmentUri(attachmentId));
}
return Response.ok().entity(new AttachmentUri(url)).build();
} catch (IOException e) {
logger.warn("No conectivity", e);
return Response.status(500).build();
} catch (NoSuchPeerException e) { } catch (NoSuchPeerException e) {
logger.info("No such peer: " + relay); logger.info("No such peer: " + relay);
return Response.status(404).build(); throw new WebApplicationException(Response.status(404).build());
} }
} }

View File

@@ -0,0 +1,143 @@
/**
* 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.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import io.dropwizard.auth.Auth;
@Path("/v1/devices")
public class DeviceController {
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
private final PendingDevicesManager pendingDevices;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
public DeviceController(PendingDevicesManager pendingDevices,
AccountsManager accounts,
RateLimiters rateLimiters)
{
this.pendingDevices = pendingDevices;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
}
@Timed
@GET
@Path("/provisioning_code")
@Produces(MediaType.APPLICATION_JSON)
public VerificationCode createDeviceToken(@Auth Account account)
throws RateLimitExceededException
{
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
VerificationCode verificationCode = generateVerificationCode();
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
return verificationCode;
}
@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("/{verification_code}")
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getNumber();
String password = header.getPassword();
rateLimiters.getVerifyDeviceLimiter().validate(number);
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
if (!storedVerificationCode.isPresent() ||
!verificationCode.equals(storedVerificationCode.get()))
{
throw new WebApplicationException(Response.status(403).build());
}
Optional<Account> account = accounts.get(number);
if (!account.isPresent()) {
throw new WebApplicationException(Response.status(403).build());
}
Device device = new Device();
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setId(account.get().getNextDeviceId());
account.get().addDevice(device);
accounts.update(account.get());
pendingDevices.remove(number);
return new DeviceResponse(device.getId());
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
int randomInt = 100000 + random.nextInt(900000);
return new VerificationCode(randomInt);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -16,19 +16,21 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Metered;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens; import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.entities.ClientContacts; import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Constants;
import javax.validation.Valid; import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
@@ -44,10 +46,15 @@ import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.Auth;
@Path("/v1/directory") @Path("/v1/directory")
public class DirectoryController { public class DirectoryController {
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class); private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Histogram contactsHistogram = metricRegistry.histogram(name(getClass(), "contacts"));
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final DirectoryManager directory; private final DirectoryManager directory;
@@ -57,7 +64,7 @@ public class DirectoryController {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
} }
@Timed() @Timed
@GET @GET
@Path("/{token}") @Path("/{token}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@@ -78,7 +85,7 @@ public class DirectoryController {
} }
} }
@Timed() @Timed
@PUT @PUT
@Path("/tokens") @Path("/tokens")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@@ -87,6 +94,7 @@ public class DirectoryController {
throws RateLimitExceededException throws RateLimitExceededException
{ {
rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size()); rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size());
contactsHistogram.update(contacts.getContacts().size());
try { try {
List<byte[]> tokens = new LinkedList<>(); List<byte[]> tokens = new LinkedList<>();

View File

@@ -1,157 +1,19 @@
/**
* 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.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod;
import com.google.protobuf.InvalidProtocolBufferException;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.RelayMessage;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
@Path("/v1/federation")
public class FederationController { public class FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationController.class); protected final AccountsManager accounts;
protected final AttachmentController attachmentController;
protected final MessageController messageController;
private static final int ACCOUNT_CHUNK_SIZE = 10000; public FederationController(AccountsManager accounts,
AttachmentController attachmentController,
private final PushSender pushSender; MessageController messageController)
private final Keys keys;
private final AccountsManager accounts;
private final UrlSigner urlSigner;
public FederationController(Keys keys, AccountsManager accounts, PushSender pushSender, UrlSigner urlSigner) {
this.keys = keys;
this.accounts = accounts;
this.pushSender = pushSender;
this.urlSigner = urlSigner;
}
@Timed
@GET
@Path("/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId)
{ {
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET); this.accounts = accounts;
return new AttachmentUri(url); this.attachmentController = attachmentController;
} this.messageController = messageController;
@Timed
@GET
@Path("/key/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
{
PreKey preKey = keys.get(number);
if (preKey == null) {
throw new WebApplicationException(Response.status(404).build());
}
return preKey;
}
@Timed
@PUT
@Path("/message")
@Consumes(MediaType.APPLICATION_JSON)
public void relayMessage(@Auth FederatedPeer peer, @Valid RelayMessage message)
throws IOException
{
try {
OutgoingMessageSignal signal = OutgoingMessageSignal.parseFrom(message.getOutgoingMessageSignal())
.toBuilder()
.setRelay(peer.getName())
.build();
pushSender.sendMessage(message.getDestination(), signal);
} catch (InvalidProtocolBufferException ipe) {
logger.warn("ProtoBuf", ipe);
throw new WebApplicationException(Response.status(400).build());
} catch (NoSuchUserException e) {
logger.debug("No User", e);
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/user_count")
@Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount());
}
@Timed
@GET
@Path("/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> clientContacts = new LinkedList<>();
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
if (Util.isEmpty(account.getApnRegistrationId()) &&
Util.isEmpty(account.getGcmRegistrationId()))
{
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}
return new ClientContacts(clientContacts);
} }
} }

View File

@@ -0,0 +1,164 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/federation")
public class FederationControllerV1 extends FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationControllerV1.class);
private static final int ACCOUNT_CHUNK_SIZE = 10000;
private final KeysControllerV1 keysControllerV1;
public FederationControllerV1(AccountsManager accounts,
AttachmentController attachmentController,
MessageController messageController,
KeysControllerV1 keysControllerV1)
{
super(accounts, attachmentController, messageController);
this.keysControllerV1 = keysControllerV1;
}
@Timed
@GET
@Path("/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId)
throws IOException
{
return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()),
attachmentId, Optional.<String>absent());
}
@Timed
@GET
@Path("/key/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyV1> getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
throws IOException
{
try {
return keysControllerV1.get(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV1> getKeysV1(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, device, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@PUT
@Path("/messages/{source}/{sourceDeviceId}/{destination}")
public void sendMessages(@Auth FederatedPeer peer,
@PathParam("source") String source,
@PathParam("sourceDeviceId") long sourceDeviceId,
@PathParam("destination") String destination,
@Valid IncomingMessageList messages)
throws IOException
{
try {
messages.setRelay(null);
messageController.sendMessage(new NonLimitedAccount(source, sourceDeviceId, peer.getName()), destination, messages);
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/user_count")
@Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount());
}
@Timed
@GET
@Path("/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> clientContacts = new LinkedList<>();
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
if (!account.isActive()) {
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}
return new ClientContacts(clientContacts);
}
}

View File

@@ -0,0 +1,51 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import io.dropwizard.auth.Auth;
@Path("/v2/federation")
public class FederationControllerV2 extends FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationControllerV2.class);
private final KeysControllerV2 keysControllerV2;
public FederationControllerV2(AccountsManager accounts, AttachmentController attachmentController, MessageController messageController, KeysControllerV2 keysControllerV2) {
super(accounts, attachmentController, messageController);
this.keysControllerV2 = keysControllerV2;
}
@Timed
@GET
@Path("/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV2> getKeysV2(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysControllerV2.getDeviceKeys(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, device, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
}

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2013 Open WhisperSystems * Copyright (C) 2014 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@@ -16,76 +16,100 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.yammer.dropwizard.auth.Auth; import com.codahale.metrics.annotation.Timed;
import com.yammer.metrics.annotation.Timed; import com.google.common.base.Optional;
import org.slf4j.Logger; import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyList;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
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.Device;
import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys; import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.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.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/keys")
public class KeysController { public class KeysController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class); protected final RateLimiters rateLimiters;
protected final Keys keys;
protected final AccountsManager accounts;
protected final FederatedClientManager federatedClientManager;
private final RateLimiters rateLimiters; public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
private final Keys keys;
private final FederatedClientManager federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys,
FederatedClientManager federatedClientManager) FederatedClientManager federatedClientManager)
{ {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.keys = keys; this.keys = keys;
this.accounts = accounts;
this.federatedClientManager = federatedClientManager; this.federatedClientManager = federatedClientManager;
} }
@Timed @Timed
@PUT @GET
@Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) { public PreKeyCount getStatus(@Auth Account account) {
keys.store(account.getNumber(), preKeys.getLastResortKey(), preKeys.getKeys()); int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
if (count > 0) {
count = count - 1;
}
return new PreKeyCount(count);
} }
@Timed protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
@GET throws NoSuchUserException
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") String relay)
throws RateLimitExceededException
{ {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number); Optional<Account> destination = accounts.get(number);
if (!destination.isPresent() || !destination.get().isActive()) {
throw new NoSuchUserException("Target account is inactive");
}
try { try {
PreKey key; if (deviceIdSelector.equals("*")) {
Optional<List<KeyRecord>> preKeys = keys.get(number);
return new TargetKeys(destination.get(), preKeys);
}
if (relay == null) key = keys.get(number); long deviceId = Long.parseLong(deviceIdSelector);
else key = federatedClientManager.getClient(relay).getKey(number); Optional<Device> targetDevice = destination.get().getDevice(deviceId);
if (key == null) throw new WebApplicationException(Response.status(404).build()); if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
else return key; throw new NoSuchUserException("Target device is inactive.");
} catch (NoSuchPeerException e) { }
logger.info("No peer: " + relay);
throw new WebApplicationException(Response.status(404).build()); Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
return new TargetKeys(destination.get(), preKeys);
} catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build());
}
}
public static class TargetKeys {
private final Account destination;
private final Optional<List<KeyRecord>> keys;
public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
this.destination = destination;
this.keys = keys;
}
public Optional<List<KeyRecord>> getKeys() {
return keys;
}
public Account getDestination() {
return destination;
} }
} }

View File

@@ -0,0 +1,136 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/keys")
public class KeysControllerV1 extends KeysController {
private final Logger logger = LoggerFactory.getLogger(KeysControllerV1.class);
public KeysControllerV1(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accounts, federatedClientManager);
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyStateV1 preKeys) {
Device device = account.getAuthenticatedDevice().get();
String identityKey = preKeys.getLastResortKey().getIdentityKey();
if (!identityKey.equals(account.getIdentityKey())) {
account.setIdentityKey(identityKey);
accounts.update(account);
}
keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey());
}
@Timed
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV1> getDeviceKey(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
if (relay.isPresent()) {
return federatedClientManager.getClient(relay.get()).getKeysV1(number, deviceId);
}
TargetKeys targetKeys = getLocalKeys(number, deviceId);
if (!targetKeys.getKeys().isPresent()) {
return Optional.absent();
}
List<PreKeyV1> preKeys = new LinkedList<>();
Account destination = targetKeys.getDestination();
for (KeyRecord record : targetKeys.getKeys().get()) {
Optional<Device> device = destination.getDevice(record.getDeviceId());
if (device.isPresent() && device.get().isActive()) {
preKeys.add(new PreKeyV1(record.getDeviceId(), record.getKeyId(),
record.getPublicKey(), destination.getIdentityKey(),
device.get().getRegistrationId()));
}
}
if (preKeys.isEmpty()) return Optional.absent();
else return Optional.of(new PreKeyResponseV1(preKeys));
} catch (NoSuchPeerException | NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyV1> get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
Optional<PreKeyResponseV1> results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay);
if (results.isPresent()) return Optional.of(results.get().getKeys().get(0));
else return Optional.absent();
}
}

View File

@@ -0,0 +1,156 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItemV2;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV2;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v2/keys")
public class KeysControllerV2 extends KeysController {
public KeysControllerV2(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accounts, federatedClientManager);
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyStateV2 preKeys) {
Device device = account.getAuthenticatedDevice().get();
boolean updateAccount = false;
if (!preKeys.getSignedPreKey().equals(device.getSignedPreKey())) {
device.setSignedPreKey(preKeys.getSignedPreKey());
updateAccount = true;
}
if (!preKeys.getIdentityKey().equals(account.getIdentityKey())) {
account.setIdentityKey(preKeys.getIdentityKey());
updateAccount = true;
}
if (updateAccount) {
accounts.update(account);
}
keys.store(account.getNumber(), device.getId(), preKeys.getPreKeys(), preKeys.getLastResortKey());
}
@Timed
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV2> getDeviceKeys(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
if (relay.isPresent()) {
return federatedClientManager.getClient(relay.get()).getKeysV2(number, deviceId);
}
TargetKeys targetKeys = getLocalKeys(number, deviceId);
Account destination = targetKeys.getDestination();
List<PreKeyResponseItemV2> devices = new LinkedList<>();
for (Device device : destination.getDevices()) {
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
SignedPreKey signedPreKey = device.getSignedPreKey();
PreKeyV2 preKey = null;
if (targetKeys.getKeys().isPresent()) {
for (KeyRecord keyRecord : targetKeys.getKeys().get()) {
if (keyRecord.getDeviceId() == device.getId()) {
preKey = new PreKeyV2(keyRecord.getKeyId(), keyRecord.getPublicKey());
}
}
}
if (signedPreKey != null || preKey != null) {
devices.add(new PreKeyResponseItemV2(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
}
}
}
if (devices.isEmpty()) return Optional.absent();
else return Optional.of(new PreKeyResponseV2(destination.getIdentityKey(), devices));
} catch (NoSuchPeerException | NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@PUT
@Path("/signed")
@Consumes(MediaType.APPLICATION_JSON)
public void setSignedKey(@Auth Account account, @Valid SignedPreKey signedPreKey) {
Device device = account.getAuthenticatedDevice().get();
device.setSignedPreKey(signedPreKey);
accounts.update(account);
}
@Timed
@GET
@Path("/signed")
@Produces(MediaType.APPLICATION_JSON)
public Optional<SignedPreKey> getSignedKey(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
SignedPreKey signedPreKey = device.getSignedPreKey();
if (signedPreKey != null) return Optional.of(signedPreKey);
else return Optional.absent();
}
}

View File

@@ -16,298 +16,274 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.fasterxml.jackson.core.JsonProcessingException; import com.codahale.metrics.annotation.Timed;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.core.TimerContext;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
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.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.MessageResponse; import org.whispersystems.textsecuregcm.entities.MessageResponse;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.federation.FederatedClient; import org.whispersystems.textsecuregcm.federation.FederatedClient;
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.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
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 org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.IterablePair.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import javax.servlet.AsyncContext; import javax.validation.Valid;
import javax.servlet.http.HttpServlet; import javax.ws.rs.Consumes;
import javax.servlet.http.HttpServletRequest; import javax.ws.rs.POST;
import javax.servlet.http.HttpServletResponse; import javax.ws.rs.PUT;
import java.io.BufferedReader; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MessageController extends HttpServlet { import io.dropwizard.auth.Auth;
public static final String PATH = "/v1/messages/"; @Path("/v1/messages")
public class MessageController {
private final Meter successMeter = Metrics.newMeter(MessageController.class, "deliver_message", "success", TimeUnit.MINUTES); private final Logger logger = LoggerFactory.getLogger(MessageController.class);
private final Meter failureMeter = Metrics.newMeter(MessageController.class, "deliver_message", "failure", TimeUnit.MINUTES);
private final Timer timer = Metrics.newTimer(MessageController.class, "deliver_message_time", TimeUnit.MILLISECONDS, TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final AccountAuthenticator accountAuthenticator;
private final PushSender pushSender; private final PushSender pushSender;
private final FederatedClientManager federatedClientManager; private final FederatedClientManager federatedClientManager;
private final ObjectMapper objectMapper; private final AccountsManager accountsManager;
private final ExecutorService executor;
public MessageController(RateLimiters rateLimiters, public MessageController(RateLimiters rateLimiters,
AccountAuthenticator accountAuthenticator,
PushSender pushSender, PushSender pushSender,
AccountsManager accountsManager,
FederatedClientManager federatedClientManager) FederatedClientManager federatedClientManager)
{ {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.accountAuthenticator = accountAuthenticator;
this.pushSender = pushSender; this.pushSender = pushSender;
this.accountsManager = accountsManager;
this.federatedClientManager = federatedClientManager; this.federatedClientManager = federatedClientManager;
this.objectMapper = new ObjectMapper();
this.executor = Executors.newFixedThreadPool(10);
} }
@Override @Timed
protected void doPost(HttpServletRequest req, HttpServletResponse resp) { @Path("/{destination}")
TimerContext timerContext = timer.time(); @PUT
@Consumes(MediaType.APPLICATION_JSON)
public void sendMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException
{
rateLimiters.getMessagesLimiter().validate(source.getNumber());
try { try {
Account sender = authenticate(req); if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages);
IncomingMessageList messages = parseIncomingMessages(req); else sendRelayMessage(source, destinationName, messages);
} catch (NoSuchUserException e) {
rateLimiters.getMessagesLimiter().validate(sender.getNumber()); throw new WebApplicationException(Response.status(404).build());
} catch (MismatchedDevicesException e) {
List<IncomingMessage> incomingMessages = messages.getMessages(); throw new WebApplicationException(Response.status(409)
List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(), .type(MediaType.APPLICATION_JSON_TYPE)
incomingMessages); .entity(new MismatchedDevices(e.getMissingDevices(),
e.getExtraDevices()))
IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages, .build());
outgoingMessages); } catch (StaleDevicesException e) {
throw new WebApplicationException(Response.status(410)
handleAsyncDelivery(timerContext, req.startAsync(), listPair); .type(MediaType.APPLICATION_JSON)
} catch (AuthenticationException e) { .entity(new StaleDevices(e.getStaleDevices()))
failureMeter.mark(); .build());
timerContext.stop();
resp.setStatus(401);
} catch (ValidationException e) {
failureMeter.mark();
timerContext.stop();
resp.setStatus(415);
} catch (IOException e) {
logger.warn("IOE", e);
failureMeter.mark();
timerContext.stop();
resp.setStatus(501);
} catch (RateLimitExceededException e) {
timerContext.stop();
failureMeter.mark();
resp.setStatus(413);
} }
} }
private void handleAsyncDelivery(final TimerContext timerContext, @Timed
final AsyncContext context, @POST
final IterablePair<IncomingMessage, OutgoingMessageSignal> listPair) @Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public MessageResponse sendMessageLegacy(@Auth Account source, @Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException
{ {
executor.submit(new Runnable() { try {
@Override List<IncomingMessage> incomingMessages = messages.getMessages();
public void run() { validateLegacyDestinations(incomingMessages);
List<String> success = new LinkedList<>();
List<String> failure = new LinkedList<>();
HttpServletResponse response = (HttpServletResponse) context.getResponse();
try { messages.setRelay(incomingMessages.get(0).getRelay());
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) { sendMessage(source, incomingMessages.get(0).getDestination(), messages);
String destination = messagePair.first().getDestination();
String relay = messagePair.first().getRelay();
try { return new MessageResponse(new LinkedList<String>(), new LinkedList<String>());
if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second()); } catch (ValidationException e) {
else sendRelayMessage(relay, destination, messagePair.second()); throw new WebApplicationException(Response.status(422).build());
success.add(destination); }
} catch (NoSuchUserException e) { }
logger.debug("No such user", e);
failure.add(destination);
}
}
byte[] responseData = serializeResponse(new MessageResponse(success, failure)); private void sendLocalMessage(Account source,
response.setContentLength(responseData.length); String destinationName,
response.getOutputStream().write(responseData); IncomingMessageList messages)
context.complete(); throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
successMeter.mark(); {
} catch (IOException e) { Account destination = getDestinationAccount(destinationName);
logger.warn("Async Handler", e);
failureMeter.mark();
response.setStatus(501);
context.complete();
}
timerContext.stop(); validateCompleteDeviceList(destination, messages.getMessages());
validateRegistrationIds(destination, messages.getMessages());
for (IncomingMessage incomingMessage : messages.getMessages()) {
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
if (destinationDevice.isPresent()) {
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
} }
}); }
} }
private void sendLocalMessage(String destination, OutgoingMessageSignal outgoingMessage) private void sendLocalMessage(Account source,
throws IOException, NoSuchUserException Account destinationAccount,
Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage)
throws NoSuchUserException, IOException
{ {
pushSender.sendMessage(destination, outgoingMessage); try {
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
OutgoingMessageSignal.Builder messageBuilder = OutgoingMessageSignal.newBuilder();
messageBuilder.setType(incomingMessage.getType())
.setSource(source.getNumber())
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
if (messageBody.isPresent()) {
messageBuilder.setMessage(ByteString.copyFrom(messageBody.get()));
}
if (source.getRelay().isPresent()) {
messageBuilder.setRelay(source.getRelay().get());
}
pushSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build());
} catch (NotPushRegisteredException e) {
if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
else logger.debug("Not registered", e);
} catch (TransientPushFailureException e) {
if (destinationDevice.isMaster()) throw new IOException(e);
else logger.debug("Transient failure", e);
}
} }
private void sendRelayMessage(String relay, String destination, OutgoingMessageSignal outgoingMessage) private void sendRelayMessage(Account source,
String destinationName,
IncomingMessageList messages)
throws IOException, NoSuchUserException throws IOException, NoSuchUserException
{ {
try { try {
FederatedClient client = federatedClientManager.getClient(relay); FederatedClient client = federatedClientManager.getClient(messages.getRelay());
client.sendMessage(destination, outgoingMessage); client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
destinationName, messages);
} catch (NoSuchPeerException e) { } catch (NoSuchPeerException e) {
logger.info("No such peer", e);
throw new NoSuchUserException(e); throw new NoSuchUserException(e);
} }
} }
private List<OutgoingMessageSignal> getOutgoingMessageSignals(String number, private Account getDestinationAccount(String destination)
List<IncomingMessage> incomingMessages) throws NoSuchUserException
{ {
List<OutgoingMessageSignal> outgoingMessages = new LinkedList<>(); Optional<Account> account = accountsManager.get(destination);
for (IncomingMessage incoming : incomingMessages) { if (!account.isPresent() || !account.get().isActive()) {
OutgoingMessageSignal.Builder outgoingMessage = OutgoingMessageSignal.newBuilder(); throw new NoSuchUserException(destination);
outgoingMessage.setType(incoming.getType()); }
outgoingMessage.setSource(number);
byte[] messageBody = getMessageBody(incoming); return account.get();
}
if (messageBody != null) { private void validateRegistrationIds(Account account, List<IncomingMessage> messages)
outgoingMessage.setMessage(ByteString.copyFrom(messageBody)); throws StaleDevicesException
{
List<Long> staleDevices = new LinkedList<>();
for (IncomingMessage message : messages) {
Optional<Device> device = account.getDevice(message.getDestinationDeviceId());
if (device.isPresent() &&
message.getDestinationRegistrationId() > 0 &&
message.getDestinationRegistrationId() != device.get().getRegistrationId())
{
staleDevices.add(device.get().getId());
} }
}
outgoingMessage.setTimestamp(System.currentTimeMillis()); if (!staleDevices.isEmpty()) {
throw new StaleDevicesException(staleDevices);
}
}
int index = 0; private void validateCompleteDeviceList(Account account, List<IncomingMessage> messages)
throws MismatchedDevicesException
{
Set<Long> messageDeviceIds = new HashSet<>();
Set<Long> accountDeviceIds = new HashSet<>();
for (IncomingMessage sub : incomingMessages) { List<Long> missingDeviceIds = new LinkedList<>();
if (sub != incoming) { List<Long> extraDeviceIds = new LinkedList<>();
outgoingMessage.setDestinations(index++, sub.getDestination());
for (IncomingMessage message : messages) {
messageDeviceIds.add(message.getDestinationDeviceId());
}
for (Device device : account.getDevices()) {
if (device.isActive()) {
accountDeviceIds.add(device.getId());
if (!messageDeviceIds.contains(device.getId())) {
missingDeviceIds.add(device.getId());
} }
} }
outgoingMessages.add(outgoingMessage.build());
} }
return outgoingMessages; for (IncomingMessage message : messages) {
} if (!accountDeviceIds.contains(message.getDestinationDeviceId())) {
extraDeviceIds.add(message.getDestinationDeviceId());
}
}
private byte[] getMessageBody(IncomingMessage message) { if (!missingDeviceIds.isEmpty() || !extraDeviceIds.isEmpty()) {
try { throw new MismatchedDevicesException(missingDeviceIds, extraDeviceIds);
return Base64.decode(message.getBody());
} catch (IOException ioe) {
ioe.printStackTrace();
return null;
} }
} }
private byte[] serializeResponse(MessageResponse response) throws IOException { private void validateLegacyDestinations(List<IncomingMessage> messages)
try { throws ValidationException
return objectMapper.writeValueAsBytes(response);
} catch (JsonProcessingException e) {
throw new IOException(e);
}
}
private IncomingMessageList parseIncomingMessages(HttpServletRequest request)
throws IOException, ValidationException
{ {
BufferedReader reader = request.getReader(); String destination = null;
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) { for (IncomingMessage message : messages) {
content.append(line); if ((message.getDestination() == null) ||
(destination != null && !destination.equals(message.getDestination())))
{
throw new ValidationException("Multiple account destinations!");
}
destination = message.getDestination();
} }
IncomingMessageList messages = objectMapper.readValue(content.toString(),
IncomingMessageList.class);
if (messages.getMessages() == null) {
throw new ValidationException();
}
for (IncomingMessage message : messages.getMessages()) {
if (message.getBody() == null) throw new ValidationException();
if (message.getDestination() == null) throw new ValidationException();
}
return messages;
} }
private Account authenticate(HttpServletRequest request) throws AuthenticationException { private Optional<byte[]> getMessageBody(IncomingMessage message) {
try { try {
AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization")); return Optional.of(Base64.decode(message.getBody()));
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(), } catch (IOException ioe) {
authorizationHeader.getPassword() ); logger.debug("Bad B64", ioe);
return Optional.absent();
Optional<Account> account = accountAuthenticator.authenticate(credentials);
if (account.isPresent()) return account.get();
else throw new AuthenticationException("Bad credentials");
} catch (InvalidAuthorizationHeaderException e) {
throw new AuthenticationException(e);
} }
} }
// @Timed
// @POST
// @Consumes(MediaType.APPLICATION_JSON)
// @Produces(MediaType.APPLICATION_JSON)
// public MessageResponse sendMessage(@Auth Account sender, IncomingMessageList messages)
// throws IOException
// {
// List<String> success = new LinkedList<>();
// List<String> failure = new LinkedList<>();
// List<IncomingMessage> incomingMessages = messages.getMessages();
// List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(), incomingMessages);
//
// IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages, outgoingMessages);
//
// for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
// String destination = messagePair.first().getDestination();
// String relay = messagePair.first().getRelay();
//
// try {
// if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
// else sendRelayMessage(relay, destination, messagePair.second());
// success.add(destination);
// } catch (NoSuchUserException e) {
// logger.debug("No such user", e);
// failure.add(destination);
// }
// }
//
// return new MessageResponse(success, failure);
// }
} }

View File

@@ -0,0 +1,22 @@
package org.whispersystems.textsecuregcm.controllers;
import java.util.List;
public class MismatchedDevicesException extends Exception {
private final List<Long> missingDevices;
private final List<Long> extraDevices;
public MismatchedDevicesException(List<Long> missingDevices, List<Long> extraDevices) {
this.missingDevices = missingDevices;
this.extraDevices = extraDevices;
}
public List<Long> getMissingDevices() {
return missingDevices;
}
public List<Long> getExtraDevices() {
return extraDevices;
}
}

View File

@@ -16,8 +16,6 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -27,7 +25,7 @@ public class NoSuchUserException extends Exception {
public NoSuchUserException(String user) { public NoSuchUserException(String user) {
super(user); super(user);
missing = new LinkedList<String>(); missing = new LinkedList<>();
missing.add(user); missing.add(user);
} }

View File

@@ -0,0 +1,108 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
import io.dropwizard.auth.Auth;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
@Path("/v1/receipt")
public class ReceiptController {
private final AccountsManager accountManager;
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
public ReceiptController(AccountsManager accountManager,
FederatedClientManager federatedClientManager,
PushSender pushSender)
{
this.accountManager = accountManager;
this.federatedClientManager = federatedClientManager;
this.pushSender = pushSender;
}
@Timed
@PUT
@Path("/{destination}/{messageId}")
public void sendDeliveryReceipt(@Auth Account source,
@PathParam("destination") String destination,
@PathParam("messageId") long messageId,
@QueryParam("relay") Optional<String> relay)
throws IOException
{
try {
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
else sendDirectReceipt(source, destination, messageId);
} catch (NoSuchUserException | NotPushRegisteredException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
} catch (TransientPushFailureException e) {
throw new IOException(e);
}
}
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
throws NoSuchUserException, IOException
{
try {
federatedClientManager.getClient(relay)
.sendDeliveryReceipt(source.getNumber(),
source.getAuthenticatedDevice().get().getId(),
destination, messageId);
} catch (NoSuchPeerException e) {
throw new NoSuchUserException(e);
}
}
private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{
Account destinationAccount = getDestinationAccount(destination);
List<Device> destinationDevices = destinationAccount.getDevices();
OutgoingMessageSignal.Builder message =
OutgoingMessageSignal.newBuilder()
.setSource(source.getNumber())
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
.setTimestamp(messageId)
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
if (source.getRelay().isPresent()) {
message.setRelay(source.getRelay().get());
}
for (Device destinationDevice : destinationDevices) {
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
}
}
private Account getDestinationAccount(String destination)
throws NoSuchUserException
{
Optional<Account> account = accountManager.get(destination);
if (!account.isPresent()) {
throw new NoSuchUserException(destination);
}
return account.get();
}
}

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecuregcm.controllers;
import java.util.List;
public class StaleDevicesException extends Throwable {
private final List<Long> staleDevices;
public StaleDevicesException(List<Long> staleDevices) {
this.staleDevices = staleDevices;
}
public List<Long> getStaleDevices() {
return staleDevices;
}
}

View File

@@ -18,4 +18,7 @@ package org.whispersystems.textsecuregcm.controllers;
public class ValidationException extends Exception { public class ValidationException extends Exception {
public ValidationException(String s) {
super(s);
}
} }

View File

@@ -28,11 +28,19 @@ public class AccountAttributes {
@JsonProperty @JsonProperty
private boolean supportsSms; private boolean supportsSms;
@JsonProperty
private boolean fetchesMessages;
@JsonProperty
private int registrationId;
public AccountAttributes() {} public AccountAttributes() {}
public AccountAttributes(String signalingKey, boolean supportsSms) { public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages, int registrationId) {
this.signalingKey = signalingKey; this.signalingKey = signalingKey;
this.supportsSms = supportsSms; this.supportsSms = supportsSms;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
} }
public String getSignalingKey() { public String getSignalingKey() {
@@ -43,4 +51,11 @@ public class AccountAttributes {
return supportsSms; return supportsSms;
} }
public boolean getFetchesMessages() {
return fetchesMessages;
}
public int getRegistrationId() {
return registrationId;
}
} }

View File

@@ -0,0 +1,23 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class AcknowledgeWebsocketMessage extends IncomingWebsocketMessage {
@JsonProperty
private long id;
public AcknowledgeWebsocketMessage() {}
public AcknowledgeWebsocketMessage(long id) {
this.type = TYPE_ACKNOWLEDGE_MESSAGE;
this.id = id;
}
public long getId() {
return id;
}
}

View File

@@ -30,4 +30,11 @@ public class ClientContactTokens {
public List<String> getContacts() { public List<String> getContacts() {
return contacts; return contacts;
} }
public ClientContactTokens() {}
public ClientContactTokens(List<String> contacts) {
this.contacts = contacts;
}
} }

View File

@@ -0,0 +1,13 @@
package org.whispersystems.textsecuregcm.entities;
public class CryptoEncodingException extends Exception {
public CryptoEncodingException(String s) {
super(s);
}
public CryptoEncodingException(Exception e) {
super(e);
}
}

View File

@@ -0,0 +1,21 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
public class DeviceResponse {
@JsonProperty
private long deviceId;
@VisibleForTesting
public DeviceResponse() {}
public DeviceResponse(long deviceId) {
this.deviceId = deviceId;
}
public long getDeviceId() {
return deviceId;
}
}

View File

@@ -41,27 +41,26 @@ public class EncryptedOutgoingMessage {
private static final int MAC_KEY_SIZE = 20; private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10; private static final int MAC_SIZE = 10;
private final OutgoingMessageSignal outgoingMessage; private final String serialized;
private final String signalingKey;
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage, public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
String signalingKey) String signalingKey)
throws CryptoEncodingException
{ {
this.outgoingMessage = outgoingMessage;
this.signalingKey = signalingKey;
}
public String serialize() throws IOException {
byte[] plaintext = outgoingMessage.toByteArray(); byte[] plaintext = outgoingMessage.toByteArray();
SecretKeySpec cipherKey = getCipherKey (signalingKey); SecretKeySpec cipherKey = getCipherKey (signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey); SecretKeySpec macKey = getMacKey(signalingKey);
byte[] ciphertext = getCiphertext(plaintext, cipherKey, macKey); byte[] ciphertext = getCiphertext(plaintext, cipherKey, macKey);
return Base64.encodeBytes(ciphertext); this.serialized = Base64.encodeBytes(ciphertext);
}
public String serialize() {
return serialized;
} }
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey) private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
throws IOException throws CryptoEncodingException
{ {
try { try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
@@ -85,31 +84,39 @@ public class EncryptedOutgoingMessage {
throw new AssertionError(e); throw new AssertionError(e);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
logger.warn("Invalid Key", e); logger.warn("Invalid Key", e);
throw new IOException("Invalid key!"); throw new CryptoEncodingException("Invalid key!");
} }
} }
private SecretKeySpec getCipherKey(String signalingKey) throws IOException { private SecretKeySpec getCipherKey(String signalingKey) throws CryptoEncodingException {
byte[] signalingKeyBytes = Base64.decode(signalingKey); try {
byte[] cipherKey = new byte[CIPHER_KEY_SIZE]; byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
if (signalingKeyBytes.length < CIPHER_KEY_SIZE) if (signalingKeyBytes.length < CIPHER_KEY_SIZE)
throw new IOException("Signaling key too short!"); throw new CryptoEncodingException("Signaling key too short!");
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length); System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
return new SecretKeySpec(cipherKey, "AES"); return new SecretKeySpec(cipherKey, "AES");
} catch (IOException e) {
throw new CryptoEncodingException(e);
}
} }
private SecretKeySpec getMacKey(String signalingKey) throws IOException { private SecretKeySpec getMacKey(String signalingKey) throws CryptoEncodingException {
byte[] signalingKeyBytes = Base64.decode(signalingKey); try {
byte[] macKey = new byte[MAC_KEY_SIZE]; byte[] signalingKeyBytes = Base64.decode(signalingKey);
byte[] macKey = new byte[MAC_KEY_SIZE];
if (signalingKeyBytes.length < CIPHER_KEY_SIZE + MAC_KEY_SIZE) if (signalingKeyBytes.length < CIPHER_KEY_SIZE + MAC_KEY_SIZE)
throw new IOException(("Signaling key too short!")); throw new CryptoEncodingException("Signaling key too short!");
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length); System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
return new SecretKeySpec(macKey, "HmacSHA256"); return new SecretKeySpec(macKey, "HmacSHA256");
} catch (IOException e) {
throw new CryptoEncodingException(e);
}
} }
} }

View File

@@ -25,9 +25,14 @@ public class IncomingMessage {
private int type; private int type;
@JsonProperty @JsonProperty
@NotEmpty
private String destination; private String destination;
@JsonProperty
private long destinationDeviceId = 1;
@JsonProperty
private int destinationRegistrationId;
@JsonProperty @JsonProperty
@NotEmpty @NotEmpty
private String body; private String body;
@@ -36,7 +41,8 @@ public class IncomingMessage {
private String relay; private String relay;
@JsonProperty @JsonProperty
private long timestamp; private long timestamp; // deprecated
public String getDestination() { public String getDestination() {
return destination; return destination;
@@ -53,4 +59,12 @@ public class IncomingMessage {
public String getRelay() { public String getRelay() {
return relay; return relay;
} }
public long getDestinationDeviceId() {
return destinationDeviceId;
}
public int getDestinationRegistrationId() {
return destinationRegistrationId;
}
} }

View File

@@ -29,9 +29,27 @@ public class IncomingMessageList {
@Valid @Valid
private List<IncomingMessage> messages; private List<IncomingMessage> messages;
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
public IncomingMessageList() {} public IncomingMessageList() {}
public List<IncomingMessage> getMessages() { public List<IncomingMessage> getMessages() {
return messages; return messages;
} }
public String getRelay() {
return relay;
}
public void setRelay(String relay) {
this.relay = relay;
}
public long getTimestamp() {
return timestamp;
}
} }

View File

@@ -0,0 +1,25 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class IncomingWebsocketMessage {
public static final int TYPE_ACKNOWLEDGE_MESSAGE = 1;
public static final int TYPE_PING_MESSAGE = 2;
public static final int TYPE_PONG_MESSAGE = 3;
@JsonProperty
protected int type;
public IncomingWebsocketMessage() {}
public IncomingWebsocketMessage(int type) {
this.type = type;
}
public int getType() {
return type;
}
}

View File

@@ -16,15 +16,26 @@
*/ */
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set;
public class MessageResponse { public class MessageResponse {
private List<String> success; private List<String> success;
private List<String> failure; private List<String> failure;
private Set<String> missingDeviceIds;
public MessageResponse(List<String> success, List<String> failure) { public MessageResponse(List<String> success, List<String> failure) {
this.success = success; this.success = success;
this.failure = failure; this.failure = failure;
this.missingDeviceIds = new HashSet<>();
}
public MessageResponse(Set<String> missingDeviceIds) {
this.success = new LinkedList<>();
this.failure = new LinkedList<>(missingDeviceIds);
this.missingDeviceIds = missingDeviceIds;
} }
public MessageResponse() {} public MessageResponse() {}
@@ -33,8 +44,23 @@ public class MessageResponse {
return success; return success;
} }
public void setSuccess(List<String> success) {
this.success = success;
}
public List<String> getFailure() { public List<String> getFailure() {
return failure; return failure;
} }
public void setFailure(List<String> failure) {
this.failure = failure;
}
public Set<String> getNumbersMissingDevices() {
return missingDeviceIds;
}
public void setNumbersMissingDevices(Set<String> numbersMissingDevices) {
this.missingDeviceIds = numbersMissingDevices;
}
} }

View File

@@ -0,0 +1,24 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
public class MismatchedDevices {
@JsonProperty
public List<Long> missingDevices;
@JsonProperty
public List<Long> extraDevices;
@VisibleForTesting
public MismatchedDevices() {}
public MismatchedDevices(List<Long> missingDevices, List<Long> extraDevices) {
this.missingDevices = missingDevices;
this.extraDevices = extraDevices;
}
}

View File

@@ -0,0 +1,60 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PendingMessage {
@JsonProperty
private String sender;
@JsonProperty
private long messageId;
@JsonProperty
private String encryptedOutgoingMessage;
@JsonProperty
private boolean receipt;
public PendingMessage() {}
public PendingMessage(String sender, long messageId, boolean receipt, String encryptedOutgoingMessage) {
this.sender = sender;
this.messageId = messageId;
this.receipt = receipt;
this.encryptedOutgoingMessage = encryptedOutgoingMessage;
}
public String getEncryptedOutgoingMessage() {
return encryptedOutgoingMessage;
}
public long getMessageId() {
return messageId;
}
public String getSender() {
return sender;
}
public boolean isReceipt() {
return receipt;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof PendingMessage)) return false;
PendingMessage that = (PendingMessage)other;
return
this.sender.equals(that.sender) &&
this.messageId == that.messageId &&
this.receipt == that.receipt &&
this.encryptedOutgoingMessage.equals(that.encryptedOutgoingMessage);
}
@Override
public int hashCode() {
return this.sender.hashCode() ^ (int)this.messageId ^ this.encryptedOutgoingMessage.hashCode() ^ (receipt ? 1 : 0);
}
}

View File

@@ -0,0 +1,8 @@
package org.whispersystems.textsecuregcm.entities;
public interface PreKeyBase {
public long getKeyId();
public String getPublicKey();
}

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PreKeyCount {
@JsonProperty
private int count;
public PreKeyCount(int count) {
this.count = count;
}
public PreKeyCount() {}
public int getCount() {
return count;
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
public class PreKeyResponseItemV2 {
@JsonProperty
private long deviceId;
@JsonProperty
private int registrationId;
@JsonProperty
private SignedPreKey signedPreKey;
@JsonProperty
private PreKeyV2 preKey;
public PreKeyResponseItemV2() {}
public PreKeyResponseItemV2(long deviceId, int registrationId, SignedPreKey signedPreKey, PreKeyV2 preKey) {
this.deviceId = deviceId;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
this.preKey = preKey;
}
@VisibleForTesting
public SignedPreKey getSignedPreKey() {
return signedPreKey;
}
@VisibleForTesting
public PreKeyV2 getPreKey() {
return preKey;
}
@VisibleForTesting
public int getRegistrationId() {
return registrationId;
}
@VisibleForTesting
public long getDeviceId() {
return deviceId;
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (C) 2014 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class PreKeyResponseV1 {
@JsonProperty
@NotNull
@Valid
private List<PreKeyV1> keys;
@VisibleForTesting
public PreKeyResponseV1() {}
public PreKeyResponseV1(PreKeyV1 preKey) {
this.keys = new LinkedList<>();
this.keys.add(preKey);
}
public PreKeyResponseV1(List<PreKeyV1> preKeys) {
this.keys = preKeys;
}
public List<PreKeyV1> getKeys() {
return keys;
}
@VisibleForTesting
public boolean equals(Object o) {
if (!(o instanceof PreKeyResponseV1) ||
((PreKeyResponseV1) o).keys.size() != keys.size())
return false;
Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
for (PreKeyV1 key : keys) {
if (!otherKeys.next().equals(key))
return false;
}
return true;
}
public int hashCode() {
int ret = 0xFBA4C795 * keys.size();
for (PreKeyV1 key : keys)
ret ^= key.getPublicKey().hashCode();
return ret;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
public class PreKeyResponseV2 {
@JsonProperty
private String identityKey;
@JsonProperty
private List<PreKeyResponseItemV2> devices;
public PreKeyResponseV2() {}
public PreKeyResponseV2(String identityKey, List<PreKeyResponseItemV2> devices) {
this.identityKey = identityKey;
this.devices = devices;
}
@VisibleForTesting
public String getIdentityKey() {
return identityKey;
}
@VisibleForTesting
public List<PreKeyResponseItemV2> getDevices() {
return devices;
}
}

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2013 Open WhisperSystems * Copyright (C) 2014 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@@ -17,28 +17,39 @@
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty; import com.google.common.annotations.VisibleForTesting;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
public class PreKeyList { public class PreKeyStateV1 {
@JsonProperty
@NotNull
private PreKey lastResortKey;
@JsonProperty @JsonProperty
@NotNull @NotNull
@Valid @Valid
private List<PreKey> keys; private PreKeyV1 lastResortKey;
public List<PreKey> getKeys() { @JsonProperty
@NotNull
@Valid
private List<PreKeyV1> keys;
public List<PreKeyV1> getKeys() {
return keys; return keys;
} }
public PreKey getLastResortKey() { @VisibleForTesting
public void setKeys(List<PreKeyV1> keys) {
this.keys = keys;
}
public PreKeyV1 getLastResortKey() {
return lastResortKey; return lastResortKey;
} }
@VisibleForTesting
public void setLastResortKey(PreKeyV1 lastResortKey) {
this.lastResortKey = lastResortKey;
}
} }

View File

@@ -0,0 +1,76 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
public class PreKeyStateV2 {
@JsonProperty
@NotNull
@Valid
private List<PreKeyV2> preKeys;
@JsonProperty
@NotNull
@Valid
private SignedPreKey signedPreKey;
@JsonProperty
@NotNull
@Valid
private PreKeyV2 lastResortKey;
@JsonProperty
@NotEmpty
private String identityKey;
public PreKeyStateV2() {}
@VisibleForTesting
public PreKeyStateV2(String identityKey, SignedPreKey signedPreKey,
List<PreKeyV2> keys, PreKeyV2 lastResortKey)
{
this.identityKey = identityKey;
this.signedPreKey = signedPreKey;
this.preKeys = keys;
this.lastResortKey = lastResortKey;
}
public List<PreKeyV2> getPreKeys() {
return preKeys;
}
public SignedPreKey getSignedPreKey() {
return signedPreKey;
}
public String getIdentityKey() {
return identityKey;
}
public PreKeyV2 getLastResortKey() {
return lastResortKey;
}
}

View File

@@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2013 Open WhisperSystems * Copyright (C) 2014 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@@ -17,22 +17,17 @@
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlTransient;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_DEFAULT) @JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class PreKey { public class PreKeyV1 implements PreKeyBase {
@JsonIgnore @JsonProperty
private long id; private long deviceId;
@JsonIgnore
private String number;
@JsonProperty @JsonProperty
@NotNull @NotNull
@@ -47,70 +42,55 @@ public class PreKey {
private String identityKey; private String identityKey;
@JsonProperty @JsonProperty
private boolean lastResort; private int registrationId;
public PreKey() {} public PreKeyV1() {}
public PreKey(long id, String number, long keyId, public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
String publicKey, String identityKey,
boolean lastResort)
{ {
this.id = id; this.deviceId = deviceId;
this.number = number; this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.registrationId = registrationId;
}
@VisibleForTesting
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey)
{
this.deviceId = deviceId;
this.keyId = keyId; this.keyId = keyId;
this.publicKey = publicKey; this.publicKey = publicKey;
this.identityKey = identityKey; this.identityKey = identityKey;
this.lastResort = lastResort;
}
@XmlTransient
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@XmlTransient
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
} }
@Override
public String getPublicKey() { public String getPublicKey() {
return publicKey; return publicKey;
} }
public void setPublicKey(String publicKey) { @Override
this.publicKey = publicKey;
}
public long getKeyId() { public long getKeyId() {
return keyId; return keyId;
} }
public void setKeyId(long keyId) {
this.keyId = keyId;
}
public String getIdentityKey() { public String getIdentityKey() {
return identityKey; return identityKey;
} }
public void setIdentityKey(String identityKey) { public void setDeviceId(long deviceId) {
this.identityKey = identityKey; this.deviceId = deviceId;
} }
@XmlTransient public long getDeviceId() {
public boolean isLastResort() { return deviceId;
return lastResort;
} }
public void setLastResort(boolean lastResort) { public int getRegistrationId() {
this.lastResort = lastResort; return registrationId;
}
public void setRegistrationId(int registrationId) {
this.registrationId = registrationId;
} }
} }

View File

@@ -0,0 +1,82 @@
package org.whispersystems.textsecuregcm.entities;
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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/>.
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class PreKeyV2 implements PreKeyBase {
@JsonProperty
@NotNull
private long keyId;
@JsonProperty
@NotEmpty
private String publicKey;
public PreKeyV2() {}
public PreKeyV2(long keyId, String publicKey)
{
this.keyId = keyId;
this.publicKey = publicKey;
}
@Override
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
@Override
public long getKeyId() {
return keyId;
}
public void setKeyId(long keyId) {
this.keyId = keyId;
}
@Override
public boolean equals(Object object) {
if (object == null || !(object instanceof PreKeyV2)) return false;
PreKeyV2 that = (PreKeyV2)object;
if (publicKey == null) {
return this.keyId == that.keyId && that.publicKey == null;
} else {
return this.keyId == that.keyId && this.publicKey.equals(that.publicKey);
}
}
@Override
public int hashCode() {
if (publicKey == null) {
return (int)this.keyId;
} else {
return ((int)this.keyId) ^ publicKey.hashCode();
}
}
}

View File

@@ -32,6 +32,10 @@ public class RelayMessage {
@NotEmpty @NotEmpty
private String destination; private String destination;
@JsonProperty
@NotEmpty
private long destinationDeviceId;
@JsonProperty @JsonProperty
@NotNull @NotNull
@JsonSerialize(using = ByteArrayAdapter.Serializing.class) @JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@@ -40,8 +44,9 @@ public class RelayMessage {
public RelayMessage() {} public RelayMessage() {}
public RelayMessage(String destination, byte[] outgoingMessageSignal) { public RelayMessage(String destination, long destinationDeviceId, byte[] outgoingMessageSignal) {
this.destination = destination; this.destination = destination;
this.destinationDeviceId = destinationDeviceId;
this.outgoingMessageSignal = outgoingMessageSignal; this.outgoingMessageSignal = outgoingMessageSignal;
} }
@@ -49,6 +54,10 @@ public class RelayMessage {
return destination; return destination;
} }
public long getDestinationDeviceId() {
return destinationDeviceId;
}
public byte[] getOutgoingMessageSignal() { public byte[] getOutgoingMessageSignal() {
return outgoingMessageSignal; return outgoingMessageSignal;
} }

View File

@@ -0,0 +1,46 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import java.io.Serializable;
public class SignedPreKey extends PreKeyV2 {
@JsonProperty
@NotEmpty
private String signature;
public SignedPreKey() {}
public SignedPreKey(long keyId, String publicKey, String signature) {
super(keyId, publicKey);
this.signature = signature;
}
public String getSignature() {
return signature;
}
@Override
public boolean equals(Object object) {
if (object == null || !(object instanceof SignedPreKey)) return false;
SignedPreKey that = (SignedPreKey) object;
if (signature == null) {
return super.equals(object) && that.signature == null;
} else {
return super.equals(object) && this.signature.equals(that.signature);
}
}
@Override
public int hashCode() {
if (signature == null) {
return super.hashCode();
} else {
return super.hashCode() ^ signature.hashCode();
}
}
}

View File

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

View File

@@ -17,6 +17,7 @@
package org.whispersystems.textsecuregcm.federation; package org.whispersystems.textsecuregcm.federation;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
@@ -30,19 +31,20 @@ import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.bouncycastle.openssl.PEMReader; import org.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.AccountCount; import org.whispersystems.textsecuregcm.entities.AccountCount;
import org.whispersystems.textsecuregcm.entities.AttachmentUri; import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts; import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.RelayMessage; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.util.Base64; 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.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -55,16 +57,19 @@ 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;
public class FederatedClient { public class FederatedClient {
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class); private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
private static final String USER_COUNT_PATH = "/v1/federation/user_count"; private static final String USER_COUNT_PATH = "/v1/federation/user_count";
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d"; private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
private static final String RELAY_MESSAGE_PATH = "/v1/federation/message"; private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
private static final String PREKEY_PATH = "/v1/federation/key/%s"; private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d"; private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
private final FederatedPeer peer; private final FederatedPeer peer;
private final Client client; private final Client client;
@@ -89,28 +94,63 @@ public class FederatedClient {
WebResource resource = client.resource(peer.getUrl()) WebResource resource = client.resource(peer.getUrl())
.path(String.format(ATTACHMENT_URI_PATH, attachmentId)); .path(String.format(ATTACHMENT_URI_PATH, attachmentId));
return resource.accept(MediaType.APPLICATION_JSON) ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader) .header("Authorization", authorizationHeader)
.get(AttachmentUri.class) .get(ClientResponse.class);
.getLocation();
if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new WebApplicationException(clientResponseToResponse(response));
}
return response.getEntity(AttachmentUri.class).getLocation();
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("Bad URI", e); logger.warn("Bad URI", e);
throw new IOException(e); throw new IOException(e);
} }
} }
public PreKey getKey(String destination) { public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination)); WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device));
return resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader) ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.get(PreKey.class); .header("Authorization", authorizationHeader)
.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) { } catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e); logger.warn("PreKey", e);
return null; return Optional.absent();
} }
} }
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device));
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.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);
return Optional.absent();
}
}
public int getUserCount() { public int getUserCount() {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH); WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
@@ -139,22 +179,37 @@ public class FederatedClient {
} }
} }
public void sendMessage(String destination, OutgoingMessageSignal message) public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
throws IOException, NoSuchUserException throws IOException
{ {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH); WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination));
ClientResponse response = resource.type(MediaType.APPLICATION_JSON) ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader) .header("Authorization", authorizationHeader)
.entity(new RelayMessage(destination, message.toByteArray())) .entity(messages)
.put(ClientResponse.class); .put(ClientResponse.class);
if (response.getStatus() == 404) { if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new NoSuchUserException("No remote user: " + destination); throw new WebApplicationException(clientResponseToResponse(response));
} }
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
}
}
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException
{
try {
String path = String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId);
WebResource resource = client.resource(peer.getUrl()).path(path);
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.put(ClientResponse.class);
if (response.getStatus() != 200 && response.getStatus() != 204) { if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new IOException("Bad response: " + response.getStatus()); throw new WebApplicationException(clientResponseToResponse(response));
} }
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("sendMessage", e); logger.warn("sendMessage", e);
@@ -206,6 +261,19 @@ 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

@@ -0,0 +1,45 @@
package org.whispersystems.textsecuregcm.federation;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
public class NonLimitedAccount extends Account {
@JsonIgnore
private final String number;
@JsonIgnore
private final String relay;
@JsonIgnore
private final long deviceId;
public NonLimitedAccount(String number, long deviceId, String relay) {
this.number = number;
this.deviceId = deviceId;
this.relay = relay;
}
@Override
public String getNumber() {
return number;
}
@Override
public boolean isRateLimited() {
return false;
}
@Override
public Optional<String> getRelay() {
return Optional.of(relay);
}
@Override
public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null));
}
}

View File

@@ -16,13 +16,14 @@
*/ */
package org.whispersystems.textsecuregcm.limits; package org.whispersystems.textsecuregcm.limits;
import com.yammer.metrics.Metrics; import com.codahale.metrics.Meter;
import com.yammer.metrics.core.Meter; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import net.spy.memcached.MemcachedClient; import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.concurrent.TimeUnit; import static com.codahale.metrics.MetricRegistry.name;
public class RateLimiter { public class RateLimiter {
@@ -35,7 +36,9 @@ public class RateLimiter {
public RateLimiter(MemcachedClient memcachedClient, String name, public RateLimiter(MemcachedClient memcachedClient, String name,
int bucketSize, double leakRatePerMinute) int bucketSize, double leakRatePerMinute)
{ {
this.meter = Metrics.newMeter(RateLimiter.class, name, "exceeded", TimeUnit.MINUTES); MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
this.meter = metricRegistry.meter(name(getClass(), name, "exceeded"));
this.memcachedClient = memcachedClient; this.memcachedClient = memcachedClient;
this.name = name; this.name = name;
this.bucketSize = bucketSize; this.bucketSize = bucketSize;

View File

@@ -31,6 +31,9 @@ public class RateLimiters {
private final RateLimiter preKeysLimiter; private final RateLimiter preKeysLimiter;
private final RateLimiter messagesLimiter; private final RateLimiter messagesLimiter;
private final RateLimiter allocateDeviceLimiter;
private final RateLimiter verifyDeviceLimiter;
public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) { public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) {
this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination", this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination",
config.getSmsDestination().getBucketSize(), config.getSmsDestination().getBucketSize(),
@@ -59,6 +62,23 @@ public class RateLimiters {
this.messagesLimiter = new RateLimiter(memcachedClient, "messages", this.messagesLimiter = new RateLimiter(memcachedClient, "messages",
config.getMessages().getBucketSize(), config.getMessages().getBucketSize(),
config.getMessages().getLeakRatePerMinute()); config.getMessages().getLeakRatePerMinute());
this.allocateDeviceLimiter = new RateLimiter(memcachedClient, "allocateDevice",
config.getAllocateDevice().getBucketSize(),
config.getAllocateDevice().getLeakRatePerMinute());
this.verifyDeviceLimiter = new RateLimiter(memcachedClient, "verifyDevice",
config.getVerifyDevice().getBucketSize(),
config.getVerifyDevice().getLeakRatePerMinute());
}
public RateLimiter getAllocateDeviceLimiter() {
return allocateDeviceLimiter;
}
public RateLimiter getVerifyDeviceLimiter() {
return verifyDeviceLimiter;
} }
public RateLimiter getMessagesLimiter() { public RateLimiter getMessagesLimiter() {

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
public class CpuUsageGauge implements Gauge<Integer> {
@Override
public Integer getValue() {
OperatingSystemMXBean mbean = (com.sun.management.OperatingSystemMXBean)
ManagementFactory.getOperatingSystemMXBean();
return (int) Math.ceil(mbean.getSystemCpuLoad() * 100);
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
public class FreeMemoryGauge implements Gauge<Long> {
@Override
public Long getValue() {
OperatingSystemMXBean mbean = (com.sun.management.OperatingSystemMXBean)
ManagementFactory.getOperatingSystemMXBean();
return mbean.getFreePhysicalMemorySize();
}
}

View File

@@ -0,0 +1,184 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metered;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ScheduledReporter;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* Adapted from MetricsServlet.
*/
public class JsonMetricsReporter extends ScheduledReporter {
private final Logger logger = LoggerFactory.getLogger(JsonMetricsReporter.class);
private final JsonFactory factory = new JsonFactory();
private final String table;
private final String sunnylabsHost;
private final String host;
public JsonMetricsReporter(MetricRegistry registry, String token, String sunnylabsHost)
throws UnknownHostException
{
super(registry, "jsonmetrics-reporter", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.MILLISECONDS);
this.table = token;
this.sunnylabsHost = sunnylabsHost;
this.host = InetAddress.getLocalHost().getHostName();
}
@Override
public void report(SortedMap<String, Gauge> stringGaugeSortedMap,
SortedMap<String, Counter> stringCounterSortedMap,
SortedMap<String, Histogram> stringHistogramSortedMap,
SortedMap<String, Meter> stringMeterSortedMap,
SortedMap<String, Timer> stringTimerSortedMap)
{
try {
logger.info("Reporting metrics...");
URL url = new URL("https", sunnylabsHost, 443, "/report/metrics?t=" + table + "&h=" + host);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.addRequestProperty("Content-Type", "application/json");
OutputStream outputStream = connection.getOutputStream();
JsonGenerator json = factory.createGenerator(outputStream, JsonEncoding.UTF8);
json.writeStartObject();
for (Map.Entry<String, Gauge> gauge : stringGaugeSortedMap.entrySet()) {
reportGauge(json, gauge.getKey(), gauge.getValue());
}
for (Map.Entry<String, Counter> counter : stringCounterSortedMap.entrySet()) {
reportCounter(json, counter.getKey(), counter.getValue());
}
for (Map.Entry<String, Histogram> histogram : stringHistogramSortedMap.entrySet()) {
reportHistogram(json, histogram.getKey(), histogram.getValue());
}
for (Map.Entry<String, Meter> meter : stringMeterSortedMap.entrySet()) {
reportMeter(json, meter.getKey(), meter.getValue());
}
for (Map.Entry<String, Timer> timer : stringTimerSortedMap.entrySet()) {
reportTimer(json, timer.getKey(), timer.getValue());
}
json.writeEndObject();
json.close();
outputStream.close();
logger.info("Metrics server response: " + connection.getResponseCode());
} catch (IOException e) {
logger.warn("Error sending metrics", e);
} catch (Exception e) {
logger.warn("error", e);
}
}
private void reportGauge(JsonGenerator json, String name, Gauge gauge) throws IOException {
Object gaugeValue = evaluateGauge(gauge);
if (gaugeValue instanceof Number) {
json.writeFieldName(sanitize(name));
json.writeObject(gaugeValue);
}
}
private void reportCounter(JsonGenerator json, String name, Counter counter) throws IOException {
json.writeFieldName(sanitize(name));
json.writeNumber(counter.getCount());
}
private void reportHistogram(JsonGenerator json, String name, Histogram histogram) throws IOException {
Snapshot snapshot = histogram.getSnapshot();
json.writeFieldName(sanitize(name));
json.writeStartObject();
json.writeNumberField("count", histogram.getCount());
writeSnapshot(json, snapshot);
json.writeEndObject();
}
private void reportMeter(JsonGenerator json, String name, Meter meter) throws IOException {
json.writeFieldName(sanitize(name));
json.writeStartObject();
writeMetered(json, meter);
json.writeEndObject();
}
private void reportTimer(JsonGenerator json, String name, Timer timer) throws IOException {
json.writeFieldName(sanitize(name));
json.writeStartObject();
json.writeFieldName("rate");
json.writeStartObject();
writeMetered(json, timer);
json.writeEndObject();
json.writeFieldName("duration");
json.writeStartObject();
writeSnapshot(json, timer.getSnapshot());
json.writeEndObject();
json.writeEndObject();
}
private Object evaluateGauge(Gauge gauge) {
try {
return gauge.getValue();
} catch (RuntimeException e) {
logger.warn("Error reading gauge", e);
return "error reading gauge";
}
}
private void writeSnapshot(JsonGenerator json, Snapshot snapshot) throws IOException {
json.writeNumberField("max", convertDuration(snapshot.getMax()));
json.writeNumberField("mean", convertDuration(snapshot.getMean()));
json.writeNumberField("min", convertDuration(snapshot.getMin()));
json.writeNumberField("stddev", convertDuration(snapshot.getStdDev()));
json.writeNumberField("median", convertDuration(snapshot.getMedian()));
json.writeNumberField("p75", convertDuration(snapshot.get75thPercentile()));
json.writeNumberField("p95", convertDuration(snapshot.get95thPercentile()));
json.writeNumberField("p98", convertDuration(snapshot.get98thPercentile()));
json.writeNumberField("p99", convertDuration(snapshot.get99thPercentile()));
json.writeNumberField("p999", convertDuration(snapshot.get999thPercentile()));
}
private void writeMetered(JsonGenerator json, Metered meter) throws IOException {
json.writeNumberField("count", convertRate(meter.getCount()));
json.writeNumberField("mean", convertRate(meter.getMeanRate()));
json.writeNumberField("m1", convertRate(meter.getOneMinuteRate()));
json.writeNumberField("m5", convertRate(meter.getFiveMinuteRate()));
json.writeNumberField("m15", convertRate(meter.getFifteenMinuteRate()));
}
private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
private String sanitize(String metricName) {
return SIMPLE_NAMES.matcher(metricName).replaceAll("_");
}
}

View File

@@ -0,0 +1,36 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.Gauge;
import org.whispersystems.textsecuregcm.util.Pair;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public abstract class NetworkGauge implements Gauge<Long> {
protected Pair<Long, Long> getSentReceived() throws IOException {
File proc = new File("/proc/net/dev");
BufferedReader reader = new BufferedReader(new FileReader(proc));
String header = reader.readLine();
String header2 = reader.readLine();
long bytesSent = 0;
long bytesReceived = 0;
String interfaceStats;
while ((interfaceStats = reader.readLine()) != null) {
String[] stats = interfaceStats.split("\\s+");
if (!stats[1].equals("lo:")) {
bytesReceived += Long.parseLong(stats[2]);
bytesSent += Long.parseLong(stats[10]);
}
}
return new Pair<>(bytesSent, bytesReceived);
}
}

View File

@@ -0,0 +1,36 @@
package org.whispersystems.textsecuregcm.metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Pair;
import java.io.IOException;
public class NetworkReceivedGauge extends NetworkGauge {
private final Logger logger = LoggerFactory.getLogger(NetworkSentGauge.class);
private long lastTimestamp;
private long lastReceived;
@Override
public Long getValue() {
try {
long timestamp = System.currentTimeMillis();
Pair<Long, Long> sentAndReceived = getSentReceived();
long result = 0;
if (lastTimestamp != 0) {
result = sentAndReceived.second() - lastReceived;
lastReceived = sentAndReceived.second();
}
lastTimestamp = timestamp;
return result;
} catch (IOException e) {
logger.warn("NetworkReceivedGauge", e);
return -1L;
}
}
}

View File

@@ -0,0 +1,35 @@
package org.whispersystems.textsecuregcm.metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Pair;
import java.io.IOException;
public class NetworkSentGauge extends NetworkGauge {
private final Logger logger = LoggerFactory.getLogger(NetworkSentGauge.class);
private long lastTimestamp;
private long lastSent;
@Override
public Long getValue() {
try {
long timestamp = System.currentTimeMillis();
Pair<Long, Long> sentAndReceived = getSentReceived();
long result = 0;
if (lastTimestamp != 0) {
result = sentAndReceived.first() - lastSent;
lastSent = sentAndReceived.first();
}
lastTimestamp = timestamp;
return result;
} catch (IOException e) {
logger.warn("NetworkSentGauge", e);
return -1L;
}
}
}

View File

@@ -16,8 +16,7 @@
*/ */
package org.whispersystems.textsecuregcm.providers; package org.whispersystems.textsecuregcm.providers;
import com.yammer.metrics.core.HealthCheck; import com.codahale.metrics.health.HealthCheck;
import com.yammer.metrics.core.HealthCheck.Result;
import net.spy.memcached.MemcachedClient; import net.spy.memcached.MemcachedClient;
import java.security.SecureRandom; import java.security.SecureRandom;
@@ -27,7 +26,6 @@ public class MemcacheHealthCheck extends HealthCheck {
private final MemcachedClient client; private final MemcachedClient client;
public MemcacheHealthCheck(MemcachedClient client) { public MemcacheHealthCheck(MemcachedClient client) {
super("memcached");
this.client = client; this.client = client;
} }
@@ -47,6 +45,8 @@ public class MemcacheHealthCheck extends HealthCheck {
return Result.unhealthy("Fetch failed"); return Result.unhealthy("Fetch failed");
} }
this.client.delete("HEALTH" + random);
return Result.healthy(); return Result.healthy();
} }

View File

@@ -16,7 +16,7 @@
*/ */
package org.whispersystems.textsecuregcm.providers; package org.whispersystems.textsecuregcm.providers;
import com.yammer.metrics.core.HealthCheck; import com.codahale.metrics.health.HealthCheck;
import redis.clients.jedis.Jedis; import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
@@ -26,7 +26,6 @@ public class RedisHealthCheck extends HealthCheck {
private final JedisPool clientPool; private final JedisPool clientPool;
public RedisHealthCheck(JedisPool clientPool) { public RedisHealthCheck(JedisPool clientPool) {
super("redis");
this.clientPool = clientPool; this.clientPool = clientPool;
} }

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.providers;
public class TimeProvider {
public long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
}

View File

@@ -16,23 +16,34 @@
*/ */
package org.whispersystems.textsecuregcm.push; package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.notnoop.apns.APNS; import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsService; import com.notnoop.apns.ApnsService;
import com.notnoop.exceptions.NetworkIOException; import com.notnoop.exceptions.NetworkIOException;
import com.yammer.metrics.Metrics; import net.spy.memcached.MemcachedClient;
import com.yammer.metrics.core.Meter;
import org.bouncycastle.openssl.PEMReader; import org.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
@@ -40,56 +51,102 @@ import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class APNSender { import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
private final Meter success = Metrics.newMeter(APNSender.class, "sent", "success", TimeUnit.MINUTES); public class APNSender implements Managed {
private final Meter failure = Metrics.newMeter(APNSender.class, "sent", "failure", TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(APNSender.class); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter websocketMeter = metricRegistry.meter(name(getClass(), "websocket"));
private final Meter pushMeter = metricRegistry.meter(name(getClass(), "push"));
private final Meter failureMeter = metricRegistry.meter(name(getClass(), "failure"));
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
private static final String MESSAGE_BODY = "m"; private static final String MESSAGE_BODY = "m";
private final Optional<ApnsService> apnService; private static final ObjectMapper mapper = SystemMapper.getMapper();
public APNSender(String apnCertificate, String apnKey) private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
private final AccountsManager accounts;
private final PubSubManager pubSubManager;
private final StoredMessages storedMessages;
private final MemcachedClient memcachedClient;
private final String apnCertificate;
private final String apnKey;
private Optional<ApnsService> apnService;
public APNSender(AccountsManager accounts,
PubSubManager pubSubManager,
StoredMessages storedMessages,
MemcachedClient memcachedClient,
String apnCertificate, String apnKey)
{ {
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) { this.accounts = accounts;
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey); this.pubSubManager = pubSubManager;
this.apnService = Optional.of(APNS.newService() this.storedMessages = storedMessages;
.withCert(new ByteArrayInputStream(keyStore), "insecure") this.apnCertificate = apnCertificate;
.withSandboxDestination().build()); this.apnKey = apnKey;
} else { this.memcachedClient = memcachedClient;
this.apnService = Optional.absent(); }
public void sendMessage(Account account, Device device,
String registrationId, PendingMessage message)
throws TransientPushFailureException
{
try {
String serializedPendingMessage = mapper.writeValueAsString(message);
WebsocketAddress websocketAddress = new WebsocketAddress(account.getNumber(), device.getId());
if (pubSubManager.publish(websocketAddress, new PubSubMessage(PubSubMessage.TYPE_DELIVER,
serializedPendingMessage)))
{
websocketMeter.mark();
} else {
memcacheSet(registrationId, account.getNumber());
storedMessages.insert(websocketAddress, message);
if (!message.isReceipt()) {
sendPush(registrationId, serializedPendingMessage);
}
}
} catch (IOException e) {
throw new TransientPushFailureException(e);
} }
} }
public void sendMessage(String registrationId, EncryptedOutgoingMessage message) private void sendPush(String registrationId, String message)
throws IOException throws TransientPushFailureException
{ {
try { try {
if (!apnService.isPresent()) { if (!apnService.isPresent()) {
failure.mark(); failureMeter.mark();
throw new IOException("APN access not configured!"); throw new TransientPushFailureException("APN access not configured!");
} }
String payload = APNS.newPayload() String payload = APNS.newPayload()
.alertBody("Message!") .alertBody("Message!")
.customField(MESSAGE_BODY, message.serialize()) .customField(MESSAGE_BODY, message)
.build(); .build();
logger.debug("APN Payload: " + payload); logger.debug("APN Payload: " + payload);
apnService.get().push(registrationId, payload); apnService.get().push(registrationId, payload);
success.mark(); pushMeter.mark();
} catch (MalformedURLException mue) {
throw new AssertionError(mue);
} catch (NetworkIOException nioe) { } catch (NetworkIOException nioe) {
logger.warn("Network Error", nioe); logger.warn("Network Error", nioe);
failure.mark(); failureMeter.mark();
throw new IOException("Error sending APN"); throw new TransientPushFailureException(nioe);
} }
} }
private static byte[] initializeKeyStore(String pemCertificate, String pemKey) private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
@@ -99,7 +156,7 @@ public class APNSender {
X509Certificate certificate = (X509Certificate) reader.readObject(); X509Certificate certificate = (X509Certificate) reader.readObject();
Certificate[] certificateChain = {certificate}; Certificate[] certificateChain = {certificate};
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes()))); reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes())));
KeyPair keyPair = (KeyPair) reader.readObject(); KeyPair keyPair = (KeyPair) reader.readObject();
KeyStore keyStore = KeyStore.getInstance("pkcs12"); KeyStore keyStore = KeyStore.getInstance("pkcs12");
@@ -113,4 +170,79 @@ public class APNSender {
return baos.toByteArray(); return baos.toByteArray();
} }
@Override
public void start() throws Exception {
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
this.apnService = Optional.of(APNS.newService()
.withCert(new ByteArrayInputStream(keyStore), "insecure")
.asQueued()
.withSandboxDestination().build());
this.executor.scheduleAtFixedRate(new FeedbackRunnable(), 0, 1, TimeUnit.HOURS);
} else {
this.apnService = Optional.absent();
}
}
@Override
public void stop() throws Exception {
if (apnService.isPresent()) {
apnService.get().stop();
}
}
private void memcacheSet(String registrationId, String number) {
if (memcachedClient != null) {
memcachedClient.set("APN-" + registrationId, 60 * 60 * 24, number);
}
}
private Optional<String> memcacheGet(String registrationId) {
if (memcachedClient != null) {
return Optional.fromNullable((String)memcachedClient.get("APN-" + registrationId));
} else {
return Optional.absent();
}
}
private class FeedbackRunnable implements Runnable {
private void updateAccount(Account account, String registrationId) {
boolean needsUpdate = false;
for (Device device : account.getDevices()) {
if (registrationId.equals(device.getApnId())) {
needsUpdate = true;
device.setApnId(null);
}
}
if (needsUpdate) {
accounts.update(account);
}
}
@Override
public void run() {
if (apnService.isPresent()) {
Map<String, Date> inactiveDevices = apnService.get().getInactiveDevices();
for (String registrationId : inactiveDevices.keySet()) {
Optional<String> number = memcacheGet(registrationId);
if (number.isPresent()) {
Optional<Account> account = accounts.get(number.get());
if (account.isPresent()) {
updateAccount(account.get(), registrationId);
}
} else {
logger.warn("APN unregister event received for uncached ID: " + registrationId);
}
}
}
}
}
} }

View File

@@ -1,63 +1,424 @@
/**
* 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.push; package org.whispersystems.textsecuregcm.push;
import com.google.android.gcm.server.Constants; import com.codahale.metrics.Meter;
import com.google.android.gcm.server.Message; import com.codahale.metrics.MetricRegistry;
import com.google.android.gcm.server.Result; import com.codahale.metrics.SharedMetricRegistries;
import com.google.android.gcm.server.Sender; import com.google.common.base.Optional;
import com.yammer.metrics.Metrics; import org.jivesoftware.smack.ConnectionConfiguration;
import com.yammer.metrics.core.Meter; import org.jivesoftware.smack.ConnectionListener;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException; import org.jivesoftware.smack.PacketListener;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.DefaultPacketExtension;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.provider.PacketExtensionProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.util.StringUtils;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Util;
import org.xmlpull.v1.XmlPullParser;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.TimeUnit; import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class GCMSender { import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
private final Meter success = Metrics.newMeter(GCMSender.class, "sent", "success", TimeUnit.MINUTES); public class GCMSender implements Managed, PacketListener {
private final Meter failure = Metrics.newMeter(GCMSender.class, "sent", "failure", TimeUnit.MINUTES);
private final Sender sender; private final Logger logger = LoggerFactory.getLogger(GCMSender.class);
public GCMSender(String apiKey) { private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(org.whispersystems.textsecuregcm.util.Constants.METRICS_NAME);
this.sender = new Sender(apiKey); private final Meter success = metricRegistry.meter(name(getClass(), "sent", "success"));
private final Meter failure = metricRegistry.meter(name(getClass(), "sent", "failure"));
private final Meter unregistered = metricRegistry.meter(name(getClass(), "sent", "unregistered"));
private static final String GCM_SERVER = "gcm.googleapis.com";
private static final int GCM_PORT = 5235;
private static final String GCM_ELEMENT_NAME = "gcm";
private static final String GCM_NAMESPACE = "google:mobile:data";
private final Map<String, UnacknowledgedMessage> pendingMessages = new ConcurrentHashMap<>();
private final long senderId;
private final String apiKey;
private final AccountsManager accounts;
private XMPPTCPConnection connection;
public GCMSender(AccountsManager accounts, long senderId, String apiKey) {
this.accounts = accounts;
this.senderId = senderId;
this.apiKey = apiKey;
ProviderManager.addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE,
new GcmPacketExtensionProvider());
} }
public String sendMessage(String gcmRegistrationId, EncryptedOutgoingMessage outgoingMessage) public void sendMessage(String destinationNumber, long destinationDeviceId,
throws IOException, NoSuchUserException String registrationId, PendingMessage message)
{ {
Message gcmMessage = new Message.Builder().addData("type", "message") String messageId = "m-" + UUID.randomUUID().toString();
.addData("message", outgoingMessage.serialize()) UnacknowledgedMessage unacknowledgedMessage = new UnacknowledgedMessage(destinationNumber,
.build(); destinationDeviceId,
registrationId, message);
Result result = sender.send(gcmMessage, gcmRegistrationId, 5); sendMessage(messageId, unacknowledgedMessage);
}
if (result.getMessageId() != null) { public void sendMessage(String messageId, UnacknowledgedMessage message) {
success.mark(); try {
return result.getCanonicalRegistrationId(); boolean isReceipt = message.getPendingMessage().isReceipt();
Map<String, String> dataObject = new HashMap<>();
dataObject.put("type", "message");
dataObject.put(isReceipt ? "receipt" : "message", message.getPendingMessage().getEncryptedOutgoingMessage());
Map<String, Object> messageObject = new HashMap<>();
messageObject.put("to", message.getRegistrationId());
messageObject.put("message_id", messageId);
messageObject.put("data", dataObject);
String json = JSONObject.toJSONString(messageObject);
pendingMessages.put(messageId, message);
connection.sendPacket(new GcmPacketExtension(json).toPacket());
} catch (SmackException.NotConnectedException e) {
logger.warn("GCMClient", "No connection", e);
}
}
@Override
public void start() throws Exception {
this.connection = connect(senderId, apiKey);
}
@Override
public void stop() throws Exception {
this.connection.disconnect();
}
@Override
public void processPacket(Packet packet) throws SmackException.NotConnectedException {
Message incomingMessage = (Message) packet;
GcmPacketExtension gcmPacket = (GcmPacketExtension) incomingMessage.getExtension(GCM_NAMESPACE);
String json = gcmPacket.getJson();
try {
Map<String, Object> jsonObject = (Map<String, Object>) JSONValue.parseWithException(json);
Object messageType = jsonObject.get("message_type");
if (messageType == null) {
handleUpstreamMessage(jsonObject);
return;
}
switch (messageType.toString()) {
case "ack" : handleAckReceipt(jsonObject); break;
case "nack" : handleNackReceipt(jsonObject); break;
case "receipt" : handleDeliveryReceipt(jsonObject); break;
case "control" : handleControlMessage(jsonObject); break;
default:
logger.warn("Received unknown GCM message: " + messageType.toString());
}
} catch (ParseException e) {
logger.warn("GCMClient", "Received unparsable message", e);
} catch (Exception e) {
logger.warn("GCMClient", "Failed to process packet", e);
}
}
private void handleControlMessage(Map<String, Object> message) {
String controlType = (String) message.get("control_type");
if ("CONNECTION_DRAINING".equals(controlType)) {
logger.warn("GCM Connection is draining! Initiating reconnect...");
reconnect();
} else { } else {
failure.mark(); logger.warn("Received unknown GCM control message: " + controlType);
if (result.getErrorCodeName().equals(Constants.ERROR_NOT_REGISTERED)) { }
throw new NoSuchUserException("User no longer registered with GCM."); }
} else {
throw new IOException("GCM Failed: " + result.getErrorCodeName()); private void handleDeliveryReceipt(Map<String, Object> message) {
logger.warn("Got delivery receipt!");
}
private void handleNackReceipt(Map<String, Object> message) {
String messageId = (String) message.get("message_id");
String errorCode = (String) message.get("error");
if (errorCode == null) {
logger.warn("Null GCM error code!");
if (messageId != null) {
pendingMessages.remove(messageId);
}
return;
}
switch (errorCode) {
case "BAD_REGISTRATION" : handleBadRegistration(message); break;
case "DEVICE_UNREGISTERED" : handleBadRegistration(message); break;
case "INTERNAL_SERVER_ERROR" : handleServerFailure(message); break;
case "INVALID_JSON" : handleClientFailure(message); break;
case "QUOTA_EXCEEDED" : handleClientFailure(message); break;
case "SERVICE_UNAVAILABLE" : handleServerFailure(message); break;
}
}
private void handleAckReceipt(Map<String, Object> message) {
success.mark();
String messageId = (String) message.get("message_id");
if (messageId != null) {
pendingMessages.remove(messageId);
}
}
private void handleUpstreamMessage(Map<String, Object> message)
throws SmackException.NotConnectedException
{
logger.warn("Got upstream message from GCM Server!");
for (String key : message.keySet()) {
logger.warn(key + " : " + message.get(key));
}
Map<String, Object> ack = new HashMap<>();
message.put("message_type", "ack");
message.put("to", message.get("from"));
message.put("message_id", message.get("message_id"));
String json = JSONValue.toJSONString(ack);
Packet request = new GcmPacketExtension(json).toPacket();
connection.sendPacket(request);
}
private void handleBadRegistration(Map<String, Object> message) {
unregistered.mark();
String messageId = (String) message.get("message_id");
if (messageId != null) {
UnacknowledgedMessage unacknowledgedMessage = pendingMessages.remove(messageId);
if (unacknowledgedMessage != null) {
Optional<Account> account = accounts.get(unacknowledgedMessage.getDestinationNumber());
if (account.isPresent()) {
Optional<Device> device = account.get().getDevice(unacknowledgedMessage.getDestinationDeviceId());
if (device.isPresent()) {
device.get().setGcmId(null);
accounts.update(account.get());
}
}
} }
} }
} }
private void handleServerFailure(Map<String, Object> message) {
failure.mark();
String messageId = (String)message.get("message_id");
if (messageId != null) {
UnacknowledgedMessage unacknowledgedMessage = pendingMessages.remove(messageId);
if (unacknowledgedMessage != null) {
sendMessage(messageId, unacknowledgedMessage);
}
}
}
private void handleClientFailure(Map<String, Object> message) {
failure.mark();
logger.warn("Unrecoverable error: " + message.get("error"));
String messageId = (String)message.get("message_id");
if (messageId != null) {
pendingMessages.remove(messageId);
}
}
private void reconnect() {
try {
this.connection.disconnect();
} catch (SmackException.NotConnectedException e) {
logger.warn("GCMClient", "Disconnect attempt", e);
}
while (true) {
try {
this.connection = connect(senderId, apiKey);
return;
} catch (XMPPException | IOException | SmackException e) {
logger.warn("GCMClient", "Reconnecting", e);
Util.sleep(1000);
}
}
}
private XMPPTCPConnection connect(long senderId, String apiKey)
throws XMPPException, IOException, SmackException
{
ConnectionConfiguration config = new ConnectionConfiguration(GCM_SERVER, GCM_PORT);
config.setSecurityMode(ConnectionConfiguration.SecurityMode.enabled);
config.setReconnectionAllowed(true);
config.setRosterLoadedAtLogin(false);
config.setSendPresence(false);
config.setSocketFactory(SSLSocketFactory.getDefault());
XMPPTCPConnection connection = new XMPPTCPConnection(config);
connection.connect();
connection.addConnectionListener(new LoggingConnectionListener());
connection.addPacketListener(this, new PacketTypeFilter(Message.class));
connection.login(senderId + "@gcm.googleapis.com", apiKey);
return connection;
}
private static class GcmPacketExtensionProvider implements PacketExtensionProvider {
@Override
public PacketExtension parseExtension(XmlPullParser xmlPullParser) throws Exception {
String json = xmlPullParser.nextText();
return new GcmPacketExtension(json);
}
}
private static final class GcmPacketExtension extends DefaultPacketExtension {
private final String json;
public GcmPacketExtension(String json) {
super(GCM_ELEMENT_NAME, GCM_NAMESPACE);
this.json = json;
}
public String getJson() {
return json;
}
@Override
public String toXML() {
return String.format("<%s xmlns=\"%s\">%s</%s>", GCM_ELEMENT_NAME, GCM_NAMESPACE,
StringUtils.escapeForXML(json), GCM_ELEMENT_NAME);
}
public Packet toPacket() {
Message message = new Message();
message.addExtension(this);
return message;
}
}
private class LoggingConnectionListener implements ConnectionListener {
@Override
public void connected(XMPPConnection xmppConnection) {
logger.warn("GCM XMPP Connected.");
}
@Override
public void authenticated(XMPPConnection xmppConnection) {
logger.warn("GCM XMPP Authenticated.");
}
@Override
public void reconnectionSuccessful() {
logger.warn("GCM XMPP Reconnecting..");
Iterator<Map.Entry<String, UnacknowledgedMessage>> iterator =
pendingMessages.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, UnacknowledgedMessage> entry = iterator.next();
iterator.remove();
sendMessage(entry.getKey(), entry.getValue());
}
}
@Override
public void reconnectionFailed(Exception e) {
logger.warn("GCM XMPP Reconnection failed!", e);
reconnect();
}
@Override
public void reconnectingIn(int seconds) {
logger.warn(String.format("GCM XMPP Reconnecting in %d secs", seconds));
}
@Override
public void connectionClosedOnError(Exception e) {
logger.warn("GCM XMPP Connection closed on error.");
}
@Override
public void connectionClosed() {
logger.warn("GCM XMPP Connection closed.");
reconnect();
}
}
private static class UnacknowledgedMessage {
private final String destinationNumber;
private final long destinationDeviceId;
private final String registrationId;
private final PendingMessage pendingMessage;
private UnacknowledgedMessage(String destinationNumber,
long destinationDeviceId,
String registrationId,
PendingMessage pendingMessage)
{
this.destinationNumber = destinationNumber;
this.destinationDeviceId = destinationDeviceId;
this.registrationId = registrationId;
this.pendingMessage = pendingMessage;
}
private String getRegistrationId() {
return registrationId;
}
private PendingMessage getPendingMessage() {
return pendingMessage;
}
public String getDestinationNumber() {
return destinationNumber;
}
public long getDestinationDeviceId() {
return destinationDeviceId;
}
}
} }

View File

@@ -0,0 +1,11 @@
package org.whispersystems.textsecuregcm.push;
public class NotPushRegisteredException extends Exception {
public NotPushRegisteredException(String s) {
super(s);
}
public NotPushRegisteredException(Exception e) {
super(e);
}
}

View File

@@ -16,88 +16,77 @@
*/ */
package org.whispersystems.textsecuregcm.push; package org.whispersystems.textsecuregcm.push;
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.textsecuregcm.configuration.ApnConfiguration; import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import java.io.IOException; import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
public class PushSender { public class PushSender {
private final Logger logger = LoggerFactory.getLogger(PushSender.class); private final Logger logger = LoggerFactory.getLogger(PushSender.class);
private final AccountsManager accounts; private final GCMSender gcmSender;
private final DirectoryManager directory; private final APNSender apnSender;
private final WebsocketSender webSocketSender;
private final GCMSender gcmSender; public PushSender(GCMSender gcmClient,
private final APNSender apnSender; APNSender apnSender,
WebsocketSender websocketSender)
public PushSender(GcmConfiguration gcmConfiguration,
ApnConfiguration apnConfiguration,
AccountsManager accounts,
DirectoryManager directory)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
{ {
this.accounts = accounts; this.gcmSender = gcmClient;
this.directory = directory; this.apnSender = apnSender;
this.webSocketSender = websocketSender;
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
} }
public void sendMessage(String destination, MessageProtos.OutgoingMessageSignal outgoingMessage) public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
throws IOException, NoSuchUserException throws NotPushRegisteredException, TransientPushFailureException
{
Optional<Account> account = accounts.get(destination);
if (!account.isPresent()) {
directory.remove(destination);
throw new NoSuchUserException("No such local destination: " + destination);
}
String signalingKey = account.get().getSignalingKey();
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
if (account.get().getGcmRegistrationId() != null) sendGcmMessage(account.get(), message);
else if (account.get().getApnRegistrationId() != null) sendApnMessage(account.get(), message);
else throw new NoSuchUserException("No push identifier!");
}
private void sendGcmMessage(Account account, EncryptedOutgoingMessage outgoingMessage)
throws IOException, NoSuchUserException
{ {
try { try {
String canonicalId = gcmSender.sendMessage(account.getGcmRegistrationId(), boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
outgoingMessage); String signalingKey = device.getSignalingKey();
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, signalingKey);
PendingMessage pendingMessage = new PendingMessage(message.getSource(),
message.getTimestamp(),
isReceipt,
encryptedMessage.serialize());
if (canonicalId != null) { sendMessage(account, device, pendingMessage);
account.setGcmRegistrationId(canonicalId); } catch (CryptoEncodingException e) {
accounts.update(account); throw new NotPushRegisteredException(e);
}
} catch (NoSuchUserException e) {
logger.debug("No Such User", e);
account.setGcmRegistrationId(null);
accounts.update(account);
throw new NoSuchUserException("User no longer exists in GCM.");
} }
} }
private void sendApnMessage(Account account, EncryptedOutgoingMessage outgoingMessage) public void sendMessage(Account account, Device device, PendingMessage pendingMessage)
throws IOException throws NotPushRegisteredException, TransientPushFailureException
{ {
apnSender.sendMessage(account.getApnRegistrationId(), outgoingMessage); if (device.getGcmId() != null) sendGcmMessage(account, device, pendingMessage);
else if (device.getApnId() != null) sendApnMessage(account, device, pendingMessage);
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, pendingMessage);
else throw new NotPushRegisteredException("No delivery possible!");
} }
private void sendGcmMessage(Account account, Device device, PendingMessage pendingMessage) {
String number = account.getNumber();
long deviceId = device.getId();
String registrationId = device.getGcmId();
gcmSender.sendMessage(number, deviceId, registrationId, pendingMessage);
}
private void sendApnMessage(Account account, Device device, PendingMessage outgoingMessage)
throws TransientPushFailureException
{
apnSender.sendMessage(account, device, device.getApnId(), outgoingMessage);
}
private void sendWebSocketMessage(Account account, Device device, PendingMessage outgoingMessage)
{
webSocketSender.sendMessage(account, device, outgoingMessage);
}
} }

View File

@@ -0,0 +1,11 @@
package org.whispersystems.textsecuregcm.push;
public class TransientPushFailureException extends Exception {
public TransientPushFailureException(String s) {
super(s);
}
public TransientPushFailureException(Exception e) {
super(e);
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (C) 2014 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.push;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static com.codahale.metrics.MetricRegistry.name;
public class WebsocketSender {
private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter onlineMeter = metricRegistry.meter(name(getClass(), "online"));
private final Meter offlineMeter = metricRegistry.meter(name(getClass(), "offline"));
private static final ObjectMapper mapper = SystemMapper.getMapper();
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
public WebsocketSender(StoredMessages storedMessages, PubSubManager pubSubManager) {
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
}
public void sendMessage(Account account, Device device, PendingMessage pendingMessage) {
try {
String serialized = mapper.writeValueAsString(pendingMessage);
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
PubSubMessage pubSubMessage = new PubSubMessage(PubSubMessage.TYPE_DELIVER, serialized);
if (pubSubManager.publish(address, pubSubMessage)) {
onlineMeter.mark();
} else {
offlineMeter.mark();
storedMessages.insert(address, pendingMessage);
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null));
}
} catch (JsonProcessingException e) {
logger.warn("WebsocketSender", "Unable to serialize json", e);
}
}
}

View File

@@ -16,11 +16,13 @@
*/ */
package org.whispersystems.textsecuregcm.sms; package org.whispersystems.textsecuregcm.sms;
import com.yammer.metrics.Metrics; import com.codahale.metrics.Meter;
import com.yammer.metrics.core.Meter; import com.codahale.metrics.MetricRegistry;
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.configuration.NexmoConfiguration; import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.util.Constants;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
@@ -28,16 +30,15 @@ import java.io.InputStreamReader;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.concurrent.TimeUnit;
import org.whispersystems.textsecuregcm.sms.SenderFactory.VoxSender; import static com.codahale.metrics.MetricRegistry.name;
import org.whispersystems.textsecuregcm.sms.SenderFactory.SmsSender;
public class NexmoSmsSender implements SmsSender, VoxSender { public class NexmoSmsSender {
private final Meter smsMeter = Metrics.newMeter(NexmoSmsSender.class, "sms", "delivered", TimeUnit.MINUTES); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter voxMeter = Metrics.newMeter(NexmoSmsSender.class, "vox", "delivered", TimeUnit.MINUTES); private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
private final Logger logger = LoggerFactory.getLogger(NexmoSmsSender.class); 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 = 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"; "https://rest.nexmo.com/sms/json?api_key=%s&api_secret=%s&from=%s&to=%s&text=%s";
@@ -55,10 +56,9 @@ public class NexmoSmsSender implements SmsSender, VoxSender {
this.number = config.getNumber(); this.number = config.getNumber();
} }
@Override
public void deliverSmsVerification(String destination, String verificationCode) throws IOException { public void deliverSmsVerification(String destination, String verificationCode) throws IOException {
URL url = new URL(String.format(NEXMO_SMS_URL, apiKey, apiSecret, number, destination, URL url = new URL(String.format(NEXMO_SMS_URL, apiKey, apiSecret, number, destination,
URLEncoder.encode(SmsSender.VERIFICATION_TEXT + verificationCode, "UTF-8"))); URLEncoder.encode(SmsSender.SMS_VERIFICATION_TEXT + verificationCode, "UTF-8")));
URLConnection connection = url.openConnection(); URLConnection connection = url.openConnection();
connection.setDoInput(true); connection.setDoInput(true);
@@ -70,10 +70,9 @@ public class NexmoSmsSender implements SmsSender, VoxSender {
smsMeter.mark(); smsMeter.mark();
} }
@Override
public void deliverVoxVerification(String destination, String message) throws IOException { public void deliverVoxVerification(String destination, String message) throws IOException {
URL url = new URL(String.format(NEXMO_VOX_URL, apiKey, apiSecret, destination, URL url = new URL(String.format(NEXMO_VOX_URL, apiKey, apiSecret, destination,
URLEncoder.encode(VoxSender.VERIFICATION_TEXT + message, "UTF-8"))); URLEncoder.encode(SmsSender.VOX_VERIFICATION_TEXT + message, "UTF-8")));
URLConnection connection = url.openConnection(); URLConnection connection = url.openConnection();
connection.setDoInput(true); connection.setDoInput(true);

View File

@@ -1,70 +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.google.common.base.Optional;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import java.io.IOException;
public class SenderFactory {
private final TwilioSmsSender twilioSender;
private final Optional<NexmoSmsSender> nexmoSender;
public SenderFactory(TwilioConfiguration twilioConfig, NexmoConfiguration nexmoConfig) {
this.twilioSender = new TwilioSmsSender(twilioConfig);
if (nexmoConfig != null) {
this.nexmoSender = Optional.of(new NexmoSmsSender(nexmoConfig));
} else {
this.nexmoSender = Optional.absent();
}
}
public SmsSender getSmsSender(String number) {
if (nexmoSender.isPresent() && !isTwilioDestination(number)) {
return nexmoSender.get();
} else {
return twilioSender;
}
}
public VoxSender getVoxSender(String number) {
if (nexmoSender.isPresent()) {
return nexmoSender.get();
} else {
return twilioSender;
}
}
private boolean isTwilioDestination(String number) {
return number.length() == 12 && number.startsWith("+1");
}
public interface SmsSender {
public static final String VERIFICATION_TEXT = "Your TextSecure verification code: ";
public void deliverSmsVerification(String destination, String verificationCode) throws IOException;
}
public interface VoxSender {
public static final String VERIFICATION_TEXT = "Your TextSecure verification code is: ";
public void deliverVoxVerification(String destination, String verificationCode) throws IOException;
}
}

View File

@@ -0,0 +1,84 @@
/**
* 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.google.common.base.Optional;
import com.twilio.sdk.TwilioRestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class SmsSender {
static final String SMS_VERIFICATION_TEXT = "Your TextSecure verification code: ";
static final String VOX_VERIFICATION_TEXT = "Your TextSecure verification code is: ";
private final Logger logger = LoggerFactory.getLogger(SmsSender.class);
private final TwilioSmsSender twilioSender;
private final Optional<NexmoSmsSender> nexmoSender;
private final boolean isTwilioInternational;
public SmsSender(TwilioSmsSender twilioSender,
Optional<NexmoSmsSender> nexmoSender,
boolean isTwilioInternational)
{
this.isTwilioInternational = isTwilioInternational;
this.twilioSender = twilioSender;
this.nexmoSender = nexmoSender;
}
public void deliverSmsVerification(String destination, String verificationCode)
throws IOException
{
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) {
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
} else {
try {
twilioSender.deliverSmsVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio SMS Fallback", e);
if (nexmoSender.isPresent()) {
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
}
}
}
}
public void deliverVoxVerification(String destination, String verificationCode)
throws IOException
{
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) {
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
} else {
try {
twilioSender.deliverVoxVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio Vox Fallback", e);
if (nexmoSender.isPresent()) {
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
}
}
}
}
private boolean isTwilioDestination(String number) {
return isTwilioInternational || number.length() == 12 && number.startsWith("+1");
}
}

View File

@@ -16,32 +16,36 @@
*/ */
package org.whispersystems.textsecuregcm.sms; package org.whispersystems.textsecuregcm.sms;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
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;
import com.twilio.sdk.resource.factory.MessageFactory; import com.twilio.sdk.resource.factory.MessageFactory;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.util.Constants;
import java.io.IOException; import java.io.IOException;
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.concurrent.TimeUnit;
public class TwilioSmsSender implements SenderFactory.SmsSender, SenderFactory.VoxSender { import static com.codahale.metrics.MetricRegistry.name;
public class TwilioSmsSender {
public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Response>\n" + "<Response>\n" +
" <Say voice=\"woman\" language=\"en\">%s</Say>\n" + " <Say voice=\"woman\" language=\"en\">" + SmsSender.VOX_VERIFICATION_TEXT + "%s</Say>\n" +
"</Response>"; "</Response>";
private final Meter smsMeter = Metrics.newMeter(TwilioSmsSender.class, "sms", "delivered", TimeUnit.MINUTES); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter voxMeter = Metrics.newMeter(TwilioSmsSender.class, "vox", "delivered", TimeUnit.MINUTES); private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "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;
@@ -55,37 +59,39 @@ public class TwilioSmsSender implements SenderFactory.SmsSender, SenderFactory.V
this.localDomain = config.getLocalDomain(); this.localDomain = config.getLocalDomain();
} }
@Override
public void deliverSmsVerification(String destination, String verificationCode) public void deliverSmsVerification(String destination, String verificationCode)
throws IOException throws IOException, TwilioRestException
{ {
TwilioRestClient client = new TwilioRestClient(accountId, accountToken);
MessageFactory messageFactory = client.getAccount().getMessageFactory();
List<NameValuePair> messageParams = new LinkedList<>();
messageParams.add(new BasicNameValuePair("To", destination));
messageParams.add(new BasicNameValuePair("From", number));
messageParams.add(new BasicNameValuePair("Body", SmsSender.SMS_VERIFICATION_TEXT + verificationCode));
try { try {
TwilioRestClient client = new TwilioRestClient(accountId, accountToken);
MessageFactory messageFactory = client.getAccount().getMessageFactory();
List<NameValuePair> messageParams = new LinkedList<>();
messageParams.add(new BasicNameValuePair("To", destination));
messageParams.add(new BasicNameValuePair("From", number));
messageParams.add(new BasicNameValuePair("Body", SenderFactory.SmsSender.VERIFICATION_TEXT + verificationCode));
messageFactory.create(messageParams); messageFactory.create(messageParams);
} catch (TwilioRestException e) { } catch (RuntimeException damnYouTwilio) {
throw new IOException(e); throw new IOException(damnYouTwilio);
} }
smsMeter.mark(); smsMeter.mark();
} }
@Override public void deliverVoxVerification(String destination, String verificationCode)
public void deliverVoxVerification(String destination, String verificationCode) throws IOException { throws IOException, TwilioRestException
{
TwilioRestClient client = new TwilioRestClient(accountId, accountToken);
CallFactory callFactory = client.getAccount().getCallFactory();
Map<String, String> callParams = new HashMap<>();
callParams.put("To", destination);
callParams.put("From", number);
callParams.put("Url", "https://" + localDomain + "/v1/accounts/voice/twiml/" + verificationCode);
try { try {
TwilioRestClient client = new TwilioRestClient(accountId, accountToken);
CallFactory callFactory = client.getAccount().getCallFactory();
Map<String, String> callParams = new HashMap<>();
callParams.put("To", destination);
callParams.put("From", number);
callParams.put("Url", "https://" + localDomain + "/v1/accounts/voice/twiml/" + verificationCode);
callFactory.create(callParams); callFactory.create(callParams);
} catch (TwilioRestException e) { } catch (RuntimeException damnYouTwilio) {
throw new IOException(e); throw new IOException(damnYouTwilio);
} }
voxMeter.mark(); voxMeter.mark();

View File

@@ -17,53 +17,48 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import java.io.Serializable; import java.util.LinkedList;
import java.util.List;
public class Account implements Serializable { public class Account {
public static final int MEMCACHE_VERION = 1; public static final int MEMCACHE_VERION = 5;
private long id; @JsonProperty
private String number; private String number;
private String hashedAuthenticationToken;
private String salt; @JsonProperty
private String signalingKey;
private String gcmRegistrationId;
private String apnRegistrationId;
private boolean supportsSms; private boolean supportsSms;
@JsonProperty
private List<Device> devices = new LinkedList<>();
@JsonProperty
private String identityKey;
@JsonIgnore
private Optional<Device> authenticatedDevice;
public Account() {} public Account() {}
public Account(long id, String number, String hashedAuthenticationToken, String salt, @VisibleForTesting
String signalingKey, String gcmRegistrationId, String apnRegistrationId, public Account(String number, boolean supportsSms, List<Device> devices) {
boolean supportsSms) this.number = number;
{ this.supportsSms = supportsSms;
this.id = id; this.devices = devices;
this.number = number;
this.hashedAuthenticationToken = hashedAuthenticationToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmRegistrationId = gcmRegistrationId;
this.apnRegistrationId = apnRegistrationId;
this.supportsSms = supportsSms;
} }
public String getApnRegistrationId() { public Optional<Device> getAuthenticatedDevice() {
return apnRegistrationId; return authenticatedDevice;
} }
public void setApnRegistrationId(String apnRegistrationId) { public void setAuthenticatedDevice(Device device) {
this.apnRegistrationId = apnRegistrationId; this.authenticatedDevice = Optional.of(device);
}
public String getGcmRegistrationId() {
return gcmRegistrationId;
}
public void setGcmRegistrationId(String gcmRegistrationId) {
this.gcmRegistrationId = gcmRegistrationId;
} }
public void setNumber(String number) { public void setNumber(String number) {
@@ -74,23 +69,6 @@ public class Account implements Serializable {
return number; return number;
} }
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
}
public AuthenticationCredentials getAuthenticationCredentials() {
return new AuthenticationCredentials(hashedAuthenticationToken, salt);
}
public String getSignalingKey() {
return signalingKey;
}
public void setSignalingKey(String signalingKey) {
this.signalingKey = signalingKey;
}
public boolean getSupportsSms() { public boolean getSupportsSms() {
return supportsSms; return supportsSms;
} }
@@ -99,11 +77,63 @@ public class Account implements Serializable {
this.supportsSms = supportsSms; this.supportsSms = supportsSms;
} }
public long getId() { public void addDevice(Device device) {
return id; this.devices.add(device);
} }
public void setId(long id) { public void setDevices(List<Device> devices) {
this.id = id; this.devices = devices;
}
public List<Device> getDevices() {
return devices;
}
public Optional<Device> getMasterDevice() {
return getDevice(Device.MASTER_ID);
}
public Optional<Device> getDevice(long deviceId) {
for (Device device : devices) {
if (device.getId() == deviceId) {
return Optional.of(device);
}
}
return Optional.absent();
}
public boolean isActive() {
return
getMasterDevice().isPresent() &&
getMasterDevice().get().isActive();
}
public long getNextDeviceId() {
long highestDevice = Device.MASTER_ID;
for (Device device : devices) {
if (device.getId() > highestDevice) {
highestDevice = device.getId();
}
}
return highestDevice + 1;
}
public boolean isRateLimited() {
return true;
}
public Optional<String> getRelay() {
return Optional.absent();
}
public void setIdentityKey(String identityKey) {
this.identityKey = identityKey;
}
public String getIdentityKey() {
return identityKey;
} }
} }

View File

@@ -16,6 +16,10 @@
*/ */
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.databind.ObjectMapper;
import org.skife.jdbi.v2.SQLStatement; import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.TransactionIsolationLevel; import org.skife.jdbi.v2.TransactionIsolationLevel;
@@ -29,7 +33,9 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction; import org.skife.jdbi.v2.sqlobject.Transaction;
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.util.SystemMapper;
import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@@ -42,36 +48,27 @@ import java.util.List;
public abstract class Accounts { public abstract class Accounts {
public static final String ID = "id"; private static final String ID = "id";
public static final String NUMBER = "number"; private static final String NUMBER = "number";
public static final String AUTH_TOKEN = "auth_token"; private static final String DATA = "data";
public static final String SALT = "salt";
public static final String SIGNALING_KEY = "signaling_key";
public static final String GCM_ID = "gcm_id";
public static final String APN_ID = "apn_id";
public static final String SUPPORTS_SMS = "supports_sms";
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + AUTH_TOKEN + ", " + private static final ObjectMapper mapper = SystemMapper.getMapper();
SALT + ", " + SIGNALING_KEY + ", " + GCM_ID + ", " +
APN_ID + ", " + SUPPORTS_SMS + ") " + @SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DATA + ") VALUES (:number, CAST(:data AS json))")
"VALUES (:number, :auth_token, :salt, :signaling_key, :gcm_id, :apn_id, :supports_sms)")
@GetGeneratedKeys @GetGeneratedKeys
abstract long createStep(@AccountBinder Account account); abstract long insertStep(@AccountBinder Account account);
@SqlUpdate("DELETE FROM accounts WHERE number = :number") @SqlUpdate("DELETE FROM accounts WHERE " + NUMBER + " = :number")
abstract void removeStep(@Bind("number") String number); abstract void removeAccount(@Bind("number") String number);
@SqlUpdate("UPDATE accounts SET " + AUTH_TOKEN + " = :auth_token, " + SALT + " = :salt, " + @SqlUpdate("UPDATE accounts SET " + DATA + " = CAST(:data AS json) WHERE " + NUMBER + " = :number")
SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " +
APN_ID + " = :apn_id, " + SUPPORTS_SMS + " = :supports_sms " +
"WHERE " + NUMBER + " = :number")
abstract void update(@AccountBinder Account account); abstract void update(@AccountBinder Account account);
@Mapper(AccountMapper.class) @Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number") @SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number")
abstract Account get(@Bind("number") String number); abstract Account get(@Bind("number") String number);
@SqlQuery("SELECT COUNT(*) from accounts") @SqlQuery("SELECT COUNT(DISTINCT " + NUMBER + ") from accounts")
abstract long getCount(); abstract long getCount();
@Mapper(AccountMapper.class) @Mapper(AccountMapper.class)
@@ -80,25 +77,30 @@ public abstract class Accounts {
@Mapper(AccountMapper.class) @Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts") @SqlQuery("SELECT * FROM accounts")
abstract Iterator<Account> getAll(); public abstract Iterator<Account> getAll();
@Transaction(TransactionIsolationLevel.REPEATABLE_READ) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public long create(Account account) { public long create(Account account) {
removeStep(account.getNumber()); removeAccount(account.getNumber());
return createStep(account); return insertStep(account);
} }
public static class AccountMapper implements ResultSetMapper<Account> { @SqlUpdate("VACUUM accounts")
public abstract void vacuum();
public static class AccountMapper implements ResultSetMapper<Account> {
@Override @Override
public Account map(int i, ResultSet resultSet, StatementContext statementContext) public Account map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException throws SQLException
{ {
return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER), try {
resultSet.getString(AUTH_TOKEN), resultSet.getString(SALT), Account account = mapper.readValue(resultSet.getString(DATA), Account.class);
resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID), // account.setId(resultSet.getLong(ID));
resultSet.getString(APN_ID),
resultSet.getInt(SUPPORTS_SMS) == 1); return account;
} catch (IOException e) {
throw new SQLException(e);
}
} }
} }
@@ -115,15 +117,14 @@ public abstract class Accounts {
AccountBinder accountBinder, AccountBinder accountBinder,
Account account) Account account)
{ {
sql.bind(ID, account.getId()); try {
sql.bind(NUMBER, account.getNumber()); String serialized = mapper.writeValueAsString(account);
sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials()
.getHashedAuthenticationToken()); sql.bind(NUMBER, account.getNumber());
sql.bind(SALT, account.getAuthenticationCredentials().getSalt()); sql.bind(DATA, serialized);
sql.bind(SIGNALING_KEY, account.getSignalingKey()); } catch (JsonProcessingException e) {
sql.bind(GCM_ID, account.getGcmRegistrationId()); throw new IllegalArgumentException(e);
sql.bind(APN_ID, account.getApnRegistrationId()); }
sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0);
} }
}; };
} }

View File

@@ -17,19 +17,30 @@
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.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient; import net.spy.memcached.MemcachedClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
public class AccountsManager { public class AccountsManager {
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
private final Accounts accounts; private final Accounts accounts;
private final MemcachedClient memcachedClient; private final MemcachedClient memcachedClient;
private final DirectoryManager directory; private final DirectoryManager directory;
private final ObjectMapper mapper;
public AccountsManager(Accounts accounts, public AccountsManager(Accounts accounts,
DirectoryManager directory, DirectoryManager directory,
@@ -38,6 +49,7 @@ public class AccountsManager {
this.accounts = accounts; this.accounts = accounts;
this.directory = directory; this.directory = directory;
this.memcachedClient = memcachedClient; this.memcachedClient = memcachedClient;
this.mapper = SystemMapper.getMapper();
} }
public long getCount() { public long getCount() {
@@ -53,47 +65,40 @@ public class AccountsManager {
} }
public void create(Account account) { public void create(Account account) {
long id = accounts.create(account); accounts.create(account);
memcacheSet(account.getNumber(), account);
account.setId(id);
if (memcachedClient != null) {
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
updateDirectory(account); updateDirectory(account);
} }
public void update(Account account) { public void update(Account account) {
if (memcachedClient != null) { memcacheSet(account.getNumber(), account);
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
accounts.update(account); accounts.update(account);
updateDirectory(account); updateDirectory(account);
} }
public Optional<Account> get(String number) { public Optional<Account> get(String number) {
Account account = null; Optional<Account> account = memcacheGet(number);
if (memcachedClient != null) { if (!account.isPresent()) {
account = (Account)memcachedClient.get(getKey(number)); account = Optional.fromNullable(accounts.get(number));
}
if (account == null) { if (account.isPresent()) {
account = accounts.get(number); memcacheSet(number, account.get());
if (account != null && memcachedClient != null) {
memcachedClient.set(getKey(number), 0, account);
} }
} }
if (account != null) return Optional.of(account); return account;
else return Optional.absent(); }
public boolean isRelayListed(String number) {
byte[] token = Util.getContactToken(number);
Optional<ClientContact> contact = directory.get(token);
return contact.isPresent() && !Util.isEmpty(contact.get().getRelay());
} }
private void updateDirectory(Account account) { private void updateDirectory(Account account) {
if (account.getGcmRegistrationId() != null || account.getApnRegistrationId() != null) { 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.getSupportsSms());
directory.add(clientContact); directory.add(clientContact);
@@ -105,4 +110,31 @@ public class AccountsManager {
private String getKey(String number) { private String getKey(String number) {
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number; return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
} }
private void memcacheSet(String number, Account account) {
if (memcachedClient != null) {
try {
String json = mapper.writeValueAsString(account);
memcachedClient.set(getKey(number), 0, json);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
}
private Optional<Account> memcacheGet(String number) {
if (memcachedClient == null) return Optional.absent();
try {
String json = (String)memcachedClient.get(getKey(number));
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
else return Optional.absent();
} catch (IOException e) {
logger.warn("AccountsManager", "Deserialization error", e);
return Optional.absent();
}
}
} }

View File

@@ -0,0 +1,148 @@
/**
* 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.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.Serializable;
public class Device {
public static final long MASTER_ID = 1;
@JsonProperty
private long id;
@JsonProperty
private String authToken;
@JsonProperty
private String salt;
@JsonProperty
private String signalingKey;
@JsonProperty
private String gcmId;
@JsonProperty
private String apnId;
@JsonProperty
private boolean fetchesMessages;
@JsonProperty
private int registrationId;
@JsonProperty
private SignedPreKey signedPreKey;
public Device() {}
public Device(long id, String authToken, String salt,
String signalingKey, String gcmId, String apnId,
boolean fetchesMessages, int registrationId,
SignedPreKey signedPreKey)
{
this.id = id;
this.authToken = authToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmId = gcmId;
this.apnId = apnId;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
}
public String getApnId() {
return apnId;
}
public void setApnId(String apnId) {
this.apnId = apnId;
}
public String getGcmId() {
return gcmId;
}
public void setGcmId(String gcmId) {
this.gcmId = gcmId;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
this.authToken = credentials.getHashedAuthenticationToken();
this.salt = credentials.getSalt();
}
public AuthenticationCredentials getAuthenticationCredentials() {
return new AuthenticationCredentials(authToken, salt);
}
public String getSignalingKey() {
return signalingKey;
}
public void setSignalingKey(String signalingKey) {
this.signalingKey = signalingKey;
}
public boolean isActive() {
return fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
}
public boolean getFetchesMessages() {
return fetchesMessages;
}
public void setFetchesMessages(boolean fetchesMessages) {
this.fetchesMessages = fetchesMessages;
}
public boolean isMaster() {
return getId() == MASTER_ID;
}
public int getRegistrationId() {
return registrationId;
}
public void setRegistrationId(int registrationId) {
this.registrationId = registrationId;
}
public SignedPreKey getSignedPreKey() {
return signedPreKey;
}
public void setSignedPreKey(SignedPreKey signedPreKey) {
this.signedPreKey = signedPreKey;
}
}

View File

@@ -21,6 +21,7 @@ import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.IterablePair; import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.util.LinkedList; import java.util.LinkedList;
@@ -75,6 +76,11 @@ public class DirectoryManager {
pipeline.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes()); pipeline.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes());
} }
public PendingClientContact get(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline;
return new PendingClientContact(token, pipeline.hget(DIRECTORY_KEY, token));
}
public Optional<ClientContact> get(byte[] token) { public Optional<ClientContact> get(byte[] token) {
Jedis jedis = redisPool.getResource(); Jedis jedis = redisPool.getResource();
@@ -110,7 +116,7 @@ public class DirectoryManager {
IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures); IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures);
for (IterablePair.Pair<byte[], Response<byte[]>> pair : lists) { for (Pair<byte[], Response<byte[]>> pair : lists) {
if (pair.second().get() != null) { if (pair.second().get() != null) {
TokenValue tokenValue = new Gson().fromJson(new String(pair.second().get()), TokenValue.class); TokenValue tokenValue = new Gson().fromJson(new String(pair.second().get()), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms); ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms);
@@ -161,4 +167,26 @@ public class DirectoryManager {
this.supportsSms = supportsSms; this.supportsSms = supportsSms;
} }
} }
public static class PendingClientContact {
private final byte[] token;
private final Response<byte[]> response;
PendingClientContact(byte[] token, Response<byte[]> response) {
this.token = token;
this.response = response;
}
public Optional<ClientContact> get() {
byte[] result = response.get();
if (result == null) {
return Optional.absent();
}
TokenValue tokenValue = new Gson().fromJson(new String(result), TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
}
}
} }

View File

@@ -0,0 +1,46 @@
package org.whispersystems.textsecuregcm.storage;
public class KeyRecord {
private long id;
private String number;
private long deviceId;
private long keyId;
private String publicKey;
private boolean lastResort;
public KeyRecord(long id, String number, long deviceId, long keyId,
String publicKey, boolean lastResort)
{
this.id = id;
this.number = number;
this.deviceId = deviceId;
this.keyId = keyId;
this.publicKey = publicKey;
this.lastResort = lastResort;
}
public long getId() {
return id;
}
public String getNumber() {
return number;
}
public long getDeviceId() {
return deviceId;
}
public long getKeyId() {
return keyId;
}
public String getPublicKey() {
return publicKey;
}
public boolean isLastResort() {
return lastResort;
}
}

View File

@@ -16,6 +16,7 @@
*/ */
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import org.skife.jdbi.v2.SQLStatement; import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.TransactionIsolationLevel; import org.skife.jdbi.v2.TransactionIsolationLevel;
@@ -29,7 +30,7 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction; import org.skife.jdbi.v2.sqlobject.Transaction;
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.PreKey; import org.whispersystems.textsecuregcm.entities.PreKeyBase;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
@@ -38,50 +39,82 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List; import java.util.List;
public abstract class Keys { public abstract class Keys {
@SqlUpdate("DELETE FROM keys WHERE number = :number") @SqlUpdate("DELETE FROM keys WHERE number = :number AND device_id = :device_id")
abstract void removeKeys(@Bind("number") String number); abstract void removeKeys(@Bind("number") String number, @Bind("device_id") long deviceId);
@SqlUpdate("DELETE FROM keys WHERE id = :id") @SqlUpdate("DELETE FROM keys WHERE id = :id")
abstract void removeKey(@Bind("id") long id); abstract void removeKey(@Bind("id") long id);
@SqlBatch("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)") @SqlBatch("INSERT INTO keys (number, device_id, key_id, public_key, last_resort) VALUES " +
abstract void append(@PreKeyBinder List<PreKey> preKeys); "(:number, :device_id, :key_id, :public_key, :last_resort)")
abstract void append(@PreKeyBinder List<KeyRecord> preKeys);
@SqlUpdate("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)") @SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE")
abstract void append(@PreKeyBinder PreKey preKey);
@SqlQuery("SELECT * FROM keys WHERE number = :number ORDER BY id LIMIT 1 FOR UPDATE")
@Mapper(PreKeyMapper.class) @Mapper(PreKeyMapper.class)
abstract PreKey retrieveFirst(@Bind("number") String number); abstract KeyRecord retrieveFirst(@Bind("number") String number, @Bind("device_id") long deviceId);
@SqlQuery("SELECT DISTINCT ON (number, device_id) * FROM keys WHERE number = :number ORDER BY number, device_id, key_id ASC")
@Mapper(PreKeyMapper.class)
abstract List<KeyRecord> retrieveFirst(@Bind("number") String number);
@SqlQuery("SELECT COUNT(*) FROM keys WHERE number = :number AND device_id = :device_id")
public abstract int getCount(@Bind("number") String number, @Bind("device_id") long deviceId);
@Transaction(TransactionIsolationLevel.SERIALIZABLE) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public void store(String number, PreKey lastResortKey, List<PreKey> keys) { public void store(String number, long deviceId, List<? extends PreKeyBase> keys, PreKeyBase lastResortKey) {
for (PreKey key : keys) { List<KeyRecord> records = new LinkedList<>();
key.setNumber(number);
for (PreKeyBase key : keys) {
records.add(new KeyRecord(0, number, deviceId, key.getKeyId(), key.getPublicKey(), false));
} }
lastResortKey.setNumber(number); records.add(new KeyRecord(0, number, deviceId, lastResortKey.getKeyId(),
lastResortKey.getPublicKey(), true));
removeKeys(number); removeKeys(number, deviceId);
append(keys); append(records);
append(lastResortKey);
} }
@Transaction(TransactionIsolationLevel.SERIALIZABLE) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public PreKey get(String number) { public Optional<List<KeyRecord>> get(String number, long deviceId) {
PreKey preKey = retrieveFirst(number); final KeyRecord record = retrieveFirst(number, deviceId);
if (preKey != null && !preKey.isLastResort()) { if (record != null && !record.isLastResort()) {
removeKey(preKey.getId()); removeKey(record.getId());
} else if (record == null) {
return Optional.absent();
} }
return preKey; List<KeyRecord> results = new LinkedList<>();
results.add(record);
return Optional.of(results);
} }
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
public Optional<List<KeyRecord>> get(String number) {
List<KeyRecord> preKeys = retrieveFirst(number);
if (preKeys != null) {
for (KeyRecord preKey : preKeys) {
if (!preKey.isLastResort()) {
removeKey(preKey.getId());
}
}
}
if (preKeys != null) return Optional.of(preKeys);
else return Optional.absent();
}
@SqlUpdate("VACUUM keys")
public abstract void vacuum();
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class) @BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER}) @Target({ElementType.PARAMETER})
@@ -89,16 +122,16 @@ public abstract class Keys {
public static class PreKeyBinderFactory implements BinderFactory { public static class PreKeyBinderFactory implements BinderFactory {
@Override @Override
public Binder build(Annotation annotation) { public Binder build(Annotation annotation) {
return new Binder<PreKeyBinder, PreKey>() { return new Binder<PreKeyBinder, KeyRecord>() {
@Override @Override
public void bind(SQLStatement<?> sql, PreKeyBinder accountBinder, PreKey preKey) public void bind(SQLStatement<?> sql, PreKeyBinder accountBinder, KeyRecord record)
{ {
sql.bind("id", preKey.getId()); sql.bind("id", record.getId());
sql.bind("number", preKey.getNumber()); sql.bind("number", record.getNumber());
sql.bind("key_id", preKey.getKeyId()); sql.bind("device_id", record.getDeviceId());
sql.bind("public_key", preKey.getPublicKey()); sql.bind("key_id", record.getKeyId());
sql.bind("identity_key", preKey.getIdentityKey()); sql.bind("public_key", record.getPublicKey());
sql.bind("last_resort", preKey.isLastResort() ? 1 : 0); sql.bind("last_resort", record.isLastResort() ? 1 : 0);
} }
}; };
} }
@@ -106,15 +139,14 @@ public abstract class Keys {
} }
public static class PreKeyMapper implements ResultSetMapper<PreKey> { public static class PreKeyMapper implements ResultSetMapper<KeyRecord> {
@Override @Override
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext) public KeyRecord map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException throws SQLException
{ {
return new PreKey(resultSet.getLong("id"), resultSet.getString("number"), return new KeyRecord(resultSet.getLong("id"), resultSet.getString("number"),
resultSet.getLong("key_id"), resultSet.getString("public_key"), resultSet.getLong("device_id"), resultSet.getLong("key_id"),
resultSet.getString("identity_key"), resultSet.getString("public_key"), resultSet.getInt("last_resort") == 1);
resultSet.getInt("last_resort") == 1);
} }
} }

View File

@@ -29,4 +29,9 @@ public interface PendingAccounts {
@SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number") @SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number")
String getCodeForNumber(@Bind("number") String number); String getCodeForNumber(@Bind("number") String number);
@SqlUpdate("DELETE FROM pending_accounts WHERE number = :number")
void remove(@Bind("number") String number);
@SqlUpdate("VACUUM pending_accounts")
public void vacuum();
} }

View File

@@ -34,29 +34,46 @@ public class PendingAccountsManager {
} }
public void store(String number, String code) { public void store(String number, String code) {
if (memcachedClient != null) { memcacheSet(number, code);
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
pendingAccounts.insert(number, code); pendingAccounts.insert(number, code);
} }
public void remove(String number) {
memcacheDelete(number);
pendingAccounts.remove(number);
}
public Optional<String> getCodeForNumber(String number) { public Optional<String> getCodeForNumber(String number) {
String code = null; Optional<String> code = memcacheGet(number);
if (memcachedClient != null) { if (!code.isPresent()) {
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number); code = Optional.fromNullable(pendingAccounts.getCodeForNumber(number));
}
if (code == null) { if (code.isPresent()) {
code = pendingAccounts.getCodeForNumber(number); memcacheSet(number, code.get());
if (code != null && memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
} }
} }
if (code != null) return Optional.of(code); return code;
else return Optional.absent(); }
private void memcacheSet(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
}
private Optional<String> memcacheGet(String number) {
if (memcachedClient != null) {
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
} else {
return Optional.absent();
}
}
private void memcacheDelete(String number) {
if (memcachedClient != null) {
memcachedClient.delete(MEMCACHE_PREFIX + number);
}
} }
} }

View File

@@ -0,0 +1,34 @@
/**
* Copyright (C) 2014 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.storage;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
public interface PendingDevices {
@SqlUpdate("WITH upsert AS (UPDATE pending_devices SET verification_code = :verification_code WHERE number = :number RETURNING *) " +
"INSERT INTO pending_devices (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)")
void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode);
@SqlQuery("SELECT verification_code FROM pending_devices WHERE number = :number")
String getCodeForNumber(@Bind("number") String number);
@SqlUpdate("DELETE FROM pending_devices WHERE number = :number")
void remove(@Bind("number") String number);
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (C) 2014 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.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
public class PendingDevicesManager {
private static final String MEMCACHE_PREFIX = "pending_devices";
private final PendingDevices pendingDevices;
private final MemcachedClient memcachedClient;
public PendingDevicesManager(PendingDevices pendingDevices,
MemcachedClient memcachedClient)
{
this.pendingDevices = pendingDevices;
this.memcachedClient = memcachedClient;
}
public void store(String number, String code) {
memcacheSet(number, code);
pendingDevices.insert(number, code);
}
public void remove(String number) {
memcacheDelete(number);
pendingDevices.remove(number);
}
public Optional<String> getCodeForNumber(String number) {
Optional<String> code = memcacheGet(number);
if (!code.isPresent()) {
code = Optional.fromNullable(pendingDevices.getCodeForNumber(number));
if (code.isPresent()) {
memcacheSet(number, code.get());
}
}
return code;
}
private void memcacheSet(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
}
private Optional<String> memcacheGet(String number) {
if (memcachedClient != null) {
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
} else {
return Optional.absent();
}
}
private void memcacheDelete(String number) {
if (memcachedClient != null) {
memcachedClient.delete(MEMCACHE_PREFIX + number);
}
}
}

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.storage;
public interface PubSubListener {
public void onPubSubMessage(PubSubMessage outgoingMessage);
}

View File

@@ -0,0 +1,156 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPubSub;
public class PubSubManager {
private static final String KEEPALIVE_CHANNEL = "KEEPALIVE";
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final ObjectMapper mapper = SystemMapper.getMapper();
private final SubscriptionListener baseListener = new SubscriptionListener();
private final Map<String, PubSubListener> listeners = new HashMap<>();
private final JedisPool jedisPool;
private boolean subscribed = false;
public PubSubManager(final JedisPool jedisPool) {
this.jedisPool = jedisPool;
initializePubSubWorker();
waitForSubscription();
}
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
listeners.put(address.serialize(), listener);
baseListener.subscribe(address.serialize());
}
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
if (listeners.get(address.serialize()) == listener) {
listeners.remove(address.serialize());
baseListener.unsubscribe(address.serialize());
}
}
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
return publish(address.serialize(), message);
}
private synchronized boolean publish(String channel, PubSubMessage message) {
try {
String serialized = mapper.writeValueAsString(message);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.publish(channel, serialized) != 0;
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
} catch (JsonProcessingException e) {
throw new AssertionError(e);
}
}
private synchronized void waitForSubscription() {
try {
while (!subscribed) {
wait();
}
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
private void initializePubSubWorker() {
new Thread("PubSubListener") {
@Override
public void run() {
for (;;) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.subscribe(baseListener, KEEPALIVE_CHANNEL);
logger.warn("**** Unsubscribed from holding channel!!! ******");
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
}
}
}.start();
new Thread("PubSubKeepAlive") {
@Override
public void run() {
for (;;) {
try {
Thread.sleep(20000);
publish(KEEPALIVE_CHANNEL, new PubSubMessage(0, "foo"));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}
}.start();
}
private class SubscriptionListener extends JedisPubSub {
@Override
public void onMessage(String channel, String message) {
try {
PubSubListener listener;
synchronized (PubSubManager.this) {
listener = listeners.get(channel);
}
if (listener != null) {
listener.onPubSubMessage(mapper.readValue(message, PubSubMessage.class));
}
} catch (IOException e) {
logger.warn("IOE", e);
}
}
@Override
public void onPMessage(String s, String s2, String s3) {
logger.warn("Received PMessage!");
}
@Override
public void onSubscribe(String channel, int count) {
if (KEEPALIVE_CHANNEL.equals(channel)) {
synchronized (PubSubManager.this) {
subscribed = true;
PubSubManager.this.notifyAll();
}
}
}
@Override
public void onUnsubscribe(String s, int i) {}
@Override
public void onPUnsubscribe(String s, int i) {}
@Override
public void onPSubscribe(String s, int i) {}
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class PubSubMessage {
public static final int TYPE_QUERY_DB = 1;
public static final int TYPE_DELIVER = 2;
@JsonProperty
private int type;
@JsonProperty
private String contents;
public PubSubMessage() {}
public PubSubMessage(int type, String contents) {
this.type = type;
this.contents = contents;
}
public int getType() {
return type;
}
public String getContents() {
return contents;
}
}

View File

@@ -0,0 +1,118 @@
/**
* Copyright (C) 2014 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.storage;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static com.codahale.metrics.MetricRegistry.name;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class StoredMessages {
private static final Logger logger = LoggerFactory.getLogger(StoredMessages.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
private static final ObjectMapper mapper = SystemMapper.getMapper();
private static final String QUEUE_PREFIX = "msgs";
private final JedisPool jedisPool;
public StoredMessages(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public void clear(WebsocketAddress address) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(getKey(address));
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
}
public void insert(WebsocketAddress address, PendingMessage message) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String serializedMessage = mapper.writeValueAsString(message);
long queueSize = jedis.lpush(getKey(address), serializedMessage);
queueSizeHistogram.update(queueSize);
if (queueSize > 1000) {
jedis.ltrim(getKey(address), 0, 999);
}
} catch (JsonProcessingException e) {
logger.warn("StoredMessages", "Unable to store correctly", e);
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
}
public List<PendingMessage> getMessagesForDevice(WebsocketAddress address) {
List<PendingMessage> messages = new LinkedList<>();
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String message;
while ((message = jedis.rpop(getKey(address))) != null) {
try {
messages.add(mapper.readValue(message, PendingMessage.class));
} catch (IOException e) {
logger.warn("StoredMessages", "Not a valid PendingMessage", e);
}
}
return messages;
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
}
private String getKey(WebsocketAddress address) {
return QUEUE_PREFIX + ":" + address.serialize();
}
}

View File

@@ -1241,7 +1241,7 @@ public class Base64
* @since 1.4 * @since 1.4
*/ */
public static byte[] decode( String s ) throws java.io.IOException { public static byte[] decode( String s ) throws java.io.IOException {
return decode( s, NO_OPTIONS ); return decode( s, DONT_GUNZIP );
} }

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.util;
public class Constants {
public static final String METRICS_NAME = "textsecure";
}

View File

@@ -19,7 +19,7 @@ package org.whispersystems.textsecuregcm.util;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
public class IterablePair <T1, T2> implements Iterable<IterablePair.Pair<T1,T2>> { public class IterablePair <T1, T2> implements Iterable<Pair<T1,T2>> {
private final List<T1> first; private final List<T1> first;
private final List<T2> second; private final List<T2> second;
@@ -33,24 +33,6 @@ public class IterablePair <T1, T2> implements Iterable<IterablePair.Pair<T1,T2>>
return new ParallelIterator<>( first.iterator(), second.iterator() ); return new ParallelIterator<>( first.iterator(), second.iterator() );
} }
public static class Pair<T1, T2> {
private final T1 v1;
private final T2 v2;
Pair(T1 v1, T2 v2) {
this.v1 = v1;
this.v2 = v2;
}
public T1 first(){
return v1;
}
public T2 second(){
return v2;
}
}
public static class ParallelIterator <T1, T2> implements Iterator<Pair<T1, T2>> { public static class ParallelIterator <T1, T2> implements Iterator<Pair<T1, T2>> {
private final Iterator<T1> it1; private final Iterator<T1> it1;

Some files were not shown because too many files have changed in this diff Show More