mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-11 01:40:22 +00:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa379bf21 | ||
|
|
820a2f1a63 | ||
|
|
6fac7614f5 | ||
|
|
b724ea8d3b | ||
|
|
06f80c320d | ||
|
|
d9de015eab | ||
|
|
dd36c861ba | ||
|
|
b34e46af93 | ||
|
|
405802c492 | ||
|
|
e15f3c9d2b | ||
|
|
885af064c9 | ||
|
|
40529dc41f | ||
|
|
2452f6ef8a | ||
|
|
4c543e6f06 | ||
|
|
bc5fd5d441 | ||
|
|
7a33cef27e | ||
|
|
b433b9c879 | ||
|
|
5d169c523f | ||
|
|
98d277368f | ||
|
|
3bd58bf25e | ||
|
|
ba05e577ae | ||
|
|
4206f6af45 | ||
|
|
0c5da1cc47 | ||
|
|
d9bd1c679e | ||
|
|
437eb8de37 | ||
|
|
f14c181840 | ||
|
|
d46c9fb157 | ||
|
|
6913e4dfd2 | ||
|
|
aea3f299a0 | ||
|
|
5667476780 | ||
|
|
b263f47826 | ||
|
|
21723d6313 | ||
|
|
a63cdc76b0 | ||
|
|
129e372613 | ||
|
|
53de38fc06 | ||
|
|
67e5794722 | ||
|
|
6aaca59020 | ||
|
|
f4ecb5d7be | ||
|
|
35e212a30f | ||
|
|
a6463df5bb | ||
|
|
a9994ef5aa | ||
|
|
6e0ae70f02 | ||
|
|
a0889130e5 | ||
|
|
8e763f62f5 | ||
|
|
866f8bf1ef | ||
|
|
7bb505db4c | ||
|
|
519f982604 | ||
|
|
2f85cd214e | ||
|
|
74f71fd8a6 | ||
|
|
6f9226dcf9 | ||
|
|
eedaa8b3f4 | ||
|
|
7af3c51cc4 | ||
|
|
d3830a7fd4 | ||
|
|
ce9d3548e4 | ||
|
|
0bd82784a0 | ||
|
|
542bf73a75 | ||
|
|
bd6cf10402 | ||
|
|
5a837d4481 | ||
|
|
b08eb0df5c | ||
|
|
e39016ad35 | ||
|
|
8c74ad073b | ||
|
|
918ef4a7ca | ||
|
|
2473505d4e | ||
|
|
591d26981e | ||
|
|
605e88d4bf | ||
|
|
48fe609d53 | ||
|
|
a0768e219a | ||
|
|
40a988c0cd | ||
|
|
5845d2dedd | ||
|
|
cb185a6552 | ||
|
|
2dc5857645 | ||
|
|
7d8336fd30 | ||
|
|
f9d7c1de57 | ||
|
|
648812a267 | ||
|
|
ef1160eda8 | ||
|
|
4cd1082a4a |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ run.sh
|
||||
local.yml
|
||||
config/production.yml
|
||||
config/federated.yml
|
||||
config/staging.yml
|
||||
.opsmanage
|
||||
|
||||
@@ -30,6 +30,10 @@ whispersystems@lists.riseup.net
|
||||
|
||||
https://lists.riseup.net/www/info/whispersystems
|
||||
|
||||
Current BitHub Payment Per Commit:
|
||||
=================
|
||||

