Compare commits

...

24 Commits
v0.24 ... v0.29

Author SHA1 Message Date
Moxie Marlinspike
5c1c80dad3 Bump version to 0.29
// FREEBIE
2015-01-03 19:43:46 -08:00
Moxie Marlinspike
32c0712715 Chunk local directory update queries.
// FREEBIE
2015-01-03 19:43:19 -08:00
Moxie Marlinspike
fa4e492d1c Get rid of GSON dependency.
// FREEBIE
2015-01-03 18:28:51 -08:00
Moxie Marlinspike
4711fa2a9a Bump version to 0.27
// FREEBIE
2015-01-03 17:34:44 -08:00
Moxie Marlinspike
08291502eb Expire in-memory queues after 30 days of inactivity.
// FREEBIE
2015-01-03 17:24:35 -08:00
Moxie Marlinspike
1f0acd0622 Don't warn on connection timeout exceptions.
// FREEBIE
2015-01-03 16:51:59 -08:00
Moxie Marlinspike
e88b732715 Add PaperTrail support.
// FREEBIE
2015-01-03 16:51:28 -08:00
Moxie Marlinspike
dafda85c36 Move JSON reporter to Dropwizard ReporterFactory structure. 2015-01-02 23:53:40 -08:00
Moxie Marlinspike
8441fa9687 Fix bugs associated with PubSub encoding.
// FREEBIE
2014-12-12 12:35:05 -08:00
Moxie Marlinspike
77800dfb01 Update websocket-resources.
// FREEBIE
2014-12-08 09:07:45 -08:00
Moxie Marlinspike
41d15b738b Refactor direct connect delivery pipeline and message store.
1) Make message store contents more memory efficient.

2) Make notification pipeline simpler and more memory efficient.

3) Don't b64 encode websocket message bodies.

// FREEBIE
2014-12-06 20:00:39 -08:00
Moxie Marlinspike
aa2a5ff929 Bump version to 0.26
// FREEBIE
2014-12-03 13:36:25 -08:00
Moxie Marlinspike
56d3c1e73f Turn down log levels.
// FREEBIE
2014-12-03 11:44:40 -08:00
Moxie Marlinspike
f401f9a674 Schedule at 1min instead of 10min.
// FREEBIE
2014-12-03 11:35:38 -08:00
Moxie Marlinspike
30933d792b Timestamp comparison should be the other way.
// FREEBIE
2014-12-03 11:33:34 -08:00
Moxie Marlinspike
905717977e Turn down logging on metrics reporter.
// FREEBIE
2014-12-03 11:09:37 -08:00
Moxie Marlinspike
b802994809 Do a timestamp comparison on unregister events.
// FREEBIE
2014-12-03 11:09:01 -08:00
Moxie Marlinspike
ac96f906b3 Bump version to 0.25
// FREEBIE
2014-12-02 15:37:40 -08:00
Moxie Marlinspike
cc395e914f Fix APN push payload.
// FREEBIE
2014-12-01 14:01:53 -08:00
Moxie Marlinspike
f8063f8faf Add feedback handler.
// FREEBIE
2014-12-01 13:27:06 -08:00
Moxie Marlinspike
958ada9110 Bump dropwizard version.
// FREEBIE
2014-12-01 12:10:14 -08:00
Moxie Marlinspike
3452ea29b8 Use push microservice instead of doing push directly.
// FREEBIE
2014-12-01 11:23:29 -08:00
Moxie Marlinspike
675b6f4b5e Update APN payload.
// FREEBIE
2014-11-27 18:20:23 -08:00
Moxie Marlinspike
4fab67b0f5 Switch to production APN endpoint.
// FREEBIE
2014-11-27 16:25:02 -08:00
45 changed files with 2246 additions and 1191 deletions

26
pom.xml
View File

@@ -9,10 +9,10 @@
<groupId>org.whispersystems.textsecure</groupId> <groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId> <artifactId>TextSecureServer</artifactId>
<version>0.24</version> <version>0.29</version>
<properties> <properties>
<dropwizard.version>0.7.0</dropwizard.version> <dropwizard.version>0.7.1</dropwizard.version>
<jackson.api.version>2.3.3</jackson.api.version> <jackson.api.version>2.3.3</jackson.api.version>
<commons-codec.version>1.6</commons-codec.version> <commons-codec.version>1.6</commons-codec.version>
</properties> </properties>
@@ -53,6 +53,17 @@
<artifactId>dropwizard-metrics-graphite</artifactId> <artifactId>dropwizard-metrics-graphite</artifactId>
<version>${dropwizard.version}</version> <version>${dropwizard.version}</version>
</dependency> </dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>com.dcsquare</groupId>
<artifactId>dropwizard-papertrail</artifactId>
<version>1.1</version>
</dependency>
<dependency> <dependency>
<groupId>com.sun.jersey</groupId> <groupId>com.sun.jersey</groupId>
@@ -80,11 +91,6 @@
<artifactId>gcm-server</artifactId> <artifactId>gcm-server</artifactId>
<version>1.0.2</version> <version>1.0.2</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.2</version>
</dependency>
<dependency> <dependency>
<groupId>net.spy</groupId> <groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId> <artifactId>spymemcached</artifactId>
@@ -110,7 +116,7 @@
<dependency> <dependency>
<groupId>redis.clients</groupId> <groupId>redis.clients</groupId>
<artifactId>jedis</artifactId> <artifactId>jedis</artifactId>
<version>2.2.1</version> <version>2.6.1</version>
<type>jar</type> <type>jar</type>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
@@ -131,9 +137,9 @@
<version>4.0.0</version> <version>4.0.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.whispersystems.websocket</groupId> <groupId>org.whispersystems</groupId>
<artifactId>websocket-resources</artifactId> <artifactId>websocket-resources</artifactId>
<version>0.1-SNAPSHOT</version> <version>0.2.0</version>
</dependency> </dependency>

View File

@@ -1,3 +1,3 @@
all: all:
protoc --java_out=../src/main/java/ OutgoingMessageSignal.proto protoc --java_out=../src/main/java/ OutgoingMessageSignal.proto PubSubMessage.proto StoredMessage.proto

View File

@@ -36,4 +36,4 @@ message OutgoingMessageSignal {
// repeated string destinations = 4; // repeated string destinations = 4;
optional uint64 timestamp = 5; optional uint64 timestamp = 5;
optional bytes message = 6; optional bytes message = 6;
} }

View File

