mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-09 01:30:15 +00:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac96f906b3 | ||
|
|
cc395e914f | ||
|
|
f8063f8faf | ||
|
|
958ada9110 | ||
|
|
3452ea29b8 | ||
|
|
675b6f4b5e | ||
|
|
4fab67b0f5 | ||
|
|
8a2131416d | ||
|
|
2525304215 | ||
|
|
fdb35d4f77 | ||
|
|
222c7ea641 | ||
|
|
8f2722263f | ||
|
|
fd662e3401 | ||
|
|
bc65461ecb | ||
|
|
30017371df | ||
|
|
b944b86bf8 | ||
|
|
6ba8352fa6 | ||
|
|
aadf76692e | ||
|
|
c9a1386a55 | ||
|
|
4eb88a3e02 | ||
|
|
160c0bfe14 | ||
|
|
4cd098af1d | ||
|
|
362abd618f | ||
|
|
69de9f6684 | ||
|
|
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 | ||
|
|
aea3f299a0 | ||
|
|
5667476780 | ||
|
|
b263f47826 | ||
|
|
21723d6313 | ||
|
|
a63cdc76b0 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ run.sh
|
||||
local.yml
|
||||
config/production.yml
|
||||
config/federated.yml
|
||||
config/staging.yml
|
||||
.opsmanage
|
||||
|
||||
@@ -15,6 +15,7 @@ nexmo:
|
||||
number:
|
||||
|
||||
gcm:
|
||||
senderId:
|
||||
apiKey:
|
||||
|
||||
# Optional. Only if iOS clients are supported.
|
||||
@@ -56,9 +57,6 @@ graphite:
|
||||
host:
|
||||
port:
|
||||
|
||||
http:
|
||||
shutdownGracePeriod: 0s
|
||||
|
||||
database:
|
||||
# the name of your JDBC driver
|
||||
driverClass: org.postgresql.Driver
|
||||
@@ -75,24 +73,3 @@ database:
|
||||
# any properties specific to your JDBC driver:
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
|
||||
# the maximum amount of time to wait on an empty pool before throwing an exception
|
||||
maxWaitForConnection: 1s
|
||||
|
||||
# the SQL query to run when validating a connection's liveness
|
||||
validationQuery: "/* MyService Health Check */ SELECT 1"
|
||||
|
||||
# the minimum number of connections to keep open
|
||||
minSize: 8
|
||||
|
||||
# the maximum number of connections to keep open
|
||||
maxSize: 32
|
||||
|
||||
# whether or not idle connections should be validated
|
||||
checkConnectionWhileIdle: false
|
||||
|
||||
# how long a connection must be held before it can be validated
|
||||
checkConnectionHealthWhenIdleFor: 10s
|
||||
|
||||
# the maximum lifetime of an idle connection
|
||||
closeConnectionIfIdleFor: 1 minute
|
||||
|
||||
142
pom.xml
142
pom.xml
@@ -9,24 +9,78 @@
|
||||
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<version>0.5</version>
|
||||
<version>0.25</version>
|
||||
|
||||
<properties>
|
||||
<dropwizard.version>0.7.1</dropwizard.version>
|
||||
<jackson.api.version>2.3.3</jackson.api.version>
|
||||
<commons-codec.version>1.6</commons-codec.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-jdbi</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-auth</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-client</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-migrations</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-metrics-graphite</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-client</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.jersey</groupId>
|
||||
<artifactId>jersey-json</artifactId>
|
||||
<version>1.18.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.codahale.metrics</groupId>
|
||||
<artifactId>metrics-graphite</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-server</artifactId>
|
||||
<version>9.0.7.v20131107</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk16</artifactId>
|
||||
<version>140</version>
|
||||
</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>
|
||||
<groupId>com.google.android.gcm</groupId>
|
||||
<artifactId>gcm-server</artifactId>
|
||||
@@ -56,7 +110,7 @@
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>2.4.1</version>
|
||||
<version>2.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -66,35 +120,10 @@
|
||||
<type>jar</type>
|
||||
<scope>compile</scope>
|
||||
</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>
|
||||
<groupId>com.twilio.sdk</groupId>
|
||||
<artifactId>twilio-java-sdk</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<version>3.4.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -102,26 +131,35 @@
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>9.1-901.jdbc4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.jersey</groupId>
|
||||
<artifactId>jersey-json</artifactId>
|
||||
<version>1.17.1</version>
|
||||
<groupId>org.igniterealtime.smack</groupId>
|
||||
<artifactId>smack-tcp</artifactId>
|
||||
<version>4.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.websocket</groupId>
|
||||
<artifactId>websocket-resources</artifactId>
|
||||
<version>0.1-SNAPSHOT</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>
|
||||
|
||||
<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>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
||||
@@ -20,6 +20,15 @@ option java_package = "org.whispersystems.textsecuregcm.entities";
|
||||
option java_outer_classname = "MessageProtos";
|
||||
|
||||
message OutgoingMessageSignal {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
CIPHERTEXT = 1;
|
||||
KEY_EXCHANGE = 2;
|
||||
PREKEY_BUNDLE = 3;
|
||||
PLAINTEXT = 4;
|
||||
RECEIPT = 5;
|
||||
}
|
||||
|
||||
optional uint32 type = 1;
|
||||
optional string source = 2;
|
||||
optional uint32 sourceDevice = 7;
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
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.DataDogConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MetricsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
@@ -35,6 +35,10 @@ import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import io.dropwizard.Configuration;
|
||||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
|
||||
public class WhisperServerConfiguration extends Configuration {
|
||||
|
||||
@NotNull
|
||||
@@ -46,8 +50,9 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
private NexmoConfiguration nexmo;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private GcmConfiguration gcm;
|
||||
private PushConfiguration push;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -64,9 +69,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private RedisConfiguration redis;
|
||||
|
||||
@JsonProperty
|
||||
private ApnConfiguration apn = new ApnConfiguration();
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private FederationConfiguration federation = new FederationConfiguration();
|
||||
@@ -74,7 +76,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DatabaseConfiguration database = new DatabaseConfiguration();
|
||||
private DataSourceFactory database = new DataSourceFactory();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@@ -87,12 +89,21 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DataDogConfiguration datadog = new DataDogConfiguration();
|
||||
private MetricsConfiguration viz = new MetricsConfiguration();
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private WebsocketConfiguration websocket = new WebsocketConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
|
||||
|
||||
|
||||
public WebsocketConfiguration getWebsocketConfiguration() {
|
||||
return websocket;
|
||||
}
|
||||
@@ -105,12 +116,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return nexmo;
|
||||
}
|
||||
|
||||
public GcmConfiguration getGcmConfiguration() {
|
||||
return gcm;
|
||||
public PushConfiguration getPushConfiguration() {
|
||||
return push;
|
||||
}
|
||||
|
||||
public ApnConfiguration getApnConfiguration() {
|
||||
return apn;
|
||||
public JerseyClientConfiguration getJerseyClientConfiguration() {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public S3Configuration getS3Configuration() {
|
||||
@@ -125,7 +136,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return redis;
|
||||
}
|
||||
|
||||
public DatabaseConfiguration getDatabaseConfiguration() {
|
||||
public DataSourceFactory getDataSourceFactory() {
|
||||
return database;
|
||||
}
|
||||
|
||||
@@ -141,7 +152,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return graphite;
|
||||
}
|
||||
|
||||
public DataDogConfiguration getDataDogConfiguration() {
|
||||
return datadog;
|
||||
public MetricsConfiguration getMetricsConfiguration() {
|
||||
return viz;
|
||||
}
|
||||
|
||||
public RedPhoneConfiguration getRedphoneConfiguration() {
|
||||
return redphone;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,14 @@
|
||||
*/
|
||||
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.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.core.Clock;
|
||||
import com.yammer.metrics.core.MetricPredicate;
|
||||
import com.yammer.metrics.reporting.DatadogReporter;
|
||||
import com.yammer.metrics.reporting.GraphiteReporter;
|
||||
import com.sun.jersey.api.client.Client;
|
||||
import net.spy.memcached.MemcachedClient;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.skife.jdbi.v2.DBI;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
|
||||
@@ -39,20 +33,31 @@ import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
import org.whispersystems.textsecuregcm.controllers.FederationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
|
||||
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.WebsocketControllerFactory;
|
||||
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.JsonMetricsReporter;
|
||||
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
||||
import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
|
||||
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||
import org.whispersystems.textsecuregcm.push.FeedbackHandler;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.PushServiceClient;
|
||||
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||
@@ -66,18 +71,35 @@ import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevices;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
|
||||
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.websocket.ConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
|
||||
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.FilterRegistration;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import java.security.Security;
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.Application;
|
||||
import io.dropwizard.client.JerseyClientBuilder;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.jdbi.DBIFactory;
|
||||
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
|
||||
import io.dropwizard.migrations.MigrationsBundle;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
import io.dropwizard.setup.Environment;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
||||
public class WhisperServerService extends Application<WhisperServerConfiguration> {
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
@@ -85,94 +107,129 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
||||
|
||||
@Override
|
||||
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||
bootstrap.setName("whisper-server");
|
||||
bootstrap.addCommand(new DirectoryCommand());
|
||||
bootstrap.addCommand(new VacuumCommand());
|
||||
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
|
||||
@Override
|
||||
public DatabaseConfiguration getDatabaseConfiguration(WhisperServerConfiguration configuration) {
|
||||
return configuration.getDatabaseConfiguration();
|
||||
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
return configuration.getDataSourceFactory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "whisper-server";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(WhisperServerConfiguration config, Environment environment)
|
||||
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();
|
||||
DBI jdbi = dbiFactory.build(environment, config.getDatabaseConfiguration(), "postgresql");
|
||||
DBI jdbi = dbiFactory.build(environment, config.getDataSourceFactory(), "postgresql");
|
||||
|
||||
Accounts accounts = jdbi.onDemand(Accounts.class);
|
||||
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
|
||||
PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class);
|
||||
Keys keys = jdbi.onDemand(Keys.class);
|
||||
StoredMessages storedMessages = jdbi.onDemand(StoredMessages.class );
|
||||
|
||||
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
|
||||
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
|
||||
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
|
||||
.build(getName());
|
||||
|
||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
||||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
|
||||
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, memcachedClient);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||
PubSubManager pubSubManager = new PubSubManager(redisClient);
|
||||
StoredMessageManager storedMessageManager = new StoredMessageManager(storedMessages, pubSubManager);
|
||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
||||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
|
||||
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, memcachedClient );
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||
StoredMessages storedMessages = new StoredMessages(redisClient);
|
||||
PubSubManager pubSubManager = new PubSubManager(redisClient);
|
||||
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
|
||||
WebsocketSender websocketSender = new WebsocketSender(storedMessages, pubSubManager);
|
||||
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
||||
|
||||
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
|
||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||
PushSender pushSender = new PushSender(pushServiceClient, websocketSender);
|
||||
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
|
||||
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
|
||||
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
|
||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
|
||||
config.getApnConfiguration(),
|
||||
storedMessageManager,
|
||||
accountsManager);
|
||||
environment.lifecycle().manage(feedbackHandler);
|
||||
|
||||
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);
|
||||
|
||||
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||
FederatedPeer.class,
|
||||
deviceAuthenticator,
|
||||
Device.class, "WhisperServer"));
|
||||
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||
FederatedPeer.class,
|
||||
deviceAuthenticator,
|
||||
Device.class, "WhisperServer"));
|
||||
|
||||
environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender));
|
||||
environment.addResource(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
|
||||
environment.addResource(new DirectoryController(rateLimiters, directory));
|
||||
environment.addResource(new FederationController(accountsManager, attachmentController, keysController, messageController));
|
||||
environment.addResource(attachmentController);
|
||||
environment.addResource(keysController);
|
||||
environment.addResource(messageController);
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, storedMessages, new TimeProvider(), authorizationKey));
|
||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
|
||||
environment.jersey().register(new DirectoryController(rateLimiters, directory));
|
||||
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
|
||||
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
|
||||
environment.jersey().register(new ReceiptController(accountsManager, federatedClientManager, pushSender));
|
||||
environment.jersey().register(attachmentController);
|
||||
environment.jersey().register(keysControllerV1);
|
||||
environment.jersey().register(keysControllerV2);
|
||||
environment.jersey().register(messageController);
|
||||
|
||||
if (config.getWebsocketConfiguration().isEnabled()) {
|
||||
environment.addServlet(new WebsocketControllerFactory(deviceAuthenticator, storedMessageManager, pubSubManager),
|
||||
"/v1/websocket/");
|
||||
environment.addFilter(new CORSHeaderFilter(), "/*");
|
||||
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment);
|
||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
|
||||
webSocketEnvironment.setConnectListener(new ConnectListener(accountsManager, pushSender, storedMessages, pubSubManager));
|
||||
|
||||
WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment);
|
||||
|
||||
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet);
|
||||
websocket.addMapping("/v1/websocket/*");
|
||||
websocket.setAsyncSupported(true);
|
||||
|
||||
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
|
||||
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
|
||||
filter.setInitParameter("allowedOrigins", "*");
|
||||
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin");
|
||||
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
|
||||
filter.setInitParameter("preflightMaxAge", "5184000");
|
||||
filter.setInitParameter("allowCredentials", "true");
|
||||
}
|
||||
|
||||
environment.addHealthCheck(new RedisHealthCheck(redisClient));
|
||||
environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient));
|
||||
environment.healthChecks().register("redis", new RedisHealthCheck(redisClient));
|
||||
environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
|
||||
|
||||
environment.addProvider(new IOExceptionMapper());
|
||||
environment.addProvider(new RateLimitExceededExceptionMapper());
|
||||
environment.jersey().register(new IOExceptionMapper());
|
||||
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
|
||||
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
|
||||
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
|
||||
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
|
||||
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
|
||||
|
||||
if (config.getGraphiteConfiguration().isEnabled()) {
|
||||
GraphiteReporter.enable(15, TimeUnit.SECONDS,
|
||||
config.getGraphiteConfiguration().getHost(),
|
||||
config.getGraphiteConfiguration().getPort());
|
||||
GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory();
|
||||
graphiteReporterFactory.setHost(config.getGraphiteConfiguration().getHost());
|
||||
graphiteReporterFactory.setPort(config.getGraphiteConfiguration().getPort());
|
||||
|
||||
GraphiteReporter graphiteReporter = (GraphiteReporter) graphiteReporterFactory.build(environment.metrics());
|
||||
graphiteReporter.start(15, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
if (config.getDataDogConfiguration().isEnabled()) {
|
||||
new DatadogReporter.Builder().withApiKey(config.getDataDogConfiguration().getApiKey())
|
||||
.withVmMetricsEnabled(true)
|
||||
.build()
|
||||
.start(15, TimeUnit.SECONDS);
|
||||
if (config.getMetricsConfiguration().isEnabled()) {
|
||||
new JsonMetricsReporter(environment.metrics(),
|
||||
config.getMetricsConfiguration().getToken(),
|
||||
config.getMetricsConfiguration().getHost())
|
||||
.start(60, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,29 +16,27 @@
|
||||
*/
|
||||
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.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.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
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> {
|
||||
|
||||
private final Meter authenticationFailedMeter = Metrics.newMeter(AccountAuthenticator.class,
|
||||
"authentication", "failed",
|
||||
TimeUnit.MINUTES);
|
||||
|
||||
private final Meter authenticationSucceededMeter = Metrics.newMeter(AccountAuthenticator.class,
|
||||
"authentication", "succeeded",
|
||||
TimeUnit.MINUTES);
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
|
||||
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded"));
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ import java.io.IOException;
|
||||
public class AuthorizationHeader {
|
||||
|
||||
private final String number;
|
||||
private final long accountId;
|
||||
private final long accountId;
|
||||
private final String password;
|
||||
|
||||
private AuthorizationHeader(String number, long accountId, String password) {
|
||||
this.number = number;
|
||||
this.number = number;
|
||||
this.accountId = accountId;
|
||||
this.password = password;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AuthorizationToken {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
|
||||
|
||||
private final String token;
|
||||
private final byte[] key;
|
||||
|
||||
public AuthorizationToken(String token, byte[] key) {
|
||||
this.token = token;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public boolean isValid(String number, long currentTimeMillis) {
|
||||
String[] parts = token.split(":");
|
||||
|
||||
if (parts.length != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!number.equals(parts[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidTime(parts[1], currentTimeMillis)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isValidSignature(parts[0] + ":" + parts[1], parts[2]);
|
||||
}
|
||||
|
||||
private boolean isValidTime(String timeString, long currentTimeMillis) {
|
||||
try {
|
||||
long tokenTime = Long.parseLong(timeString);
|
||||
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
|
||||
|
||||
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Number Format", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidSignature(String prefix, String suffix) {
|
||||
try {
|
||||
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
|
||||
byte[] ourSuffix = Util.truncate(hmac.doFinal(prefix.getBytes()), 10);
|
||||
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
|
||||
|
||||
return MessageDigest.isEqual(ourSuffix, theirSuffix);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (DecoderException e) {
|
||||
logger.warn("Authorizationtoken", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,30 +16,35 @@
|
||||
*/
|
||||
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.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.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
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> {
|
||||
|
||||
private final Meter authenticationFailedMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
|
||||
"authentication", "failed",
|
||||
TimeUnit.MINUTES);
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
|
||||
private final Meter authenticationSucceededMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
|
||||
"authentication", "succeeded",
|
||||
TimeUnit.MINUTES);
|
||||
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(),
|
||||
"authentication",
|
||||
"failed"));
|
||||
|
||||
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(),
|
||||
"authentication",
|
||||
"succeeded"));
|
||||
|
||||
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.spi.inject.Injectable;
|
||||
import com.sun.jersey.spi.inject.InjectableProvider;
|
||||
import com.yammer.dropwizard.auth.Auth;
|
||||
import com.yammer.dropwizard.auth.Authenticator;
|
||||
import com.yammer.dropwizard.auth.basic.BasicAuthProvider;
|
||||
import com.yammer.dropwizard.auth.basic.BasicCredentials;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicAuthProvider;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MultiBasicAuthProvider.class);
|
||||
|
||||
private final BasicAuthProvider<T1> provider1;
|
||||
private final BasicAuthProvider<T2> provider2;
|
||||
|
||||
@@ -44,8 +41,8 @@ public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, P
|
||||
Class<?> clazz2,
|
||||
String realm)
|
||||
{
|
||||
this.provider1 = new BasicAuthProvider<T1>(authenticator1, realm);
|
||||
this.provider2 = new BasicAuthProvider<T2>(authenticator2, realm);
|
||||
this.provider1 = new BasicAuthProvider<>(authenticator1, realm);
|
||||
this.provider2 = new BasicAuthProvider<>(authenticator2, realm);
|
||||
this.clazz1 = clazz1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,14 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class GcmConfiguration {
|
||||
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private long senderId;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String apiKey;
|
||||
@@ -28,4 +34,8 @@ public class GcmConfiguration {
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public long getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class MetricsConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private String token;
|
||||
|
||||
@JsonProperty
|
||||
private String host;
|
||||
|
||||
@JsonProperty
|
||||
private boolean enabled = false;
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled && token != null && host != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
public class PushConfiguration {
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String host;
|
||||
|
||||
@JsonProperty
|
||||
@Min(1)
|
||||
private int port;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String password;
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.Optional;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
public class RedPhoneConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private String authKey;
|
||||
|
||||
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
|
||||
if (authKey == null || authKey.trim().length() == 0) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,9 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import com.yammer.dropwizard.auth.Auth;
|
||||
import com.yammer.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
@@ -28,15 +27,19 @@ import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
@@ -55,6 +58,8 @@ import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/accounts")
|
||||
public class AccountController {
|
||||
|
||||
@@ -64,16 +69,25 @@ public class AccountController {
|
||||
private final AccountsManager accounts;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
private final StoredMessages storedMessages;
|
||||
private final TimeProvider timeProvider;
|
||||
private final Optional<byte[]> authorizationKey;
|
||||
|
||||
public AccountController(PendingAccountsManager pendingAccounts,
|
||||
AccountsManager accounts,
|
||||
RateLimiters rateLimiters,
|
||||
SmsSender smsSenderFactory)
|
||||
SmsSender smsSenderFactory,
|
||||
StoredMessages storedMessages,
|
||||
TimeProvider timeProvider,
|
||||
Optional<byte[]> authorizationKey)
|
||||
{
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
this.storedMessages = storedMessages;
|
||||
this.timeProvider = timeProvider;
|
||||
this.authorizationKey = authorizationKey;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -135,30 +149,50 @@ public class AccountController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Device device = new Device();
|
||||
device.setId(Device.MASTER_ID);
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
if (accounts.isRelayListed(number)) {
|
||||
throw new WebApplicationException(Response.status(417).build());
|
||||
}
|
||||
|
||||
Account account = new Account();
|
||||
account.setNumber(number);
|
||||
account.setSupportsSms(accountAttributes.getSupportsSms());
|
||||
account.addDevice(device);
|
||||
|
||||
accounts.create(account);
|
||||
|
||||
pendingAccounts.remove(number);
|
||||
|
||||
logger.debug("Stored device...");
|
||||
createAccount(number, password, accountAttributes);
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad Authorization Header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Path("/token/{verification_token}")
|
||||
public void verifyToken(@PathParam("verification_token") String verificationToken,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
try {
|
||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||
String number = header.getNumber();
|
||||
String password = header.getPassword();
|
||||
|
||||
rateLimiters.getVerifyLimiter().validate(number);
|
||||
|
||||
if (!authorizationKey.isPresent()) {
|
||||
logger.debug("Attempt to authorize with key but not configured...");
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
AuthorizationToken token = new AuthorizationToken(verificationToken, authorizationKey.get());
|
||||
|
||||
if (!token.isValid(number, timeProvider.getCurrentTimeMillis())) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
createAccount(number, password, accountAttributes);
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad authorization header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@@ -209,6 +243,26 @@ public class AccountController {
|
||||
encodedVerificationText)).build();
|
||||
}
|
||||
|
||||
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
|
||||
Device device = new Device();
|
||||
device.setId(Device.MASTER_ID);
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
|
||||
Account account = new Account();
|
||||
account.setNumber(number);
|
||||
account.setSupportsSms(accountAttributes.getSupportsSms());
|
||||
account.addDevice(device);
|
||||
|
||||
accounts.create(account);
|
||||
storedMessages.clear(new WebsocketAddress(number, Device.MASTER_ID));
|
||||
pendingAccounts.remove(number);
|
||||
|
||||
logger.debug("Stored device...");
|
||||
}
|
||||
|
||||
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||
try {
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.amazonaws.HttpMethod;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
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.AttachmentDescriptor;
|
||||
@@ -44,6 +43,8 @@ import java.net.URL;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
|
||||
@Path("/v1/attachments")
|
||||
public class AttachmentController {
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import com.yammer.dropwizard.auth.Auth;
|
||||
import com.yammer.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
@@ -48,6 +47,8 @@ import javax.ws.rs.core.Response;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/devices")
|
||||
public class DeviceController {
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
*/
|
||||
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.yammer.dropwizard.auth.Auth;
|
||||
import com.yammer.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
@@ -43,10 +46,15 @@ import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/directory")
|
||||
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 DirectoryManager directory;
|
||||
@@ -56,7 +64,7 @@ public class DirectoryController {
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
@Timed()
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/{token}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -77,7 +85,7 @@ public class DirectoryController {
|
||||
}
|
||||
}
|
||||
|
||||
@Timed()
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/tokens")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -86,6 +94,7 @@ public class DirectoryController {
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size());
|
||||
contactsHistogram.update(contacts.getContacts().size());
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
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.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 {
|
||||
|
||||
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;
|
||||
|
||||
private final AccountsManager accounts;
|
||||
private final AttachmentController attachmentController;
|
||||
private final KeysController keysController;
|
||||
private final MessageController messageController;
|
||||
|
||||
public FederationController(AccountsManager accounts,
|
||||
public FederationController(AccountsManager accounts,
|
||||
AttachmentController attachmentController,
|
||||
KeysController keysController,
|
||||
MessageController messageController)
|
||||
MessageController messageController)
|
||||
{
|
||||
this.accounts = accounts;
|
||||
this.attachmentController = attachmentController;
|
||||
this.keysController = keysController;
|
||||
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
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -16,45 +16,32 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
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.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyList;
|
||||
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
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;
|
||||
|
||||
@Path("/v1/keys")
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
public class KeysController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(KeysController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
protected final RateLimiters rateLimiters;
|
||||
protected final Keys keys;
|
||||
protected final AccountsManager accounts;
|
||||
protected final FederatedClientManager federatedClientManager;
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||
FederatedClientManager federatedClientManager)
|
||||
@@ -65,98 +52,65 @@ public class KeysController {
|
||||
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
|
||||
@GET
|
||||
@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);
|
||||
}
|
||||
public PreKeyCount getStatus(@Auth Account account) {
|
||||
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
|
||||
|
||||
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());
|
||||
if (count > 0) {
|
||||
count = count - 1;
|
||||
}
|
||||
|
||||
return new PreKeyCount(count);
|
||||
}
|
||||
|
||||
@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
|
||||
protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
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);
|
||||
|
||||
if (!destination.isPresent() || !destination.get().isActive()) {
|
||||
return Optional.absent();
|
||||
throw new NoSuchUserException("Target account is inactive");
|
||||
}
|
||||
|
||||
try {
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
Optional<UnstructuredPreKeyList> preKeys = keys.get(number);
|
||||
return getActiveKeys(destination.get(), preKeys);
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
}
|
||||
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
Optional<Device> targetDevice = destination.get().getDevice(deviceId);
|
||||
|
||||
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
|
||||
return Optional.absent();
|
||||
throw new NoSuchUserException("Target device is inactive.");
|
||||
}
|
||||
|
||||
Optional<UnstructuredPreKeyList> preKeys = keys.get(number, deviceId);
|
||||
return getActiveKeys(destination.get(), preKeys);
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
} catch (NumberFormatException e) {
|
||||
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()) {
|
||||
Optional<Device> device = destination.getDevice(preKey.getDeviceId());
|
||||
|
||||
if (device.isPresent() && device.get().isActive()) {
|
||||
preKey.setRegistrationId(device.get().getRegistrationId());
|
||||
filteredKeys.add(preKey);
|
||||
}
|
||||
public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
|
||||
this.destination = destination;
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
if (filteredKeys.isEmpty()) return Optional.absent();
|
||||
else return Optional.of(new UnstructuredPreKeyList(filteredKeys));
|
||||
public Optional<List<KeyRecord>> getKeys() {
|
||||
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;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.protobuf.ByteString;
|
||||
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.IncomingMessage;
|
||||
@@ -56,6 +55,8 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/messages")
|
||||
public class MessageController {
|
||||
|
||||
@@ -141,7 +142,7 @@ public class MessageController {
|
||||
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
|
||||
|
||||
if (destinationDevice.isPresent()) {
|
||||
sendLocalMessage(source, destination, destinationDevice.get(), incomingMessage);
|
||||
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,6 +150,7 @@ public class MessageController {
|
||||
private void sendLocalMessage(Account source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
IncomingMessage incomingMessage)
|
||||
throws NoSuchUserException, IOException
|
||||
{
|
||||
@@ -158,7 +160,7 @@ public class MessageController {
|
||||
|
||||
messageBuilder.setType(incomingMessage.getType())
|
||||
.setSource(source.getNumber())
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MismatchedDevicesException extends Exception {
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,7 +25,7 @@ public class NoSuchUserException extends Exception {
|
||||
|
||||
public NoSuchUserException(String user) {
|
||||
super(user);
|
||||
missing = new LinkedList<String>();
|
||||
missing = new LinkedList<>();
|
||||
missing.add(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
|
||||
@Path("/v1/receipt")
|
||||
public class ReceiptController {
|
||||
|
||||
private final AccountsManager accountManager;
|
||||
private final PushSender pushSender;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
|
||||
public ReceiptController(AccountsManager accountManager,
|
||||
FederatedClientManager federatedClientManager,
|
||||
PushSender pushSender)
|
||||
{
|
||||
this.accountManager = accountManager;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
this.pushSender = pushSender;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/{destination}/{messageId}")
|
||||
public void sendDeliveryReceipt(@Auth Account source,
|
||||
@PathParam("destination") String destination,
|
||||
@PathParam("messageId") long messageId,
|
||||
@QueryParam("relay") Optional<String> relay)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
|
||||
else sendDirectReceipt(source, destination, messageId);
|
||||
} catch (NoSuchUserException | NotPushRegisteredException e) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
} catch (TransientPushFailureException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
|
||||
throws NoSuchUserException, IOException
|
||||
{
|
||||
try {
|
||||
federatedClientManager.getClient(relay)
|
||||
.sendDeliveryReceipt(source.getNumber(),
|
||||
source.getAuthenticatedDevice().get().getId(),
|
||||
destination, messageId);
|
||||
} catch (NoSuchPeerException e) {
|
||||
throw new NoSuchUserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDirectReceipt(Account source, String destination, long messageId)
|
||||
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
|
||||
{
|
||||
Account destinationAccount = getDestinationAccount(destination);
|
||||
List<Device> destinationDevices = destinationAccount.getDevices();
|
||||
|
||||
OutgoingMessageSignal.Builder message =
|
||||
OutgoingMessageSignal.newBuilder()
|
||||
.setSource(source.getNumber())
|
||||
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
|
||||
.setTimestamp(messageId)
|
||||
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
|
||||
|
||||
if (source.getRelay().isPresent()) {
|
||||
message.setRelay(source.getRelay().get());
|
||||
}
|
||||
|
||||
for (Device destinationDevice : destinationDevices) {
|
||||
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
|
||||
}
|
||||
}
|
||||
|
||||
private Account getDestinationAccount(String destination)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
Optional<Account> account = accountManager.get(destination);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
throw new NoSuchUserException(destination);
|
||||
}
|
||||
|
||||
return account.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.eclipse.jetty.websocket.WebSocket;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingWebsocketMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubListener;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WebsocketController implements WebSocket.OnTextMessage, PubSubListener {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
private static final Map<Long, String> pendingMessages = new HashMap<>();
|
||||
|
||||
private final StoredMessageManager storedMessageManager;
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
private final Account account;
|
||||
private final Device device;
|
||||
|
||||
private Connection connection;
|
||||
private long pendingMessageSequence;
|
||||
|
||||
public WebsocketController(StoredMessageManager storedMessageManager,
|
||||
PubSubManager pubSubManager,
|
||||
Account account)
|
||||
{
|
||||
this.storedMessageManager = storedMessageManager;
|
||||
this.pubSubManager = pubSubManager;
|
||||
this.account = account;
|
||||
this.device = account.getAuthenticatedDevice().get();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onOpen(Connection connection) {
|
||||
this.connection = connection;
|
||||
pubSubManager.subscribe(new WebsocketAddress(this.account.getId(), this.device.getId()), this);
|
||||
handleQueryDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(int i, String s) {
|
||||
handleClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(String body) {
|
||||
try {
|
||||
IncomingWebsocketMessage incomingMessage = mapper.readValue(body, IncomingWebsocketMessage.class);
|
||||
|
||||
switch (incomingMessage.getType()) {
|
||||
case IncomingWebsocketMessage.TYPE_ACKNOWLEDGE_MESSAGE: handleMessageAck(body); break;
|
||||
case IncomingWebsocketMessage.TYPE_PING_MESSAGE: handlePing(); break;
|
||||
default: handleClose(); break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("Parse", e);
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPubSubMessage(PubSubMessage outgoingMessage) {
|
||||
switch (outgoingMessage.getType()) {
|
||||
case PubSubMessage.TYPE_DELIVER: handleDeliverOutgoingMessage(outgoingMessage.getContents()); break;
|
||||
case PubSubMessage.TYPE_QUERY_DB: handleQueryDatabase(); break;
|
||||
default:
|
||||
logger.warn("Unknown pubsub message: " + outgoingMessage.getType());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeliverOutgoingMessage(String message) {
|
||||
try {
|
||||
long messageSequence;
|
||||
|
||||
synchronized (pendingMessages) {
|
||||
messageSequence = pendingMessageSequence++;
|
||||
pendingMessages.put(messageSequence, message);
|
||||
}
|
||||
|
||||
connection.sendMessage(mapper.writeValueAsString(new WebsocketMessage(messageSequence, message)));
|
||||
} catch (IOException e) {
|
||||
logger.debug("Response failed", e);
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessageAck(String message) {
|
||||
try {
|
||||
AcknowledgeWebsocketMessage ack = mapper.readValue(message, AcknowledgeWebsocketMessage.class);
|
||||
|
||||
synchronized (pendingMessages) {
|
||||
pendingMessages.remove(ack.getId());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Mapping", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePing() {
|
||||
try {
|
||||
IncomingWebsocketMessage pongMessage = new IncomingWebsocketMessage(IncomingWebsocketMessage.TYPE_PONG_MESSAGE);
|
||||
connection.sendMessage(mapper.writeValueAsString(pongMessage));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Pong failed", e);
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClose() {
|
||||
pubSubManager.unsubscribe(new WebsocketAddress(account.getId(), device.getId()), this);
|
||||
connection.close();
|
||||
|
||||
List<String> remainingMessages = new LinkedList<>();
|
||||
|
||||
synchronized (pendingMessages) {
|
||||
Long[] pendingKeys = pendingMessages.keySet().toArray(new Long[0]);
|
||||
Arrays.sort(pendingKeys);
|
||||
|
||||
for (long pendingKey : pendingKeys) {
|
||||
remainingMessages.add(pendingMessages.get(pendingKey));
|
||||
}
|
||||
|
||||
pendingMessages.clear();
|
||||
}
|
||||
|
||||
storedMessageManager.storeMessages(account, device, remainingMessages);
|
||||
}
|
||||
|
||||
private void handleQueryDatabase() {
|
||||
List<String> messages = storedMessageManager.getOutgoingMessages(account, device);
|
||||
|
||||
for (String message : messages) {
|
||||
handleDeliverOutgoingMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
public class ApnMessage {
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String apnId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
@Min(1)
|
||||
private int deviceId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String message;
|
||||
|
||||
public ApnMessage() {}
|
||||
|
||||
public ApnMessage(String apnId, String number, int deviceId, String message) {
|
||||
this.apnId = apnId;
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,11 @@ public class ClientContactTokens {
|
||||
public List<String> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public ClientContactTokens() {}
|
||||
|
||||
public ClientContactTokens(List<String> contacts) {
|
||||
this.contacts = contacts;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,23 +41,22 @@ public class EncryptedOutgoingMessage {
|
||||
private static final int MAC_KEY_SIZE = 20;
|
||||
private static final int MAC_SIZE = 10;
|
||||
|
||||
private final OutgoingMessageSignal outgoingMessage;
|
||||
private final String signalingKey;
|
||||
private final String serialized;
|
||||
|
||||
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
|
||||
String signalingKey)
|
||||
throws CryptoEncodingException
|
||||
{
|
||||
this.outgoingMessage = outgoingMessage;
|
||||
this.signalingKey = signalingKey;
|
||||
}
|
||||
|
||||
public String serialize() throws CryptoEncodingException {
|
||||
byte[] plaintext = outgoingMessage.toByteArray();
|
||||
SecretKeySpec cipherKey = getCipherKey (signalingKey);
|
||||
SecretKeySpec macKey = getMacKey(signalingKey);
|
||||
byte[] ciphertext = getCiphertext(plaintext, cipherKey, macKey);
|
||||
|
||||
return Base64.encodeBytes(ciphertext);
|
||||
this.serialized = Base64.encodeBytes(ciphertext);
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
public class GcmMessage {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String gcmId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
@Min(1)
|
||||
private int deviceId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String message;
|
||||
|
||||
@JsonProperty
|
||||
private boolean receipt;
|
||||
|
||||
public GcmMessage() {}
|
||||
|
||||
public GcmMessage(String gcmId, String number, int deviceId, String message, boolean receipt) {
|
||||
this.gcmId = gcmId;
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.message = message;
|
||||
this.receipt = receipt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class IncomingMessage {
|
||||
private String relay;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
private long timestamp; // deprecated
|
||||
|
||||
|
||||
public String getDestination() {
|
||||
|
||||
@@ -32,6 +32,9 @@ public class IncomingMessageList {
|
||||
@JsonProperty
|
||||
private String relay;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
public IncomingMessageList() {}
|
||||
|
||||
public List<IncomingMessage> getMessages() {
|
||||
@@ -45,4 +48,8 @@ public class IncomingMessageList {
|
||||
public void setRelay(String relay) {
|
||||
this.relay = relay;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PendingMessage {
|
||||
|
||||
@JsonProperty
|
||||
private String sender;
|
||||
|
||||
@JsonProperty
|
||||
private long messageId;
|
||||
|
||||
@JsonProperty
|
||||
private String encryptedOutgoingMessage;
|
||||
|
||||
@JsonProperty
|
||||
private boolean receipt;
|
||||
|
||||
public PendingMessage() {}
|
||||
|
||||
public PendingMessage(String sender, long messageId, boolean receipt, String encryptedOutgoingMessage) {
|
||||
this.sender = sender;
|
||||
this.messageId = messageId;
|
||||
this.receipt = receipt;
|
||||
this.encryptedOutgoingMessage = encryptedOutgoingMessage;
|
||||
}
|
||||
|
||||
public String getEncryptedOutgoingMessage() {
|
||||
return encryptedOutgoingMessage;
|
||||
}
|
||||
|
||||
public long getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public String getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public boolean isReceipt() {
|
||||
return receipt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof PendingMessage)) return false;
|
||||
PendingMessage that = (PendingMessage)other;
|
||||
|
||||
return
|
||||
this.sender.equals(that.sender) &&
|
||||
this.messageId == that.messageId &&
|
||||
this.receipt == that.receipt &&
|
||||
this.encryptedOutgoingMessage.equals(that.encryptedOutgoingMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.sender.hashCode() ^ (int)this.messageId ^ this.encryptedOutgoingMessage.hashCode() ^ (receipt ? 1 : 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
public interface PreKeyBase {
|
||||
|
||||
public long getKeyId();
|
||||
public String getPublicKey();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PreKeyCount {
|
||||
|
||||
@JsonProperty
|
||||
private int count;
|
||||
|
||||
public PreKeyCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public PreKeyCount() {}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -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.google.common.annotations.VisibleForTesting;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
@@ -26,36 +25,36 @@ import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class UnstructuredPreKeyList {
|
||||
public class PreKeyResponseV1 {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private List<PreKey> keys;
|
||||
private List<PreKeyV1> keys;
|
||||
|
||||
@VisibleForTesting
|
||||
public UnstructuredPreKeyList() {}
|
||||
public PreKeyResponseV1() {}
|
||||
|
||||
public UnstructuredPreKeyList(PreKey preKey) {
|
||||
this.keys = new LinkedList<PreKey>();
|
||||
public PreKeyResponseV1(PreKeyV1 preKey) {
|
||||
this.keys = new LinkedList<>();
|
||||
this.keys.add(preKey);
|
||||
}
|
||||
|
||||
public UnstructuredPreKeyList(List<PreKey> preKeys) {
|
||||
public PreKeyResponseV1(List<PreKeyV1> preKeys) {
|
||||
this.keys = preKeys;
|
||||
}
|
||||
|
||||
public List<PreKey> getKeys() {
|
||||
public List<PreKeyV1> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof UnstructuredPreKeyList) ||
|
||||
((UnstructuredPreKeyList) o).keys.size() != keys.size())
|
||||
if (!(o instanceof PreKeyResponseV1) ||
|
||||
((PreKeyResponseV1) o).keys.size() != keys.size())
|
||||
return false;
|
||||
Iterator<PreKey> otherKeys = ((UnstructuredPreKeyList) o).keys.iterator();
|
||||
for (PreKey key : keys) {
|
||||
Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
|
||||
for (PreKeyV1 key : keys) {
|
||||
if (!otherKeys.next().equals(key))
|
||||
return false;
|
||||
}
|
||||
@@ -64,7 +63,7 @@ public class UnstructuredPreKeyList {
|
||||
|
||||
public int hashCode() {
|
||||
int ret = 0xFBA4C795 * keys.size();
|
||||
for (PreKey key : keys)
|
||||
for (PreKeyV1 key : keys)
|
||||
ret ^= key.getPublicKey().hashCode();
|
||||
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
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,28 +17,39 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
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.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyList {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private PreKey lastResortKey;
|
||||
public class PreKeyStateV1 {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@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;
|
||||
}
|
||||
|
||||
public PreKey getLastResortKey() {
|
||||
@VisibleForTesting
|
||||
public void setKeys(List<PreKeyV1> keys) {
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
public PreKeyV1 getLastResortKey() {
|
||||
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
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,22 +17,14 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.io.Serializable;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
public class PreKey {
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
|
||||
@JsonIgnore
|
||||
private String number;
|
||||
public class PreKeyV1 implements PreKeyBase {
|
||||
|
||||
@JsonProperty
|
||||
private long deviceId;
|
||||
@@ -49,78 +41,43 @@ public class PreKey {
|
||||
@NotNull
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private boolean lastResort;
|
||||
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
public PreKey() {}
|
||||
public PreKeyV1() {}
|
||||
|
||||
public PreKey(long id, String number, long deviceId, long keyId,
|
||||
String publicKey, String identityKey,
|
||||
boolean lastResort)
|
||||
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
|
||||
{
|
||||
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.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
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() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public void setPublicKey(String publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public void setKeyId(long keyId) {
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
public String getIdentityKey() {
|
||||
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) {
|
||||
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 {
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
public class UnregisteredEvent {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String registrationId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
@Min(1)
|
||||
private int deviceId;
|
||||
|
||||
public String getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class UnregisteredEventList {
|
||||
|
||||
@JsonProperty
|
||||
private List<UnregisteredEvent> devices;
|
||||
|
||||
public List<UnregisteredEvent> getDevices() {
|
||||
if (devices == null) return new LinkedList<>();
|
||||
else return devices;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,8 @@ 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.UnstructuredPreKeyList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
@@ -62,11 +63,13 @@ public class FederatedClient {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
|
||||
|
||||
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 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 ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
|
||||
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 RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
|
||||
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
|
||||
private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
|
||||
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
|
||||
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
|
||||
|
||||
private final FederatedPeer peer;
|
||||
private final Client client;
|
||||
@@ -107,9 +110,9 @@ public class FederatedClient {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<UnstructuredPreKeyList> getKeys(String destination, String device) {
|
||||
public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
|
||||
try {
|
||||
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)
|
||||
.header("Authorization", authorizationHeader)
|
||||
@@ -119,7 +122,7 @@ public class FederatedClient {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return Optional.of(response.getEntity(UnstructuredPreKeyList.class));
|
||||
return Optional.of(response.getEntity(PreKeyResponseV1.class));
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("PreKey", e);
|
||||
@@ -127,6 +130,27 @@ public class FederatedClient {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device));
|
||||
|
||||
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return Optional.of(response.getEntity(PreKeyResponseV2.class));
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("PreKey", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getUserCount() {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
|
||||
@@ -174,6 +198,25 @@ public class FederatedClient {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
String path = String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId);
|
||||
WebResource resource = client.resource(peer.getUrl()).path(path);
|
||||
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.put(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("sendMessage", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
|
||||
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
|
||||
}
|
||||
|
||||
@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
|
||||
|
||||
@Override
|
||||
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;
|
||||
|
||||
import com.yammer.metrics.Metrics;
|
||||
import com.yammer.metrics.core.Meter;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import net.spy.memcached.MemcachedClient;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -35,7 +36,9 @@ public class RateLimiter {
|
||||
public RateLimiter(MemcachedClient memcachedClient, String name,
|
||||
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.name = name;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.codahale.metrics.Counter;
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.Metered;
|
||||
import com.codahale.metrics.MetricFilter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.ScheduledReporter;
|
||||
import com.codahale.metrics.Snapshot;
|
||||
import com.codahale.metrics.Timer;
|
||||
import com.fasterxml.jackson.core.JsonEncoding;
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Adapted from MetricsServlet.
|
||||
*/
|
||||
public class JsonMetricsReporter extends ScheduledReporter {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(JsonMetricsReporter.class);
|
||||
private final JsonFactory factory = new JsonFactory();
|
||||
|
||||
private final String table;
|
||||
private final String sunnylabsHost;
|
||||
private final String host;
|
||||
|
||||
public JsonMetricsReporter(MetricRegistry registry, String token, String sunnylabsHost)
|
||||
throws UnknownHostException
|
||||
{
|
||||
super(registry, "jsonmetrics-reporter", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.MILLISECONDS);
|
||||
this.table = token;
|
||||
this.sunnylabsHost = sunnylabsHost;
|
||||
this.host = InetAddress.getLocalHost().getHostName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void report(SortedMap<String, Gauge> stringGaugeSortedMap,
|
||||
SortedMap<String, Counter> stringCounterSortedMap,
|
||||
SortedMap<String, Histogram> stringHistogramSortedMap,
|
||||
SortedMap<String, Meter> stringMeterSortedMap,
|
||||
SortedMap<String, Timer> stringTimerSortedMap)
|
||||
{
|
||||
try {
|
||||
logger.info("Reporting metrics...");
|
||||
URL url = new URL("https", sunnylabsHost, 443, "/report/metrics?t=" + table + "&h=" + host);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
||||
connection.setDoOutput(true);
|
||||
connection.addRequestProperty("Content-Type", "application/json");
|
||||
|
||||
OutputStream outputStream = connection.getOutputStream();
|
||||
JsonGenerator json = factory.createGenerator(outputStream, JsonEncoding.UTF8);
|
||||
|
||||
json.writeStartObject();
|
||||
|
||||
for (Map.Entry<String, Gauge> gauge : stringGaugeSortedMap.entrySet()) {
|
||||
reportGauge(json, gauge.getKey(), gauge.getValue());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Counter> counter : stringCounterSortedMap.entrySet()) {
|
||||
reportCounter(json, counter.getKey(), counter.getValue());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Histogram> histogram : stringHistogramSortedMap.entrySet()) {
|
||||
reportHistogram(json, histogram.getKey(), histogram.getValue());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Meter> meter : stringMeterSortedMap.entrySet()) {
|
||||
reportMeter(json, meter.getKey(), meter.getValue());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Timer> timer : stringTimerSortedMap.entrySet()) {
|
||||
reportTimer(json, timer.getKey(), timer.getValue());
|
||||
}
|
||||
|
||||
json.writeEndObject();
|
||||
json.close();
|
||||
|
||||
outputStream.close();
|
||||
|
||||
logger.info("Metrics server response: " + connection.getResponseCode());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error sending metrics", e);
|
||||
} catch (Exception e) {
|
||||
logger.warn("error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportGauge(JsonGenerator json, String name, Gauge gauge) throws IOException {
|
||||
Object gaugeValue = evaluateGauge(gauge);
|
||||
|
||||
if (gaugeValue instanceof Number) {
|
||||
json.writeFieldName(sanitize(name));
|
||||
json.writeObject(gaugeValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportCounter(JsonGenerator json, String name, Counter counter) throws IOException {
|
||||
json.writeFieldName(sanitize(name));
|
||||
json.writeNumber(counter.getCount());
|
||||
}
|
||||
|
||||
private void reportHistogram(JsonGenerator json, String name, Histogram histogram) throws IOException {
|
||||
Snapshot snapshot = histogram.getSnapshot();
|
||||
json.writeFieldName(sanitize(name));
|
||||
json.writeStartObject();
|
||||
json.writeNumberField("count", histogram.getCount());
|
||||
writeSnapshot(json, snapshot);
|
||||
json.writeEndObject();
|
||||
}
|
||||
|
||||
private void reportMeter(JsonGenerator json, String name, Meter meter) throws IOException {
|
||||
json.writeFieldName(sanitize(name));
|
||||
json.writeStartObject();
|
||||
writeMetered(json, meter);
|
||||
json.writeEndObject();
|
||||
}
|
||||
|
||||
private void reportTimer(JsonGenerator json, String name, Timer timer) throws IOException {
|
||||
json.writeFieldName(sanitize(name));
|
||||
json.writeStartObject();
|
||||
json.writeFieldName("rate");
|
||||
json.writeStartObject();
|
||||
writeMetered(json, timer);
|
||||
json.writeEndObject();
|
||||
json.writeFieldName("duration");
|
||||
json.writeStartObject();
|
||||
writeSnapshot(json, timer.getSnapshot());
|
||||
json.writeEndObject();
|
||||
json.writeEndObject();
|
||||
}
|
||||
|
||||
private Object evaluateGauge(Gauge gauge) {
|
||||
try {
|
||||
return gauge.getValue();
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Error reading gauge", e);
|
||||
return "error reading gauge";
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSnapshot(JsonGenerator json, Snapshot snapshot) throws IOException {
|
||||
json.writeNumberField("max", convertDuration(snapshot.getMax()));
|
||||
json.writeNumberField("mean", convertDuration(snapshot.getMean()));
|
||||
json.writeNumberField("min", convertDuration(snapshot.getMin()));
|
||||
json.writeNumberField("stddev", convertDuration(snapshot.getStdDev()));
|
||||
json.writeNumberField("median", convertDuration(snapshot.getMedian()));
|
||||
json.writeNumberField("p75", convertDuration(snapshot.get75thPercentile()));
|
||||
json.writeNumberField("p95", convertDuration(snapshot.get95thPercentile()));
|
||||
json.writeNumberField("p98", convertDuration(snapshot.get98thPercentile()));
|
||||
json.writeNumberField("p99", convertDuration(snapshot.get99thPercentile()));
|
||||
json.writeNumberField("p999", convertDuration(snapshot.get999thPercentile()));
|
||||
}
|
||||
|
||||
private void writeMetered(JsonGenerator json, Metered meter) throws IOException {
|
||||
json.writeNumberField("count", convertRate(meter.getCount()));
|
||||
json.writeNumberField("mean", convertRate(meter.getMeanRate()));
|
||||
json.writeNumberField("m1", convertRate(meter.getOneMinuteRate()));
|
||||
json.writeNumberField("m5", convertRate(meter.getFiveMinuteRate()));
|
||||
json.writeNumberField("m15", convertRate(meter.getFifteenMinuteRate()));
|
||||
}
|
||||
|
||||
private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
|
||||
|
||||
private String sanitize(String metricName) {
|
||||
return SIMPLE_NAMES.matcher(metricName).replaceAll("_");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import com.yammer.metrics.core.HealthCheck;
|
||||
import com.yammer.metrics.core.HealthCheck.Result;
|
||||
import com.codahale.metrics.health.HealthCheck;
|
||||
import net.spy.memcached.MemcachedClient;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
@@ -27,7 +26,6 @@ public class MemcacheHealthCheck extends HealthCheck {
|
||||
private final MemcachedClient client;
|
||||
|
||||
public MemcacheHealthCheck(MemcachedClient client) {
|
||||
super("memcached");
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@@ -47,6 +45,8 @@ public class MemcacheHealthCheck extends HealthCheck {
|
||||
return Result.unhealthy("Fetch failed");
|
||||
}
|
||||
|
||||
this.client.delete("HEALTH" + random);
|
||||
|
||||
return Result.healthy();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
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.JedisPool;
|
||||
@@ -26,7 +26,6 @@ public class RedisHealthCheck extends HealthCheck {
|
||||
private final JedisPool clientPool;
|
||||
|
||||
public RedisHealthCheck(JedisPool clientPool) {
|
||||
super("redis");
|
||||
this.clientPool = clientPool;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.textsecuregcm.providers;
|
||||
|
||||
public class TimeProvider {
|
||||
public long getCurrentTimeMillis() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.notnoop.apns.APNS;
|
||||
import com.notnoop.apns.ApnsService;
|
||||
import com.notnoop.exceptions.NetworkIOException;
|
||||
import com.yammer.metrics.Metrics;
|
||||
import com.yammer.metrics.core.Meter;
|
||||
import org.bouncycastle.openssl.PEMReader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class APNSender {
|
||||
|
||||
private final Meter success = Metrics.newMeter(APNSender.class, "sent", "success", TimeUnit.MINUTES);
|
||||
private final Meter failure = Metrics.newMeter(APNSender.class, "sent", "failure", TimeUnit.MINUTES);
|
||||
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
|
||||
|
||||
private static final String MESSAGE_BODY = "m";
|
||||
|
||||
private final Optional<ApnsService> apnService;
|
||||
|
||||
public APNSender(String apnCertificate, String apnKey)
|
||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
||||
{
|
||||
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
|
||||
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
|
||||
this.apnService = Optional.of(APNS.newService()
|
||||
.withCert(new ByteArrayInputStream(keyStore), "insecure")
|
||||
.withSandboxDestination().build());
|
||||
} else {
|
||||
this.apnService = Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(String registrationId, EncryptedOutgoingMessage message)
|
||||
throws TransientPushFailureException, NotPushRegisteredException
|
||||
{
|
||||
try {
|
||||
if (!apnService.isPresent()) {
|
||||
failure.mark();
|
||||
throw new TransientPushFailureException("APN access not configured!");
|
||||
}
|
||||
|
||||
String payload = APNS.newPayload()
|
||||
.alertBody("Message!")
|
||||
.customField(MESSAGE_BODY, message.serialize())
|
||||
.build();
|
||||
|
||||
logger.debug("APN Payload: " + payload);
|
||||
|
||||
apnService.get().push(registrationId, payload);
|
||||
success.mark();
|
||||
} catch (NetworkIOException nioe) {
|
||||
logger.warn("Network Error", nioe);
|
||||
failure.mark();
|
||||
throw new TransientPushFailureException(nioe);
|
||||
} catch (CryptoEncodingException e) {
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
|
||||
throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException
|
||||
{
|
||||
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
|
||||
X509Certificate certificate = (X509Certificate) reader.readObject();
|
||||
Certificate[] certificateChain = {certificate};
|
||||
|
||||
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes())));
|
||||
KeyPair keyPair = (KeyPair) reader.readObject();
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("pkcs12");
|
||||
keyStore.load(null);
|
||||
keyStore.setEntry("apn",
|
||||
new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), certificateChain),
|
||||
new KeyStore.PasswordProtection("insecure".toCharArray()));
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
keyStore.store(baos, "insecure".toCharArray());
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
|
||||
public class FeedbackHandler implements Managed, Runnable {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PushServiceClient.class);
|
||||
|
||||
private final PushServiceClient client;
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
private ScheduledExecutorService executor;
|
||||
|
||||
public FeedbackHandler(PushServiceClient client, AccountsManager accountsManager) {
|
||||
this.client = client;
|
||||
this.accountsManager = accountsManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
this.executor = Executors.newSingleThreadScheduledExecutor();
|
||||
this.executor.scheduleAtFixedRate(this, 0, 10, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
if (this.executor != null) {
|
||||
this.executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
List<UnregisteredEvent> gcmFeedback = client.getGcmFeedback();
|
||||
List<UnregisteredEvent> apnFeedback = client.getApnFeedback();
|
||||
|
||||
for (UnregisteredEvent gcmEvent : gcmFeedback) {
|
||||
handleGcmUnregistered(gcmEvent);
|
||||
}
|
||||
|
||||
for (UnregisteredEvent apnEvent : apnFeedback) {
|
||||
handleApnUnregistered(apnEvent);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error retrieving feedback: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGcmUnregistered(UnregisteredEvent event) {
|
||||
logger.warn("Got GCM Unregistered: " + event.getNumber() + "," + event.getDeviceId());
|
||||
|
||||
Optional<Account> account = accountsManager.get(event.getNumber());
|
||||
|
||||
if (account.isPresent()) {
|
||||
Optional<Device> device = account.get().getDevice(event.getDeviceId());
|
||||
|
||||
if (device.isPresent()) {
|
||||
if (event.getRegistrationId().equals(device.get().getGcmId())) {
|
||||
logger.warn("GCM Unregister GCM ID matches!");
|
||||
device.get().setGcmId(null);
|
||||
accountsManager.update(account.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleApnUnregistered(UnregisteredEvent event) {
|
||||
logger.warn("Got APN Unregistered: " + event.getNumber() + "," + event.getDeviceId());
|
||||
|
||||
Optional<Account> account = accountsManager.get(event.getNumber());
|
||||
|
||||
if (account.isPresent()) {
|
||||
Optional<Device> device = account.get().getDevice(event.getDeviceId());
|
||||
|
||||
if (device.isPresent()) {
|
||||
if (event.getRegistrationId().equals(device.get().getApnId())) {
|
||||
logger.warn("APN Unregister APN ID matches!");
|
||||
device.get().setApnId(null);
|
||||
accountsManager.update(account.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.google.android.gcm.server.Constants;
|
||||
import com.google.android.gcm.server.Message;
|
||||
import com.google.android.gcm.server.Result;
|
||||
import com.google.android.gcm.server.Sender;
|
||||
import com.yammer.metrics.Metrics;
|
||||
import com.yammer.metrics.core.Meter;
|
||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class GCMSender {
|
||||
|
||||
private final Meter success = Metrics.newMeter(GCMSender.class, "sent", "success", TimeUnit.MINUTES);
|
||||
private final Meter failure = Metrics.newMeter(GCMSender.class, "sent", "failure", TimeUnit.MINUTES);
|
||||
|
||||
private final Sender sender;
|
||||
|
||||
public GCMSender(String apiKey) {
|
||||
this.sender = new Sender(apiKey);
|
||||
}
|
||||
|
||||
public String sendMessage(String gcmRegistrationId, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
try {
|
||||
Message gcmMessage = new Message.Builder().addData("type", "message")
|
||||
.addData("message", outgoingMessage.serialize())
|
||||
.build();
|
||||
|
||||
Result result = sender.send(gcmMessage, gcmRegistrationId, 5);
|
||||
|
||||
if (result.getMessageId() != null) {
|
||||
success.mark();
|
||||
return result.getCanonicalRegistrationId();
|
||||
} else {
|
||||
failure.mark();
|
||||
if (result.getErrorCodeName().equals(Constants.ERROR_NOT_REGISTERED)) {
|
||||
throw new NotPushRegisteredException("Device no longer registered with GCM.");
|
||||
} else {
|
||||
throw new TransientPushFailureException("GCM Failed: " + result.getErrorCodeName());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new TransientPushFailureException(e);
|
||||
} catch (CryptoEncodingException e) {
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,92 +18,84 @@ package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.PendingMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessageManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
|
||||
public class PushSender {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
|
||||
|
||||
private final AccountsManager accounts;
|
||||
private final GCMSender gcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final StoredMessageManager storedMessageManager;
|
||||
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"},\"content-available\":1,\"category\":\"Signal_Message\"}}";
|
||||
|
||||
public PushSender(GcmConfiguration gcmConfiguration,
|
||||
ApnConfiguration apnConfiguration,
|
||||
StoredMessageManager storedMessageManager,
|
||||
AccountsManager accounts)
|
||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
||||
{
|
||||
this.accounts = accounts;
|
||||
this.storedMessageManager = storedMessageManager;
|
||||
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
|
||||
this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
|
||||
private final PushServiceClient pushServiceClient;
|
||||
private final WebsocketSender webSocketSender;
|
||||
|
||||
public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
|
||||
this.pushServiceClient = pushServiceClient;
|
||||
this.webSocketSender = websocketSender;
|
||||
}
|
||||
|
||||
public void sendMessage(Account account, Device device, MessageProtos.OutgoingMessageSignal outgoingMessage)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
String signalingKey = device.getSignalingKey();
|
||||
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
|
||||
|
||||
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
|
||||
else if (device.getApnId() != null) sendApnMessage(account, device, message);
|
||||
else if (device.getFetchesMessages()) storeFetchedMessage(account, device, message);
|
||||
else throw new NotPushRegisteredException("No delivery possible!");
|
||||
}
|
||||
|
||||
private void sendGcmMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||
public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
try {
|
||||
String canonicalId = gcmSender.sendMessage(device.getGcmId(), outgoingMessage);
|
||||
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
|
||||
String signalingKey = device.getSignalingKey();
|
||||
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, signalingKey);
|
||||
PendingMessage pendingMessage = new PendingMessage(message.getSource(),
|
||||
message.getTimestamp(),
|
||||
isReceipt,
|
||||
encryptedMessage.serialize());
|
||||
|
||||
if (canonicalId != null) {
|
||||
device.setGcmId(canonicalId);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
} catch (NotPushRegisteredException e) {
|
||||
logger.debug("No Such User", e);
|
||||
device.setGcmId(null);
|
||||
accounts.update(account);
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendApnMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws TransientPushFailureException, NotPushRegisteredException
|
||||
{
|
||||
try {
|
||||
apnSender.sendMessage(device.getApnId(), outgoingMessage);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
device.setApnId(null);
|
||||
accounts.update(account);
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeFetchedMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws NotPushRegisteredException
|
||||
{
|
||||
try {
|
||||
storedMessageManager.storeMessage(account, device, outgoingMessage);
|
||||
sendMessage(account, device, pendingMessage);
|
||||
} catch (CryptoEncodingException e) {
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(Account account, Device device, PendingMessage pendingMessage)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
if (device.getGcmId() != null) sendGcmMessage(account, device, pendingMessage);
|
||||
else if (device.getApnId() != null) sendApnMessage(account, device, pendingMessage);
|
||||
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, pendingMessage);
|
||||
else throw new NotPushRegisteredException("No delivery possible!");
|
||||
}
|
||||
|
||||
private void sendGcmMessage(Account account, Device device, PendingMessage pendingMessage)
|
||||
throws TransientPushFailureException
|
||||
{
|
||||
String number = account.getNumber();
|
||||
long deviceId = device.getId();
|
||||
String registrationId = device.getGcmId();
|
||||
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int)deviceId,
|
||||
pendingMessage.getEncryptedOutgoingMessage(),
|
||||
pendingMessage.isReceipt() );
|
||||
|
||||
pushServiceClient.send(gcmMessage);
|
||||
}
|
||||
|
||||
private void sendApnMessage(Account account, Device device, PendingMessage outgoingMessage)
|
||||
throws TransientPushFailureException
|
||||
{
|
||||
boolean online = webSocketSender.sendMessage(account, device, outgoingMessage, true);
|
||||
|
||||
if (!online && !outgoingMessage.isReceipt()) {
|
||||
ApnMessage apnMessage = new ApnMessage(device.getApnId(), account.getNumber(),
|
||||
(int)device.getId(), APN_PAYLOAD);
|
||||
pushServiceClient.send(apnMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendWebSocketMessage(Account account, Device device, PendingMessage outgoingMessage)
|
||||
{
|
||||
webSocketSender.sendMessage(account, device, outgoingMessage, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.sun.jersey.api.client.Client;
|
||||
import com.sun.jersey.api.client.ClientHandlerException;
|
||||
import com.sun.jersey.api.client.ClientResponse;
|
||||
import com.sun.jersey.api.client.UniformInterfaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
|
||||
import org.whispersystems.textsecuregcm.entities.UnregisteredEventList;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class PushServiceClient {
|
||||
|
||||
private static final String PUSH_GCM_PATH = "/api/v1/push/gcm";
|
||||
private static final String PUSH_APN_PATH = "/api/v1/push/apn";
|
||||
|
||||
private static final String APN_FEEDBACK_PATH = "/api/v1/feedback/apn";
|
||||
private static final String GCM_FEEDBACK_PATH = "/api/v1/feedback/gcm";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PushServiceClient.class);
|
||||
|
||||
private final Client client;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String authorization;
|
||||
|
||||
public PushServiceClient(Client client, PushConfiguration config) {
|
||||
this.client = client;
|
||||
this.host = config.getHost();
|
||||
this.port = config.getPort();
|
||||
this.authorization = getAuthorizationHeader(config.getUsername(), config.getPassword());
|
||||
}
|
||||
|
||||
public void send(GcmMessage message) throws TransientPushFailureException {
|
||||
sendPush(PUSH_GCM_PATH, message);
|
||||
}
|
||||
|
||||
public void send(ApnMessage message) throws TransientPushFailureException {
|
||||
sendPush(PUSH_APN_PATH, message);
|
||||
}
|
||||
|
||||
public List<UnregisteredEvent> getGcmFeedback() throws IOException {
|
||||
return getFeedback(GCM_FEEDBACK_PATH);
|
||||
}
|
||||
|
||||
public List<UnregisteredEvent> getApnFeedback() throws IOException {
|
||||
return getFeedback(APN_FEEDBACK_PATH);
|
||||
}
|
||||
|
||||
private void sendPush(String path, Object entity) throws TransientPushFailureException {
|
||||
try {
|
||||
ClientResponse response = client.resource("http://" + host + ":" + port + path)
|
||||
.header("Authorization", authorization)
|
||||
.entity(entity, MediaType.APPLICATION_JSON)
|
||||
.put(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() != 204 && response.getStatus() != 200) {
|
||||
logger.warn("PushServer response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase());
|
||||
throw new TransientPushFailureException("Bad response: " + response.getStatus());
|
||||
}
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("Push error: ", e);
|
||||
throw new TransientPushFailureException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<UnregisteredEvent> getFeedback(String path) throws IOException {
|
||||
try {
|
||||
UnregisteredEventList unregisteredEvents = client.resource("http://" + host + ":" + port + path)
|
||||
.header("Authorization", authorization)
|
||||
.get(UnregisteredEventList.class);
|
||||
|
||||
return unregisteredEvents.getDevices();
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("Request error:", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getAuthorizationHeader(String username, String password) {
|
||||
return "Basic " + Base64.encodeBytes((username + ":" + password).getBytes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.PendingMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class WebsocketSender {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class);
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
|
||||
private final Meter websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" ));
|
||||
private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" ));
|
||||
|
||||
private final Meter apnOnlineMeter = metricRegistry.meter(name(getClass(), "apn_online" ));
|
||||
private final Meter apnOfflineMeter = metricRegistry.meter(name(getClass(), "apn_offline"));
|
||||
|
||||
private static final ObjectMapper mapper = SystemMapper.getMapper();
|
||||
|
||||
private final StoredMessages storedMessages;
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
public WebsocketSender(StoredMessages storedMessages, PubSubManager pubSubManager) {
|
||||
this.storedMessages = storedMessages;
|
||||
this.pubSubManager = pubSubManager;
|
||||
}
|
||||
|
||||
public boolean sendMessage(Account account, Device device, PendingMessage pendingMessage, boolean apn) {
|
||||
try {
|
||||
String serialized = mapper.writeValueAsString(pendingMessage);
|
||||
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||
PubSubMessage pubSubMessage = new PubSubMessage(PubSubMessage.TYPE_DELIVER, serialized);
|
||||
|
||||
if (pubSubManager.publish(address, pubSubMessage)) {
|
||||
if (apn) apnOnlineMeter.mark();
|
||||
else websocketOnlineMeter.mark();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (apn) apnOfflineMeter.mark();
|
||||
else websocketOfflineMeter.mark();
|
||||
|
||||
storedMessages.insert(address, pendingMessage);
|
||||
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null));
|
||||
return false;
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("WebsocketSender", "Unable to serialize json", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,13 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.sms;
|
||||
|
||||
import com.yammer.metrics.Metrics;
|
||||
import com.yammer.metrics.core.Meter;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@@ -28,13 +30,15 @@ import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class NexmoSmsSender {
|
||||
|
||||
private final Meter smsMeter = Metrics.newMeter(NexmoSmsSender.class, "sms", "delivered", TimeUnit.MINUTES);
|
||||
private final Meter voxMeter = Metrics.newMeter(NexmoSmsSender.class, "vox", "delivered", TimeUnit.MINUTES);
|
||||
private final Logger logger = LoggerFactory.getLogger(NexmoSmsSender.class);
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
|
||||
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
|
||||
private final Logger logger = LoggerFactory.getLogger(NexmoSmsSender.class);
|
||||
|
||||
private static final String NEXMO_SMS_URL =
|
||||
"https://rest.nexmo.com/sms/json?api_key=%s&api_secret=%s&from=%s&to=%s&text=%s";
|
||||
|
||||
@@ -16,22 +16,25 @@
|
||||
*/
|
||||
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.TwilioRestException;
|
||||
import com.twilio.sdk.resource.factory.CallFactory;
|
||||
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.message.BasicNameValuePair;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class TwilioSmsSender {
|
||||
|
||||
@@ -40,8 +43,9 @@ public class TwilioSmsSender {
|
||||
" <Say voice=\"woman\" language=\"en\">" + SmsSender.VOX_VERIFICATION_TEXT + "%s</Say>\n" +
|
||||
"</Response>";
|
||||
|
||||
private final Meter smsMeter = Metrics.newMeter(TwilioSmsSender.class, "sms", "delivered", TimeUnit.MINUTES);
|
||||
private final Meter voxMeter = Metrics.newMeter(TwilioSmsSender.class, "vox", "delivered", TimeUnit.MINUTES);
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
|
||||
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
|
||||
|
||||
private final String accountId;
|
||||
private final String accountToken;
|
||||
|
||||
@@ -22,16 +22,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class Account implements Serializable {
|
||||
public class Account {
|
||||
|
||||
public static final int MEMCACHE_VERION = 2;
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
public static final int MEMCACHE_VERION = 5;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
@@ -42,16 +38,14 @@ public class Account implements Serializable {
|
||||
@JsonProperty
|
||||
private List<Device> devices = new LinkedList<>();
|
||||
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonIgnore
|
||||
private Optional<Device> authenticatedDevice;
|
||||
|
||||
public Account() {}
|
||||
|
||||
public Account(String number, boolean supportsSms) {
|
||||
this.number = number;
|
||||
this.supportsSms = supportsSms;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account(String number, boolean supportsSms, List<Device> devices) {
|
||||
this.number = number;
|
||||
@@ -59,14 +53,6 @@ public class Account implements Serializable {
|
||||
this.devices = devices;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Optional<Device> getAuthenticatedDevice() {
|
||||
return authenticatedDevice;
|
||||
}
|
||||
@@ -142,4 +128,12 @@ public class Account implements Serializable {
|
||||
public Optional<String> getRelay() {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public void setIdentityKey(String identityKey) {
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public String getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
|
||||
import org.skife.jdbi.v2.sqlobject.Transaction;
|
||||
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
||||
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
@@ -51,12 +52,7 @@ public abstract class Accounts {
|
||||
private static final String NUMBER = "number";
|
||||
private static final String DATA = "data";
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
}
|
||||
private static final ObjectMapper mapper = SystemMapper.getMapper();
|
||||
|
||||
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DATA + ") VALUES (:number, CAST(:data AS json))")
|
||||
@GetGeneratedKeys
|
||||
@@ -89,6 +85,9 @@ public abstract class Accounts {
|
||||
return insertStep(account);
|
||||
}
|
||||
|
||||
@SqlUpdate("VACUUM accounts")
|
||||
public abstract void vacuum();
|
||||
|
||||
public static class AccountMapper implements ResultSetMapper<Account> {
|
||||
@Override
|
||||
public Account map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||
@@ -96,7 +95,7 @@ public abstract class Accounts {
|
||||
{
|
||||
try {
|
||||
Account account = mapper.readValue(resultSet.getString(DATA), Account.class);
|
||||
account.setId(resultSet.getLong(ID));
|
||||
// account.setId(resultSet.getLong(ID));
|
||||
|
||||
return account;
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -17,19 +17,30 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import net.spy.memcached.MemcachedClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class AccountsManager {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
|
||||
|
||||
private final Accounts accounts;
|
||||
private final MemcachedClient memcachedClient;
|
||||
private final DirectoryManager directory;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public AccountsManager(Accounts accounts,
|
||||
DirectoryManager directory,
|
||||
@@ -38,6 +49,7 @@ public class AccountsManager {
|
||||
this.accounts = accounts;
|
||||
this.directory = directory;
|
||||
this.memcachedClient = memcachedClient;
|
||||
this.mapper = SystemMapper.getMapper();
|
||||
}
|
||||
|
||||
public long getCount() {
|
||||
@@ -54,40 +66,35 @@ public class AccountsManager {
|
||||
|
||||
public void create(Account account) {
|
||||
accounts.create(account);
|
||||
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(getKey(account.getNumber()), 0, account);
|
||||
}
|
||||
|
||||
memcacheSet(account.getNumber(), account);
|
||||
updateDirectory(account);
|
||||
}
|
||||
|
||||
public void update(Account account) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(getKey(account.getNumber()), 0, account);
|
||||
}
|
||||
|
||||
memcacheSet(account.getNumber(), account);
|
||||
accounts.update(account);
|
||||
updateDirectory(account);
|
||||
}
|
||||
|
||||
public Optional<Account> get(String number) {
|
||||
Account account = null;
|
||||
Optional<Account> account = memcacheGet(number);
|
||||
|
||||
if (memcachedClient != null) {
|
||||
account = (Account)memcachedClient.get(getKey(number));
|
||||
}
|
||||
if (!account.isPresent()) {
|
||||
account = Optional.fromNullable(accounts.get(number));
|
||||
|
||||
if (account == null) {
|
||||
account = accounts.get(number);
|
||||
|
||||
if (account != null && memcachedClient != null) {
|
||||
memcachedClient.set(getKey(number), 0, account);
|
||||
if (account.isPresent()) {
|
||||
memcacheSet(number, account.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (account != null) return Optional.of(account);
|
||||
else return Optional.absent();
|
||||
return account;
|
||||
}
|
||||
|
||||
public boolean isRelayListed(String number) {
|
||||
byte[] token = Util.getContactToken(number);
|
||||
Optional<ClientContact> contact = directory.get(token);
|
||||
|
||||
return contact.isPresent() && !Util.isEmpty(contact.get().getRelay());
|
||||
}
|
||||
|
||||
private void updateDirectory(Account account) {
|
||||
@@ -104,4 +111,30 @@ public class AccountsManager {
|
||||
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
|
||||
}
|
||||
|
||||
private void memcacheSet(String number, Account account) {
|
||||
if (memcachedClient != null) {
|
||||
try {
|
||||
String json = mapper.writeValueAsString(account);
|
||||
memcachedClient.set(getKey(number), 0, json);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> memcacheGet(String number) {
|
||||
if (memcachedClient == null) return Optional.absent();
|
||||
|
||||
try {
|
||||
String json = (String)memcachedClient.get(getKey(number));
|
||||
|
||||
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
|
||||
else return Optional.absent();
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("AccountsManager", "Deserialization error", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,11 +19,12 @@ package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Device implements Serializable {
|
||||
public class Device {
|
||||
|
||||
public static final long MASTER_ID = 1;
|
||||
|
||||
@@ -51,11 +52,15 @@ public class Device implements Serializable {
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
@JsonProperty
|
||||
private SignedPreKey signedPreKey;
|
||||
|
||||
public Device() {}
|
||||
|
||||
public Device(long id, String authToken, String salt,
|
||||
String signalingKey, String gcmId, String apnId,
|
||||
boolean fetchesMessages, int registrationId)
|
||||
boolean fetchesMessages, int registrationId,
|
||||
SignedPreKey signedPreKey)
|
||||
{
|
||||
this.id = id;
|
||||
this.authToken = authToken;
|
||||
@@ -65,6 +70,7 @@ public class Device implements Serializable {
|
||||
this.apnId = apnId;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.signedPreKey = signedPreKey;
|
||||
}
|
||||
|
||||
public String getApnId() {
|
||||
@@ -131,4 +137,12 @@ public class Device implements Serializable {
|
||||
public void setRegistrationId(int 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());
|
||||
}
|
||||
|
||||
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) {
|
||||
Jedis jedis = redisPool.getResource();
|
||||
|
||||
@@ -162,4 +167,26 @@ public class DirectoryManager {
|
||||
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,7 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
|
||||
import org.skife.jdbi.v2.sqlobject.Transaction;
|
||||
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
||||
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyBase;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
@@ -51,66 +50,71 @@ public abstract class Keys {
|
||||
@SqlUpdate("DELETE FROM keys WHERE id = :id")
|
||||
abstract void removeKey(@Bind("id") long id);
|
||||
|
||||
@SqlBatch("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 List<PreKey> 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);
|
||||
@SqlBatch("INSERT INTO keys (number, device_id, key_id, public_key, last_resort) VALUES " +
|
||||
"(:number, :device_id, :key_id, :public_key, :last_resort)")
|
||||
abstract void append(@PreKeyBinder List<KeyRecord> preKeys);
|
||||
|
||||
@SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE")
|
||||
@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")
|
||||
@Mapper(PreKeyMapper.class)
|
||||
abstract List<PreKey> retrieveFirst(@Bind("number") String number);
|
||||
abstract List<KeyRecord> retrieveFirst(@Bind("number") String number);
|
||||
|
||||
@SqlQuery("SELECT COUNT(*) FROM keys WHERE number = :number AND device_id = :device_id")
|
||||
public abstract int getCount(@Bind("number") String number, @Bind("device_id") long deviceId);
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public void store(String number, long deviceId, List<PreKey> keys, PreKey lastResortKey) {
|
||||
for (PreKey key : keys) {
|
||||
key.setNumber(number);
|
||||
key.setDeviceId(deviceId);
|
||||
public void store(String number, long deviceId, List<? extends PreKeyBase> keys, PreKeyBase lastResortKey) {
|
||||
List<KeyRecord> records = new LinkedList<>();
|
||||
|
||||
for (PreKeyBase key : keys) {
|
||||
records.add(new KeyRecord(0, number, deviceId, key.getKeyId(), key.getPublicKey(), false));
|
||||
}
|
||||
|
||||
lastResortKey.setNumber(number);
|
||||
lastResortKey.setDeviceId(deviceId);
|
||||
lastResortKey.setLastResort(true);
|
||||
records.add(new KeyRecord(0, number, deviceId, lastResortKey.getKeyId(),
|
||||
lastResortKey.getPublicKey(), true));
|
||||
|
||||
removeKeys(number, deviceId);
|
||||
append(keys);
|
||||
append(lastResortKey);
|
||||
append(records);
|
||||
}
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public Optional<UnstructuredPreKeyList> get(String number, long deviceId) {
|
||||
PreKey preKey = retrieveFirst(number, deviceId);
|
||||
public Optional<List<KeyRecord>> get(String number, long deviceId) {
|
||||
final KeyRecord record = retrieveFirst(number, deviceId);
|
||||
|
||||
if (preKey != null && !preKey.isLastResort()) {
|
||||
removeKey(preKey.getId());
|
||||
if (record != null && !record.isLastResort()) {
|
||||
removeKey(record.getId());
|
||||
} else if (record == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
if (preKey != null) return Optional.of(new UnstructuredPreKeyList(preKey));
|
||||
else return Optional.absent();
|
||||
List<KeyRecord> results = new LinkedList<>();
|
||||
results.add(record);
|
||||
|
||||
return Optional.of(results);
|
||||
}
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public Optional<UnstructuredPreKeyList> get(String number) {
|
||||
List<PreKey> preKeys = retrieveFirst(number);
|
||||
public Optional<List<KeyRecord>> get(String number) {
|
||||
List<KeyRecord> preKeys = retrieveFirst(number);
|
||||
|
||||
if (preKeys != null) {
|
||||
for (PreKey preKey : preKeys) {
|
||||
for (KeyRecord preKey : preKeys) {
|
||||
if (!preKey.isLastResort()) {
|
||||
removeKey(preKey.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preKeys != null) return Optional.of(new UnstructuredPreKeyList(preKeys));
|
||||
if (preKeys != null) return Optional.of(preKeys);
|
||||
else return Optional.absent();
|
||||
}
|
||||
|
||||
@SqlUpdate("VACUUM keys")
|
||||
public abstract void vacuum();
|
||||
|
||||
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.PARAMETER})
|
||||
@@ -118,17 +122,16 @@ public abstract class Keys {
|
||||
public static class PreKeyBinderFactory implements BinderFactory {
|
||||
@Override
|
||||
public Binder build(Annotation annotation) {
|
||||
return new Binder<PreKeyBinder, PreKey>() {
|
||||
return new Binder<PreKeyBinder, KeyRecord>() {
|
||||
@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("number", preKey.getNumber());
|
||||
sql.bind("device_id", preKey.getDeviceId());
|
||||
sql.bind("key_id", preKey.getKeyId());
|
||||
sql.bind("public_key", preKey.getPublicKey());
|
||||
sql.bind("identity_key", preKey.getIdentityKey());
|
||||
sql.bind("last_resort", preKey.isLastResort() ? 1 : 0);
|
||||
sql.bind("id", record.getId());
|
||||
sql.bind("number", record.getNumber());
|
||||
sql.bind("device_id", record.getDeviceId());
|
||||
sql.bind("key_id", record.getKeyId());
|
||||
sql.bind("public_key", record.getPublicKey());
|
||||
sql.bind("last_resort", record.isLastResort() ? 1 : 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -136,15 +139,14 @@ public abstract class Keys {
|
||||
}
|
||||
|
||||
|
||||
public static class PreKeyMapper implements ResultSetMapper<PreKey> {
|
||||
public static class PreKeyMapper implements ResultSetMapper<KeyRecord> {
|
||||
@Override
|
||||
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||
public KeyRecord map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||
throws SQLException
|
||||
{
|
||||
return new PreKey(resultSet.getLong("id"), resultSet.getString("number"), resultSet.getLong("device_id"),
|
||||
resultSet.getLong("key_id"), resultSet.getString("public_key"),
|
||||
resultSet.getString("identity_key"),
|
||||
resultSet.getInt("last_resort") == 1);
|
||||
return new KeyRecord(resultSet.getLong("id"), resultSet.getString("number"),
|
||||
resultSet.getLong("device_id"), resultSet.getLong("key_id"),
|
||||
resultSet.getString("public_key"), resultSet.getInt("last_resort") == 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,4 +31,7 @@ public interface PendingAccounts {
|
||||
|
||||
@SqlUpdate("DELETE FROM pending_accounts WHERE number = :number")
|
||||
void remove(@Bind("number") String number);
|
||||
|
||||
@SqlUpdate("VACUUM pending_accounts")
|
||||
public void vacuum();
|
||||
}
|
||||
|
||||
@@ -34,35 +34,46 @@ public class PendingAccountsManager {
|
||||
}
|
||||
|
||||
public void store(String number, String code) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
}
|
||||
|
||||
memcacheSet(number, code);
|
||||
pendingAccounts.insert(number, code);
|
||||
}
|
||||
|
||||
public void remove(String number) {
|
||||
if (memcachedClient != null)
|
||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
||||
memcacheDelete(number);
|
||||
pendingAccounts.remove(number);
|
||||
}
|
||||
|
||||
public Optional<String> getCodeForNumber(String number) {
|
||||
String code = null;
|
||||
Optional<String> code = memcacheGet(number);
|
||||
|
||||
if (memcachedClient != null) {
|
||||
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
|
||||
}
|
||||
if (!code.isPresent()) {
|
||||
code = Optional.fromNullable(pendingAccounts.getCodeForNumber(number));
|
||||
|
||||
if (code == null) {
|
||||
code = pendingAccounts.getCodeForNumber(number);
|
||||
|
||||
if (code != null && memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
if (code.isPresent()) {
|
||||
memcacheSet(number, code.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (code != null) return Optional.of(code);
|
||||
else return Optional.absent();
|
||||
return code;
|
||||
}
|
||||
|
||||
private void memcacheSet(String number, String code) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> memcacheGet(String number) {
|
||||
if (memcachedClient != null) {
|
||||
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
private void memcacheDelete(String number) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,37 +34,47 @@ public class PendingDevicesManager {
|
||||
}
|
||||
|
||||
public void store(String number, String code) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
}
|
||||
|
||||
memcacheSet(number, code);
|
||||
pendingDevices.insert(number, code);
|
||||
}
|
||||
|
||||
public void remove(String number) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
||||
}
|
||||
|
||||
memcacheDelete(number);
|
||||
pendingDevices.remove(number);
|
||||
}
|
||||
|
||||
public Optional<String> getCodeForNumber(String number) {
|
||||
String code = null;
|
||||
Optional<String> code = memcacheGet(number);
|
||||
|
||||
if (memcachedClient != null) {
|
||||
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
|
||||
}
|
||||
if (!code.isPresent()) {
|
||||
code = Optional.fromNullable(pendingDevices.getCodeForNumber(number));
|
||||
|
||||
if (code == null) {
|
||||
code = pendingDevices.getCodeForNumber(number);
|
||||
|
||||
if (code != null && memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
if (code.isPresent()) {
|
||||
memcacheSet(number, code.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (code != null) return Optional.of(code);
|
||||
else return Optional.absent();
|
||||
return code;
|
||||
}
|
||||
|
||||
private void memcacheSet(String number, String code) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> memcacheGet(String number) {
|
||||
if (memcachedClient != null) {
|
||||
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
private void memcacheDelete(String number) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
@@ -17,10 +18,12 @@ import redis.clients.jedis.JedisPubSub;
|
||||
|
||||
public class PubSubManager {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final SubscriptionListener baseListener = new SubscriptionListener();
|
||||
private final Map<WebsocketAddress, PubSubListener> listeners = new HashMap<>();
|
||||
private static final String KEEPALIVE_CHANNEL = "KEEPALIVE";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
|
||||
private final ObjectMapper mapper = SystemMapper.getMapper();
|
||||
private final SubscriptionListener baseListener = new SubscriptionListener();
|
||||
private final Map<String, PubSubListener> listeners = new HashMap<>();
|
||||
|
||||
private final JedisPool jedisPool;
|
||||
private boolean subscribed = false;
|
||||
@@ -32,25 +35,29 @@ public class PubSubManager {
|
||||
}
|
||||
|
||||
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
|
||||
listeners.put(address, listener);
|
||||
baseListener.subscribe(address.toString());
|
||||
listeners.put(address.serialize(), listener);
|
||||
baseListener.subscribe(address.serialize());
|
||||
}
|
||||
|
||||
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
|
||||
if (listeners.get(address) == listener) {
|
||||
listeners.remove(address);
|
||||
baseListener.unsubscribe(address.toString());
|
||||
if (listeners.get(address.serialize()) == listener) {
|
||||
listeners.remove(address.serialize());
|
||||
baseListener.unsubscribe(address.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
|
||||
return publish(address.serialize(), message);
|
||||
}
|
||||
|
||||
private synchronized boolean publish(String channel, PubSubMessage message) {
|
||||
try {
|
||||
String serialized = mapper.writeValueAsString(message);
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
return jedis.publish(address.toString(), serialized) != 0;
|
||||
return jedis.publish(channel, serialized) != 0;
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
@@ -78,7 +85,7 @@ public class PubSubManager {
|
||||
Jedis jedis = null;
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
jedis.subscribe(baseListener, new WebsocketAddress(0, 0).toString());
|
||||
jedis.subscribe(baseListener, KEEPALIVE_CHANNEL);
|
||||
logger.warn("**** Unsubscribed from holding channel!!! ******");
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
@@ -94,7 +101,7 @@ public class PubSubManager {
|
||||
for (;;) {
|
||||
try {
|
||||
Thread.sleep(20000);
|
||||
publish(new WebsocketAddress(0, 0), new PubSubMessage(0, "foo"));
|
||||
publish(KEEPALIVE_CHANNEL, new PubSubMessage(0, "foo"));
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
@@ -108,18 +115,15 @@ public class PubSubManager {
|
||||
@Override
|
||||
public void onMessage(String channel, String message) {
|
||||
try {
|
||||
WebsocketAddress address = new WebsocketAddress(channel);
|
||||
PubSubListener listener;
|
||||
|
||||
synchronized (PubSubManager.this) {
|
||||
listener = listeners.get(address);
|
||||
listener = listeners.get(channel);
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onPubSubMessage(mapper.readValue(message, PubSubMessage.class));
|
||||
}
|
||||
} catch (InvalidWebsocketAddressException e) {
|
||||
logger.warn("Address", e);
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOE", e);
|
||||
}
|
||||
@@ -132,16 +136,11 @@ public class PubSubManager {
|
||||
|
||||
@Override
|
||||
public void onSubscribe(String channel, int count) {
|
||||
try {
|
||||
WebsocketAddress address = new WebsocketAddress(channel);
|
||||
if (address.getAccountId() == 0 && address.getDeviceId() == 0) {
|
||||
synchronized (PubSubManager.this) {
|
||||
subscribed = true;
|
||||
PubSubManager.this.notifyAll();
|
||||
}
|
||||
if (KEEPALIVE_CHANNEL.equals(channel)) {
|
||||
synchronized (PubSubManager.this) {
|
||||
subscribed = true;
|
||||
PubSubManager.this.notifyAll();
|
||||
}
|
||||
} catch (InvalidWebsocketAddressException e) {
|
||||
logger.warn("Weird address", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,103 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import org.skife.jdbi.v2.sqlobject.Bind;
|
||||
import org.skife.jdbi.v2.sqlobject.SqlBatch;
|
||||
import org.skife.jdbi.v2.sqlobject.SqlQuery;
|
||||
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.PendingMessage;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
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)")
|
||||
void insert(@Bind("account_id") long accountId, @Bind("device_id") long deviceId, @Bind("encrypted_message") String encryptedOutgoingMessage);
|
||||
public class StoredMessages {
|
||||
|
||||
@SqlBatch("INSERT INTO messages (account_id, device_id, encrypted_message) VALUES (:account_id, :device_id, :encrypted_message)")
|
||||
void insert(@Bind("account_id") long accountId, @Bind("device_id") long deviceId, @Bind("encrypted_message") List<String> encryptedOutgoingMessages);
|
||||
private static final Logger logger = LoggerFactory.getLogger(StoredMessages.class);
|
||||
|
||||
@SqlQuery("DELETE FROM messages WHERE account_id = :account_id AND device_id = :device_id RETURNING encrypted_message")
|
||||
List<String> getMessagesForDevice(@Bind("account_id") long accountId, @Bind("device_id") long deviceId);
|
||||
}
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
|
||||
|
||||
|
||||
private static final ObjectMapper mapper = SystemMapper.getMapper();
|
||||
private static final String QUEUE_PREFIX = "msgs";
|
||||
|
||||
private final JedisPool jedisPool;
|
||||
|
||||
public StoredMessages(JedisPool jedisPool) {
|
||||
this.jedisPool = jedisPool;
|
||||
}
|
||||
|
||||
public void clear(WebsocketAddress address) {
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
jedis.del(getKey(address));
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
}
|
||||
|
||||
public void insert(WebsocketAddress address, PendingMessage message) {
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
|
||||
String serializedMessage = mapper.writeValueAsString(message);
|
||||
long queueSize = jedis.lpush(getKey(address), serializedMessage);
|
||||
queueSizeHistogram.update(queueSize);
|
||||
|
||||
if (queueSize > 1000) {
|
||||
jedis.ltrim(getKey(address), 0, 999);
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("StoredMessages", "Unable to store correctly", e);
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
}
|
||||
|
||||
public List<PendingMessage> getMessagesForDevice(WebsocketAddress address) {
|
||||
List<PendingMessage> messages = new LinkedList<>();
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
String message;
|
||||
|
||||
while ((message = jedis.rpop(getKey(address))) != null) {
|
||||
try {
|
||||
messages.add(mapper.readValue(message, PendingMessage.class));
|
||||
} catch (IOException e) {
|
||||
logger.warn("StoredMessages", "Not a valid PendingMessage", e);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
}
|
||||
|
||||
private String getKey(WebsocketAddress address) {
|
||||
return QUEUE_PREFIX + ":" + address.serialize();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1241,7 +1241,7 @@ public class Base64
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] decode( String s ) throws java.io.IOException {
|
||||
return decode( s, NO_OPTIONS );
|
||||
return decode( s, DONT_GUNZIP );
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
public class CORSHeaderFilter implements Filter {
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
if (response instanceof HttpServletResponse) {
|
||||
((HttpServletResponse) response).addHeader("Access-Control-Allow-Origin", "*");
|
||||
((HttpServletResponse) response).addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
|
||||
((HttpServletResponse) response).addHeader("Access-Control-Allow-Headers", "Authorization, Content-type");
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override public void init(FilterConfig filterConfig) throws ServletException { }
|
||||
@Override public void destroy() { }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
public class Constants {
|
||||
|
||||
public static final String METRICS_NAME = "textsecure";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class SystemMapper {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
}
|
||||
|
||||
public static ObjectMapper getMapper() {
|
||||
return mapper;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,4 +83,40 @@ public class Util {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
|
||||
byte[][] parts = new byte[2][];
|
||||
|
||||
parts[0] = new byte[firstLength];
|
||||
System.arraycopy(input, 0, parts[0], 0, firstLength);
|
||||
|
||||
parts[1] = new byte[secondLength];
|
||||
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength, int fourthLength) {
|
||||
byte[][] parts = new byte[4][];
|
||||
|
||||
parts[0] = new byte[firstLength];
|
||||
System.arraycopy(input, 0, parts[0], 0, firstLength);
|
||||
|
||||
parts[1] = new byte[secondLength];
|
||||
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
|
||||
|
||||
parts[2] = new byte[thirdLength];
|
||||
System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength);
|
||||
|
||||
parts[3] = new byte[fourthLength];
|
||||
System.arraycopy(input, firstLength + secondLength + thirdLength, parts[3], 0, fourthLength);
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public static void sleep(int i) {
|
||||
try {
|
||||
Thread.sleep(i);
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
||||
import org.whispersystems.websocket.setup.WebSocketConnectListener;
|
||||
|
||||
public class ConnectListener implements WebSocketConnectListener {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final PushSender pushSender;
|
||||
private final StoredMessages storedMessages;
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
public ConnectListener(AccountsManager accountsManager, PushSender pushSender,
|
||||
StoredMessages storedMessages, PubSubManager pubSubManager)
|
||||
{
|
||||
this.accountsManager = accountsManager;
|
||||
this.pushSender = pushSender;
|
||||
this.storedMessages = storedMessages;
|
||||
this.pubSubManager = pubSubManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(WebSocketSessionContext context) {
|
||||
Optional<Account> account = context.getAuthenticated(Account.class);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
logger.debug("WS Connection with no authentication...");
|
||||
context.getClient().close(4001, "Authentication failed");
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Device> device = account.get().getAuthenticatedDevice();
|
||||
|
||||
if (!device.isPresent()) {
|
||||
logger.debug("WS Connection with no authenticated device...");
|
||||
context.getClient().close(4001, "Device authentication failed");
|
||||
return;
|
||||
}
|
||||
|
||||
final WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender,
|
||||
storedMessages, pubSubManager,
|
||||
account.get(), device.get(),
|
||||
context.getClient());
|
||||
|
||||
connection.onConnected();
|
||||
|
||||
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
|
||||
@Override
|
||||
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
|
||||
connection.onConnectionLost();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.websocket.auth.AuthenticationException;
|
||||
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
|
||||
public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Account> {
|
||||
|
||||
private final AccountAuthenticator accountAuthenticator;
|
||||
|
||||
public WebSocketAccountAuthenticator(AccountAuthenticator accountAuthenticator) {
|
||||
this.accountAuthenticator = accountAuthenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Account> authenticate(UpgradeRequest request) throws AuthenticationException {
|
||||
try {
|
||||
Map<String, String[]> parameters = request.getParameterMap();
|
||||
String[] usernames = parameters.get("login");
|
||||
String[] passwords = parameters.get("password");
|
||||
|
||||
if (usernames == null || usernames.length == 0 ||
|
||||
passwords == null || passwords.length == 0)
|
||||
{
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
BasicCredentials credentials = new BasicCredentials(usernames[0], passwords[0]);
|
||||
return accountAuthenticator.authenticate(credentials);
|
||||
} catch (io.dropwizard.auth.AuthenticationException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.PendingMessage;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubListener;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.websocket.WebSocketClient;
|
||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
|
||||
public class WebSocketConnection implements PubSubListener {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||
private static final ObjectMapper objectMapper = SystemMapper.getMapper();
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final PushSender pushSender;
|
||||
private final StoredMessages storedMessages;
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
private final Account account;
|
||||
private final Device device;
|
||||
private final WebsocketAddress address;
|
||||
private final WebSocketClient client;
|
||||
|
||||
public WebSocketConnection(AccountsManager accountsManager,
|
||||
PushSender pushSender,
|
||||
StoredMessages storedMessages,
|
||||
PubSubManager pubSubManager,
|
||||
Account account,
|
||||
Device device,
|
||||
WebSocketClient client)
|
||||
{
|
||||
this.accountsManager = accountsManager;
|
||||
this.pushSender = pushSender;
|
||||
this.storedMessages = storedMessages;
|
||||
this.pubSubManager = pubSubManager;
|
||||
this.account = account;
|
||||
this.device = device;
|
||||
this.client = client;
|
||||
this.address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||
}
|
||||
|
||||
public void onConnected() {
|
||||
pubSubManager.subscribe(address, this);
|
||||
processStoredMessages();
|
||||
}
|
||||
|
||||
public void onConnectionLost() {
|
||||
pubSubManager.unsubscribe(address, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPubSubMessage(PubSubMessage message) {
|
||||
try {
|
||||
switch (message.getType()) {
|
||||
case PubSubMessage.TYPE_QUERY_DB:
|
||||
processStoredMessages();
|
||||
break;
|
||||
case PubSubMessage.TYPE_DELIVER:
|
||||
PendingMessage pendingMessage = objectMapper.readValue(message.getContents(),
|
||||
PendingMessage.class);
|
||||
sendMessage(pendingMessage);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Unknown pubsub message: " + message.getType());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error deserializing PendingMessage", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(final PendingMessage message) {
|
||||
String content = message.getEncryptedOutgoingMessage();
|
||||
Optional<byte[]> body = Optional.fromNullable(content.getBytes());
|
||||
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/api/v1/message", body);
|
||||
|
||||
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable WebSocketResponseMessage response) {
|
||||
if (isSuccessResponse(response) && !message.isReceipt()) {
|
||||
sendDeliveryReceiptFor(message);
|
||||
} else if (!isSuccessResponse(response)) {
|
||||
requeueMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@Nonnull Throwable throwable) {
|
||||
requeueMessage(message);
|
||||
}
|
||||
|
||||
private boolean isSuccessResponse(WebSocketResponseMessage response) {
|
||||
return response != null && response.getStatus() >= 200 && response.getStatus() < 300;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void requeueMessage(PendingMessage message) {
|
||||
try {
|
||||
pushSender.sendMessage(account, device, message);
|
||||
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
||||
logger.warn("requeueMessage", e);
|
||||
storedMessages.insert(address, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDeliveryReceiptFor(PendingMessage message) {
|
||||
try {
|
||||
Optional<Account> source = accountsManager.get(message.getSender());
|
||||
|
||||
if (!source.isPresent()) {
|
||||
logger.warn("Source account disappeared? (%s)", message.getSender());
|
||||
return;
|
||||
}
|
||||
|
||||
OutgoingMessageSignal.Builder receipt =
|
||||
OutgoingMessageSignal.newBuilder()
|
||||
.setSource(account.getNumber())
|
||||
.setSourceDevice((int) device.getId())
|
||||
.setTimestamp(message.getMessageId())
|
||||
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
|
||||
|
||||
for (Device device : source.get().getDevices()) {
|
||||
pushSender.sendMessage(source.get(), device, receipt.build());
|
||||
}
|
||||
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
||||
logger.warn("sendDeliveryReceiptFor", "Delivery receipet", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processStoredMessages() {
|
||||
List<PendingMessage> messages = storedMessages.getMessagesForDevice(address);
|
||||
|
||||
for (PendingMessage message : messages) {
|
||||
sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,20 @@ package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
public class WebsocketAddress {
|
||||
|
||||
private final long accountId;
|
||||
private final long deviceId;
|
||||
private final String number;
|
||||
private final long deviceId;
|
||||
|
||||
public WebsocketAddress(String serialized) throws InvalidWebsocketAddressException {
|
||||
try {
|
||||
String[] parts = serialized.split(":");
|
||||
|
||||
if (parts == null || parts.length != 2) {
|
||||
throw new InvalidWebsocketAddressException(serialized);
|
||||
}
|
||||
|
||||
this.accountId = Long.parseLong(parts[0]);
|
||||
this.deviceId = Long.parseLong(parts[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new InvalidWebsocketAddressException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WebsocketAddress(long accountId, long deviceId) {
|
||||
this.accountId = accountId;
|
||||
public WebsocketAddress(String number, long deviceId) {
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public long getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
public String serialize() {
|
||||
return number + ":" + deviceId;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return accountId + ":" + deviceId;
|
||||
return serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,13 +26,13 @@ public class WebsocketAddress {
|
||||
WebsocketAddress that = (WebsocketAddress)other;
|
||||
|
||||
return
|
||||
this.accountId == that.accountId &&
|
||||
this.number.equals(that.number) &&
|
||||
this.deviceId == that.deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int)accountId ^ (int)deviceId;
|
||||
return number.hashCode() ^ (int)deviceId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class WebsocketMessage {
|
||||
|
||||
@JsonProperty
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private String message;
|
||||
|
||||
public WebsocketMessage(long id, String message) {
|
||||
this.id = id;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,13 +16,6 @@
|
||||
*/
|
||||
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.spy.memcached.MemcachedClient;
|
||||
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.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;
|
||||
|
||||
public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfiguration> {
|
||||
@@ -53,8 +53,8 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
|
||||
throws Exception
|
||||
{
|
||||
try {
|
||||
DatabaseConfiguration dbConfig = config.getDatabaseConfiguration();
|
||||
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
|
||||
DataSourceFactory dbConfig = config.getDataSourceFactory();
|
||||
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
|
||||
|
||||
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
|
||||
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.BatchOperationHandle;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.storage.DirectoryManager.PendingClientContact;
|
||||
|
||||
public class DirectoryUpdater {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class);
|
||||
@@ -82,54 +84,69 @@ public class DirectoryUpdater {
|
||||
|
||||
public void updateFromPeers() {
|
||||
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) {
|
||||
logger.info("Updating directory from peer: " + client.getPeerName());
|
||||
// BatchOperationHandle handle = directory.startBatchOperation();
|
||||
|
||||
try {
|
||||
int userCount = client.getUserCount();
|
||||
int retrieved = 0;
|
||||
int userCount = client.getUserCount();
|
||||
int retrieved = 0;
|
||||
|
||||
logger.info("Remote peer user count: " + userCount);
|
||||
logger.info("Remote peer user count: " + userCount);
|
||||
|
||||
while (retrieved < userCount) {
|
||||
logger.info("Retrieving remote tokens...");
|
||||
List<ClientContact> clientContacts = client.getUserTokens(retrieved);
|
||||
while (retrieved < userCount) {
|
||||
logger.info("Retrieving remote tokens...");
|
||||
List<ClientContact> remoteContacts = client.getUserTokens(retrieved);
|
||||
List<PendingClientContact> localContacts = new LinkedList<>();
|
||||
BatchOperationHandle handle = directory.startBatchOperation();
|
||||
|
||||
if (clientContacts == null) {
|
||||
logger.info("Remote tokens empty, ending...");
|
||||
break;
|
||||
} else {
|
||||
logger.info("Retrieved " + clientContacts.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.");
|
||||
if (remoteContacts == null) {
|
||||
logger.info("Remote tokens empty, ending...");
|
||||
break;
|
||||
} else {
|
||||
logger.info("Retrieved " + remoteContacts.size() + " remote tokens...");
|
||||
}
|
||||
|
||||
logger.info("Update from peer complete.");
|
||||
} finally {
|
||||
// directory.stopBatchOperation(handle);
|
||||
for (ClientContact remoteContact : remoteContacts) {
|
||||
localContacts.add(directory.get(handle, remoteContact.getToken()));
|
||||
}
|
||||
|
||||
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(String.format("Added %d and removed %d remove contacts.", contactsAdded, contactsRemoved));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.whispersystems.textsecuregcm.workers;
|
||||
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import org.skife.jdbi.v2.DBI;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
||||
|
||||
import io.dropwizard.cli.ConfiguredCommand;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.jdbi.ImmutableListContainerFactory;
|
||||
import io.dropwizard.jdbi.ImmutableSetContainerFactory;
|
||||
import io.dropwizard.jdbi.OptionalContainerFactory;
|
||||
import io.dropwizard.jdbi.args.OptionalArgumentFactory;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
|
||||
|
||||
public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class);
|
||||
|
||||
public VacuumCommand() {
|
||||
super("vacuum", "Vacuum Postgres Tables");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Bootstrap<WhisperServerConfiguration> bootstrap,
|
||||
Namespace namespace,
|
||||
WhisperServerConfiguration config)
|
||||
throws Exception
|
||||
{
|
||||
DataSourceFactory dbConfig = config.getDataSourceFactory();
|
||||
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
|
||||
|
||||
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
|
||||
dbi.registerContainerFactory(new ImmutableListContainerFactory());
|
||||
dbi.registerContainerFactory(new ImmutableSetContainerFactory());
|
||||
dbi.registerContainerFactory(new OptionalContainerFactory());
|
||||
|
||||
Accounts accounts = dbi.onDemand(Accounts.class );
|
||||
Keys keys = dbi.onDemand(Keys.class );
|
||||
PendingAccounts pendingAccounts = dbi.onDemand(PendingAccounts.class);
|
||||
|
||||
logger.warn("Vacuuming accounts...");
|
||||
accounts.vacuum();
|
||||
|
||||
logger.warn("Vacuuming pending_accounts...");
|
||||
pendingAccounts.vacuum();
|
||||
|
||||
logger.warn("Vacuuming keys...");
|
||||
keys.vacuum();
|
||||
|
||||
Thread.sleep(3000);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -142,4 +142,33 @@
|
||||
</createIndex>
|
||||
|
||||
</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>
|
||||
|
||||
@@ -2,54 +2,71 @@ package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.sun.jersey.api.client.ClientResponse;
|
||||
import com.yammer.dropwizard.testing.ResourceTest;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class AccountControllerTest extends ResourceTest {
|
||||
public class AccountControllerTest {
|
||||
|
||||
private static final String SENDER = "+14152222222";
|
||||
|
||||
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
|
||||
private AccountsManager accountsManager = mock(AccountsManager.class );
|
||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
private SmsSender smsSender = mock(SmsSender.class );
|
||||
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
|
||||
private AccountsManager accountsManager = mock(AccountsManager.class );
|
||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||
private SmsSender smsSender = mock(SmsSender.class );
|
||||
private StoredMessages storedMessages = mock(StoredMessages.class );
|
||||
private TimeProvider timeProvider = mock(TimeProvider.class );
|
||||
private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f");
|
||||
|
||||
@Override
|
||||
protected void setUpResources() throws Exception {
|
||||
addProvider(AuthHelper.getAuthenticator());
|
||||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthenticator())
|
||||
.addResource(new AccountController(pendingAccountsManager,
|
||||
accountsManager,
|
||||
rateLimiters,
|
||||
smsSender,
|
||||
storedMessages,
|
||||
timeProvider,
|
||||
Optional.of(authorizationKey)))
|
||||
.build();
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
|
||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis());
|
||||
|
||||
addResource(new AccountController(pendingAccountsManager,
|
||||
accountsManager,
|
||||
rateLimiters,
|
||||
smsSender));
|
||||
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendCode() throws Exception {
|
||||
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);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
@@ -60,7 +77,7 @@ public class AccountControllerTest extends ResourceTest {
|
||||
@Test
|
||||
public void testVerifyCode() throws Exception {
|
||||
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"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 2222))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
@@ -68,13 +85,13 @@ public class AccountControllerTest extends ResourceTest {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(accountsManager).create(isA(Account.class));
|
||||
verify(accountsManager, times(1)).create(isA(Account.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyBadCode() throws Exception {
|
||||
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"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 3333))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
@@ -85,4 +102,84 @@ public class AccountControllerTest extends ResourceTest {
|
||||
verifyNoMoreInteractions(accountsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyToken() throws Exception {
|
||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
|
||||
|
||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
||||
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/token/%s", token))
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
verify(accountsManager, times(1)).create(isA(Account.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyBadToken() throws Exception {
|
||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
|
||||
|
||||
String token = SENDER + ":1415906574:af4f046107c21721224a";
|
||||
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/token/%s", token))
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(accountsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyWrongToken() throws Exception {
|
||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
|
||||
|
||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
||||
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/token/%s", token))
|
||||
.header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(accountsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyExpiredToken() throws Exception {
|
||||
when(timeProvider.getCurrentTimeMillis()).thenReturn(1416003757901L);
|
||||
|
||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
||||
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/token/%s", token))
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
verifyNoMoreInteractions(accountsManager);
|
||||
}
|
||||
|
||||
private static byte[] decodeHex(String hex) {
|
||||
try {
|
||||
return Hex.decodeHex(hex.toCharArray());
|
||||
} catch (DecoderException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user