Compare commits

...

57 Commits
v0.5 ... v0.25

Author SHA1 Message Date
Moxie Marlinspike
ac96f906b3 Bump version to 0.25
// FREEBIE
2014-12-02 15:37:40 -08:00
Moxie Marlinspike
cc395e914f Fix APN push payload.
// FREEBIE
2014-12-01 14:01:53 -08:00
Moxie Marlinspike
f8063f8faf Add feedback handler.
// FREEBIE
2014-12-01 13:27:06 -08:00
Moxie Marlinspike
958ada9110 Bump dropwizard version.
// FREEBIE
2014-12-01 12:10:14 -08:00
Moxie Marlinspike
3452ea29b8 Use push microservice instead of doing push directly.
// FREEBIE
2014-12-01 11:23:29 -08:00
Moxie Marlinspike
675b6f4b5e Update APN payload.
// FREEBIE
2014-11-27 18:20:23 -08:00
Moxie Marlinspike
4fab67b0f5 Switch to production APN endpoint.
// FREEBIE
2014-11-27 16:25:02 -08:00
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
115 changed files with 5311 additions and 2248 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

@@ -15,6 +15,7 @@ nexmo:
number: number:
gcm: gcm:
senderId:
apiKey: apiKey:
# Optional. Only if iOS clients are supported. # Optional. Only if iOS clients are supported.
@@ -56,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
@@ -75,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

142
pom.xml
View File

@@ -9,24 +9,78 @@
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<version>0.5</version> <version>0.25</version>
<properties>
<dropwizard.version>0.7.1</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>io.dropwizard</groupId>
<artifactId>dropwizard-client</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 +110,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 +120,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,26 +131,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>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-websocket</artifactId>
<version>8.1.14.v20131031</version>
</dependency>
<dependency>
<groupId>org.coursera</groupId>
<artifactId>metrics-datadog</artifactId>
<version>0.1.5</version>
</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,6 +20,15 @@ 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 uint32 sourceDevice = 7;

View File

@@ -17,16 +17,16 @@
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.DataDogConfiguration;
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.PushConfiguration;
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;
@@ -35,6 +35,10 @@ 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.client.JerseyClientConfiguration;
import io.dropwizard.db.DataSourceFactory;
public class WhisperServerConfiguration extends Configuration { public class WhisperServerConfiguration extends Configuration {
@NotNull @NotNull
@@ -46,8 +50,9 @@ public class WhisperServerConfiguration extends Configuration {
private NexmoConfiguration nexmo; private NexmoConfiguration nexmo;
@NotNull @NotNull
@Valid
@JsonProperty @JsonProperty
private GcmConfiguration gcm; private PushConfiguration push;
@NotNull @NotNull
@Valid @Valid
@@ -64,9 +69,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private RedisConfiguration redis; private RedisConfiguration redis;
@JsonProperty
private ApnConfiguration apn = new ApnConfiguration();
@Valid @Valid
@JsonProperty @JsonProperty
private FederationConfiguration federation = new FederationConfiguration(); private FederationConfiguration federation = new FederationConfiguration();
@@ -74,7 +76,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
@@ -87,12 +89,21 @@ public class WhisperServerConfiguration extends Configuration {
@Valid @Valid
@JsonProperty @JsonProperty
private DataDogConfiguration datadog = new DataDogConfiguration(); private MetricsConfiguration viz = new MetricsConfiguration();
@Valid @Valid
@JsonProperty @JsonProperty
private WebsocketConfiguration websocket = new WebsocketConfiguration(); private WebsocketConfiguration websocket = new WebsocketConfiguration();
@JsonProperty
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
@Valid
@NotNull
@JsonProperty
private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
public WebsocketConfiguration getWebsocketConfiguration() { public WebsocketConfiguration getWebsocketConfiguration() {
return websocket; return websocket;
} }
@@ -105,12 +116,12 @@ public class WhisperServerConfiguration extends Configuration {
return nexmo; return nexmo;
} }
public GcmConfiguration getGcmConfiguration() { public PushConfiguration getPushConfiguration() {
return gcm; return push;
} }
public ApnConfiguration getApnConfiguration() { public JerseyClientConfiguration getJerseyClientConfiguration() {
return apn; return httpClient;
} }
public S3Configuration getS3Configuration() { public S3Configuration getS3Configuration() {
@@ -125,7 +136,7 @@ public class WhisperServerConfiguration extends Configuration {
return redis; return redis;
} }
public DatabaseConfiguration getDatabaseConfiguration() { public DataSourceFactory getDataSourceFactory() {
return database; return database;
} }
@@ -141,7 +152,11 @@ public class WhisperServerConfiguration extends Configuration {
return graphite; return graphite;
} }
public DataDogConfiguration getDataDogConfiguration() { public MetricsConfiguration getMetricsConfiguration() {
return datadog; return viz;
}
public RedPhoneConfiguration getRedphoneConfiguration() {
return redphone;
} }
} }

View File

@@ -16,20 +16,14 @@
*/ */
package org.whispersystems.textsecuregcm; package org.whispersystems.textsecuregcm;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.yammer.dropwizard.Service; import com.sun.jersey.api.client.Client;
import com.yammer.dropwizard.config.Bootstrap;
import com.yammer.dropwizard.config.Environment;
import com.yammer.dropwizard.config.HttpConfiguration;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import com.yammer.dropwizard.jdbi.DBIFactory;
import com.yammer.dropwizard.migrations.MigrationsBundle;
import com.yammer.metrics.core.Clock;
import com.yammer.metrics.core.MetricPredicate;
import com.yammer.metrics.reporting.DatadogReporter;
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;
@@ -39,20 +33,31 @@ import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController; import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DeviceController;
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.WebsocketControllerFactory; 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.FeedbackHandler;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.PushServiceClient;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender; import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
@@ -66,18 +71,35 @@ import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingDevices; import org.whispersystems.textsecuregcm.storage.PendingDevices;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages; import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.CORSHeaderFilter; 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.client.JerseyClientBuilder;
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());
@@ -85,94 +107,129 @@ 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
{ {
config.getHttpConfiguration().setConnectorType(HttpConfiguration.ConnectorType.NONBLOCKING); 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); PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class);
Keys keys = jdbi.onDemand(Keys.class); Keys keys = jdbi.onDemand(Keys.class);
StoredMessages storedMessages = jdbi.onDemand(StoredMessages.class );
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient(); MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool(); JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
.build(getName());
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); PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, memcachedClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration()); FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
PubSubManager pubSubManager = new PubSubManager(redisClient); StoredMessages storedMessages = new StoredMessages(redisClient);
StoredMessageManager storedMessageManager = new StoredMessageManager(storedMessages, pubSubManager); PubSubManager pubSubManager = new PubSubManager(redisClient);
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
WebsocketSender websocketSender = new WebsocketSender(storedMessages, pubSubManager);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager); TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient); 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(pushServiceClient, websocketSender);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration()); environment.lifecycle().manage(feedbackHandler);
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(config.getGcmConfiguration(),
config.getApnConfiguration(),
storedMessageManager,
accountsManager);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner); AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, federatedClientManager); KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager); MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()), environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
FederatedPeer.class, FederatedPeer.class,
deviceAuthenticator, deviceAuthenticator,
Device.class, "WhisperServer")); Device.class, "WhisperServer"));
environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender)); environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, storedMessages, new TimeProvider(), authorizationKey));
environment.addResource(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters)); environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.addResource(new DirectoryController(rateLimiters, directory)); environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.addResource(new FederationController(accountsManager, attachmentController, keysController, messageController)); environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
environment.addResource(attachmentController); environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
environment.addResource(keysController); environment.jersey().register(new ReceiptController(accountsManager, federatedClientManager, pushSender));
environment.addResource(messageController); environment.jersey().register(attachmentController);
environment.jersey().register(keysControllerV1);
environment.jersey().register(keysControllerV2);
environment.jersey().register(messageController);
if (config.getWebsocketConfiguration().isEnabled()) { if (config.getWebsocketConfiguration().isEnabled()) {
environment.addServlet(new WebsocketControllerFactory(deviceAuthenticator, storedMessageManager, pubSubManager), WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment);
"/v1/websocket/"); webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
environment.addFilter(new CORSHeaderFilter(), "/*"); 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.addHealthCheck(new RedisHealthCheck(redisClient)); environment.healthChecks().register("redis", new RedisHealthCheck(redisClient));
environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient)); environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
environment.addProvider(new IOExceptionMapper()); environment.jersey().register(new IOExceptionMapper());
environment.addProvider(new RateLimitExceededExceptionMapper()); 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.getDataDogConfiguration().isEnabled()) { if (config.getMetricsConfiguration().isEnabled()) {
new DatadogReporter.Builder().withApiKey(config.getDataDogConfiguration().getApiKey()) new JsonMetricsReporter(environment.metrics(),
.withVmMetricsEnabled(true) config.getMetricsConfiguration().getToken(),
.build() config.getMetricsConfiguration().getHost())
.start(15, TimeUnit.SECONDS); .start(60, TimeUnit.SECONDS);
} }
} }

View File

@@ -16,29 +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.Device;
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);

View File

