Compare commits

...

63 Commits
v0.18 ... v0.35

Author SHA1 Message Date
Moxie Marlinspike
887f49760f Bump version to 0.35
// FREEBIE
2015-03-07 09:25:24 -08:00
Moxie Marlinspike
be77f2291b Temporarily disable GCM websocket notifications.
Workaround for a client bug.

// FREEBIE
2015-03-07 09:24:55 -08:00
Moxie Marlinspike
b585b849a1 Bump version to 0.34 2015-03-05 11:59:41 -08:00
Moxie Marlinspike
0c94e3d994 Don't print the full stack trace for twilio exceptions. 2015-03-05 08:37:27 -08:00
Moxie Marlinspike
4a93658d0f Don't consider an empty string to be a possible relay.
// FREEBIE
2015-03-05 08:36:56 -08:00
Moxie Marlinspike
6da19c6254 Set registration id when newly provisioned device registers. 2015-03-05 08:36:30 -08:00
Moxie Marlinspike
289058be81 Bump version to 0.33 2015-02-23 21:26:27 -08:00
Moxie Marlinspike
864675ecde Return stored messages in order.
// FREEBIE
2015-02-23 12:14:41 -08:00
Moxie Marlinspike
c79d7e3e30 Close existing websocket connection for a device on new connect.
// FREEBIE
2015-02-23 12:11:07 -08:00
Moxie Marlinspike
549cc6f492 Bump version to 0.32
// FREEBIE
2015-02-23 11:27:25 -08:00
Moxie Marlinspike
aa84ab66af Support for GCM stored messages.
// FREEBIE
2015-02-04 14:19:50 -08:00
Moxie Marlinspike
1fef812c67 Bump version to 0.31
// FREEBIE
2015-02-02 09:37:50 -08:00
Moxie Marlinspike
9170f74887 Vacuum messages now too.
// FREEBIE
2015-02-02 08:59:32 -08:00
Moxie Marlinspike
c9bd700d31 Bump version to 0.30
// FREEBIE
2015-02-02 08:55:11 -08:00
Moxie Marlinspike
0928e4c035 Fix leaky bucket serialization.
// FREEBIE
2015-01-30 11:48:49 -08:00
Moxie Marlinspike
75aec0a8d4 Switch to Redis for all caching.
// FREEBIE
2015-01-29 15:37:28 -08:00
Moxie Marlinspike
1f5ee36a6b Switch to postgresql-backed message DB.
// FREEBIE
2015-01-29 13:25:33 -08:00
Moxie Marlinspike
45a0b74b89 Device provisioning fixes.
// FREEBIE
2015-01-21 15:15:40 -08:00
Moxie Marlinspike
f7132bdbbc Rearrange provisioning flow. Add needsMessageSync response.
// FREEBIE
2015-01-21 13:56:58 -08:00
Moxie Marlinspike
d2dbff173a Adjust encoding 2015-01-19 19:03:06 -08:00
Moxie Marlinspike
79f83babb3 Support for ephemeral provisioning communication channels.
// FREEBIE
2015-01-18 18:09:25 -08:00
Moxie Marlinspike
715181f830 Remove duplicate dependency.
// FREEBIE

Fixes #26
2015-01-03 20:26:11 -08:00
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
Moxie Marlinspike
8a2131416d Bump version to 0.24
// FREEBIE
2014-11-27 16:24:27 -08:00
Moxie Marlinspike
2525304215 Account for websocket-resources changes.
// FREEBIE
2014-11-15 09:48:09 -08:00
Moxie Marlinspike
fdb35d4f77 Switch to WebSocket-Resources
// FREEBIE
2014-11-14 17:59:50 -08:00
Moxie Marlinspike
222c7ea641 Support for signature token based account verification. 2014-11-13 14:56:24 -08:00
Moxie Marlinspike
8f2722263f Bump version to 0.23 2014-11-04 19:33:07 -08:00
Moxie Marlinspike
fd662e3401 Add vacuum command.
// FREEBIE
2014-11-04 19:32:35 -08:00
Moxie Marlinspike
bc65461ecb Bump version to 0.22 2014-10-01 15:03:25 -07:00
Moxie Marlinspike
30017371df Reconnect even when Smack thinks it doesn't need to. 2014-10-01 14:07:12 -07:00
Moxie Marlinspike
b944b86bf8 Bump version to 0.21
// FREEBIE
2014-07-30 11:45:45 -07:00
Moxie Marlinspike
6ba8352fa6 Update sample config to include GCM senderId
// FREEBIE
2014-07-30 11:38:23 -07:00
Moxie Marlinspike
aadf76692e Bump version to 0.20
// FREEBIE
2014-07-30 11:36:54 -07:00
Moxie Marlinspike
c9a1386a55 Fix for PubSub channel.
1) Create channels based on numbers rather than DB row ids.

2) Ensure that stored messages are cleared at reregistration
   time.
2014-07-26 20:41:25 -07:00
Moxie Marlinspike
4eb88a3e02 Server side support for delivery receipts. 2014-07-25 15:48:34 -07:00
Moxie Marlinspike
160c0bfe14 Switch from Java serialization to JSON for memcache storage. 2014-07-23 18:02:35 -07:00
Moxie Marlinspike
4cd098af1d Switch to GCM CCS and add support for APN feedback processing. 2014-07-23 18:00:49 -07:00
Moxie Marlinspike
362abd618f Bump version to 0.19
// FREEBIE
2014-07-21 01:20:57 -07:00
Moxie Marlinspike
69de9f6684 Fix stored message retrieval.
// FREEBIE
2014-07-21 01:20:14 -07:00
105 changed files with 5344 additions and 2056 deletions

View File

@@ -15,6 +15,7 @@ nexmo:
number:
gcm:
senderId:
apiKey:
# Optional. Only if iOS clients are supported.

34
pom.xml
View File

@@ -9,10 +9,10 @@
<groupId>org.whispersystems.textsecure</groupId>
<artifactId>TextSecureServer</artifactId>
<version>0.18</version>
<version>0.35</version>
<properties>
<dropwizard.version>0.7.0</dropwizard.version>
<dropwizard.version>0.7.1</dropwizard.version>
<jackson.api.version>2.3.3</jackson.api.version>
<commons-codec.version>1.6</commons-codec.version>
</properties>
@@ -53,6 +53,12 @@
<artifactId>dropwizard-metrics-graphite</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>com.dcsquare</groupId>
<artifactId>dropwizard-papertrail</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
@@ -80,16 +86,6 @@
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.notnoop.apns</groupId>
<artifactId>apns</artifactId>
@@ -104,13 +100,13 @@
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.4.1</version>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.2.1</version>
<version>2.6.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
@@ -125,6 +121,16 @@
<artifactId>postgresql</artifactId>
<version>9.1-901.jdbc4</version>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-tcp</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>websocket-resources</artifactId>
<version>0.2.1</version>
</dependency>
</dependencies>

View File

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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* Copyright (C) 2013 - 2015 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
@@ -20,6 +20,15 @@ option java_package = "org.whispersystems.textsecuregcm.entities";
option java_outer_classname = "MessageProtos";
message OutgoingMessageSignal {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
optional uint32 type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
@@ -27,4 +36,8 @@ message OutgoingMessageSignal {
// repeated string destinations = 4;
optional uint64 timestamp = 5;
optional bytes message = 6;
}
message ProvisioningUuid {
optional string uuid = 1;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open WhisperSystems
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -14,33 +14,20 @@
* 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.configuration;
package textsecure;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
option java_package = "org.whispersystems.textsecuregcm.storage";
option java_outer_classname = "PubSubProtos";
public class MemcacheConfiguration {
@NotEmpty
@JsonProperty
private String servers;
@JsonProperty
private String user;
@JsonProperty
private String password;
public String getServers() {
return servers;
message PubSubMessage {
enum Type {
UNKNOWN = 0;
QUERY_DB = 1;
DELIVER = 2;
KEEPALIVE = 3;
CLOSE = 4;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
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,14 +17,12 @@
package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MetricsConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
@@ -34,6 +32,7 @@ import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import io.dropwizard.Configuration;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.db.DataSourceFactory;
public class WhisperServerConfiguration extends Configuration {
@@ -47,8 +46,9 @@ public class WhisperServerConfiguration extends Configuration {
private NexmoConfiguration nexmo;
@NotNull
@Valid
@JsonProperty
private GcmConfiguration gcm;
private PushConfiguration push;
@NotNull
@Valid
@@ -58,15 +58,18 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private MemcacheConfiguration memcache;
private RedisConfiguration cache;
@NotNull
@Valid
@JsonProperty
private RedisConfiguration redis;
private RedisConfiguration directory;
@Valid
@NotNull
@JsonProperty
private ApnConfiguration apn = new ApnConfiguration();
private DataSourceFactory messageStore;
@Valid
@JsonProperty
@@ -88,11 +91,16 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@JsonProperty
private MetricsConfiguration viz = new MetricsConfiguration();
private WebsocketConfiguration websocket = new WebsocketConfiguration();
@JsonProperty
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
@Valid
@NotNull
@JsonProperty
private WebsocketConfiguration websocket = new WebsocketConfiguration();
private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
public WebsocketConfiguration getWebsocketConfiguration() {
return websocket;
@@ -106,24 +114,28 @@ public class WhisperServerConfiguration extends Configuration {
return nexmo;
}
public GcmConfiguration getGcmConfiguration() {
return gcm;
public PushConfiguration getPushConfiguration() {
return push;
}
public ApnConfiguration getApnConfiguration() {
return apn;
public JerseyClientConfiguration getJerseyClientConfiguration() {
return httpClient;
}
public S3Configuration getS3Configuration() {
return s3;
}
public MemcacheConfiguration getMemcacheConfiguration() {
return memcache;
public RedisConfiguration getCacheConfiguration() {
return cache;
}
public RedisConfiguration getRedisConfiguration() {
return redis;
public RedisConfiguration getDirectoryConfiguration() {
return directory;
}
public DataSourceFactory getMessageStoreConfiguration() {
return messageStore;
}
public DataSourceFactory getDataSourceFactory() {
@@ -142,7 +154,7 @@ public class WhisperServerConfiguration extends Configuration {
return graphite;
}
public MetricsConfiguration getMetricsConfiguration() {
return viz;
public RedPhoneConfiguration getRedphoneConfiguration() {
return redphone;
}
}

View File

@@ -20,7 +20,7 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import com.sun.jersey.api.client.Client;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.skife.jdbi.v2.DBI;
@@ -34,24 +34,29 @@ import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
import org.whispersystems.textsecuregcm.metrics.JsonMetricsReporter;
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.push.FeedbackHandler;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.PushServiceClient;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
@@ -60,16 +65,22 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.Messages;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.storage.PendingDevices;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.UrlSigner;
import org.whispersystems.textsecuregcm.websocket.WebsocketControllerFactory;
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
@@ -80,10 +91,10 @@ import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.Application;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool;
@@ -97,12 +108,20 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
@Override
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
bootstrap.addCommand(new DirectoryCommand());
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
bootstrap.addCommand(new VacuumCommand());
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
@Override
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
return configuration.getDataSourceFactory();
}
});
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("messagedb", "messagedb.xml") {
@Override
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
return configuration.getMessageStoreConfiguration();
}
});
}
@Override
@@ -118,35 +137,41 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
DBIFactory dbiFactory = new DBIFactory();
DBI jdbi = dbiFactory.build(environment, config.getDataSourceFactory(), "postgresql");
DBI database = dbiFactory.build(environment, config.getDataSourceFactory(), "accountdb");
DBI messagedb = dbiFactory.build(environment, config.getMessageStoreConfiguration(), "messagedb");
Accounts accounts = jdbi.onDemand(Accounts.class);
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class);
Keys keys = jdbi.onDemand(Keys.class);
Accounts accounts = database.onDemand(Accounts.class);
PendingAccounts pendingAccounts = database.onDemand(PendingAccounts.class);
PendingDevices pendingDevices = database.onDemand(PendingDevices.class);
Keys keys = database.onDemand(Keys.class);
Messages messages = messagedb.onDemand(Messages.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
JedisPool cacheClient = new RedisClientFactory(config.getCacheConfiguration().getUrl()).getRedisClientPool();
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
.build(getName());
DirectoryManager directory = new DirectoryManager(redisClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, memcachedClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
DirectoryManager directory = new DirectoryManager(directoryClient);
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
StoredMessages storedMessages = new StoredMessages(redisClient);
PubSubManager pubSubManager = new PubSubManager(redisClient);
MessagesManager messagesManager = new MessagesManager(messages);
PubSubManager pubSubManager = new PubSubManager(cacheClient);
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(pushServiceClient, websocketSender);
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
config.getApnConfiguration(),
storedMessages, pubSubManager,
accountsManager);
environment.lifecycle().manage(feedbackHandler);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
@@ -158,26 +183,43 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
deviceAuthenticator,
Device.class, "WhisperServer"));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
environment.jersey().register(new ReceiptController(accountsManager, federatedClientManager, pushSender));
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
environment.jersey().register(attachmentController);
environment.jersey().register(keysControllerV1);
environment.jersey().register(keysControllerV2);
environment.jersey().register(messageController);
if (config.getWebsocketConfiguration().isEnabled()) {
WebsocketControllerFactory servlet = new WebsocketControllerFactory(deviceAuthenticator,
pushSender,
storedMessages,
pubSubManager);
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config);
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, messagesManager, pubSubManager));
webSocketEnvironment.jersey().register(new KeepAliveController());
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet);
websocket.addMapping("/v1/websocket/*");
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config);
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
provisioningEnvironment.jersey().register(new KeepAliveController());
WebSocketResourceProviderFactory webSocketServlet = new WebSocketResourceProviderFactory(webSocketEnvironment );
WebSocketResourceProviderFactory provisioningServlet = new WebSocketResourceProviderFactory(provisioningEnvironment);
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet );
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
websocket.addMapping("/v1/websocket/");
websocket.setAsyncSupported(true);
provisioning.addMapping("/v1/websocket/provisioning/");
provisioning.setAsyncSupported(true);
webSocketServlet.start();
provisioningServlet.start();
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
filter.setInitParameter("allowedOrigins", "*");
@@ -187,8 +229,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
filter.setInitParameter("allowCredentials", "true");
}
environment.healthChecks().register("redis", new RedisHealthCheck(redisClient));
environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
environment.healthChecks().register("cache", new RedisHealthCheck(cacheClient));
environment.jersey().register(new IOExceptionMapper());
environment.jersey().register(new RateLimitExceededExceptionMapper());
@@ -206,13 +248,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
GraphiteReporter graphiteReporter = (GraphiteReporter) graphiteReporterFactory.build(environment.metrics());
graphiteReporter.start(15, TimeUnit.SECONDS);
}
if (config.getMetricsConfiguration().isEnabled()) {
new JsonMetricsReporter(environment.metrics(),
config.getMetricsConfiguration().getToken(),
config.getMetricsConfiguration().getHost())
.start(60, TimeUnit.SECONDS);
}
}
private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) {

View File

@@ -0,0 +1,76 @@
package org.whispersystems.textsecuregcm.auth;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
public class AuthorizationToken {
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
private final String token;
private final byte[] key;
public AuthorizationToken(String token, byte[] key) {
this.token = token;
this.key = key;
}
public boolean isValid(String number, long currentTimeMillis) {
String[] parts = token.split(":");
if (parts.length != 3) {
return false;
}
if (!number.equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2]);
}
private boolean isValidTime(String timeString, long currentTimeMillis) {
try {
long tokenTime = Long.parseLong(timeString);
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
} catch (NumberFormatException e) {
logger.warn("Number Format", e);
return false;
}
}
private boolean isValidSignature(String prefix, String suffix) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] ourSuffix = Util.truncate(hmac.doFinal(prefix.getBytes()), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (DecoderException e) {
logger.warn("Authorizationtoken", e);
return false;
}
}
}

