mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-11 01:40:22 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa379bf21 | ||
|
|
820a2f1a63 | ||
|
|
6fac7614f5 | ||
|
|
b724ea8d3b | ||
|
|
06f80c320d | ||
|
|
d9de015eab | ||
|
|
dd36c861ba | ||
|
|
b34e46af93 | ||
|
|
405802c492 | ||
|
|
e15f3c9d2b | ||
|
|
885af064c9 | ||
|
|
40529dc41f | ||
|
|
2452f6ef8a | ||
|
|
4c543e6f06 | ||
|
|
bc5fd5d441 | ||
|
|
7a33cef27e | ||
|
|
b433b9c879 | ||
|
|
5d169c523f | ||
|
|
98d277368f | ||
|
|
3bd58bf25e | ||
|
|
ba05e577ae | ||
|
|
4206f6af45 | ||
|
|
0c5da1cc47 | ||
|
|
d9bd1c679e | ||
|
|
437eb8de37 | ||
|
|
f14c181840 | ||
|
|
d46c9fb157 | ||
|
|
6913e4dfd2 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -56,9 +56,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 +72,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
|
|
||||||
|
|||||||
128
pom.xml
128
pom.xml
@@ -9,24 +9,72 @@
|
|||||||
|
|
||||||
<groupId>org.whispersystems.textsecure</groupId>
|
<groupId>org.whispersystems.textsecure</groupId>
|
||||||
<artifactId>TextSecureServer</artifactId>
|
<artifactId>TextSecureServer</artifactId>
|
||||||
<version>0.8</version>
|
<version>0.18</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<dropwizard.version>0.7.0</dropwizard.version>
|
||||||
|
<jackson.api.version>2.3.3</jackson.api.version>
|
||||||
|
<commons-codec.version>1.6</commons-codec.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-core</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-jdbi</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-auth</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-client</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-migrations</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-testing</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-metrics-graphite</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.sun.jersey</groupId>
|
||||||
|
<artifactId>jersey-json</artifactId>
|
||||||
|
<version>1.18.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.codahale.metrics</groupId>
|
||||||
|
<artifactId>metrics-graphite</artifactId>
|
||||||
|
<version>3.0.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||||
|
<artifactId>websocket-server</artifactId>
|
||||||
|
<version>9.0.7.v20131107</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>bouncycastle</groupId>
|
<groupId>bouncycastle</groupId>
|
||||||
<artifactId>bcprov-jdk16</artifactId>
|
<artifactId>bcprov-jdk16</artifactId>
|
||||||
<version>140</version>
|
<version>140</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-core</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.metrics</groupId>
|
|
||||||
<artifactId>metrics-graphite</artifactId>
|
|
||||||
<version>2.2.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.android.gcm</groupId>
|
<groupId>com.google.android.gcm</groupId>
|
||||||
<artifactId>gcm-server</artifactId>
|
<artifactId>gcm-server</artifactId>
|
||||||
@@ -66,35 +114,10 @@
|
|||||||
<type>jar</type>
|
<type>jar</type>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-jdbi</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-auth</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-client</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-migrations</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-testing</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.twilio.sdk</groupId>
|
<groupId>com.twilio.sdk</groupId>
|
||||||
<artifactId>twilio-java-sdk</artifactId>
|
<artifactId>twilio-java-sdk</artifactId>
|
||||||
<version>3.4.1</version>
|
<version>3.4.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -103,25 +126,24 @@
|
|||||||
<version>9.1-901.jdbc4</version>
|
<version>9.1-901.jdbc4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.sun.jersey</groupId>
|
|
||||||
<artifactId>jersey-json</artifactId>
|
|
||||||
<version>1.17.1</version>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
package org.whispersystems.textsecuregcm;
|
package org.whispersystems.textsecuregcm;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.yammer.dropwizard.config.Configuration;
|
|
||||||
import com.yammer.dropwizard.db.DatabaseConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||||
@@ -35,6 +33,9 @@ import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
|
|||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import io.dropwizard.Configuration;
|
||||||
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
|
|
||||||
public class WhisperServerConfiguration extends Configuration {
|
public class WhisperServerConfiguration extends Configuration {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -74,7 +75,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,7 +88,7 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private MetricsConfiguration metrics = new MetricsConfiguration();
|
private MetricsConfiguration viz = new MetricsConfiguration();
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@@ -125,7 +126,7 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
return redis;
|
return redis;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DatabaseConfiguration getDatabaseConfiguration() {
|
public DataSourceFactory getDataSourceFactory() {
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +143,6 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MetricsConfiguration getMetricsConfiguration() {
|
public MetricsConfiguration getMetricsConfiguration() {
|
||||||
return metrics;
|
return viz;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,21 +16,13 @@
|
|||||||
*/
|
*/
|
||||||
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.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.Metrics;
|
|
||||||
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;
|
||||||
@@ -40,16 +32,21 @@ 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.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.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;
|
||||||
@@ -68,18 +65,30 @@ 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.WebsocketControllerFactory;
|
||||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||||
|
|
||||||
|
import javax.servlet.DispatcherType;
|
||||||
|
import javax.servlet.FilterRegistration;
|
||||||
|
import javax.servlet.ServletRegistration;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import io.dropwizard.Application;
|
||||||
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
|
import io.dropwizard.jdbi.DBIFactory;
|
||||||
|
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
|
||||||
|
import io.dropwizard.migrations.MigrationsBundle;
|
||||||
|
import io.dropwizard.setup.Bootstrap;
|
||||||
|
import io.dropwizard.setup.Environment;
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
public class WhisperServerService extends Application<WhisperServerConfiguration> {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
@@ -87,41 +96,45 @@ 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.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();
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
|
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
|
||||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
||||||
@@ -132,46 +145,70 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
|||||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||||
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
|
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
|
||||||
config.getApnConfiguration(),
|
config.getApnConfiguration(),
|
||||||
storedMessageManager,
|
storedMessages, pubSubManager,
|
||||||
accountsManager);
|
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));
|
||||||
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(attachmentController);
|
||||||
environment.addResource(messageController);
|
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),
|
WebsocketControllerFactory servlet = new WebsocketControllerFactory(deviceAuthenticator,
|
||||||
"/v1/websocket/");
|
pushSender,
|
||||||
environment.addFilter(new CORSHeaderFilter(), "/*");
|
storedMessages,
|
||||||
|
pubSubManager);
|
||||||
|
|
||||||
|
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.getMetricsConfiguration().isEnabled()) {
|
if (config.getMetricsConfiguration().isEnabled()) {
|
||||||
new JsonMetricsReporter("textsecure", Metrics.defaultRegistry(),
|
new JsonMetricsReporter(environment.metrics(),
|
||||||
config.getMetricsConfiguration().getToken(),
|
config.getMetricsConfiguration().getToken(),
|
||||||
config.getMetricsConfiguration().getHost())
|
config.getMetricsConfiguration().getHost())
|
||||||
.start(60, TimeUnit.SECONDS);
|
.start(60, TimeUnit.SECONDS);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
@@ -55,6 +54,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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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<>();
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,46 +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.PreKeyStatus;
|
|
||||||
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)
|
||||||
@@ -66,112 +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("/")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public PreKeyStatus getStatus(@Auth Account account) {
|
public PreKeyCount getStatus(@Auth Account account) {
|
||||||
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
|
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
|
||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
count = count - 1;
|
count = count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PreKeyStatus(count);
|
return new PreKeyCount(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
|
||||||
@GET
|
throws NoSuchUserException
|
||||||
@Path("/{number}/{device_id}")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public UnstructuredPreKeyList 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<UnstructuredPreKeyList> results;
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
|
||||||
@GET
|
|
||||||
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.eclipse.jetty.websocket.WebSocket;
|
import com.google.common.base.Optional;
|
||||||
|
import org.eclipse.jetty.websocket.api.CloseStatus;
|
||||||
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
|
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||||
|
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
|
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||||
import org.whispersystems.textsecuregcm.entities.IncomingWebsocketMessage;
|
import org.whispersystems.textsecuregcm.entities.IncomingWebsocketMessage;
|
||||||
|
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.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubListener;
|
import org.whispersystems.textsecuregcm.storage.PubSubListener;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
|
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
|
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketMessage;
|
import org.whispersystems.textsecuregcm.websocket.WebsocketMessage;
|
||||||
|
|
||||||
@@ -22,65 +31,132 @@ import java.util.LinkedList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class WebsocketController implements WebSocket.OnTextMessage, PubSubListener {
|
import io.dropwizard.auth.AuthenticationException;
|
||||||
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
|
|
||||||
|
public class WebsocketController implements WebSocketListener, PubSubListener {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
|
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
private static final Map<Long, String> pendingMessages = new HashMap<>();
|
private static final Map<Long, String> pendingMessages = new HashMap<>();
|
||||||
|
|
||||||
private final StoredMessageManager storedMessageManager;
|
private final AccountAuthenticator accountAuthenticator;
|
||||||
private final PubSubManager pubSubManager;
|
private final PubSubManager pubSubManager;
|
||||||
|
private final StoredMessages storedMessages;
|
||||||
|
private final PushSender pushSender;
|
||||||
|
|
||||||
private final Account account;
|
private WebsocketAddress address;
|
||||||
private final Device device;
|
private Account account;
|
||||||
|
private Device device;
|
||||||
|
private Session session;
|
||||||
|
|
||||||
private Connection connection;
|
private long pendingMessageSequence;
|
||||||
private long pendingMessageSequence;
|
|
||||||
|
|
||||||
public WebsocketController(StoredMessageManager storedMessageManager,
|
public WebsocketController(AccountAuthenticator accountAuthenticator,
|
||||||
PubSubManager pubSubManager,
|
PushSender pushSender,
|
||||||
Account account)
|
PubSubManager pubSubManager,
|
||||||
|
StoredMessages storedMessages)
|
||||||
{
|
{
|
||||||
this.storedMessageManager = storedMessageManager;
|
this.accountAuthenticator = accountAuthenticator;
|
||||||
|
this.pushSender = pushSender;
|
||||||
this.pubSubManager = pubSubManager;
|
this.pubSubManager = pubSubManager;
|
||||||
this.account = account;
|
this.storedMessages = storedMessages;
|
||||||
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
|
@Override
|
||||||
public void onClose(int i, String s) {
|
public void onWebSocketConnect(Session session) {
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(String body) {
|
|
||||||
try {
|
try {
|
||||||
IncomingWebsocketMessage incomingMessage = mapper.readValue(body, IncomingWebsocketMessage.class);
|
UpgradeRequest request = session.getUpgradeRequest();
|
||||||
|
Map<String, String[]> parameters = request.getParameterMap();
|
||||||
|
String[] usernames = parameters.get("login" );
|
||||||
|
String[] passwords = parameters.get("password");
|
||||||
|
|
||||||
switch (incomingMessage.getType()) {
|
if (usernames == null || usernames.length == 0 ||
|
||||||
case IncomingWebsocketMessage.TYPE_ACKNOWLEDGE_MESSAGE: handleMessageAck(body); break;
|
passwords == null || passwords.length == 0)
|
||||||
case IncomingWebsocketMessage.TYPE_PING_MESSAGE: handlePing(); break;
|
{
|
||||||
default: handleClose(); break;
|
session.close(new CloseStatus(4001, "Unauthorized"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
logger.debug("Parse", e);
|
BasicCredentials credentials = new BasicCredentials(usernames[0], passwords[0]);
|
||||||
handleClose();
|
Optional<Account> account = accountAuthenticator.authenticate(credentials);
|
||||||
|
|
||||||
|
if (!account.isPresent()) {
|
||||||
|
session.close(new CloseStatus(4001, "Unauthorized"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.account = account.get();
|
||||||
|
this.device = account.get().getAuthenticatedDevice().get();
|
||||||
|
this.address = new WebsocketAddress(this.account.getId(), this.device.getId());
|
||||||
|
this.session = session;
|
||||||
|
|
||||||
|
this.session.setIdleTimeout(10 * 60 * 1000);
|
||||||
|
this.pubSubManager.subscribe(this.address, this);
|
||||||
|
|
||||||
|
handleQueryDatabase();
|
||||||
|
} catch (AuthenticationException e) {
|
||||||
|
try { session.close(1011, "Server Error");} catch (IOException e1) {}
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
logger.info("Abrupt session close.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketText(String body) {
|
||||||
|
try {
|
||||||
|
IncomingWebsocketMessage incomingMessage = mapper.readValue(body, IncomingWebsocketMessage.class);
|
||||||
|
|
||||||
|
switch (incomingMessage.getType()) {
|
||||||
|
case IncomingWebsocketMessage.TYPE_ACKNOWLEDGE_MESSAGE:
|
||||||
|
handleMessageAck(body);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
close(new CloseStatus(1008, "Unknown Type"));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.debug("Parse", e);
|
||||||
|
close(new CloseStatus(1008, "Badly Formatted"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketClose(int i, String s) {
|
||||||
|
pubSubManager.unsubscribe(this.address, this);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String remainingMessage : remainingMessages) {
|
||||||
|
try {
|
||||||
|
pushSender.sendMessage(account, device, new EncryptedOutgoingMessage(remainingMessage));
|
||||||
|
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
||||||
|
logger.warn("onWebSocketClose", e);
|
||||||
|
storedMessages.insert(account.getId(), device.getId(), remainingMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPubSubMessage(PubSubMessage outgoingMessage) {
|
public void onPubSubMessage(PubSubMessage outgoingMessage) {
|
||||||
switch (outgoingMessage.getType()) {
|
switch (outgoingMessage.getType()) {
|
||||||
case PubSubMessage.TYPE_DELIVER: handleDeliverOutgoingMessage(outgoingMessage.getContents()); break;
|
case PubSubMessage.TYPE_DELIVER:
|
||||||
case PubSubMessage.TYPE_QUERY_DB: handleQueryDatabase(); break;
|
handleDeliverOutgoingMessage(outgoingMessage.getContents());
|
||||||
|
break;
|
||||||
|
case PubSubMessage.TYPE_QUERY_DB:
|
||||||
|
handleQueryDatabase();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logger.warn("Unknown pubsub message: " + outgoingMessage.getType());
|
logger.warn("Unknown pubsub message: " + outgoingMessage.getType());
|
||||||
}
|
}
|
||||||
@@ -95,10 +171,11 @@ public class WebsocketController implements WebSocket.OnTextMessage, PubSubListe
|
|||||||
pendingMessages.put(messageSequence, message);
|
pendingMessages.put(messageSequence, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.sendMessage(mapper.writeValueAsString(new WebsocketMessage(messageSequence, message)));
|
WebsocketMessage websocketMessage = new WebsocketMessage(messageSequence, message);
|
||||||
|
session.getRemote().sendStringByFuture(mapper.writeValueAsString(websocketMessage));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.debug("Response failed", e);
|
logger.debug("Response failed", e);
|
||||||
handleClose();
|
close(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,42 +191,33 @@ public class WebsocketController implements WebSocket.OnTextMessage, PubSubListe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
private void handleQueryDatabase() {
|
||||||
List<String> messages = storedMessageManager.getOutgoingMessages(account, device);
|
List<String> messages = storedMessages.getMessagesForDevice(account.getId(), device.getId());
|
||||||
|
|
||||||
for (String message : messages) {
|
for (String message : messages) {
|
||||||
handleDeliverOutgoingMessage(message);
|
handleDeliverOutgoingMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketBinary(byte[] bytes, int i, int i2) {
|
||||||
|
logger.info("Received binary message!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketError(Throwable throwable) {
|
||||||
|
logger.info("onWebSocketError", throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void close(CloseStatus closeStatus) {
|
||||||
|
try {
|
||||||
|
if (this.session != null) {
|
||||||
|
if (closeStatus != null) this.session.close(closeStatus);
|
||||||
|
else this.session.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.info("close()", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,23 +41,26 @@ public class EncryptedOutgoingMessage {
|
|||||||
private static final int MAC_KEY_SIZE = 20;
|
private static final int MAC_KEY_SIZE = 20;
|
||||||
private static final int MAC_SIZE = 10;
|
private static final int MAC_SIZE = 10;
|
||||||
|
|
||||||
private final OutgoingMessageSignal outgoingMessage;
|
private final String serialized;
|
||||||
private final String signalingKey;
|
|
||||||
|
|
||||||
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
|
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
|
||||||
String signalingKey)
|
String signalingKey)
|
||||||
|
throws CryptoEncodingException
|
||||||
{
|
{
|
||||||
this.outgoingMessage = outgoingMessage;
|
|
||||||
this.signalingKey = signalingKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String serialize() throws 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 EncryptedOutgoingMessage(String serialized) {
|
||||||
|
this.serialized = serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String serialize() {
|
||||||
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
|
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
public interface PreKeyBase {
|
||||||
|
|
||||||
|
public long getKeyId();
|
||||||
|
public String getPublicKey();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,16 +3,16 @@ package org.whispersystems.textsecuregcm.entities;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
public class PreKeyStatus {
|
public class PreKeyCount {
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private int count;
|
private int count;
|
||||||
|
|
||||||
public PreKeyStatus(int count) {
|
public PreKeyCount(int count) {
|
||||||
this.count = count;
|
this.count = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreKeyStatus() {}
|
public PreKeyCount() {}
|
||||||
|
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return count;
|
return count;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 implements Serializable {
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,12 @@ 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 final FederatedPeer peer;
|
private final FederatedPeer peer;
|
||||||
private final Client client;
|
private final Client client;
|
||||||
@@ -107,9 +109,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 +121,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 +129,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);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
package org.whispersystems.textsecuregcm.metrics;
|
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.JsonEncoding;
|
||||||
import com.fasterxml.jackson.core.JsonFactory;
|
import com.fasterxml.jackson.core.JsonFactory;
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.yammer.metrics.core.Clock;
|
import org.slf4j.Logger;
|
||||||
import com.yammer.metrics.core.Counter;
|
import org.slf4j.LoggerFactory;
|
||||||
import com.yammer.metrics.core.Gauge;
|
|
||||||
import com.yammer.metrics.core.Histogram;
|
|
||||||
import com.yammer.metrics.core.Metered;
|
|
||||||
import com.yammer.metrics.core.Metric;
|
|
||||||
import com.yammer.metrics.core.MetricName;
|
|
||||||
import com.yammer.metrics.core.MetricProcessor;
|
|
||||||
import com.yammer.metrics.core.MetricsRegistry;
|
|
||||||
import com.yammer.metrics.core.Sampling;
|
|
||||||
import com.yammer.metrics.core.Summarizable;
|
|
||||||
import com.yammer.metrics.core.Timer;
|
|
||||||
import com.yammer.metrics.core.VirtualMachineMetrics;
|
|
||||||
import com.yammer.metrics.reporting.AbstractPollingReporter;
|
|
||||||
import com.yammer.metrics.stats.Snapshot;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -33,292 +30,155 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* Adapted from MetricsServlet.
|
* Adapted from MetricsServlet.
|
||||||
*/
|
*/
|
||||||
public class JsonMetricsReporter extends AbstractPollingReporter implements MetricProcessor<JsonMetricsReporter.Context> {
|
public class JsonMetricsReporter extends ScheduledReporter {
|
||||||
private final Clock clock = Clock.defaultClock();
|
|
||||||
private final VirtualMachineMetrics vm = VirtualMachineMetrics.getInstance();
|
private final Logger logger = LoggerFactory.getLogger(JsonMetricsReporter.class);
|
||||||
private final String service;
|
|
||||||
private final MetricsRegistry registry;
|
|
||||||
private final JsonFactory factory = new JsonFactory();
|
private final JsonFactory factory = new JsonFactory();
|
||||||
|
|
||||||
private final String table;
|
private final String table;
|
||||||
private final String sunnylabsHost;
|
private final String sunnylabsHost;
|
||||||
private final String host;
|
private final String host;
|
||||||
|
|
||||||
private final boolean includeVMMetrics;
|
public JsonMetricsReporter(MetricRegistry registry, String token, String sunnylabsHost)
|
||||||
|
throws UnknownHostException
|
||||||
public JsonMetricsReporter(String service, MetricsRegistry registry, String token, String sunnylabsHost) throws UnknownHostException {
|
{
|
||||||
this(service, registry, token, sunnylabsHost, true);
|
super(registry, "jsonmetrics-reporter", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.MILLISECONDS);
|
||||||
}
|
this.table = token;
|
||||||
|
|
||||||
public JsonMetricsReporter(String service, MetricsRegistry registry, String token, String sunnylabsHost, boolean includeVMMetrics) throws UnknownHostException {
|
|
||||||
super(registry, "jsonmetrics-reporter");
|
|
||||||
this.service = service;
|
|
||||||
this.registry = registry;
|
|
||||||
this.table = token;
|
|
||||||
this.sunnylabsHost = sunnylabsHost;
|
this.sunnylabsHost = sunnylabsHost;
|
||||||
this.host = InetAddress.getLocalHost().getHostName();
|
this.host = InetAddress.getLocalHost().getHostName();
|
||||||
this.includeVMMetrics = includeVMMetrics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void report(SortedMap<String, Gauge> stringGaugeSortedMap,
|
||||||
|
SortedMap<String, Counter> stringCounterSortedMap,
|
||||||
|
SortedMap<String, Histogram> stringHistogramSortedMap,
|
||||||
|
SortedMap<String, Meter> stringMeterSortedMap,
|
||||||
|
SortedMap<String, Timer> stringTimerSortedMap)
|
||||||
|
{
|
||||||
try {
|
try {
|
||||||
URL http = new URL("https", sunnylabsHost, 443, "/report/metrics?t=" + table + "&h=" + host);
|
logger.info("Reporting metrics...");
|
||||||
System.out.println("Reporting started to: " + http);
|
URL url = new URL("https", sunnylabsHost, 443, "/report/metrics?t=" + table + "&h=" + host);
|
||||||
HttpURLConnection urlc = (HttpURLConnection) http.openConnection();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
urlc.setDoOutput(true);
|
|
||||||
urlc.addRequestProperty("Content-Type", "application/json");
|
connection.setDoOutput(true);
|
||||||
OutputStream outputStream = urlc.getOutputStream();
|
connection.addRequestProperty("Content-Type", "application/json");
|
||||||
writeJson(outputStream);
|
|
||||||
|
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();
|
outputStream.close();
|
||||||
System.out.println("Reporting complete: " + urlc.getResponseCode());
|
|
||||||
|
logger.info("Metrics server response: " + connection.getResponseCode());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.warn("Error sending metrics", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("error", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class Context {
|
private void reportGauge(JsonGenerator json, String name, Gauge gauge) throws IOException {
|
||||||
final boolean showFullSamples;
|
Object gaugeValue = evaluateGauge(gauge);
|
||||||
final JsonGenerator json;
|
|
||||||
|
|
||||||
Context(JsonGenerator json, boolean showFullSamples) {
|
if (gaugeValue instanceof Number) {
|
||||||
this.json = json;
|
json.writeFieldName(sanitize(name));
|
||||||
this.showFullSamples = showFullSamples;
|
json.writeObject(gaugeValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeJson(OutputStream out) throws IOException {
|
private void reportCounter(JsonGenerator json, String name, Counter counter) throws IOException {
|
||||||
final JsonGenerator json = factory.createGenerator(out, JsonEncoding.UTF8);
|
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.writeStartObject();
|
||||||
if (includeVMMetrics) {
|
json.writeNumberField("count", histogram.getCount());
|
||||||
writeVmMetrics(json);
|
writeSnapshot(json, snapshot);
|
||||||
}
|
|
||||||
writeRegularMetrics(json, false);
|
|
||||||
json.writeEndObject();
|
json.writeEndObject();
|
||||||
json.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeVmMetrics(JsonGenerator json) throws IOException {
|
private void reportMeter(JsonGenerator json, String name, Meter meter) throws IOException {
|
||||||
json.writeFieldName(service);
|
json.writeFieldName(sanitize(name));
|
||||||
json.writeStartObject();
|
json.writeStartObject();
|
||||||
json.writeFieldName("jvm");
|
writeMetered(json, meter);
|
||||||
|
json.writeEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportTimer(JsonGenerator json, String name, Timer timer) throws IOException {
|
||||||
|
json.writeFieldName(sanitize(name));
|
||||||
json.writeStartObject();
|
json.writeStartObject();
|
||||||
{
|
json.writeFieldName("rate");
|
||||||
json.writeFieldName("vm");
|
json.writeStartObject();
|
||||||
json.writeStartObject();
|
writeMetered(json, timer);
|
||||||
{
|
json.writeEndObject();
|
||||||
json.writeStringField("name", vm.name());
|
json.writeFieldName("duration");
|
||||||
json.writeStringField("version", vm.version());
|
json.writeStartObject();
|
||||||
}
|
writeSnapshot(json, timer.getSnapshot());
|
||||||
json.writeEndObject();
|
|
||||||
|
|
||||||
json.writeFieldName("memory");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
json.writeNumberField("totalInit", vm.totalInit());
|
|
||||||
json.writeNumberField("totalUsed", vm.totalUsed());
|
|
||||||
json.writeNumberField("totalMax", vm.totalMax());
|
|
||||||
json.writeNumberField("totalCommitted", vm.totalCommitted());
|
|
||||||
|
|
||||||
json.writeNumberField("heapInit", vm.heapInit());
|
|
||||||
json.writeNumberField("heapUsed", vm.heapUsed());
|
|
||||||
json.writeNumberField("heapMax", vm.heapMax());
|
|
||||||
json.writeNumberField("heapCommitted", vm.heapCommitted());
|
|
||||||
|
|
||||||
json.writeNumberField("heap_usage", vm.heapUsage());
|
|
||||||
json.writeNumberField("non_heap_usage", vm.nonHeapUsage());
|
|
||||||
json.writeFieldName("memory_pool_usages");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
for (Map.Entry<String, Double> pool : vm.memoryPoolUsage().entrySet()) {
|
|
||||||
json.writeNumberField(pool.getKey(), pool.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
|
|
||||||
final Map<String, VirtualMachineMetrics.BufferPoolStats> bufferPoolStats = vm.getBufferPoolStats();
|
|
||||||
if (!bufferPoolStats.isEmpty()) {
|
|
||||||
json.writeFieldName("buffers");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
json.writeFieldName("direct");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
json.writeNumberField("count", bufferPoolStats.get("direct").getCount());
|
|
||||||
json.writeNumberField("memoryUsed", bufferPoolStats.get("direct").getMemoryUsed());
|
|
||||||
json.writeNumberField("totalCapacity", bufferPoolStats.get("direct").getTotalCapacity());
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
|
|
||||||
json.writeFieldName("mapped");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
json.writeNumberField("count", bufferPoolStats.get("mapped").getCount());
|
|
||||||
json.writeNumberField("memoryUsed", bufferPoolStats.get("mapped").getMemoryUsed());
|
|
||||||
json.writeNumberField("totalCapacity", bufferPoolStats.get("mapped").getTotalCapacity());
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
json.writeNumberField("daemon_thread_count", vm.daemonThreadCount());
|
|
||||||
json.writeNumberField("thread_count", vm.threadCount());
|
|
||||||
json.writeNumberField("current_time", clock.time());
|
|
||||||
json.writeNumberField("uptime", vm.uptime());
|
|
||||||
json.writeNumberField("fd_usage", vm.fileDescriptorUsage());
|
|
||||||
|
|
||||||
json.writeFieldName("thread-states");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
for (Map.Entry<Thread.State, Double> entry : vm.threadStatePercentages()
|
|
||||||
.entrySet()) {
|
|
||||||
json.writeNumberField(entry.getKey().toString().toLowerCase(),
|
|
||||||
entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
|
|
||||||
json.writeFieldName("garbage-collectors");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
for (Map.Entry<String, VirtualMachineMetrics.GarbageCollectorStats> entry : vm.garbageCollectors()
|
|
||||||
.entrySet()) {
|
|
||||||
json.writeFieldName(entry.getKey());
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
final VirtualMachineMetrics.GarbageCollectorStats gc = entry.getValue();
|
|
||||||
json.writeNumberField("runs", gc.getRuns());
|
|
||||||
json.writeNumberField("time", gc.getTime(TimeUnit.MILLISECONDS));
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
json.writeEndObject();
|
||||||
json.writeEndObject();
|
json.writeEndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeRegularMetrics(JsonGenerator json, boolean showFullSamples) throws IOException {
|
private Object evaluateGauge(Gauge gauge) {
|
||||||
for (Map.Entry<String, SortedMap<MetricName, Metric>> entry : registry.groupedMetrics().entrySet()) {
|
|
||||||
for (Map.Entry<MetricName, Metric> subEntry : entry.getValue().entrySet()) {
|
|
||||||
json.writeFieldName(sanitize(subEntry.getKey()));
|
|
||||||
try {
|
|
||||||
subEntry.getValue()
|
|
||||||
.processWith(this,
|
|
||||||
subEntry.getKey(),
|
|
||||||
new Context(json, showFullSamples));
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processHistogram(MetricName name, Histogram histogram, Context context) throws Exception {
|
|
||||||
final JsonGenerator json = context.json;
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
json.writeNumberField("count", histogram.count());
|
|
||||||
writeSummarizable(histogram, json);
|
|
||||||
writeSampling(histogram, json);
|
|
||||||
if (context.showFullSamples) {
|
|
||||||
json.writeObjectField("values", histogram.getSnapshot().getValues());
|
|
||||||
}
|
|
||||||
histogram.clear();
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processCounter(MetricName name, Counter counter, Context context) throws Exception {
|
|
||||||
final JsonGenerator json = context.json;
|
|
||||||
json.writeNumber(counter.count());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processGauge(MetricName name, Gauge<?> gauge, Context context) throws Exception {
|
|
||||||
final JsonGenerator json = context.json;
|
|
||||||
json.writeObject(evaluateGauge(gauge));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processMeter(MetricName name, Metered meter, Context context) throws Exception {
|
|
||||||
final JsonGenerator json = context.json;
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
writeMeteredFields(meter, json);
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processTimer(MetricName name, Timer timer, Context context) throws Exception {
|
|
||||||
final JsonGenerator json = context.json;
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
json.writeFieldName("duration");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
json.writeStringField("unit", timer.durationUnit().toString().toLowerCase());
|
|
||||||
writeSummarizable(timer, json);
|
|
||||||
writeSampling(timer, json);
|
|
||||||
if (context.showFullSamples) {
|
|
||||||
json.writeObjectField("values", timer.getSnapshot().getValues());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
|
|
||||||
json.writeFieldName("rate");
|
|
||||||
json.writeStartObject();
|
|
||||||
{
|
|
||||||
writeMeteredFields(timer, json);
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
json.writeEndObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object evaluateGauge(Gauge<?> gauge) {
|
|
||||||
try {
|
try {
|
||||||
return gauge.value();
|
return gauge.getValue();
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
return "error reading gauge: " + e.getMessage();
|
logger.warn("Error reading gauge", e);
|
||||||
|
return "error reading gauge";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void writeSummarizable(Summarizable metric, JsonGenerator json) throws IOException {
|
private void writeSnapshot(JsonGenerator json, Snapshot snapshot) throws IOException {
|
||||||
json.writeNumberField("min", metric.min());
|
json.writeNumberField("max", convertDuration(snapshot.getMax()));
|
||||||
json.writeNumberField("max", metric.max());
|
json.writeNumberField("mean", convertDuration(snapshot.getMean()));
|
||||||
json.writeNumberField("mean", metric.mean());
|
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 static void writeSampling(Sampling metric, JsonGenerator json) throws IOException {
|
private void writeMetered(JsonGenerator json, Metered meter) throws IOException {
|
||||||
final Snapshot snapshot = metric.getSnapshot();
|
json.writeNumberField("count", convertRate(meter.getCount()));
|
||||||
json.writeNumberField("median", snapshot.getMedian());
|
json.writeNumberField("mean", convertRate(meter.getMeanRate()));
|
||||||
json.writeNumberField("p75", snapshot.get75thPercentile());
|
json.writeNumberField("m1", convertRate(meter.getOneMinuteRate()));
|
||||||
json.writeNumberField("p95", snapshot.get95thPercentile());
|
json.writeNumberField("m5", convertRate(meter.getFiveMinuteRate()));
|
||||||
json.writeNumberField("p99", snapshot.get99thPercentile());
|
json.writeNumberField("m15", convertRate(meter.getFifteenMinuteRate()));
|
||||||
json.writeNumberField("p999", snapshot.get999thPercentile());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeMeteredFields(Metered metered, JsonGenerator json) throws IOException {
|
|
||||||
json.writeNumberField("count", metered.count());
|
|
||||||
json.writeNumberField("mean", metered.meanRate());
|
|
||||||
json.writeNumberField("m1", metered.oneMinuteRate());
|
|
||||||
json.writeNumberField("m5", metered.fiveMinuteRate());
|
|
||||||
json.writeNumberField("m15", metered.fifteenMinuteRate());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
|
private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
|
||||||
|
|
||||||
private String sanitize(MetricName metricName) {
|
private String sanitize(String metricName) {
|
||||||
return SIMPLE_NAMES.matcher(metricName.getGroup() + "." + metricName.getName()).replaceAll("_");
|
return SIMPLE_NAMES.matcher(metricName).replaceAll("_");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,25 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.push;
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.notnoop.apns.APNS;
|
import com.notnoop.apns.APNS;
|
||||||
import com.notnoop.apns.ApnsService;
|
import com.notnoop.apns.ApnsService;
|
||||||
import com.notnoop.exceptions.NetworkIOException;
|
import com.notnoop.exceptions.NetworkIOException;
|
||||||
import com.yammer.metrics.Metrics;
|
|
||||||
import com.yammer.metrics.core.Meter;
|
|
||||||
import org.bouncycastle.openssl.PEMReader;
|
import org.bouncycastle.openssl.PEMReader;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||||
|
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.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@@ -40,21 +47,31 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
|
||||||
public class APNSender {
|
public class APNSender {
|
||||||
|
|
||||||
private final Meter success = Metrics.newMeter(APNSender.class, "sent", "success", TimeUnit.MINUTES);
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
private final Meter failure = Metrics.newMeter(APNSender.class, "sent", "failure", TimeUnit.MINUTES);
|
private final Meter websocketMeter = metricRegistry.meter(name(getClass(), "websocket"));
|
||||||
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
|
private final Meter pushMeter = metricRegistry.meter(name(getClass(), "push"));
|
||||||
|
private final Meter failureMeter = metricRegistry.meter(name(getClass(), "failure"));
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
|
||||||
|
|
||||||
private static final String MESSAGE_BODY = "m";
|
private static final String MESSAGE_BODY = "m";
|
||||||
|
|
||||||
private final Optional<ApnsService> apnService;
|
private final Optional<ApnsService> apnService;
|
||||||
|
private final PubSubManager pubSubManager;
|
||||||
|
private final StoredMessages storedMessages;
|
||||||
|
|
||||||
public APNSender(String apnCertificate, String apnKey)
|
public APNSender(PubSubManager pubSubManager,
|
||||||
|
StoredMessages storedMessages,
|
||||||
|
String apnCertificate, String apnKey)
|
||||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
||||||
{
|
{
|
||||||
|
this.pubSubManager = pubSubManager;
|
||||||
|
this.storedMessages = storedMessages;
|
||||||
|
|
||||||
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
|
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
|
||||||
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
|
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
|
||||||
this.apnService = Optional.of(APNS.newService()
|
this.apnService = Optional.of(APNS.newService()
|
||||||
@@ -65,31 +82,44 @@ public class APNSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(String registrationId, EncryptedOutgoingMessage message)
|
public void sendMessage(Account account, Device device,
|
||||||
|
String registrationId, EncryptedOutgoingMessage message)
|
||||||
throws TransientPushFailureException, NotPushRegisteredException
|
throws TransientPushFailureException, NotPushRegisteredException
|
||||||
|
{
|
||||||
|
if (pubSubManager.publish(new WebsocketAddress(account.getId(), device.getId()),
|
||||||
|
new PubSubMessage(PubSubMessage.TYPE_DELIVER, message.serialize())))
|
||||||
|
{
|
||||||
|
websocketMeter.mark();
|
||||||
|
} else {
|
||||||
|
storedMessages.insert(account.getId(), device.getId(), message.serialize());
|
||||||
|
sendPush(registrationId, message.serialize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPush(String registrationId, String message)
|
||||||
|
throws TransientPushFailureException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!apnService.isPresent()) {
|
if (!apnService.isPresent()) {
|
||||||
failure.mark();
|
failureMeter.mark();
|
||||||
throw new TransientPushFailureException("APN access not configured!");
|
throw new TransientPushFailureException("APN access not configured!");
|
||||||
}
|
}
|
||||||
|
|
||||||
String payload = APNS.newPayload()
|
String payload = APNS.newPayload()
|
||||||
.alertBody("Message!")
|
.alertBody("Message!")
|
||||||
.customField(MESSAGE_BODY, message.serialize())
|
.customField(MESSAGE_BODY, message)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
logger.debug("APN Payload: " + payload);
|
logger.debug("APN Payload: " + payload);
|
||||||
|
|
||||||
apnService.get().push(registrationId, payload);
|
apnService.get().push(registrationId, payload);
|
||||||
success.mark();
|
pushMeter.mark();
|
||||||
} catch (NetworkIOException nioe) {
|
} catch (NetworkIOException nioe) {
|
||||||
logger.warn("Network Error", nioe);
|
logger.warn("Network Error", nioe);
|
||||||
failure.mark();
|
failureMeter.mark();
|
||||||
throw new TransientPushFailureException(nioe);
|
throw new TransientPushFailureException(nioe);
|
||||||
} catch (CryptoEncodingException e) {
|
|
||||||
throw new NotPushRegisteredException(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
|
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
|
||||||
|
|||||||
@@ -16,22 +16,24 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.push;
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
import com.google.android.gcm.server.Constants;
|
import com.google.android.gcm.server.Constants;
|
||||||
import com.google.android.gcm.server.Message;
|
import com.google.android.gcm.server.Message;
|
||||||
import com.google.android.gcm.server.Result;
|
import com.google.android.gcm.server.Result;
|
||||||
import com.google.android.gcm.server.Sender;
|
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 org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
|
||||||
public class GCMSender {
|
public class GCMSender {
|
||||||
|
|
||||||
private final Meter success = Metrics.newMeter(GCMSender.class, "sent", "success", TimeUnit.MINUTES);
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(org.whispersystems.textsecuregcm.util.Constants.METRICS_NAME);
|
||||||
private final Meter failure = Metrics.newMeter(GCMSender.class, "sent", "failure", TimeUnit.MINUTES);
|
private final Meter success = metricRegistry.meter(name(getClass(), "sent", "success"));
|
||||||
|
private final Meter failure = metricRegistry.meter(name(getClass(), "sent", "failure"));
|
||||||
|
|
||||||
private final Sender sender;
|
private final Sender sender;
|
||||||
|
|
||||||
@@ -62,8 +64,6 @@ public class GCMSender {
|
|||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new TransientPushFailureException(e);
|
throw new TransientPushFailureException(e);
|
||||||
} catch (CryptoEncodingException e) {
|
|
||||||
throw new NotPushRegisteredException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
|||||||
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.StoredMessageManager;
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
@@ -37,32 +38,45 @@ public class PushSender {
|
|||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
|
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
|
||||||
|
|
||||||
private final AccountsManager accounts;
|
private final AccountsManager accounts;
|
||||||
private final GCMSender gcmSender;
|
private final GCMSender gcmSender;
|
||||||
private final APNSender apnSender;
|
private final APNSender apnSender;
|
||||||
private final StoredMessageManager storedMessageManager;
|
private final WebsocketSender webSocketSender;
|
||||||
|
|
||||||
public PushSender(GcmConfiguration gcmConfiguration,
|
public PushSender(GcmConfiguration gcmConfiguration,
|
||||||
ApnConfiguration apnConfiguration,
|
ApnConfiguration apnConfiguration,
|
||||||
StoredMessageManager storedMessageManager,
|
StoredMessages storedMessages,
|
||||||
AccountsManager accounts)
|
PubSubManager pubSubManager,
|
||||||
|
AccountsManager accounts)
|
||||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
||||||
{
|
{
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.storedMessageManager = storedMessageManager;
|
this.webSocketSender = new WebsocketSender(storedMessages, pubSubManager);
|
||||||
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
|
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
|
||||||
this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
|
this.apnSender = new APNSender(pubSubManager, storedMessages,
|
||||||
|
apnConfiguration.getCertificate(),
|
||||||
|
apnConfiguration.getKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(Account account, Device device, MessageProtos.OutgoingMessageSignal outgoingMessage)
|
public void sendMessage(Account account, Device device, MessageProtos.OutgoingMessageSignal message)
|
||||||
throws NotPushRegisteredException, TransientPushFailureException
|
throws NotPushRegisteredException, TransientPushFailureException
|
||||||
{
|
{
|
||||||
String signalingKey = device.getSignalingKey();
|
try {
|
||||||
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
|
String signalingKey = device.getSignalingKey();
|
||||||
|
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, signalingKey);
|
||||||
|
|
||||||
|
sendMessage(account, device, encryptedMessage);
|
||||||
|
} catch (CryptoEncodingException e) {
|
||||||
|
throw new NotPushRegisteredException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessage(Account account, Device device, EncryptedOutgoingMessage message)
|
||||||
|
throws NotPushRegisteredException, TransientPushFailureException
|
||||||
|
{
|
||||||
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
|
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
|
||||||
else if (device.getApnId() != null) sendApnMessage(account, device, message);
|
else if (device.getApnId() != null) sendApnMessage(account, device, message);
|
||||||
else if (device.getFetchesMessages()) storeFetchedMessage(account, device, message);
|
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message);
|
||||||
else throw new NotPushRegisteredException("No delivery possible!");
|
else throw new NotPushRegisteredException("No delivery possible!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +103,7 @@ public class PushSender {
|
|||||||
throws TransientPushFailureException, NotPushRegisteredException
|
throws TransientPushFailureException, NotPushRegisteredException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
apnSender.sendMessage(device.getApnId(), outgoingMessage);
|
apnSender.sendMessage(account, device, device.getApnId(), outgoingMessage);
|
||||||
} catch (NotPushRegisteredException e) {
|
} catch (NotPushRegisteredException e) {
|
||||||
device.setApnId(null);
|
device.setApnId(null);
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
@@ -97,11 +111,11 @@ public class PushSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storeFetchedMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
private void sendWebSocketMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||||
throws NotPushRegisteredException
|
throws NotPushRegisteredException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
storedMessageManager.storeMessage(account, device, outgoingMessage);
|
webSocketSender.sendMessage(account, device, outgoingMessage);
|
||||||
} catch (CryptoEncodingException e) {
|
} catch (CryptoEncodingException e) {
|
||||||
throw new NotPushRegisteredException(e);
|
throw new NotPushRegisteredException(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 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 org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||||
|
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.websocket.WebsocketAddress;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
|
||||||
|
public class WebsocketSender {
|
||||||
|
|
||||||
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
private final Meter onlineMeter = metricRegistry.meter(name(getClass(), "online"));
|
||||||
|
private final Meter offlineMeter = metricRegistry.meter(name(getClass(), "offline"));
|
||||||
|
|
||||||
|
private final StoredMessages storedMessages;
|
||||||
|
private final PubSubManager pubSubManager;
|
||||||
|
|
||||||
|
public WebsocketSender(StoredMessages storedMessages, PubSubManager pubSubManager) {
|
||||||
|
this.storedMessages = storedMessages;
|
||||||
|
this.pubSubManager = pubSubManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||||
|
throws CryptoEncodingException
|
||||||
|
{
|
||||||
|
sendMessage(account, device, outgoingMessage.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage(Account account, Device device, String serializedMessage) {
|
||||||
|
WebsocketAddress address = new WebsocketAddress(account.getId(), device.getId());
|
||||||
|
PubSubMessage pubSubMessage = new PubSubMessage(PubSubMessage.TYPE_DELIVER, serializedMessage);
|
||||||
|
|
||||||
|
if (pubSubManager.publish(address, pubSubMessage)) {
|
||||||
|
onlineMeter.mark();
|
||||||
|
} else {
|
||||||
|
offlineMeter.mark();
|
||||||
|
storedMessages.insert(account.getId(), device.getId(), serializedMessage);
|
||||||
|
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.List;
|
|||||||
|
|
||||||
public class Account implements Serializable {
|
public class Account implements Serializable {
|
||||||
|
|
||||||
public static final int MEMCACHE_VERION = 2;
|
public static final int MEMCACHE_VERION = 4;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private long id;
|
private long id;
|
||||||
@@ -42,16 +42,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;
|
||||||
@@ -142,4 +140,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,8 +30,9 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
|
|||||||
import org.skife.jdbi.v2.sqlobject.Transaction;
|
import org.skife.jdbi.v2.sqlobject.Transaction;
|
||||||
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
||||||
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKeyBase;
|
||||||
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
@@ -40,6 +41,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class Keys {
|
public abstract class Keys {
|
||||||
@@ -50,66 +52,65 @@ 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")
|
@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);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,17 +121,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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -138,15 +138,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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ public class PubSubManager {
|
|||||||
public void onSubscribe(String channel, int count) {
|
public void onSubscribe(String channel, int count) {
|
||||||
try {
|
try {
|
||||||
WebsocketAddress address = new WebsocketAddress(channel);
|
WebsocketAddress address = new WebsocketAddress(channel);
|
||||||
|
|
||||||
if (address.getAccountId() == 0 && address.getDeviceId() == 0) {
|
if (address.getAccountId() == 0 && address.getDeviceId() == 0) {
|
||||||
synchronized (PubSubManager.this) {
|
synchronized (PubSubManager.this) {
|
||||||
subscribed = true;
|
subscribed = true;
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,21 +16,71 @@
|
|||||||
*/
|
*/
|
||||||
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 org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
|
||||||
|
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 final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
void insert(@Bind("account_id") long accountId, @Bind("device_id") long deviceId, @Bind("encrypted_message") List<String> encryptedOutgoingMessages);
|
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
|
||||||
|
|
||||||
@SqlQuery("DELETE FROM messages WHERE account_id = :account_id AND device_id = :device_id RETURNING encrypted_message")
|
private static final String QUEUE_PREFIX = "msgs";
|
||||||
List<String> getMessagesForDevice(@Bind("account_id") long accountId, @Bind("device_id") long deviceId);
|
|
||||||
}
|
private final JedisPool jedisPool;
|
||||||
|
|
||||||
|
public StoredMessages(JedisPool jedisPool) {
|
||||||
|
this.jedisPool = jedisPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insert(long accountId, long deviceId, String message) {
|
||||||
|
Jedis jedis = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jedis = jedisPool.getResource();
|
||||||
|
|
||||||
|
long queueSize = jedis.lpush(getKey(accountId, deviceId), message);
|
||||||
|
queueSizeHistogram.update(queueSize);
|
||||||
|
|
||||||
|
if (queueSize > 1000) {
|
||||||
|
jedis.ltrim(getKey(accountId, deviceId), 0, 999);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (jedis != null)
|
||||||
|
jedisPool.returnResource(jedis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getMessagesForDevice(long accountId, long deviceId) {
|
||||||
|
List<String> messages = new LinkedList<>();
|
||||||
|
Jedis jedis = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jedis = jedisPool.getResource();
|
||||||
|
String message;
|
||||||
|
|
||||||
|
while ((message = jedis.rpop(QUEUE_PREFIX + accountId + ":" + deviceId)) != null) {
|
||||||
|
messages.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
} finally {
|
||||||
|
if (jedis != null)
|
||||||
|
jedisPool.returnResource(jedis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getKey(long accountId, long deviceId) {
|
||||||
|
return QUEUE_PREFIX + ":" + accountId + ":" + deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
|
public class Constants {
|
||||||
|
|
||||||
|
public static final String METRICS_NAME = "textsecure";
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.websocket;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||||
|
import org.eclipse.jetty.websocket.api.UpgradeResponse;
|
||||||
|
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
|
||||||
|
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||||
|
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.WebsocketController;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||||
|
|
||||||
|
|
||||||
|
public class WebsocketControllerFactory extends WebSocketServlet implements WebSocketCreator {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(WebsocketControllerFactory.class);
|
||||||
|
|
||||||
|
private final PushSender pushSender;
|
||||||
|
private final StoredMessages storedMessages;
|
||||||
|
private final PubSubManager pubSubManager;
|
||||||
|
private final AccountAuthenticator accountAuthenticator;
|
||||||
|
|
||||||
|
public WebsocketControllerFactory(AccountAuthenticator accountAuthenticator,
|
||||||
|
PushSender pushSender,
|
||||||
|
StoredMessages storedMessages,
|
||||||
|
PubSubManager pubSubManager)
|
||||||
|
{
|
||||||
|
this.accountAuthenticator = accountAuthenticator;
|
||||||
|
this.pushSender = pushSender;
|
||||||
|
this.storedMessages = storedMessages;
|
||||||
|
this.pubSubManager = pubSubManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(WebSocketServletFactory factory) {
|
||||||
|
factory.setCreator(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object createWebSocket(UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse) {
|
||||||
|
return new WebsocketController(accountAuthenticator, pushSender, pubSubManager, storedMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" <> "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>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ 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.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;
|
||||||
@@ -16,11 +17,12 @@ 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";
|
||||||
|
|
||||||
@@ -30,26 +32,29 @@ public class AccountControllerTest extends ResourceTest {
|
|||||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
private SmsSender smsSender = mock(SmsSender.class );
|
private SmsSender smsSender = mock(SmsSender.class );
|
||||||
|
|
||||||
@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))
|
||||||
|
.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(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
|
||||||
|
|
||||||
addResource(new AccountController(pendingAccountsManager,
|
|
||||||
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 +65,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 +73,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)
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.testing.ResourceTest;
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
@@ -33,10 +34,11 @@ import org.whispersystems.textsecuregcm.util.VerificationCode;
|
|||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
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.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
public class DeviceControllerTest extends ResourceTest {
|
public class DeviceControllerTest {
|
||||||
@Path("/v1/devices")
|
@Path("/v1/devices")
|
||||||
static class DumbVerificationDeviceController extends DeviceController {
|
static class DumbVerificationDeviceController extends DeviceController {
|
||||||
public DumbVerificationDeviceController(PendingDevicesManager pendingDevices, AccountsManager accounts, RateLimiters rateLimiters) {
|
public DumbVerificationDeviceController(PendingDevicesManager pendingDevices, AccountsManager accounts, RateLimiters rateLimiters) {
|
||||||
@@ -55,10 +57,17 @@ public class DeviceControllerTest extends ResourceTest {
|
|||||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
private Account account = mock(Account.class );
|
private Account account = mock(Account.class );
|
||||||
|
|
||||||
@Override
|
@Rule
|
||||||
protected void setUpResources() throws Exception {
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
addProvider(AuthHelper.getAuthenticator());
|
.addProvider(AuthHelper.getAuthenticator())
|
||||||
|
.addResource(new DumbVerificationDeviceController(pendingDevicesManager,
|
||||||
|
accountsManager,
|
||||||
|
rateLimiters))
|
||||||
|
.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);
|
||||||
@@ -69,19 +78,17 @@ public class DeviceControllerTest extends ResourceTest {
|
|||||||
|
|
||||||
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901"));
|
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901"));
|
||||||
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account));
|
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account));
|
||||||
|
|
||||||
addResource(new DumbVerificationDeviceController(pendingDevicesManager, accountsManager, rateLimiters));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validDeviceRegisterTest() throws Exception {
|
public void validDeviceRegisterTest() throws Exception {
|
||||||
VerificationCode deviceCode = client().resource("/v1/devices/provisioning_code")
|
VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning_code")
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(VerificationCode.class);
|
.get(VerificationCode.class);
|
||||||
|
|
||||||
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
assertThat(deviceCode).isEqualTo(new VerificationCode(5678901));
|
||||||
|
|
||||||
DeviceResponse response = client().resource("/v1/devices/5678901")
|
DeviceResponse response = resources.client().resource("/v1/devices/5678901")
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||||
.entity(new AccountAttributes("keykeykeykey", false, true, 1234))
|
.entity(new AccountAttributes("keykeykeykey", false, true, 1234))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Matchers.anyList;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class DirectoryControllerTest {
|
||||||
|
|
||||||
|
private final RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||||
|
private final RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
|
private final DirectoryManager directoryManager = mock(DirectoryManager.class);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
|
.addProvider(AuthHelper.getAuthenticator())
|
||||||
|
.addResource(new DirectoryController(rateLimiters,
|
||||||
|
directoryManager))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws Exception {
|
||||||
|
when(rateLimiters.getContactsLimiter()).thenReturn(rateLimiter);
|
||||||
|
when(directoryManager.get(anyList())).thenAnswer(new Answer<List<byte[]>>() {
|
||||||
|
@Override
|
||||||
|
public List<byte[]> answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||||
|
List<byte[]> query = (List<byte[]>) invocationOnMock.getArguments()[0];
|
||||||
|
List<byte[]> response = new LinkedList<>(query);
|
||||||
|
response.remove(0);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testContactIntersection() throws Exception {
|
||||||
|
List<String> tokens = new LinkedList<String>() {{
|
||||||
|
add(Base64.encodeBytes("foo".getBytes()));
|
||||||
|
add(Base64.encodeBytes("bar".getBytes()));
|
||||||
|
add(Base64.encodeBytes("baz".getBytes()));
|
||||||
|
}};
|
||||||
|
|
||||||
|
List<String> expectedResponse = new LinkedList<>(tokens);
|
||||||
|
expectedResponse.remove(0);
|
||||||
|
|
||||||
|
ClientResponse response =
|
||||||
|
resources.client().resource("/v1/directory/tokens/")
|
||||||
|
.entity(new ClientContactTokens(tokens))
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.header("Authorization",
|
||||||
|
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER,
|
||||||
|
AuthHelper.VALID_PASSWORD))
|
||||||
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
assertThat(response.getEntity(ClientContactTokens.class).getContacts()).isEqualTo(expectedResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,19 @@ package org.whispersystems.textsecuregcm.tests.controllers;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.sun.jersey.api.client.ClientResponse;
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
import com.yammer.dropwizard.testing.ResourceTest;
|
import org.hamcrest.CoreMatchers;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.controllers.FederationController;
|
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
|
||||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItemV2;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
@@ -23,17 +30,16 @@ import javax.ws.rs.core.MediaType;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static com.yammer.dropwizard.testing.JsonHelpers.jsonFixture;
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.*;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture;
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
public class FederatedControllerTest extends ResourceTest {
|
public class FederatedControllerTest {
|
||||||
|
|
||||||
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
|
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
|
||||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||||
@@ -44,19 +50,32 @@ public class FederatedControllerTest extends ResourceTest {
|
|||||||
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 final SignedPreKey signedPreKey = new SignedPreKey(3333, "foo", "baar");
|
||||||
|
private final PreKeyResponseV2 preKeyResponseV2 = new PreKeyResponseV2("foo", new LinkedList<PreKeyResponseItemV2>());
|
||||||
|
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
private final MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
|
||||||
protected void setUpResources() throws Exception {
|
private final KeysControllerV2 keysControllerV2 = mock(KeysControllerV2.class);
|
||||||
addProvider(AuthHelper.getAuthenticator());
|
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
|
.addProvider(AuthHelper.getAuthenticator())
|
||||||
|
.addResource(new FederationControllerV1(accountsManager, null, messageController, null))
|
||||||
|
.addResource(new FederationControllerV2(accountsManager, null, messageController, keysControllerV2))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws Exception {
|
||||||
List<Device> singleDeviceList = new LinkedList<Device>() {{
|
List<Device> singleDeviceList = new LinkedList<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
List<Device> multiDeviceList = new LinkedList<Device>() {{
|
List<Device> multiDeviceList = new LinkedList<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
|
||||||
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333));
|
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
||||||
@@ -67,14 +86,15 @@ public class FederatedControllerTest extends ResourceTest {
|
|||||||
|
|
||||||
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
|
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
|
||||||
|
|
||||||
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
|
when(keysControllerV2.getSignedKey(any(Account.class))).thenReturn(Optional.of(signedPreKey));
|
||||||
addResource(new FederationController(accountsManager, null, null, messageController));
|
when(keysControllerV2.getDeviceKeys(any(Account.class), anyString(), anyString(), any(Optional.class)))
|
||||||
|
.thenReturn(Optional.of(preKeyResponseV2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSingleDeviceCurrent() throws Exception {
|
public void testSingleDeviceCurrent() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/federation/messages/+14152223333/1/%s", SINGLE_DEVICE_RECIPIENT))
|
resources.client().resource(String.format("/v1/federation/messages/+14152223333/1/%s", SINGLE_DEVICE_RECIPIENT))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
|
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
|
||||||
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class))
|
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
@@ -85,5 +105,14 @@ public class FederatedControllerTest extends ResourceTest {
|
|||||||
verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSignedPreKeyV2() throws Exception {
|
||||||
|
PreKeyResponseV2 response =
|
||||||
|
resources.client().resource("/v2/federation/key/+14152223333/1")
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
|
||||||
|
.get(PreKeyResponseV2.class);
|
||||||
|
|
||||||
|
assertThat("good response", response.getIdentityKey().equals(preKeyResponseV2.getIdentityKey()));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,138 +2,286 @@ 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.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKeyStatus;
|
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
|
||||||
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyStateV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyStateV2;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
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 org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
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.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
public class KeyControllerTest extends ResourceTest {
|
public class KeyControllerTest {
|
||||||
|
|
||||||
private final String EXISTS_NUMBER = "+14152222222";
|
private static final String EXISTS_NUMBER = "+14152222222";
|
||||||
private final String NOT_EXISTS_NUMBER = "+14152222220";
|
private static String NOT_EXISTS_NUMBER = "+14152222220";
|
||||||
|
|
||||||
private final int SAMPLE_REGISTRATION_ID = 999;
|
private static int SAMPLE_REGISTRATION_ID = 999;
|
||||||
private final int SAMPLE_REGISTRATION_ID2 = 1002;
|
private static int SAMPLE_REGISTRATION_ID2 = 1002;
|
||||||
|
private static int SAMPLE_REGISTRATION_ID4 = 1555;
|
||||||
|
|
||||||
private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", "test2", false);
|
private final KeyRecord SAMPLE_KEY = new KeyRecord(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", false);
|
||||||
private final PreKey SAMPLE_KEY2 = new PreKey(2, EXISTS_NUMBER, 2, 5667, "test3", "test4", false );
|
private final KeyRecord SAMPLE_KEY2 = new KeyRecord(2, EXISTS_NUMBER, 2, 5667, "test3", false );
|
||||||
private final PreKey SAMPLE_KEY3 = new PreKey(3, EXISTS_NUMBER, 3, 334, "test5", "test6", false);
|
private final KeyRecord SAMPLE_KEY3 = new KeyRecord(3, EXISTS_NUMBER, 3, 334, "test5", false );
|
||||||
private final Keys keys = mock(Keys.class );
|
private final KeyRecord SAMPLE_KEY4 = new KeyRecord(4, EXISTS_NUMBER, 4, 336, "test6", false );
|
||||||
private final AccountsManager accounts = mock(AccountsManager.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUpResources() {
|
|
||||||
addProvider(AuthHelper.getAuthenticator());
|
|
||||||
|
|
||||||
RateLimiters rateLimiters = mock(RateLimiters.class);
|
private final SignedPreKey SAMPLE_SIGNED_KEY = new SignedPreKey(1111, "foofoo", "sig11");
|
||||||
RateLimiter rateLimiter = mock(RateLimiter.class );
|
private final SignedPreKey SAMPLE_SIGNED_KEY2 = new SignedPreKey(2222, "foobar", "sig22");
|
||||||
|
private final SignedPreKey SAMPLE_SIGNED_KEY3 = new SignedPreKey(3333, "barfoo", "sig33");
|
||||||
|
|
||||||
Device sampleDevice = mock(Device.class );
|
private final Keys keys = mock(Keys.class );
|
||||||
Device sampleDevice2 = mock(Device.class);
|
private final AccountsManager accounts = mock(AccountsManager.class);
|
||||||
Device sampleDevice3 = mock(Device.class);
|
private final Account existsAccount = mock(Account.class );
|
||||||
Account existsAccount = mock(Account.class);
|
|
||||||
|
private RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
|
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
|
.addProvider(AuthHelper.getAuthenticator())
|
||||||
|
.addResource(new KeysControllerV1(rateLimiters, keys, accounts, null))
|
||||||
|
.addResource(new KeysControllerV2(rateLimiters, keys, accounts, null))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
final Device sampleDevice = mock(Device.class);
|
||||||
|
final Device sampleDevice2 = mock(Device.class);
|
||||||
|
final Device sampleDevice3 = mock(Device.class);
|
||||||
|
final Device sampleDevice4 = mock(Device.class);
|
||||||
|
|
||||||
|
List<Device> allDevices = new LinkedList<Device>() {{
|
||||||
|
add(sampleDevice);
|
||||||
|
add(sampleDevice2);
|
||||||
|
add(sampleDevice3);
|
||||||
|
add(sampleDevice4);
|
||||||
|
}};
|
||||||
|
|
||||||
when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID);
|
when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID);
|
||||||
when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
|
when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
|
||||||
when(sampleDevice3.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
|
when(sampleDevice3.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
|
||||||
|
when(sampleDevice4.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID4);
|
||||||
when(sampleDevice.isActive()).thenReturn(true);
|
when(sampleDevice.isActive()).thenReturn(true);
|
||||||
when(sampleDevice2.isActive()).thenReturn(true);
|
when(sampleDevice2.isActive()).thenReturn(true);
|
||||||
when(sampleDevice3.isActive()).thenReturn(false);
|
when(sampleDevice3.isActive()).thenReturn(false);
|
||||||
|
when(sampleDevice4.isActive()).thenReturn(true);
|
||||||
|
when(sampleDevice.getSignedPreKey()).thenReturn(SAMPLE_SIGNED_KEY);
|
||||||
|
when(sampleDevice2.getSignedPreKey()).thenReturn(SAMPLE_SIGNED_KEY2);
|
||||||
|
when(sampleDevice3.getSignedPreKey()).thenReturn(SAMPLE_SIGNED_KEY3);
|
||||||
|
when(sampleDevice4.getSignedPreKey()).thenReturn(null);
|
||||||
|
when(sampleDevice.getId()).thenReturn(1L);
|
||||||
|
when(sampleDevice2.getId()).thenReturn(2L);
|
||||||
|
when(sampleDevice3.getId()).thenReturn(3L);
|
||||||
|
when(sampleDevice4.getId()).thenReturn(4L);
|
||||||
|
|
||||||
when(existsAccount.getDevice(1L)).thenReturn(Optional.of(sampleDevice));
|
when(existsAccount.getDevice(1L)).thenReturn(Optional.of(sampleDevice));
|
||||||
when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2));
|
when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2));
|
||||||
when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3));
|
when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3));
|
||||||
|
when(existsAccount.getDevice(4L)).thenReturn(Optional.of(sampleDevice4));
|
||||||
|
when(existsAccount.getDevice(22L)).thenReturn(Optional.<Device>absent());
|
||||||
|
when(existsAccount.getDevices()).thenReturn(allDevices);
|
||||||
when(existsAccount.isActive()).thenReturn(true);
|
when(existsAccount.isActive()).thenReturn(true);
|
||||||
|
when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey");
|
||||||
|
|
||||||
when(accounts.get(EXISTS_NUMBER)).thenReturn(Optional.of(existsAccount));
|
when(accounts.get(EXISTS_NUMBER)).thenReturn(Optional.of(existsAccount));
|
||||||
when(accounts.get(NOT_EXISTS_NUMBER)).thenReturn(Optional.<Account>absent());
|
when(accounts.get(NOT_EXISTS_NUMBER)).thenReturn(Optional.<Account>absent());
|
||||||
|
|
||||||
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
|
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
|
||||||
|
|
||||||
when(keys.get(eq(EXISTS_NUMBER), eq(1L))).thenReturn(Optional.of(new UnstructuredPreKeyList(SAMPLE_KEY)));
|
List<KeyRecord> singleDevice = new LinkedList<>();
|
||||||
when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.<UnstructuredPreKeyList>absent());
|
singleDevice.add(SAMPLE_KEY);
|
||||||
|
when(keys.get(eq(EXISTS_NUMBER), eq(1L))).thenReturn(Optional.of(singleDevice));
|
||||||
|
|
||||||
List<PreKey> allKeys = new LinkedList<>();
|
when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.<List<KeyRecord>>absent());
|
||||||
allKeys.add(SAMPLE_KEY);
|
|
||||||
allKeys.add(SAMPLE_KEY2);
|
List<KeyRecord> multiDevice = new LinkedList<>();
|
||||||
allKeys.add(SAMPLE_KEY3);
|
multiDevice.add(SAMPLE_KEY);
|
||||||
|
multiDevice.add(SAMPLE_KEY2);
|
||||||
|
multiDevice.add(SAMPLE_KEY3);
|
||||||
|
multiDevice.add(SAMPLE_KEY4);
|
||||||
|
when(keys.get(EXISTS_NUMBER)).thenReturn(Optional.of(multiDevice));
|
||||||
|
|
||||||
when(keys.get(EXISTS_NUMBER)).thenReturn(Optional.of(new UnstructuredPreKeyList(allKeys)));
|
|
||||||
when(keys.getCount(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(5);
|
when(keys.getCount(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(5);
|
||||||
|
|
||||||
addResource(new KeysController(rateLimiters, keys, accounts, null));
|
when(AuthHelper.VALID_DEVICE.getSignedPreKey()).thenReturn(new SignedPreKey(89898, "zoofarb", "sigvalid"));
|
||||||
|
when(AuthHelper.VALID_ACCOUNT.getIdentityKey()).thenReturn(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validKeyStatusTest() throws Exception {
|
public void validKeyStatusTestV1() throws Exception {
|
||||||
PreKeyStatus result = client().resource("/v1/keys")
|
PreKeyCount result = resources.client().resource("/v1/keys")
|
||||||
.header("Authorization",
|
.header("Authorization",
|
||||||
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(PreKeyStatus.class);
|
.get(PreKeyCount.class);
|
||||||
|
|
||||||
assertThat(result.getCount() == 4);
|
assertThat(result.getCount() == 4);
|
||||||
|
|
||||||
verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L));
|
verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validKeyStatusTestV2() throws Exception {
|
||||||
|
PreKeyCount result = resources.client().resource("/v2/keys")
|
||||||
|
.header("Authorization",
|
||||||
|
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(PreKeyCount.class);
|
||||||
|
|
||||||
|
assertThat(result.getCount() == 4);
|
||||||
|
|
||||||
|
verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSignedPreKeyV2() throws Exception {
|
||||||
|
SignedPreKey result = resources.client().resource("/v2/keys/signed")
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(SignedPreKey.class);
|
||||||
|
|
||||||
|
assertThat(result.equals(SAMPLE_SIGNED_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void putSignedPreKeyV2() throws Exception {
|
||||||
|
SignedPreKey test = new SignedPreKey(9999, "fooozzz", "baaarzzz");
|
||||||
|
ClientResponse response = resources.client().resource("/v2/keys/signed")
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.put(ClientResponse.class, test);
|
||||||
|
|
||||||
|
assertThat(response.getStatus() == 204);
|
||||||
|
|
||||||
|
verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(test));
|
||||||
|
verify(accounts).update(eq(AuthHelper.VALID_ACCOUNT));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validLegacyRequestTest() throws Exception {
|
public void validLegacyRequestTest() throws Exception {
|
||||||
PreKey result = client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER))
|
PreKeyV1 result = resources.client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(PreKey.class);
|
.get(PreKeyV1.class);
|
||||||
|
|
||||||
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
||||||
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
||||||
assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY.getIdentityKey());
|
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
|
|
||||||
assertThat(result.getId() == 0);
|
|
||||||
assertThat(result.getNumber() == null);
|
|
||||||
|
|
||||||
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
|
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
|
||||||
verifyNoMoreInteractions(keys);
|
verifyNoMoreInteractions(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validMultiRequestTest() throws Exception {
|
public void validSingleRequestTestV2() throws Exception {
|
||||||
UnstructuredPreKeyList results = client().resource(String.format("/v1/keys/%s/*", EXISTS_NUMBER))
|
PreKeyResponseV2 result = resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(PreKeyResponseV2.class);
|
||||||
|
|
||||||
|
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
|
assertThat(result.getDevices().size()).isEqualTo(1);
|
||||||
|
assertThat(result.getDevices().get(0).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
||||||
|
assertThat(result.getDevices().get(0).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
||||||
|
assertThat(result.getDevices().get(0).getSignedPreKey()).isEqualTo(existsAccount.getDevice(1).get().getSignedPreKey());
|
||||||
|
|
||||||
|
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
|
||||||
|
verifyNoMoreInteractions(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validMultiRequestTestV1() throws Exception {
|
||||||
|
PreKeyResponseV1 results = resources.client().resource(String.format("/v1/keys/%s/*", EXISTS_NUMBER))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(UnstructuredPreKeyList.class);
|
.get(PreKeyResponseV1.class);
|
||||||
|
|
||||||
assertThat(results.getKeys().size()).isEqualTo(2);
|
assertThat(results.getKeys().size()).isEqualTo(3);
|
||||||
|
|
||||||
PreKey result = results.getKeys().get(0);
|
PreKeyV1 result = results.getKeys().get(0);
|
||||||
|
|
||||||
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
||||||
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
||||||
assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY.getIdentityKey());
|
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID);
|
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||||
|
|
||||||
assertThat(result.getId() == 0);
|
|
||||||
assertThat(result.getNumber() == null);
|
|
||||||
|
|
||||||
result = results.getKeys().get(1);
|
result = results.getKeys().get(1);
|
||||||
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
|
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
|
||||||
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
|
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
|
||||||
assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY2.getIdentityKey());
|
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID2);
|
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID2);
|
||||||
|
|
||||||
assertThat(result.getId() == 0);
|
result = results.getKeys().get(2);
|
||||||
assertThat(result.getNumber() == null);
|
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId());
|
||||||
|
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey());
|
||||||
|
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
|
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID4);
|
||||||
|
|
||||||
|
verify(keys).get(eq(EXISTS_NUMBER));
|
||||||
|
verifyNoMoreInteractions(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validMultiRequestTestV2() throws Exception {
|
||||||
|
PreKeyResponseV2 results = resources.client().resource(String.format("/v2/keys/%s/*", EXISTS_NUMBER))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(PreKeyResponseV2.class);
|
||||||
|
|
||||||
|
assertThat(results.getDevices().size()).isEqualTo(3);
|
||||||
|
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
|
|
||||||
|
PreKeyV2 signedPreKey = results.getDevices().get(0).getSignedPreKey();
|
||||||
|
PreKeyV2 preKey = results.getDevices().get(0).getPreKey();
|
||||||
|
long registrationId = results.getDevices().get(0).getRegistrationId();
|
||||||
|
long deviceId = results.getDevices().get(0).getDeviceId();
|
||||||
|
|
||||||
|
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
||||||
|
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
||||||
|
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID);
|
||||||
|
assertThat(signedPreKey.getKeyId()).isEqualTo(SAMPLE_SIGNED_KEY.getKeyId());
|
||||||
|
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY.getPublicKey());
|
||||||
|
assertThat(deviceId).isEqualTo(1);
|
||||||
|
|
||||||
|
signedPreKey = results.getDevices().get(1).getSignedPreKey();
|
||||||
|
preKey = results.getDevices().get(1).getPreKey();
|
||||||
|
registrationId = results.getDevices().get(1).getRegistrationId();
|
||||||
|
deviceId = results.getDevices().get(1).getDeviceId();
|
||||||
|
|
||||||
|
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
|
||||||
|
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
|
||||||
|
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID2);
|
||||||
|
assertThat(signedPreKey.getKeyId()).isEqualTo(SAMPLE_SIGNED_KEY2.getKeyId());
|
||||||
|
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY2.getPublicKey());
|
||||||
|
assertThat(deviceId).isEqualTo(2);
|
||||||
|
|
||||||
|
signedPreKey = results.getDevices().get(2).getSignedPreKey();
|
||||||
|
preKey = results.getDevices().get(2).getPreKey();
|
||||||
|
registrationId = results.getDevices().get(2).getRegistrationId();
|
||||||
|
deviceId = results.getDevices().get(2).getDeviceId();
|
||||||
|
|
||||||
|
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId());
|
||||||
|
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey());
|
||||||
|
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID4);
|
||||||
|
assertThat(signedPreKey).isNull();
|
||||||
|
assertThat(deviceId).isEqualTo(4);
|
||||||
|
|
||||||
verify(keys).get(eq(EXISTS_NUMBER));
|
verify(keys).get(eq(EXISTS_NUMBER));
|
||||||
verifyNoMoreInteractions(keys);
|
verifyNoMoreInteractions(keys);
|
||||||
@@ -141,28 +289,135 @@ public class KeyControllerTest extends ResourceTest {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invalidRequestTest() throws Exception {
|
public void invalidRequestTestV1() throws Exception {
|
||||||
ClientResponse response = client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
|
ClientResponse response = resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(ClientResponse.class);
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(404);
|
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void unauthorizedRequestTest() throws Exception {
|
public void invalidRequestTestV2() throws Exception {
|
||||||
|
ClientResponse response = resources.client().resource(String.format("/v2/keys/%s", NOT_EXISTS_NUMBER))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
|
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anotherInvalidRequestTestV2() throws Exception {
|
||||||
|
ClientResponse response = resources.client().resource(String.format("/v2/keys/%s/22", EXISTS_NUMBER))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
|
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unauthorizedRequestTestV1() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
|
resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
|
||||||
.get(ClientResponse.class);
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(401);
|
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
|
||||||
|
|
||||||
response =
|
response =
|
||||||
client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
|
resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
|
||||||
.get(ClientResponse.class);
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(401);
|
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unauthorizedRequestTestV2() throws Exception {
|
||||||
|
ClientResponse response =
|
||||||
|
resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
|
||||||
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
|
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
|
||||||
|
|
||||||
|
response =
|
||||||
|
resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
|
||||||
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
|
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void putKeysTestV1() throws Exception {
|
||||||
|
final PreKeyV1 newKey = new PreKeyV1(1L, 31337, "foobar", "foobarbaz");
|
||||||
|
final PreKeyV1 lastResortKey = new PreKeyV1(1L, 0xFFFFFF, "fooz", "foobarbaz");
|
||||||
|
|
||||||
|
List<PreKeyV1> preKeys = new LinkedList<PreKeyV1>() {{
|
||||||
|
add(newKey);
|
||||||
|
}};
|
||||||
|
|
||||||
|
PreKeyStateV1 preKeyList = new PreKeyStateV1();
|
||||||
|
preKeyList.setKeys(preKeys);
|
||||||
|
preKeyList.setLastResortKey(lastResortKey);
|
||||||
|
|
||||||
|
ClientResponse response =
|
||||||
|
resources.client().resource("/v1/keys")
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.put(ClientResponse.class, preKeyList);
|
||||||
|
|
||||||
|
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204);
|
||||||
|
|
||||||
|
ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class );
|
||||||
|
ArgumentCaptor<PreKeyV1> lastResortCaptor = ArgumentCaptor.forClass(PreKeyV1.class);
|
||||||
|
verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), lastResortCaptor.capture());
|
||||||
|
|
||||||
|
List<PreKeyV1> capturedList = listCaptor.getValue();
|
||||||
|
assertThat(capturedList.size() == 1);
|
||||||
|
assertThat(capturedList.get(0).getIdentityKey().equals("foobarbaz"));
|
||||||
|
assertThat(capturedList.get(0).getKeyId() == 31337);
|
||||||
|
assertThat(capturedList.get(0).getPublicKey().equals("foobar"));
|
||||||
|
|
||||||
|
assertThat(lastResortCaptor.getValue().getPublicKey().equals("fooz"));
|
||||||
|
assertThat(lastResortCaptor.getValue().getIdentityKey().equals("foobarbaz"));
|
||||||
|
|
||||||
|
verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq("foobarbaz"));
|
||||||
|
verify(accounts).update(AuthHelper.VALID_ACCOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void putKeysTestV2() throws Exception {
|
||||||
|
final PreKeyV2 preKey = new PreKeyV2(31337, "foobar");
|
||||||
|
final PreKeyV2 lastResortKey = new PreKeyV2(31339, "barbar");
|
||||||
|
final SignedPreKey signedPreKey = new SignedPreKey(31338, "foobaz", "myvalidsig");
|
||||||
|
final String identityKey = "barbar";
|
||||||
|
|
||||||
|
List<PreKeyV2> preKeys = new LinkedList<PreKeyV2>() {{
|
||||||
|
add(preKey);
|
||||||
|
}};
|
||||||
|
|
||||||
|
PreKeyStateV2 preKeyState = new PreKeyStateV2(identityKey, signedPreKey, preKeys, lastResortKey);
|
||||||
|
|
||||||
|
ClientResponse response =
|
||||||
|
resources.client().resource("/v2/keys")
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.put(ClientResponse.class, preKeyState);
|
||||||
|
|
||||||
|
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204);
|
||||||
|
|
||||||
|
ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class);
|
||||||
|
verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), eq(lastResortKey));
|
||||||
|
|
||||||
|
List<PreKeyV2> capturedList = listCaptor.getValue();
|
||||||
|
assertThat(capturedList.size() == 1);
|
||||||
|
assertThat(capturedList.get(0).getKeyId() == 31337);
|
||||||
|
assertThat(capturedList.get(0).getPublicKey().equals("foobar"));
|
||||||
|
|
||||||
|
verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq("barbar"));
|
||||||
|
verify(AuthHelper.VALID_DEVICE).setSignedPreKey(eq(signedPreKey));
|
||||||
|
verify(accounts).update(AuthHelper.VALID_ACCOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,10 @@ package org.whispersystems.textsecuregcm.tests.controllers;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.sun.jersey.api.client.ClientResponse;
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
import com.yammer.dropwizard.testing.ResourceTest;
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||||
@@ -24,42 +24,46 @@ import javax.ws.rs.core.MediaType;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static com.yammer.dropwizard.testing.JsonHelpers.asJson;
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static com.yammer.dropwizard.testing.JsonHelpers.jsonFixture;
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.asJson;
|
||||||
|
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture;
|
||||||
|
|
||||||
public class MessageControllerTest extends ResourceTest {
|
public class MessageControllerTest {
|
||||||
|
|
||||||
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
|
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
|
||||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||||
|
|
||||||
private PushSender pushSender = mock(PushSender.class );
|
private final PushSender pushSender = mock(PushSender.class );
|
||||||
private FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
||||||
private AccountsManager accountsManager = mock(AccountsManager.class );
|
private final AccountsManager accountsManager = mock(AccountsManager.class );
|
||||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
private final RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
private final RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
|
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
@Rule
|
||||||
protected void setUpResources() throws Exception {
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
addProvider(AuthHelper.getAuthenticator());
|
.addProvider(AuthHelper.getAuthenticator())
|
||||||
|
.addResource(new MessageController(rateLimiters, pushSender, accountsManager,
|
||||||
|
federatedClientManager))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws Exception {
|
||||||
List<Device> singleDeviceList = new LinkedList<Device>() {{
|
List<Device> singleDeviceList = new LinkedList<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
List<Device> multiDeviceList = new LinkedList<Device>() {{
|
List<Device> multiDeviceList = new LinkedList<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
|
||||||
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333));
|
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
||||||
@@ -69,15 +73,12 @@ public class MessageControllerTest extends ResourceTest {
|
|||||||
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
|
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
|
||||||
|
|
||||||
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
|
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
|
||||||
|
|
||||||
addResource(new MessageController(rateLimiters, pushSender, accountsManager,
|
|
||||||
federatedClientManager));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSingleDeviceLegacy() throws Exception {
|
public synchronized void testSingleDeviceLegacy() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource("/v1/messages/")
|
resources.client().resource("/v1/messages/")
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.entity(mapper.readValue(jsonFixture("fixtures/legacy_message_single_device.json"), IncomingMessageList.class))
|
.entity(mapper.readValue(jsonFixture("fixtures/legacy_message_single_device.json"), IncomingMessageList.class))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
@@ -85,13 +86,13 @@ public class MessageControllerTest extends ResourceTest {
|
|||||||
|
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSingleDeviceCurrent() throws Exception {
|
public synchronized void testSingleDeviceCurrent() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT))
|
resources.client().resource(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class))
|
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
@@ -99,13 +100,13 @@ public class MessageControllerTest extends ResourceTest {
|
|||||||
|
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(204)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(204)));
|
||||||
|
|
||||||
verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMultiDeviceMissing() throws Exception {
|
public synchronized void testMultiDeviceMissing() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class))
|
.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
@@ -121,9 +122,9 @@ public class MessageControllerTest extends ResourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMultiDeviceExtra() throws Exception {
|
public synchronized void testMultiDeviceExtra() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.entity(mapper.readValue(jsonFixture("fixtures/current_message_extra_device.json"), IncomingMessageList.class))
|
.entity(mapper.readValue(jsonFixture("fixtures/current_message_extra_device.json"), IncomingMessageList.class))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
@@ -139,9 +140,9 @@ public class MessageControllerTest extends ResourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMultiDevice() throws Exception {
|
public synchronized void testMultiDevice() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.entity(mapper.readValue(jsonFixture("fixtures/current_message_multi_device.json"), IncomingMessageList.class))
|
.entity(mapper.readValue(jsonFixture("fixtures/current_message_multi_device.json"), IncomingMessageList.class))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
@@ -153,9 +154,9 @@ public class MessageControllerTest extends ResourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRegistrationIdMismatch() throws Exception {
|
public synchronized void testRegistrationIdMismatch() throws Exception {
|
||||||
ClientResponse response =
|
ClientResponse response =
|
||||||
client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
resources.client().resource(String.format("/v1/messages/%s", MULTI_DEVICE_RECIPIENT))
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.entity(mapper.readValue(jsonFixture("fixtures/current_message_registration_id.json"), IncomingMessageList.class))
|
.entity(mapper.readValue(jsonFixture("fixtures/current_message_registration_id.json"), IncomingMessageList.class))
|
||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
|||||||
@@ -2,25 +2,28 @@ package org.whispersystems.textsecuregcm.tests.controllers;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.auth.basic.BasicCredentials;
|
import org.eclipse.jetty.websocket.api.CloseStatus;
|
||||||
import org.eclipse.jetty.websocket.WebSocket;
|
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||||
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
|
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.controllers.WebsocketController;
|
import org.whispersystems.textsecuregcm.controllers.WebsocketController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.WebsocketControllerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
|
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
|
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.WebsocketControllerFactory;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import java.util.HashMap;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.fest.assertions.api.Assertions.assertThat;
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@@ -34,13 +37,14 @@ public class WebsocketControllerTest {
|
|||||||
private static final String VALID_PASSWORD = "secure";
|
private static final String VALID_PASSWORD = "secure";
|
||||||
private static final String INVALID_PASSWORD = "insecure";
|
private static final String INVALID_PASSWORD = "insecure";
|
||||||
|
|
||||||
private final StoredMessageManager storedMessageManager = mock(StoredMessageManager.class);
|
private static final StoredMessages storedMessages = mock(StoredMessages.class);
|
||||||
private final AccountAuthenticator accountAuthenticator = mock(AccountAuthenticator.class);
|
private static final AccountAuthenticator accountAuthenticator = mock(AccountAuthenticator.class);
|
||||||
private final PubSubManager pubSubManager = mock(PubSubManager.class );
|
private static final PubSubManager pubSubManager = mock(PubSubManager.class );
|
||||||
private final Account account = mock(Account.class );
|
private static final Account account = mock(Account.class );
|
||||||
private final Device device = mock(Device.class );
|
private static final Device device = mock(Device.class );
|
||||||
private final HttpServletRequest request = mock(HttpServletRequest.class );
|
private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class );
|
||||||
private final WebSocket.Connection connection = mock(WebSocket.Connection.class);
|
private static final Session session = mock(Session.class );
|
||||||
|
private static final PushSender pushSender = mock(PushSender.class);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCredentials() throws Exception {
|
public void testCredentials() throws Exception {
|
||||||
@@ -50,23 +54,35 @@ public class WebsocketControllerTest {
|
|||||||
when(accountAuthenticator.authenticate(eq(new BasicCredentials(INVALID_USER, INVALID_PASSWORD))))
|
when(accountAuthenticator.authenticate(eq(new BasicCredentials(INVALID_USER, INVALID_PASSWORD))))
|
||||||
.thenReturn(Optional.<Account>absent());
|
.thenReturn(Optional.<Account>absent());
|
||||||
|
|
||||||
WebsocketControllerFactory factory = new WebsocketControllerFactory(accountAuthenticator,
|
when(session.getUpgradeRequest()).thenReturn(upgradeRequest);
|
||||||
storedMessageManager,
|
|
||||||
pubSubManager);
|
|
||||||
|
|
||||||
when(request.getParameter(eq("user"))).thenReturn(VALID_USER);
|
WebsocketController controller = new WebsocketController(accountAuthenticator, pushSender, pubSubManager, storedMessages);
|
||||||
when(request.getParameter(eq("password"))).thenReturn(VALID_PASSWORD);
|
|
||||||
|
|
||||||
assertThat(factory.checkOrigin(request, "foobar")).isEqualTo(true);
|
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
|
||||||
|
put("login", new String[] {VALID_USER});
|
||||||
|
put("password", new String[] {VALID_PASSWORD});
|
||||||
|
}});
|
||||||
|
|
||||||
when(request.getParameter(eq("user"))).thenReturn(INVALID_USER);
|
controller.onWebSocketConnect(session);
|
||||||
when(request.getParameter(eq("password"))).thenReturn(INVALID_PASSWORD);
|
|
||||||
|
|
||||||
assertThat(factory.checkOrigin(request, "foobar")).isEqualTo(false);
|
verify(session, never()).close();
|
||||||
|
verify(session, never()).close(any(CloseStatus.class));
|
||||||
|
verify(session, never()).close(anyInt(), anyString());
|
||||||
|
|
||||||
|
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
|
||||||
|
put("login", new String[] {INVALID_USER});
|
||||||
|
put("password", new String[] {INVALID_PASSWORD});
|
||||||
|
}});
|
||||||
|
|
||||||
|
controller.onWebSocketConnect(session);
|
||||||
|
|
||||||
|
verify(session).close(any(CloseStatus.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpen() throws Exception {
|
public void testOpen() throws Exception {
|
||||||
|
RemoteEndpoint remote = mock(RemoteEndpoint.class);
|
||||||
|
|
||||||
List<String> outgoingMessages = new LinkedList<String>() {{
|
List<String> outgoingMessages = new LinkedList<String>() {{
|
||||||
add("first");
|
add("first");
|
||||||
add("second");
|
add("second");
|
||||||
@@ -76,36 +92,36 @@ public class WebsocketControllerTest {
|
|||||||
when(device.getId()).thenReturn(2L);
|
when(device.getId()).thenReturn(2L);
|
||||||
when(account.getId()).thenReturn(31337L);
|
when(account.getId()).thenReturn(31337L);
|
||||||
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
|
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
|
||||||
|
when(session.getRemote()).thenReturn(remote);
|
||||||
|
when(session.getUpgradeRequest()).thenReturn(upgradeRequest);
|
||||||
|
|
||||||
when(request.getParameter(eq("user"))).thenReturn(VALID_USER);
|
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
|
||||||
when(request.getParameter(eq("password"))).thenReturn(VALID_PASSWORD);
|
put("login", new String[] {VALID_USER});
|
||||||
|
put("password", new String[] {VALID_PASSWORD});
|
||||||
|
}});
|
||||||
|
|
||||||
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
|
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
|
||||||
.thenReturn(Optional.of(account));
|
.thenReturn(Optional.of(account));
|
||||||
|
|
||||||
when(storedMessageManager.getOutgoingMessages(eq(account), eq(device))).thenReturn(outgoingMessages);
|
when(storedMessages.getMessagesForDevice(account.getId(), device.getId())).thenReturn(outgoingMessages);
|
||||||
|
|
||||||
WebsocketControllerFactory factory = new WebsocketControllerFactory(accountAuthenticator,
|
WebsocketControllerFactory factory = new WebsocketControllerFactory(accountAuthenticator, pushSender, storedMessages, pubSubManager);
|
||||||
storedMessageManager,
|
WebsocketController controller = (WebsocketController) factory.createWebSocket(null, null);
|
||||||
pubSubManager);
|
|
||||||
|
|
||||||
assertThat(factory.checkOrigin(request, "foobar")).isEqualTo(true);
|
controller.onWebSocketConnect(session);
|
||||||
|
|
||||||
WebsocketController socket = (WebsocketController)factory.doWebSocketConnect(request, "foo");
|
verify(pubSubManager).subscribe(eq(new WebsocketAddress(31337L, 2L)), eq((controller)));
|
||||||
socket.onOpen(connection);
|
verify(remote, times(3)).sendStringByFuture(anyString());
|
||||||
|
|
||||||
verify(pubSubManager).subscribe(eq(new WebsocketAddress(31337L, 2L)), eq((socket)));
|
controller.onWebSocketText(mapper.writeValueAsString(new AcknowledgeWebsocketMessage(1)));
|
||||||
verify(connection, times(3)).sendMessage(anyString());
|
controller.onWebSocketClose(1000, "Closed");
|
||||||
|
|
||||||
socket.onMessage(mapper.writeValueAsString(new AcknowledgeWebsocketMessage(1)));
|
|
||||||
socket.onClose(1000, "Closed");
|
|
||||||
|
|
||||||
List<String> pending = new LinkedList<String>() {{
|
List<String> pending = new LinkedList<String>() {{
|
||||||
add("first");
|
add("first");
|
||||||
add("third");
|
add("third");
|
||||||
}};
|
}};
|
||||||
|
|
||||||
verify(storedMessageManager).storeMessages(eq(account), eq(device), eq(pending));
|
verify(pushSender, times(2)).sendMessage(eq(account), eq(device), any(EncryptedOutgoingMessage.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package org.whispersystems.textsecuregcm.tests.entities;
|
package org.whispersystems.textsecuregcm.tests.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
import static com.yammer.dropwizard.testing.JsonHelpers.*;
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.asJson;
|
||||||
|
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.fromJson;
|
||||||
|
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture;
|
||||||
|
|
||||||
public class ClientContactTest {
|
public class ClientContactTest {
|
||||||
|
|
||||||
@@ -41,4 +44,5 @@ public class ClientContactTest {
|
|||||||
is(contact));
|
is(contact));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ package org.whispersystems.textsecuregcm.tests.entities;
|
|||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
import static com.yammer.dropwizard.testing.JsonHelpers.*;
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*;
|
||||||
|
|
||||||
public class PreKeyTest {
|
public class PreKeyTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serializeToJSON() throws Exception {
|
public void serializeToJSONV1() throws Exception {
|
||||||
PreKey preKey = new PreKey(1, "+14152222222", 1, 1234, "test", "identityTest", false);
|
PreKeyV1 preKey = new PreKeyV1(1, 1234, "test", "identityTest");
|
||||||
preKey.setRegistrationId(987);
|
preKey.setRegistrationId(987);
|
||||||
|
|
||||||
assertThat("Basic Contact Serialization works",
|
assertThat("Basic Contact Serialization works",
|
||||||
@@ -23,7 +24,7 @@ public class PreKeyTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void deserializeFromJSON() throws Exception {
|
public void deserializeFromJSONV() throws Exception {
|
||||||
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
|
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
|
||||||
"whisper", true);
|
"whisper", true);
|
||||||
|
|
||||||
@@ -32,4 +33,13 @@ public class PreKeyTest {
|
|||||||
is(contact));
|
is(contact));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serializeToJSONV2() throws Exception {
|
||||||
|
PreKeyV2 preKey = new PreKeyV2(1234, "test");
|
||||||
|
|
||||||
|
assertThat("PreKeyV2 Serialization works",
|
||||||
|
asJson(preKey),
|
||||||
|
is(equalTo(jsonFixture("fixtures/prekey_v2.json"))));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,20 +27,20 @@ public class AuthHelper {
|
|||||||
public static final String INVVALID_NUMBER = "+14151111111";
|
public static final String INVVALID_NUMBER = "+14151111111";
|
||||||
public static final String INVALID_PASSWORD = "bar";
|
public static final String INVALID_PASSWORD = "bar";
|
||||||
|
|
||||||
public static MultiBasicAuthProvider<FederatedPeer, Account> getAuthenticator() {
|
public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class );
|
||||||
AccountsManager accounts = mock(AccountsManager.class );
|
public static Account VALID_ACCOUNT = mock(Account.class );
|
||||||
Account account = mock(Account.class );
|
public static Device VALID_DEVICE = mock(Device.class );
|
||||||
Device device = mock(Device.class );
|
public static AuthenticationCredentials VALID_CREDENTIALS = mock(AuthenticationCredentials.class);
|
||||||
AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
|
|
||||||
|
|
||||||
when(credentials.verify("foo")).thenReturn(true);
|
public static MultiBasicAuthProvider<FederatedPeer, Account> getAuthenticator() {
|
||||||
when(device.getAuthenticationCredentials()).thenReturn(credentials);
|
when(VALID_CREDENTIALS.verify("foo")).thenReturn(true);
|
||||||
when(device.getId()).thenReturn(1L);
|
when(VALID_DEVICE.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS);
|
||||||
when(account.getDevice(anyLong())).thenReturn(Optional.of(device));
|
when(VALID_DEVICE.getId()).thenReturn(1L);
|
||||||
when(account.getNumber()).thenReturn(VALID_NUMBER);
|
when(VALID_ACCOUNT.getDevice(anyLong())).thenReturn(Optional.of(VALID_DEVICE));
|
||||||
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
|
when(VALID_ACCOUNT.getNumber()).thenReturn(VALID_NUMBER);
|
||||||
when(account.getRelay()).thenReturn(Optional.<String>absent());
|
when(VALID_ACCOUNT.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE));
|
||||||
when(accounts.get(VALID_NUMBER)).thenReturn(Optional.of(account));
|
when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.<String>absent());
|
||||||
|
when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT));
|
||||||
|
|
||||||
List<FederatedPeer> peer = new LinkedList<FederatedPeer>() {{
|
List<FederatedPeer> peer = new LinkedList<FederatedPeer>() {{
|
||||||
add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz"));
|
add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz"));
|
||||||
@@ -51,7 +51,7 @@ public class AuthHelper {
|
|||||||
|
|
||||||
return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(federationConfiguration),
|
return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(federationConfiguration),
|
||||||
FederatedPeer.class,
|
FederatedPeer.class,
|
||||||
new AccountAuthenticator(accounts),
|
new AccountAuthenticator(ACCOUNTS_MANAGER),
|
||||||
Account.class, "WhisperServer");
|
Account.class, "WhisperServer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.tests.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static io.dropwizard.testing.FixtureHelpers.fixture;
|
||||||
|
|
||||||
|
public class JsonHelpers {
|
||||||
|
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public static String asJson(Object object) throws JsonProcessingException {
|
||||||
|
return objectMapper.writeValueAsString(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T fromJson(String value, Class<T> clazz) throws IOException {
|
||||||
|
return objectMapper.readValue(value, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String jsonFixture(String filename) throws IOException {
|
||||||
|
return objectMapper.writeValueAsString(objectMapper.readValue(fixture(filename), JsonNode.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"token" : "BQVVHxMt5zAFXA",
|
"relay" : "whisper",
|
||||||
"relay" : "whisper"
|
"token" : "BQVVHxMt5zAFXA"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"token" : "BQVVHxMt5zAFXA",
|
|
||||||
"relay" : "whisper",
|
"relay" : "whisper",
|
||||||
"supportsSms" : true
|
"supportsSms" : true,
|
||||||
|
"token" : "BQVVHxMt5zAFXA"
|
||||||
}
|
}
|
||||||
4
src/test/resources/fixtures/prekey_v2.json
Normal file
4
src/test/resources/fixtures/prekey_v2.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"keyId" : 1234,
|
||||||
|
"publicKey" : "test"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user