@@ -0,0 +1,32 @@
/**
* 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 textsecure;
option java_package = "org.whispersystems.textsecuregcm.storage";
option java_outer_classname = "PubSubProtos";
message PubSubMessage {
enum Type {
UNKNOWN = 0;
QUERY_DB = 1;
DELIVER = 2;
KEEPALIVE = 3;
}
optional Type type = 1;
optional bytes content = 2;
}

View File

@@ -0,0 +1,30 @@
/**
* 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 textsecure;
option java_package = "org.whispersystems.textsecuregcm.storage";
option java_outer_classname = "StoredMessageProtos";
message StoredMessage {
enum Type {
UNKNOWN = 0;
MESSAGE = 1;
}
optional Type type = 1;
optional bytes content = 2;
}

View File

@@ -17,16 +17,15 @@
package org.whispersystems.textsecuregcm; package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration; import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration; import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration; import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration; import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MetricsConfiguration; import org.whispersystems.textsecuregcm.configuration.MessageStoreConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration; import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration; import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration; import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.S3Configuration; import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration; import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
@@ -35,6 +34,7 @@ import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import io.dropwizard.Configuration; import io.dropwizard.Configuration;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.DataSourceFactory;
public class WhisperServerConfiguration extends Configuration { public class WhisperServerConfiguration extends Configuration {
@@ -50,7 +50,7 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull @NotNull
@Valid @Valid
@JsonProperty @JsonProperty
private GcmConfiguration gcm; private PushConfiguration push;
@NotNull @NotNull
@Valid @Valid
@@ -65,10 +65,12 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull @NotNull
@Valid @Valid
@JsonProperty @JsonProperty
private RedisConfiguration redis; private DirectoryConfiguration directory;
@NotNull
@Valid
@JsonProperty @JsonProperty
private ApnConfiguration apn = new ApnConfiguration(); private MessageStoreConfiguration messageStore;
@Valid @Valid
@JsonProperty @JsonProperty
@@ -88,10 +90,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private GraphiteConfiguration graphite = new GraphiteConfiguration(); private GraphiteConfiguration graphite = new GraphiteConfiguration();
@Valid
@JsonProperty
private MetricsConfiguration viz = new MetricsConfiguration();
@Valid @Valid
@JsonProperty @JsonProperty
private WebsocketConfiguration websocket = new WebsocketConfiguration(); private WebsocketConfiguration websocket = new WebsocketConfiguration();
@@ -99,6 +97,12 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private RedPhoneConfiguration redphone = new RedPhoneConfiguration(); private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
@Valid
@NotNull
@JsonProperty
private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
public WebsocketConfiguration getWebsocketConfiguration() { public WebsocketConfiguration getWebsocketConfiguration() {
return websocket; return websocket;
} }
@@ -111,12 +115,12 @@ public class WhisperServerConfiguration extends Configuration {
return nexmo; return nexmo;
} }
public GcmConfiguration getGcmConfiguration() { public PushConfiguration getPushConfiguration() {
return gcm; return push;
} }
public ApnConfiguration getApnConfiguration() { public JerseyClientConfiguration getJerseyClientConfiguration() {
return apn; return httpClient;
} }
public S3Configuration getS3Configuration() { public S3Configuration getS3Configuration() {
@@ -127,8 +131,12 @@ public class WhisperServerConfiguration extends Configuration {
return memcache; return memcache;
} }
public RedisConfiguration getRedisConfiguration() { public DirectoryConfiguration getDirectoryConfiguration() {
return redis; return directory;
}
public MessageStoreConfiguration getMessageStoreConfiguration() {
return messageStore;
} }
public DataSourceFactory getDataSourceFactory() { public DataSourceFactory getDataSourceFactory() {
@@ -147,10 +155,6 @@ public class WhisperServerConfiguration extends Configuration {
return graphite; return graphite;
} }
public MetricsConfiguration getMetricsConfiguration() {
return viz;
}
public RedPhoneConfiguration getRedphoneConfiguration() { public RedPhoneConfiguration getRedphoneConfiguration() {
return redphone; return redphone;
} }

View File

@@ -20,6 +20,7 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter; import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.Client;
import net.spy.memcached.MemcachedClient; import net.spy.memcached.MemcachedClient;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter; import org.eclipse.jetty.servlets.CrossOriginFilter;
@@ -34,6 +35,7 @@ import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController; import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1; import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2; import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1; import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2; import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.controllers.MessageController; import org.whispersystems.textsecuregcm.controllers.MessageController;
@@ -53,9 +55,9 @@ import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory; import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck; import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
import org.whispersystems.textsecuregcm.providers.TimeProvider; import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.push.APNSender; import org.whispersystems.textsecuregcm.push.FeedbackHandler;
import org.whispersystems.textsecuregcm.push.GCMSender;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.PushServiceClient;
import org.whispersystems.textsecuregcm.push.WebsocketSender; import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender; import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.SmsSender;
@@ -89,6 +91,7 @@ import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.Application; import io.dropwizard.Application;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory; import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.metrics.graphite.GraphiteReporterFactory; import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
@@ -135,40 +138,34 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class); PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class);
Keys keys = jdbi.onDemand(Keys.class); Keys keys = jdbi.onDemand(Keys.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient(); MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool(); JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
JedisPool messageStoreClient = new RedisClientFactory(config.getMessageStoreConfiguration().getUrl()).getRedisClientPool();
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
.build(getName());
DirectoryManager directory = new DirectoryManager(redisClient); DirectoryManager directory = new DirectoryManager(directoryClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient); PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, memcachedClient ); PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, memcachedClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration()); FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
StoredMessages storedMessages = new StoredMessages(redisClient); StoredMessages storedMessages = new StoredMessages(messageStoreClient);
PubSubManager pubSubManager = new PubSubManager(redisClient); PubSubManager pubSubManager = new PubSubManager(messageStoreClient);
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
APNSender apnSender = new APNSender(accountsManager, pubSubManager, storedMessages, memcachedClient, WebsocketSender websocketSender = new WebsocketSender(storedMessages, pubSubManager);
config.getApnConfiguration().getCertificate(), AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
config.getApnConfiguration().getKey()); RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
GCMSender gcmSender = new GCMSender(accountsManager,
config.getGcmConfiguration().getSenderId(),
config.getGcmConfiguration().getApiKey());
WebsocketSender websocketSender = new WebsocketSender(storedMessages, pubSubManager);
environment.lifecycle().manage(apnSender);
environment.lifecycle().manage(gcmSender);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration()); TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration()); Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational()); SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration()); UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(gcmSender, apnSender, websocketSender); PushSender pushSender = new PushSender(pushServiceClient, websocketSender);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey(); Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
environment.lifecycle().manage(feedbackHandler);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner); AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager); KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager); KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
@@ -191,15 +188,17 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(messageController); environment.jersey().register(messageController);
if (config.getWebsocketConfiguration().isEnabled()) { if (config.getWebsocketConfiguration().isEnabled()) {
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment); WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config);
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator)); webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
webSocketEnvironment.setConnectListener(new ConnectListener(accountsManager, pushSender, storedMessages, pubSubManager)); webSocketEnvironment.setConnectListener(new ConnectListener(accountsManager, pushSender, storedMessages, pubSubManager));
webSocketEnvironment.jersey().register(new KeepAliveController());
WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment); WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment);
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet); ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet);
websocket.addMapping("/v1/websocket/*"); websocket.addMapping("/v1/websocket/*");
websocket.setAsyncSupported(true); websocket.setAsyncSupported(true);
servlet.start();
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class); FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
@@ -210,7 +209,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
filter.setInitParameter("allowCredentials", "true"); filter.setInitParameter("allowCredentials", "true");
} }
environment.healthChecks().register("redis", new RedisHealthCheck(redisClient)); environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
environment.healthChecks().register("messagestore", new RedisHealthCheck(messageStoreClient));
environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient)); environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
environment.jersey().register(new IOExceptionMapper()); environment.jersey().register(new IOExceptionMapper());
@@ -229,13 +229,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
GraphiteReporter graphiteReporter = (GraphiteReporter) graphiteReporterFactory.build(environment.metrics()); GraphiteReporter graphiteReporter = (GraphiteReporter) graphiteReporterFactory.build(environment.metrics());
graphiteReporter.start(15, TimeUnit.SECONDS); graphiteReporter.start(15, TimeUnit.SECONDS);
} }
if (config.getMetricsConfiguration().isEnabled()) {
new JsonMetricsReporter(environment.metrics(),
config.getMetricsConfiguration().getToken(),
config.getMetricsConfiguration().getHost())
.start(60, TimeUnit.SECONDS);
}
} }
private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) { private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) {

View File

@@ -21,7 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.URL; import org.hibernate.validator.constraints.URL;
public class RedisConfiguration { public class DirectoryConfiguration {
@JsonProperty @JsonProperty
@NotEmpty @NotEmpty

View File

@@ -18,12 +18,8 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import java.util.LinkedList;
import java.util.List; import java.util.List;
public class FederationConfiguration { public class FederationConfiguration {
@@ -34,31 +30,7 @@ public class FederationConfiguration {
@JsonProperty @JsonProperty
private String name; private String name;
@JsonProperty
private String herokuPeers;
public List<FederatedPeer> getPeers() { public List<FederatedPeer> getPeers() {
if (peers != null) {
return peers;
}
if (herokuPeers != null) {
List<FederatedPeer> peers = new LinkedList<>();
JsonElement root = new JsonParser().parse(herokuPeers);
JsonArray peerElements = root.getAsJsonArray();
for (JsonElement peer : peerElements) {
String name = peer.getAsJsonObject().get("name").getAsString();
String url = peer.getAsJsonObject().get("url").getAsString();
String authenticationToken = peer.getAsJsonObject().get("authenticationToken").getAsString();
String certificate = peer.getAsJsonObject().get("certificate").getAsString();
peers.add(new FederatedPeer(name, url, authenticationToken, certificate));
}
return peers;
}
return peers; return peers;
} }

View File

@@ -0,0 +1,14 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class MessageStoreConfiguration {
@JsonProperty
@NotEmpty
private String url;
public String getUrl() {
return url;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
@Path("/v1/keepalive")
public class KeepAliveController {
@Timed
@GET
public Response getKeepAlive() {
return Response.ok().build();
}
}

View File

@@ -0,0 +1,34 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class ApnMessage {
@JsonProperty
@NotEmpty
private String apnId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
@JsonProperty
@NotEmpty
private String message;
public ApnMessage() {}
public ApnMessage(String apnId, String number, int deviceId, String message) {
this.apnId = apnId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
}
}

View File

@@ -18,15 +18,10 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.gson.Gson;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.Arrays; import java.util.Arrays;
@JsonInclude(JsonInclude.Include.NON_DEFAULT) @JsonInclude(JsonInclude.Include.NON_DEFAULT)
@@ -73,9 +68,9 @@ public class ClientContact {
this.inactive = inactive; this.inactive = inactive;
} }
public String toString() { // public String toString() {
return new Gson().toJson(this); // return new Gson().toJson(this);
} // }
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {

View File

@@ -41,7 +41,8 @@ public class EncryptedOutgoingMessage {
private static final int MAC_KEY_SIZE = 20; private static final int MAC_KEY_SIZE = 20;
private static final int MAC_SIZE = 10; private static final int MAC_SIZE = 10;
private final String serialized; private final byte[] serialized;
private final String serializedAndEncoded;
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage, public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
String signalingKey) String signalingKey)
@@ -50,12 +51,16 @@ public class EncryptedOutgoingMessage {
byte[] plaintext = outgoingMessage.toByteArray(); byte[] plaintext = outgoingMessage.toByteArray();
SecretKeySpec cipherKey = getCipherKey (signalingKey); SecretKeySpec cipherKey = getCipherKey (signalingKey);
SecretKeySpec macKey = getMacKey(signalingKey); SecretKeySpec macKey = getMacKey(signalingKey);
byte[] ciphertext = getCiphertext(plaintext, cipherKey, macKey);
this.serialized = Base64.encodeBytes(ciphertext); this.serialized = getCiphertext(plaintext, cipherKey, macKey);
this.serializedAndEncoded = Base64.encodeBytes(this.serialized);
} }
public String serialize() { public String toEncodedString() {
return serializedAndEncoded;
}
public byte[] toByteArray() {
return serialized; return serialized;
} }

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Min;
public class UnregisteredEvent {
@JsonProperty
@NotEmpty
private String registrationId;
@JsonProperty
@NotEmpty
private String number;
@JsonProperty
@Min(1)
private int deviceId;
@JsonProperty
private long timestamp;
public String getRegistrationId() {
return registrationId;
}
public String getNumber() {
return number;
}
public int getDeviceId() {
return deviceId;
}
public long getTimestamp() {
return timestamp;
}
}

View File

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

View File

@@ -31,7 +31,9 @@ public class IOExceptionMapper implements ExceptionMapper<IOException> {
@Override @Override
public Response toResponse(IOException e) { public Response toResponse(IOException e) {
logger.warn("IOExceptionMapper", e); if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) {
logger.warn("IOExceptionMapper", e);
}
return Response.status(503).build(); return Response.status(503).build();
} }
} }

View File

@@ -27,25 +27,25 @@ import java.util.SortedMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/**
* Adapted from MetricsServlet.
*/
public class JsonMetricsReporter extends ScheduledReporter { public class JsonMetricsReporter extends ScheduledReporter {
private final Logger logger = LoggerFactory.getLogger(JsonMetricsReporter.class); private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
private final Logger logger = LoggerFactory.getLogger(JsonMetricsReporter.class);
private final JsonFactory factory = new JsonFactory(); private final JsonFactory factory = new JsonFactory();
private final String table; private final String token;
private final String sunnylabsHost; private final String hostname;
private final String host; private final String host;
public JsonMetricsReporter(MetricRegistry registry, String token, String sunnylabsHost) public JsonMetricsReporter(MetricRegistry registry, String token, String hostname,
MetricFilter filter, TimeUnit rateUnit, TimeUnit durationUnit)
throws UnknownHostException throws UnknownHostException
{ {
super(registry, "jsonmetrics-reporter", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.MILLISECONDS); super(registry, "json-reporter", filter, rateUnit, durationUnit);
this.table = token; this.token = token;
this.sunnylabsHost = sunnylabsHost; this.hostname = hostname;
this.host = InetAddress.getLocalHost().getHostName(); this.host = InetAddress.getLocalHost().getHostName();
} }
@Override @Override
@@ -56,8 +56,8 @@ public class JsonMetricsReporter extends ScheduledReporter {
SortedMap<String, Timer> stringTimerSortedMap) SortedMap<String, Timer> stringTimerSortedMap)
{ {
try { try {
logger.info("Reporting metrics..."); logger.debug("Reporting metrics...");
URL url = new URL("https", sunnylabsHost, 443, "/report/metrics?t=" + table + "&h=" + host); URL url = new URL("https", hostname, 443, String.format("/report/metrics?t=%s&h=%s", token, host));
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true); connection.setDoOutput(true);
@@ -93,7 +93,7 @@ public class JsonMetricsReporter extends ScheduledReporter {
outputStream.close(); outputStream.close();
logger.info("Metrics server response: " + connection.getResponseCode()); logger.debug("Metrics server response: " + connection.getResponseCode());
} catch (IOException e) { } catch (IOException e) {
logger.warn("Error sending metrics", e); logger.warn("Error sending metrics", e);
} catch (Exception e) { } catch (Exception e) {
@@ -175,10 +175,66 @@ public class JsonMetricsReporter extends ScheduledReporter {
json.writeNumberField("m15", convertRate(meter.getFifteenMinuteRate())); json.writeNumberField("m15", convertRate(meter.getFifteenMinuteRate()));
} }
private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
private String sanitize(String metricName) { private String sanitize(String metricName) {
return SIMPLE_NAMES.matcher(metricName).replaceAll("_"); return SIMPLE_NAMES.matcher(metricName).replaceAll("_");
} }
public static Builder forRegistry(MetricRegistry registry) {
return new Builder(registry);
}
public static class Builder {
private final MetricRegistry registry;
private MetricFilter filter = MetricFilter.ALL;
private TimeUnit rateUnit = TimeUnit.SECONDS;
private TimeUnit durationUnit = TimeUnit.MILLISECONDS;
private String token;
private String hostname;
private Builder(MetricRegistry registry) {
this.registry = registry;
this.rateUnit = TimeUnit.SECONDS;
this.durationUnit = TimeUnit.MILLISECONDS;
this.filter = MetricFilter.ALL;
}
public Builder convertRatesTo(TimeUnit rateUnit) {
this.rateUnit = rateUnit;
return this;
}
public Builder convertDurationsTo(TimeUnit durationUnit) {
this.durationUnit = durationUnit;
return this;
}
public Builder filter(MetricFilter filter) {
this.filter = filter;
return this;
}
public Builder withToken(String token) {
this.token = token;
return this;
}
public Builder withHostname(String hostname) {
this.hostname = hostname;
return this;
}
public JsonMetricsReporter build() throws UnknownHostException {
if (hostname == null) {
throw new IllegalArgumentException("No hostname specified!");
}
if (token == null) {
throw new IllegalArgumentException("No token specified!");
}
return new JsonMetricsReporter(registry, token, hostname, filter, rateUnit, durationUnit);
}
}
} }

View File

@@ -0,0 +1,38 @@
package org.whispersystems.textsecuregcm.metrics;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ScheduledReporter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import javax.validation.constraints.NotNull;
import java.net.UnknownHostException;
import io.dropwizard.metrics.BaseReporterFactory;
@JsonTypeName("json")
public class JsonMetricsReporterFactory extends BaseReporterFactory {
@JsonProperty
@NotNull
private String hostname;
@JsonProperty
@NotNull
private String token;
@Override
public ScheduledReporter build(MetricRegistry metricRegistry) {
try {
return JsonMetricsReporter.forRegistry(metricRegistry)
.withHostname(hostname)
.withToken(token)
.convertRatesTo(getRateUnit())
.convertDurationsTo(getDurationUnit())
.filter(getFilter())
.build();
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e);
}
}
}

View File

@@ -16,7 +16,7 @@
*/ */
package org.whispersystems.textsecuregcm.providers; package org.whispersystems.textsecuregcm.providers;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration; import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.net.URI; import java.net.URI;
@@ -30,11 +30,11 @@ public class RedisClientFactory {
private final JedisPool jedisPool; private final JedisPool jedisPool;
public RedisClientFactory(RedisConfiguration redisConfig) throws URISyntaxException { public RedisClientFactory(String url) throws URISyntaxException {
JedisPoolConfig poolConfig = new JedisPoolConfig(); JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setTestOnBorrow(true); poolConfig.setTestOnBorrow(true);
URI redisURI = new URI(redisConfig.getUrl()); URI redisURI = new URI(url);
String redisHost = redisURI.getHost(); String redisHost = redisURI.getHost();
int redisPort = redisURI.getPort(); int redisPort = redisURI.getPort();
String redisPassword = null; String redisPassword = null;

View File

@@ -1,248 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsService;
import com.notnoop.exceptions.NetworkIOException;
import net.spy.memcached.MemcachedClient;
import org.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
public class APNSender implements Managed {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter websocketMeter = metricRegistry.meter(name(getClass(), "websocket"));
private final Meter pushMeter = metricRegistry.meter(name(getClass(), "push"));
private final Meter failureMeter = metricRegistry.meter(name(getClass(), "failure"));
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
private static final String MESSAGE_BODY = "m";
private static final ObjectMapper mapper = SystemMapper.getMapper();
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private final AccountsManager accounts;
private final PubSubManager pubSubManager;
private final StoredMessages storedMessages;
private final MemcachedClient memcachedClient;
private final String apnCertificate;
private final String apnKey;
private Optional<ApnsService> apnService;
public APNSender(AccountsManager accounts,
PubSubManager pubSubManager,
StoredMessages storedMessages,
MemcachedClient memcachedClient,
String apnCertificate, String apnKey)
{
this.accounts = accounts;
this.pubSubManager = pubSubManager;
this.storedMessages = storedMessages;
this.apnCertificate = apnCertificate;
this.apnKey = apnKey;
this.memcachedClient = memcachedClient;
}
public void sendMessage(Account account, Device device,
String registrationId, PendingMessage message)
throws TransientPushFailureException
{
try {
String serializedPendingMessage = mapper.writeValueAsString(message);
WebsocketAddress websocketAddress = new WebsocketAddress(account.getNumber(), device.getId());
if (pubSubManager.publish(websocketAddress, new PubSubMessage(PubSubMessage.TYPE_DELIVER,
serializedPendingMessage)))
{
websocketMeter.mark();
} else {
memcacheSet(registrationId, account.getNumber());
storedMessages.insert(websocketAddress, message);
if (!message.isReceipt()) {
sendPush(registrationId, serializedPendingMessage);
}
}
} catch (IOException e) {
throw new TransientPushFailureException(e);
}
}
private void sendPush(String registrationId, String message)
throws TransientPushFailureException
{
try {
if (!apnService.isPresent()) {
failureMeter.mark();
throw new TransientPushFailureException("APN access not configured!");
}
String payload = APNS.newPayload()
.alertBody("Message!")
.customField(MESSAGE_BODY, message)
.build();
logger.debug("APN Payload: " + payload);
apnService.get().push(registrationId, payload);
pushMeter.mark();
} catch (NetworkIOException nioe) {
logger.warn("Network Error", nioe);
failureMeter.mark();
throw new TransientPushFailureException(nioe);
}
}
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException
{
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
X509Certificate certificate = (X509Certificate) reader.readObject();
Certificate[] certificateChain = {certificate};
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes())));
KeyPair keyPair = (KeyPair) reader.readObject();
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(null);
keyStore.setEntry("apn",
new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), certificateChain),
new KeyStore.PasswordProtection("insecure".toCharArray()));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
keyStore.store(baos, "insecure".toCharArray());
return baos.toByteArray();
}
@Override
public void start() throws Exception {
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
this.apnService = Optional.of(APNS.newService()
.withCert(new ByteArrayInputStream(keyStore), "insecure")
.asQueued()
.withSandboxDestination().build());
this.executor.scheduleAtFixedRate(new FeedbackRunnable(), 0, 1, TimeUnit.HOURS);
} else {
this.apnService = Optional.absent();
}
}
@Override
public void stop() throws Exception {
if (apnService.isPresent()) {
apnService.get().stop();
}
}
private void memcacheSet(String registrationId, String number) {
if (memcachedClient != null) {
memcachedClient.set("APN-" + registrationId, 60 * 60 * 24, number);
}
}
private Optional<String> memcacheGet(String registrationId) {
if (memcachedClient != null) {
return Optional.fromNullable((String)memcachedClient.get("APN-" + registrationId));
} else {
return Optional.absent();
}
}
private class FeedbackRunnable implements Runnable {
private void updateAccount(Account account, String registrationId) {
boolean needsUpdate = false;
for (Device device : account.getDevices()) {
if (registrationId.equals(device.getApnId())) {
needsUpdate = true;
device.setApnId(null);
}
}
if (needsUpdate) {
accounts.update(account);
}
}
@Override
public void run() {
if (apnService.isPresent()) {
Map<String, Date> inactiveDevices = apnService.get().getInactiveDevices();
for (String registrationId : inactiveDevices.keySet()) {
Optional<String> number = memcacheGet(registrationId);
if (number.isPresent()) {
Optional<Account> account = accounts.get(number.get());
if (account.isPresent()) {
updateAccount(account.get(), registrationId);
}
} else {
logger.warn("APN unregister event received for uncached ID: " + registrationId);
}
}
}
}
}
}