@@ -25,13 +25,13 @@ import java.io.IOException;
public class AuthorizationHeader { public class AuthorizationHeader {
private final String number; private final String number;
private final long accountId; private final long accountId;
private final String password; private final String password;
private AuthorizationHeader(String number, long accountId, String password) { private AuthorizationHeader(String number, long accountId, String password) {
this.number = number; this.number = number;
this.accountId = accountId; this.accountId = accountId;
this.password = password; this.password = password;
} }
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException { public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {

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

@@ -1,20 +0,0 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DataDogConfiguration {
@JsonProperty
private String apiKey;
@JsonProperty
private boolean enabled = false;
public String getApiKey() {
return apiKey;
}
public boolean isEnabled() {
return enabled && apiKey != null;
}
}

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

@@ -0,0 +1,40 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class PushConfiguration {
@JsonProperty
@NotEmpty
private String host;
@JsonProperty
@Min(1)
private int port;
@JsonProperty
@NotEmpty
private String username;
@JsonProperty
@NotEmpty
private String password;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}

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

@@ -16,10 +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.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;
@@ -28,15 +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.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.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;
@@ -55,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 {
@@ -64,16 +69,25 @@ public class AccountController {
private final AccountsManager accounts; private final AccountsManager accounts;
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final SmsSender smsSender; private final SmsSender smsSender;
private final 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,
SmsSender 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.smsSender = smsSenderFactory; this.smsSender = smsSenderFactory;
this.storedMessages = storedMessages;
this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey;
} }
@Timed @Timed
@@ -135,30 +149,50 @@ public class AccountController {
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());
} }
Device device = new Device(); if (accounts.isRelayListed(number)) {
device.setId(Device.MASTER_ID); throw new WebApplicationException(Response.status(417).build());
device.setAuthenticationCredentials(new AuthenticationCredentials(password)); }
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
Account account = new Account(); createAccount(number, password, accountAttributes);
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
pendingAccounts.remove(number);
logger.debug("Stored device...");
} 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
@@ -209,6 +243,26 @@ public class AccountController {
encodedVerificationText)).build(); encodedVerificationText)).build();
} }
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() { @VisibleForTesting protected VerificationCode generateVerificationCode() {
try { try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG");

View File

@@ -17,9 +17,8 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod; import com.amazonaws.HttpMethod;
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.Timed;
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;
@@ -44,6 +43,8 @@ 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 {

View File

@@ -16,10 +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.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;
@@ -29,8 +28,8 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.DeviceResponse; import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.VerificationCode; import org.whispersystems.textsecuregcm.util.VerificationCode;
@@ -48,6 +47,8 @@ import javax.ws.rs.core.Response;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import io.dropwizard.auth.Auth;
@Path("/v1/devices") @Path("/v1/devices")
public class DeviceController { public class DeviceController {

View File

@@ -16,9 +16,11 @@
*/ */
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.Timed;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
@@ -28,6 +30,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account; 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;
@@ -43,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;
@@ -56,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)
@@ -77,7 +85,7 @@ public class DirectoryController {
} }
} }
@Timed() @Timed
@PUT @PUT
@Path("/tokens") @Path("/tokens")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@@ -86,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,167 +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.google.common.base.Optional;
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.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
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.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;
@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,
private final AccountsManager accounts;
private final AttachmentController attachmentController;
private final KeysController keysController;
private final MessageController messageController;
public FederationController(AccountsManager accounts,
AttachmentController attachmentController, AttachmentController attachmentController,
KeysController keysController, MessageController messageController)
MessageController messageController)
{ {
this.accounts = accounts; this.accounts = accounts;
this.attachmentController = attachmentController; this.attachmentController = attachmentController;
this.keysController = keysController;
this.messageController = messageController; this.messageController = messageController;
} }
@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 PreKey getKey(@Auth FederatedPeer peer,
@PathParam("number") String number)
throws IOException
{
try {
return keysController.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 UnstructuredPreKeyList getKeys(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysController.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,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,45 +16,32 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth; import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyList;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
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.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; 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.LinkedList;
import java.util.List; import java.util.List;
@Path("/v1/keys") import io.dropwizard.auth.Auth;
public class KeysController { public class KeysController {
private final Logger logger = LoggerFactory.getLogger(KeysController.class); protected final RateLimiters rateLimiters;
protected final Keys keys;
private final RateLimiters rateLimiters; protected final AccountsManager accounts;
private final Keys keys; protected final FederatedClientManager federatedClientManager;
private final AccountsManager accounts;
private final FederatedClientManager federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts, public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager) FederatedClientManager federatedClientManager)
@@ -65,98 +52,65 @@ public class KeysController {
this.federatedClientManager = federatedClientManager; this.federatedClientManager = federatedClientManager;
} }
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
Device device = account.getAuthenticatedDevice().get();
keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey());
}
@Timed @Timed
@GET @GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public UnstructuredPreKeyList getDeviceKey(@Auth Account account, public PreKeyCount getStatus(@Auth Account account) {
@PathParam("number") String number, int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
Optional<UnstructuredPreKeyList> results; if (count > 0) {
count = count - 1;
if (!relay.isPresent()) results = getLocalKeys(number, deviceId);
else results = federatedClientManager.getClient(relay.get()).getKeys(number, deviceId);
if (results.isPresent()) return results.get();
else throw new WebApplicationException(Response.status(404).build());
} catch (NoSuchPeerException e) {
throw new WebApplicationException(Response.status(404).build());
} }
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") Optional<String> relay)
throws RateLimitExceededException
{ {
UnstructuredPreKeyList results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay);
return results.getKeys().get(0);
}
private Optional<UnstructuredPreKeyList> getLocalKeys(String number, String deviceIdSelector) {
Optional<Account> destination = accounts.get(number); Optional<Account> destination = accounts.get(number);
if (!destination.isPresent() || !destination.get().isActive()) { if (!destination.isPresent() || !destination.get().isActive()) {
return Optional.absent(); throw new NoSuchUserException("Target account is inactive");
} }
try { try {
if (deviceIdSelector.equals("*")) { if (deviceIdSelector.equals("*")) {
Optional<UnstructuredPreKeyList> preKeys = keys.get(number); Optional<List<KeyRecord>> preKeys = keys.get(number);
return getActiveKeys(destination.get(), preKeys); return new TargetKeys(destination.get(), preKeys);
} }
long deviceId = Long.parseLong(deviceIdSelector); long deviceId = Long.parseLong(deviceIdSelector);
Optional<Device> targetDevice = destination.get().getDevice(deviceId); Optional<Device> targetDevice = destination.get().getDevice(deviceId);
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) { if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
return Optional.absent(); throw new NoSuchUserException("Target device is inactive.");
} }
Optional<UnstructuredPreKeyList> preKeys = keys.get(number, deviceId); Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
return getActiveKeys(destination.get(), preKeys); return new TargetKeys(destination.get(), preKeys);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build()); throw new WebApplicationException(Response.status(422).build());
} }
} }
private Optional<UnstructuredPreKeyList> getActiveKeys(Account destination,
Optional<UnstructuredPreKeyList> preKeys)
{
if (!preKeys.isPresent()) return Optional.absent();
List<PreKey> filteredKeys = new LinkedList<>(); public static class TargetKeys {
private final Account destination;
private final Optional<List<KeyRecord>> keys;
for (PreKey preKey : preKeys.get().getKeys()) { public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
Optional<Device> device = destination.getDevice(preKey.getDeviceId()); this.destination = destination;
this.keys = keys;
if (device.isPresent() && device.get().isActive()) {
preKey.setRegistrationId(device.get().getRegistrationId());
filteredKeys.add(preKey);
}
} }
if (filteredKeys.isEmpty()) return Optional.absent(); public Optional<List<KeyRecord>> getKeys() {
else return Optional.of(new UnstructuredPreKeyList(filteredKeys)); 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,10 +16,9 @@
*/ */
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
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.entities.IncomingMessage; import org.whispersystems.textsecuregcm.entities.IncomingMessage;
@@ -56,6 +55,8 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import io.dropwizard.auth.Auth;
@Path("/v1/messages") @Path("/v1/messages")
public class MessageController { public class MessageController {
@@ -141,7 +142,7 @@ public class MessageController {
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId()); Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
if (destinationDevice.isPresent()) { if (destinationDevice.isPresent()) {
sendLocalMessage(source, destination, destinationDevice.get(), incomingMessage); sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
} }
} }
} }
@@ -149,6 +150,7 @@ public class MessageController {
private void sendLocalMessage(Account source, private void sendLocalMessage(Account source,
Account destinationAccount, Account destinationAccount,
Device destinationDevice, Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage) IncomingMessage incomingMessage)
throws NoSuchUserException, IOException throws NoSuchUserException, IOException
{ {
@@ -158,7 +160,7 @@ public class MessageController {
messageBuilder.setType(incomingMessage.getType()) messageBuilder.setType(incomingMessage.getType())
.setSource(source.getNumber()) .setSource(source.getNumber())
.setTimestamp(System.currentTimeMillis()) .setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId()); .setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
if (messageBody.isPresent()) { if (messageBody.isPresent()) {

View File

@@ -1,7 +1,6 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import java.util.List; import java.util.List;
import java.util.Set;
public class MismatchedDevicesException extends Exception { public class MismatchedDevicesException extends Exception {

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

@@ -1,155 +0,0 @@
package org.whispersystems.textsecuregcm.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.jetty.websocket.WebSocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
import org.whispersystems.textsecuregcm.entities.IncomingWebsocketMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.textsecuregcm.websocket.WebsocketMessage;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class WebsocketController implements WebSocket.OnTextMessage, PubSubListener {
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
private static final ObjectMapper mapper = new ObjectMapper();
private static final Map<Long, String> pendingMessages = new HashMap<>();
private final StoredMessageManager storedMessageManager;
private final PubSubManager pubSubManager;
private final Account account;
private final Device device;
private Connection connection;
private long pendingMessageSequence;
public WebsocketController(StoredMessageManager storedMessageManager,
PubSubManager pubSubManager,
Account account)
{
this.storedMessageManager = storedMessageManager;
this.pubSubManager = pubSubManager;
this.account = account;
this.device = account.getAuthenticatedDevice().get();
}
@Override
public void onOpen(Connection connection) {
this.connection = connection;
pubSubManager.subscribe(new WebsocketAddress(this.account.getId(), this.device.getId()), this);
handleQueryDatabase();
}
@Override
public void onClose(int i, String s) {
handleClose();
}
@Override
public void onMessage(String body) {
try {
IncomingWebsocketMessage incomingMessage = mapper.readValue(body, IncomingWebsocketMessage.class);
switch (incomingMessage.getType()) {
case IncomingWebsocketMessage.TYPE_ACKNOWLEDGE_MESSAGE: handleMessageAck(body); break;
case IncomingWebsocketMessage.TYPE_PING_MESSAGE: handlePing(); break;
default: handleClose(); break;
}
} catch (IOException e) {
logger.debug("Parse", e);
handleClose();
}
}
@Override
public void onPubSubMessage(PubSubMessage outgoingMessage) {
switch (outgoingMessage.getType()) {
case PubSubMessage.TYPE_DELIVER: handleDeliverOutgoingMessage(outgoingMessage.getContents()); break;
case PubSubMessage.TYPE_QUERY_DB: handleQueryDatabase(); break;
default:
logger.warn("Unknown pubsub message: " + outgoingMessage.getType());
}
}
private void handleDeliverOutgoingMessage(String message) {
try {
long messageSequence;
synchronized (pendingMessages) {
messageSequence = pendingMessageSequence++;
pendingMessages.put(messageSequence, message);
}
connection.sendMessage(mapper.writeValueAsString(new WebsocketMessage(messageSequence, message)));
} catch (IOException e) {
logger.debug("Response failed", e);
handleClose();
}
}
private void handleMessageAck(String message) {
try {
AcknowledgeWebsocketMessage ack = mapper.readValue(message, AcknowledgeWebsocketMessage.class);
synchronized (pendingMessages) {
pendingMessages.remove(ack.getId());
}
} catch (IOException e) {
logger.warn("Mapping", e);
}
}
private void handlePing() {
try {
IncomingWebsocketMessage pongMessage = new IncomingWebsocketMessage(IncomingWebsocketMessage.TYPE_PONG_MESSAGE);
connection.sendMessage(mapper.writeValueAsString(pongMessage));
} catch (IOException e) {
logger.warn("Pong failed", e);
handleClose();
}
}
private void handleClose() {
pubSubManager.unsubscribe(new WebsocketAddress(account.getId(), device.getId()), this);
connection.close();
List<String> remainingMessages = new LinkedList<>();
synchronized (pendingMessages) {
Long[] pendingKeys = pendingMessages.keySet().toArray(new Long[0]);
Arrays.sort(pendingKeys);
for (long pendingKey : pendingKeys) {
remainingMessages.add(pendingMessages.get(pendingKey));
}
pendingMessages.clear();
}
storedMessageManager.storeMessages(account, device, remainingMessages);
}
private void handleQueryDatabase() {
List<String> messages = storedMessageManager.getOutgoingMessages(account, device);
for (String message : messages) {
handleDeliverOutgoingMessage(message);
}
}
}

View File

@@ -1,98 +0,0 @@
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.basic.BasicCredentials;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
public class WebsocketControllerFactory extends WebSocketServlet {
private final Logger logger = LoggerFactory.getLogger(WebsocketControllerFactory.class);
private final StoredMessageManager storedMessageManager;
private final PubSubManager pubSubManager;
private final AccountAuthenticator accountAuthenticator;
private final LinkedHashMap<BasicCredentials, Optional<Account>> cache =
new LinkedHashMap<BasicCredentials, Optional<Account>>() {
@Override
protected boolean removeEldestEntry(Map.Entry<BasicCredentials, Optional<Account>> eldest) {
return size() > 10;
}
};
public WebsocketControllerFactory(AccountAuthenticator accountAuthenticator,
StoredMessageManager storedMessageManager,
PubSubManager pubSubManager)
{
this.accountAuthenticator = accountAuthenticator;
this.storedMessageManager = storedMessageManager;
this.pubSubManager = pubSubManager;
}
@Override
public WebSocket doWebSocketConnect(HttpServletRequest request, String s) {
try {
String username = request.getParameter("user");
String password = request.getParameter("password");
if (username == null || password == null) {
return null;
}
BasicCredentials credentials = new BasicCredentials(username, password);
Optional<Account> account = cache.remove(credentials);
if (account == null) {
account = accountAuthenticator.authenticate(new BasicCredentials(username, password));
}
if (!account.isPresent()) {
return null;
}
return new WebsocketController(storedMessageManager, pubSubManager, account.get());
} catch (AuthenticationException e) {
throw new AssertionError(e);
}
}
@Override
public boolean checkOrigin(HttpServletRequest request, String origin) {
try {
String username = request.getParameter("user");
String password = request.getParameter("password");
if (username == null || password == null) {
return false;
}
BasicCredentials credentials = new BasicCredentials(username, password);
Optional<Account> account = accountAuthenticator.authenticate(credentials);
if (!account.isPresent()) {
return false;
}
cache.put(credentials, account);
return true;
} catch (AuthenticationException e) {
logger.warn("Auth Failure", e);
return false;
}
}
}

View File

@@ -0,0 +1,34 @@
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.constraints.Min;
public class ApnMessage {
@JsonProperty
@NotEmpty
private String apnId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
@JsonProperty
@NotEmpty
private String message;
public ApnMessage() {}
public ApnMessage(String apnId, String number, int deviceId, String message) {
this.apnId = apnId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
}
}

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

@@ -41,23 +41,22 @@ 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 CryptoEncodingException {
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)

View File

@@ -0,0 +1,39 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class GcmMessage {
@JsonProperty
@NotEmpty
private String gcmId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
@JsonProperty
@NotEmpty
private String message;
@JsonProperty
private boolean receipt;
public GcmMessage() {}
public GcmMessage(String gcmId, String number, int deviceId, String message, boolean receipt) {
this.gcmId = gcmId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
this.receipt = receipt;
}
}

View File

@@ -41,7 +41,7 @@ public class IncomingMessage {
private String relay; private String relay;
@JsonProperty @JsonProperty
private long timestamp; private long timestamp; // deprecated
public String getDestination() { public String getDestination() {

View File

@@ -32,6 +32,9 @@ public class IncomingMessageList {
@JsonProperty @JsonProperty
private String relay; private String relay;
@JsonProperty
private long timestamp;
public IncomingMessageList() {} public IncomingMessageList() {}
public List<IncomingMessage> getMessages() { public List<IncomingMessage> getMessages() {
@@ -45,4 +48,8 @@ public class IncomingMessageList {
public void setRelay(String relay) { public void setRelay(String relay) {
this.relay = relay; this.relay = relay;
} }
public long getTimestamp() {
return timestamp;
}
} }

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

@@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@@ -26,36 +25,36 @@ import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
public class UnstructuredPreKeyList { public class PreKeyResponseV1 {
@JsonProperty @JsonProperty
@NotNull @NotNull
@Valid @Valid
private List<PreKey> keys; private List<PreKeyV1> keys;
@VisibleForTesting @VisibleForTesting
public UnstructuredPreKeyList() {} public PreKeyResponseV1() {}
public UnstructuredPreKeyList(PreKey preKey) { public PreKeyResponseV1(PreKeyV1 preKey) {
this.keys = new LinkedList<PreKey>(); this.keys = new LinkedList<>();
this.keys.add(preKey); this.keys.add(preKey);
} }
public UnstructuredPreKeyList(List<PreKey> preKeys) { public PreKeyResponseV1(List<PreKeyV1> preKeys) {
this.keys = preKeys; this.keys = preKeys;
} }
public List<PreKey> getKeys() { public List<PreKeyV1> getKeys() {
return keys; return keys;
} }
@VisibleForTesting @VisibleForTesting
public boolean equals(Object o) { public boolean equals(Object o) {
if (!(o instanceof UnstructuredPreKeyList) || if (!(o instanceof PreKeyResponseV1) ||
((UnstructuredPreKeyList) o).keys.size() != keys.size()) ((PreKeyResponseV1) o).keys.size() != keys.size())
return false; return false;
Iterator<PreKey> otherKeys = ((UnstructuredPreKeyList) o).keys.iterator(); Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
for (PreKey key : keys) { for (PreKeyV1 key : keys) {
if (!otherKeys.next().equals(key)) if (!otherKeys.next().equals(key))
return false; return false;
} }
@@ -64,7 +63,7 @@ public class UnstructuredPreKeyList {
public int hashCode() { public int hashCode() {
int ret = 0xFBA4C795 * keys.size(); int ret = 0xFBA4C795 * keys.size();
for (PreKey key : keys) for (PreKeyV1 key : keys)
ret ^= key.getPublicKey().hashCode(); ret ^= key.getPublicKey().hashCode();
return ret; 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,14 @@
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
private long id;
@JsonIgnore
private String number;
@JsonProperty @JsonProperty
private long deviceId; private long deviceId;
@@ -49,78 +41,43 @@ public class PreKey {
@NotNull @NotNull
private String identityKey; private String identityKey;
@JsonProperty
private boolean lastResort;
@JsonProperty @JsonProperty
private int registrationId; private int registrationId;
public PreKey() {} public PreKeyV1() {}
public PreKey(long id, String number, long deviceId, long keyId, public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
String publicKey, String identityKey, {
boolean lastResort) this.deviceId = deviceId;
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.registrationId = registrationId;
}
@VisibleForTesting
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey)
{ {
this.id = id;
this.number = number;
this.deviceId = deviceId; 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) {
this.identityKey = identityKey;
}
@XmlTransient
public boolean isLastResort() {
return lastResort;
}
public void setLastResort(boolean lastResort) {
this.lastResort = lastResort;
}
public void setDeviceId(long deviceId) { public void setDeviceId(long deviceId) {
this.deviceId = deviceId; this.deviceId = deviceId;
} }

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

@@ -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,33 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class UnregisteredEvent {
@JsonProperty
@NotEmpty
private String registrationId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
public String getRegistrationId() {
return registrationId;
}
public String getNumber() {
return number;
}
public int getDeviceId() {
return deviceId;
}
}

View File

@@ -0,0 +1,17 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class UnregisteredEventList {
@JsonProperty
private List<UnregisteredEvent> devices;
public List<UnregisteredEvent> getDevices() {
if (devices == null) return new LinkedList<>();
else return devices;
}
}

View File

@@ -36,7 +36,8 @@ 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.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
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;
@@ -62,11 +63,13 @@ 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/messages/%s/%d/%s"; private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
private static final String PREKEY_PATH_DEVICE = "/v1/federation/key/%s/%s"; private static final String 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;
@@ -107,9 +110,9 @@ public class FederatedClient {
} }
} }
public Optional<UnstructuredPreKeyList> getKeys(String destination, String device) { public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE, destination, device)); WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device));
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON) ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader) .header("Authorization", authorizationHeader)
@@ -119,7 +122,7 @@ public class FederatedClient {
throw new WebApplicationException(clientResponseToResponse(response)); throw new WebApplicationException(clientResponseToResponse(response));
} }
return Optional.of(response.getEntity(UnstructuredPreKeyList.class)); return Optional.of(response.getEntity(PreKeyResponseV1.class));
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e); logger.warn("PreKey", e);
@@ -127,6 +130,27 @@ public class FederatedClient {
} }
} }
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);
@@ -174,6 +198,25 @@ public class FederatedClient {
} }
} }
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) {
throw new WebApplicationException(clientResponseToResponse(response));
}
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
}
}
private String getAuthorizationHeader(String federationName, FederatedPeer peer) { private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes()); return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
} }