|
||||
|
||||
|
||||
Cryptography Notice
|
||||
------------
|
||||
|
||||
@@ -56,9 +56,6 @@ graphite:
|
||||
host:
|
||||
port:
|
||||
|
||||
http:
|
||||
shutdownGracePeriod: 0s
|
||||
|
||||
database:
|
||||
# the name of your JDBC driver
|
||||
driverClass: org.postgresql.Driver
|
||||
@@ -75,24 +72,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
|
||||
|
||||
117
pom.xml
117
pom.xml
@@ -9,24 +9,72 @@
|
||||
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<version>0.3</version>
|
||||
<version>0.18</version>
|
||||
|
||||
<properties>
|
||||
<dropwizard.version>0.7.0</dropwizard.version>
|
||||
<jackson.api.version>2.3.3</jackson.api.version>
|
||||
<commons-codec.version>1.6</commons-codec.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-jdbi</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-auth</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-client</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-migrations</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-metrics-graphite</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.jersey</groupId>
|
||||
<artifactId>jersey-json</artifactId>
|
||||
<version>1.18.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.codahale.metrics</groupId>
|
||||
<artifactId>metrics-graphite</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-server</artifactId>
|
||||
<version>9.0.7.v20131107</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<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>
|
||||
@@ -66,35 +114,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>
|
||||
@@ -103,14 +126,24 @@
|
||||
<version>9.1-901.jdbc4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.jersey</groupId>
|
||||
<artifactId>jersey-json</artifactId>
|
||||
<version>1.17.1</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>
|
||||
|
||||
@@ -22,8 +22,9 @@ option java_outer_classname = "MessageProtos";
|
||||
message OutgoingMessageSignal {
|
||||
optional uint32 type = 1;
|
||||
optional string source = 2;
|
||||
optional uint32 sourceDevice = 7;
|
||||
optional string relay = 3;
|
||||
repeated string destinations = 4;
|
||||
// repeated string destinations = 4;
|
||||
optional uint64 timestamp = 5;
|
||||
optional bytes message = 6;
|
||||
}
|
||||
@@ -17,22 +17,25 @@
|
||||
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.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.RateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import io.dropwizard.Configuration;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
|
||||
public class WhisperServerConfiguration extends Configuration {
|
||||
|
||||
@NotNull
|
||||
@@ -72,7 +75,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DatabaseConfiguration database = new DatabaseConfiguration();
|
||||
private DataSourceFactory database = new DataSourceFactory();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@@ -83,6 +86,18 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private GraphiteConfiguration graphite = new GraphiteConfiguration();
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private MetricsConfiguration viz = new MetricsConfiguration();
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private WebsocketConfiguration websocket = new WebsocketConfiguration();
|
||||
|
||||
public WebsocketConfiguration getWebsocketConfiguration() {
|
||||
return websocket;
|
||||
}
|
||||
|
||||
public TwilioConfiguration getTwilioConfiguration() {
|
||||
return twilio;
|
||||
}
|
||||
@@ -111,7 +126,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return redis;
|
||||
}
|
||||
|
||||
public DatabaseConfiguration getDatabaseConfiguration() {
|
||||
public DataSourceFactory getDataSourceFactory() {
|
||||
return database;
|
||||
}
|
||||
|
||||
@@ -126,4 +141,8 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
public GraphiteConfiguration getGraphiteConfiguration() {
|
||||
return graphite;
|
||||
}
|
||||
|
||||
public MetricsConfiguration getMetricsConfiguration() {
|
||||
return viz;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,13 @@
|
||||
*/
|
||||
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.db.DatabaseConfiguration;
|
||||
import com.yammer.dropwizard.jdbi.DBIFactory;
|
||||
import com.yammer.dropwizard.migrations.MigrationsBundle;
|
||||
import com.yammer.metrics.reporting.GraphiteReporter;
|
||||
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;
|
||||
@@ -33,15 +30,23 @@ import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
|
||||
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||
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.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;
|
||||
@@ -50,22 +55,40 @@ import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevices;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketControllerFactory;
|
||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||
|
||||
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.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());
|
||||
@@ -73,69 +96,122 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
||||
|
||||
@Override
|
||||
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||
bootstrap.setName("whisper-server");
|
||||
bootstrap.addCommand(new DirectoryCommand());
|
||||
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
|
||||
{
|
||||
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);
|
||||
|
||||
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
|
||||
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
|
||||
|
||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
||||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager );
|
||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||
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);
|
||||
|
||||
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(config.getGcmConfiguration(),
|
||||
config.getApnConfiguration(),
|
||||
accountsManager, directory);
|
||||
storedMessages, pubSubManager,
|
||||
accountsManager);
|
||||
|
||||
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||
FederatedPeer.class,
|
||||
accountAuthenticator,
|
||||
Account.class, "WhisperServer"));
|
||||
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
||||
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
|
||||
|
||||
environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender));
|
||||
environment.addResource(new DirectoryController(rateLimiters, directory));
|
||||
environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner));
|
||||
environment.addResource(new KeysController(rateLimiters, keys, federatedClientManager));
|
||||
environment.addResource(new FederationController(keys, accountsManager, pushSender, urlSigner));
|
||||
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||
FederatedPeer.class,
|
||||
deviceAuthenticator,
|
||||
Device.class, "WhisperServer"));
|
||||
|
||||
environment.addServlet(new MessageController(rateLimiters, accountAuthenticator,
|
||||
pushSender, federatedClientManager),
|
||||
MessageController.PATH);
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender));
|
||||
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(attachmentController);
|
||||
environment.jersey().register(keysControllerV1);
|
||||
environment.jersey().register(keysControllerV2);
|
||||
environment.jersey().register(messageController);
|
||||
|
||||
environment.addHealthCheck(new RedisHealthCheck(redisClient));
|
||||
environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient));
|
||||
if (config.getWebsocketConfiguration().isEnabled()) {
|
||||
WebsocketControllerFactory servlet = new WebsocketControllerFactory(deviceAuthenticator,
|
||||
pushSender,
|
||||
storedMessages,
|
||||
pubSubManager);
|
||||
|
||||
environment.addProvider(new IOExceptionMapper());
|
||||
environment.addProvider(new RateLimitExceededExceptionMapper());
|
||||
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet);
|
||||
websocket.addMapping("/v1/websocket/*");
|
||||
websocket.setAsyncSupported(true);
|
||||
|
||||
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
|
||||
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
|
||||
filter.setInitParameter("allowedOrigins", "*");
|
||||
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin");
|
||||
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
|
||||
filter.setInitParameter("preflightMaxAge", "5184000");
|
||||
filter.setInitParameter("allowCredentials", "true");
|
||||
}
|
||||
|
||||
environment.healthChecks().register("redis", new RedisHealthCheck(redisClient));
|
||||
environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
|
||||
|
||||
environment.jersey().register(new IOExceptionMapper());
|
||||
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
|
||||
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
|
||||
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
|
||||
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
|
||||
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
|
||||
|
||||
if (config.getGraphiteConfiguration().isEnabled()) {
|
||||
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.getMetricsConfiguration().isEnabled()) {
|
||||
new JsonMetricsReporter(environment.metrics(),
|
||||
config.getMetricsConfiguration().getToken(),
|
||||
config.getMetricsConfiguration().getHost())
|
||||
.start(60, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,28 +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.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);
|
||||
|
||||
@@ -51,18 +50,30 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
|
||||
public Optional<Account> authenticate(BasicCredentials basicCredentials)
|
||||
throws AuthenticationException
|
||||
{
|
||||
Optional<Account> account = accountsManager.get(basicCredentials.getUsername());
|
||||
try {
|
||||
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
|
||||
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber());
|
||||
|
||||
if (!account.isPresent()) {
|
||||
if (!account.isPresent()) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
|
||||
|
||||
if (!device.isPresent()) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
|
||||
authenticationSucceededMeter.mark();
|
||||
account.get().setAuthenticatedDevice(device.get());
|
||||
return account;
|
||||
}
|
||||
|
||||
authenticationFailedMeter.mark();
|
||||
return Optional.absent();
|
||||
} catch (InvalidAuthorizationHeaderException iahe) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
if (account.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
|
||||
authenticationSucceededMeter.mark();
|
||||
return account;
|
||||
}
|
||||
|
||||
authenticationFailedMeter.mark();
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,28 @@ import java.io.IOException;
|
||||
|
||||
public class AuthorizationHeader {
|
||||
|
||||
private final String user;
|
||||
private final String number;
|
||||
private final long accountId;
|
||||
private final String password;
|
||||
|
||||
public AuthorizationHeader(String header) throws InvalidAuthorizationHeaderException {
|
||||
private AuthorizationHeader(String number, long accountId, String password) {
|
||||
this.number = number;
|
||||
this.accountId = accountId;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {
|
||||
try {
|
||||
String[] numberAndId = user.split("\\.");
|
||||
return new AuthorizationHeader(numberAndId[0],
|
||||
numberAndId.length > 1 ? Long.parseLong(numberAndId[1]) : 1,
|
||||
password);
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new InvalidAuthorizationHeaderException(nfe);
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthorizationHeader fromFullHeader(String header) throws InvalidAuthorizationHeaderException {
|
||||
try {
|
||||
if (header == null) {
|
||||
throw new InvalidAuthorizationHeaderException("Null header");
|
||||
@@ -55,16 +73,18 @@ public class AuthorizationHeader {
|
||||
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
|
||||
}
|
||||
|
||||
this.user = credentialParts[0];
|
||||
this.password = credentialParts[1];
|
||||
|
||||
return fromUserAndPassword(credentialParts[0], credentialParts[1]);
|
||||
} catch (IOException ioe) {
|
||||
throw new InvalidAuthorizationHeaderException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return user;
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,20 @@ public class RateLimitsConfiguration {
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration messages = new RateLimitConfiguration(60, 60);
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration allocateDevice = new RateLimitConfiguration(2, 1.0 / 2.0);
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration verifyDevice = new RateLimitConfiguration(2, 2);
|
||||
|
||||
public RateLimitConfiguration getAllocateDevice() {
|
||||
return allocateDevice;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getVerifyDevice() {
|
||||
return verifyDevice;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class WebsocketConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private boolean enabled = false;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,9 +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;
|
||||
@@ -32,6 +32,7 @@ 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.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
@@ -53,6 +54,8 @@ import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/accounts")
|
||||
public class AccountController {
|
||||
|
||||
@@ -94,7 +97,7 @@ public class AccountController {
|
||||
rateLimiters.getVoiceDestinationLimiter().validate(number);
|
||||
break;
|
||||
default:
|
||||
throw new WebApplicationException(Response.status(415).build());
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
|
||||
VerificationCode verificationCode = generateVerificationCode();
|
||||
@@ -119,8 +122,8 @@ public class AccountController {
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
try {
|
||||
AuthorizationHeader header = new AuthorizationHeader(authorizationHeader);
|
||||
String number = header.getUserName();
|
||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||
String number = header.getNumber();
|
||||
String password = header.getPassword();
|
||||
|
||||
rateLimiters.getVerifyLimiter().validate(number);
|
||||
@@ -133,28 +136,43 @@ public class AccountController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
if (accounts.isRelayListed(number)) {
|
||||
throw new WebApplicationException(Response.status(417).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());
|
||||
|
||||
Account account = new Account();
|
||||
account.setNumber(number);
|
||||
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
account.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
account.setSupportsSms(accountAttributes.getSupportsSms());
|
||||
account.addDevice(device);
|
||||
|
||||
accounts.create(account);
|
||||
logger.debug("Stored account...");
|
||||
|
||||
pendingAccounts.remove(number);
|
||||
|
||||
logger.debug("Stored device...");
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad Authorization Header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/gcm/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
|
||||
account.setApnRegistrationId(null);
|
||||
account.setGcmRegistrationId(registrationId.getGcmRegistrationId());
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setApnId(null);
|
||||
device.setGcmId(registrationId.getGcmRegistrationId());
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@@ -162,7 +180,8 @@ public class AccountController {
|
||||
@DELETE
|
||||
@Path("/gcm/")
|
||||
public void deleteGcmRegistrationId(@Auth Account account) {
|
||||
account.setGcmRegistrationId(null);
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setGcmId(null);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@@ -171,8 +190,9 @@ public class AccountController {
|
||||
@Path("/apn/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
|
||||
account.setApnRegistrationId(registrationId.getApnRegistrationId());
|
||||
account.setGcmRegistrationId(null);
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setApnId(registrationId.getApnRegistrationId());
|
||||
device.setGcmId(null);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@@ -180,7 +200,8 @@ public class AccountController {
|
||||
@DELETE
|
||||
@Path("/apn/")
|
||||
public void deleteApnRegistrationId(@Auth Account account) {
|
||||
account.setApnRegistrationId(null);
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setApnId(null);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@@ -190,10 +211,10 @@ public class AccountController {
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public Response getTwiml(@PathParam("code") String encodedVerificationText) {
|
||||
return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML,
|
||||
encodedVerificationText)).build();
|
||||
encodedVerificationText)).build();
|
||||
}
|
||||
|
||||
private VerificationCode generateVerificationCode() {
|
||||
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||
try {
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
int randomInt = 100000 + random.nextInt(900000);
|
||||
@@ -202,5 +223,4 @@ public class AccountController {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.amazonaws.HttpMethod;
|
||||
import com.yammer.dropwizard.auth.Auth;
|
||||
import com.yammer.metrics.annotation.Timed;
|
||||
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.AttachmentDescriptor;
|
||||
@@ -35,14 +35,16 @@ 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.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
|
||||
@Path("/v1/attachments")
|
||||
public class AttachmentController {
|
||||
@@ -65,37 +67,38 @@ public class AttachmentController {
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response allocateAttachment(@Auth Account account) throws RateLimitExceededException {
|
||||
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
|
||||
public AttachmentDescriptor allocateAttachment(@Auth Account account)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (account.isRateLimited()) {
|
||||
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
|
||||
}
|
||||
|
||||
long attachmentId = generateAttachmentId();
|
||||
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);
|
||||
AttachmentDescriptor descriptor = new AttachmentDescriptor(attachmentId, url.toExternalForm());
|
||||
long attachmentId = generateAttachmentId();
|
||||
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);
|
||||
|
||||
return new AttachmentDescriptor(attachmentId, url.toExternalForm());
|
||||
|
||||
return Response.ok().entity(descriptor).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{attachmentId}")
|
||||
public Response redirectToAttachment(@Auth Account account,
|
||||
@PathParam("attachmentId") long attachmentId,
|
||||
@QueryParam("relay") String relay)
|
||||
public AttachmentUri redirectToAttachment(@Auth Account account,
|
||||
@PathParam("attachmentId") long attachmentId,
|
||||
@QueryParam("relay") Optional<String> relay)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
URL url;
|
||||
|
||||
if (relay == null) url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
|
||||
else url = federatedClientManager.getClient(relay).getSignedAttachmentUri(attachmentId);
|
||||
|
||||
return Response.ok().entity(new AttachmentUri(url)).build();
|
||||
} catch (IOException e) {
|
||||
logger.warn("No conectivity", e);
|
||||
return Response.status(500).build();
|
||||
if (!relay.isPresent()) {
|
||||
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET));
|
||||
} else {
|
||||
return new AttachmentUri(federatedClientManager.getClient(relay.get()).getSignedAttachmentUri(attachmentId));
|
||||
}
|
||||
} catch (NoSuchPeerException e) {
|
||||
logger.info("No such peer: " + relay);
|
||||
return Response.status(404).build();
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/devices")
|
||||
public class DeviceController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
|
||||
|
||||
private final PendingDevicesManager pendingDevices;
|
||||
private final AccountsManager accounts;
|
||||
private final RateLimiters rateLimiters;
|
||||
|
||||
public DeviceController(PendingDevicesManager pendingDevices,
|
||||
AccountsManager accounts,
|
||||
RateLimiters rateLimiters)
|
||||
{
|
||||
this.pendingDevices = pendingDevices;
|
||||
this.accounts = accounts;
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/provisioning_code")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public VerificationCode createDeviceToken(@Auth Account account)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
|
||||
|
||||
VerificationCode verificationCode = generateVerificationCode();
|
||||
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
|
||||
|
||||
return verificationCode;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Path("/{verification_code}")
|
||||
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
try {
|
||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||
String number = header.getNumber();
|
||||
String password = header.getPassword();
|
||||
|
||||
rateLimiters.getVerifyDeviceLimiter().validate(number);
|
||||
|
||||
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
|
||||
|
||||
if (!storedVerificationCode.isPresent() ||
|
||||
!verificationCode.equals(storedVerificationCode.get()))
|
||||
{
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Optional<Account> account = accounts.get(number);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Device device = new Device();
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setId(account.get().getNextDeviceId());
|
||||
|
||||
account.get().addDevice(device);
|
||||
accounts.update(account.get());
|
||||
|
||||
pendingDevices.remove(number);
|
||||
|
||||
return new DeviceResponse(device.getId());
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad Authorization Header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||
try {
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
int randomInt = 100000 + random.nextInt(900000);
|
||||
return new VerificationCode(randomInt);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,21 @@
|
||||
*/
|
||||
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.Metered;
|
||||
import com.yammer.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
@@ -44,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;
|
||||
@@ -57,7 +64,7 @@ public class DirectoryController {
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
@Timed()
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/{token}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -78,7 +85,7 @@ public class DirectoryController {
|
||||
}
|
||||
}
|
||||
|
||||
@Timed()
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/tokens")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@@ -87,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,157 +1,19 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.amazonaws.HttpMethod;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.yammer.dropwizard.auth.Auth;
|
||||
import com.yammer.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/v1/federation")
|
||||
public class FederationController {
|
||||
|
||||
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 PushSender pushSender;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final UrlSigner urlSigner;
|
||||
|
||||
public FederationController(Keys keys, AccountsManager accounts, PushSender pushSender, UrlSigner urlSigner) {
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
this.pushSender = pushSender;
|
||||
this.urlSigner = urlSigner;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/attachment/{attachmentId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
|
||||
@PathParam("attachmentId") long attachmentId)
|
||||
public FederationController(AccountsManager accounts,
|
||||
AttachmentController attachmentController,
|
||||
MessageController messageController)
|
||||
{
|
||||
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
|
||||
return new AttachmentUri(url);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/key/{number}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKey getKey(@Auth FederatedPeer peer,
|
||||
@PathParam("number") String number)
|
||||
{
|
||||
PreKey preKey = keys.get(number);
|
||||
|
||||
if (preKey == null) {
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
}
|
||||
|
||||
return preKey;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/message")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void relayMessage(@Auth FederatedPeer peer, @Valid RelayMessage message)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
OutgoingMessageSignal signal = OutgoingMessageSignal.parseFrom(message.getOutgoingMessageSignal())
|
||||
.toBuilder()
|
||||
.setRelay(peer.getName())
|
||||
.build();
|
||||
|
||||
pushSender.sendMessage(message.getDestination(), signal);
|
||||
} catch (InvalidProtocolBufferException ipe) {
|
||||
logger.warn("ProtoBuf", ipe);
|
||||
throw new WebApplicationException(Response.status(400).build());
|
||||
} catch (NoSuchUserException e) {
|
||||
logger.debug("No User", e);
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/user_count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCount getUserCount(@Auth FederatedPeer peer) {
|
||||
return new AccountCount((int)accounts.getCount());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/user_tokens/{offset}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
|
||||
@PathParam("offset") int offset)
|
||||
{
|
||||
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
|
||||
List<ClientContact> clientContacts = new LinkedList<>();
|
||||
|
||||
for (Account account : accountList) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
||||
|
||||
if (Util.isEmpty(account.getApnRegistrationId()) &&
|
||||
Util.isEmpty(account.getGcmRegistrationId()))
|
||||
{
|
||||
clientContact.setInactive(true);
|
||||
}
|
||||
|
||||
clientContacts.add(clientContact);
|
||||
}
|
||||
|
||||
return new ClientContacts(clientContacts);
|
||||
this.accounts = accounts;
|
||||
this.attachmentController = attachmentController;
|
||||
this.messageController = messageController;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,76 +16,100 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
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 com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
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.List;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/keys")
|
||||
public class KeysController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
||||
protected final RateLimiters rateLimiters;
|
||||
protected final Keys keys;
|
||||
protected final AccountsManager accounts;
|
||||
protected final FederatedClientManager federatedClientManager;
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Keys keys;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys,
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||
FederatedClientManager federatedClientManager)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
|
||||
keys.store(account.getNumber(), preKeys.getLastResortKey(), preKeys.getKeys());
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKeyCount getStatus(@Auth Account account) {
|
||||
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
|
||||
|
||||
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") String relay)
|
||||
throws RateLimitExceededException
|
||||
protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number);
|
||||
Optional<Account> destination = accounts.get(number);
|
||||
|
||||
if (!destination.isPresent() || !destination.get().isActive()) {
|
||||
throw new NoSuchUserException("Target account is inactive");
|
||||
}
|
||||
|
||||
try {
|
||||
PreKey key;
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
}
|
||||
|
||||
if (relay == null) key = keys.get(number);
|
||||
else key = federatedClientManager.getClient(relay).getKey(number);
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
Optional<Device> targetDevice = destination.get().getDevice(deviceId);
|
||||
|
||||
if (key == null) throw new WebApplicationException(Response.status(404).build());
|
||||
else return key;
|
||||
} catch (NoSuchPeerException e) {
|
||||
logger.info("No peer: " + relay);
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
|
||||
throw new NoSuchUserException("Target device is inactive.");
|
||||
}
|
||||
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class TargetKeys {
|
||||
private final Account destination;
|
||||
private final Optional<List<KeyRecord>> keys;
|
||||
|
||||
public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
|
||||
this.destination = destination;
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
public Optional<List<KeyRecord>> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public Account getDestination() {
|
||||
return destination;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,298 +16,273 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.yammer.dropwizard.auth.AuthenticationException;
|
||||
import com.yammer.dropwizard.auth.basic.BasicCredentials;
|
||||
import com.yammer.metrics.Metrics;
|
||||
import com.yammer.metrics.core.Meter;
|
||||
import com.yammer.metrics.core.Timer;
|
||||
import com.yammer.metrics.core.TimerContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClient;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
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.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.IterablePair;
|
||||
import org.whispersystems.textsecuregcm.util.IterablePair.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.servlet.AsyncContext;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedReader;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Set;
|
||||
|
||||
public class MessageController extends HttpServlet {
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
public static final String PATH = "/v1/messages/";
|
||||
@Path("/v1/messages")
|
||||
public class MessageController {
|
||||
|
||||
private final Meter successMeter = Metrics.newMeter(MessageController.class, "deliver_message", "success", TimeUnit.MINUTES);
|
||||
private final Meter failureMeter = Metrics.newMeter(MessageController.class, "deliver_message", "failure", TimeUnit.MINUTES);
|
||||
private final Timer timer = Metrics.newTimer(MessageController.class, "deliver_message_time", TimeUnit.MILLISECONDS, TimeUnit.MINUTES);
|
||||
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final AccountAuthenticator accountAuthenticator;
|
||||
private final PushSender pushSender;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ExecutorService executor;
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
public MessageController(RateLimiters rateLimiters,
|
||||
AccountAuthenticator accountAuthenticator,
|
||||
PushSender pushSender,
|
||||
AccountsManager accountsManager,
|
||||
FederatedClientManager federatedClientManager)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountAuthenticator = accountAuthenticator;
|
||||
this.pushSender = pushSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.executor = Executors.newFixedThreadPool(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
||||
TimerContext timerContext = timer.time();
|
||||
@Timed
|
||||
@Path("/{destination}")
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void sendMessage(@Auth Account source,
|
||||
@PathParam("destination") String destinationName,
|
||||
@Valid IncomingMessageList messages)
|
||||
throws IOException, RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||
|
||||
try {
|
||||
Account sender = authenticate(req);
|
||||
IncomingMessageList messages = parseIncomingMessages(req);
|
||||
|
||||
rateLimiters.getMessagesLimiter().validate(sender.getNumber());
|
||||
|
||||
List<IncomingMessage> incomingMessages = messages.getMessages();
|
||||
List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(),
|
||||
incomingMessages);
|
||||
|
||||
IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages,
|
||||
outgoingMessages);
|
||||
|
||||
handleAsyncDelivery(timerContext, req.startAsync(), listPair);
|
||||
} catch (AuthenticationException e) {
|
||||
failureMeter.mark();
|
||||
timerContext.stop();
|
||||
resp.setStatus(401);
|
||||
} catch (ValidationException e) {
|
||||
failureMeter.mark();
|
||||
timerContext.stop();
|
||||
resp.setStatus(415);
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOE", e);
|
||||
failureMeter.mark();
|
||||
timerContext.stop();
|
||||
resp.setStatus(501);
|
||||
} catch (RateLimitExceededException e) {
|
||||
timerContext.stop();
|
||||
failureMeter.mark();
|
||||
resp.setStatus(413);
|
||||
if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages);
|
||||
else sendRelayMessage(source, destinationName, messages);
|
||||
} catch (NoSuchUserException e) {
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
} catch (MismatchedDevicesException e) {
|
||||
throw new WebApplicationException(Response.status(409)
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.entity(new MismatchedDevices(e.getMissingDevices(),
|
||||
e.getExtraDevices()))
|
||||
.build());
|
||||
} catch (StaleDevicesException e) {
|
||||
throw new WebApplicationException(Response.status(410)
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(new StaleDevices(e.getStaleDevices()))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAsyncDelivery(final TimerContext timerContext,
|
||||
final AsyncContext context,
|
||||
final IterablePair<IncomingMessage, OutgoingMessageSignal> listPair)
|
||||
@Timed
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public MessageResponse sendMessageLegacy(@Auth Account source, @Valid IncomingMessageList messages)
|
||||
throws IOException, RateLimitExceededException
|
||||
{
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<String> success = new LinkedList<>();
|
||||
List<String> failure = new LinkedList<>();
|
||||
HttpServletResponse response = (HttpServletResponse) context.getResponse();
|
||||
try {
|
||||
List<IncomingMessage> incomingMessages = messages.getMessages();
|
||||
validateLegacyDestinations(incomingMessages);
|
||||
|
||||
try {
|
||||
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
|
||||
String destination = messagePair.first().getDestination();
|
||||
String relay = messagePair.first().getRelay();
|
||||
messages.setRelay(incomingMessages.get(0).getRelay());
|
||||
sendMessage(source, incomingMessages.get(0).getDestination(), messages);
|
||||
|
||||
try {
|
||||
if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
|
||||
else sendRelayMessage(relay, destination, messagePair.second());
|
||||
success.add(destination);
|
||||
} catch (NoSuchUserException e) {
|
||||
logger.debug("No such user", e);
|
||||
failure.add(destination);
|
||||
}
|
||||
}
|
||||
return new MessageResponse(new LinkedList<String>(), new LinkedList<String>());
|
||||
} catch (ValidationException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
}
|
||||
|
||||
byte[] responseData = serializeResponse(new MessageResponse(success, failure));
|
||||
response.setContentLength(responseData.length);
|
||||
response.getOutputStream().write(responseData);
|
||||
context.complete();
|
||||
successMeter.mark();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Async Handler", e);
|
||||
failureMeter.mark();
|
||||
response.setStatus(501);
|
||||
context.complete();
|
||||
}
|
||||
private void sendLocalMessage(Account source,
|
||||
String destinationName,
|
||||
IncomingMessageList messages)
|
||||
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
|
||||
{
|
||||
Account destination = getDestinationAccount(destinationName);
|
||||
|
||||
timerContext.stop();
|
||||
validateCompleteDeviceList(destination, messages.getMessages());
|
||||
validateRegistrationIds(destination, messages.getMessages());
|
||||
|
||||
for (IncomingMessage incomingMessage : messages.getMessages()) {
|
||||
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
|
||||
|
||||
if (destinationDevice.isPresent()) {
|
||||
sendLocalMessage(source, destination, destinationDevice.get(), incomingMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sendLocalMessage(String destination, OutgoingMessageSignal outgoingMessage)
|
||||
throws IOException, NoSuchUserException
|
||||
private void sendLocalMessage(Account source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
IncomingMessage incomingMessage)
|
||||
throws NoSuchUserException, IOException
|
||||
{
|
||||
pushSender.sendMessage(destination, outgoingMessage);
|
||||
try {
|
||||
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
|
||||
OutgoingMessageSignal.Builder messageBuilder = OutgoingMessageSignal.newBuilder();
|
||||
|
||||
messageBuilder.setType(incomingMessage.getType())
|
||||
.setSource(source.getNumber())
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
messageBuilder.setMessage(ByteString.copyFrom(messageBody.get()));
|
||||
}
|
||||
|
||||
if (source.getRelay().isPresent()) {
|
||||
messageBuilder.setRelay(source.getRelay().get());
|
||||
}
|
||||
|
||||
pushSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build());
|
||||
} catch (NotPushRegisteredException e) {
|
||||
if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
|
||||
else logger.debug("Not registered", e);
|
||||
} catch (TransientPushFailureException e) {
|
||||
if (destinationDevice.isMaster()) throw new IOException(e);
|
||||
else logger.debug("Transient failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRelayMessage(String relay, String destination, OutgoingMessageSignal outgoingMessage)
|
||||
private void sendRelayMessage(Account source,
|
||||
String destinationName,
|
||||
IncomingMessageList messages)
|
||||
throws IOException, NoSuchUserException
|
||||
{
|
||||
try {
|
||||
FederatedClient client = federatedClientManager.getClient(relay);
|
||||
client.sendMessage(destination, outgoingMessage);
|
||||
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
|
||||
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
|
||||
destinationName, messages);
|
||||
} catch (NoSuchPeerException e) {
|
||||
logger.info("No such peer", e);
|
||||
throw new NoSuchUserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OutgoingMessageSignal> getOutgoingMessageSignals(String number,
|
||||
List<IncomingMessage> incomingMessages)
|
||||
private Account getDestinationAccount(String destination)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
List<OutgoingMessageSignal> outgoingMessages = new LinkedList<>();
|
||||
Optional<Account> account = accountsManager.get(destination);
|
||||
|
||||
for (IncomingMessage incoming : incomingMessages) {
|
||||
OutgoingMessageSignal.Builder outgoingMessage = OutgoingMessageSignal.newBuilder();
|
||||
outgoingMessage.setType(incoming.getType());
|
||||
outgoingMessage.setSource(number);
|
||||
if (!account.isPresent() || !account.get().isActive()) {
|
||||
throw new NoSuchUserException(destination);
|
||||
}
|
||||
|
||||
byte[] messageBody = getMessageBody(incoming);
|
||||
return account.get();
|
||||
}
|
||||
|
||||
if (messageBody != null) {
|
||||
outgoingMessage.setMessage(ByteString.copyFrom(messageBody));
|
||||
private void validateRegistrationIds(Account account, List<IncomingMessage> messages)
|
||||
throws StaleDevicesException
|
||||
{
|
||||
List<Long> staleDevices = new LinkedList<>();
|
||||
|
||||
for (IncomingMessage message : messages) {
|
||||
Optional<Device> device = account.getDevice(message.getDestinationDeviceId());
|
||||
|
||||
if (device.isPresent() &&
|
||||
message.getDestinationRegistrationId() > 0 &&
|
||||
message.getDestinationRegistrationId() != device.get().getRegistrationId())
|
||||
{
|
||||
staleDevices.add(device.get().getId());
|
||||
}
|
||||
}
|
||||
|
||||
outgoingMessage.setTimestamp(System.currentTimeMillis());
|
||||
if (!staleDevices.isEmpty()) {
|
||||
throw new StaleDevicesException(staleDevices);
|
||||
}
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
private void validateCompleteDeviceList(Account account, List<IncomingMessage> messages)
|
||||
throws MismatchedDevicesException
|
||||
{
|
||||
Set<Long> messageDeviceIds = new HashSet<>();
|
||||
Set<Long> accountDeviceIds = new HashSet<>();
|
||||
|
||||
for (IncomingMessage sub : incomingMessages) {
|
||||
if (sub != incoming) {
|
||||
outgoingMessage.setDestinations(index++, sub.getDestination());
|
||||
List<Long> missingDeviceIds = new LinkedList<>();
|
||||
List<Long> extraDeviceIds = new LinkedList<>();
|
||||
|
||||
for (IncomingMessage message : messages) {
|
||||
messageDeviceIds.add(message.getDestinationDeviceId());
|
||||
}
|
||||
|
||||
for (Device device : account.getDevices()) {
|
||||
if (device.isActive()) {
|
||||
accountDeviceIds.add(device.getId());
|
||||
|
||||
if (!messageDeviceIds.contains(device.getId())) {
|
||||
missingDeviceIds.add(device.getId());
|
||||
}
|
||||
}
|
||||
|
||||
outgoingMessages.add(outgoingMessage.build());
|
||||
}
|
||||
|
||||
return outgoingMessages;
|
||||
}
|
||||
for (IncomingMessage message : messages) {
|
||||
if (!accountDeviceIds.contains(message.getDestinationDeviceId())) {
|
||||
extraDeviceIds.add(message.getDestinationDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getMessageBody(IncomingMessage message) {
|
||||
try {
|
||||
return Base64.decode(message.getBody());
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
return null;
|
||||
if (!missingDeviceIds.isEmpty() || !extraDeviceIds.isEmpty()) {
|
||||
throw new MismatchedDevicesException(missingDeviceIds, extraDeviceIds);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] serializeResponse(MessageResponse response) throws IOException {
|
||||
try {
|
||||
return objectMapper.writeValueAsBytes(response);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private IncomingMessageList parseIncomingMessages(HttpServletRequest request)
|
||||
throws IOException, ValidationException
|
||||
private void validateLegacyDestinations(List<IncomingMessage> messages)
|
||||
throws ValidationException
|
||||
{
|
||||
BufferedReader reader = request.getReader();
|
||||
StringBuilder content = new StringBuilder();
|
||||
String line;
|
||||
String destination = null;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
content.append(line);
|
||||
for (IncomingMessage message : messages) {
|
||||
if ((message.getDestination() == null) ||
|
||||
(destination != null && !destination.equals(message.getDestination())))
|
||||
{
|
||||
throw new ValidationException("Multiple account destinations!");
|
||||
}
|
||||
|
||||
destination = message.getDestination();
|
||||
}
|
||||
|
||||
IncomingMessageList messages = objectMapper.readValue(content.toString(),
|
||||
IncomingMessageList.class);
|
||||
|
||||
if (messages.getMessages() == null) {
|
||||
throw new ValidationException();
|
||||
}
|
||||
|
||||
for (IncomingMessage message : messages.getMessages()) {
|
||||
if (message.getBody() == null) throw new ValidationException();
|
||||
if (message.getDestination() == null) throw new ValidationException();
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private Account authenticate(HttpServletRequest request) throws AuthenticationException {
|
||||
private Optional<byte[]> getMessageBody(IncomingMessage message) {
|
||||
try {
|
||||
AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization"));
|
||||
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(),
|
||||
authorizationHeader.getPassword() );
|
||||
|
||||
Optional<Account> account = accountAuthenticator.authenticate(credentials);
|
||||
|
||||
if (account.isPresent()) return account.get();
|
||||
else throw new AuthenticationException("Bad credentials");
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
throw new AuthenticationException(e);
|
||||
return Optional.of(Base64.decode(message.getBody()));
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Bad B64", ioe);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// @Timed
|
||||
// @POST
|
||||
// @Consumes(MediaType.APPLICATION_JSON)
|
||||
// @Produces(MediaType.APPLICATION_JSON)
|
||||
// public MessageResponse sendMessage(@Auth Account sender, IncomingMessageList messages)
|
||||
// throws IOException
|
||||
// {
|
||||
// List<String> success = new LinkedList<>();
|
||||
// List<String> failure = new LinkedList<>();
|
||||
// List<IncomingMessage> incomingMessages = messages.getMessages();
|
||||
// List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(), incomingMessages);
|
||||
//
|
||||
// IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages, outgoingMessages);
|
||||
//
|
||||
// for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
|
||||
// String destination = messagePair.first().getDestination();
|
||||
// String relay = messagePair.first().getRelay();
|
||||
//
|
||||
// try {
|
||||
// if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
|
||||
// else sendRelayMessage(relay, destination, messagePair.second());
|
||||
// success.add(destination);
|
||||
// } catch (NoSuchUserException e) {
|
||||
// logger.debug("No such user", e);
|
||||
// failure.add(destination);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return new MessageResponse(success, failure);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MismatchedDevicesException extends Exception {
|
||||
|
||||
private final List<Long> missingDevices;
|
||||
private final List<Long> extraDevices;
|
||||
|
||||
public MismatchedDevicesException(List<Long> missingDevices, List<Long> extraDevices) {
|
||||
this.missingDevices = missingDevices;
|
||||
this.extraDevices = extraDevices;
|
||||
}
|
||||
|
||||
public List<Long> getMissingDevices() {
|
||||
return missingDevices;
|
||||
}
|
||||
|
||||
public List<Long> getExtraDevices() {
|
||||
return extraDevices;
|
||||
}
|
||||
}
|
||||
@@ -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,16 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class StaleDevicesException extends Throwable {
|
||||
private final List<Long> staleDevices;
|
||||
|
||||
public StaleDevicesException(List<Long> staleDevices) {
|
||||
this.staleDevices = staleDevices;
|
||||
}
|
||||
|
||||
public List<Long> getStaleDevices() {
|
||||
return staleDevices;
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,7 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
|
||||
public class ValidationException extends Exception {
|
||||
public ValidationException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Optional;
|
||||
import org.eclipse.jetty.websocket.api.CloseStatus;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingWebsocketMessage;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.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.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;
|
||||
|
||||
import io.dropwizard.auth.AuthenticationException;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
public class WebsocketController implements WebSocketListener, PubSubListener {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
private static final Map<Long, String> pendingMessages = new HashMap<>();
|
||||
|
||||
private final AccountAuthenticator accountAuthenticator;
|
||||
private final PubSubManager pubSubManager;
|
||||
private final StoredMessages storedMessages;
|
||||
private final PushSender pushSender;
|
||||
|
||||
private WebsocketAddress address;
|
||||
private Account account;
|
||||
private Device device;
|
||||
private Session session;
|
||||
|
||||
private long pendingMessageSequence;
|
||||
|
||||
public WebsocketController(AccountAuthenticator accountAuthenticator,
|
||||
PushSender pushSender,
|
||||
PubSubManager pubSubManager,
|
||||
StoredMessages storedMessages)
|
||||
{
|
||||
this.accountAuthenticator = accountAuthenticator;
|
||||
this.pushSender = pushSender;
|
||||
this.pubSubManager = pubSubManager;
|
||||
this.storedMessages = storedMessages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
try {
|
||||
UpgradeRequest request = session.getUpgradeRequest();
|
||||
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)
|
||||
{
|
||||
session.close(new CloseStatus(4001, "Unauthorized"));
|
||||
return;
|
||||
}
|
||||
|
||||
BasicCredentials credentials = new BasicCredentials(usernames[0], passwords[0]);
|
||||
Optional<Account> account = accountAuthenticator.authenticate(credentials);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
session.close(new CloseStatus(4001, "Unauthorized"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.account = account.get();
|
||||
this.device = account.get().getAuthenticatedDevice().get();
|
||||
this.address = new WebsocketAddress(this.account.getId(), this.device.getId());
|
||||
this.session = session;
|
||||
|
||||
this.session.setIdleTimeout(10 * 60 * 1000);
|
||||
this.pubSubManager.subscribe(this.address, this);
|
||||
|
||||
handleQueryDatabase();
|
||||
} catch (AuthenticationException e) {
|
||||
try { session.close(1011, "Server Error");} catch (IOException e1) {}
|
||||
} catch (IOException ioe) {
|
||||
logger.info("Abrupt session close.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(String body) {
|
||||
try {
|
||||
IncomingWebsocketMessage incomingMessage = mapper.readValue(body, IncomingWebsocketMessage.class);
|
||||
|
||||
switch (incomingMessage.getType()) {
|
||||
case IncomingWebsocketMessage.TYPE_ACKNOWLEDGE_MESSAGE:
|
||||
handleMessageAck(body);
|
||||
break;
|
||||
default:
|
||||
close(new CloseStatus(1008, "Unknown Type"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("Parse", e);
|
||||
close(new CloseStatus(1008, "Badly Formatted"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int i, String s) {
|
||||
pubSubManager.unsubscribe(this.address, this);
|
||||
|
||||
List<String> remainingMessages = new LinkedList<>();
|
||||
|
||||
synchronized (pendingMessages) {
|
||||
Long[] pendingKeys = pendingMessages.keySet().toArray(new Long[0]);
|
||||
Arrays.sort(pendingKeys);
|
||||
|
||||
for (long pendingKey : pendingKeys) {
|
||||
remainingMessages.add(pendingMessages.get(pendingKey));
|
||||
}
|
||||
|
||||
pendingMessages.clear();
|
||||
}
|
||||
|
||||
for (String remainingMessage : remainingMessages) {
|
||||
try {
|
||||
pushSender.sendMessage(account, device, new EncryptedOutgoingMessage(remainingMessage));
|
||||
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
||||
logger.warn("onWebSocketClose", e);
|
||||
storedMessages.insert(account.getId(), device.getId(), remainingMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
WebsocketMessage websocketMessage = new WebsocketMessage(messageSequence, message);
|
||||
session.getRemote().sendStringByFuture(mapper.writeValueAsString(websocketMessage));
|
||||
} catch (IOException e) {
|
||||
logger.debug("Response failed", e);
|
||||
close(null);
|
||||
}
|
||||
}
|
||||
|
||||
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 handleQueryDatabase() {
|
||||
List<String> messages = storedMessages.getMessagesForDevice(account.getId(), device.getId());
|
||||
|
||||
for (String message : messages) {
|
||||
handleDeliverOutgoingMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(byte[] bytes, int i, int i2) {
|
||||
logger.info("Received binary message!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(Throwable throwable) {
|
||||
logger.info("onWebSocketError", throwable);
|
||||
}
|
||||
|
||||
|
||||
private void close(CloseStatus closeStatus) {
|
||||
try {
|
||||
if (this.session != null) {
|
||||
if (closeStatus != null) this.session.close(closeStatus);
|
||||
else this.session.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.info("close()", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,19 @@ public class AccountAttributes {
|
||||
@JsonProperty
|
||||
private boolean supportsSms;
|
||||
|
||||
@JsonProperty
|
||||
private boolean fetchesMessages;
|
||||
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
public AccountAttributes() {}
|
||||
|
||||
public AccountAttributes(String signalingKey, boolean supportsSms) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.supportsSms = supportsSms;
|
||||
public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages, int registrationId) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.supportsSms = supportsSms;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
}
|
||||
|
||||
public String getSignalingKey() {
|
||||
@@ -43,4 +51,11 @@ public class AccountAttributes {
|
||||
return supportsSms;
|
||||
}
|
||||
|
||||
public boolean getFetchesMessages() {
|
||||
return fetchesMessages;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AcknowledgeWebsocketMessage extends IncomingWebsocketMessage {
|
||||
|
||||
@JsonProperty
|
||||
private long id;
|
||||
|
||||
public AcknowledgeWebsocketMessage() {}
|
||||
|
||||
public AcknowledgeWebsocketMessage(long id) {
|
||||
this.type = TYPE_ACKNOWLEDGE_MESSAGE;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,4 +30,11 @@ public class ClientContactTokens {
|
||||
public List<String> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public ClientContactTokens() {}
|
||||
|
||||
public ClientContactTokens(List<String> contacts) {
|
||||
this.contacts = contacts;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
public class CryptoEncodingException extends Exception {
|
||||
|
||||
public CryptoEncodingException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public CryptoEncodingException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
public class DeviceResponse {
|
||||
|
||||
@JsonProperty
|
||||
private long deviceId;
|
||||
|
||||
@VisibleForTesting
|
||||
public DeviceResponse() {}
|
||||
|
||||
public DeviceResponse(long deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
}
|
||||
@@ -41,27 +41,30 @@ 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 IOException {
|
||||
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 EncryptedOutgoingMessage(String serialized) {
|
||||
this.serialized = serialized;
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
|
||||
throws IOException
|
||||
throws CryptoEncodingException
|
||||
{
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
@@ -85,31 +88,39 @@ public class EncryptedOutgoingMessage {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Invalid Key", e);
|
||||
throw new IOException("Invalid key!");
|
||||
throw new CryptoEncodingException("Invalid key!");
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
||||
private SecretKeySpec getCipherKey(String signalingKey) throws CryptoEncodingException {
|
||||
try {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
||||
|
||||
if (signalingKeyBytes.length < CIPHER_KEY_SIZE)
|
||||
throw new IOException("Signaling key too short!");
|
||||
if (signalingKeyBytes.length < CIPHER_KEY_SIZE)
|
||||
throw new CryptoEncodingException("Signaling key too short!");
|
||||
|
||||
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
||||
return new SecretKeySpec(cipherKey, "AES");
|
||||
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
||||
return new SecretKeySpec(cipherKey, "AES");
|
||||
} catch (IOException e) {
|
||||
throw new CryptoEncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] macKey = new byte[MAC_KEY_SIZE];
|
||||
private SecretKeySpec getMacKey(String signalingKey) throws CryptoEncodingException {
|
||||
try {
|
||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||
byte[] macKey = new byte[MAC_KEY_SIZE];
|
||||
|
||||
if (signalingKeyBytes.length < CIPHER_KEY_SIZE + MAC_KEY_SIZE)
|
||||
throw new IOException(("Signaling key too short!"));
|
||||
if (signalingKeyBytes.length < CIPHER_KEY_SIZE + MAC_KEY_SIZE)
|
||||
throw new CryptoEncodingException("Signaling key too short!");
|
||||
|
||||
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
||||
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
||||
|
||||
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||
} catch (IOException e) {
|
||||
throw new CryptoEncodingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,9 +25,14 @@ public class IncomingMessage {
|
||||
private int type;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String destination;
|
||||
|
||||
@JsonProperty
|
||||
private long destinationDeviceId = 1;
|
||||
|
||||
@JsonProperty
|
||||
private int destinationRegistrationId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String body;
|
||||
@@ -38,6 +43,7 @@ public class IncomingMessage {
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
@@ -53,4 +59,12 @@ public class IncomingMessage {
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
|
||||
public long getDestinationDeviceId() {
|
||||
return destinationDeviceId;
|
||||
}
|
||||
|
||||
public int getDestinationRegistrationId() {
|
||||
return destinationRegistrationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,20 @@ public class IncomingMessageList {
|
||||
@Valid
|
||||
private List<IncomingMessage> messages;
|
||||
|
||||
@JsonProperty
|
||||
private String relay;
|
||||
|
||||
public IncomingMessageList() {}
|
||||
|
||||
public List<IncomingMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
|
||||
public void setRelay(String relay) {
|
||||
this.relay = relay;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class IncomingWebsocketMessage {
|
||||
|
||||
public static final int TYPE_ACKNOWLEDGE_MESSAGE = 1;
|
||||
public static final int TYPE_PING_MESSAGE = 2;
|
||||
public static final int TYPE_PONG_MESSAGE = 3;
|
||||
|
||||
@JsonProperty
|
||||
protected int type;
|
||||
|
||||
public IncomingWebsocketMessage() {}
|
||||
|
||||
public IncomingWebsocketMessage(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,14 @@ public final class MessageProtos {
|
||||
boolean hasSource();
|
||||
String getSource();
|
||||
|
||||
// optional uint32 sourceDevice = 7;
|
||||
boolean hasSourceDevice();
|
||||
int getSourceDevice();
|
||||
|
||||
// optional string relay = 3;
|
||||
boolean hasRelay();
|
||||
String getRelay();
|
||||
|
||||
// repeated string destinations = 4;
|
||||
java.util.List<String> getDestinationsList();
|
||||
int getDestinationsCount();
|
||||
String getDestinations(int index);
|
||||
|
||||
// optional uint64 timestamp = 5;
|
||||
boolean hasTimestamp();
|
||||
long getTimestamp();
|
||||
@@ -107,11 +106,21 @@ public final class MessageProtos {
|
||||
}
|
||||
}
|
||||
|
||||
// optional uint32 sourceDevice = 7;
|
||||
public static final int SOURCEDEVICE_FIELD_NUMBER = 7;
|
||||
private int sourceDevice_;
|
||||
public boolean hasSourceDevice() {
|
||||
return ((bitField0_ & 0x00000004) == 0x00000004);
|
||||
}
|
||||
public int getSourceDevice() {
|
||||
return sourceDevice_;
|
||||
}
|
||||
|
||||
// optional string relay = 3;
|
||||
public static final int RELAY_FIELD_NUMBER = 3;
|
||||
private java.lang.Object relay_;
|
||||
public boolean hasRelay() {
|
||||
return ((bitField0_ & 0x00000004) == 0x00000004);
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
}
|
||||
public String getRelay() {
|
||||
java.lang.Object ref = relay_;
|
||||
@@ -139,25 +148,11 @@ public final class MessageProtos {
|
||||
}
|
||||
}
|
||||
|
||||
// repeated string destinations = 4;
|
||||
public static final int DESTINATIONS_FIELD_NUMBER = 4;
|
||||
private com.google.protobuf.LazyStringList destinations_;
|
||||
public java.util.List<String>
|
||||
getDestinationsList() {
|
||||
return destinations_;
|
||||
}
|
||||
public int getDestinationsCount() {
|
||||
return destinations_.size();
|
||||
}
|
||||
public String getDestinations(int index) {
|
||||
return destinations_.get(index);
|
||||
}
|
||||
|
||||
// optional uint64 timestamp = 5;
|
||||
public static final int TIMESTAMP_FIELD_NUMBER = 5;
|
||||
private long timestamp_;
|
||||
public boolean hasTimestamp() {
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
}
|
||||
public long getTimestamp() {
|
||||
return timestamp_;
|
||||
@@ -167,7 +162,7 @@ public final class MessageProtos {
|
||||
public static final int MESSAGE_FIELD_NUMBER = 6;
|
||||
private com.google.protobuf.ByteString message_;
|
||||
public boolean hasMessage() {
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||
}
|
||||
public com.google.protobuf.ByteString getMessage() {
|
||||
return message_;
|
||||
@@ -176,8 +171,8 @@ public final class MessageProtos {
|
||||
private void initFields() {
|
||||
type_ = 0;
|
||||
source_ = "";
|
||||
sourceDevice_ = 0;
|
||||
relay_ = "";
|
||||
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
|
||||
timestamp_ = 0L;
|
||||
message_ = com.google.protobuf.ByteString.EMPTY;
|
||||
}
|
||||
@@ -199,18 +194,18 @@ public final class MessageProtos {
|
||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
||||
output.writeBytes(2, getSourceBytes());
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
output.writeBytes(3, getRelayBytes());
|
||||
}
|
||||
for (int i = 0; i < destinations_.size(); i++) {
|
||||
output.writeBytes(4, destinations_.getByteString(i));
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
output.writeUInt64(5, timestamp_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
output.writeBytes(6, message_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
output.writeUInt32(7, sourceDevice_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@@ -228,27 +223,22 @@ public final class MessageProtos {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(2, getSourceBytes());
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(3, getRelayBytes());
|
||||
}
|
||||
{
|
||||
int dataSize = 0;
|
||||
for (int i = 0; i < destinations_.size(); i++) {
|
||||
dataSize += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSizeNoTag(destinations_.getByteString(i));
|
||||
}
|
||||
size += dataSize;
|
||||
size += 1 * getDestinationsList().size();
|
||||
}
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt64Size(5, timestamp_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(6, message_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(7, sourceDevice_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@@ -377,9 +367,9 @@ public final class MessageProtos {
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
source_ = "";
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
relay_ = "";
|
||||
sourceDevice_ = 0;
|
||||
bitField0_ = (bitField0_ & ~0x00000004);
|
||||
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
|
||||
relay_ = "";
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
timestamp_ = 0L;
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
@@ -434,19 +424,17 @@ public final class MessageProtos {
|
||||
if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
to_bitField0_ |= 0x00000004;
|
||||
}
|
||||
result.relay_ = relay_;
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
destinations_ = new com.google.protobuf.UnmodifiableLazyStringList(
|
||||
destinations_);
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
}
|
||||
result.destinations_ = destinations_;
|
||||
if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
result.sourceDevice_ = sourceDevice_;
|
||||
if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
to_bitField0_ |= 0x00000008;
|
||||
}
|
||||
result.relay_ = relay_;
|
||||
if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
to_bitField0_ |= 0x00000010;
|
||||
}
|
||||
result.timestamp_ = timestamp_;
|
||||
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
to_bitField0_ |= 0x00000010;
|
||||
to_bitField0_ |= 0x00000020;
|
||||
}
|
||||
result.message_ = message_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
@@ -471,19 +459,12 @@ public final class MessageProtos {
|
||||
if (other.hasSource()) {
|
||||
setSource(other.getSource());
|
||||
}
|
||||
if (other.hasSourceDevice()) {
|
||||
setSourceDevice(other.getSourceDevice());
|
||||
}
|
||||
if (other.hasRelay()) {
|
||||
setRelay(other.getRelay());
|
||||
}
|
||||
if (!other.destinations_.isEmpty()) {
|
||||
if (destinations_.isEmpty()) {
|
||||
destinations_ = other.destinations_;
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
} else {
|
||||
ensureDestinationsIsMutable();
|
||||
destinations_.addAll(other.destinations_);
|
||||
}
|
||||
onChanged();
|
||||
}
|
||||
if (other.hasTimestamp()) {
|
||||
setTimestamp(other.getTimestamp());
|
||||
}
|
||||
@@ -532,15 +513,10 @@ public final class MessageProtos {
|
||||
break;
|
||||
}
|
||||
case 26: {
|
||||
bitField0_ |= 0x00000004;
|
||||
bitField0_ |= 0x00000008;
|
||||
relay_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 34: {
|
||||
ensureDestinationsIsMutable();
|
||||
destinations_.add(input.readBytes());
|
||||
break;
|
||||
}
|
||||
case 40: {
|
||||
bitField0_ |= 0x00000010;
|
||||
timestamp_ = input.readUInt64();
|
||||
@@ -551,6 +527,11 @@ public final class MessageProtos {
|
||||
message_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 56: {
|
||||
bitField0_ |= 0x00000004;
|
||||
sourceDevice_ = input.readUInt32();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -614,10 +595,31 @@ public final class MessageProtos {
|
||||
onChanged();
|
||||
}
|
||||
|
||||
// optional uint32 sourceDevice = 7;
|
||||
private int sourceDevice_ ;
|
||||
public boolean hasSourceDevice() {
|
||||
return ((bitField0_ & 0x00000004) == 0x00000004);
|
||||
}
|
||||
public int getSourceDevice() {
|
||||
return sourceDevice_;
|
||||
}
|
||||
public Builder setSourceDevice(int value) {
|
||||
bitField0_ |= 0x00000004;
|
||||
sourceDevice_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder clearSourceDevice() {
|
||||
bitField0_ = (bitField0_ & ~0x00000004);
|
||||
sourceDevice_ = 0;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// optional string relay = 3;
|
||||
private java.lang.Object relay_ = "";
|
||||
public boolean hasRelay() {
|
||||
return ((bitField0_ & 0x00000004) == 0x00000004);
|
||||
return ((bitField0_ & 0x00000008) == 0x00000008);
|
||||
}
|
||||
public String getRelay() {
|
||||
java.lang.Object ref = relay_;
|
||||
@@ -633,79 +635,23 @@ public final class MessageProtos {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000004;
|
||||
bitField0_ |= 0x00000008;
|
||||
relay_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder clearRelay() {
|
||||
bitField0_ = (bitField0_ & ~0x00000004);
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
relay_ = getDefaultInstance().getRelay();
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
void setRelay(com.google.protobuf.ByteString value) {
|
||||
bitField0_ |= 0x00000004;
|
||||
bitField0_ |= 0x00000008;
|
||||
relay_ = value;
|
||||
onChanged();
|
||||
}
|
||||
|
||||
// repeated string destinations = 4;
|
||||
private com.google.protobuf.LazyStringList destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
|
||||
private void ensureDestinationsIsMutable() {
|
||||
if (!((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
destinations_ = new com.google.protobuf.LazyStringArrayList(destinations_);
|
||||
bitField0_ |= 0x00000008;
|
||||
}
|
||||
}
|
||||
public java.util.List<String>
|
||||
getDestinationsList() {
|
||||
return java.util.Collections.unmodifiableList(destinations_);
|
||||
}
|
||||
public int getDestinationsCount() {
|
||||
return destinations_.size();
|
||||
}
|
||||
public String getDestinations(int index) {
|
||||
return destinations_.get(index);
|
||||
}
|
||||
public Builder setDestinations(
|
||||
int index, String value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
ensureDestinationsIsMutable();
|
||||
destinations_.set(index, value);
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder addDestinations(String value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
ensureDestinationsIsMutable();
|
||||
destinations_.add(value);
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder addAllDestinations(
|
||||
java.lang.Iterable<String> values) {
|
||||
ensureDestinationsIsMutable();
|
||||
super.addAll(values, destinations_);
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder clearDestinations() {
|
||||
destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
void addDestinations(com.google.protobuf.ByteString value) {
|
||||
ensureDestinationsIsMutable();
|
||||
destinations_.add(value);
|
||||
onChanged();
|
||||
}
|
||||
|
||||
// optional uint64 timestamp = 5;
|
||||
private long timestamp_ ;
|
||||
public boolean hasTimestamp() {
|
||||
@@ -778,8 +724,8 @@ public final class MessageProtos {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n\033OutgoingMessageSignal.proto\022\ntextsecur" +
|
||||
"e\"~\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(\r" +
|
||||
"\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022\024\n\014desti" +
|
||||
"nations\030\004 \003(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mess" +
|
||||
"\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceDevice\030\007 \001(\r\022\r" +
|
||||
"\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mess" +
|
||||
"age\030\006 \001(\014B:\n)org.whispersystems.textsecu" +
|
||||
"regcm.entitiesB\rMessageProtos"
|
||||
};
|
||||
@@ -793,7 +739,7 @@ public final class MessageProtos {
|
||||
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_textsecure_OutgoingMessageSignal_descriptor,
|
||||
new java.lang.String[] { "Type", "Source", "Relay", "Destinations", "Timestamp", "Message", },
|
||||
new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "Message", },
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class,
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class);
|
||||
return null;
|
||||
|
||||
@@ -16,15 +16,26 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MessageResponse {
|
||||
private List<String> success;
|
||||
private List<String> failure;
|
||||
private Set<String> missingDeviceIds;
|
||||
|
||||
public MessageResponse(List<String> success, List<String> failure) {
|
||||
this.success = success;
|
||||
this.failure = failure;
|
||||
this.success = success;
|
||||
this.failure = failure;
|
||||
this.missingDeviceIds = new HashSet<>();
|
||||
}
|
||||
|
||||
public MessageResponse(Set<String> missingDeviceIds) {
|
||||
this.success = new LinkedList<>();
|
||||
this.failure = new LinkedList<>(missingDeviceIds);
|
||||
this.missingDeviceIds = missingDeviceIds;
|
||||
}
|
||||
|
||||
public MessageResponse() {}
|
||||
@@ -33,8 +44,23 @@ public class MessageResponse {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(List<String> success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public List<String> getFailure() {
|
||||
return failure;
|
||||
}
|
||||
|
||||
public void setFailure(List<String> failure) {
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
public Set<String> getNumbersMissingDevices() {
|
||||
return missingDeviceIds;
|
||||
}
|
||||
|
||||
public void setNumbersMissingDevices(Set<String> numbersMissingDevices) {
|
||||
this.missingDeviceIds = numbersMissingDevices;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MismatchedDevices {
|
||||
|
||||
@JsonProperty
|
||||
public List<Long> missingDevices;
|
||||
|
||||
@JsonProperty
|
||||
public List<Long> extraDevices;
|
||||
|
||||
@VisibleForTesting
|
||||
public MismatchedDevices() {}
|
||||
|
||||
public MismatchedDevices(List<Long> missingDevices, List<Long> extraDevices) {
|
||||
this.missingDevices = missingDevices;
|
||||
this.extraDevices = extraDevices;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyResponseV1 {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private List<PreKeyV1> keys;
|
||||
|
||||
@VisibleForTesting
|
||||
public PreKeyResponseV1() {}
|
||||
|
||||
public PreKeyResponseV1(PreKeyV1 preKey) {
|
||||
this.keys = new LinkedList<>();
|
||||
this.keys.add(preKey);
|
||||
}
|
||||
|
||||
public PreKeyResponseV1(List<PreKeyV1> preKeys) {
|
||||
this.keys = preKeys;
|
||||
}
|
||||
|
||||
public List<PreKeyV1> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof PreKeyResponseV1) ||
|
||||
((PreKeyResponseV1) o).keys.size() != keys.size())
|
||||
return false;
|
||||
Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
|
||||
for (PreKeyV1 key : keys) {
|
||||
if (!otherKeys.next().equals(key))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
int ret = 0xFBA4C795 * keys.size();
|
||||
for (PreKeyV1 key : keys)
|
||||
ret ^= key.getPublicKey().hashCode();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -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,17 @@
|
||||
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 {
|
||||
public class PreKeyV1 implements PreKeyBase {
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
|
||||
@JsonIgnore
|
||||
private String number;
|
||||
@JsonProperty
|
||||
private long deviceId;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@@ -47,70 +42,55 @@ public class PreKey {
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private boolean lastResort;
|
||||
private int registrationId;
|
||||
|
||||
public PreKey() {}
|
||||
public PreKeyV1() {}
|
||||
|
||||
public PreKey(long id, String number, long keyId,
|
||||
String publicKey, String identityKey,
|
||||
boolean lastResort)
|
||||
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
|
||||
{
|
||||
this.id = id;
|
||||
this.number = number;
|
||||
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.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;
|
||||
public void setDeviceId(long deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
public boolean isLastResort() {
|
||||
return lastResort;
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public void setLastResort(boolean lastResort) {
|
||||
this.lastResort = lastResort;
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public void setRegistrationId(int registrationId) {
|
||||
this.registrationId = registrationId;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,10 @@ public class RelayMessage {
|
||||
@NotEmpty
|
||||
private String destination;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private long destinationDeviceId;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@@ -40,8 +44,9 @@ public class RelayMessage {
|
||||
|
||||
public RelayMessage() {}
|
||||
|
||||
public RelayMessage(String destination, byte[] outgoingMessageSignal) {
|
||||
public RelayMessage(String destination, long destinationDeviceId, byte[] outgoingMessageSignal) {
|
||||
this.destination = destination;
|
||||
this.destinationDeviceId = destinationDeviceId;
|
||||
this.outgoingMessageSignal = outgoingMessageSignal;
|
||||
}
|
||||
|
||||
@@ -49,6 +54,10 @@ public class RelayMessage {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public long getDestinationDeviceId() {
|
||||
return destinationDeviceId;
|
||||
}
|
||||
|
||||
public byte[] getOutgoingMessageSignal() {
|
||||
return outgoingMessageSignal;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class SignedPreKey extends PreKeyV2 implements Serializable {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String signature;
|
||||
|
||||
public SignedPreKey() {}
|
||||
|
||||
public SignedPreKey(long keyId, String publicKey, String signature) {
|
||||
super(keyId, publicKey);
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (object == null || !(object instanceof SignedPreKey)) return false;
|
||||
SignedPreKey that = (SignedPreKey) object;
|
||||
|
||||
if (signature == null) {
|
||||
return super.equals(object) && that.signature == null;
|
||||
} else {
|
||||
return super.equals(object) && this.signature.equals(that.signature);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (signature == null) {
|
||||
return super.hashCode();
|
||||
} else {
|
||||
return super.hashCode() ^ signature.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StaleDevices {
|
||||
|
||||
@JsonProperty
|
||||
private List<Long> staleDevices;
|
||||
|
||||
public StaleDevices() {}
|
||||
|
||||
public StaleDevices(List<Long> staleDevices) {
|
||||
this.staleDevices = staleDevices;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.whispersystems.textsecuregcm.federation;
|
||||
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.sun.jersey.api.client.Client;
|
||||
import com.sun.jersey.api.client.ClientHandlerException;
|
||||
import com.sun.jersey.api.client.ClientResponse;
|
||||
@@ -30,19 +31,20 @@ import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
import org.bouncycastle.openssl.PEMReader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -55,16 +57,18 @@ import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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/message";
|
||||
private static final String PREKEY_PATH = "/v1/federation/key/%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 final FederatedPeer peer;
|
||||
private final Client client;
|
||||
@@ -89,28 +93,63 @@ public class FederatedClient {
|
||||
WebResource resource = client.resource(peer.getUrl())
|
||||
.path(String.format(ATTACHMENT_URI_PATH, attachmentId));
|
||||
|
||||
return resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(AttachmentUri.class)
|
||||
.getLocation();
|
||||
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 response.getEntity(AttachmentUri.class).getLocation();
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("Bad URI", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public PreKey getKey(String destination) {
|
||||
public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination));
|
||||
return resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(PreKey.class);
|
||||
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)
|
||||
.get(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return Optional.of(response.getEntity(PreKeyResponseV1.class));
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("PreKey", e);
|
||||
return null;
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device));
|
||||
|
||||
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return Optional.of(response.getEntity(PreKeyResponseV2.class));
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("PreKey", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getUserCount() {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
|
||||
@@ -139,22 +178,18 @@ public class FederatedClient {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(String destination, OutgoingMessageSignal message)
|
||||
throws IOException, NoSuchUserException
|
||||
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH);
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination));
|
||||
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.entity(new RelayMessage(destination, message.toByteArray()))
|
||||
.entity(messages)
|
||||
.put(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() == 404) {
|
||||
throw new NoSuchUserException("No remote user: " + destination);
|
||||
}
|
||||
|
||||
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||
throw new IOException("Bad response: " + response.getStatus());
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
logger.warn("sendMessage", e);
|
||||
@@ -206,6 +241,19 @@ public class FederatedClient {
|
||||
}
|
||||
}
|
||||
|
||||
private Response clientResponseToResponse(ClientResponse r) {
|
||||
Response.ResponseBuilder rb = Response.status(r.getStatus());
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : r.getHeaders().entrySet()) {
|
||||
for (String value : entry.getValue()) {
|
||||
rb.header(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
rb.entity(r.getEntityInputStream());
|
||||
return rb.build();
|
||||
}
|
||||
|
||||
public String getPeerName() {
|
||||
return peer.getName();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.whispersystems.textsecuregcm.federation;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class NonLimitedAccount extends Account {
|
||||
|
||||
@JsonIgnore
|
||||
private final String number;
|
||||
|
||||
@JsonIgnore
|
||||
private final String relay;
|
||||
|
||||
@JsonIgnore
|
||||
private final long deviceId;
|
||||
|
||||
public NonLimitedAccount(String number, long deviceId, String relay) {
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.relay = relay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRateLimited() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getRelay() {
|
||||
return Optional.of(relay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Device> getAuthenticatedDevice() {
|
||||
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -31,6 +31,9 @@ public class RateLimiters {
|
||||
private final RateLimiter preKeysLimiter;
|
||||
private final RateLimiter messagesLimiter;
|
||||
|
||||
private final RateLimiter allocateDeviceLimiter;
|
||||
private final RateLimiter verifyDeviceLimiter;
|
||||
|
||||
public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) {
|
||||
this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination",
|
||||
config.getSmsDestination().getBucketSize(),
|
||||
@@ -59,6 +62,23 @@ public class RateLimiters {
|
||||
this.messagesLimiter = new RateLimiter(memcachedClient, "messages",
|
||||
config.getMessages().getBucketSize(),
|
||||
config.getMessages().getLeakRatePerMinute());
|
||||
|
||||
this.allocateDeviceLimiter = new RateLimiter(memcachedClient, "allocateDevice",
|
||||
config.getAllocateDevice().getBucketSize(),
|
||||
config.getAllocateDevice().getLeakRatePerMinute());
|
||||
|
||||
this.verifyDeviceLimiter = new RateLimiter(memcachedClient, "verifyDevice",
|
||||
config.getVerifyDevice().getBucketSize(),
|
||||
config.getVerifyDevice().getLeakRatePerMinute());
|
||||
|
||||
}
|
||||
|
||||
public RateLimiter getAllocateDeviceLimiter() {
|
||||
return allocateDeviceLimiter;
|
||||
}
|
||||
|
||||
public RateLimiter getVerifyDeviceLimiter() {
|
||||
return verifyDeviceLimiter;
|
||||
}
|
||||
|
||||
public RateLimiter getMessagesLimiter() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,23 +16,30 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import com.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.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
@@ -40,21 +47,31 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
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 final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter websocketMeter = metricRegistry.meter(name(getClass(), "websocket"));
|
||||
private final Meter pushMeter = metricRegistry.meter(name(getClass(), "push"));
|
||||
private final Meter failureMeter = metricRegistry.meter(name(getClass(), "failure"));
|
||||
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
|
||||
|
||||
private static final String MESSAGE_BODY = "m";
|
||||
|
||||
private final Optional<ApnsService> apnService;
|
||||
private final PubSubManager pubSubManager;
|
||||
private final StoredMessages storedMessages;
|
||||
|
||||
public APNSender(String apnCertificate, String apnKey)
|
||||
public APNSender(PubSubManager pubSubManager,
|
||||
StoredMessages storedMessages,
|
||||
String apnCertificate, String apnKey)
|
||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
||||
{
|
||||
this.pubSubManager = pubSubManager;
|
||||
this.storedMessages = storedMessages;
|
||||
|
||||
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
|
||||
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
|
||||
this.apnService = Optional.of(APNS.newService()
|
||||
@@ -65,31 +82,44 @@ public class APNSender {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(String registrationId, EncryptedOutgoingMessage message)
|
||||
throws IOException
|
||||
public void sendMessage(Account account, Device device,
|
||||
String registrationId, EncryptedOutgoingMessage message)
|
||||
throws TransientPushFailureException, NotPushRegisteredException
|
||||
{
|
||||
if (pubSubManager.publish(new WebsocketAddress(account.getId(), device.getId()),
|
||||
new PubSubMessage(PubSubMessage.TYPE_DELIVER, message.serialize())))
|
||||
{
|
||||
websocketMeter.mark();
|
||||
} else {
|
||||
storedMessages.insert(account.getId(), device.getId(), message.serialize());
|
||||
sendPush(registrationId, message.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPush(String registrationId, String message)
|
||||
throws TransientPushFailureException
|
||||
{
|
||||
try {
|
||||
if (!apnService.isPresent()) {
|
||||
failure.mark();
|
||||
throw new IOException("APN access not configured!");
|
||||
failureMeter.mark();
|
||||
throw new TransientPushFailureException("APN access not configured!");
|
||||
}
|
||||
|
||||
String payload = APNS.newPayload()
|
||||
.alertBody("Message!")
|
||||
.customField(MESSAGE_BODY, message.serialize())
|
||||
.customField(MESSAGE_BODY, message)
|
||||
.build();
|
||||
|
||||
logger.debug("APN Payload: " + payload);
|
||||
|
||||
apnService.get().push(registrationId, payload);
|
||||
success.mark();
|
||||
} catch (MalformedURLException mue) {
|
||||
throw new AssertionError(mue);
|
||||
pushMeter.mark();
|
||||
} catch (NetworkIOException nioe) {
|
||||
logger.warn("Network Error", nioe);
|
||||
failure.mark();
|
||||
throw new IOException("Error sending APN");
|
||||
failureMeter.mark();
|
||||
throw new TransientPushFailureException(nioe);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
|
||||
|
||||
@@ -16,22 +16,24 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.android.gcm.server.Constants;
|
||||
import com.google.android.gcm.server.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.controllers.NoSuchUserException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
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 MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(org.whispersystems.textsecuregcm.util.Constants.METRICS_NAME);
|
||||
private final Meter success = metricRegistry.meter(name(getClass(), "sent", "success"));
|
||||
private final Meter failure = metricRegistry.meter(name(getClass(), "sent", "failure"));
|
||||
|
||||
private final Sender sender;
|
||||
|
||||
@@ -40,24 +42,28 @@ public class GCMSender {
|
||||
}
|
||||
|
||||
public String sendMessage(String gcmRegistrationId, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws IOException, NoSuchUserException
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
Message gcmMessage = new Message.Builder().addData("type", "message")
|
||||
.addData("message", outgoingMessage.serialize())
|
||||
.build();
|
||||
try {
|
||||
Message gcmMessage = new Message.Builder().addData("type", "message")
|
||||
.addData("message", outgoingMessage.serialize())
|
||||
.build();
|
||||
|
||||
Result result = sender.send(gcmMessage, gcmRegistrationId, 5);
|
||||
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 NoSuchUserException("User no longer registered with GCM.");
|
||||
if (result.getMessageId() != null) {
|
||||
success.mark();
|
||||
return result.getCanonicalRegistrationId();
|
||||
} else {
|
||||
throw new IOException("GCM Failed: " + result.getErrorCodeName());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
public class NotPushRegisteredException extends Exception {
|
||||
public NotPushRegisteredException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public NotPushRegisteredException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,18 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
|
||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStoreException;
|
||||
@@ -37,67 +38,86 @@ public class PushSender {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
|
||||
|
||||
private final AccountsManager accounts;
|
||||
private final DirectoryManager directory;
|
||||
|
||||
private final GCMSender gcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final AccountsManager accounts;
|
||||
private final GCMSender gcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final WebsocketSender webSocketSender;
|
||||
|
||||
public PushSender(GcmConfiguration gcmConfiguration,
|
||||
ApnConfiguration apnConfiguration,
|
||||
AccountsManager accounts,
|
||||
DirectoryManager directory)
|
||||
StoredMessages storedMessages,
|
||||
PubSubManager pubSubManager,
|
||||
AccountsManager accounts)
|
||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
||||
{
|
||||
this.accounts = accounts;
|
||||
this.directory = directory;
|
||||
|
||||
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
|
||||
this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey());
|
||||
this.accounts = accounts;
|
||||
this.webSocketSender = new WebsocketSender(storedMessages, pubSubManager);
|
||||
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
|
||||
this.apnSender = new APNSender(pubSubManager, storedMessages,
|
||||
apnConfiguration.getCertificate(),
|
||||
apnConfiguration.getKey());
|
||||
}
|
||||
|
||||
public void sendMessage(String destination, MessageProtos.OutgoingMessageSignal outgoingMessage)
|
||||
throws IOException, NoSuchUserException
|
||||
{
|
||||
Optional<Account> account = accounts.get(destination);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
directory.remove(destination);
|
||||
throw new NoSuchUserException("No such local destination: " + destination);
|
||||
}
|
||||
|
||||
String signalingKey = account.get().getSignalingKey();
|
||||
EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey);
|
||||
|
||||
if (account.get().getGcmRegistrationId() != null) sendGcmMessage(account.get(), message);
|
||||
else if (account.get().getApnRegistrationId() != null) sendApnMessage(account.get(), message);
|
||||
else throw new NoSuchUserException("No push identifier!");
|
||||
}
|
||||
|
||||
private void sendGcmMessage(Account account, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws IOException, NoSuchUserException
|
||||
public void sendMessage(Account account, Device device, MessageProtos.OutgoingMessageSignal message)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
try {
|
||||
String canonicalId = gcmSender.sendMessage(account.getGcmRegistrationId(),
|
||||
outgoingMessage);
|
||||
String signalingKey = device.getSignalingKey();
|
||||
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, signalingKey);
|
||||
|
||||
sendMessage(account, device, encryptedMessage);
|
||||
} catch (CryptoEncodingException e) {
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(Account account, Device device, EncryptedOutgoingMessage message)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
|
||||
else if (device.getApnId() != null) sendApnMessage(account, device, message);
|
||||
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message);
|
||||
else throw new NotPushRegisteredException("No delivery possible!");
|
||||
}
|
||||
|
||||
private void sendGcmMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
try {
|
||||
String canonicalId = gcmSender.sendMessage(device.getGcmId(), outgoingMessage);
|
||||
|
||||
if (canonicalId != null) {
|
||||
account.setGcmRegistrationId(canonicalId);
|
||||
device.setGcmId(canonicalId);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
} catch (NoSuchUserException e) {
|
||||
} catch (NotPushRegisteredException e) {
|
||||
logger.debug("No Such User", e);
|
||||
account.setGcmRegistrationId(null);
|
||||
device.setGcmId(null);
|
||||
accounts.update(account);
|
||||
throw new NoSuchUserException("User no longer exists in GCM.");
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendApnMessage(Account account, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws IOException
|
||||
private void sendApnMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws TransientPushFailureException, NotPushRegisteredException
|
||||
{
|
||||
apnSender.sendMessage(account.getApnRegistrationId(), outgoingMessage);
|
||||
try {
|
||||
apnSender.sendMessage(account, device, device.getApnId(), outgoingMessage);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
device.setApnId(null);
|
||||
accounts.update(account);
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendWebSocketMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws NotPushRegisteredException
|
||||
{
|
||||
try {
|
||||
webSocketSender.sendMessage(account, device, outgoingMessage);
|
||||
} catch (CryptoEncodingException e) {
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
public class TransientPushFailureException extends Exception {
|
||||
public TransientPushFailureException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public TransientPushFailureException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class WebsocketSender {
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter onlineMeter = metricRegistry.meter(name(getClass(), "online"));
|
||||
private final Meter offlineMeter = metricRegistry.meter(name(getClass(), "offline"));
|
||||
|
||||
private final StoredMessages storedMessages;
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
public WebsocketSender(StoredMessages storedMessages, PubSubManager pubSubManager) {
|
||||
this.storedMessages = storedMessages;
|
||||
this.pubSubManager = pubSubManager;
|
||||
}
|
||||
|
||||
public void sendMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
|
||||
throws CryptoEncodingException
|
||||
{
|
||||
sendMessage(account, device, outgoingMessage.serialize());
|
||||
}
|
||||
|
||||
private void sendMessage(Account account, Device device, String serializedMessage) {
|
||||
WebsocketAddress address = new WebsocketAddress(account.getId(), device.getId());
|
||||
PubSubMessage pubSubMessage = new PubSubMessage(PubSubMessage.TYPE_DELIVER, serializedMessage);
|
||||
|
||||
if (pubSubManager.publish(address, pubSubMessage)) {
|
||||
onlineMeter.mark();
|
||||
} else {
|
||||
offlineMeter.mark();
|
||||
storedMessages.insert(account.getId(), device.getId(), serializedMessage);
|
||||
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,13 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.sms;
|
||||
|
||||
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;
|
||||
|
||||
@@ -17,53 +17,60 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class Account implements Serializable {
|
||||
|
||||
public static final int MEMCACHE_VERION = 1;
|
||||
public static final int MEMCACHE_VERION = 4;
|
||||
|
||||
private long id;
|
||||
private String number;
|
||||
private String hashedAuthenticationToken;
|
||||
private String salt;
|
||||
private String signalingKey;
|
||||
private String gcmRegistrationId;
|
||||
private String apnRegistrationId;
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
private boolean supportsSms;
|
||||
|
||||
@JsonProperty
|
||||
private List<Device> devices = new LinkedList<>();
|
||||
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonIgnore
|
||||
private Optional<Device> authenticatedDevice;
|
||||
|
||||
public Account() {}
|
||||
|
||||
public Account(long id, String number, String hashedAuthenticationToken, String salt,
|
||||
String signalingKey, String gcmRegistrationId, String apnRegistrationId,
|
||||
boolean supportsSms)
|
||||
{
|
||||
this.id = id;
|
||||
this.number = number;
|
||||
this.hashedAuthenticationToken = hashedAuthenticationToken;
|
||||
this.salt = salt;
|
||||
this.signalingKey = signalingKey;
|
||||
this.gcmRegistrationId = gcmRegistrationId;
|
||||
this.apnRegistrationId = apnRegistrationId;
|
||||
this.supportsSms = supportsSms;
|
||||
@VisibleForTesting
|
||||
public Account(String number, boolean supportsSms, List<Device> devices) {
|
||||
this.number = number;
|
||||
this.supportsSms = supportsSms;
|
||||
this.devices = devices;
|
||||
}
|
||||
|
||||
public String getApnRegistrationId() {
|
||||
return apnRegistrationId;
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setApnRegistrationId(String apnRegistrationId) {
|
||||
this.apnRegistrationId = apnRegistrationId;
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getGcmRegistrationId() {
|
||||
return gcmRegistrationId;
|
||||
public Optional<Device> getAuthenticatedDevice() {
|
||||
return authenticatedDevice;
|
||||
}
|
||||
|
||||
public void setGcmRegistrationId(String gcmRegistrationId) {
|
||||
this.gcmRegistrationId = gcmRegistrationId;
|
||||
public void setAuthenticatedDevice(Device device) {
|
||||
this.authenticatedDevice = Optional.of(device);
|
||||
}
|
||||
|
||||
public void setNumber(String number) {
|
||||
@@ -74,23 +81,6 @@ public class Account implements Serializable {
|
||||
return number;
|
||||
}
|
||||
|
||||
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
|
||||
this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken();
|
||||
this.salt = credentials.getSalt();
|
||||
}
|
||||
|
||||
public AuthenticationCredentials getAuthenticationCredentials() {
|
||||
return new AuthenticationCredentials(hashedAuthenticationToken, salt);
|
||||
}
|
||||
|
||||
public String getSignalingKey() {
|
||||
return signalingKey;
|
||||
}
|
||||
|
||||
public void setSignalingKey(String signalingKey) {
|
||||
this.signalingKey = signalingKey;
|
||||
}
|
||||
|
||||
public boolean getSupportsSms() {
|
||||
return supportsSms;
|
||||
}
|
||||
@@ -99,11 +89,63 @@ public class Account implements Serializable {
|
||||
this.supportsSms = supportsSms;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
public void addDevice(Device device) {
|
||||
this.devices.add(device);
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
public void setDevices(List<Device> devices) {
|
||||
this.devices = devices;
|
||||
}
|
||||
|
||||
public List<Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
public Optional<Device> getMasterDevice() {
|
||||
return getDevice(Device.MASTER_ID);
|
||||
}
|
||||
|
||||
public Optional<Device> getDevice(long deviceId) {
|
||||
for (Device device : devices) {
|
||||
if (device.getId() == deviceId) {
|
||||
return Optional.of(device);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return
|
||||
getMasterDevice().isPresent() &&
|
||||
getMasterDevice().get().isActive();
|
||||
}
|
||||
|
||||
public long getNextDeviceId() {
|
||||
long highestDevice = Device.MASTER_ID;
|
||||
|
||||
for (Device device : devices) {
|
||||
if (device.getId() > highestDevice) {
|
||||
highestDevice = device.getId();
|
||||
}
|
||||
}
|
||||
|
||||
return highestDevice + 1;
|
||||
}
|
||||
|
||||
public boolean isRateLimited() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public Optional<String> getRelay() {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public void setIdentityKey(String identityKey) {
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public String getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.skife.jdbi.v2.SQLStatement;
|
||||
import org.skife.jdbi.v2.StatementContext;
|
||||
import org.skife.jdbi.v2.TransactionIsolationLevel;
|
||||
@@ -30,6 +34,7 @@ import org.skife.jdbi.v2.sqlobject.Transaction;
|
||||
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
||||
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
@@ -42,36 +47,32 @@ import java.util.List;
|
||||
|
||||
public abstract class Accounts {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String NUMBER = "number";
|
||||
public static final String AUTH_TOKEN = "auth_token";
|
||||
public static final String SALT = "salt";
|
||||
public static final String SIGNALING_KEY = "signaling_key";
|
||||
public static final String GCM_ID = "gcm_id";
|
||||
public static final String APN_ID = "apn_id";
|
||||
public static final String SUPPORTS_SMS = "supports_sms";
|
||||
private static final String ID = "id";
|
||||
private static final String NUMBER = "number";
|
||||
private static final String DATA = "data";
|
||||
|
||||
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + AUTH_TOKEN + ", " +
|
||||
SALT + ", " + SIGNALING_KEY + ", " + GCM_ID + ", " +
|
||||
APN_ID + ", " + SUPPORTS_SMS + ") " +
|
||||
"VALUES (:number, :auth_token, :salt, :signaling_key, :gcm_id, :apn_id, :supports_sms)")
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
}
|
||||
|
||||
@SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DATA + ") VALUES (:number, CAST(:data AS json))")
|
||||
@GetGeneratedKeys
|
||||
abstract long createStep(@AccountBinder Account account);
|
||||
abstract long insertStep(@AccountBinder Account account);
|
||||
|
||||
@SqlUpdate("DELETE FROM accounts WHERE number = :number")
|
||||
abstract void removeStep(@Bind("number") String number);
|
||||
@SqlUpdate("DELETE FROM accounts WHERE " + NUMBER + " = :number")
|
||||
abstract void removeAccount(@Bind("number") String number);
|
||||
|
||||
@SqlUpdate("UPDATE accounts SET " + AUTH_TOKEN + " = :auth_token, " + SALT + " = :salt, " +
|
||||
SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " +
|
||||
APN_ID + " = :apn_id, " + SUPPORTS_SMS + " = :supports_sms " +
|
||||
"WHERE " + NUMBER + " = :number")
|
||||
@SqlUpdate("UPDATE accounts SET " + DATA + " = CAST(:data AS json) WHERE " + NUMBER + " = :number")
|
||||
abstract void update(@AccountBinder Account account);
|
||||
|
||||
@Mapper(AccountMapper.class)
|
||||
@SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number")
|
||||
abstract Account get(@Bind("number") String number);
|
||||
|
||||
@SqlQuery("SELECT COUNT(*) from accounts")
|
||||
@SqlQuery("SELECT COUNT(DISTINCT " + NUMBER + ") from accounts")
|
||||
abstract long getCount();
|
||||
|
||||
@Mapper(AccountMapper.class)
|
||||
@@ -80,25 +81,27 @@ public abstract class Accounts {
|
||||
|
||||
@Mapper(AccountMapper.class)
|
||||
@SqlQuery("SELECT * FROM accounts")
|
||||
abstract Iterator<Account> getAll();
|
||||
public abstract Iterator<Account> getAll();
|
||||
|
||||
@Transaction(TransactionIsolationLevel.REPEATABLE_READ)
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public long create(Account account) {
|
||||
removeStep(account.getNumber());
|
||||
return createStep(account);
|
||||
removeAccount(account.getNumber());
|
||||
return insertStep(account);
|
||||
}
|
||||
|
||||
public static class AccountMapper implements ResultSetMapper<Account> {
|
||||
|
||||
@Override
|
||||
public Account map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||
throws SQLException
|
||||
{
|
||||
return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER),
|
||||
resultSet.getString(AUTH_TOKEN), resultSet.getString(SALT),
|
||||
resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID),
|
||||
resultSet.getString(APN_ID),
|
||||
resultSet.getInt(SUPPORTS_SMS) == 1);
|
||||
try {
|
||||
Account account = mapper.readValue(resultSet.getString(DATA), Account.class);
|
||||
account.setId(resultSet.getLong(ID));
|
||||
|
||||
return account;
|
||||
} catch (IOException e) {
|
||||
throw new SQLException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,15 +118,14 @@ public abstract class Accounts {
|
||||
AccountBinder accountBinder,
|
||||
Account account)
|
||||
{
|
||||
sql.bind(ID, account.getId());
|
||||
sql.bind(NUMBER, account.getNumber());
|
||||
sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials()
|
||||
.getHashedAuthenticationToken());
|
||||
sql.bind(SALT, account.getAuthenticationCredentials().getSalt());
|
||||
sql.bind(SIGNALING_KEY, account.getSignalingKey());
|
||||
sql.bind(GCM_ID, account.getGcmRegistrationId());
|
||||
sql.bind(APN_ID, account.getApnRegistrationId());
|
||||
sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0);
|
||||
try {
|
||||
String serialized = mapper.writeValueAsString(account);
|
||||
|
||||
sql.bind(NUMBER, account.getNumber());
|
||||
sql.bind(DATA, serialized);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,9 +53,7 @@ public class AccountsManager {
|
||||
}
|
||||
|
||||
public void create(Account account) {
|
||||
long id = accounts.create(account);
|
||||
|
||||
account.setId(id);
|
||||
accounts.create(account);
|
||||
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(getKey(account.getNumber()), 0, account);
|
||||
@@ -92,8 +90,15 @@ public class AccountsManager {
|
||||
else return Optional.absent();
|
||||
}
|
||||
|
||||
public boolean isRelayListed(String number) {
|
||||
byte[] token = Util.getContactToken(number);
|
||||
Optional<ClientContact> contact = directory.get(token);
|
||||
|
||||
return contact.isPresent() && !Util.isEmpty(contact.get().getRelay());
|
||||
}
|
||||
|
||||
private void updateDirectory(Account account) {
|
||||
if (account.getGcmRegistrationId() != null || account.getApnRegistrationId() != null) {
|
||||
if (account.isActive()) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
||||
directory.add(clientContact);
|
||||
@@ -105,4 +110,5 @@ public class AccountsManager {
|
||||
private String getKey(String number) {
|
||||
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Device implements Serializable {
|
||||
|
||||
public static final long MASTER_ID = 1;
|
||||
|
||||
@JsonProperty
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private String authToken;
|
||||
|
||||
@JsonProperty
|
||||
private String salt;
|
||||
|
||||
@JsonProperty
|
||||
private String signalingKey;
|
||||
|
||||
@JsonProperty
|
||||
private String gcmId;
|
||||
|
||||
@JsonProperty
|
||||
private String apnId;
|
||||
|
||||
@JsonProperty
|
||||
private boolean fetchesMessages;
|
||||
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
@JsonProperty
|
||||
private SignedPreKey signedPreKey;
|
||||
|
||||
public Device() {}
|
||||
|
||||
public Device(long id, String authToken, String salt,
|
||||
String signalingKey, String gcmId, String apnId,
|
||||
boolean fetchesMessages, int registrationId,
|
||||
SignedPreKey signedPreKey)
|
||||
{
|
||||
this.id = id;
|
||||
this.authToken = authToken;
|
||||
this.salt = salt;
|
||||
this.signalingKey = signalingKey;
|
||||
this.gcmId = gcmId;
|
||||
this.apnId = apnId;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.signedPreKey = signedPreKey;
|
||||
}
|
||||
|
||||
public String getApnId() {
|
||||
return apnId;
|
||||
}
|
||||
|
||||
public void setApnId(String apnId) {
|
||||
this.apnId = apnId;
|
||||
}
|
||||
|
||||
public String getGcmId() {
|
||||
return gcmId;
|
||||
}
|
||||
|
||||
public void setGcmId(String gcmId) {
|
||||
this.gcmId = gcmId;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
|
||||
this.authToken = credentials.getHashedAuthenticationToken();
|
||||
this.salt = credentials.getSalt();
|
||||
}
|
||||
|
||||
public AuthenticationCredentials getAuthenticationCredentials() {
|
||||
return new AuthenticationCredentials(authToken, salt);
|
||||
}
|
||||
|
||||
public String getSignalingKey() {
|
||||
return signalingKey;
|
||||
}
|
||||
|
||||
public void setSignalingKey(String signalingKey) {
|
||||
this.signalingKey = signalingKey;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
|
||||
}
|
||||
|
||||
public boolean getFetchesMessages() {
|
||||
return fetchesMessages;
|
||||
}
|
||||
|
||||
public void setFetchesMessages(boolean fetchesMessages) {
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
}
|
||||
|
||||
public boolean isMaster() {
|
||||
return getId() == MASTER_ID;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public void setRegistrationId(int registrationId) {
|
||||
this.registrationId = registrationId;
|
||||
}
|
||||
|
||||
public SignedPreKey getSignedPreKey() {
|
||||
return signedPreKey;
|
||||
}
|
||||
|
||||
public void setSignedPreKey(SignedPreKey signedPreKey) {
|
||||
this.signedPreKey = signedPreKey;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.util.IterablePair;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@@ -75,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();
|
||||
|
||||
@@ -110,7 +116,7 @@ public class DirectoryManager {
|
||||
|
||||
IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures);
|
||||
|
||||
for (IterablePair.Pair<byte[], Response<byte[]>> pair : lists) {
|
||||
for (Pair<byte[], Response<byte[]>> pair : lists) {
|
||||
if (pair.second().get() != null) {
|
||||
TokenValue tokenValue = new Gson().fromJson(new String(pair.second().get()), TokenValue.class);
|
||||
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms);
|
||||
@@ -161,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;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.skife.jdbi.v2.SQLStatement;
|
||||
import org.skife.jdbi.v2.StatementContext;
|
||||
import org.skife.jdbi.v2.TransactionIsolationLevel;
|
||||
@@ -29,7 +30,9 @@ 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.PreKeyBase;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
@@ -38,48 +41,77 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Keys {
|
||||
|
||||
@SqlUpdate("DELETE FROM keys WHERE number = :number")
|
||||
abstract void removeKeys(@Bind("number") String number);
|
||||
@SqlUpdate("DELETE FROM keys WHERE number = :number AND device_id = :device_id")
|
||||
abstract void removeKeys(@Bind("number") String number, @Bind("device_id") long deviceId);
|
||||
|
||||
@SqlUpdate("DELETE FROM keys WHERE id = :id")
|
||||
abstract void removeKey(@Bind("id") long id);
|
||||
|
||||
@SqlBatch("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
|
||||
abstract void append(@PreKeyBinder List<PreKey> preKeys);
|
||||
@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);
|
||||
|
||||
@SqlUpdate("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)")
|
||||
abstract void append(@PreKeyBinder PreKey preKey);
|
||||
|
||||
@SqlQuery("SELECT * FROM keys WHERE number = :number ORDER BY id LIMIT 1 FOR UPDATE")
|
||||
@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);
|
||||
abstract KeyRecord retrieveFirst(@Bind("number") String number, @Bind("device_id") long deviceId);
|
||||
|
||||
@SqlQuery("SELECT DISTINCT ON (number, device_id) * FROM keys WHERE number = :number ORDER BY number, device_id, key_id ASC")
|
||||
@Mapper(PreKeyMapper.class)
|
||||
abstract List<KeyRecord> retrieveFirst(@Bind("number") String number);
|
||||
|
||||
@SqlQuery("SELECT COUNT(*) FROM keys WHERE number = :number AND device_id = :device_id")
|
||||
public abstract int getCount(@Bind("number") String number, @Bind("device_id") long deviceId);
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public void store(String number, PreKey lastResortKey, List<PreKey> keys) {
|
||||
for (PreKey key : keys) {
|
||||
key.setNumber(number);
|
||||
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);
|
||||
records.add(new KeyRecord(0, number, deviceId, lastResortKey.getKeyId(),
|
||||
lastResortKey.getPublicKey(), true));
|
||||
|
||||
removeKeys(number);
|
||||
append(keys);
|
||||
append(lastResortKey);
|
||||
removeKeys(number, deviceId);
|
||||
append(records);
|
||||
}
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public PreKey get(String number) {
|
||||
PreKey preKey = retrieveFirst(number);
|
||||
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();
|
||||
}
|
||||
|
||||
return preKey;
|
||||
List<KeyRecord> results = new LinkedList<>();
|
||||
results.add(record);
|
||||
|
||||
return Optional.of(results);
|
||||
}
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public Optional<List<KeyRecord>> get(String number) {
|
||||
List<KeyRecord> preKeys = retrieveFirst(number);
|
||||
|
||||
if (preKeys != null) {
|
||||
for (KeyRecord preKey : preKeys) {
|
||||
if (!preKey.isLastResort()) {
|
||||
removeKey(preKey.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preKeys != null) return Optional.of(preKeys);
|
||||
else return Optional.absent();
|
||||
}
|
||||
|
||||
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
|
||||
@@ -89,16 +121,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("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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -106,15 +138,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("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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,6 @@ public interface PendingAccounts {
|
||||
@SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number")
|
||||
String getCodeForNumber(@Bind("number") String number);
|
||||
|
||||
@SqlUpdate("DELETE FROM pending_accounts WHERE number = :number")
|
||||
void remove(@Bind("number") String number);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ public class PendingAccountsManager {
|
||||
pendingAccounts.insert(number, code);
|
||||
}
|
||||
|
||||
public void remove(String number) {
|
||||
if (memcachedClient != null)
|
||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
||||
pendingAccounts.remove(number);
|
||||
}
|
||||
|
||||
public Optional<String> getCodeForNumber(String number) {
|
||||
String code = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import org.skife.jdbi.v2.sqlobject.Bind;
|
||||
import org.skife.jdbi.v2.sqlobject.SqlQuery;
|
||||
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
|
||||
|
||||
public interface PendingDevices {
|
||||
|
||||
@SqlUpdate("WITH upsert AS (UPDATE pending_devices SET verification_code = :verification_code WHERE number = :number RETURNING *) " +
|
||||
"INSERT INTO pending_devices (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)")
|
||||
void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode);
|
||||
|
||||
@SqlQuery("SELECT verification_code FROM pending_devices WHERE number = :number")
|
||||
String getCodeForNumber(@Bind("number") String number);
|
||||
|
||||
@SqlUpdate("DELETE FROM pending_devices WHERE number = :number")
|
||||
void remove(@Bind("number") String number);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import net.spy.memcached.MemcachedClient;
|
||||
|
||||
public class PendingDevicesManager {
|
||||
|
||||
private static final String MEMCACHE_PREFIX = "pending_devices";
|
||||
|
||||
private final PendingDevices pendingDevices;
|
||||
private final MemcachedClient memcachedClient;
|
||||
|
||||
public PendingDevicesManager(PendingDevices pendingDevices,
|
||||
MemcachedClient memcachedClient)
|
||||
{
|
||||
this.pendingDevices = pendingDevices;
|
||||
this.memcachedClient = memcachedClient;
|
||||
}
|
||||
|
||||
public void store(String number, String code) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
}
|
||||
|
||||
pendingDevices.insert(number, code);
|
||||
}
|
||||
|
||||
public void remove(String number) {
|
||||
if (memcachedClient != null) {
|
||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
||||
}
|
||||
|
||||
pendingDevices.remove(number);
|
||||
}
|
||||
|
||||
public Optional<String> getCodeForNumber(String number) {
|
||||
String code = null;
|
||||
|
||||
if (memcachedClient != null) {
|
||||
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
|
||||
}
|
||||
|
||||
if (code == null) {
|
||||
code = pendingDevices.getCodeForNumber(number);
|
||||
|
||||
if (code != null && memcachedClient != null) {
|
||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
||||
}
|
||||
}
|
||||
|
||||
if (code != null) return Optional.of(code);
|
||||
else return Optional.absent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
public interface PubSubListener {
|
||||
|
||||
public void onPubSubMessage(PubSubMessage outgoingMessage);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPubSub;
|
||||
|
||||
public class PubSubManager {
|
||||
|
||||
private 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 final JedisPool jedisPool;
|
||||
private boolean subscribed = false;
|
||||
|
||||
public PubSubManager(final JedisPool jedisPool) {
|
||||
this.jedisPool = jedisPool;
|
||||
initializePubSubWorker();
|
||||
waitForSubscription();
|
||||
}
|
||||
|
||||
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
|
||||
listeners.put(address, listener);
|
||||
baseListener.subscribe(address.toString());
|
||||
}
|
||||
|
||||
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
|
||||
if (listeners.get(address) == listener) {
|
||||
listeners.remove(address);
|
||||
baseListener.unsubscribe(address.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
|
||||
try {
|
||||
String serialized = mapper.writeValueAsString(message);
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
return jedis.publish(address.toString(), serialized) != 0;
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void waitForSubscription() {
|
||||
try {
|
||||
while (!subscribed) {
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePubSubWorker() {
|
||||
new Thread("PubSubListener") {
|
||||
@Override
|
||||
public void run() {
|
||||
for (;;) {
|
||||
Jedis jedis = null;
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
jedis.subscribe(baseListener, new WebsocketAddress(0, 0).toString());
|
||||
logger.warn("**** Unsubscribed from holding channel!!! ******");
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
new Thread("PubSubKeepAlive") {
|
||||
@Override
|
||||
public void run() {
|
||||
for (;;) {
|
||||
try {
|
||||
Thread.sleep(20000);
|
||||
publish(new WebsocketAddress(0, 0), new PubSubMessage(0, "foo"));
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private class SubscriptionListener extends JedisPubSub {
|
||||
|
||||
@Override
|
||||
public void onMessage(String channel, String message) {
|
||||
try {
|
||||
WebsocketAddress address = new WebsocketAddress(channel);
|
||||
PubSubListener listener;
|
||||
|
||||
synchronized (PubSubManager.this) {
|
||||
listener = listeners.get(address);
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onPubSubMessage(mapper.readValue(message, PubSubMessage.class));
|
||||
}
|
||||
} catch (InvalidWebsocketAddressException e) {
|
||||
logger.warn("Address", e);
|
||||
} catch (IOException e) {
|
||||
logger.warn("IOE", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPMessage(String s, String s2, String s3) {
|
||||
logger.warn("Received PMessage!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(String channel, int count) {
|
||||
try {
|
||||
WebsocketAddress address = new WebsocketAddress(channel);
|
||||
|
||||
if (address.getAccountId() == 0 && address.getDeviceId() == 0) {
|
||||
synchronized (PubSubManager.this) {
|
||||
subscribed = true;
|
||||
PubSubManager.this.notifyAll();
|
||||
}
|
||||
}
|
||||
} catch (InvalidWebsocketAddressException e) {
|
||||
logger.warn("Weird address", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnsubscribe(String s, int i) {}
|
||||
|
||||
@Override
|
||||
public void onPUnsubscribe(String s, int i) {}
|
||||
|
||||
@Override
|
||||
public void onPSubscribe(String s, int i) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class PubSubMessage {
|
||||
|
||||
public static final int TYPE_QUERY_DB = 1;
|
||||
public static final int TYPE_DELIVER = 2;
|
||||
|
||||
@JsonProperty
|
||||
private int type;
|
||||
|
||||
@JsonProperty
|
||||
private String contents;
|
||||
|
||||
public PubSubMessage() {}
|
||||
|
||||
public PubSubMessage(int type, String contents) {
|
||||
this.type = type;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getContents() {
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open WhisperSystems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
public class StoredMessages {
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
|
||||
|
||||
private static final String QUEUE_PREFIX = "msgs";
|
||||
|
||||
private final JedisPool jedisPool;
|
||||
|
||||
public StoredMessages(JedisPool jedisPool) {
|
||||
this.jedisPool = jedisPool;
|
||||
}
|
||||
|
||||
public void insert(long accountId, long deviceId, String message) {
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
|
||||
long queueSize = jedis.lpush(getKey(accountId, deviceId), message);
|
||||
queueSizeHistogram.update(queueSize);
|
||||
|
||||
if (queueSize > 1000) {
|
||||
jedis.ltrim(getKey(accountId, deviceId), 0, 999);
|
||||
}
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getMessagesForDevice(long accountId, long deviceId) {
|
||||
List<String> messages = new LinkedList<>();
|
||||
Jedis jedis = null;
|
||||
|
||||
try {
|
||||
jedis = jedisPool.getResource();
|
||||
String message;
|
||||
|
||||
while ((message = jedis.rpop(QUEUE_PREFIX + accountId + ":" + deviceId)) != null) {
|
||||
messages.add(message);
|
||||
}
|
||||
|
||||
return messages;
|
||||
} finally {
|
||||
if (jedis != null)
|
||||
jedisPool.returnResource(jedis);
|
||||
}
|
||||
}
|
||||
|
||||
private String getKey(long accountId, long deviceId) {
|
||||
return QUEUE_PREFIX + ":" + accountId + ":" + deviceId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1241,7 +1241,7 @@ public class Base64
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] decode( String s ) throws java.io.IOException {
|
||||
return decode( s, NO_OPTIONS );
|
||||
return decode( s, DONT_GUNZIP );
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package org.whispersystems.textsecuregcm.util;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class IterablePair <T1, T2> implements Iterable<IterablePair.Pair<T1,T2>> {
|
||||
public class IterablePair <T1, T2> implements Iterable<Pair<T1,T2>> {
|
||||
private final List<T1> first;
|
||||
private final List<T2> second;
|
||||
|
||||
@@ -33,24 +33,6 @@ public class IterablePair <T1, T2> implements Iterable<IterablePair.Pair<T1,T2>>
|
||||
return new ParallelIterator<>( first.iterator(), second.iterator() );
|
||||
}
|
||||
|
||||
public static class Pair<T1, T2> {
|
||||
private final T1 v1;
|
||||
private final T2 v2;
|
||||
|
||||
Pair(T1 v1, T2 v2) {
|
||||
this.v1 = v1;
|
||||
this.v2 = v2;
|
||||
}
|
||||
|
||||
public T1 first(){
|
||||
return v1;
|
||||
}
|
||||
|
||||
public T2 second(){
|
||||
return v2;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ParallelIterator <T1, T2> implements Iterator<Pair<T1, T2>> {
|
||||
|
||||
private final Iterator<T1> it1;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 static com.google.common.base.Objects.equal;
|
||||
|
||||
public class Pair<T1, T2> {
|
||||
private final T1 v1;
|
||||
private final T2 v2;
|
||||
|
||||
public Pair(T1 v1, T2 v2) {
|
||||
this.v1 = v1;
|
||||
this.v2 = v2;
|
||||
}
|
||||
|
||||
public T1 first(){
|
||||
return v1;
|
||||
}
|
||||
|
||||
public T2 second(){
|
||||
return v2;
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof Pair &&
|
||||
equal(((Pair) o).first(), first()) &&
|
||||
equal(((Pair) o).second(), second());
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return first().hashCode() ^ second().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,24 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
public class VerificationCode {
|
||||
|
||||
@JsonProperty
|
||||
private String verificationCode;
|
||||
@JsonIgnore
|
||||
private String verificationCodeDisplay;
|
||||
@JsonIgnore
|
||||
private String verificationCodeSpeech;
|
||||
|
||||
@VisibleForTesting VerificationCode() {}
|
||||
|
||||
public VerificationCode(int verificationCode) {
|
||||
this.verificationCode = verificationCode + "";
|
||||
this.verificationCodeDisplay = this.verificationCode.substring(0, 3) + "-" +
|
||||
@@ -54,4 +66,11 @@ public class VerificationCode {
|
||||
return delimited;
|
||||
}
|
||||
|
||||
@VisibleForTesting public boolean equals(Object o) {
|
||||
return o instanceof VerificationCode && verificationCode.equals(((VerificationCode) o).verificationCode);
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Integer.parseInt(verificationCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
public class InvalidWebsocketAddressException extends Exception {
|
||||
public InvalidWebsocketAddressException(String serialized) {
|
||||
super(serialized);
|
||||
}
|
||||
|
||||
public InvalidWebsocketAddressException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
public class WebsocketAddress {
|
||||
|
||||
private final long accountId;
|
||||
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;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public long getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return accountId + ":" + deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof WebsocketAddress)) return false;
|
||||
|
||||
WebsocketAddress that = (WebsocketAddress)other;
|
||||
|
||||
return
|
||||
this.accountId == that.accountId &&
|
||||
this.deviceId == that.deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int)accountId ^ (int)deviceId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.controllers.WebsocketController;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||
|
||||
|
||||
public class WebsocketControllerFactory extends WebSocketServlet implements WebSocketCreator {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WebsocketControllerFactory.class);
|
||||
|
||||
private final PushSender pushSender;
|
||||
private final StoredMessages storedMessages;
|
||||
private final PubSubManager pubSubManager;
|
||||
private final AccountAuthenticator accountAuthenticator;
|
||||
|
||||
public WebsocketControllerFactory(AccountAuthenticator accountAuthenticator,
|
||||
PushSender pushSender,
|
||||
StoredMessages storedMessages,
|
||||
PubSubManager pubSubManager)
|
||||
{
|
||||
this.accountAuthenticator = accountAuthenticator;
|
||||
this.pushSender = pushSender;
|
||||
this.storedMessages = storedMessages;
|
||||
this.pubSubManager = pubSubManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.setCreator(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object createWebSocket(UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse) {
|
||||
return new WebsocketController(accountAuthenticator, pushSender, pubSubManager, storedMessages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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,16 +16,11 @@
|
||||
*/
|
||||
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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
|
||||
@@ -34,10 +29,19 @@ 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> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class);
|
||||
|
||||
public DirectoryCommand() {
|
||||
super("directory", "Update directory from DB and peers.");
|
||||
}
|
||||
@@ -49,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());
|
||||
@@ -68,6 +72,9 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
|
||||
|
||||
update.updateFromLocalDatabase();
|
||||
update.updateFromPeers();
|
||||
} catch (Exception ex) {
|
||||
logger.warn("Directory Exception", ex);
|
||||
throw new RuntimeException(ex);
|
||||
} finally {
|
||||
Thread.sleep(3000);
|
||||
System.exit(0);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user