View File

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

View File

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

View File

@@ -18,10 +18,10 @@ package org.whispersystems.textsecuregcm.push;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException; import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.entities.GcmMessage;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
@@ -31,62 +31,57 @@ public class PushSender {
private final Logger logger = LoggerFactory.getLogger(PushSender.class); private final Logger logger = LoggerFactory.getLogger(PushSender.class);
private final GCMSender gcmSender; private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"},\"content-available\":1,\"category\":\"Signal_Message\"}}";
private final APNSender apnSender;
private final WebsocketSender webSocketSender;
public PushSender(GCMSender gcmClient, private final PushServiceClient pushServiceClient;
APNSender apnSender, private final WebsocketSender webSocketSender;
WebsocketSender websocketSender)
{ public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
this.gcmSender = gcmClient; this.pushServiceClient = pushServiceClient;
this.apnSender = apnSender; this.webSocketSender = websocketSender;
this.webSocketSender = websocketSender;
} }
public void sendMessage(Account account, Device device, OutgoingMessageSignal message) public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
throws NotPushRegisteredException, TransientPushFailureException throws NotPushRegisteredException, TransientPushFailureException
{ {
try { if (device.getGcmId() != null) sendGcmMessage(account, device, message);
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE; else if (device.getApnId() != null) sendApnMessage(account, device, message);
String signalingKey = device.getSignalingKey(); else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message);
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, signalingKey); else throw new NotPushRegisteredException("No delivery possible!");
PendingMessage pendingMessage = new PendingMessage(message.getSource(), }
message.getTimestamp(),
isReceipt,
encryptedMessage.serialize());
sendMessage(account, device, pendingMessage); private void sendGcmMessage(Account account, Device device, OutgoingMessageSignal message)
throws TransientPushFailureException, NotPushRegisteredException
{
try {
String number = account.getNumber();
long deviceId = device.getId();
String registrationId = device.getGcmId();
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int) deviceId,
encryptedMessage.toEncodedString(), isReceipt);
pushServiceClient.send(gcmMessage);
} catch (CryptoEncodingException e) { } catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e); throw new NotPushRegisteredException(e);
} }
} }
public void sendMessage(Account account, Device device, PendingMessage pendingMessage) private void sendApnMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
throws NotPushRegisteredException, TransientPushFailureException
{
if (device.getGcmId() != null) sendGcmMessage(account, device, pendingMessage);
else if (device.getApnId() != null) sendApnMessage(account, device, pendingMessage);
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, pendingMessage);
else throw new NotPushRegisteredException("No delivery possible!");
}
private void sendGcmMessage(Account account, Device device, PendingMessage pendingMessage) {
String number = account.getNumber();
long deviceId = device.getId();
String registrationId = device.getGcmId();
gcmSender.sendMessage(number, deviceId, registrationId, pendingMessage);
}
private void sendApnMessage(Account account, Device device, PendingMessage outgoingMessage)
throws TransientPushFailureException throws TransientPushFailureException
{ {
apnSender.sendMessage(account, device, device.getApnId(), outgoingMessage); boolean online = webSocketSender.sendMessage(account, device, outgoingMessage, true);
if (!online && outgoingMessage.getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) {
ApnMessage apnMessage = new ApnMessage(device.getApnId(), account.getNumber(),
(int)device.getId(), APN_PAYLOAD);
pushServiceClient.send(apnMessage);
}
} }
private void sendWebSocketMessage(Account account, Device device, PendingMessage outgoingMessage) private void sendWebSocketMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
{ {
webSocketSender.sendMessage(account, device, outgoingMessage); webSocketSender.sendMessage(account, device, outgoingMessage, false);
} }
} }