View File

@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
@Override @Override
public Optional<Device> getAuthenticatedDevice() { public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0)); 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

@@ -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

@@ -1,116 +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.push;
import com.google.common.base.Optional;
import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsService;
import com.notnoop.exceptions.NetworkIOException;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
public class APNSender {
private final Meter success = Metrics.newMeter(APNSender.class, "sent", "success", TimeUnit.MINUTES);
private final Meter failure = Metrics.newMeter(APNSender.class, "sent", "failure", TimeUnit.MINUTES);
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
private static final String MESSAGE_BODY = "m";
private final Optional<ApnsService> apnService;
public APNSender(String apnCertificate, String apnKey)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
{
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
this.apnService = Optional.of(APNS.newService()
.withCert(new ByteArrayInputStream(keyStore), "insecure")
.withSandboxDestination().build());
} else {
this.apnService = Optional.absent();
}
}
public void sendMessage(String registrationId, EncryptedOutgoingMessage message)
throws TransientPushFailureException, NotPushRegisteredException
{
try {
if (!apnService.isPresent()) {
failure.mark();
throw new TransientPushFailureException("APN access not configured!");
}
String payload = APNS.newPayload()
.alertBody("Message!")
.customField(MESSAGE_BODY, message.serialize())
.build();
logger.debug("APN Payload: " + payload);
apnService.get().push(registrationId, payload);
success.mark();
} catch (NetworkIOException nioe) {
logger.warn("Network Error", nioe);
failure.mark();
throw new TransientPushFailureException(nioe);
} catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e);
}
}
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException
{
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
X509Certificate certificate = (X509Certificate) reader.readObject();
Certificate[] certificateChain = {certificate};
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes())));
KeyPair keyPair = (KeyPair) reader.readObject();
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(null);
keyStore.setEntry("apn",
new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), certificateChain),
new KeyStore.PasswordProtection("insecure".toCharArray()));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
keyStore.store(baos, "insecure".toCharArray());
return baos.toByteArray();
}
}