View File

@@ -18,12 +18,8 @@ package org.whispersystems.textsecuregcm.configuration;
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 java.util.LinkedList;
import java.util.List;
public class FederationConfiguration {
@@ -34,31 +30,7 @@ public class FederationConfiguration {
@JsonProperty
private String name;
@JsonProperty
private String herokuPeers;
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;
}

View File

@@ -19,8 +19,14 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class GcmConfiguration {
@NotNull
@JsonProperty
private long senderId;
@NotEmpty
@JsonProperty
private String apiKey;
@@ -28,4 +34,8 @@ public class GcmConfiguration {
public String getApiKey() {
return apiKey;
}
public long getSenderId() {
return senderId;
}
}

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.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Optional;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
public class RedPhoneConfiguration {
@JsonProperty
private String authKey;
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
if (authKey == null || authKey.trim().length() == 0) {
return Optional.absent();
}
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
}
}

View File

@@ -23,16 +23,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@@ -65,16 +68,25 @@ public class AccountController {
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final MessagesManager messagesManager;
private final TimeProvider timeProvider;
private final Optional<byte[]> authorizationKey;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
SmsSender smsSenderFactory)
SmsSender smsSenderFactory,
MessagesManager messagesManager,
TimeProvider timeProvider,
Optional<byte[]> authorizationKey)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.messagesManager = messagesManager;
this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey;
}
@Timed
@@ -140,39 +152,59 @@ public class AccountController {
throw new WebApplicationException(Response.status(417).build());
}
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
Account account = new Account();
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
pendingAccounts.remove(number);
logger.debug("Stored device...");
createAccount(number, password, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Path("/token/{verification_token}")
public void verifyToken(@PathParam("verification_token") String verificationToken,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getNumber();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
if (!authorizationKey.isPresent()) {
logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(403).build());
}
AuthorizationToken token = new AuthorizationToken(verificationToken, authorizationKey.get());
if (!token.isValid(number, timeProvider.getCurrentTimeMillis())) {
throw new WebApplicationException(Response.status(403).build());
}
createAccount(number, password, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad authorization header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@Path("/gcm/")
@Consumes(MediaType.APPLICATION_JSON)
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
Device device = account.getAuthenticatedDevice().get();
device.setApnId(null);
device.setGcmId(registrationId.getGcmRegistrationId());
if (registrationId.isWebSocketChannel()) device.setFetchesMessages(true);
else device.setFetchesMessages(false);
accounts.update(account);
}
@@ -182,6 +214,7 @@ public class AccountController {
public void deleteGcmRegistrationId(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setGcmId(null);
device.setFetchesMessages(false);
accounts.update(account);
}
@@ -193,6 +226,7 @@ public class AccountController {
Device device = account.getAuthenticatedDevice().get();
device.setApnId(registrationId.getApnRegistrationId());
device.setGcmId(null);
device.setFetchesMessages(true);
accounts.update(account);
}
@@ -202,6 +236,25 @@ public class AccountController {
public void deleteApnRegistrationId(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setApnId(null);
device.setFetchesMessages(false);
accounts.update(account);
}
@Timed
@PUT
@Path("/wsc/")
public void setWebSocketChannelSupported(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(true);
accounts.update(account);
}
@Timed
@DELETE
@Path("/wsc/")
public void deleteWebSocketChannel(@Auth Account account) {
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(false);
accounts.update(account);
}
@@ -214,6 +267,26 @@ public class AccountController {
encodedVerificationText)).build();
}
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
Account account = new Account();
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
messagesManager.clear(number);
pendingAccounts.remove(number);
logger.debug("Stored device...");
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");

View File

@@ -31,6 +31,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import javax.validation.Valid;
@@ -44,6 +45,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -69,7 +71,7 @@ public class DeviceController {
@Timed
@GET
@Path("/provisioning_code")
@Path("/provisioning/code")
@Produces(MediaType.APPLICATION_JSON)
public VerificationCode createDeviceToken(@Auth Account account)
throws RateLimitExceededException
@@ -102,7 +104,7 @@ public class DeviceController {
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
if (!storedVerificationCode.isPresent() ||
!verificationCode.equals(storedVerificationCode.get()))
!MessageDigest.isEqual(verificationCode.getBytes(), storedVerificationCode.get().getBytes()))
{
throw new WebApplicationException(Response.status(403).build());
}
@@ -118,6 +120,8 @@ public class DeviceController {
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setId(account.get().getNextDeviceId());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setLastSeen(Util.todayInMillis());
account.get().addDevice(device);
accounts.update(account.get());

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.controllers;
public class InvalidDestinationException extends Exception {
public InvalidDestinationException(String message) {
super(message);
}
}

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

@@ -26,6 +26,7 @@ import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.entities.MessageResponse;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.federation.FederatedClient;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
@@ -38,6 +39,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
@@ -82,16 +84,21 @@ public class MessageController {
@Path("/{destination}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void sendMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid IncomingMessageList messages)
@Produces(MediaType.APPLICATION_JSON)
public SendMessageResponse sendMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException
{
rateLimiters.getMessagesLimiter().validate(source.getNumber());
try {
if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages);
else sendRelayMessage(source, destinationName, messages);
boolean isSyncMessage = source.getNumber().equals(destinationName);
if (Util.isEmpty(messages.getRelay())) sendLocalMessage(source, destinationName, messages, isSyncMessage);
else sendRelayMessage(source, destinationName, messages, isSyncMessage);
return new SendMessageResponse(!isSyncMessage && source.getActiveDeviceCount() > 1);
} catch (NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
} catch (MismatchedDevicesException e) {
@@ -105,6 +112,8 @@ public class MessageController {
.type(MediaType.APPLICATION_JSON)
.entity(new StaleDevices(e.getStaleDevices()))
.build());
} catch (InvalidDestinationException e) {
throw new WebApplicationException(Response.status(400).build());
}
}
@@ -130,19 +139,23 @@ public class MessageController {
private void sendLocalMessage(Account source,
String destinationName,
IncomingMessageList messages)
IncomingMessageList messages,
boolean isSyncMessage)
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
{
Account destination = getDestinationAccount(destinationName);
Account destination;
validateCompleteDeviceList(destination, messages.getMessages());
if (!isSyncMessage) destination = getDestinationAccount(destinationName);
else destination = source;
validateCompleteDeviceList(destination, messages.getMessages(), isSyncMessage);
validateRegistrationIds(destination, messages.getMessages());
for (IncomingMessage incomingMessage : messages.getMessages()) {
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
if (destinationDevice.isPresent()) {
sendLocalMessage(source, destination, destinationDevice.get(), incomingMessage);
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
}
}
}
@@ -150,6 +163,7 @@ public class MessageController {
private void sendLocalMessage(Account source,
Account destinationAccount,
Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage)
throws NoSuchUserException, IOException
{
@@ -159,7 +173,7 @@ public class MessageController {
messageBuilder.setType(incomingMessage.getType())
.setSource(source.getNumber())
.setTimestamp(System.currentTimeMillis())
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
if (messageBody.isPresent()) {
@@ -182,9 +196,12 @@ public class MessageController {
private void sendRelayMessage(Account source,
String destinationName,
IncomingMessageList messages)
throws IOException, NoSuchUserException
IncomingMessageList messages,
boolean isSyncMessage)
throws IOException, NoSuchUserException, InvalidDestinationException
{
if (isSyncMessage) throw new InvalidDestinationException("Transcript messages can't be relayed!");
try {
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
@@ -227,7 +244,9 @@ public class MessageController {
}
}
private void validateCompleteDeviceList(Account account, List<IncomingMessage> messages)
private void validateCompleteDeviceList(Account account,
List<IncomingMessage> messages,
boolean isSyncMessage)
throws MismatchedDevicesException
{
Set<Long> messageDeviceIds = new HashSet<>();
@@ -241,7 +260,9 @@ public class MessageController {
}
for (Device device : account.getDevices()) {
if (device.isActive()) {
if (device.isActive() &&
!(isSyncMessage && device.getId() == account.getAuthenticatedDevice().get().getId()))
{
accountDeviceIds.add(device.getId());
if (!messageDeviceIds.contains(device.getId())) {

View File

@@ -0,0 +1,53 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import io.dropwizard.auth.Auth;
@Path("/v1/provisioning")
public class ProvisioningController {
private final RateLimiters rateLimiters;
private final WebsocketSender websocketSender;
public ProvisioningController(RateLimiters rateLimiters, PushSender pushSender) {
this.rateLimiters = rateLimiters;
this.websocketSender = pushSender.getWebSocketSender();
}
@Timed
@Path("/{destination}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void sendProvisioningMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid ProvisioningMessage message)
throws RateLimitExceededException, InvalidWebsocketAddressException, IOException
{
rateLimiters.getMessagesLimiter().validate(source.getNumber());
if (!websocketSender.sendProvisioningMessage(new ProvisioningAddress(destinationName),
Base64.decode(message.getBody())))
{
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,108 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Set;
import io.dropwizard.auth.Auth;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
@Path("/v1/receipt")
public class ReceiptController {
private final AccountsManager accountManager;
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
public ReceiptController(AccountsManager accountManager,
FederatedClientManager federatedClientManager,
PushSender pushSender)
{
this.accountManager = accountManager;
this.federatedClientManager = federatedClientManager;
this.pushSender = pushSender;
}
@Timed
@PUT
@Path("/{destination}/{messageId}")
public void sendDeliveryReceipt(@Auth Account source,
@PathParam("destination") String destination,
@PathParam("messageId") long messageId,
@QueryParam("relay") Optional<String> relay)
throws IOException
{
try {
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
else sendDirectReceipt(source, destination, messageId);
} catch (NoSuchUserException | NotPushRegisteredException e) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
} catch (TransientPushFailureException e) {
throw new IOException(e);
}
}
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
throws NoSuchUserException, IOException
{
try {
federatedClientManager.getClient(relay)
.sendDeliveryReceipt(source.getNumber(),
source.getAuthenticatedDevice().get().getId(),
destination, messageId);
} catch (NoSuchPeerException e) {
throw new NoSuchUserException(e);
}
}
private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{
Account destinationAccount = getDestinationAccount(destination);
Set<Device> destinationDevices = destinationAccount.getDevices();
OutgoingMessageSignal.Builder message =
OutgoingMessageSignal.newBuilder()
.setSource(source.getNumber())
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
.setTimestamp(messageId)
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
if (source.getRelay().isPresent()) {
message.setRelay(source.getRelay().get());
}
for (Device destinationDevice : destinationDevices) {
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
}
}
private Account getDestinationAccount(String destination)
throws NoSuchUserException
{
Optional<Account> account = accountManager.get(destination);
if (!account.isPresent()) {
throw new NoSuchUserException(destination);
}
return account.get();
}
}

View File

@@ -1,223 +0,0 @@
package org.whispersystems.textsecuregcm.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.entities.AcknowledgeWebsocketMessage;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingWebsocketMessage;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.textsecuregcm.websocket.WebsocketMessage;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.basic.BasicCredentials;
public class WebsocketController implements WebSocketListener, PubSubListener {
private static final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
private static final ObjectMapper mapper = new ObjectMapper();
private static final Map<Long, String> pendingMessages = new HashMap<>();
private final AccountAuthenticator accountAuthenticator;
private final PubSubManager pubSubManager;
private final StoredMessages storedMessages;
private final PushSender pushSender;
private WebsocketAddress address;
private Account account;
private Device device;
private Session session;
private long pendingMessageSequence;
public WebsocketController(AccountAuthenticator accountAuthenticator,
PushSender pushSender,
PubSubManager pubSubManager,
StoredMessages storedMessages)
{
this.accountAuthenticator = accountAuthenticator;
this.pushSender = pushSender;
this.pubSubManager = pubSubManager;
this.storedMessages = storedMessages;
}
@Override
public void onWebSocketConnect(Session session) {
try {
UpgradeRequest request = session.getUpgradeRequest();
Map<String, String[]> parameters = request.getParameterMap();
String[] usernames = parameters.get("login" );
String[] passwords = parameters.get("password");
if (usernames == null || usernames.length == 0 ||
passwords == null || passwords.length == 0)
{
session.close(new CloseStatus(4001, "Unauthorized"));
return;
}
BasicCredentials credentials = new BasicCredentials(usernames[0], passwords[0]);
Optional<Account> account = accountAuthenticator.authenticate(credentials);
if (!account.isPresent()) {
session.close(new CloseStatus(4001, "Unauthorized"));
return;
}
this.account = account.get();
this.device = account.get().getAuthenticatedDevice().get();
this.address = new WebsocketAddress(this.account.getId(), this.device.getId());
this.session = session;
this.session.setIdleTimeout(10 * 60 * 1000);
this.pubSubManager.subscribe(this.address, this);
handleQueryDatabase();
} catch (AuthenticationException e) {
try { session.close(1011, "Server Error");} catch (IOException e1) {}
} catch (IOException ioe) {
logger.info("Abrupt session close.");
}
}
@Override
public void onWebSocketText(String body) {
try {
IncomingWebsocketMessage incomingMessage = mapper.readValue(body, IncomingWebsocketMessage.class);
switch (incomingMessage.getType()) {
case IncomingWebsocketMessage.TYPE_ACKNOWLEDGE_MESSAGE:
handleMessageAck(body);
break;
default:
close(new CloseStatus(1008, "Unknown Type"));
}
} catch (IOException e) {
logger.debug("Parse", e);
close(new CloseStatus(1008, "Badly Formatted"));
}
}
@Override
public void onWebSocketClose(int i, String s) {
pubSubManager.unsubscribe(this.address, this);
List<String> remainingMessages = new LinkedList<>();
synchronized (pendingMessages) {
Long[] pendingKeys = pendingMessages.keySet().toArray(new Long[0]);
Arrays.sort(pendingKeys);
for (long pendingKey : pendingKeys) {
remainingMessages.add(pendingMessages.get(pendingKey));
}
pendingMessages.clear();
}
for (String remainingMessage : remainingMessages) {
try {
pushSender.sendMessage(account, device, new EncryptedOutgoingMessage(remainingMessage));
} catch (NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("onWebSocketClose", e);
storedMessages.insert(account.getId(), device.getId(), remainingMessage);
}
}
}
@Override
public void onPubSubMessage(PubSubMessage outgoingMessage) {
switch (outgoingMessage.getType()) {
case PubSubMessage.TYPE_DELIVER:
handleDeliverOutgoingMessage(outgoingMessage.getContents());
break;
case PubSubMessage.TYPE_QUERY_DB:
handleQueryDatabase();
break;
default:
logger.warn("Unknown pubsub message: " + outgoingMessage.getType());
}
}
private void handleDeliverOutgoingMessage(String message) {
try {
long messageSequence;
synchronized (pendingMessages) {
messageSequence = pendingMessageSequence++;
pendingMessages.put(messageSequence, message);
}
WebsocketMessage websocketMessage = new WebsocketMessage(messageSequence, message);
session.getRemote().sendStringByFuture(mapper.writeValueAsString(websocketMessage));
} catch (IOException e) {
logger.debug("Response failed", e);
close(null);
}
}
private void handleMessageAck(String message) {
try {
AcknowledgeWebsocketMessage ack = mapper.readValue(message, AcknowledgeWebsocketMessage.class);
synchronized (pendingMessages) {
pendingMessages.remove(ack.getId());
}
} catch (IOException e) {
logger.warn("Mapping", e);
}
}
private void handleQueryDatabase() {
List<String> messages = storedMessages.getMessagesForDevice(account.getId(), device.getId());
for (String message : messages) {
handleDeliverOutgoingMessage(message);
}
}
@Override
public void onWebSocketBinary(byte[] bytes, int i, int i2) {
logger.info("Received binary message!");
}
@Override
public void onWebSocketError(Throwable throwable) {
logger.info("onWebSocketError", throwable);
}
private void close(CloseStatus closeStatus) {
try {
if (this.session != null) {
if (closeStatus != null) this.session.close(closeStatus);
else this.session.close();
}
} catch (IOException e) {
logger.info("close()", e);
}
}
}

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.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
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 javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.Arrays;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@@ -73,9 +68,9 @@ public class ClientContact {
this.inactive = inactive;
}
public String toString() {
return new Gson().toJson(this);
}
// public String toString() {
// return new Gson().toJson(this);
// }
@Override
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_SIZE = 10;
private final String serialized;
private final byte[] serialized;
private final String serializedAndEncoded;
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
String signalingKey)
@@ -50,16 +51,16 @@ public class EncryptedOutgoingMessage {
byte[] plaintext = outgoingMessage.toByteArray();
SecretKeySpec cipherKey = getCipherKey (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 EncryptedOutgoingMessage(String serialized) {
this.serialized = serialized;
public String toEncodedString() {
return serializedAndEncoded;
}
public String serialize() {
public byte[] toByteArray() {
return serialized;
}

View File

@@ -0,0 +1,42 @@
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
private String message;
@JsonProperty
private boolean receipt;
@JsonProperty
private boolean notification;
public GcmMessage() {}
public GcmMessage(String gcmId, String number, int deviceId, String message, boolean receipt, boolean notification) {
this.gcmId = gcmId;
this.number = number;
this.deviceId = deviceId;
this.message = message;
this.receipt = receipt;
this.notification = notification;
}
}

View File

@@ -25,9 +25,15 @@ public class GcmRegistrationId {
@NotEmpty
private String gcmRegistrationId;
@JsonProperty
private boolean webSocketChannel;
public String getGcmRegistrationId() {
return gcmRegistrationId;
}
public boolean isWebSocketChannel() {
return webSocketChannel;
}
}

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
@@ -42,7 +43,19 @@ public class PreKeyResponseV2 {
}
@VisibleForTesting
public List<PreKeyResponseItemV2> getDevices() {
return devices;
@JsonIgnore
public PreKeyResponseItemV2 getDevice(int deviceId) {
for (PreKeyResponseItemV2 device : devices) {
if (device.getDeviceId() == deviceId) return device;
}
return null;
}
@VisibleForTesting
@JsonIgnore
public int getDevicesCount() {
return devices.size();
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class ProvisioningMessage {
@JsonProperty
@NotEmpty
private String body;
public String getBody() {
return body;
}
}

View File

@@ -0,0 +1,16 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SendMessageResponse {
@JsonProperty
private boolean needsSync;
public SendMessageResponse() {}
public SendMessageResponse(boolean needsSync) {
this.needsSync = needsSync;
}
}

View File

@@ -5,7 +5,7 @@ import org.hibernate.validator.constraints.NotEmpty;
import java.io.Serializable;
public class SignedPreKey extends PreKeyV2 implements Serializable {
public class SignedPreKey extends PreKeyV2 {
@JsonProperty
@NotEmpty

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

@@ -69,6 +69,7 @@ public class FederatedClient {
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
private final FederatedPeer peer;
private final Client client;
@@ -197,6 +198,25 @@ public class FederatedClient {
}
}
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException
{
try {
String path = String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId);
WebResource resource = client.resource(peer.getUrl()).path(path);
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.put(ClientResponse.class);
if (response.getStatus() != 200 && response.getStatus() != 204) {
throw new WebApplicationException(clientResponseToResponse(response));
}
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
}
}
private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
}

View File

@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
@Override
public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null));
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null, System.currentTimeMillis()));
}
}

View File

@@ -16,9 +16,13 @@
*/
package org.whispersystems.textsecuregcm.limits;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class LeakyBucket implements Serializable {
import java.io.IOException;
public class LeakyBucket {
private final int bucketSize;
private final double leakRatePerMillis;
@@ -27,10 +31,14 @@ public class LeakyBucket implements Serializable {
private long lastUpdateTimeMillis;
public LeakyBucket(int bucketSize, double leakRatePerMillis) {
this(bucketSize, leakRatePerMillis, bucketSize, System.currentTimeMillis());
}
private LeakyBucket(int bucketSize, double leakRatePerMillis, int spaceRemaining, long lastUpdateTimeMillis) {
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMillis;
this.spaceRemaining = bucketSize;
this.lastUpdateTimeMillis = System.currentTimeMillis();
this.spaceRemaining = spaceRemaining;
this.lastUpdateTimeMillis = lastUpdateTimeMillis;
}
public boolean add(int amount) {
@@ -50,4 +58,40 @@ public class LeakyBucket implements Serializable {
return Math.min(this.bucketSize,
(int)Math.floor(this.spaceRemaining + (elapsedTime * this.leakRatePerMillis)));
}
public String serialize(ObjectMapper mapper) throws JsonProcessingException {
return mapper.writeValueAsString(new LeakyBucketEntity(bucketSize, leakRatePerMillis, spaceRemaining, lastUpdateTimeMillis));
}
public static LeakyBucket fromSerialized(ObjectMapper mapper, String serialized) throws IOException {
LeakyBucketEntity entity = mapper.readValue(serialized, LeakyBucketEntity.class);
return new LeakyBucket(entity.bucketSize, entity.leakRatePerMillis,
entity.spaceRemaining, entity.lastUpdateTimeMillis);
}
private static class LeakyBucketEntity {
@JsonProperty
private int bucketSize;
@JsonProperty
private double leakRatePerMillis;
@JsonProperty
private int spaceRemaining;
@JsonProperty
private long lastUpdateTimeMillis;
public LeakyBucketEntity() {}
private LeakyBucketEntity(int bucketSize, double leakRatePerMillis,
int spaceRemaining, long lastUpdateTimeMillis)
{
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMillis;
this.spaceRemaining = spaceRemaining;
this.lastUpdateTimeMillis = lastUpdateTimeMillis;
}
}
}

View File

@@ -19,27 +19,38 @@ package org.whispersystems.textsecuregcm.limits;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import net.spy.memcached.MemcachedClient;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import static com.codahale.metrics.MetricRegistry.name;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RateLimiter {
private final Meter meter;
private final MemcachedClient memcachedClient;
private final String name;
private final int bucketSize;
private final double leakRatePerMillis;
private final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
private final ObjectMapper mapper = SystemMapper.getMapper();
public RateLimiter(MemcachedClient memcachedClient, String name,
private final Meter meter;
private final JedisPool cacheClient;
private final String name;
private final int bucketSize;
private final double leakRatePerMillis;
public RateLimiter(JedisPool cacheClient, String name,
int bucketSize, double leakRatePerMinute)
{
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
this.meter = metricRegistry.meter(name(getClass(), name, "exceeded"));
this.memcachedClient = memcachedClient;
this.cacheClient = cacheClient;
this.name = name;
this.bucketSize = bucketSize;
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
@@ -61,21 +72,29 @@ public class RateLimiter {
}
private void setBucket(String key, LeakyBucket bucket) {
memcachedClient.set(getBucketName(key),
(int)Math.ceil((bucketSize / leakRatePerMillis) / 1000), bucket);
}
private LeakyBucket getBucket(String key) {
LeakyBucket bucket = (LeakyBucket)memcachedClient.get(getBucketName(key));
if (bucket == null) {
return new LeakyBucket(bucketSize, leakRatePerMillis);
} else {
return bucket;
try (Jedis jedis = cacheClient.getResource()) {
String serialized = bucket.serialize(mapper);
jedis.setex(getBucketName(key), (int) Math.ceil((bucketSize / leakRatePerMillis) / 1000), serialized);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
private LeakyBucket getBucket(String key) {
try (Jedis jedis = cacheClient.getResource()) {
String serialized = jedis.get(getBucketName(key));
if (serialized != null) {
return LeakyBucket.fromSerialized(mapper, serialized);
}
} catch (IOException e) {
logger.warn("Deserialization error", e);
}
return new LeakyBucket(bucketSize, leakRatePerMillis);
}
private String getBucketName(String key) {
return LeakyBucket.class.getSimpleName() + name + key;
return "leaky_bucket::" + name + "::" + key;
}
}

View File

@@ -17,9 +17,10 @@
package org.whispersystems.textsecuregcm.limits;
import net.spy.memcached.MemcachedClient;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import redis.clients.jedis.JedisPool;
public class RateLimiters {
private final RateLimiter smsDestinationLimiter;
@@ -34,40 +35,40 @@ public class RateLimiters {
private final RateLimiter allocateDeviceLimiter;
private final RateLimiter verifyDeviceLimiter;
public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) {
this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination",
public RateLimiters(RateLimitsConfiguration config, JedisPool cacheClient) {
this.smsDestinationLimiter = new RateLimiter(cacheClient, "smsDestination",
config.getSmsDestination().getBucketSize(),
config.getSmsDestination().getLeakRatePerMinute());
this.voiceDestinationLimiter = new RateLimiter(memcachedClient, "voxDestination",
this.voiceDestinationLimiter = new RateLimiter(cacheClient, "voxDestination",
config.getVoiceDestination().getBucketSize(),
config.getVoiceDestination().getLeakRatePerMinute());
this.verifyLimiter = new RateLimiter(memcachedClient, "verify",
this.verifyLimiter = new RateLimiter(cacheClient, "verify",
config.getVerifyNumber().getBucketSize(),
config.getVerifyNumber().getLeakRatePerMinute());
this.attachmentLimiter = new RateLimiter(memcachedClient, "attachmentCreate",
this.attachmentLimiter = new RateLimiter(cacheClient, "attachmentCreate",
config.getAttachments().getBucketSize(),
config.getAttachments().getLeakRatePerMinute());
this.contactsLimiter = new RateLimiter(memcachedClient, "contactsQuery",
this.contactsLimiter = new RateLimiter(cacheClient, "contactsQuery",
config.getContactQueries().getBucketSize(),
config.getContactQueries().getLeakRatePerMinute());
this.preKeysLimiter = new RateLimiter(memcachedClient, "prekeys",
this.preKeysLimiter = new RateLimiter(cacheClient, "prekeys",
config.getPreKeys().getBucketSize(),
config.getPreKeys().getLeakRatePerMinute());
this.messagesLimiter = new RateLimiter(memcachedClient, "messages",
this.messagesLimiter = new RateLimiter(cacheClient, "messages",
config.getMessages().getBucketSize(),
config.getMessages().getLeakRatePerMinute());
this.allocateDeviceLimiter = new RateLimiter(memcachedClient, "allocateDevice",
this.allocateDeviceLimiter = new RateLimiter(cacheClient, "allocateDevice",
config.getAllocateDevice().getBucketSize(),
config.getAllocateDevice().getLeakRatePerMinute());
this.verifyDeviceLimiter = new RateLimiter(memcachedClient, "verifyDevice",
this.verifyDeviceLimiter = new RateLimiter(cacheClient, "verifyDevice",
config.getVerifyDevice().getBucketSize(),
config.getVerifyDevice().getLeakRatePerMinute());

View File

@@ -0,0 +1,65 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.codahale.metrics.MetricRegistry;
import net.sourceforge.argparse4j.inf.Namespace;
import java.sql.SQLException;
import io.dropwizard.Configuration;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.db.DatabaseConfiguration;
import io.dropwizard.db.ManagedDataSource;
import io.dropwizard.setup.Bootstrap;
import liquibase.Liquibase;
import liquibase.exception.LiquibaseException;
import liquibase.exception.ValidationFailedException;
public abstract class AbstractLiquibaseCommand<T extends Configuration> extends ConfiguredCommand<T> {
private final DatabaseConfiguration<T> strategy;
private final Class<T> configurationClass;
private final String migrations;
protected AbstractLiquibaseCommand(String name,
String description,
String migrations,
DatabaseConfiguration<T> strategy,
Class<T> configurationClass) {
super(name, description);
this.migrations = migrations;
this.strategy = strategy;
this.configurationClass = configurationClass;
}
@Override
protected Class<T> getConfigurationClass() {
return configurationClass;
}
@Override
@SuppressWarnings("UseOfSystemOutOrSystemErr")
protected void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception {
final DataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration);
dbConfig.setMaxSize(1);
dbConfig.setMinSize(1);
dbConfig.setInitialSize(1);
try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) {
run(namespace, liquibase);
} catch (ValidationFailedException e) {
e.printDescriptiveError(System.err);
throw e;
}
}
private CloseableLiquibase openLiquibase(final DataSourceFactory dataSourceFactory, final Namespace namespace)
throws ClassNotFoundException, SQLException, LiquibaseException
{
final ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(), "liquibase");
return new CloseableLiquibase(dataSource, migrations);
}
protected abstract void run(Namespace namespace, Liquibase liquibase) throws Exception;
}

View File

@@ -0,0 +1,28 @@
package org.whispersystems.textsecuregcm.liquibase;
import java.sql.SQLException;
import io.dropwizard.db.ManagedDataSource;
import liquibase.Liquibase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;
public class CloseableLiquibase extends Liquibase implements AutoCloseable {
private final ManagedDataSource dataSource;
public CloseableLiquibase(ManagedDataSource dataSource, String migrations)
throws LiquibaseException, ClassNotFoundException, SQLException
{
super(migrations,
new ClassLoaderResourceAccessor(),
new JdbcConnection(dataSource.getConnection()));
this.dataSource = dataSource;
}
@Override
public void close() throws Exception {
dataSource.stop();
}
}

View File

@@ -0,0 +1,72 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import java.io.OutputStreamWriter;
import java.util.List;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import liquibase.Liquibase;
public class DbMigrateCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
public DbMigrateCommand(String migration, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
super("migrate", "Apply all pending change sets.", migration, strategy, configurationClass);
}
@Override
public void configure(Subparser subparser) {
super.configure(subparser);
subparser.addArgument("-n", "--dry-run")
.action(Arguments.storeTrue())
.dest("dry-run")
.setDefault(Boolean.FALSE)
.help("output the DDL to stdout, don't run it");
subparser.addArgument("-c", "--count")
.type(Integer.class)
.dest("count")
.help("only apply the next N change sets");
subparser.addArgument("-i", "--include")
.action(Arguments.append())
.dest("contexts")
.help("include change sets from the given context");
}
@Override
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
final String context = getContext(namespace);
final Integer count = namespace.getInt("count");
final Boolean dryRun = namespace.getBoolean("dry-run");
if (count != null) {
if (dryRun) {
liquibase.update(count, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
} else {
liquibase.update(count, context);
}
} else {
if (dryRun) {
liquibase.update(context, new OutputStreamWriter(System.out, Charsets.UTF_8));
} else {
liquibase.update(context);
}
}
}
private String getContext(Namespace namespace) {
final List<Object> contexts = namespace.getList("contexts");
if (contexts == null) {
return "";
}
return Joiner.on(',').join(contexts);
}
}

View File

@@ -0,0 +1,51 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import java.io.OutputStreamWriter;
import java.util.List;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import liquibase.Liquibase;
public class DbStatusCommand <T extends Configuration> extends AbstractLiquibaseCommand<T> {
public DbStatusCommand(String migrations, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
super("status", "Check for pending change sets.", migrations, strategy, configurationClass);
}
@Override
public void configure(Subparser subparser) {
super.configure(subparser);
subparser.addArgument("-v", "--verbose")
.action(Arguments.storeTrue())
.dest("verbose")
.help("Output verbose information");
subparser.addArgument("-i", "--include")
.action(Arguments.append())
.dest("contexts")
.help("include change sets from the given context");
}
@Override
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
liquibase.reportStatus(namespace.getBoolean("verbose"),
getContext(namespace),
new OutputStreamWriter(System.out, Charsets.UTF_8));
}
private String getContext(Namespace namespace) {
final List<Object> contexts = namespace.getList("contexts");
if (contexts == null) {
return "";
}
return Joiner.on(',').join(contexts);
}
}

View File

@@ -0,0 +1,44 @@
package org.whispersystems.textsecuregcm.liquibase;
import com.google.common.collect.Maps;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import java.util.SortedMap;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import liquibase.Liquibase;
public class NameableDbCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
private static final String COMMAND_NAME_ATTR = "subcommand";
private final SortedMap<String, AbstractLiquibaseCommand<T>> subcommands;
public NameableDbCommand(String name, String migrations, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
super(name, "Run database migrations tasks", migrations, strategy, configurationClass);
this.subcommands = Maps.newTreeMap();
addSubcommand(new DbMigrateCommand<>(migrations, strategy, configurationClass));
addSubcommand(new DbStatusCommand<>(migrations, strategy, configurationClass));
}
private void addSubcommand(AbstractLiquibaseCommand<T> subcommand) {
subcommands.put(subcommand.getName(), subcommand);
}
@Override
public void configure(Subparser subparser) {
for (AbstractLiquibaseCommand<T> subcommand : subcommands.values()) {
final Subparser cmdParser = subparser.addSubparsers()
.addParser(subcommand.getName())
.setDefault(COMMAND_NAME_ATTR, subcommand.getName())
.description(subcommand.getDescription());
subcommand.configure(cmdParser);
}
}
@Override
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
final AbstractLiquibaseCommand<T> subcommand = subcommands.get(namespace.getString(COMMAND_NAME_ATTR));
subcommand.run(namespace, liquibase);
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.textsecuregcm.liquibase;
import io.dropwizard.Bundle;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.util.Generics;
public abstract class NameableMigrationsBundle<T extends Configuration> implements Bundle, DatabaseConfiguration<T> {
private final String name;
private final String migrations;
public NameableMigrationsBundle(String name, String migrations) {
this.name = name;
this.migrations = migrations;
}
public final void initialize(Bootstrap<?> bootstrap) {
Class klass = Generics.getTypeParameter(this.getClass(), Configuration.class);
bootstrap.addCommand(new NameableDbCommand(name, migrations, this, klass));
}
public final void run(Environment environment) {
}
}

View File

@@ -31,7 +31,9 @@ public class IOExceptionMapper implements ExceptionMapper<IOException> {
@Override
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();
}
}

View File

@@ -0,0 +1,15 @@
package org.whispersystems.textsecuregcm.mappers;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class InvalidWebsocketAddressExceptionMapper implements ExceptionMapper<InvalidWebsocketAddressException> {
@Override
public Response toResponse(InvalidWebsocketAddressException exception) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}

View File

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

@@ -1,51 +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.providers;
import com.codahale.metrics.health.HealthCheck;
import net.spy.memcached.MemcachedClient;
import java.security.SecureRandom;
public class MemcacheHealthCheck extends HealthCheck {
private final MemcachedClient client;
public MemcacheHealthCheck(MemcachedClient client) {
this.client = client;
}
@Override
protected Result check() throws Exception {
if (client == null) {
return Result.unhealthy("not configured");
}
int random = SecureRandom.getInstance("SHA1PRNG").nextInt();
int value = SecureRandom.getInstance("SHA1PRNG").nextInt();
this.client.set("HEALTH" + random, 2000, String.valueOf(value));
String result = (String)this.client.get("HEALTH" + random);
if (result == null || Integer.parseInt(result) != value) {
return Result.unhealthy("Fetch failed");
}
return Result.healthy();
}
}

View File

@@ -1,54 +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.providers;
import net.spy.memcached.AddrUtil;
import net.spy.memcached.ConnectionFactoryBuilder;
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.auth.AuthDescriptor;
import net.spy.memcached.auth.PlainCallbackHandler;
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
public class MemcachedClientFactory {
private final MemcachedClient client;
public MemcachedClientFactory(MemcacheConfiguration config) throws IOException {
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
builder.setProtocol(ConnectionFactoryBuilder.Protocol.BINARY);
if (!Util.isEmpty(config.getUser())) {
AuthDescriptor ad = new AuthDescriptor(new String[] { "PLAIN" },
new PlainCallbackHandler(config.getUser(),
config.getPassword()));
builder.setAuthDescriptor(ad);
}
this.client = new MemcachedClient(builder.build(),
AddrUtil.getAddresses(config.getServers()));
}
public MemcachedClient getClient() {
return client;
}
}

View File

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

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.providers;
public class TimeProvider {
public long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
}

View File

@@ -1,146 +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.google.common.base.Optional;
import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsService;
import com.notnoop.exceptions.NetworkIOException;
import org.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.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 static com.codahale.metrics.MetricRegistry.name;
public class APNSender {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter websocketMeter = metricRegistry.meter(name(getClass(), "websocket"));
private final Meter pushMeter = metricRegistry.meter(name(getClass(), "push"));
private final Meter failureMeter = metricRegistry.meter(name(getClass(), "failure"));
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
private static final String MESSAGE_BODY = "m";
private final Optional<ApnsService> apnService;
private final PubSubManager pubSubManager;
private final StoredMessages storedMessages;
public APNSender(PubSubManager pubSubManager,
StoredMessages storedMessages,
String apnCertificate, String apnKey)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
{
this.pubSubManager = pubSubManager;
this.storedMessages = storedMessages;
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
this.apnService = Optional.of(APNS.newService()
.withCert(new ByteArrayInputStream(keyStore), "insecure")
.withSandboxDestination().build());
} else {
this.apnService = Optional.absent();
}
}
public void sendMessage(Account account, Device device,
String registrationId, EncryptedOutgoingMessage message)
throws TransientPushFailureException, NotPushRegisteredException
{
if (pubSubManager.publish(new WebsocketAddress(account.getId(), device.getId()),
new PubSubMessage(PubSubMessage.TYPE_DELIVER, message.serialize())))
{
websocketMeter.mark();
} else {
storedMessages.insert(account.getId(), device.getId(), message.serialize());
sendPush(registrationId, message.serialize());
}
}
private void sendPush(String registrationId, String message)
throws TransientPushFailureException
{
try {
if (!apnService.isPresent()) {
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();
}
}

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,69 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.android.gcm.server.Constants;
import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import java.io.IOException;
import static com.codahale.metrics.MetricRegistry.name;
public class GCMSender {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(org.whispersystems.textsecuregcm.util.Constants.METRICS_NAME);
private final Meter success = metricRegistry.meter(name(getClass(), "sent", "success"));
private final Meter failure = metricRegistry.meter(name(getClass(), "sent", "failure"));
private final Sender sender;
public GCMSender(String apiKey) {
this.sender = new Sender(apiKey);
}
public String sendMessage(String gcmRegistrationId, EncryptedOutgoingMessage outgoingMessage)
throws NotPushRegisteredException, TransientPushFailureException
{
try {
Message gcmMessage = new Message.Builder().addData("type", "message")
.addData("message", outgoingMessage.serialize())
.build();
Result result = sender.send(gcmMessage, gcmRegistrationId, 5);
if (result.getMessageId() != null) {
success.mark();
return result.getCanonicalRegistrationId();
} else {
failure.mark();
if (result.getErrorCodeName().equals(Constants.ERROR_NOT_REGISTERED)) {
throw new NotPushRegisteredException("Device no longer registered with GCM.");
} else {
throw new TransientPushFailureException("GCM Failed: " + result.getErrorCodeName());
}
}
} catch (IOException e) {
throw new TransientPushFailureException(e);
}
}
}

View File

@@ -18,60 +18,31 @@ package org.whispersystems.textsecuregcm.push;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.entities.ApnMessage;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.GcmMessage;
import org.whispersystems.textsecuregcm.push.WebsocketSender.DeliveryStatus;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
public class PushSender {
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
private final AccountsManager accounts;
private final GCMSender gcmSender;
private final APNSender apnSender;
private final WebsocketSender webSocketSender;
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"badge\":%d,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
public PushSender(GcmConfiguration gcmConfiguration,
ApnConfiguration apnConfiguration,
StoredMessages storedMessages,
PubSubManager pubSubManager,
AccountsManager accounts)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
{
this.accounts = accounts;
this.webSocketSender = new WebsocketSender(storedMessages, pubSubManager);
this.gcmSender = new GCMSender(gcmConfiguration.getApiKey());
this.apnSender = new APNSender(pubSubManager, storedMessages,
apnConfiguration.getCertificate(),
apnConfiguration.getKey());
private final PushServiceClient pushServiceClient;
private final WebsocketSender webSocketSender;
public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
this.pushServiceClient = pushServiceClient;
this.webSocketSender = websocketSender;
}
public void sendMessage(Account account, Device device, MessageProtos.OutgoingMessageSignal message)
throws NotPushRegisteredException, TransientPushFailureException
{
try {
String signalingKey = device.getSignalingKey();
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, signalingKey);
sendMessage(account, device, encryptedMessage);
} catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e);
}
}
public void sendMessage(Account account, Device device, EncryptedOutgoingMessage message)
public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
throws NotPushRegisteredException, TransientPushFailureException
{
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
@@ -80,44 +51,66 @@ public class PushSender {
else throw new NotPushRegisteredException("No delivery possible!");
}
private void sendGcmMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
throws NotPushRegisteredException, TransientPushFailureException
{
try {
String canonicalId = gcmSender.sendMessage(device.getGcmId(), outgoingMessage);
if (canonicalId != null) {
device.setGcmId(canonicalId);
accounts.update(account);
}
} catch (NotPushRegisteredException e) {
logger.debug("No Such User", e);
device.setGcmId(null);
accounts.update(account);
throw new NotPushRegisteredException(e);
}
public WebsocketSender getWebSocketSender() {
return webSocketSender;
}
private void sendApnMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
private void sendGcmMessage(Account account, Device device, OutgoingMessageSignal message)
throws TransientPushFailureException, NotPushRegisteredException
{
// if (device.getFetchesMessages()) sendNotificationGcmMessage(account, device, message);
// else sendPayloadGcmMessage(account, device, message);
sendPayloadGcmMessage(account, device, message);
}
private void sendPayloadGcmMessage(Account account, Device device, OutgoingMessageSignal message)
throws TransientPushFailureException, NotPushRegisteredException
{
try {
apnSender.sendMessage(account, device, device.getApnId(), outgoingMessage);
} catch (NotPushRegisteredException e) {
device.setApnId(null);
accounts.update(account);
throw new NotPushRegisteredException(e);
}
}
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, false);
private void sendWebSocketMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
throws NotPushRegisteredException
{
try {
webSocketSender.sendMessage(account, device, outgoingMessage);
pushServiceClient.send(gcmMessage);
} catch (CryptoEncodingException e) {
throw new NotPushRegisteredException(e);
}
}
private void sendNotificationGcmMessage(Account account, Device device, OutgoingMessageSignal message)
throws TransientPushFailureException
{
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, message, WebsocketSender.Type.GCM);
if (!deliveryStatus.isDelivered()) {
GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(),
(int)device.getId(), "", false, true);
pushServiceClient.send(gcmMessage);
} else {
logger.warn("Delivered!");
}
}
private void sendApnMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
throws TransientPushFailureException
{
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.APN);
if (!deliveryStatus.isDelivered() && outgoingMessage.getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) {
ApnMessage apnMessage = new ApnMessage(device.getApnId(), account.getNumber(), (int)device.getId(),
String.format(APN_PAYLOAD, deliveryStatus.getMessageQueueDepth()));
pushServiceClient.send(apnMessage);
}
}
private void sendWebSocketMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
{
webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.WEB);
}
}

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,50 +19,111 @@ package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import com.google.protobuf.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubMessage;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.util.List;
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 static enum Type {
APN,
GCM,
WEB
}
private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter onlineMeter = metricRegistry.meter(name(getClass(), "online"));
private final Meter offlineMeter = metricRegistry.meter(name(getClass(), "offline"));
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
private final Meter websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" ));
private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" ));
public WebsocketSender(StoredMessages storedMessages, PubSubManager pubSubManager) {
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
private final Meter apnOnlineMeter = metricRegistry.meter(name(getClass(), "apn_online" ));
private final Meter apnOfflineMeter = metricRegistry.meter(name(getClass(), "apn_offline"));
private final Meter gcmOnlineMeter = metricRegistry.meter(name(getClass(), "gcm_online" ));
private final Meter gcmOfflineMeter = metricRegistry.meter(name(getClass(), "gcm_offline"));
private final Meter provisioningOnlineMeter = metricRegistry.meter(name(getClass(), "provisioning_online" ));
private final Meter provisioningOfflineMeter = metricRegistry.meter(name(getClass(), "provisioning_offline"));
private final MessagesManager messagesManager;
private final PubSubManager pubSubManager;
public WebsocketSender(MessagesManager messagesManager, PubSubManager pubSubManager) {
this.messagesManager = messagesManager;
this.pubSubManager = pubSubManager;
}
public void sendMessage(Account account, Device device, EncryptedOutgoingMessage outgoingMessage)
throws CryptoEncodingException
{
sendMessage(account, device, outgoingMessage.serialize());
}
private void sendMessage(Account account, Device device, String serializedMessage) {
WebsocketAddress address = new WebsocketAddress(account.getId(), device.getId());
PubSubMessage pubSubMessage = new PubSubMessage(PubSubMessage.TYPE_DELIVER, serializedMessage);
public DeliveryStatus sendMessage(Account account, Device device, OutgoingMessageSignal message, Type channel) {
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.DELIVER)
.setContent(message.toByteString())
.build();
if (pubSubManager.publish(address, pubSubMessage)) {
onlineMeter.mark();
if (channel == Type.APN) apnOnlineMeter.mark();
else if (channel == Type.GCM) gcmOnlineMeter.mark();
else websocketOnlineMeter.mark();
return new DeliveryStatus(true, 0);
} else {
offlineMeter.mark();
storedMessages.insert(account.getId(), device.getId(), serializedMessage);
pubSubManager.publish(address, new PubSubMessage(PubSubMessage.TYPE_QUERY_DB, null));
if (channel == Type.APN) apnOfflineMeter.mark();
else if (channel == Type.GCM) gcmOfflineMeter.mark();
else websocketOfflineMeter.mark();
int queueDepth = messagesManager.insert(account.getNumber(), device.getId(), message);
pubSubManager.publish(address, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.QUERY_DB)
.build());
return new DeliveryStatus(false, queueDepth);
}
}
public boolean sendProvisioningMessage(ProvisioningAddress address, byte[] body) {
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.DELIVER)
.setContent(ByteString.copyFrom(body))
.build();
if (pubSubManager.publish(address, pubSubMessage)) {
provisioningOnlineMeter.mark();
return true;
} else {
provisioningOfflineMeter.mark();
return false;
}
}
public static class DeliveryStatus {
private final boolean delivered;
private final int messageQueueDepth;
public DeliveryStatus(boolean delivered, int messageQueueDepth) {
this.delivered = delivered;
this.messageQueueDepth = messageQueueDepth;
}
public boolean isDelivered() {
return delivered;
}
public int getMessageQueueDepth() {
return messageQueueDepth;
}
}
}

View File

@@ -53,7 +53,7 @@ public class SmsSender {
try {
twilioSender.deliverSmsVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio SMS Fallback", e);
logger.info("Twilio SMS Failed: " + e.getErrorMessage());
if (nexmoSender.isPresent()) {
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
}
@@ -70,7 +70,7 @@ public class SmsSender {
try {
twilioSender.deliverVoxVerification(destination, verificationCode);
} catch (TwilioRestException e) {
logger.info("Twilio Vox Fallback", e);
logger.info("Twilio Vox Failed: " + e.getErrorMessage());
if (nexmoSender.isPresent()) {
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
}

View File

@@ -22,16 +22,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
public class Account implements Serializable {
public class Account {
public static final int MEMCACHE_VERION = 4;
@JsonIgnore
private long id;
public static final int MEMCACHE_VERION = 5;
@JsonProperty
private String number;
@@ -40,37 +36,29 @@ public class Account implements Serializable {
private boolean supportsSms;
@JsonProperty
private List<Device> devices = new LinkedList<>();
private Set<Device> devices = new HashSet<>();
@JsonProperty
private String identityKey;
@JsonIgnore
private Optional<Device> authenticatedDevice;
private Device authenticatedDevice;
public Account() {}
@VisibleForTesting
public Account(String number, boolean supportsSms, List<Device> devices) {
public Account(String number, boolean supportsSms, Set<Device> devices) {
this.number = number;
this.supportsSms = supportsSms;
this.devices = devices;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Optional<Device> getAuthenticatedDevice() {
return authenticatedDevice;
return Optional.fromNullable(authenticatedDevice);
}
public void setAuthenticatedDevice(Device device) {
this.authenticatedDevice = Optional.of(device);
this.authenticatedDevice = device;
}
public void setNumber(String number) {
@@ -90,14 +78,11 @@ public class Account implements Serializable {
}
public void addDevice(Device device) {
this.devices.remove(device);
this.devices.add(device);
}
public void setDevices(List<Device> devices) {
this.devices = devices;
}
public List<Device> getDevices() {
public Set<Device> getDevices() {
return devices;
}
@@ -125,7 +110,9 @@ public class Account implements Serializable {
long highestDevice = Device.MASTER_ID;
for (Device device : devices) {
if (device.getId() > highestDevice) {
if (!device.isActive()) {
return device.getId();
} else if (device.getId() > highestDevice) {
highestDevice = device.getId();
}
}
@@ -133,6 +120,16 @@ public class Account implements Serializable {
return highestDevice + 1;
}
public int getActiveDeviceCount() {
int count = 0;
for (Device device : devices) {
if (device.isActive()) count++;
}
return count;
}
public boolean isRateLimited() {
return true;
}

View File

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

View File

@@ -17,27 +17,39 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class AccountsManager {
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
private final Accounts accounts;
private final MemcachedClient memcachedClient;
private final JedisPool cacheClient;
private final DirectoryManager directory;
private final ObjectMapper mapper;
public AccountsManager(Accounts accounts,
DirectoryManager directory,
MemcachedClient memcachedClient)
JedisPool cacheClient)
{
this.accounts = accounts;
this.directory = directory;
this.memcachedClient = memcachedClient;
this.accounts = accounts;
this.directory = directory;
this.cacheClient = cacheClient;
this.mapper = SystemMapper.getMapper();
}
public long getCount() {
@@ -54,40 +66,28 @@ public class AccountsManager {
public void create(Account account) {
accounts.create(account);
if (memcachedClient != null) {
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
memcacheSet(account.getNumber(), account);
updateDirectory(account);
}
public void update(Account account) {
if (memcachedClient != null) {
memcachedClient.set(getKey(account.getNumber()), 0, account);
}
memcacheSet(account.getNumber(), account);
accounts.update(account);
updateDirectory(account);
}
public Optional<Account> get(String number) {
Account account = null;
Optional<Account> account = memcacheGet(number);
if (memcachedClient != null) {
account = (Account)memcachedClient.get(getKey(number));
}
if (!account.isPresent()) {
account = Optional.fromNullable(accounts.get(number));
if (account == null) {
account = accounts.get(number);
if (account != null && memcachedClient != null) {
memcachedClient.set(getKey(number), 0, account);
if (account.isPresent()) {
memcacheSet(number, account.get());
}
}
if (account != null) return Optional.of(account);
else return Optional.absent();
return account;
}
public boolean isRelayListed(String number) {
@@ -111,4 +111,24 @@ public class AccountsManager {
return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number;
}
private void memcacheSet(String number, Account account) {
try (Jedis jedis = cacheClient.getResource()) {
jedis.set(getKey(number), mapper.writeValueAsString(account));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
private Optional<Account> memcacheGet(String number) {
try (Jedis jedis = cacheClient.getResource()) {
String json = jedis.get(getKey(number));
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
else return Optional.absent();
} catch (IOException e) {
logger.warn("AccountsManager", "Deserialization error", e);
return Optional.absent();
}
}
}

View File

@@ -22,9 +22,9 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
public class Device implements Serializable {
public class Device {
public static final long MASTER_ID = 1;
@@ -46,6 +46,9 @@ public class Device implements Serializable {
@JsonProperty
private String apnId;
@JsonProperty
private long pushTimestamp;
@JsonProperty
private boolean fetchesMessages;
@@ -55,12 +58,15 @@ public class Device implements Serializable {
@JsonProperty
private SignedPreKey signedPreKey;
@JsonProperty
private long lastSeen;
public Device() {}
public Device(long id, String authToken, String salt,
String signalingKey, String gcmId, String apnId,
boolean fetchesMessages, int registrationId,
SignedPreKey signedPreKey)
SignedPreKey signedPreKey, long lastSeen)
{
this.id = id;
this.authToken = authToken;
@@ -71,6 +77,7 @@ public class Device implements Serializable {
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
this.lastSeen = lastSeen;
}
public String getApnId() {
@@ -79,6 +86,18 @@ public class Device implements Serializable {
public void setApnId(String apnId) {
this.apnId = apnId;
if (apnId != null) {
this.pushTimestamp = System.currentTimeMillis();
}
}
public void setLastSeen(long lastSeen) {
this.lastSeen = lastSeen;
}
public long getLastSeen() {
return lastSeen;
}
public String getGcmId() {
@@ -87,6 +106,10 @@ public class Device implements Serializable {
public void setGcmId(String gcmId) {
this.gcmId = gcmId;
if (gcmId != null) {
this.pushTimestamp = System.currentTimeMillis();
}
}
public long getId() {
@@ -115,7 +138,10 @@ public class Device implements Serializable {
}
public boolean isActive() {
return fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
boolean hasChannel = fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
return (id == MASTER_ID && hasChannel) ||
(id != MASTER_ID && hasChannel && signedPreKey != null && lastSeen > (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30)));
}
public boolean getFetchesMessages() {
@@ -145,4 +171,21 @@ public class Device implements Serializable {
public void setSignedPreKey(SignedPreKey signedPreKey) {
this.signedPreKey = signedPreKey;
}
public long getPushTimestamp() {
return pushTimestamp;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof Device)) return false;
Device that = (Device)other;
return this.id == that.id;
}
@Override
public int hashCode() {
return (int)this.id;
}
}

View File

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

View File

@@ -31,8 +31,6 @@ import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.PreKeyBase;
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
@@ -114,6 +112,9 @@ public abstract class Keys {
else return Optional.absent();
}
@SqlUpdate("VACUUM keys")
public abstract void vacuum();
@BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})

View File

@@ -0,0 +1,102 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.protobuf.ByteString;
import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.Binder;
import org.skife.jdbi.v2.sqlobject.BinderFactory;
import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.util.Pair;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
public abstract class Messages {
private static final String ID = "id";
private static final String TYPE = "type";
private static final String RELAY = "relay";
private static final String TIMESTAMP = "timestamp";
private static final String SOURCE = "source";
private static final String SOURCE_DEVICE = "source_device";
private static final String DESTINATION = "destination";
private static final String DESTINATION_DEVICE = "destination_device";
private static final String MESSAGE = "message";
@SqlQuery("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ") " +
"VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message) " +
"RETURNING (SELECT COUNT(id) FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + TYPE + " != " + OutgoingMessageSignal.Type.RECEIPT_VALUE + ")")
abstract int store(@MessageBinder OutgoingMessageSignal message,
@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice);
@Mapper(MessageMapper.class)
@SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC")
abstract List<Pair<Long, OutgoingMessageSignal>> load(@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice);
@SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id")
abstract void remove(@Bind("id") long id);
@SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination")
abstract void clear(@Bind("destination") String destination);
@SqlUpdate("VACUUM messages")
public abstract void vacuum();
public static class MessageMapper implements ResultSetMapper<Pair<Long, OutgoingMessageSignal>> {
@Override
public Pair<Long, OutgoingMessageSignal> map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException
{
return new Pair<>(resultSet.getLong(ID),
OutgoingMessageSignal.newBuilder()
.setType(resultSet.getInt(TYPE))
.setRelay(resultSet.getString(RELAY))
.setTimestamp(resultSet.getLong(TIMESTAMP))
.setSource(resultSet.getString(SOURCE))
.setSourceDevice(resultSet.getInt(SOURCE_DEVICE))
.setMessage(ByteString.copyFrom(resultSet.getBytes(MESSAGE)))
.build());
}
}
@BindingAnnotation(MessageBinder.AccountBinderFactory.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface MessageBinder {
public static class AccountBinderFactory implements BinderFactory {
@Override
public Binder build(Annotation annotation) {
return new Binder<MessageBinder, OutgoingMessageSignal>() {
@Override
public void bind(SQLStatement<?> sql,
MessageBinder accountBinder,
OutgoingMessageSignal message)
{
sql.bind(TYPE, message.getType());
sql.bind(RELAY, message.getRelay());
sql.bind(TIMESTAMP, message.getTimestamp());
sql.bind(SOURCE, message.getSource());
sql.bind(SOURCE_DEVICE, message.getSourceDevice());
sql.bind(MESSAGE, message.getMessage().toByteArray());
}
};
}
}
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecuregcm.storage;
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import org.whispersystems.textsecuregcm.util.Pair;
import java.util.List;
public class MessagesManager {
private final Messages messages;
public MessagesManager(Messages messages) {
this.messages = messages;
}
public int insert(String destination, long destinationDevice, OutgoingMessageSignal message) {
return this.messages.store(message, destination, destinationDevice) + 1;
}
public List<Pair<Long, OutgoingMessageSignal>> getMessagesForDevice(String destination, long destinationDevice) {
return this.messages.load(destination, destinationDevice);
}
public void clear(String destination) {
this.messages.clear(destination);
}
public void delete(long id) {
this.messages.remove(id);
}
}

View File

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

View File

@@ -17,52 +17,62 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class PendingAccountsManager {
private static final String MEMCACHE_PREFIX = "pending_account";
private static final String CACHE_PREFIX = "pending_account::";
private final PendingAccounts pendingAccounts;
private final MemcachedClient memcachedClient;
private final JedisPool cacheClient;
public PendingAccountsManager(PendingAccounts pendingAccounts,
MemcachedClient memcachedClient)
public PendingAccountsManager(PendingAccounts pendingAccounts, JedisPool cacheClient)
{
this.pendingAccounts = pendingAccounts;
this.memcachedClient = memcachedClient;
this.cacheClient = cacheClient;
}
public void store(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
memcacheSet(number, code);
pendingAccounts.insert(number, code);
}
public void remove(String number) {
if (memcachedClient != null)
memcachedClient.delete(MEMCACHE_PREFIX + number);
memcacheDelete(number);
pendingAccounts.remove(number);
}
public Optional<String> getCodeForNumber(String number) {
String code = null;
Optional<String> code = memcacheGet(number);
if (memcachedClient != null) {
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
}
if (!code.isPresent()) {
code = Optional.fromNullable(pendingAccounts.getCodeForNumber(number));
if (code == null) {
code = pendingAccounts.getCodeForNumber(number);
if (code != null && memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
if (code.isPresent()) {
memcacheSet(number, code.get());
}
}
if (code != null) return Optional.of(code);
else return Optional.absent();
return code;
}
private void memcacheSet(String number, String code) {
try (Jedis jedis = cacheClient.getResource()) {
jedis.set(CACHE_PREFIX + number, code);
}
}
private Optional<String> memcacheGet(String number) {
try (Jedis jedis = cacheClient.getResource()) {
return Optional.fromNullable(jedis.get(CACHE_PREFIX + number));
}
}
private void memcacheDelete(String number) {
try (Jedis jedis = cacheClient.getResource()) {
jedis.del(CACHE_PREFIX + number);
}
}
}

View File

@@ -17,54 +17,64 @@
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import net.spy.memcached.MemcachedClient;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class PendingDevicesManager {
private static final String MEMCACHE_PREFIX = "pending_devices";
private static final String CACHE_PREFIX = "pending_devices::";
private final PendingDevices pendingDevices;
private final MemcachedClient memcachedClient;
private final JedisPool cacheClient;
public PendingDevicesManager(PendingDevices pendingDevices,
MemcachedClient memcachedClient)
JedisPool cacheClient)
{
this.pendingDevices = pendingDevices;
this.memcachedClient = memcachedClient;
this.pendingDevices = pendingDevices;
this.cacheClient = cacheClient;
}
public void store(String number, String code) {
if (memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
}
memcacheSet(number, code);
pendingDevices.insert(number, code);
}
public void remove(String number) {
if (memcachedClient != null) {
memcachedClient.delete(MEMCACHE_PREFIX + number);
}
memcacheDelete(number);
pendingDevices.remove(number);
}
public Optional<String> getCodeForNumber(String number) {
String code = null;
Optional<String> code = memcacheGet(number);
if (memcachedClient != null) {
code = (String)memcachedClient.get(MEMCACHE_PREFIX + number);
}
if (!code.isPresent()) {
code = Optional.fromNullable(pendingDevices.getCodeForNumber(number));
if (code == null) {
code = pendingDevices.getCodeForNumber(number);
if (code != null && memcachedClient != null) {
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
if (code.isPresent()) {
memcacheSet(number, code.get());
}
}
if (code != null) return Optional.of(code);
else return Optional.absent();
return code;
}
private void memcacheSet(String number, String code) {
try (Jedis jedis = cacheClient.getResource()) {
jedis.set(CACHE_PREFIX + number, code);
}
}
private Optional<String> memcacheGet(String number) {
try (Jedis jedis = cacheClient.getResource()) {
return Optional.fromNullable(jedis.get(CACHE_PREFIX + number));
}
}
private void memcacheDelete(String number) {
try (Jedis jedis = cacheClient.getResource()) {
jedis.del(CACHE_PREFIX + number);
}
}
}

View File

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

View File

@@ -1,62 +1,59 @@
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
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.JedisPool;
import redis.clients.jedis.JedisPubSub;
public class PubSubManager {
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final ObjectMapper mapper = new ObjectMapper();
private final SubscriptionListener baseListener = new SubscriptionListener();
private final Map<WebsocketAddress, PubSubListener> listeners = new HashMap<>();
private static final byte[] KEEPALIVE_CHANNEL = "KEEPALIVE".getBytes();
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
private final SubscriptionListener baseListener = new SubscriptionListener();
private final Map<String, PubSubListener> listeners = new HashMap<>();
private final JedisPool jedisPool;
private boolean subscribed = false;
public PubSubManager(final JedisPool jedisPool) {
public PubSubManager(JedisPool jedisPool) {
this.jedisPool = jedisPool;
initializePubSubWorker();
waitForSubscription();
}
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
listeners.put(address, listener);
baseListener.subscribe(address.toString());
String serializedAddress = address.serialize();
listeners.put(serializedAddress, listener);
baseListener.subscribe(serializedAddress.getBytes());
}
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
if (listeners.get(address) == listener) {
listeners.remove(address);
baseListener.unsubscribe(address.toString());
String serializedAddress = address.serialize();
if (listeners.get(serializedAddress) == listener) {
listeners.remove(serializedAddress);
baseListener.unsubscribe(serializedAddress.getBytes());
}
}
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
try {
String serialized = mapper.writeValueAsString(message);
Jedis jedis = null;
return publish(address.serialize().getBytes(), message);
}
try {
jedis = jedisPool.getResource();
return jedis.publish(address.toString(), serialized) != 0;
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
} catch (JsonProcessingException e) {
throw new AssertionError(e);
private synchronized boolean publish(byte[] channel, PubSubMessage message) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.publish(channel, message.toByteArray()) != 0;
}
}
@@ -75,14 +72,9 @@ public class PubSubManager {
@Override
public void run() {
for (;;) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.subscribe(baseListener, new WebsocketAddress(0, 0).toString());
try (Jedis jedis = jedisPool.getResource()) {
jedis.subscribe(baseListener, KEEPALIVE_CHANNEL);
logger.warn("**** Unsubscribed from holding channel!!! ******");
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
}
}
@@ -94,7 +86,9 @@ public class PubSubManager {
for (;;) {
try {
Thread.sleep(20000);
publish(new WebsocketAddress(0, 0), new PubSubMessage(0, "foo"));
publish(KEEPALIVE_CHANNEL, PubSubMessage.newBuilder()
.setType(PubSubMessage.Type.KEEPALIVE)
.build());
} catch (InterruptedException e) {
throw new AssertionError(e);
}
@@ -103,56 +97,47 @@ public class PubSubManager {
}.start();
}
private class SubscriptionListener extends JedisPubSub {
private class SubscriptionListener extends BinaryJedisPubSub {
@Override
public void onMessage(String channel, String message) {
public void onMessage(byte[] channel, byte[] message) {
try {
WebsocketAddress address = new WebsocketAddress(channel);
PubSubListener listener;
PubSubListener listener;
synchronized (PubSubManager.this) {
listener = listeners.get(address);
listener = listeners.get(new String(channel));
}
if (listener != null) {
listener.onPubSubMessage(mapper.readValue(message, PubSubMessage.class));
listener.onPubSubMessage(PubSubMessage.parseFrom(message));
}
} catch (InvalidWebsocketAddressException e) {
logger.warn("Address", e);
} catch (IOException e) {
logger.warn("IOE", e);
} catch (InvalidProtocolBufferException e) {
logger.warn("Error parsing PubSub protobuf", e);
}
}
@Override
public void onPMessage(String s, String s2, String s3) {
public void onPMessage(byte[] s, byte[] s2, byte[] s3) {
logger.warn("Received PMessage!");
}
@Override
public void onSubscribe(String channel, int count) {
try {
WebsocketAddress address = new WebsocketAddress(channel);
if (address.getAccountId() == 0 && address.getDeviceId() == 0) {
synchronized (PubSubManager.this) {
subscribed = true;
PubSubManager.this.notifyAll();
}
public void onSubscribe(byte[] channel, int count) {
if (Arrays.equals(KEEPALIVE_CHANNEL, channel)) {
synchronized (PubSubManager.this) {
subscribed = true;
PubSubManager.this.notifyAll();
}
} catch (InvalidWebsocketAddressException e) {
logger.warn("Weird address", e);
}
}
@Override
public void onUnsubscribe(String s, int i) {}
public void onUnsubscribe(byte[] s, int i) {}
@Override
public void onPUnsubscribe(String s, int i) {}
public void onPUnsubscribe(byte[] s, int i) {}
@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,652 @@
// 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>CLOSE = 4;</code>
*/
CLOSE(4, 4),
;
/**
* <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;
/**
* <code>CLOSE = 4;</code>
*/
public static final int CLOSE_VALUE = 4;
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;
case 4: return CLOSE;
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\"\230\001\n\rPu" +
"bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" +
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"H\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\003\022\t\n\005CLOSE\020\004B8\n(org.whis" +
"persystems.textsecuregcm.storageB\014PubSub" +
"Protos"
};
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

@@ -1,86 +0,0 @@
/**
* Copyright (C) 2014 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.LinkedList;
import java.util.List;
import static com.codahale.metrics.MetricRegistry.name;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class StoredMessages {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
private static final String QUEUE_PREFIX = "msgs";
private final JedisPool jedisPool;
public StoredMessages(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public void insert(long accountId, long deviceId, String message) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
long queueSize = jedis.lpush(getKey(accountId, deviceId), message);
queueSizeHistogram.update(queueSize);
if (queueSize > 1000) {
jedis.ltrim(getKey(accountId, deviceId), 0, 999);
}
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
}
public List<String> getMessagesForDevice(long accountId, long deviceId) {
List<String> messages = new LinkedList<>();
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String message;
while ((message = jedis.rpop(QUEUE_PREFIX + accountId + ":" + deviceId)) != null) {
messages.add(message);
}
return messages;
} finally {
if (jedis != null)
jedisPool.returnResource(jedis);
}
}
private String getKey(long accountId, long deviceId) {
return QUEUE_PREFIX + ":" + accountId + ":" + deviceId;
}
}

View File

@@ -1,41 +0,0 @@
/**
* Copyright (C) 2014 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.util;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CORSHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
((HttpServletResponse) response).addHeader("Access-Control-Allow-Origin", "*");
((HttpServletResponse) response).addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
((HttpServletResponse) response).addHeader("Access-Control-Allow-Headers", "Authorization, Content-type");
}
chain.doFilter(request, response);
}
@Override public void init(FilterConfig filterConfig) throws ServletException { }
@Override public void destroy() { }
}

View File

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

View File

@@ -21,6 +21,7 @@ import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class Util {
@@ -83,4 +84,44 @@ public class Util {
return result;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
return parts;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength, int fourthLength) {
byte[][] parts = new byte[4][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
parts[2] = new byte[thirdLength];
System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength);
parts[3] = new byte[fourthLength];
System.arraycopy(input, firstLength + secondLength + thirdLength, parts[3], 0, fourthLength);
return parts;
}
public static void sleep(int i) {
try {
Thread.sleep(i);
} catch (InterruptedException ie) {}
}
public static long todayInMillis() {
return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
}
}

View File

@@ -0,0 +1,71 @@
package org.whispersystems.textsecuregcm.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.websocket.session.WebSocketSessionContext;
import org.whispersystems.websocket.setup.WebSocketConnectListener;
public class AuthenticatedConnectListener implements WebSocketConnectListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private final AccountsManager accountsManager;
private final PushSender pushSender;
private final MessagesManager messagesManager;
private final PubSubManager pubSubManager;
public AuthenticatedConnectListener(AccountsManager accountsManager, PushSender pushSender,
MessagesManager messagesManager, PubSubManager pubSubManager)
{
this.accountsManager = accountsManager;
this.pushSender = pushSender;
this.messagesManager = messagesManager;
this.pubSubManager = pubSubManager;
}
@Override
public void onWebSocketConnect(WebSocketSessionContext context) {
Account account = context.getAuthenticated(Account.class).get();
Device device = account.getAuthenticatedDevice().get();
updateLastSeen(account, device);
closeExistingDeviceConnection(account, device);
final WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender,
messagesManager, pubSubManager,
account, device,
context.getClient());
connection.onConnected();
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
@Override
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
connection.onConnectionLost();
}
});
}
private void updateLastSeen(Account account, Device device) {
if (device.getLastSeen() != Util.todayInMillis()) {
device.setLastSeen(Util.todayInMillis());
accountsManager.update(account);
}
}
private void closeExistingDeviceConnection(Account account, Device device) {
pubSubManager.publish(new WebsocketAddress(account.getNumber(), device.getId()),
PubSubProtos.PubSubMessage.newBuilder()
.setType(PubSubProtos.PubSubMessage.Type.CLOSE)
.build());
}
}

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecuregcm.websocket;
import org.whispersystems.textsecuregcm.util.Base64;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class ProvisioningAddress extends WebsocketAddress {
private final String address;
public ProvisioningAddress(String address) throws InvalidWebsocketAddressException {
super(address, 0);
this.address = address;
}
public String getAddress() {
return address;
}
public static ProvisioningAddress generate() {
try {
byte[] random = new byte[16];
SecureRandom.getInstance("SHA1PRNG").nextBytes(random);
return new ProvisioningAddress(Base64.encodeBytesWithoutPadding(random)
.replace('+', '-').replace('/', '_'));
} catch (NoSuchAlgorithmException | InvalidWebsocketAddressException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.textsecuregcm.websocket;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.websocket.session.WebSocketSessionContext;
import org.whispersystems.websocket.setup.WebSocketConnectListener;
public class ProvisioningConnectListener implements WebSocketConnectListener {
private final PubSubManager pubSubManager;
public ProvisioningConnectListener(PubSubManager pubSubManager) {
this.pubSubManager = pubSubManager;
}
@Override
public void onWebSocketConnect(WebSocketSessionContext context) {
final ProvisioningConnection connection = new ProvisioningConnection(pubSubManager, context.getClient());
connection.onConnected();
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
@Override
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
connection.onConnectionLost();
}
});
}
}

View File

@@ -0,0 +1,61 @@
package org.whispersystems.textsecuregcm.websocket;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid;
import org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
public class ProvisioningConnection implements PubSubListener {
private final PubSubManager pubSubManager;
private final ProvisioningAddress provisioningAddress;
private final WebSocketClient client;
public ProvisioningConnection(PubSubManager pubSubManager, WebSocketClient client) {
this.pubSubManager = pubSubManager;
this.client = client;
this.provisioningAddress = ProvisioningAddress.generate();
}
@Override
public void onPubSubMessage(PubSubMessage outgoingMessage) {
if (outgoingMessage.getType() == PubSubMessage.Type.DELIVER) {
Optional<byte[]> body = Optional.of(outgoingMessage.getContent().toByteArray());
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/v1/message", body);
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
@Override
public void onSuccess(WebSocketResponseMessage webSocketResponseMessage) {
pubSubManager.unsubscribe(provisioningAddress, ProvisioningConnection.this);
client.close(1001, "All you get.");
}
@Override
public void onFailure(Throwable throwable) {
pubSubManager.unsubscribe(provisioningAddress, ProvisioningConnection.this);
client.close(1001, "That's all!");
}
});
}
}
public void onConnected() {
this.pubSubManager.subscribe(provisioningAddress, this);
this.client.sendRequest("PUT", "/v1/address", Optional.of(ProvisioningUuid.newBuilder()
.setUuid(provisioningAddress.getAddress())
.build()
.toByteArray()));
}
public void onConnectionLost() {
this.pubSubManager.unsubscribe(provisioningAddress, this);
this.client.close(1001, "Done");
}
}

View File

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

View File

@@ -0,0 +1,171 @@
package org.whispersystems.textsecuregcm.websocket;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PubSubListener;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
public class WebSocketConnection implements PubSubListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private final AccountsManager accountsManager;
private final PushSender pushSender;
private final MessagesManager messagesManager;
private final PubSubManager pubSubManager;
private final Account account;
private final Device device;
private final WebsocketAddress address;
private final WebSocketClient client;
public WebSocketConnection(AccountsManager accountsManager,
PushSender pushSender,
MessagesManager messagesManager,
PubSubManager pubSubManager,
Account account,
Device device,
WebSocketClient client)
{
this.accountsManager = accountsManager;
this.pushSender = pushSender;
this.messagesManager = messagesManager;
this.pubSubManager = pubSubManager;
this.account = account;
this.device = device;
this.client = client;
this.address = new WebsocketAddress(account.getNumber(), device.getId());
}
public void onConnected() {
pubSubManager.subscribe(address, this);
processStoredMessages();
}
public void onConnectionLost() {
pubSubManager.unsubscribe(address, this);
}
@Override
public void onPubSubMessage(PubSubMessage pubSubMessage) {
try {
switch (pubSubMessage.getType().getNumber()) {
case PubSubMessage.Type.QUERY_DB_VALUE:
processStoredMessages();
break;
case PubSubMessage.Type.DELIVER_VALUE:
sendMessage(OutgoingMessageSignal.parseFrom(pubSubMessage.getContent()), Optional.<Long>absent());
break;
case PubSubMessage.Type.CLOSE_VALUE:
client.close(1000, "OK");
pubSubManager.unsubscribe(address, this);
break;
default:
logger.warn("Unknown pubsub message: " + pubSubMessage.getType().getNumber());
}
} catch (InvalidProtocolBufferException e) {
logger.warn("Protobuf parse error", e);
}
}
private void sendMessage(final OutgoingMessageSignal message,
final Optional<Long> storedMessageId)
{
try {
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
Optional<byte[]> body = Optional.fromNullable(encryptedMessage.toByteArray());
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/api/v1/message", body);
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
@Override
public void onSuccess(@Nullable WebSocketResponseMessage response) {
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
if (isSuccessResponse(response)) {
if (storedMessageId.isPresent()) messagesManager.delete(storedMessageId.get());
if (!isReceipt) sendDeliveryReceiptFor(message);
} else if (!isSuccessResponse(response) & !storedMessageId.isPresent()) {
requeueMessage(message);
}
}
@Override
public void onFailure(@Nonnull Throwable throwable) {
if (!storedMessageId.isPresent()) requeueMessage(message);
}
private boolean isSuccessResponse(WebSocketResponseMessage response) {
return response != null && response.getStatus() >= 200 && response.getStatus() < 300;
}
});
} catch (CryptoEncodingException e) {
logger.warn("Bad signaling key", e);
}
}
private void requeueMessage(OutgoingMessageSignal message) {
try {
pushSender.sendMessage(account, device, message);
} catch (NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("requeueMessage", e);
messagesManager.insert(account.getNumber(), device.getId(), message);
}
}
private void sendDeliveryReceiptFor(OutgoingMessageSignal message) {
try {
Optional<Account> source = accountsManager.get(message.getSource());
if (!source.isPresent()) {
logger.warn("Source account disappeared? (%s)", message.getSource());
return;
}
OutgoingMessageSignal.Builder receipt =
OutgoingMessageSignal.newBuilder()
.setSource(account.getNumber())
.setSourceDevice((int) device.getId())
.setTimestamp(message.getTimestamp())
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
for (Device device : source.get().getDevices()) {
pushSender.sendMessage(source.get(), device, receipt.build());
}
} catch (NotPushRegisteredException | TransientPushFailureException e) {
logger.warn("sendDeliveryReceiptFor", "Delivery receipet", e);
}
}
private void processStoredMessages() {
List<Pair<Long, OutgoingMessageSignal>> messages = messagesManager.getMessagesForDevice(account.getNumber(),
device.getId());
for (Pair<Long, OutgoingMessageSignal> message : messages) {
sendMessage(message.second(), Optional.of(message.first()));
}
}
}

View File

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

View File

@@ -1,47 +0,0 @@
package org.whispersystems.textsecuregcm.websocket;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.controllers.WebsocketController;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.WebsocketSender;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
public class WebsocketControllerFactory extends WebSocketServlet implements WebSocketCreator {
private final Logger logger = LoggerFactory.getLogger(WebsocketControllerFactory.class);
private final PushSender pushSender;
private final StoredMessages storedMessages;
private final PubSubManager pubSubManager;
private final AccountAuthenticator accountAuthenticator;
public WebsocketControllerFactory(AccountAuthenticator accountAuthenticator,
PushSender pushSender,
StoredMessages storedMessages,
PubSubManager pubSubManager)
{
this.accountAuthenticator = accountAuthenticator;
this.pushSender = pushSender;
this.storedMessages = storedMessages;
this.pubSubManager = pubSubManager;
}
@Override
public void configure(WebSocketServletFactory factory) {
factory.setCreator(this);
}
@Override
public Object createWebSocket(UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse) {
return new WebsocketController(accountAuthenticator, pushSender, pubSubManager, storedMessages);
}
}

View File

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

View File

@@ -17,13 +17,11 @@
package org.whispersystems.textsecuregcm.workers;
import net.sourceforge.argparse4j.inf.Namespace;
import net.spy.memcached.MemcachedClient;
import org.skife.jdbi.v2.DBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
@@ -62,10 +60,10 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
dbi.registerContainerFactory(new OptionalContainerFactory());
Accounts accounts = dbi.onDemand(Accounts.class);
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
JedisPool cacheClient = new RedisClientFactory(config.getCacheConfiguration().getUrl()).getRedisClientPool();
JedisPool redisClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
<changeSet id="1" author="moxie">
<createTable tableName="messages">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="type" type="tinyint">
<constraints nullable="false"/>
</column>
<column name="relay" type="text">
<constraints nullable="false"/>
</column>
<column name="timestamp" type="bigint">
<constraints nullable="false"/>
</column>
<column name="source" type="text">
<constraints nullable="false"/>
</column>
<column name="source_device" type="int">
<constraints nullable="false"/>
</column>
<column name="destination" type="text">
<constraints nullable="false"/>
</column>
<column name="destination_device" type="int">
<constraints nullable="false"/>
</column>
<column name="message" type="bytea">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="messages" indexName="destination_index">
<column name="destination"></column>
<column name="destination_device"></column>
</createIndex>
<createIndex tableName="messages" indexName="destination_and_type_index">
<column name="destination"></column>
<column name="destination_device"></column>
<column name="type"></column>
</createIndex>
</changeSet>
</databaseChangeLog>

View File

@@ -2,6 +2,8 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -9,10 +11,13 @@ import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
//import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType;
@@ -26,11 +31,14 @@ public class AccountControllerTest {
private static final String SENDER = "+14152222222";
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class );
private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class );
private SmsSender smsSender = mock(SmsSender.class );
private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class );
private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class );
private SmsSender smsSender = mock(SmsSender.class );
private MessagesManager storedMessages = mock(MessagesManager.class );
private TimeProvider timeProvider = mock(TimeProvider.class );
private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f");
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
@@ -38,7 +46,10 @@ public class AccountControllerTest {
.addResource(new AccountController(pendingAccountsManager,
accountsManager,
rateLimiters,
smsSender))
smsSender,
storedMessages,
timeProvider,
Optional.of(authorizationKey)))
.build();
@@ -48,6 +59,8 @@ public class AccountControllerTest {
when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter);
when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis());
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234"));
}
@@ -90,4 +103,84 @@ public class AccountControllerTest {
verifyNoMoreInteractions(accountsManager);
}
@Test
public void testVerifyToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager, times(1)).create(isA(Account.class));
}
@Test
public void testVerifyBadToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
String token = SENDER + ":1415906574:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager);
}
@Test
public void testVerifyWrongToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L);
String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager);
}
@Test
public void testVerifyExpiredToken() throws Exception {
when(timeProvider.getCurrentTimeMillis()).thenReturn(1416003757901L);
String token = SENDER + ":1415906573:af4f046107c21721224a";
ClientResponse response =
resources.client().resource(String.format("/v1/accounts/token/%s", token))
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class);
assertThat(response.getStatus()).isEqualTo(403);
verifyNoMoreInteractions(accountsManager);
}
private static byte[] decodeHex(String hex) {
try {
return Hex.decodeHex(hex.toCharArray());
} catch (DecoderException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -82,7 +82,7 @@ public class DeviceControllerTest {
@Test
public void validDeviceRegisterTest() throws Exception {
VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning_code")
VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning/code")
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(VerificationCode.class);

View File

@@ -27,8 +27,10 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.hamcrest.CoreMatchers.equalTo;
@@ -69,13 +71,13 @@ public class FederatedControllerTest {
@Before
public void setup() throws Exception {
List<Device> singleDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null, System.currentTimeMillis()));
}};
List<Device> multiDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null, System.currentTimeMillis()));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null, System.currentTimeMillis()));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);

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