View File

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

View File

@@ -19,31 +19,30 @@ package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages; import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress; import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public class WebsocketSender { public class WebsocketSender {
private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class); private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter onlineMeter = metricRegistry.meter(name(getClass(), "online"));
private final Meter offlineMeter = metricRegistry.meter(name(getClass(), "offline"));
private static final ObjectMapper mapper = SystemMapper.getMapper(); private final Meter websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" ));
private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" ));
private final Meter apnOnlineMeter = metricRegistry.meter(name(getClass(), "apn_online" ));
private final Meter apnOfflineMeter = metricRegistry.meter(name(getClass(), "apn_offline"));
private final StoredMessages storedMessages; private final StoredMessages storedMessages;
private final PubSubManager pubSubManager; private final PubSubManager pubSubManager;
@@ -53,21 +52,28 @@ public class WebsocketSender {
this.pubSubManager = pubSubManager; this.pubSubManager = pubSubManager;
} }
public void sendMessage(Account account, Device device, PendingMessage pendingMessage) { public boolean sendMessage(Account account, Device device, OutgoingMessageSignal message, boolean apn) {
try { WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
String serialized = mapper.writeValueAsString(pendingMessage); PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId()); .setType(PubSubMessage.Type.DELIVER)
PubSubMessage pubSubMessage = new PubSubMessage(PubSubMessage.TYPE_DELIVER, serialized); .setContent(message.toByteString())
.build();
if (pubSubManager.publish(address, pubSubMessage)) { if (pubSubManager.publish(address, pubSubMessage)) {
onlineMeter.mark(); if (apn) apnOnlineMeter.mark();
} else { else websocketOnlineMeter.mark();
offlineMeter.mark();
storedMessages.insert(address, pendingMessage); return true;
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null)); } else {
} if (apn) apnOfflineMeter.mark();
} catch (JsonProcessingException e) { else websocketOfflineMeter.mark();
logger.warn("WebsocketSender", "Unable to serialize json", e);
storedMessages.insert(address, message);
pubSubManager.publish(address, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.QUERY_DB)
.build());
return false;
} }
} }
} }

View File