View File

@@ -0,0 +1,99 @@
package org.whispersystems.textsecuregcm.push;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import io.dropwizard.lifecycle.Managed;
public class FeedbackHandler implements Managed, Runnable {
private final Logger logger = LoggerFactory.getLogger(PushServiceClient.class);
private final PushServiceClient client;
private final AccountsManager accountsManager;
private ScheduledExecutorService executor;
public FeedbackHandler(PushServiceClient client, AccountsManager accountsManager) {
this.client = client;
this.accountsManager = accountsManager;
}
@Override
public void start() throws Exception {
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(this, 0, 10, TimeUnit.MINUTES);
}
@Override
public void stop() throws Exception {
if (this.executor != null) {
this.executor.shutdown();
}
}
@Override
public void run() {
try {
List<UnregisteredEvent> gcmFeedback = client.getGcmFeedback();
List<UnregisteredEvent> apnFeedback = client.getApnFeedback();
for (UnregisteredEvent gcmEvent : gcmFeedback) {
handleGcmUnregistered(gcmEvent);
}
for (UnregisteredEvent apnEvent : apnFeedback) {
handleApnUnregistered(apnEvent);
}
} catch (IOException e) {
logger.warn("Error retrieving feedback: ", e);
}
}
private void handleGcmUnregistered(UnregisteredEvent event) {
logger.warn("Got GCM Unregistered: " + event.getNumber() + "," + event.getDeviceId());
Optional<Account> account = accountsManager.get(event.getNumber());
if (account.isPresent()) {
Optional<Device> device = account.get().getDevice(event.getDeviceId());
if (device.isPresent()) {
if (event.getRegistrationId().equals(device.get().getGcmId())) {
logger.warn("GCM Unregister GCM ID matches!");
device.get().setGcmId(null);
accountsManager.update(account.get());
}
}
}
}
private void handleApnUnregistered(UnregisteredEvent event) {
logger.warn("Got APN Unregistered: " + event.getNumber() + "," + event.getDeviceId());
Optional<Account> account = accountsManager.get(event.getNumber());
if (account.isPresent()) {
Optional<Device> device = account.get().getDevice(event.getDeviceId());
if (device.isPresent()) {
if (event.getRegistrationId().equals(device.get().getApnId())) {
logger.warn("APN Unregister APN ID matches!");
device.get().setApnId(null);
accountsManager.update(account.get());
}
}
}
}
}

View File

@@ -1,69 +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.push;
import com.google.android.gcm.server.Constants;
import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class GCMSender {
private final Meter success = Metrics.newMeter(GCMSender.class, "sent", "success", TimeUnit.MINUTES);
private final Meter failure = Metrics.newMeter(GCMSender.class, "sent", "failure", TimeUnit.MINUTES);
private final Sender sender;
public GCMSender(String apiKey) {
this.sender = new Sender(apiKey);
}
public String sendMessage(String gcmRegistrationId, EncryptedOutgoingMessage outgoingMessage)
throws NotPushRegisteredException, TransientPushFailureException
{
try {
Message gcmMessage = new Message.Builder().addData("type", "message")
.addData("message", outgoingMessage.serialize())
.build();
Result result = sender.send(gcmMessage, gcmRegistrationId, 5);
if (result.getMessageId() != null) {
success.mark();
return result.getCanonicalRegistrationId();
} else {
failure.mark();
if (result.getErrorCodeName().equals(Constants.ERROR_NOT_REGISTERED)) {
throw new NotPushRegisteredException("Device no longer registered with GCM.");
} else {
throw new TransientPushFailureException("GCM Failed: " + result.getErrorCodeName());
}
}
} catch (IOException e) {
throw new TransientPushFailureException(e);
} catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e);
}
}
}

View File

@@ -18,92 +18,84 @@ package org.whispersystems.textsecuregcm.push;
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.ApnMessage;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException; import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.entities.GcmMessage;
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.Device;
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
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 static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"},\"content-available\":1,\"category\":\"Signal_Message\"}}";
private final GCMSender gcmSender;
private final APNSender apnSender;
private final StoredMessageManager storedMessageManager;
public PushSender(GcmConfiguration gcmConfiguration, private final PushServiceClient pushServiceClient;
ApnConfiguration apnConfiguration, private final WebsocketSender webSocketSender;
StoredMessageManager storedMessageManager,
AccountsManager accounts) public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException this.pushServiceClient = pushServiceClient;
{ this.webSocketSender = websocketSender;
this.accounts = accounts;
this.storedMessageManager = storedMessageManager;
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
} }
public void sendMessage(Account account, Device device, MessageProtos.OutgoingMessageSignal outgoingMessage) public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
throws NotPushRegisteredException, TransientPushFailureException
{
String signalingKey = device.getSignalingKey();
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
else if (device.getApnId() != null) sendApnMessage(account, device, message);
else if (device.getFetchesMessages()) storeFetchedMessage(account, device, message);
else throw new NotPushRegisteredException("No delivery possible!");
}
private void sendGcmMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
throws NotPushRegisteredException, TransientPushFailureException throws NotPushRegisteredException, TransientPushFailureException
{ {
try { try {
String canonicalId = gcmSender.sendMessage(device.getGcmId(), outgoingMessage); boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
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);
device.setGcmId(canonicalId);
accounts.update(account);
}
} catch (NotPushRegisteredException e) {
logger.debug("No Such User", e);
device.setGcmId(null);
accounts.update(account);
throw new NotPushRegisteredException(e);
}
}
private void sendApnMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
throws TransientPushFailureException, NotPushRegisteredException
{
try {
apnSender.sendMessage(device.getApnId(), outgoingMessage);
} catch (NotPushRegisteredException e) {
device.setApnId(null);
accounts.update(account);
throw new NotPushRegisteredException(e);
}
}
private void storeFetchedMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
throws NotPushRegisteredException
{
try {
storedMessageManager.storeMessage(account, device, outgoingMessage);
} catch (CryptoEncodingException e) { } catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e); throw new NotPushRegisteredException(e);
} }
} }
public void sendMessage(Account account, Device device, PendingMessage pendingMessage)
throws NotPushRegisteredException, TransientPushFailureException
{
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)
throws TransientPushFailureException
{
String number = account.getNumber();
long deviceId = device.getId();
String registrationId = device.getGcmId();
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int)deviceId,
pendingMessage.getEncryptedOutgoingMessage(),
pendingMessage.isReceipt() );
pushServiceClient.send(gcmMessage);
}
private void sendApnMessage(Account account, Device device, PendingMessage outgoingMessage)
throws TransientPushFailureException
{
boolean online = webSocketSender.sendMessage(account, device, outgoingMessage, true);
if (!online && !outgoingMessage.isReceipt()) {
ApnMessage apnMessage = new ApnMessage(device.getApnId(), account.getNumber(),
(int)device.getId(), APN_PAYLOAD);
pushServiceClient.send(apnMessage);
}
}
private void sendWebSocketMessage(Account account, Device device, PendingMessage outgoingMessage)
{
webSocketSender.sendMessage(account, device, outgoingMessage, false);
}
} }

View File

@@ -0,0 +1,91 @@
package org.whispersystems.textsecuregcm.push;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.entities.GcmMessage;
import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
import org.whispersystems.textsecuregcm.entities.UnregisteredEventList;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.List;
public class PushServiceClient {
private static final String PUSH_GCM_PATH = "/api/v1/push/gcm";
private static final String PUSH_APN_PATH = "/api/v1/push/apn";
private static final String APN_FEEDBACK_PATH = "/api/v1/feedback/apn";
private static final String GCM_FEEDBACK_PATH = "/api/v1/feedback/gcm";
private final Logger logger = LoggerFactory.getLogger(PushServiceClient.class);
private final Client client;
private final String host;
private final int port;
private final String authorization;
public PushServiceClient(Client client, PushConfiguration config) {
this.client = client;
this.host = config.getHost();
this.port = config.getPort();
this.authorization = getAuthorizationHeader(config.getUsername(), config.getPassword());
}
public void send(GcmMessage message) throws TransientPushFailureException {
sendPush(PUSH_GCM_PATH, message);
}
public void send(ApnMessage message) throws TransientPushFailureException {
sendPush(PUSH_APN_PATH, message);
}
public List<UnregisteredEvent> getGcmFeedback() throws IOException {
return getFeedback(GCM_FEEDBACK_PATH);
}
public List<UnregisteredEvent> getApnFeedback() throws IOException {
return getFeedback(APN_FEEDBACK_PATH);
}
private void sendPush(String path, Object entity) throws TransientPushFailureException {
try {
ClientResponse response = client.resource("http://" + host + ":" + port + path)
.header("Authorization", authorization)
.entity(entity, MediaType.APPLICATION_JSON)
.put(ClientResponse.class);
if (response.getStatus() != 204 && response.getStatus() != 200) {
logger.warn("PushServer response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase());
throw new TransientPushFailureException("Bad response: " + response.getStatus());
}
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("Push error: ", e);
throw new TransientPushFailureException(e);
}
}
private List<UnregisteredEvent> getFeedback(String path) throws IOException {
try {
UnregisteredEventList unregisteredEvents = client.resource("http://" + host + ":" + port + path)
.header("Authorization", authorization)
.get(UnregisteredEventList.class);
return unregisteredEvents.getDevices();
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("Request error:", e);
throw new IOException(e);
}
}
private String getAuthorizationHeader(String username, String password) {
return "Basic " + Base64.encodeBytes((username + ":" + password).getBytes());
}
}

View File

@@ -0,0 +1,84 @@
/**
* 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 websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" ));
private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" ));
private final Meter apnOnlineMeter = metricRegistry.meter(name(getClass(), "apn_online" ));
private final Meter apnOfflineMeter = metricRegistry.meter(name(getClass(), "apn_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 boolean sendMessage(Account account, Device device, PendingMessage pendingMessage, boolean apn) {
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)) {
if (apn) apnOnlineMeter.mark();
else websocketOnlineMeter.mark();
return true;
} else {
if (apn) apnOfflineMeter.mark();
else websocketOfflineMeter.mark();
storedMessages.insert(address, pendingMessage);
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null));
return false;
}
} catch (JsonProcessingException e) {
logger.warn("WebsocketSender", "Unable to serialize json", e);
return false;
}
}
}

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,13 +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 static com.codahale.metrics.MetricRegistry.name;
public class NexmoSmsSender { 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";

View File

@@ -16,22 +16,25 @@
*/ */
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;
import static com.codahale.metrics.MetricRegistry.name;
public class TwilioSmsSender { public class TwilioSmsSender {
@@ -40,8 +43,9 @@ public class TwilioSmsSender {
" <Say voice=\"woman\" language=\"en\">" + SmsSender.VOX_VERIFICATION_TEXT + "%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;

View File

@@ -22,16 +22,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import java.io.Serializable;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
public class Account implements Serializable { public class Account {
public static final int MEMCACHE_VERION = 2; public static final int MEMCACHE_VERION = 5;
@JsonIgnore
private long id;
@JsonProperty @JsonProperty
private String number; private String number;
@@ -42,16 +38,14 @@ public class Account implements Serializable {
@JsonProperty @JsonProperty
private List<Device> devices = new LinkedList<>(); private List<Device> devices = new LinkedList<>();
@JsonProperty
private String identityKey;
@JsonIgnore @JsonIgnore
private Optional<Device> authenticatedDevice; private Optional<Device> authenticatedDevice;
public Account() {} public Account() {}
public Account(String number, boolean supportsSms) {
this.number = number;
this.supportsSms = supportsSms;
}
@VisibleForTesting @VisibleForTesting
public Account(String number, boolean supportsSms, List<Device> devices) { public Account(String number, boolean supportsSms, List<Device> devices) {
this.number = number; this.number = number;
@@ -59,14 +53,6 @@ public class Account implements Serializable {
this.devices = devices; this.devices = devices;
} }
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Optional<Device> getAuthenticatedDevice() { public Optional<Device> getAuthenticatedDevice() {
return authenticatedDevice; return authenticatedDevice;
} }
@@ -142,4 +128,12 @@ public class Account implements Serializable {
public Optional<String> getRelay() { public Optional<String> getRelay() {
return Optional.absent(); return Optional.absent();
} }
public void setIdentityKey(String identityKey) {
this.identityKey = identityKey;
}
public String getIdentityKey() {
return identityKey;
}
} }

View File

@@ -33,6 +33,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.util.SystemMapper;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
@@ -51,12 +52,7 @@ public abstract class Accounts {
private static final String NUMBER = "number"; private static final String NUMBER = "number";
private static final String DATA = "data"; private static final String DATA = "data";
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = SystemMapper.getMapper();
static {
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DATA + ") VALUES (:number, CAST(:data AS json))") @SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DATA + ") VALUES (:number, CAST(:data AS json))")
@GetGeneratedKeys @GetGeneratedKeys
@@ -89,6 +85,9 @@ public abstract class Accounts {
return insertStep(account); return insertStep(account);
} }
@SqlUpdate("VACUUM accounts")
public abstract void vacuum();
public static class AccountMapper implements ResultSetMapper<Account> { 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)
@@ -96,7 +95,7 @@ public abstract class Accounts {
{ {
try { try {
Account account = mapper.readValue(resultSet.getString(DATA), Account.class); Account account = mapper.readValue(resultSet.getString(DATA), Account.class);
account.setId(resultSet.getLong(ID)); // account.setId(resultSet.getLong(ID));
return account; return account;
} catch (IOException e) { } catch (IOException e) {

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() {
@@ -54,40 +66,35 @@ public class AccountsManager {
public void create(Account account) { public void create(Account account) {
accounts.create(account); accounts.create(account);
memcacheSet(account.getNumber(), account);
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) {
@@ -104,4 +111,30 @@ public class AccountsManager {
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

@@ -19,11 +19,12 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.io.Serializable; import java.io.Serializable;
public class Device implements Serializable { public class Device {
public static final long MASTER_ID = 1; public static final long MASTER_ID = 1;
@@ -51,11 +52,15 @@ public class Device implements Serializable {
@JsonProperty @JsonProperty
private int registrationId; private int registrationId;
@JsonProperty
private SignedPreKey signedPreKey;
public Device() {} public Device() {}
public Device(long id, String authToken, String salt, public Device(long id, String authToken, String salt,
String signalingKey, String gcmId, String apnId, String signalingKey, String gcmId, String apnId,
boolean fetchesMessages, int registrationId) boolean fetchesMessages, int registrationId,
SignedPreKey signedPreKey)
{ {
this.id = id; this.id = id;
this.authToken = authToken; this.authToken = authToken;
@@ -65,6 +70,7 @@ public class Device implements Serializable {
this.apnId = apnId; this.apnId = apnId;
this.fetchesMessages = fetchesMessages; this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId; this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
} }
public String getApnId() { public String getApnId() {
@@ -131,4 +137,12 @@ public class Device implements Serializable {
public void setRegistrationId(int registrationId) { public void setRegistrationId(int registrationId) {
this.registrationId = registrationId; this.registrationId = registrationId;
} }
public SignedPreKey getSignedPreKey() {
return signedPreKey;
}
public void setSignedPreKey(SignedPreKey signedPreKey) {
this.signedPreKey = signedPreKey;
}
} }

View File

@@ -76,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();
@@ -162,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

@@ -30,8 +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 org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
@@ -51,66 +50,71 @@ public abstract class Keys {
@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, device_id, key_id, public_key, identity_key, last_resort) VALUES " + @SqlBatch("INSERT INTO keys (number, device_id, key_id, public_key, last_resort) VALUES " +
"(:number, :device_id, :key_id, :public_key, :identity_key, :last_resort)") "(:number, :device_id, :key_id, :public_key, :last_resort)")
abstract void append(@PreKeyBinder List<PreKey> preKeys); abstract void append(@PreKeyBinder List<KeyRecord> preKeys);
@SqlUpdate("INSERT INTO keys (number, device_id, key_id, public_key, identity_key, last_resort) VALUES " +
"(:number, :device_id, :key_id, :public_key, :identity_key, :last_resort)")
abstract void append(@PreKeyBinder PreKey preKey);
@SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE") @SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE")
@Mapper(PreKeyMapper.class) @Mapper(PreKeyMapper.class)
abstract PreKey retrieveFirst(@Bind("number") String number, @Bind("device_id") long deviceId); 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") @SqlQuery("SELECT DISTINCT ON (number, device_id) * FROM keys WHERE number = :number ORDER BY number, device_id, key_id ASC")
@Mapper(PreKeyMapper.class) @Mapper(PreKeyMapper.class)
abstract List<PreKey> retrieveFirst(@Bind("number") String number); 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, long deviceId, List<PreKey> keys, PreKey lastResortKey) { 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);
key.setDeviceId(deviceId); 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.setDeviceId(deviceId); lastResortKey.getPublicKey(), true));
lastResortKey.setLastResort(true);
removeKeys(number, deviceId); removeKeys(number, deviceId);
append(keys); append(records);
append(lastResortKey);
} }
@Transaction(TransactionIsolationLevel.SERIALIZABLE) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public Optional<UnstructuredPreKeyList> get(String number, long deviceId) { public Optional<List<KeyRecord>> get(String number, long deviceId) {
PreKey preKey = retrieveFirst(number, deviceId); 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();
} }
if (preKey != null) return Optional.of(new UnstructuredPreKeyList(preKey)); List<KeyRecord> results = new LinkedList<>();
else return Optional.absent(); results.add(record);
return Optional.of(results);
} }
@Transaction(TransactionIsolationLevel.SERIALIZABLE) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public Optional<UnstructuredPreKeyList> get(String number) { public Optional<List<KeyRecord>> get(String number) {
List<PreKey> preKeys = retrieveFirst(number); List<KeyRecord> preKeys = retrieveFirst(number);
if (preKeys != null) { if (preKeys != null) {
for (PreKey preKey : preKeys) { for (KeyRecord preKey : preKeys) {
if (!preKey.isLastResort()) { if (!preKey.isLastResort()) {
removeKey(preKey.getId()); removeKey(preKey.getId());
} }
} }
} }
if (preKeys != null) return Optional.of(new UnstructuredPreKeyList(preKeys)); if (preKeys != null) return Optional.of(preKeys);
else return Optional.absent(); 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})
@@ -118,17 +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("device_id", preKey.getDeviceId()); sql.bind("device_id", record.getDeviceId());
sql.bind("key_id", preKey.getKeyId()); sql.bind("key_id", record.getKeyId());
sql.bind("public_key", preKey.getPublicKey()); sql.bind("public_key", record.getPublicKey());
sql.bind("identity_key", preKey.getIdentityKey()); sql.bind("last_resort", record.isLastResort() ? 1 : 0);
sql.bind("last_resort", preKey.isLastResort() ? 1 : 0);
} }
}; };
} }
@@ -136,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"), resultSet.getLong("device_id"), 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

@@ -31,4 +31,7 @@ public interface PendingAccounts {
@SqlUpdate("DELETE FROM pending_accounts WHERE number = :number") @SqlUpdate("DELETE FROM pending_accounts WHERE number = :number")
void remove(@Bind("number") String number); void remove(@Bind("number") String number);
@SqlUpdate("VACUUM pending_accounts")
public void vacuum();
} }

View File

@@ -34,35 +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) { public void remove(String number) {
if (memcachedClient != null) memcacheDelete(number);
memcachedClient.delete(MEMCACHE_PREFIX + number);
pendingAccounts.remove(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

@@ -34,37 +34,47 @@ public class PendingDevicesManager {
} }
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);
}
pendingDevices.insert(number, code); pendingDevices.insert(number, code);
} }
public void remove(String number) { public void remove(String number) {
if (memcachedClient != null) { memcacheDelete(number);
memcachedClient.delete(MEMCACHE_PREFIX + number);
}
pendingDevices.remove(number); pendingDevices.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(pendingDevices.getCodeForNumber(number));
}
if (code == null) { if (code.isPresent()) {
code = pendingDevices.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

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException; import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress; import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
@@ -17,10 +18,12 @@ import redis.clients.jedis.JedisPubSub;
public class PubSubManager { public class PubSubManager {
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class); private static final String KEEPALIVE_CHANNEL = "KEEPALIVE";
private final ObjectMapper mapper = new ObjectMapper();
private final SubscriptionListener baseListener = new SubscriptionListener(); private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final Map<WebsocketAddress, PubSubListener> listeners = new HashMap<>(); 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 final JedisPool jedisPool;
private boolean subscribed = false; private boolean subscribed = false;
@@ -32,25 +35,29 @@ public class PubSubManager {
} }
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) { public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
listeners.put(address, listener); listeners.put(address.serialize(), listener);
baseListener.subscribe(address.toString()); baseListener.subscribe(address.serialize());
} }
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) { public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
if (listeners.get(address) == listener) { if (listeners.get(address.serialize()) == listener) {
listeners.remove(address); listeners.remove(address.serialize());
baseListener.unsubscribe(address.toString()); baseListener.unsubscribe(address.serialize());
} }
} }
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) { public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
return publish(address.serialize(), message);
}
private synchronized boolean publish(String channel, PubSubMessage message) {
try { try {
String serialized = mapper.writeValueAsString(message); String serialized = mapper.writeValueAsString(message);
Jedis jedis = null; Jedis jedis = null;
try { try {
jedis = jedisPool.getResource(); jedis = jedisPool.getResource();
return jedis.publish(address.toString(), serialized) != 0; return jedis.publish(channel, serialized) != 0;
} finally { } finally {
if (jedis != null) if (jedis != null)
jedisPool.returnResource(jedis); jedisPool.returnResource(jedis);
@@ -78,7 +85,7 @@ public class PubSubManager {
Jedis jedis = null; Jedis jedis = null;
try { try {
jedis = jedisPool.getResource(); jedis = jedisPool.getResource();
jedis.subscribe(baseListener, new WebsocketAddress(0, 0).toString()); jedis.subscribe(baseListener, KEEPALIVE_CHANNEL);
logger.warn("**** Unsubscribed from holding channel!!! ******"); logger.warn("**** Unsubscribed from holding channel!!! ******");
} finally { } finally {
if (jedis != null) if (jedis != null)
@@ -94,7 +101,7 @@ public class PubSubManager {
for (;;) { for (;;) {
try { try {
Thread.sleep(20000); Thread.sleep(20000);
publish(new WebsocketAddress(0, 0), new PubSubMessage(0, "foo")); publish(KEEPALIVE_CHANNEL, new PubSubMessage(0, "foo"));
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@@ -108,18 +115,15 @@ public class PubSubManager {
@Override @Override
public void onMessage(String channel, String message) { public void onMessage(String channel, String message) {
try { try {
WebsocketAddress address = new WebsocketAddress(channel);
PubSubListener listener; PubSubListener listener;
synchronized (PubSubManager.this) { synchronized (PubSubManager.this) {
listener = listeners.get(address); listener = listeners.get(channel);
} }
if (listener != null) { if (listener != null) {
listener.onPubSubMessage(mapper.readValue(message, PubSubMessage.class)); listener.onPubSubMessage(mapper.readValue(message, PubSubMessage.class));
} }
} catch (InvalidWebsocketAddressException e) {
logger.warn("Address", e);
} catch (IOException e) { } catch (IOException e) {
logger.warn("IOE", e); logger.warn("IOE", e);
} }
@@ -132,16 +136,11 @@ public class PubSubManager {
@Override @Override
public void onSubscribe(String channel, int count) { public void onSubscribe(String channel, int count) {
try { if (KEEPALIVE_CHANNEL.equals(channel)) {
WebsocketAddress address = new WebsocketAddress(channel); synchronized (PubSubManager.this) {
if (address.getAccountId() == 0 && address.getDeviceId() == 0) { subscribed = true;
synchronized (PubSubManager.this) { PubSubManager.this.notifyAll();
subscribed = true;
PubSubManager.this.notifyAll();
}
} }
} catch (InvalidWebsocketAddressException e) {
logger.warn("Weird address", e);
} }
} }

View File

@@ -1,66 +0,0 @@
/**
* 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.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.util.List;
public class StoredMessageManager {
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
public StoredMessageManager(StoredMessages storedMessages, PubSubManager pubSubManager) {
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
}
public void storeMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
throws CryptoEncodingException
{
storeMessage(account, device, outgoingMessage.serialize());
}
public void storeMessages(Account account, Device device, List<String> serializedMessages) {
for (String serializedMessage : serializedMessages) {
storeMessage(account, device, serializedMessage);
}
}
private void storeMessage(Account account, Device device, String serializedMessage) {
if (device.getFetchesMessages()) {
WebsocketAddress address = new WebsocketAddress(account.getId(), device.getId());
PubSubMessage pubSubMessage = new PubSubMessage(PubSubMessage.TYPE_DELIVER, serializedMessage);
if (!pubSubManager.publish(address, pubSubMessage)) {
storedMessages.insert(account.getId(), device.getId(), serializedMessage);
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null));
}
return;
}
storedMessages.insert(account.getId(), device.getId(), serializedMessage);
}
public List<String> getOutgoingMessages(Account account, Device device) {
return storedMessages.getMessagesForDevice(account.getId(), device.getId());
}
}

View File

@@ -16,21 +16,103 @@
*/ */
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import org.skife.jdbi.v2.sqlobject.Bind; import com.codahale.metrics.Histogram;
import org.skife.jdbi.v2.sqlobject.SqlBatch; import com.codahale.metrics.MetricRegistry;
import org.skife.jdbi.v2.sqlobject.SqlQuery; import com.codahale.metrics.SharedMetricRegistries;
import org.skife.jdbi.v2.sqlobject.SqlUpdate; 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 java.util.List;
public interface StoredMessages { import static com.codahale.metrics.MetricRegistry.name;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@SqlUpdate("INSERT INTO messages (account_id, device_id, encrypted_message) VALUES (:account_id, :device_id, :encrypted_message)") public class StoredMessages {
void insert(@Bind("account_id") long accountId, @Bind("device_id") long deviceId, @Bind("encrypted_message") String encryptedOutgoingMessage);
@SqlBatch("INSERT INTO messages (account_id, device_id, encrypted_message) VALUES (:account_id, :device_id, :encrypted_message)") private static final Logger logger = LoggerFactory.getLogger(StoredMessages.class);
void insert(@Bind("account_id") long accountId, @Bind("device_id") long deviceId, @Bind("encrypted_message") List<String> encryptedOutgoingMessages);
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();
}
@SqlQuery("DELETE FROM messages WHERE account_id = :account_id AND device_id = :device_id RETURNING encrypted_message")
List<String> getMessagesForDevice(@Bind("account_id") long accountId, @Bind("device_id") long deviceId);
} }

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

@@ -1,41 +0,0 @@
/**
* 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.util;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CORSHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
((HttpServletResponse) response).addHeader("Access-Control-Allow-Origin", "*");
((HttpServletResponse) response).addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
((HttpServletResponse) response).addHeader("Access-Control-Allow-Headers", "Authorization, Content-type");
}
chain.doFilter(request, response);
}
@Override public void init(FilterConfig filterConfig) throws ServletException { }
@Override public void destroy() { }
}

View File

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

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SystemMapper {
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}
public static ObjectMapper getMapper() {
return mapper;
}
}

View File

@@ -83,4 +83,40 @@ public class Util {
return result; return result;
} }
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
return parts;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength, int fourthLength) {
byte[][] parts = new byte[4][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
parts[2] = new byte[thirdLength];
System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength);
parts[3] = new byte[fourthLength];
System.arraycopy(input, firstLength + secondLength + thirdLength, parts[3], 0, fourthLength);
return parts;
}
public static void sleep(int i) {
try {
Thread.sleep(i);
} catch (InterruptedException ie) {}
}
} }

View File

@@ -0,0 +1,65 @@
package org.whispersystems.textsecuregcm.websocket;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.websocket.session.WebSocketSessionContext;
import org.whispersystems.websocket.setup.WebSocketConnectListener;
public class ConnectListener implements WebSocketConnectListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private final AccountsManager accountsManager;
private final PushSender pushSender;
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
public ConnectListener(AccountsManager accountsManager, PushSender pushSender,
StoredMessages storedMessages, PubSubManager pubSubManager)
{
this.accountsManager = accountsManager;
this.pushSender = pushSender;
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
}
@Override
public void onWebSocketConnect(WebSocketSessionContext context) {
Optional<Account> account = context.getAuthenticated(Account.class);
if (!account.isPresent()) {
logger.debug("WS Connection with no authentication...");
context.getClient().close(4001, "Authentication failed");
return;
}
Optional<Device> device = account.get().getAuthenticatedDevice();
if (!device.isPresent()) {
logger.debug("WS Connection with no authenticated device...");
context.getClient().close(4001, "Device authentication failed");
return;
}
final WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender,
storedMessages, pubSubManager,
account.get(), device.get(),
context.getClient());
connection.onConnected();
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
@Override
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
connection.onConnectionLost();
}
});
}
}

View File

@@ -0,0 +1,43 @@
package org.whispersystems.textsecuregcm.websocket;
import com.google.common.base.Optional;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.websocket.auth.AuthenticationException;
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
import java.util.Map;
import io.dropwizard.auth.basic.BasicCredentials;
public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Account> {
private final AccountAuthenticator accountAuthenticator;
public WebSocketAccountAuthenticator(AccountAuthenticator accountAuthenticator) {
this.accountAuthenticator = accountAuthenticator;
}
@Override
public Optional<Account> authenticate(UpgradeRequest request) throws AuthenticationException {
try {
Map<String, String[]> parameters = request.getParameterMap();
String[] usernames = parameters.get("login");
String[] passwords = parameters.get("password");
if (usernames == null || usernames.length == 0 ||
passwords == null || passwords.length == 0)
{
return Optional.absent();
}
BasicCredentials credentials = new BasicCredentials(usernames[0], passwords[0]);
return accountAuthenticator.authenticate(credentials);
} catch (io.dropwizard.auth.AuthenticationException e) {
throw new AuthenticationException(e);
}
}
}

View File

@@ -0,0 +1,160 @@
package org.whispersystems.textsecuregcm.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
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 org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.List;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
public class WebSocketConnection implements PubSubListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private static final ObjectMapper objectMapper = SystemMapper.getMapper();
private final AccountsManager accountsManager;
private final PushSender pushSender;
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
private final Account account;
private final Device device;
private final WebsocketAddress address;
private final WebSocketClient client;
public WebSocketConnection(AccountsManager accountsManager,
PushSender pushSender,
StoredMessages storedMessages,
PubSubManager pubSubManager,
Account account,
Device device,
WebSocketClient client)
{
this.accountsManager = accountsManager;
this.pushSender = pushSender;
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
this.account = account;
this.device = device;
this.client = client;
this.address = new WebsocketAddress(account.getNumber(), device.getId());
}
public void onConnected() {
pubSubManager.subscribe(address, this);
processStoredMessages();
}
public void onConnectionLost() {
pubSubManager.unsubscribe(address, this);
}
@Override
public void onPubSubMessage(PubSubMessage message) {
try {
switch (message.getType()) {
case PubSubMessage.TYPE_QUERY_DB:
processStoredMessages();
break;
case PubSubMessage.TYPE_DELIVER:
PendingMessage pendingMessage = objectMapper.readValue(message.getContents(),
PendingMessage.class);
sendMessage(pendingMessage);
break;
default:
logger.warn("Unknown pubsub message: " + message.getType());
}
} catch (IOException e) {
logger.warn("Error deserializing PendingMessage", e);
}
}
private void sendMessage(final PendingMessage message) {
String content = message.getEncryptedOutgoingMessage();
Optional<byte[]> body = Optional.fromNullable(content.getBytes());
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/api/v1/message", body);
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
@Override
public void onSuccess(@Nullable WebSocketResponseMessage response) {
if (isSuccessResponse(response) && !message.isReceipt()) {
sendDeliveryReceiptFor(message);
} else if (!isSuccessResponse(response)) {
requeueMessage(message);
}
}
@Override
public void onFailure(@Nonnull Throwable throwable) {
requeueMessage(message);
}
private boolean isSuccessResponse(WebSocketResponseMessage response) {
return response != null && response.getStatus() >= 200 && response.getStatus() < 300;
}
});
}
private void requeueMessage(PendingMessage message) {
try {
pushSender.sendMessage(account, device, message);
} catch (NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("requeueMessage", e);
storedMessages.insert(address, message);
}
}
private void sendDeliveryReceiptFor(PendingMessage message) {
try {
Optional<Account> source = accountsManager.get(message.getSender());
if (!source.isPresent()) {
logger.warn("Source account disappeared? (%s)", message.getSender());
return;
}
OutgoingMessageSignal.Builder receipt =
OutgoingMessageSignal.newBuilder()
.setSource(account.getNumber())
.setSourceDevice((int) device.getId())
.setTimestamp(message.getMessageId())
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
for (Device device : source.get().getDevices()) {
pushSender.sendMessage(source.get(), device, receipt.build());
}
} catch (NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("sendDeliveryReceiptFor", "Delivery receipet", e);
}
}
private void processStoredMessages() {
List<PendingMessage> messages = storedMessages.getMessagesForDevice(address);
for (PendingMessage message : messages) {
sendMessage(message);
}
}
}

View File

@@ -2,39 +2,20 @@ package org.whispersystems.textsecuregcm.websocket;
public class WebsocketAddress { public class WebsocketAddress {
private final long accountId; private final String number;
private final long deviceId; private final long deviceId;
public WebsocketAddress(String serialized) throws InvalidWebsocketAddressException { public WebsocketAddress(String number, long deviceId) {
try { this.number = number;
String[] parts = serialized.split(":");
if (parts == null || parts.length != 2) {
throw new InvalidWebsocketAddressException(serialized);
}
this.accountId = Long.parseLong(parts[0]);
this.deviceId = Long.parseLong(parts[1]);
} catch (NumberFormatException e) {
throw new InvalidWebsocketAddressException(e);
}
}
public WebsocketAddress(long accountId, long deviceId) {
this.accountId = accountId;
this.deviceId = deviceId; this.deviceId = deviceId;
} }
public long getAccountId() { public String serialize() {
return accountId; return number + ":" + deviceId;
}
public long getDeviceId() {
return deviceId;
} }
public String toString() { public String toString() {
return accountId + ":" + deviceId; return serialize();
} }
@Override @Override
@@ -45,13 +26,13 @@ public class WebsocketAddress {
WebsocketAddress that = (WebsocketAddress)other; WebsocketAddress that = (WebsocketAddress)other;
return return
this.accountId == that.accountId && this.number.equals(that.number) &&
this.deviceId == that.deviceId; this.deviceId == that.deviceId;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return (int)accountId ^ (int)deviceId; return number.hashCode() ^ (int)deviceId;
} }
} }

View File

@@ -1,18 +0,0 @@
package org.whispersystems.textsecuregcm.websocket;
import com.fasterxml.jackson.annotation.JsonProperty;
public class WebsocketMessage {
@JsonProperty
private long id;
@JsonProperty
private String message;
public WebsocketMessage(long id, String message) {
this.id = id;
this.message = message;
}
}

View File

@@ -16,13 +16,6 @@
*/ */
package org.whispersystems.textsecuregcm.workers; package org.whispersystems.textsecuregcm.workers;
import com.yammer.dropwizard.cli.ConfiguredCommand;
import com.yammer.dropwizard.config.Bootstrap;
import com.yammer.dropwizard.db.DatabaseConfiguration;
import com.yammer.dropwizard.jdbi.ImmutableListContainerFactory;
import com.yammer.dropwizard.jdbi.ImmutableSetContainerFactory;
import com.yammer.dropwizard.jdbi.OptionalContainerFactory;
import com.yammer.dropwizard.jdbi.args.OptionalArgumentFactory;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import net.spy.memcached.MemcachedClient; import net.spy.memcached.MemcachedClient;
import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.DBI;
@@ -36,6 +29,13 @@ import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.ImmutableListContainerFactory;
import io.dropwizard.jdbi.ImmutableSetContainerFactory;
import io.dropwizard.jdbi.OptionalContainerFactory;
import io.dropwizard.jdbi.args.OptionalArgumentFactory;
import io.dropwizard.setup.Bootstrap;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfiguration> { public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfiguration> {
@@ -53,8 +53,8 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
throws Exception throws Exception
{ {
try { try {
DatabaseConfiguration dbConfig = config.getDatabaseConfiguration(); DataSourceFactory dbConfig = config.getDataSourceFactory();
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword()); DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass())); dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
dbi.registerContainerFactory(new ImmutableListContainerFactory()); dbi.registerContainerFactory(new ImmutableListContainerFactory());

View File

@@ -27,12 +27,14 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle; import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Hex;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import static org.whispersystems.textsecuregcm.storage.DirectoryManager.PendingClientContact;
public class DirectoryUpdater { public class DirectoryUpdater {
private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class); private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class);
@@ -82,54 +84,69 @@ public class DirectoryUpdater {
public void updateFromPeers() { public void updateFromPeers() {
logger.info("Updating peer directories."); logger.info("Updating peer directories.");
List<FederatedClient> clients = federatedClientManager.getClients();
int contactsAdded = 0;
int contactsRemoved = 0;
List<FederatedClient> clients = federatedClientManager.getClients();
for (FederatedClient client : clients) { for (FederatedClient client : clients) {
logger.info("Updating directory from peer: " + client.getPeerName()); logger.info("Updating directory from peer: " + client.getPeerName());
// BatchOperationHandle handle = directory.startBatchOperation();
try { int userCount = client.getUserCount();
int userCount = client.getUserCount(); int retrieved = 0;
int retrieved = 0;
logger.info("Remote peer user count: " + userCount); logger.info("Remote peer user count: " + userCount);
while (retrieved < userCount) { while (retrieved < userCount) {
logger.info("Retrieving remote tokens..."); logger.info("Retrieving remote tokens...");
List<ClientContact> clientContacts = client.getUserTokens(retrieved); List<ClientContact> remoteContacts = client.getUserTokens(retrieved);
List<PendingClientContact> localContacts = new LinkedList<>();
BatchOperationHandle handle = directory.startBatchOperation();
if (clientContacts == null) { if (remoteContacts == null) {
logger.info("Remote tokens empty, ending..."); logger.info("Remote tokens empty, ending...");
break; break;
} else { } else {
logger.info("Retrieved " + clientContacts.size() + " remote tokens..."); logger.info("Retrieved " + remoteContacts.size() + " remote tokens...");
}
for (ClientContact clientContact : clientContacts) {
clientContact.setRelay(client.getPeerName());
Optional<ClientContact> existing = directory.get(clientContact.getToken());
if (!clientContact.isInactive() && (!existing.isPresent() || client.getPeerName().equals(existing.get().getRelay()))) {
// directory.add(handle, clientContact);
directory.add(clientContact);
} else {
if (existing.isPresent() && client.getPeerName().equals(existing.get().getRelay())) {
directory.remove(clientContact.getToken());
}
}
}
retrieved += clientContacts.size();
logger.info("Processed: " + retrieved + " remote tokens.");
} }
logger.info("Update from peer complete."); for (ClientContact remoteContact : remoteContacts) {
} finally { localContacts.add(directory.get(handle, remoteContact.getToken()));
// directory.stopBatchOperation(handle); }
directory.stopBatchOperation(handle);
handle = directory.startBatchOperation();
Iterator<ClientContact> remoteContactIterator = remoteContacts.iterator();
Iterator<PendingClientContact> localContactIterator = localContacts.iterator();
while (remoteContactIterator.hasNext() && localContactIterator.hasNext()) {
ClientContact remoteContact = remoteContactIterator.next();
Optional<ClientContact> localContact = localContactIterator.next().get();
remoteContact.setRelay(client.getPeerName());
if (!remoteContact.isInactive() && (!localContact.isPresent() || client.getPeerName().equals(localContact.get().getRelay()))) {
contactsAdded++;
directory.add(handle, remoteContact);
} else {
if (localContact.isPresent() && client.getPeerName().equals(localContact.get().getRelay())) {
contactsRemoved++;
directory.remove(handle, remoteContact.getToken());
}
}
}
directory.stopBatchOperation(handle);
retrieved += remoteContacts.size();
logger.info("Processed: " + retrieved + " remote tokens.");
} }
logger.info("Update from peer complete.");
} }
logger.info("Update from peer directories complete."); logger.info("Update from peer directories complete.");
logger.info(String.format("Added %d and removed %d remove contacts.", contactsAdded, contactsRemoved));
} }
} }

View File

@@ -0,0 +1,59 @@
package org.whispersystems.textsecuregcm.workers;
import net.sourceforge.argparse4j.inf.Namespace;
import org.skife.jdbi.v2.DBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.ImmutableListContainerFactory;
import io.dropwizard.jdbi.ImmutableSetContainerFactory;
import io.dropwizard.jdbi.OptionalContainerFactory;
import io.dropwizard.jdbi.args.OptionalArgumentFactory;
import io.dropwizard.setup.Bootstrap;
public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration> {
private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class);
public VacuumCommand() {
super("vacuum", "Vacuum Postgres Tables");
}
@Override
protected void run(Bootstrap<WhisperServerConfiguration> bootstrap,
Namespace namespace,
WhisperServerConfiguration config)
throws Exception
{
DataSourceFactory dbConfig = config.getDataSourceFactory();
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
dbi.registerContainerFactory(new ImmutableListContainerFactory());
dbi.registerContainerFactory(new ImmutableSetContainerFactory());
dbi.registerContainerFactory(new OptionalContainerFactory());
Accounts accounts = dbi.onDemand(Accounts.class );
Keys keys = dbi.onDemand(Keys.class );
PendingAccounts pendingAccounts = dbi.onDemand(PendingAccounts.class);
logger.warn("Vacuuming accounts...");
accounts.vacuum();
logger.warn("Vacuuming pending_accounts...");
pendingAccounts.vacuum();
logger.warn("Vacuuming keys...");
keys.vacuum();
Thread.sleep(3000);
System.exit(0);
}
}

View File

@@ -142,4 +142,33 @@
</createIndex> </createIndex>
</changeSet> </changeSet>
<changeSet id="3" author="moxie">
<sql>CREATE OR REPLACE FUNCTION "custom_json_object_set_key"(
"json" json,
"key_to_set" TEXT,
"value_to_set" anyelement
)
RETURNS json
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT COALESCE(
(SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}')
FROM (SELECT *
FROM json_each("json")
WHERE "key" &lt;&gt; "key_to_set"
UNION ALL
SELECT "key_to_set", to_json("value_to_set")) AS "fields"),
'{}'
)::json
$function$;</sql>
<sql>UPDATE accounts SET data = custom_json_object_set_key(data, 'identityKey', k.identity_key) FROM keys k WHERE (data->>'identityKey')::text is null AND k.number = data->>'number' AND k.last_resort = 1;</sql>
<sql>UPDATE accounts SET data = custom_json_object_set_key(data, 'identityKey', k.identity_key) FROM keys k WHERE (data->>'identityKey')::text is null AND k.number = data->>'number';</sql>
</changeSet>
<changeSet id="4" author="moxie">
<dropColumn tableName="keys" columnName="identity_key"/>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -2,54 +2,71 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
import com.yammer.dropwizard.testing.ResourceTest; import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.SmsSender;
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.PendingAccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat; import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public class AccountControllerTest extends ResourceTest { public class AccountControllerTest {
private static final String SENDER = "+14152222222"; private static final String SENDER = "+14152222222";
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class); private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class ); private AccountsManager accountsManager = mock(AccountsManager.class );
private RateLimiters rateLimiters = mock(RateLimiters.class ); private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class ); private RateLimiter rateLimiter = mock(RateLimiter.class );
private SmsSender smsSender = mock(SmsSender.class ); private SmsSender smsSender = mock(SmsSender.class );
private StoredMessages storedMessages = mock(StoredMessages.class );
private TimeProvider timeProvider = mock(TimeProvider.class );
private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f");
@Override @Rule
protected void setUpResources() throws Exception { public final ResourceTestRule resources = ResourceTestRule.builder()
addProvider(AuthHelper.getAuthenticator()); .addProvider(AuthHelper.getAuthenticator())
.addResource(new AccountController(pendingAccountsManager,
accountsManager,
rateLimiters,
smsSender,
storedMessages,
timeProvider,
Optional.of(authorizationKey)))
.build();
@Before
public void setup() throws Exception {
when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter); when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234")); when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis());
addResource(new AccountController(pendingAccountsManager, when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
accountsManager,
rateLimiters,
smsSender));
} }
@Test @Test
public void testSendCode() throws Exception { public void testSendCode() throws Exception {
ClientResponse response = ClientResponse response =
client().resource(String.format("/v1/accounts/sms/code/%s", SENDER)) resources.client().resource(String.format("/v1/accounts/sms/code/%s", SENDER))
.get(ClientResponse.class); .get(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
@@ -60,7 +77,7 @@ public class AccountControllerTest extends ResourceTest {
@Test @Test
public void testVerifyCode() throws Exception { public void testVerifyCode() throws Exception {
ClientResponse response = ClientResponse response =
client().resource(String.format("/v1/accounts/code/%s", "1234")) resources.client().resource(String.format("/v1/accounts/code/%s", "1234"))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 2222)) .entity(new AccountAttributes("keykeykeykey", false, false, 2222))
.type(MediaType.APPLICATION_JSON_TYPE) .type(MediaType.APPLICATION_JSON_TYPE)
@@ -68,13 +85,13 @@ public class AccountControllerTest extends ResourceTest {
assertThat(response.getStatus()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager).create(isA(Account.class)); verify(accountsManager, times(1)).create(isA(Account.class));
} }
@Test @Test
public void testVerifyBadCode() throws Exception { public void testVerifyBadCode() throws Exception {
ClientResponse response = ClientResponse response =
client().resource(String.format("/v1/accounts/code/%s", "1111")) resources.client().resource(String.format("/v1/accounts/code/%s", "1111"))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 3333)) .entity(new AccountAttributes("keykeykeykey", false, false, 3333))
.type(MediaType.APPLICATION_JSON_TYPE) .type(MediaType.APPLICATION_JSON_TYPE)
@@ -85,4 +102,84 @@ public class AccountControllerTest extends ResourceTest {
verifyNoMoreInteractions(accountsManager); verifyNoMoreInteractions(accountsManager);
} }
@Test
public void testVerifyToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager, times(1)).create(isA(Account.class));
}
@Test
public void testVerifyBadToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
String token = SENDER + ":1415906574:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager);
}
@Test
public void testVerifyWrongToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager);
}
@Test
public void testVerifyExpiredToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1416003757901L);
String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager);
}
private static byte[] decodeHex(String hex) {
try {
return Hex.decodeHex(hex.toCharArray());
} catch (DecoderException e) {
throw new AssertionError(e);
}
}
} }

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