@@ -42,7 +42,7 @@ public class Account {
private String identityKey; private String identityKey;
@JsonIgnore @JsonIgnore
private Optional<Device> authenticatedDevice; private Device authenticatedDevice;
public Account() {} public Account() {}
@@ -54,11 +54,11 @@ public class Account {
} }
public Optional<Device> getAuthenticatedDevice() { public Optional<Device> getAuthenticatedDevice() {
return authenticatedDevice; return Optional.fromNullable(authenticatedDevice);
} }
public void setAuthenticatedDevice(Device device) { public void setAuthenticatedDevice(Device device) {
this.authenticatedDevice = Optional.of(device); this.authenticatedDevice = device;
} }
public void setNumber(String number) { public void setNumber(String number) {

View File

@@ -22,8 +22,6 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.io.Serializable;
public class Device { public class Device {
public static final long MASTER_ID = 1; public static final long MASTER_ID = 1;
@@ -46,6 +44,9 @@ public class Device {
@JsonProperty @JsonProperty
private String apnId; private String apnId;
@JsonProperty
private long pushTimestamp;
@JsonProperty @JsonProperty
private boolean fetchesMessages; private boolean fetchesMessages;
@@ -79,6 +80,10 @@ public class Device {
public void setApnId(String apnId) { public void setApnId(String apnId) {
this.apnId = apnId; this.apnId = apnId;
if (apnId != null) {
this.pushTimestamp = System.currentTimeMillis();
}
} }
public String getGcmId() { public String getGcmId() {
@@ -87,6 +92,10 @@ public class Device {
public void setGcmId(String gcmId) { public void setGcmId(String gcmId) {
this.gcmId = gcmId; this.gcmId = gcmId;
if (gcmId != null) {
this.pushTimestamp = System.currentTimeMillis();
}
} }
public long getId() { public long getId() {
@@ -145,4 +154,8 @@ public class Device {
public void setSignedPreKey(SignedPreKey signedPreKey) { public void setSignedPreKey(SignedPreKey signedPreKey) {
this.signedPreKey = signedPreKey; this.signedPreKey = signedPreKey;
} }
public long getPushTimestamp() {
return pushTimestamp;
}
} }

View File

@@ -16,14 +16,19 @@
*/ */
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.gson.Gson; import org.slf4j.Logger;
import com.google.gson.annotations.SerializedName; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.IterablePair; import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -34,12 +39,17 @@ import redis.clients.jedis.Response;
public class DirectoryManager { public class DirectoryManager {
private final Logger logger = LoggerFactory.getLogger(DirectoryManager.class);
private static final byte[] DIRECTORY_KEY = {'d', 'i', 'r', 'e', 'c', 't', 'o', 'r', 'y'}; private static final byte[] DIRECTORY_KEY = {'d', 'i', 'r', 'e', 'c', 't', 'o', 'r', 'y'};
private final ObjectMapper objectMapper;
private final JedisPool redisPool; private final JedisPool redisPool;
public DirectoryManager(JedisPool redisPool) { public DirectoryManager(JedisPool redisPool) {
this.redisPool = redisPool; this.redisPool = redisPool;
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
} }
public void remove(String number) { public void remove(String number) {
@@ -63,45 +73,48 @@ public class DirectoryManager {
public void add(ClientContact contact) { public void add(ClientContact contact) {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms()); TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
Jedis jedis = redisPool.getResource();
jedis.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes()); try (Jedis jedis = redisPool.getResource()) {
redisPool.returnResource(jedis); jedis.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) {
logger.warn("JSON Serialization", e);
}
} }
public void add(BatchOperationHandle handle, ClientContact contact) { public void add(BatchOperationHandle handle, ClientContact contact) {
Pipeline pipeline = handle.pipeline; try {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms()); Pipeline pipeline = handle.pipeline;
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
pipeline.hset(DIRECTORY_KEY, contact.getToken(), new Gson().toJson(tokenValue).getBytes()); pipeline.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) {
logger.warn("JSON Serialization", e);
}
} }
public PendingClientContact get(BatchOperationHandle handle, byte[] token) { public PendingClientContact get(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline; Pipeline pipeline = handle.pipeline;
return new PendingClientContact(token, pipeline.hget(DIRECTORY_KEY, token)); return new PendingClientContact(objectMapper, token, pipeline.hget(DIRECTORY_KEY, token));
} }
public Optional<ClientContact> get(byte[] token) { public Optional<ClientContact> get(byte[] token) {
Jedis jedis = redisPool.getResource(); try (Jedis jedis = redisPool.getResource()) {
try {
byte[] result = jedis.hget(DIRECTORY_KEY, token); byte[] result = jedis.hget(DIRECTORY_KEY, token);
if (result == null) { if (result == null) {
return Optional.absent(); return Optional.absent();
} }
TokenValue tokenValue = new Gson().fromJson(new String(result), TokenValue.class); TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms)); return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
} finally { } catch (IOException e) {
redisPool.returnResource(jedis); logger.warn("JSON Error", e);
return Optional.absent();
} }
} }
public List<ClientContact> get(List<byte[]> tokens) { public List<ClientContact> get(List<byte[]> tokens) {
Jedis jedis = redisPool.getResource(); try (Jedis jedis = redisPool.getResource()) {
try {
Pipeline pipeline = jedis.pipelined(); Pipeline pipeline = jedis.pipelined();
List<Response<byte[]>> futures = new LinkedList<>(); List<Response<byte[]>> futures = new LinkedList<>();
List<ClientContact> results = new LinkedList<>(); List<ClientContact> results = new LinkedList<>();
@@ -117,17 +130,19 @@ public class DirectoryManager {
IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures); IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures);
for (Pair<byte[], Response<byte[]>> pair : lists) { for (Pair<byte[], Response<byte[]>> pair : lists) {
if (pair.second().get() != null) { try {
TokenValue tokenValue = new Gson().fromJson(new String(pair.second().get()), TokenValue.class); if (pair.second().get() != null) {
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms); TokenValue tokenValue = objectMapper.readValue(pair.second().get(), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms);
results.add(clientContact); results.add(clientContact);
}
} catch (IOException e) {
logger.warn("Deserialization Problem: ", e);
} }
} }
return results; return results;
} finally {
redisPool.returnResource(jedis);
} }
} }
@@ -156,12 +171,15 @@ public class DirectoryManager {
} }
private static class TokenValue { private static class TokenValue {
@SerializedName("r")
@JsonProperty(value = "r")
private String relay; private String relay;
@SerializedName("s") @JsonProperty(value = "s")
private boolean supportsSms; private boolean supportsSms;
public TokenValue() {}
public TokenValue(String relay, boolean supportsSms) { public TokenValue(String relay, boolean supportsSms) {
this.relay = relay; this.relay = relay;
this.supportsSms = supportsSms; this.supportsSms = supportsSms;
@@ -169,22 +187,24 @@ public class DirectoryManager {
} }
public static class PendingClientContact { public static class PendingClientContact {
private final ObjectMapper objectMapper;
private final byte[] token; private final byte[] token;
private final Response<byte[]> response; private final Response<byte[]> response;
PendingClientContact(byte[] token, Response<byte[]> response) { PendingClientContact(ObjectMapper objectMapper, byte[] token, Response<byte[]> response) {
this.token = token; this.objectMapper = objectMapper;
this.response = response; this.token = token;
this.response = response;
} }
public Optional<ClientContact> get() { public Optional<ClientContact> get() throws IOException {
byte[] result = response.get(); byte[] result = response.get();
if (result == null) { if (result == null) {
return Optional.absent(); return Optional.absent();
} }
TokenValue tokenValue = new Gson().fromJson(new String(result), TokenValue.class); TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms)); return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
} }

View File

@@ -1,5 +1,7 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public interface PubSubListener { public interface PubSubListener {
public void onPubSubMessage(PubSubMessage outgoingMessage); public void onPubSubMessage(PubSubMessage outgoingMessage);

View File

@@ -1,69 +1,59 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException; import com.google.protobuf.InvalidProtocolBufferException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress; import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.IOException; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.Jedis; import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPubSub;
public class PubSubManager { public class PubSubManager {
private static final String KEEPALIVE_CHANNEL = "KEEPALIVE"; private static final byte[] KEEPALIVE_CHANNEL = "KEEPALIVE".getBytes();
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class); private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final ObjectMapper mapper = SystemMapper.getMapper();
private final SubscriptionListener baseListener = new SubscriptionListener(); private final SubscriptionListener baseListener = new SubscriptionListener();
private final Map<String, PubSubListener> listeners = new HashMap<>(); private final Map<String, PubSubListener> listeners = new HashMap<>();
private final JedisPool jedisPool; private final JedisPool jedisPool;
private boolean subscribed = false; private boolean subscribed = false;
public PubSubManager(final JedisPool jedisPool) { public PubSubManager(JedisPool jedisPool) {
this.jedisPool = jedisPool; this.jedisPool = jedisPool;
initializePubSubWorker(); initializePubSubWorker();
waitForSubscription(); waitForSubscription();
} }
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) { public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
listeners.put(address.serialize(), listener); String serializedAddress = address.serialize();
baseListener.subscribe(address.serialize());
listeners.put(serializedAddress, listener);
baseListener.subscribe(serializedAddress.getBytes());
} }
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) { public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
if (listeners.get(address.serialize()) == listener) { String serializedAddress = address.serialize();
listeners.remove(address.serialize());
baseListener.unsubscribe(address.serialize()); if (listeners.get(serializedAddress) == listener) {
listeners.remove(serializedAddress);
baseListener.unsubscribe(serializedAddress.getBytes());
} }
} }
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) { public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
return publish(address.serialize(), message); return publish(address.serialize().getBytes(), message);
} }
private synchronized boolean publish(String channel, PubSubMessage message) { private synchronized boolean publish(byte[] channel, PubSubMessage message) {
try { try (Jedis jedis = jedisPool.getResource()) {
String serialized = mapper.writeValueAsString(message); return jedis.publish(channel, message.toByteArray()) != 0;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.publish(channel, serialized) != 0;
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
} catch (JsonProcessingException e) {
throw new AssertionError(e);
} }
} }
@@ -82,14 +72,9 @@ public class PubSubManager {
@Override @Override
public void run() { public void run() {
for (;;) { for (;;) {
Jedis jedis = null; try (Jedis jedis = jedisPool.getResource()) {
try {
jedis = jedisPool.getResource();
jedis.subscribe(baseListener, KEEPALIVE_CHANNEL); jedis.subscribe(baseListener, KEEPALIVE_CHANNEL);
logger.warn("**** Unsubscribed from holding channel!!! ******"); logger.warn("**** Unsubscribed from holding channel!!! ******");
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
} }
} }
} }
@@ -101,7 +86,9 @@ public class PubSubManager {
for (;;) { for (;;) {
try { try {
Thread.sleep(20000); Thread.sleep(20000);
publish(KEEPALIVE_CHANNEL, new PubSubMessage(0, "foo")); publish(KEEPALIVE_CHANNEL, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.KEEPALIVE)
.build());
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@@ -110,33 +97,33 @@ public class PubSubManager {
}.start(); }.start();
} }
private class SubscriptionListener extends JedisPubSub { private class SubscriptionListener extends BinaryJedisPubSub {
@Override @Override
public void onMessage(String channel, String message) { public void onMessage(byte[] channel, byte[] message) {
try { try {
PubSubListener listener; PubSubListener listener;
synchronized (PubSubManager.this) { synchronized (PubSubManager.this) {
listener = listeners.get(channel); listener = listeners.get(new String(channel));
} }
if (listener != null) { if (listener != null) {
listener.onPubSubMessage(mapper.readValue(message, PubSubMessage.class)); listener.onPubSubMessage(PubSubMessage.parseFrom(message));
} }
} catch (IOException e) { } catch (InvalidProtocolBufferException e) {
logger.warn("IOE", e); logger.warn("Error parsing PubSub protobuf", e);
} }
} }
@Override @Override
public void onPMessage(String s, String s2, String s3) { public void onPMessage(byte[] s, byte[] s2, byte[] s3) {
logger.warn("Received PMessage!"); logger.warn("Received PMessage!");
} }
@Override @Override
public void onSubscribe(String channel, int count) { public void onSubscribe(byte[] channel, int count) {
if (KEEPALIVE_CHANNEL.equals(channel)) { if (Arrays.equals(KEEPALIVE_CHANNEL, channel)) {
synchronized (PubSubManager.this) { synchronized (PubSubManager.this) {
subscribed = true; subscribed = true;
PubSubManager.this.notifyAll(); PubSubManager.this.notifyAll();
@@ -145,12 +132,12 @@ public class PubSubManager {
} }
@Override @Override
public void onUnsubscribe(String s, int i) {} public void onUnsubscribe(byte[] s, int i) {}
@Override @Override
public void onPUnsubscribe(String s, int i) {} public void onPUnsubscribe(byte[] s, int i) {}
@Override @Override
public void onPSubscribe(String s, int i) {} public void onPSubscribe(byte[] s, int i) {}
} }
} }

View File

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

View File

@@ -0,0 +1,642 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: PubSubMessage.proto
package org.whispersystems.textsecuregcm.storage;
public final class PubSubProtos {
private PubSubProtos() {}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) {
}
public interface PubSubMessageOrBuilder
extends com.google.protobuf.MessageOrBuilder {
// optional .textsecure.PubSubMessage.Type type = 1;
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
boolean hasType();
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type getType();
// optional bytes content = 2;
/**
* <code>optional bytes content = 2;</code>
*/
boolean hasContent();
/**
* <code>optional bytes content = 2;</code>
*/
com.google.protobuf.ByteString getContent();
}
/**
* Protobuf type {@code textsecure.PubSubMessage}
*/
public static final class PubSubMessage extends
com.google.protobuf.GeneratedMessage
implements PubSubMessageOrBuilder {
// Use PubSubMessage.newBuilder() to construct.
private PubSubMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
super(builder);
this.unknownFields = builder.getUnknownFields();
}
private PubSubMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
private static final PubSubMessage defaultInstance;
public static PubSubMessage getDefaultInstance() {
return defaultInstance;
}
public PubSubMessage getDefaultInstanceForType() {
return defaultInstance;
}
private final com.google.protobuf.UnknownFieldSet unknownFields;
@java.lang.Override
public final com.google.protobuf.UnknownFieldSet
getUnknownFields() {
return this.unknownFields;
}
private PubSubMessage(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
initFields();
int mutable_bitField0_ = 0;
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
com.google.protobuf.UnknownFieldSet.newBuilder();
try {
boolean done = false;
while (!done) {
int tag = input.readTag();
switch (tag) {
case 0:
done = true;
break;
default: {
if (!parseUnknownField(input, unknownFields,
extensionRegistry, tag)) {
done = true;
}
break;
}
case 8: {
int rawValue = input.readEnum();
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type value = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.valueOf(rawValue);
if (value == null) {
unknownFields.mergeVarintField(1, rawValue);
} else {
bitField0_ |= 0x00000001;
type_ = value;
}
break;
}
case 18: {
bitField0_ |= 0x00000002;
content_ = input.readBytes();
break;
}
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw e.setUnfinishedMessage(this);
} catch (java.io.IOException e) {
throw new com.google.protobuf.InvalidProtocolBufferException(
e.getMessage()).setUnfinishedMessage(this);
} finally {
this.unknownFields = unknownFields.build();
makeExtensionsImmutable();
}
}
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_fieldAccessorTable
.ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.class, org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Builder.class);
}
public static com.google.protobuf.Parser<PubSubMessage> PARSER =
new com.google.protobuf.AbstractParser<PubSubMessage>() {
public PubSubMessage parsePartialFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return new PubSubMessage(input, extensionRegistry);
}
};
@java.lang.Override
public com.google.protobuf.Parser<PubSubMessage> getParserForType() {
return PARSER;
}
/**
* Protobuf enum {@code textsecure.PubSubMessage.Type}
*/
public enum Type
implements com.google.protobuf.ProtocolMessageEnum {
/**
* <code>UNKNOWN = 0;</code>
*/
UNKNOWN(0, 0),
/**
* <code>QUERY_DB = 1;</code>
*/
QUERY_DB(1, 1),
/**
* <code>DELIVER = 2;</code>
*/
DELIVER(2, 2),
/**
* <code>KEEPALIVE = 3;</code>
*/
KEEPALIVE(3, 3),
;
/**
* <code>UNKNOWN = 0;</code>
*/
public static final int UNKNOWN_VALUE = 0;
/**
* <code>QUERY_DB = 1;</code>
*/
public static final int QUERY_DB_VALUE = 1;
/**
* <code>DELIVER = 2;</code>
*/
public static final int DELIVER_VALUE = 2;
/**
* <code>KEEPALIVE = 3;</code>
*/
public static final int KEEPALIVE_VALUE = 3;
public final int getNumber() { return value; }
public static Type valueOf(int value) {
switch (value) {
case 0: return UNKNOWN;
case 1: return QUERY_DB;
case 2: return DELIVER;
case 3: return KEEPALIVE;
default: return null;
}
}
public static com.google.protobuf.Internal.EnumLiteMap<Type>
internalGetValueMap() {
return internalValueMap;
}
private static com.google.protobuf.Internal.EnumLiteMap<Type>
internalValueMap =
new com.google.protobuf.Internal.EnumLiteMap<Type>() {
public Type findValueByNumber(int number) {
return Type.valueOf(number);
}
};
public final com.google.protobuf.Descriptors.EnumValueDescriptor
getValueDescriptor() {
return getDescriptor().getValues().get(index);
}
public final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptorForType() {
return getDescriptor();
}
public static final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.getDescriptor().getEnumTypes().get(0);
}
private static final Type[] VALUES = values();
public static Type valueOf(
com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
if (desc.getType() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"EnumValueDescriptor is not for this type.");
}
return VALUES[desc.getIndex()];
}
private final int index;
private final int value;
private Type(int index, int value) {
this.index = index;
this.value = value;
}
// @@protoc_insertion_point(enum_scope:textsecure.PubSubMessage.Type)
}
private int bitField0_;
// optional .textsecure.PubSubMessage.Type type = 1;
public static final int TYPE_FIELD_NUMBER = 1;
private org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type type_;
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type getType() {
return type_;
}
// optional bytes content = 2;
public static final int CONTENT_FIELD_NUMBER = 2;
private com.google.protobuf.ByteString content_;
/**
* <code>optional bytes content = 2;</code>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes content = 2;</code>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
}
private void initFields() {
type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
content_ = com.google.protobuf.ByteString.EMPTY;
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
byte isInitialized = memoizedIsInitialized;
if (isInitialized != -1) return isInitialized == 1;
memoizedIsInitialized = 1;
return true;
}
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
getSerializedSize();
if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeEnum(1, type_.getNumber());
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeBytes(2, content_);
}
getUnknownFields().writeTo(output);
}
private int memoizedSerializedSize = -1;
public int getSerializedSize() {
int size = memoizedSerializedSize;
if (size != -1) return size;
size = 0;
if (((bitField0_ & 0x00000001) == 0x00000001)) {
size += com.google.protobuf.CodedOutputStream
.computeEnumSize(1, type_.getNumber());
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(2, content_);
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
}
private static final long serialVersionUID = 0L;
@java.lang.Override
protected java.lang.Object writeReplace()
throws java.io.ObjectStreamException {
return super.writeReplace();
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(java.io.InputStream input)
throws java.io.IOException {
return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException {
return PARSER.parseDelimitedFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseDelimitedFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseDelimitedFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.CodedInputStream input)
throws java.io.IOException {
return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parseFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseFrom(input, extensionRegistry);
}
public static Builder newBuilder() { return Builder.create(); }
public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder(org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage prototype) {
return newBuilder().mergeFrom(prototype);
}
public Builder toBuilder() { return newBuilder(this); }
@java.lang.Override
protected Builder newBuilderForType(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
/**
* Protobuf type {@code textsecure.PubSubMessage}
*/
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder>
implements org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessageOrBuilder {
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_fieldAccessorTable
.ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.class, org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Builder.class);
}
// Construct using org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.newBuilder()
private Builder() {
maybeForceBuilderInitialization();
}
private Builder(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
private void maybeForceBuilderInitialization() {
if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
}
}
private static Builder create() {
return new Builder();
}
public Builder clear() {
super.clear();
type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
bitField0_ = (bitField0_ & ~0x00000001);
content_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000002);
return this;
}
public Builder clone() {
return create().mergeFrom(buildPartial());
}
public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.internal_static_textsecure_PubSubMessage_descriptor;
}
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage getDefaultInstanceForType() {
return org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.getDefaultInstance();
}
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage build() {
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(result);
}
return result;
}
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage buildPartial() {
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage result = new org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage(this);
int from_bitField0_ = bitField0_;
int to_bitField0_ = 0;
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
to_bitField0_ |= 0x00000001;
}
result.type_ = type_;
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
to_bitField0_ |= 0x00000002;
}
result.content_ = content_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
}
public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage) {
return mergeFrom((org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage)other);
} else {
super.mergeFrom(other);
return this;
}
}
public Builder mergeFrom(org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage other) {
if (other == org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.getDefaultInstance()) return this;
if (other.hasType()) {
setType(other.getType());
}
if (other.hasContent()) {
setContent(other.getContent());
}
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
public final boolean isInitialized() {
return true;
}
public Builder mergeFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage parsedMessage = null;
try {
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
parsedMessage = (org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage) e.getUnfinishedMessage();
throw e;
} finally {
if (parsedMessage != null) {
mergeFrom(parsedMessage);
}
}
return this;
}
private int bitField0_;
// optional .textsecure.PubSubMessage.Type type = 1;
private org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type getType() {
return type_;
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public Builder setType(org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000001;
type_ = value;
onChanged();
return this;
}
/**
* <code>optional .textsecure.PubSubMessage.Type type = 1;</code>
*/
public Builder clearType() {
bitField0_ = (bitField0_ & ~0x00000001);
type_ = org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage.Type.UNKNOWN;
onChanged();
return this;
}
// optional bytes content = 2;
private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes content = 2;</code>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes content = 2;</code>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
}
/**
* <code>optional bytes content = 2;</code>
*/
public Builder setContent(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000002;
content_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes content = 2;</code>
*/
public Builder clearContent() {
bitField0_ = (bitField0_ & ~0x00000002);
content_ = getDefaultInstance().getContent();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.PubSubMessage)
}
static {
defaultInstance = new PubSubMessage(true);
defaultInstance.initFields();
}
// @@protoc_insertion_point(class_scope:textsecure.PubSubMessage)
}
private static com.google.protobuf.Descriptors.Descriptor
internal_static_textsecure_PubSubMessage_descriptor;
private static
com.google.protobuf.GeneratedMessage.FieldAccessorTable
internal_static_textsecure_PubSubMessage_fieldAccessorTable;
public static com.google.protobuf.Descriptors.FileDescriptor
getDescriptor() {
return descriptor;
}
private static com.google.protobuf.Descriptors.FileDescriptor
descriptor;
static {
java.lang.String[] descriptorData = {
"\n\023PubSubMessage.proto\022\ntextsecure\"\215\001\n\rPu" +
"bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" +
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"=\n\004Ty" +
"pe\022\013\n\007UNKNOWN\020\000\022\014\n\010QUERY_DB\020\001\022\013\n\007DELIVER" +
"\020\002\022\r\n\tKEEPALIVE\020\003B8\n(org.whispersystems." +
"textsecuregcm.storageB\014PubSubProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
public com.google.protobuf.ExtensionRegistry assignDescriptors(
com.google.protobuf.Descriptors.FileDescriptor root) {
descriptor = root;
internal_static_textsecure_PubSubMessage_descriptor =
getDescriptor().getMessageTypes().get(0);
internal_static_textsecure_PubSubMessage_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_PubSubMessage_descriptor,
new java.lang.String[] { "Type", "Content", });
return null;
}
};
com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
}, assigner);
}
// @@protoc_insertion_point(outer_class_scope)
}

View File

@@ -0,0 +1,624 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: StoredMessage.proto
package org.whispersystems.textsecuregcm.storage;
public final class StoredMessageProtos {
private StoredMessageProtos() {}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistry registry) {
}
public interface StoredMessageOrBuilder
extends com.google.protobuf.MessageOrBuilder {
// optional .textsecure.StoredMessage.Type type = 1;
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
boolean hasType();
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type getType();
// optional bytes content = 2;
/**
* <code>optional bytes content = 2;</code>
*/
boolean hasContent();
/**
* <code>optional bytes content = 2;</code>
*/
com.google.protobuf.ByteString getContent();
}
/**
* Protobuf type {@code textsecure.StoredMessage}
*/
public static final class StoredMessage extends
com.google.protobuf.GeneratedMessage
implements StoredMessageOrBuilder {
// Use StoredMessage.newBuilder() to construct.
private StoredMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
super(builder);
this.unknownFields = builder.getUnknownFields();
}
private StoredMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
private static final StoredMessage defaultInstance;
public static StoredMessage getDefaultInstance() {
return defaultInstance;
}
public StoredMessage getDefaultInstanceForType() {
return defaultInstance;
}
private final com.google.protobuf.UnknownFieldSet unknownFields;
@java.lang.Override
public final com.google.protobuf.UnknownFieldSet
getUnknownFields() {
return this.unknownFields;
}
private StoredMessage(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
initFields();
int mutable_bitField0_ = 0;
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
com.google.protobuf.UnknownFieldSet.newBuilder();
try {
boolean done = false;
while (!done) {
int tag = input.readTag();
switch (tag) {
case 0:
done = true;
break;
default: {
if (!parseUnknownField(input, unknownFields,
extensionRegistry, tag)) {
done = true;
}
break;
}
case 8: {
int rawValue = input.readEnum();
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type value = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.valueOf(rawValue);
if (value == null) {
unknownFields.mergeVarintField(1, rawValue);
} else {
bitField0_ |= 0x00000001;
type_ = value;
}
break;
}
case 18: {
bitField0_ |= 0x00000002;
content_ = input.readBytes();
break;
}
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw e.setUnfinishedMessage(this);
} catch (java.io.IOException e) {
throw new com.google.protobuf.InvalidProtocolBufferException(
e.getMessage()).setUnfinishedMessage(this);
} finally {
this.unknownFields = unknownFields.build();
makeExtensionsImmutable();
}
}
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_fieldAccessorTable
.ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.class, org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Builder.class);
}
public static com.google.protobuf.Parser<StoredMessage> PARSER =
new com.google.protobuf.AbstractParser<StoredMessage>() {
public StoredMessage parsePartialFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return new StoredMessage(input, extensionRegistry);
}
};
@java.lang.Override
public com.google.protobuf.Parser<StoredMessage> getParserForType() {
return PARSER;
}
/**
* Protobuf enum {@code textsecure.StoredMessage.Type}
*/
public enum Type
implements com.google.protobuf.ProtocolMessageEnum {
/**
* <code>UNKNOWN = 0;</code>
*/
UNKNOWN(0, 0),
/**
* <code>MESSAGE = 1;</code>
*/
MESSAGE(1, 1),
;
/**
* <code>UNKNOWN = 0;</code>
*/
public static final int UNKNOWN_VALUE = 0;
/**
* <code>MESSAGE = 1;</code>
*/
public static final int MESSAGE_VALUE = 1;
public final int getNumber() { return value; }
public static Type valueOf(int value) {
switch (value) {
case 0: return UNKNOWN;
case 1: return MESSAGE;
default: return null;
}
}
public static com.google.protobuf.Internal.EnumLiteMap<Type>
internalGetValueMap() {
return internalValueMap;
}
private static com.google.protobuf.Internal.EnumLiteMap<Type>
internalValueMap =
new com.google.protobuf.Internal.EnumLiteMap<Type>() {
public Type findValueByNumber(int number) {
return Type.valueOf(number);
}
};
public final com.google.protobuf.Descriptors.EnumValueDescriptor
getValueDescriptor() {
return getDescriptor().getValues().get(index);
}
public final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptorForType() {
return getDescriptor();
}
public static final com.google.protobuf.Descriptors.EnumDescriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.getDescriptor().getEnumTypes().get(0);
}
private static final Type[] VALUES = values();
public static Type valueOf(
com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
if (desc.getType() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"EnumValueDescriptor is not for this type.");
}
return VALUES[desc.getIndex()];
}
private final int index;
private final int value;
private Type(int index, int value) {
this.index = index;
this.value = value;
}
// @@protoc_insertion_point(enum_scope:textsecure.StoredMessage.Type)
}
private int bitField0_;
// optional .textsecure.StoredMessage.Type type = 1;
public static final int TYPE_FIELD_NUMBER = 1;
private org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type type_;
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type getType() {
return type_;
}
// optional bytes content = 2;
public static final int CONTENT_FIELD_NUMBER = 2;
private com.google.protobuf.ByteString content_;
/**
* <code>optional bytes content = 2;</code>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes content = 2;</code>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
}
private void initFields() {
type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
content_ = com.google.protobuf.ByteString.EMPTY;
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
byte isInitialized = memoizedIsInitialized;
if (isInitialized != -1) return isInitialized == 1;
memoizedIsInitialized = 1;
return true;
}
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
getSerializedSize();
if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeEnum(1, type_.getNumber());
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeBytes(2, content_);
}
getUnknownFields().writeTo(output);
}
private int memoizedSerializedSize = -1;
public int getSerializedSize() {
int size = memoizedSerializedSize;
if (size != -1) return size;
size = 0;
if (((bitField0_ & 0x00000001) == 0x00000001)) {
size += com.google.protobuf.CodedOutputStream
.computeEnumSize(1, type_.getNumber());
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(2, content_);
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
}
private static final long serialVersionUID = 0L;
@java.lang.Override
protected java.lang.Object writeReplace()
throws java.io.ObjectStreamException {
return super.writeReplace();
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
com.google.protobuf.ByteString data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
com.google.protobuf.ByteString data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
byte[] data,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(java.io.InputStream input)
throws java.io.IOException {
return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseDelimitedFrom(java.io.InputStream input)
throws java.io.IOException {
return PARSER.parseDelimitedFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseDelimitedFrom(
java.io.InputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseDelimitedFrom(input, extensionRegistry);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
com.google.protobuf.CodedInputStream input)
throws java.io.IOException {
return PARSER.parseFrom(input);
}
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
return PARSER.parseFrom(input, extensionRegistry);
}
public static Builder newBuilder() { return Builder.create(); }
public Builder newBuilderForType() { return newBuilder(); }
public static Builder newBuilder(org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage prototype) {
return newBuilder().mergeFrom(prototype);
}
public Builder toBuilder() { return newBuilder(this); }
@java.lang.Override
protected Builder newBuilderForType(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
Builder builder = new Builder(parent);
return builder;
}
/**
* Protobuf type {@code textsecure.StoredMessage}
*/
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder>
implements org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessageOrBuilder {
public static final com.google.protobuf.Descriptors.Descriptor
getDescriptor() {
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_descriptor;
}
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
internalGetFieldAccessorTable() {
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_fieldAccessorTable
.ensureFieldAccessorsInitialized(
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.class, org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Builder.class);
}
// Construct using org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.newBuilder()
private Builder() {
maybeForceBuilderInitialization();
}
private Builder(
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
super(parent);
maybeForceBuilderInitialization();
}
private void maybeForceBuilderInitialization() {
if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
}
}
private static Builder create() {
return new Builder();
}
public Builder clear() {
super.clear();
type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
bitField0_ = (bitField0_ & ~0x00000001);
content_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000002);
return this;
}
public Builder clone() {
return create().mergeFrom(buildPartial());
}
public com.google.protobuf.Descriptors.Descriptor
getDescriptorForType() {
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_descriptor;
}
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage getDefaultInstanceForType() {
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.getDefaultInstance();
}
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage build() {
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage result = buildPartial();
if (!result.isInitialized()) {
throw newUninitializedMessageException(result);
}
return result;
}
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage buildPartial() {
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage result = new org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage(this);
int from_bitField0_ = bitField0_;
int to_bitField0_ = 0;
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
to_bitField0_ |= 0x00000001;
}
result.type_ = type_;
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
to_bitField0_ |= 0x00000002;
}
result.content_ = content_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
}
public Builder mergeFrom(com.google.protobuf.Message other) {
if (other instanceof org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage) {
return mergeFrom((org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage)other);
} else {
super.mergeFrom(other);
return this;
}
}
public Builder mergeFrom(org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage other) {
if (other == org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.getDefaultInstance()) return this;
if (other.hasType()) {
setType(other.getType());
}
if (other.hasContent()) {
setContent(other.getContent());
}
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
public final boolean isInitialized() {
return true;
}
public Builder mergeFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws java.io.IOException {
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parsedMessage = null;
try {
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
parsedMessage = (org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage) e.getUnfinishedMessage();
throw e;
} finally {
if (parsedMessage != null) {
mergeFrom(parsedMessage);
}
}
return this;
}
private int bitField0_;
// optional .textsecure.StoredMessage.Type type = 1;
private org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
public boolean hasType() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type getType() {
return type_;
}
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
public Builder setType(org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000001;
type_ = value;
onChanged();
return this;
}
/**
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
*/
public Builder clearType() {
bitField0_ = (bitField0_ & ~0x00000001);
type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
onChanged();
return this;
}
// optional bytes content = 2;
private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes content = 2;</code>
*/
public boolean hasContent() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes content = 2;</code>
*/
public com.google.protobuf.ByteString getContent() {
return content_;
}
/**
* <code>optional bytes content = 2;</code>
*/
public Builder setContent(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000002;
content_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes content = 2;</code>
*/
public Builder clearContent() {
bitField0_ = (bitField0_ & ~0x00000002);
content_ = getDefaultInstance().getContent();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.StoredMessage)
}
static {
defaultInstance = new StoredMessage(true);
defaultInstance.initFields();
}
// @@protoc_insertion_point(class_scope:textsecure.StoredMessage)
}
private static com.google.protobuf.Descriptors.Descriptor
internal_static_textsecure_StoredMessage_descriptor;
private static
com.google.protobuf.GeneratedMessage.FieldAccessorTable
internal_static_textsecure_StoredMessage_fieldAccessorTable;
public static com.google.protobuf.Descriptors.FileDescriptor
getDescriptor() {
return descriptor;
}
private static com.google.protobuf.Descriptors.FileDescriptor
descriptor;
static {
java.lang.String[] descriptorData = {
"\n\023StoredMessage.proto\022\ntextsecure\"p\n\rSto" +
"redMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.St" +
"oredMessage.Type\022\017\n\007content\030\002 \001(\014\" \n\004Typ" +
"e\022\013\n\007UNKNOWN\020\000\022\013\n\007MESSAGE\020\001B?\n(org.whisp" +
"ersystems.textsecuregcm.storageB\023StoredM" +
"essageProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
public com.google.protobuf.ExtensionRegistry assignDescriptors(
com.google.protobuf.Descriptors.FileDescriptor root) {
descriptor = root;
internal_static_textsecure_StoredMessage_descriptor =
getDescriptor().getMessageTypes().get(0);
internal_static_textsecure_StoredMessage_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_StoredMessage_descriptor,
new java.lang.String[] { "Type", "Content", });
return null;
}
};
com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
}, assigner);
}
// @@protoc_insertion_point(outer_class_scope)
}

View File

@@ -19,21 +19,19 @@ package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.Histogram; import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.core.JsonProcessingException; import com.google.protobuf.InvalidProtocolBufferException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress; import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name; import static com.codahale.metrics.MetricRegistry.name;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage;
import redis.clients.jedis.Jedis; import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
@@ -44,8 +42,6 @@ public class StoredMessages {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size")); private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
private static final ObjectMapper mapper = SystemMapper.getMapper();
private static final String QUEUE_PREFIX = "msgs"; private static final String QUEUE_PREFIX = "msgs";
private final JedisPool jedisPool; private final JedisPool jedisPool;
@@ -55,64 +51,57 @@ public class StoredMessages {
} }
public void clear(WebsocketAddress address) { public void clear(WebsocketAddress address) {
Jedis jedis = null; try (Jedis jedis = jedisPool.getResource()) {
try {
jedis = jedisPool.getResource();
jedis.del(getKey(address)); jedis.del(getKey(address));
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
} }
} }
public void insert(WebsocketAddress address, PendingMessage message) { public void insert(WebsocketAddress address, OutgoingMessageSignal message) {
Jedis jedis = null; try (Jedis jedis = jedisPool.getResource()) {
byte[] queue = getKey(address);
StoredMessage storedMessage = StoredMessage.newBuilder()
.setType(StoredMessage.Type.MESSAGE)
.setContent(message.toByteString())
.build();
try { long queueSize = jedis.lpush(queue, storedMessage.toByteArray());
jedis = jedisPool.getResource();
String serializedMessage = mapper.writeValueAsString(message);
long queueSize = jedis.lpush(getKey(address), serializedMessage);
queueSizeHistogram.update(queueSize); queueSizeHistogram.update(queueSize);
jedis.expireAt(queue, (System.currentTimeMillis() / 1000) + TimeUnit.DAYS.toSeconds(30));
if (queueSize > 1000) { if (queueSize > 1000) {
jedis.ltrim(getKey(address), 0, 999); jedis.ltrim(getKey(address), 0, 999);
} }
} catch (JsonProcessingException e) {
logger.warn("StoredMessages", "Unable to store correctly", e);
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
} }
} }
public List<PendingMessage> getMessagesForDevice(WebsocketAddress address) { public List<OutgoingMessageSignal> getMessagesForDevice(WebsocketAddress address) {
List<PendingMessage> messages = new LinkedList<>(); List<OutgoingMessageSignal> messages = new LinkedList<>();
Jedis jedis = null;
try { try (Jedis jedis = jedisPool.getResource()) {
jedis = jedisPool.getResource(); byte[] message;
String message;
while ((message = jedis.rpop(getKey(address))) != null) { while ((message = jedis.rpop(getKey(address))) != null) {
try { try {
messages.add(mapper.readValue(message, PendingMessage.class)); StoredMessage storedMessage = StoredMessage.parseFrom(message);
} catch (IOException e) {
logger.warn("StoredMessages", "Not a valid PendingMessage", e); if (storedMessage.getType().getNumber() == StoredMessage.Type.MESSAGE_VALUE) {
messages.add(OutgoingMessageSignal.parseFrom(storedMessage.getContent()));
} else {
logger.warn("Unkown stored message type: " + storedMessage.getType().getNumber());
}
} catch (InvalidProtocolBufferException e) {
logger.warn("Error parsing protobuf", e);
} }
} }
return messages; return messages;
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
} }
} }
private String getKey(WebsocketAddress address) { private byte[] getKey(WebsocketAddress address) {
return QUEUE_PREFIX + ":" + address.serialize(); return (QUEUE_PREFIX + ":" + address.serialize()).getBytes();
} }
} }

View File

@@ -4,6 +4,7 @@ import com.google.common.base.Optional;
import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.websocket.auth.AuthenticationException; import org.whispersystems.websocket.auth.AuthenticationException;
import org.whispersystems.websocket.auth.WebSocketAuthenticator; import org.whispersystems.websocket.auth.WebSocketAuthenticator;

View File

@@ -1,13 +1,14 @@
package org.whispersystems.textsecuregcm.websocket; package org.whispersystems.textsecuregcm.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PendingMessage; import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException; import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
@@ -16,23 +17,20 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubListener; import org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages; import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.websocket.WebSocketClient; import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.messages.WebSocketResponseMessage; import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.IOException;
import java.util.List; import java.util.List;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public class WebSocketConnection implements PubSubListener { public class WebSocketConnection implements PubSubListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class); private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private static final ObjectMapper objectMapper = SystemMapper.getMapper();
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final PushSender pushSender; private final PushSender pushSender;
@@ -72,52 +70,56 @@ public class WebSocketConnection implements PubSubListener {
} }
@Override @Override
public void onPubSubMessage(PubSubMessage message) { public void onPubSubMessage(PubSubMessage pubSubMessage) {
try { try {
switch (message.getType()) { switch (pubSubMessage.getType().getNumber()) {
case PubSubMessage.TYPE_QUERY_DB: case PubSubMessage.Type.QUERY_DB_VALUE:
processStoredMessages(); processStoredMessages();
break; break;
case PubSubMessage.TYPE_DELIVER: case PubSubMessage.Type.DELIVER_VALUE:
PendingMessage pendingMessage = objectMapper.readValue(message.getContents(), sendMessage(OutgoingMessageSignal.parseFrom(pubSubMessage.getContent()));
PendingMessage.class);
sendMessage(pendingMessage);
break; break;
default: default:
logger.warn("Unknown pubsub message: " + message.getType()); logger.warn("Unknown pubsub message: " + pubSubMessage.getType().getNumber());
} }
} catch (IOException e) { } catch (InvalidProtocolBufferException e) {
logger.warn("Error deserializing PendingMessage", e); logger.warn("Protobuf parse error", e);
} }
} }
private void sendMessage(final PendingMessage message) { private void sendMessage(final OutgoingMessageSignal message) {
String content = message.getEncryptedOutgoingMessage(); try {
Optional<byte[]> body = Optional.fromNullable(content.getBytes()); EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/api/v1/message", body); Optional<byte[]> body = Optional.fromNullable(encryptedMessage.toByteArray());
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/api/v1/message", body);
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() { Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
@Override @Override
public void onSuccess(@Nullable WebSocketResponseMessage response) { public void onSuccess(@Nullable WebSocketResponseMessage response) {
if (isSuccessResponse(response) && !message.isReceipt()) { boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
sendDeliveryReceiptFor(message);
} else if (!isSuccessResponse(response)) { if (isSuccessResponse(response) && !isReceipt) {
sendDeliveryReceiptFor(message);
} else if (!isSuccessResponse(response)) {
requeueMessage(message);
}
}
@Override
public void onFailure(@Nonnull Throwable throwable) {
requeueMessage(message); requeueMessage(message);
} }
}
@Override private boolean isSuccessResponse(WebSocketResponseMessage response) {
public void onFailure(@Nonnull Throwable throwable) { return response != null && response.getStatus() >= 200 && response.getStatus() < 300;
requeueMessage(message); }
} });
} catch (CryptoEncodingException e) {
private boolean isSuccessResponse(WebSocketResponseMessage response) { logger.warn("Bad signaling key", e);
return response != null && response.getStatus() >= 200 && response.getStatus() < 300; }
}
});
} }
private void requeueMessage(PendingMessage message) { private void requeueMessage(OutgoingMessageSignal message) {
try { try {
pushSender.sendMessage(account, device, message); pushSender.sendMessage(account, device, message);
} catch (NotPushRegisteredException | TransientPushFailureException e) { } catch (NotPushRegisteredException | TransientPushFailureException e) {
@@ -126,12 +128,12 @@ public class WebSocketConnection implements PubSubListener {
} }
} }
private void sendDeliveryReceiptFor(PendingMessage message) { private void sendDeliveryReceiptFor(OutgoingMessageSignal message) {
try { try {
Optional<Account> source = accountsManager.get(message.getSender()); Optional<Account> source = accountsManager.get(message.getSource());
if (!source.isPresent()) { if (!source.isPresent()) {
logger.warn("Source account disappeared? (%s)", message.getSender()); logger.warn("Source account disappeared? (%s)", message.getSource());
return; return;
} }
@@ -139,7 +141,7 @@ public class WebSocketConnection implements PubSubListener {
OutgoingMessageSignal.newBuilder() OutgoingMessageSignal.newBuilder()
.setSource(account.getNumber()) .setSource(account.getNumber())
.setSourceDevice((int) device.getId()) .setSourceDevice((int) device.getId())
.setTimestamp(message.getMessageId()) .setTimestamp(message.getTimestamp())
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE); .setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
for (Device device : source.get().getDevices()) { for (Device device : source.get().getDevices()) {
@@ -151,9 +153,9 @@ public class WebSocketConnection implements PubSubListener {
} }
private void processStoredMessages() { private void processStoredMessages() {
List<PendingMessage> messages = storedMessages.getMessagesForDevice(address); List<OutgoingMessageSignal> messages = storedMessages.getMessagesForDevice(address);
for (PendingMessage message : messages) { for (OutgoingMessageSignal message : messages) {
sendMessage(message); sendMessage(message);
} }
} }

View File

@@ -63,7 +63,7 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
Accounts accounts = dbi.onDemand(Accounts.class); Accounts accounts = dbi.onDemand(Accounts.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient(); MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool(); JedisPool redisClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient); DirectoryManager directory = new DirectoryManager(redisClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration()); FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());

View File

@@ -29,6 +29,7 @@ import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationH
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -37,6 +38,8 @@ import static org.whispersystems.textsecuregcm.storage.DirectoryManager.PendingC
public class DirectoryUpdater { public class DirectoryUpdater {
private static final int CHUNK_SIZE = 10000;
private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class); private final Logger logger = LoggerFactory.getLogger(DirectoryUpdater.class);
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
@@ -53,33 +56,40 @@ public class DirectoryUpdater {
} }
public void updateFromLocalDatabase() { public void updateFromLocalDatabase() {
BatchOperationHandle batchOperation = directory.startBatchOperation(); int contactsAdded = 0;
int contactsRemoved = 0;
BatchOperationHandle batchOperation = directory.startBatchOperation();
try { try {
Iterator<Account> accounts = accountsManager.getAll(); logger.info("Updating from local DB.");
int offset = 0;
if (accounts == null) for (;;) {
return; List<Account> accounts = accountsManager.getAll(offset, CHUNK_SIZE);
while (accounts.hasNext()) { if (accounts == null || accounts.isEmpty()) break;
Account account = accounts.next(); else offset += accounts.size();
if (account.isActive()) { for (Account account : accounts) {
byte[] token = Util.getContactToken(account.getNumber()); if (account.isActive()) {
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
directory.add(batchOperation, clientContact); directory.add(batchOperation, clientContact);
contactsAdded++;
logger.debug("Adding local token: " + Base64.encodeBytesWithoutPadding(token)); } else {
} else { directory.remove(batchOperation, account.getNumber());
directory.remove(batchOperation, account.getNumber()); contactsRemoved++;
}
} }
logger.info("Processed " + CHUNK_SIZE + " local accounts...");
} }
} finally { } finally {
directory.stopBatchOperation(batchOperation); directory.stopBatchOperation(batchOperation);
} }
logger.info("Local directory is updated."); logger.info(String.format("Local directory is updated (%d added, %d removed).", contactsAdded, contactsRemoved));
} }
public void updateFromPeers() { public void updateFromPeers() {
@@ -121,19 +131,23 @@ public class DirectoryUpdater {
Iterator<PendingClientContact> localContactIterator = localContacts.iterator(); Iterator<PendingClientContact> localContactIterator = localContacts.iterator();
while (remoteContactIterator.hasNext() && localContactIterator.hasNext()) { while (remoteContactIterator.hasNext() && localContactIterator.hasNext()) {
ClientContact remoteContact = remoteContactIterator.next(); try {
Optional<ClientContact> localContact = localContactIterator.next().get(); ClientContact remoteContact = remoteContactIterator.next();
Optional<ClientContact> localContact = localContactIterator.next().get();
remoteContact.setRelay(client.getPeerName()); remoteContact.setRelay(client.getPeerName());
if (!remoteContact.isInactive() && (!localContact.isPresent() || client.getPeerName().equals(localContact.get().getRelay()))) { if (!remoteContact.isInactive() && (!localContact.isPresent() || client.getPeerName().equals(localContact.get().getRelay()))) {
contactsAdded++; contactsAdded++;
directory.add(handle, remoteContact); directory.add(handle, remoteContact);
} else { } else {
if (localContact.isPresent() && client.getPeerName().equals(localContact.get().getRelay())) { if (localContact.isPresent() && client.getPeerName().equals(localContact.get().getRelay())) {
contactsRemoved++; contactsRemoved++;
directory.remove(handle, remoteContact.getToken()); directory.remove(handle, remoteContact.getToken());
}
} }
} catch (IOException e) {
logger.warn("JSON Serialization Failed: ", e);
} }
} }

View File

@@ -0,0 +1 @@
org.whispersystems.textsecuregcm.metrics.JsonMetricsReporterFactory

View File

@@ -2,19 +2,19 @@ package org.whispersystems.textsecuregcm.tests.websocket;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.junit.Test; import org.junit.Test;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.PendingMessage;
import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages; import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.websocket.ConnectListener; import org.whispersystems.textsecuregcm.websocket.ConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator; import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection; import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
@@ -32,6 +32,7 @@ import io.dropwizard.auth.basic.BasicCredentials;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
public class WebSocketConnectionTest { public class WebSocketConnectionTest {
@@ -112,13 +113,15 @@ public class WebSocketConnectionTest {
public void testOpen() throws Exception { public void testOpen() throws Exception {
StoredMessages storedMessages = mock(StoredMessages.class); StoredMessages storedMessages = mock(StoredMessages.class);
List<PendingMessage> outgoingMessages = new LinkedList<PendingMessage>() {{ List<OutgoingMessageSignal> outgoingMessages = new LinkedList<OutgoingMessageSignal>() {{
add(new PendingMessage("sender1", 1111, false, "first")); add(createMessage("sender1", 1111, false, "first"));
add(new PendingMessage("sender1", 2222, false, "second")); add(createMessage("sender1", 2222, false, "second"));
add(new PendingMessage("sender2", 3333, false, "third")); add(createMessage("sender2", 3333, false, "third"));
}}; }};
when(device.getId()).thenReturn(2L); when(device.getId()).thenReturn(2L);
when(device.getSignalingKey()).thenReturn(Base64.encodeBytes(new byte[52]));
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device)); when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
when(account.getNumber()).thenReturn("+14152222222"); when(account.getNumber()).thenReturn("+14152222222");
@@ -167,16 +170,26 @@ public class WebSocketConnectionTest {
futures.get(0).setException(new IOException()); futures.get(0).setException(new IOException());
futures.get(2).setException(new IOException()); futures.get(2).setException(new IOException());
List<PendingMessage> pending = new LinkedList<PendingMessage>() {{ List<OutgoingMessageSignal> pending = new LinkedList<OutgoingMessageSignal>() {{
add(new PendingMessage("sender1", 1111, false, "first")); add(createMessage("sender1", 1111, false, "first"));
add(new PendingMessage("sender2", 3333, false, "third")); add(createMessage("sender2", 3333, false, "third"));
}}; }};
verify(pushSender, times(2)).sendMessage(eq(account), eq(device), any(PendingMessage.class)); verify(pushSender, times(2)).sendMessage(eq(account), eq(device), any(OutgoingMessageSignal.class));
verify(pushSender, times(1)).sendMessage(eq(sender1), eq(sender1device), any(MessageProtos.OutgoingMessageSignal.class)); verify(pushSender, times(1)).sendMessage(eq(sender1), eq(sender1device), any(OutgoingMessageSignal.class));
connection.onConnectionLost(); connection.onConnectionLost();
verify(pubSubManager).unsubscribe(eq(new WebsocketAddress("+14152222222", 2L)), eq(connection)); verify(pubSubManager).unsubscribe(eq(new WebsocketAddress("+14152222222", 2L)), eq(connection));
} }
private OutgoingMessageSignal createMessage(String sender, long timestamp, boolean receipt, String content) {
return OutgoingMessageSignal.newBuilder()
.setSource(sender)
.setSourceDevice(1)
.setType(receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE)
.setTimestamp(timestamp)
.setMessage(ByteString.copyFrom(content.getBytes()))
.build();
}
} }