mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-11 01:40:22 +00:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c410348278 | ||
|
|
d8a758211f | ||
|
|
33e60f2527 | ||
|
|
fb705eee23 | ||
|
|
635e16e934 | ||
|
|
1deb3ae67f | ||
|
|
16ff40f420 | ||
|
|
a8b5cb23fe | ||
|
|
82f88d04ad | ||
|
|
0e1091e0ea | ||
|
|
7b48f10cc9 | ||
|
|
0be34b1135 | ||
|
|
fb5e0242d0 | ||
|
|
d376035557 | ||
|
|
747b2dc7c5 | ||
|
|
fac2f1bee3 | ||
|
|
a211f6aed9 | ||
|
|
0bc494245d | ||
|
|
b31a88043e | ||
|
|
85509c6d8b | ||
|
|
2dd131cf79 | ||
|
|
51990d0b33 | ||
|
|
00a49afc30 | ||
|
|
faa0630851 | ||
|
|
aac3fc68fc | ||
|
|
9c08b96b50 | ||
|
|
15ddde1df4 | ||
|
|
f2a9de3ba8 | ||
|
|
fd725206e2 | ||
|
|
6368b9383a | ||
|
|
2b8a11b001 | ||
|
|
c9e0339a30 | ||
|
|
8d11595290 | ||
|
|
2fe9f3effa | ||
|
|
ae122ff8a2 | ||
|
|
8b941ddd33 | ||
|
|
2902ea6689 | ||
|
|
5ccbf355bd | ||
|
|
62d8f635b0 | ||
|
|
4c3aae63d3 | ||
|
|
8f94aa0c0d | ||
|
|
0370306bb6 | ||
|
|
c9176efe6f | ||
|
|
a3fd08b7ef | ||
|
|
83a9e36ef1 | ||
|
|
9668decc84 | ||
|
|
328bb47d44 | ||
|
|
c74e0b9eab | ||
|
|
20dc32413f | ||
|
|
d4e618893c | ||
|
|
8d0d934249 | ||
|
|
ef2441ad82 | ||
|
|
bb7859138c | ||
|
|
ebc4570941 | ||
|
|
d04baed38b | ||
|
|
001c81f797 | ||
|
|
3327bf4788 | ||
|
|
e0b480e232 | ||
|
|
b328d85230 | ||
|
|
3afaa5c1e6 | ||
|
|
f2c8699823 | ||
|
|
4c11315a3c | ||
|
|
0e3a347d6b | ||
|
|
dc723fadaa | ||
|
|
6396958a31 | ||
|
|
1fe57e4841 | ||
|
|
3885ae6337 | ||
|
|
39e3366b3b | ||
|
|
a5ffd47935 | ||
|
|
18a96a445b | ||
|
|
de366b976e | ||
|
|
8f6aff3a7e | ||
|
|
fb411b20cc | ||
|
|
52ce7d6935 | ||
|
|
75ee398633 | ||
|
|
53bdd946d6 | ||
|
|
79f36664ef | ||
|
|
83078a48ab | ||
|
|
6f67a812dc | ||
|
|
6ad705b40e | ||
|
|
4cb43415a1 | ||
|
|
bbb09b558c | ||
|
|
6363be81e0 | ||
|
|
c6810d7460 | ||
|
|
4c1e7e7c2f | ||
|
|
931081752a | ||
|
|
424e98e67e | ||
|
|
7cfa93f5f8 | ||
|
|
fd8e8d1475 | ||
|
|
7ed5eb22ec | ||
|
|
fa1c275904 | ||
|
|
558c72bbb7 | ||
|
|
37976455bc | ||
|
|
db6ee8f687 | ||
|
|
53e7ffa311 | ||
|
|
e0f7ff325a | ||
|
|
1fcd1e33c5 | ||
|
|
843b16c1f0 | ||
|
|
a58f3f0fe3 | ||
|
|
e69e395b25 | ||
|
|
456164fc24 | ||
|
|
9b7f61a09d | ||
|
|
2de9adb7ae | ||
|
|
c7e0cc1158 | ||
|
|
e79861c30a | ||
|
|
407f596b61 | ||
|
|
41d30fc8dc | ||
|
|
2e429c5b35 | ||
|
|
28afe3470b | ||
|
|
f623b24196 | ||
|
|
2016c17894 | ||
|
|
2d28077010 | ||
|
|
7011f3c3c7 | ||
|
|
1403dbd5dd | ||
|
|
6ef3845a34 | ||
|
|
de2f0914f0 | ||
|
|
080ae0985f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ config/production.yml
|
||||
config/federated.yml
|
||||
config/staging.yml
|
||||
.opsmanage
|
||||
put.sh
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
twilio:
|
||||
accountId:
|
||||
twilio: # Twilio SMS gateway configuration
|
||||
accountId:
|
||||
accountToken:
|
||||
number:
|
||||
localDomain: # The domain Twilio can call back to.
|
||||
international: # Boolean specifying Twilio for international delivery
|
||||
|
||||
# Optional. If specified, Nexmo will be used for non-US SMS and
|
||||
# voice verification if twilio.international is false. Otherwise,
|
||||
# Nexmo, if specified, Nexmo will only be used as a fallback
|
||||
# for failed Twilio deliveries.
|
||||
nexmo:
|
||||
apiKey:
|
||||
apiSecret:
|
||||
number:
|
||||
push: # GCM/APN push server configuration
|
||||
host:
|
||||
port:
|
||||
username:
|
||||
password:
|
||||
|
||||
gcm:
|
||||
senderId:
|
||||
apiKey:
|
||||
|
||||
# Optional. Only if iOS clients are supported.
|
||||
apn:
|
||||
# In PEM format.
|
||||
certificate:
|
||||
|
||||
# In PEM format.
|
||||
key:
|
||||
|
||||
s3:
|
||||
s3: # AWS S3 configuration
|
||||
accessKey:
|
||||
accessSecret:
|
||||
|
||||
@@ -35,13 +19,37 @@ s3:
|
||||
# correct permissions.
|
||||
attachmentsBucket:
|
||||
|
||||
memcache:
|
||||
servers:
|
||||
user:
|
||||
password:
|
||||
directory: # Redis server configuration for TS directory
|
||||
url:
|
||||
|
||||
redis:
|
||||
url:
|
||||
cache: # Redis server configuration for general purpose caching
|
||||
url:
|
||||
|
||||
websocket:
|
||||
enabled: true
|
||||
|
||||
messageStore: # Postgres database configuration for message store
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
|
||||
database: # Postgres database configuration for account store
|
||||
# the name of your JDBC driver
|
||||
driverClass: org.postgresql.Driver
|
||||
|
||||
# the username
|
||||
user:
|
||||
|
||||
# the password
|
||||
password:
|
||||
|
||||
# the JDBC URL
|
||||
url: jdbc:postgresql://somehost:somport/somedb
|
||||
|
||||
# any properties specific to your JDBC driver:
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
|
||||
federation:
|
||||
name:
|
||||
@@ -52,24 +60,3 @@ federation:
|
||||
authenticationToken: foo
|
||||
certificate: in pem format
|
||||
|
||||
# Optional address of graphite server to report metrics
|
||||
graphite:
|
||||
host:
|
||||
port:
|
||||
|
||||
database:
|
||||
# the name of your JDBC driver
|
||||
driverClass: org.postgresql.Driver
|
||||
|
||||
# the username
|
||||
user:
|
||||
|
||||
# the password
|
||||
password:
|
||||
|
||||
# the JDBC URL
|
||||
url: jdbc:postgresql://somehost:somport/somedb
|
||||
|
||||
# any properties specific to your JDBC driver:
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
|
||||
92
pom.xml
92
pom.xml
@@ -9,12 +9,10 @@
|
||||
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<version>0.35</version>
|
||||
<version>0.93</version>
|
||||
|
||||
<properties>
|
||||
<dropwizard.version>0.7.1</dropwizard.version>
|
||||
<jackson.api.version>2.3.3</jackson.api.version>
|
||||
<commons-codec.version>1.6</commons-codec.version>
|
||||
<dropwizard.version>0.9.0-rc3</dropwizard.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -58,94 +56,82 @@
|
||||
<artifactId>dropwizard-papertrail</artifactId>
|
||||
<version>1.1</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.jersey</groupId>
|
||||
<artifactId>jersey-json</artifactId>
|
||||
<version>1.18.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.codahale.metrics</groupId>
|
||||
<artifactId>metrics-graphite</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-server</artifactId>
|
||||
<version>9.0.7.v20131107</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>bouncycastle</groupId>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk16</artifactId>
|
||||
<version>140</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.android.gcm</groupId>
|
||||
<artifactId>gcm-server</artifactId>
|
||||
<version>1.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.notnoop.apns</groupId>
|
||||
<artifactId>apns</artifactId>
|
||||
<version>0.2.3</version>
|
||||
<version>1.46</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
<version>1.10.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>2.5.0</version>
|
||||
<version>2.6.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>2.6.1</version>
|
||||
<version>2.7.3</version>
|
||||
<type>jar</type>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.twilio.sdk</groupId>
|
||||
<artifactId>twilio-java-sdk</artifactId>
|
||||
<version>3.4.5</version>
|
||||
<version>4.4.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>postgresql</groupId>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<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>
|
||||
<version>9.4-1201-jdbc41</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
<artifactId>websocket-resources</artifactId>
|
||||
<version>0.2.1</version>
|
||||
<version>0.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
<artifactId>dropwizard-simpleauth</artifactId>
|
||||
<version>0.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
|
||||
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
|
||||
<version>2.19</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.api.version}</version>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.4.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>${commons-codec.version}</version>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
<version>4.4.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/main/java/ OutgoingMessageSignal.proto PubSubMessage.proto
|
||||
protoc --java_out=../src/main/java/ TextSecure.proto PubSubMessage.proto
|
||||
|
||||
@@ -26,6 +26,7 @@ message PubSubMessage {
|
||||
DELIVER = 2;
|
||||
KEEPALIVE = 3;
|
||||
CLOSE = 4;
|
||||
CONNECTED = 5;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
|
||||
@@ -19,23 +19,22 @@ package textsecure;
|
||||
option java_package = "org.whispersystems.textsecuregcm.entities";
|
||||
option java_outer_classname = "MessageProtos";
|
||||
|
||||
message OutgoingMessageSignal {
|
||||
message Envelope {
|
||||
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;
|
||||
optional string relay = 3;
|
||||
// repeated string destinations = 4;
|
||||
optional uint64 timestamp = 5;
|
||||
optional bytes message = 6;
|
||||
optional Type type = 1;
|
||||
optional string source = 2;
|
||||
optional uint32 sourceDevice = 7;
|
||||
optional string relay = 3;
|
||||
optional uint64 timestamp = 5;
|
||||
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
optional bytes content = 8; // Contains an encrypted Content
|
||||
}
|
||||
|
||||
message ProvisioningUuid {
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.dispatch;
|
||||
|
||||
public interface DispatchChannel {
|
||||
public void onDispatchMessage(String channel, byte[] message);
|
||||
public void onDispatchSubscribed(String channel);
|
||||
public void onDispatchUnsubscribed(String channel);
|
||||
}
|
||||
172
src/main/java/org/whispersystems/dispatch/DispatchManager.java
Normal file
172
src/main/java/org/whispersystems/dispatch/DispatchManager.java
Normal file
@@ -0,0 +1,172 @@
|
||||
package org.whispersystems.dispatch;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
|
||||
import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
import org.whispersystems.dispatch.redis.PubSubReply;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class DispatchManager extends Thread {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DispatchManager.class);
|
||||
private final Executor executor = Executors.newCachedThreadPool();
|
||||
private final Map<String, DispatchChannel> subscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
private final Optional<DispatchChannel> deadLetterChannel;
|
||||
private final RedisPubSubConnectionFactory redisPubSubConnectionFactory;
|
||||
|
||||
private PubSubConnection pubSubConnection;
|
||||
private volatile boolean running;
|
||||
|
||||
public DispatchManager(RedisPubSubConnectionFactory redisPubSubConnectionFactory,
|
||||
Optional<DispatchChannel> deadLetterChannel)
|
||||
{
|
||||
this.redisPubSubConnectionFactory = redisPubSubConnectionFactory;
|
||||
this.deadLetterChannel = deadLetterChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.pubSubConnection = redisPubSubConnectionFactory.connect();
|
||||
this.running = true;
|
||||
super.start();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.running = false;
|
||||
this.pubSubConnection.close();
|
||||
}
|
||||
|
||||
public synchronized void subscribe(String name, DispatchChannel dispatchChannel) {
|
||||
Optional<DispatchChannel> previous = Optional.fromNullable(subscriptions.get(name));
|
||||
subscriptions.put(name, dispatchChannel);
|
||||
|
||||
try {
|
||||
pubSubConnection.subscribe(name);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Subscription error", e);
|
||||
}
|
||||
|
||||
if (previous.isPresent()) {
|
||||
dispatchUnsubscription(name, previous.get());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void unsubscribe(String name, DispatchChannel channel) {
|
||||
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(name));
|
||||
|
||||
if (subscription.isPresent() && subscription.get() == channel) {
|
||||
subscriptions.remove(name);
|
||||
|
||||
try {
|
||||
pubSubConnection.unsubscribe(name);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Unsubscribe error", e);
|
||||
}
|
||||
|
||||
dispatchUnsubscription(name, subscription.get());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSubscription(String name) {
|
||||
return subscriptions.containsKey(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (running) {
|
||||
try {
|
||||
PubSubReply reply = pubSubConnection.read();
|
||||
|
||||
switch (reply.getType()) {
|
||||
case UNSUBSCRIBE: break;
|
||||
case SUBSCRIBE: dispatchSubscribe(reply); break;
|
||||
case MESSAGE: dispatchMessage(reply); break;
|
||||
default: throw new AssertionError("Unknown pubsub reply type! " + reply.getType());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("***** PubSub Connection Error *****", e);
|
||||
if (running) {
|
||||
this.pubSubConnection.close();
|
||||
this.pubSubConnection = redisPubSubConnectionFactory.connect();
|
||||
resubscribeAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn("DispatchManager Shutting Down...");
|
||||
}
|
||||
|
||||
private void dispatchSubscribe(final PubSubReply reply) {
|
||||
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
|
||||
|
||||
if (subscription.isPresent()) {
|
||||
dispatchSubscription(reply.getChannel(), subscription.get());
|
||||
} else {
|
||||
logger.info("Received subscribe event for non-existing channel: " + reply.getChannel());
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchMessage(PubSubReply reply) {
|
||||
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
|
||||
|
||||
if (subscription.isPresent()) {
|
||||
dispatchMessage(reply.getChannel(), subscription.get(), reply.getContent().get());
|
||||
} else if (deadLetterChannel.isPresent()) {
|
||||
dispatchMessage(reply.getChannel(), deadLetterChannel.get(), reply.getContent().get());
|
||||
} else {
|
||||
logger.warn("Received message for non-existing channel, with no dead letter handler: " + reply.getChannel());
|
||||
}
|
||||
}
|
||||
|
||||
private void resubscribeAll() {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (DispatchManager.this) {
|
||||
try {
|
||||
for (String name : subscriptions.keySet()) {
|
||||
pubSubConnection.subscribe(name);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void dispatchMessage(final String name, final DispatchChannel channel, final byte[] message) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
channel.onDispatchMessage(name, message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchSubscription(final String name, final DispatchChannel channel) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
channel.onDispatchSubscribed(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchUnsubscription(final String name, final DispatchChannel channel) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
channel.onDispatchUnsubscribed(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.whispersystems.dispatch.io;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class RedisInputStream {
|
||||
|
||||
private static final byte CR = 0x0D;
|
||||
private static final byte LF = 0x0A;
|
||||
|
||||
private final InputStream inputStream;
|
||||
|
||||
public RedisInputStream(InputStream inputStream) {
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
public String readLine() throws IOException {
|
||||
ByteArrayOutputStream boas = new ByteArrayOutputStream();
|
||||
|
||||
boolean foundCr = false;
|
||||
|
||||
while (true) {
|
||||
int character = inputStream.read();
|
||||
|
||||
if (character == -1) {
|
||||
throw new IOException("Stream closed!");
|
||||
}
|
||||
|
||||
boas.write(character);
|
||||
|
||||
if (foundCr && character == LF) break;
|
||||
else if (character == CR) foundCr = true;
|
||||
else if (foundCr) foundCr = false;
|
||||
}
|
||||
|
||||
byte[] data = boas.toByteArray();
|
||||
return new String(data, 0, data.length-2);
|
||||
}
|
||||
|
||||
public byte[] readFully(int size) throws IOException {
|
||||
byte[] result = new byte[size];
|
||||
int offset = 0;
|
||||
int remaining = result.length;
|
||||
|
||||
while (remaining > 0) {
|
||||
int read = inputStream.read(result, offset, remaining);
|
||||
|
||||
if (read < 0) {
|
||||
throw new IOException("Stream closed!");
|
||||
}
|
||||
|
||||
offset += read;
|
||||
remaining -= read;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.whispersystems.dispatch.io;
|
||||
|
||||
import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
|
||||
public interface RedisPubSubConnectionFactory {
|
||||
|
||||
public PubSubConnection connect();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.io.RedisInputStream;
|
||||
import org.whispersystems.dispatch.redis.protocol.ArrayReplyHeader;
|
||||
import org.whispersystems.dispatch.redis.protocol.IntReply;
|
||||
import org.whispersystems.dispatch.redis.protocol.StringReplyHeader;
|
||||
import org.whispersystems.dispatch.util.Util;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class PubSubConnection {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PubSubConnection.class);
|
||||
|
||||
private static final byte[] UNSUBSCRIBE_TYPE = {'u', 'n', 's', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
|
||||
private static final byte[] SUBSCRIBE_TYPE = {'s', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
|
||||
private static final byte[] MESSAGE_TYPE = {'m', 'e', 's', 's', 'a', 'g', 'e' };
|
||||
|
||||
private static final byte[] SUBSCRIBE_COMMAND = {'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' ' };
|
||||
private static final byte[] UNSUBSCRIBE_COMMAND = {'U', 'N', 'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' '};
|
||||
private static final byte[] CRLF = {'\r', '\n' };
|
||||
|
||||
private final OutputStream outputStream;
|
||||
private final RedisInputStream inputStream;
|
||||
private final Socket socket;
|
||||
private final AtomicBoolean closed;
|
||||
|
||||
public PubSubConnection(Socket socket) throws IOException {
|
||||
this.socket = socket;
|
||||
this.outputStream = socket.getOutputStream();
|
||||
this.inputStream = new RedisInputStream(new BufferedInputStream(socket.getInputStream()));
|
||||
this.closed = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
public void subscribe(String channelName) throws IOException {
|
||||
if (closed.get()) throw new IOException("Connection closed!");
|
||||
|
||||
byte[] command = Util.combine(SUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
|
||||
outputStream.write(command);
|
||||
}
|
||||
|
||||
public void unsubscribe(String channelName) throws IOException {
|
||||
if (closed.get()) throw new IOException("Connection closed!");
|
||||
|
||||
byte[] command = Util.combine(UNSUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
|
||||
outputStream.write(command);
|
||||
}
|
||||
|
||||
public PubSubReply read() throws IOException {
|
||||
if (closed.get()) throw new IOException("Connection closed!");
|
||||
|
||||
ArrayReplyHeader replyHeader = new ArrayReplyHeader(inputStream.readLine());
|
||||
|
||||
if (replyHeader.getElementCount() != 3) {
|
||||
throw new IOException("Received array reply header with strange count: " + replyHeader.getElementCount());
|
||||
}
|
||||
|
||||
StringReplyHeader replyTypeHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] replyType = inputStream.readFully(replyTypeHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
if (Arrays.equals(SUBSCRIBE_TYPE, replyType)) return readSubscribeReply();
|
||||
else if (Arrays.equals(UNSUBSCRIBE_TYPE, replyType)) return readUnsubscribeReply();
|
||||
else if (Arrays.equals(MESSAGE_TYPE, replyType)) return readMessageReply();
|
||||
else throw new IOException("Unknown reply type: " + new String(replyType));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
this.closed.set(true);
|
||||
this.inputStream.close();
|
||||
this.outputStream.close();
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Exception while closing", e);
|
||||
}
|
||||
}
|
||||
|
||||
private PubSubReply readMessageReply() throws IOException {
|
||||
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
StringReplyHeader messageHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] message = inputStream.readFully(messageHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
return new PubSubReply(PubSubReply.Type.MESSAGE, new String(channelName), Optional.of(message));
|
||||
}
|
||||
|
||||
private PubSubReply readUnsubscribeReply() throws IOException {
|
||||
String channelName = readSubscriptionReply();
|
||||
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.<byte[]>absent());
|
||||
}
|
||||
|
||||
private PubSubReply readSubscribeReply() throws IOException {
|
||||
String channelName = readSubscriptionReply();
|
||||
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.<byte[]>absent());
|
||||
}
|
||||
|
||||
private String readSubscriptionReply() throws IOException {
|
||||
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
IntReply subscriptionCount = new IntReply(inputStream.readLine());
|
||||
|
||||
return new String(channelName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
public class PubSubReply {
|
||||
|
||||
public enum Type {
|
||||
MESSAGE,
|
||||
SUBSCRIBE,
|
||||
UNSUBSCRIBE
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final String channel;
|
||||
private final Optional<byte[]> content;
|
||||
|
||||
public PubSubReply(Type type, String channel, Optional<byte[]> content) {
|
||||
this.type = type;
|
||||
this.channel = channel;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ArrayReplyHeader {
|
||||
|
||||
private final int elementCount;
|
||||
|
||||
public ArrayReplyHeader(String header) throws IOException {
|
||||
if (header == null || header.length() < 2 || header.charAt(0) != '*') {
|
||||
throw new IOException("Invalid array reply header: " + header);
|
||||
}
|
||||
|
||||
try {
|
||||
this.elementCount = Integer.parseInt(header.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getElementCount() {
|
||||
return elementCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IntReply {
|
||||
|
||||
private final int value;
|
||||
|
||||
public IntReply(String reply) throws IOException {
|
||||
if (reply == null || reply.length() < 2 || reply.charAt(0) != ':') {
|
||||
throw new IOException("Invalid int reply: " + reply);
|
||||
}
|
||||
|
||||
try {
|
||||
this.value = Integer.parseInt(reply.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class StringReplyHeader {
|
||||
|
||||
private final int stringLength;
|
||||
|
||||
public StringReplyHeader(String header) throws IOException {
|
||||
if (header == null || header.length() < 2 || header.charAt(0) != '$') {
|
||||
throw new IOException("Invalid string reply header: " + header);
|
||||
}
|
||||
|
||||
try {
|
||||
this.stringLength = Integer.parseInt(header.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getStringLength() {
|
||||
return stringLength;
|
||||
}
|
||||
}
|
||||
36
src/main/java/org/whispersystems/dispatch/util/Util.java
Normal file
36
src/main/java/org/whispersystems/dispatch/util/Util.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package org.whispersystems.dispatch.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Util {
|
||||
|
||||
public static byte[] combine(byte[]... elements) {
|
||||
try {
|
||||
int sum = 0;
|
||||
|
||||
for (byte[] element : elements) {
|
||||
sum += element.length;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(sum);
|
||||
|
||||
for (byte[] element : elements) {
|
||||
baos.write(element);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,17 +19,21 @@ package org.whispersystems.textsecuregcm;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
|
||||
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.TestDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.dropwizard.Configuration;
|
||||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
@@ -42,9 +46,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private TwilioConfiguration twilio;
|
||||
|
||||
@JsonProperty
|
||||
private NexmoConfiguration nexmo;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -70,6 +71,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private DataSourceFactory messageStore;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -110,10 +115,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return twilio;
|
||||
}
|
||||
|
||||
public NexmoConfiguration getNexmoConfiguration() {
|
||||
return nexmo;
|
||||
}
|
||||
|
||||
public PushConfiguration getPushConfiguration() {
|
||||
return push;
|
||||
}
|
||||
@@ -157,4 +158,15 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
public RedPhoneConfiguration getRedphoneConfiguration() {
|
||||
return redphone;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getTestDevices() {
|
||||
Map<String, Integer> results = new HashMap<>();
|
||||
|
||||
for (TestDeviceConfiguration testDeviceConfiguration : testDevices) {
|
||||
results.put(testDeviceConfiguration.getNumber(),
|
||||
testDeviceConfiguration.getCode());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,21 @@ package org.whispersystems.textsecuregcm;
|
||||
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.graphite.GraphiteReporter;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.google.common.base.Optional;
|
||||
import com.sun.jersey.api.client.Client;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.glassfish.jersey.client.ClientProperties;
|
||||
import org.skife.jdbi.v2.DBI;
|
||||
import org.whispersystems.dispatch.DispatchChannel;
|
||||
import org.whispersystems.dispatch.DispatchManager;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
|
||||
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
@@ -44,25 +49,29 @@ 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.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
import org.whispersystems.textsecuregcm.push.FeedbackHandler;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.PushServiceClient;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.Messages;
|
||||
@@ -75,9 +84,11 @@ import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.TrimMessagesCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
|
||||
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
@@ -85,6 +96,7 @@ import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.FilterRegistration;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import javax.ws.rs.client.Client;
|
||||
import java.security.Security;
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -109,6 +121,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||
bootstrap.addCommand(new DirectoryCommand());
|
||||
bootstrap.addCommand(new VacuumCommand());
|
||||
bootstrap.addCommand(new TrimMessagesCommand());
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
@@ -135,6 +148,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
{
|
||||
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
|
||||
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
DBIFactory dbiFactory = new DBIFactory();
|
||||
DBI database = dbiFactory.build(environment, config.getDataSourceFactory(), "accountdb");
|
||||
@@ -146,49 +161,61 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
Keys keys = database.onDemand(Keys.class);
|
||||
Messages messages = messagedb.onDemand(Messages.class);
|
||||
|
||||
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());
|
||||
RedisClientFactory cacheClientFactory = new RedisClientFactory(config.getCacheConfiguration().getUrl());
|
||||
JedisPool cacheClient = cacheClientFactory.getRedisClientPool();
|
||||
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
||||
Client httpClient = initializeHttpClient(environment, config);
|
||||
|
||||
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());
|
||||
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);
|
||||
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(environment, config.getJerseyClientConfiguration(), config.getFederationConfiguration());
|
||||
MessagesManager messagesManager = new MessagesManager(messages);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
|
||||
DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.<DispatchChannel>of(deadLetterHandler));
|
||||
PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager);
|
||||
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
|
||||
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
|
||||
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager );
|
||||
FederatedPeerAuthenticator federatedPeerAuthenticator = new FederatedPeerAuthenticator(config.getFederationConfiguration());
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient, pubSubManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||
PushSender pushSender = new PushSender(pushServiceClient, websocketSender);
|
||||
PushSender pushSender = new PushSender(apnFallbackManager, pushServiceClient, websocketSender, config.getPushConfiguration().getQueueSize());
|
||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
||||
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
|
||||
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
|
||||
|
||||
environment.lifecycle().manage(apnFallbackManager);
|
||||
environment.lifecycle().manage(pubSubManager);
|
||||
environment.lifecycle().manage(feedbackHandler);
|
||||
environment.lifecycle().manage(pushSender);
|
||||
|
||||
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
||||
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
|
||||
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager);
|
||||
|
||||
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||
FederatedPeer.class,
|
||||
deviceAuthenticator,
|
||||
Device.class, "WhisperServer"));
|
||||
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
|
||||
.setAuthenticator(deviceAuthenticator)
|
||||
.setPrincipal(Account.class)
|
||||
.buildAuthFilter(),
|
||||
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
|
||||
.setAuthenticator(federatedPeerAuthenticator)
|
||||
.setPrincipal(FederatedPeer.class)
|
||||
.buildAuthFilter()));
|
||||
environment.jersey().register(new AuthValueFactoryProvider.Binder());
|
||||
|
||||
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 AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, config.getTestDevices()));
|
||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, 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 ReceiptController(receiptSender));
|
||||
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
|
||||
environment.jersey().register(attachmentController);
|
||||
environment.jersey().register(keysControllerV1);
|
||||
@@ -196,14 +223,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.jersey().register(messageController);
|
||||
|
||||
if (config.getWebsocketConfiguration().isEnabled()) {
|
||||
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config);
|
||||
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000);
|
||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
|
||||
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, messagesManager, pubSubManager));
|
||||
webSocketEnvironment.jersey().register(new KeepAliveController());
|
||||
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager, apnFallbackManager));
|
||||
webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager));
|
||||
|
||||
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config);
|
||||
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
|
||||
provisioningEnvironment.jersey().register(new KeepAliveController());
|
||||
provisioningEnvironment.jersey().register(new KeepAliveController(pubSubManager));
|
||||
|
||||
WebSocketResourceProviderFactory webSocketServlet = new WebSocketResourceProviderFactory(webSocketEnvironment );
|
||||
WebSocketResourceProviderFactory provisioningServlet = new WebSocketResourceProviderFactory(provisioningEnvironment);
|
||||
@@ -223,7 +250,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
|
||||
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
|
||||
filter.setInitParameter("allowedOrigins", "*");
|
||||
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin");
|
||||
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,X-Signal-Agent");
|
||||
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
|
||||
filter.setInitParameter("preflightMaxAge", "5184000");
|
||||
filter.setInitParameter("allowCredentials", "true");
|
||||
@@ -234,11 +261,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
environment.jersey().register(new IOExceptionMapper());
|
||||
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
|
||||
environment.jersey().register(new DeviceLimitExceededExceptionMapper());
|
||||
|
||||
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
|
||||
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
|
||||
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
|
||||
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
|
||||
environment.metrics().register(name(FileDescriptorGauge.class, "fd_count"), new FileDescriptorGauge());
|
||||
|
||||
if (config.getGraphiteConfiguration().isEnabled()) {
|
||||
GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory();
|
||||
@@ -250,16 +280,17 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) {
|
||||
if (configuration == null) {
|
||||
return Optional.absent();
|
||||
} else {
|
||||
return Optional.of(new NexmoSmsSender(configuration));
|
||||
}
|
||||
private Client initializeHttpClient(Environment environment, WhisperServerConfiguration config) {
|
||||
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
|
||||
.build(getName());
|
||||
|
||||
httpClient.property(ClientProperties.CONNECT_TIMEOUT, 1000);
|
||||
httpClient.property(ClientProperties.READ_TIMEOUT, 1000);
|
||||
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new WhisperServerService().run(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dropwizard.simpleauth.Authenticator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
@@ -29,7 +30,6 @@ import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.AuthenticationException;
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.slf4j.Logger;
|
||||
@@ -16,61 +17,13 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AuthorizationToken {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
|
||||
@JsonProperty
|
||||
private String token;
|
||||
|
||||
private final String token;
|
||||
private final byte[] key;
|
||||
|
||||
public AuthorizationToken(String token, byte[] key) {
|
||||
public AuthorizationToken(String token) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
public AuthorizationToken() {}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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 AuthorizationTokenGenerator {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AuthorizationTokenGenerator.class);
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
public AuthorizationTokenGenerator(byte[] key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public AuthorizationToken generateFor(String number) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
long currentTimeSeconds = System.currentTimeMillis() / 1000;
|
||||
String prefix = number + ":" + currentTimeSeconds;
|
||||
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
String output = Hex.encodeHexString(Util.truncate(mac.doFinal(prefix.getBytes()), 10));
|
||||
String token = prefix + ":" + output;
|
||||
|
||||
return new AuthorizationToken(token);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isValid(String token, 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dropwizard.simpleauth.Authenticator;
|
||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
@@ -30,7 +31,6 @@ import java.util.List;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.AuthenticationException;
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
|
||||
|
||||
@@ -1,66 +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.auth;
|
||||
|
||||
import com.sun.jersey.api.model.Parameter;
|
||||
import com.sun.jersey.core.spi.component.ComponentContext;
|
||||
import com.sun.jersey.core.spi.component.ComponentScope;
|
||||
import com.sun.jersey.spi.inject.Injectable;
|
||||
import com.sun.jersey.spi.inject.InjectableProvider;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicAuthProvider;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
|
||||
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
|
||||
|
||||
private final BasicAuthProvider<T1> provider1;
|
||||
private final BasicAuthProvider<T2> provider2;
|
||||
|
||||
private final Class<?> clazz1;
|
||||
private final Class<?> clazz2;
|
||||
|
||||
public MultiBasicAuthProvider(Authenticator<BasicCredentials, T1> authenticator1,
|
||||
Class<?> clazz1,
|
||||
Authenticator<BasicCredentials, T2> authenticator2,
|
||||
Class<?> clazz2,
|
||||
String realm)
|
||||
{
|
||||
this.provider1 = new BasicAuthProvider<>(authenticator1, realm);
|
||||
this.provider2 = new BasicAuthProvider<>(authenticator2, realm);
|
||||
this.clazz1 = clazz1;
|
||||
this.clazz2 = clazz2;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ComponentScope getScope() {
|
||||
return ComponentScope.PerRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Injectable<?> getInjectable(ComponentContext componentContext,
|
||||
Auth auth, Parameter parameter)
|
||||
{
|
||||
if (parameter.getParameterClass().equals(clazz1)) {
|
||||
return this.provider1.getInjectable(componentContext, auth, parameter);
|
||||
} else {
|
||||
return this.provider2.getInjectable(componentContext, auth, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +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.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class NexmoConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private String apiKey;
|
||||
|
||||
@JsonProperty
|
||||
private String apiSecret;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public String getApiSecret() {
|
||||
return apiSecret;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ public class PushConfiguration {
|
||||
@NotEmpty
|
||||
private String password;
|
||||
|
||||
@JsonProperty
|
||||
@Min(0)
|
||||
private int queueSize = 200;
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
@@ -37,4 +41,8 @@ public class PushConfiguration {
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return queueSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class TestDeviceConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private int code;
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
public class TwilioConfiguration {
|
||||
|
||||
@NotEmpty
|
||||
@@ -29,17 +32,14 @@ public class TwilioConfiguration {
|
||||
@JsonProperty
|
||||
private String accountToken;
|
||||
|
||||
@NotEmpty
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private String number;
|
||||
private List<String> numbers;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String localDomain;
|
||||
|
||||
@JsonProperty
|
||||
private boolean international;
|
||||
|
||||
public String getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
@@ -48,15 +48,11 @@ public class TwilioConfiguration {
|
||||
return accountToken;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
public List<String> getNumbers() {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
public String getLocalDomain() {
|
||||
return localDomain;
|
||||
}
|
||||
|
||||
public boolean isInternational() {
|
||||
return international;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ 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.AuthorizationTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
@@ -50,12 +51,14 @@ import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Map;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@@ -64,13 +67,14 @@ public class AccountController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
||||
|
||||
private final PendingAccountsManager pendingAccounts;
|
||||
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;
|
||||
private final PendingAccountsManager pendingAccounts;
|
||||
private final AccountsManager accounts;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
private final MessagesManager messagesManager;
|
||||
private final TimeProvider timeProvider;
|
||||
private final Optional<AuthorizationTokenGenerator> tokenGenerator;
|
||||
private final Map<String, Integer> testDevices;
|
||||
|
||||
public AccountController(PendingAccountsManager pendingAccounts,
|
||||
AccountsManager accounts,
|
||||
@@ -78,7 +82,8 @@ public class AccountController {
|
||||
SmsSender smsSenderFactory,
|
||||
MessagesManager messagesManager,
|
||||
TimeProvider timeProvider,
|
||||
Optional<byte[]> authorizationKey)
|
||||
Optional<byte[]> authorizationKey,
|
||||
Map<String, Integer> testDevices)
|
||||
{
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
@@ -86,14 +91,21 @@ public class AccountController {
|
||||
this.smsSender = smsSenderFactory;
|
||||
this.messagesManager = messagesManager;
|
||||
this.timeProvider = timeProvider;
|
||||
this.authorizationKey = authorizationKey;
|
||||
this.testDevices = testDevices;
|
||||
|
||||
if (authorizationKey.isPresent()) {
|
||||
tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get()));
|
||||
} else {
|
||||
tokenGenerator = Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/{transport}/code/{number}")
|
||||
public Response createAccount(@PathParam("transport") String transport,
|
||||
@PathParam("number") String number)
|
||||
@PathParam("number") String number,
|
||||
@QueryParam("client") Optional<String> client)
|
||||
throws IOException, RateLimitExceededException
|
||||
{
|
||||
if (!Util.isValidNumber(number)) {
|
||||
@@ -112,11 +124,13 @@ public class AccountController {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
|
||||
VerificationCode verificationCode = generateVerificationCode();
|
||||
VerificationCode verificationCode = generateVerificationCode(number);
|
||||
pendingAccounts.store(number, verificationCode.getVerificationCode());
|
||||
|
||||
if (transport.equals("sms")) {
|
||||
smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
|
||||
if (testDevices.containsKey(number)) {
|
||||
// noop
|
||||
} else if (transport.equals("sms")) {
|
||||
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
|
||||
} else if (transport.equals("voice")) {
|
||||
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
|
||||
}
|
||||
@@ -130,6 +144,7 @@ public class AccountController {
|
||||
@Path("/code/{verification_code}")
|
||||
public void verifyAccount(@PathParam("verification_code") String verificationCode,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
@@ -152,7 +167,7 @@ public class AccountController {
|
||||
throw new WebApplicationException(Response.status(417).build());
|
||||
}
|
||||
|
||||
createAccount(number, password, accountAttributes);
|
||||
createAccount(number, password, userAgent, accountAttributes);
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad Authorization Header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
@@ -165,6 +180,7 @@ public class AccountController {
|
||||
@Path("/token/{verification_token}")
|
||||
public void verifyToken(@PathParam("verification_token") String verificationToken,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
@@ -175,24 +191,37 @@ public class AccountController {
|
||||
|
||||
rateLimiters.getVerifyLimiter().validate(number);
|
||||
|
||||
if (!authorizationKey.isPresent()) {
|
||||
if (!tokenGenerator.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())) {
|
||||
if (!tokenGenerator.get().isValid(verificationToken, number, timeProvider.getCurrentTimeMillis())) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
createAccount(number, password, accountAttributes);
|
||||
createAccount(number, password, userAgent, accountAttributes);
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad authorization header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/token/")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AuthorizationToken verifyToken(@Auth Account account)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (!tokenGenerator.isPresent()) {
|
||||
logger.debug("Attempt to authorize with key but not configured...");
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
}
|
||||
|
||||
return tokenGenerator.get().generateFor(account.getNumber());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/gcm/")
|
||||
@@ -200,6 +229,7 @@ public class AccountController {
|
||||
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setApnId(null);
|
||||
device.setVoipApnId(null);
|
||||
device.setGcmId(registrationId.getGcmRegistrationId());
|
||||
|
||||
if (registrationId.isWebSocketChannel()) device.setFetchesMessages(true);
|
||||
@@ -225,6 +255,7 @@ public class AccountController {
|
||||
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setApnId(registrationId.getApnRegistrationId());
|
||||
device.setVoipApnId(registrationId.getVoipRegistrationId());
|
||||
device.setGcmId(null);
|
||||
device.setFetchesMessages(true);
|
||||
accounts.update(account);
|
||||
@@ -242,19 +273,22 @@ public class AccountController {
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/wsc/")
|
||||
public void setWebSocketChannelSupported(@Auth Account account) {
|
||||
@Path("/attributes/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setAccountAttributes(@Auth Account account,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@Valid AccountAttributes attributes)
|
||||
{
|
||||
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);
|
||||
device.setFetchesMessages(attributes.getFetchesMessages());
|
||||
device.setName(attributes.getName());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setVoiceSupported(attributes.getVoice());
|
||||
device.setRegistrationId(attributes.getRegistrationId());
|
||||
device.setSignalingKey(attributes.getSignalingKey());
|
||||
device.setUserAgent(userAgent);
|
||||
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@@ -267,28 +301,34 @@ public class AccountController {
|
||||
encodedVerificationText)).build();
|
||||
}
|
||||
|
||||
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
|
||||
private void createAccount(String number, String password, String userAgent, 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());
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setVoiceSupported(accountAttributes.getVoice());
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setUserAgent(userAgent);
|
||||
|
||||
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() {
|
||||
@VisibleForTesting protected VerificationCode generateVerificationCode(String number) {
|
||||
try {
|
||||
if (testDevices.containsKey(number)) {
|
||||
return new VerificationCode(testDevices.get(number));
|
||||
}
|
||||
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
int randomInt = 100000 + random.nextInt(900000);
|
||||
return new VerificationCode(randomInt);
|
||||
|
||||
@@ -25,17 +25,21 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.PUT;
|
||||
@@ -48,6 +52,8 @@ import javax.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@@ -56,28 +62,68 @@ public class DeviceController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
|
||||
|
||||
private static final int MAX_DEVICES = 3;
|
||||
|
||||
private final PendingDevicesManager pendingDevices;
|
||||
private final AccountsManager accounts;
|
||||
private final MessagesManager messages;
|
||||
private final RateLimiters rateLimiters;
|
||||
|
||||
public DeviceController(PendingDevicesManager pendingDevices,
|
||||
AccountsManager accounts,
|
||||
MessagesManager messages,
|
||||
RateLimiters rateLimiters)
|
||||
{
|
||||
this.pendingDevices = pendingDevices;
|
||||
this.accounts = accounts;
|
||||
this.messages = messages;
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public DeviceInfoList getDevices(@Auth Account account) {
|
||||
List<DeviceInfo> devices = new LinkedList<>();
|
||||
|
||||
for (Device device : account.getDevices()) {
|
||||
devices.add(new DeviceInfo(device.getId(), device.getName(),
|
||||
device.getLastSeen(), device.getCreated()));
|
||||
}
|
||||
|
||||
return new DeviceInfoList(devices);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/{device_id}")
|
||||
public void removeDevice(@Auth Account account, @PathParam("device_id") long deviceId) {
|
||||
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
account.removeDevice(deviceId);
|
||||
accounts.update(account);
|
||||
messages.clear(account.getNumber(), deviceId);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/provisioning/code")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public VerificationCode createDeviceToken(@Auth Account account)
|
||||
throws RateLimitExceededException
|
||||
throws RateLimitExceededException, DeviceLimitExceededException
|
||||
{
|
||||
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
|
||||
|
||||
if (account.getActiveDeviceCount() >= MAX_DEVICES) {
|
||||
throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES);
|
||||
}
|
||||
|
||||
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
VerificationCode verificationCode = generateVerificationCode();
|
||||
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
|
||||
|
||||
@@ -92,7 +138,7 @@ public class DeviceController {
|
||||
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
throws RateLimitExceededException, DeviceLimitExceededException
|
||||
{
|
||||
try {
|
||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||
@@ -115,13 +161,19 @@ public class DeviceController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
if (account.get().getActiveDeviceCount() >= MAX_DEVICES) {
|
||||
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
|
||||
}
|
||||
|
||||
Device device = new Device();
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setId(account.get().getNextDeviceId());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
|
||||
account.get().addDevice(device);
|
||||
accounts.update(account.get());
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
|
||||
public class DeviceLimitExceededException extends Exception {
|
||||
|
||||
private final int currentDevices;
|
||||
private final int maxDevices;
|
||||
|
||||
public DeviceLimitExceededException(int currentDevices, int maxDevices) {
|
||||
this.currentDevices = currentDevices;
|
||||
this.maxDevices = maxDevices;
|
||||
}
|
||||
|
||||
public int getCurrentDevices() {
|
||||
return currentDevices;
|
||||
}
|
||||
|
||||
public int getMaxDevices() {
|
||||
return maxDevices;
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ public class DirectoryController {
|
||||
rateLimiters.getContactsLimiter().validate(account.getNumber());
|
||||
|
||||
try {
|
||||
Optional<ClientContact> contact = directory.get(Base64.decodeWithoutPadding(token));
|
||||
Optional<ClientContact> contact = directory.get(decodeToken(token));
|
||||
|
||||
if (contact.isPresent()) return Response.ok().entity(contact.get()).build();
|
||||
else return Response.status(404).build();
|
||||
@@ -100,7 +100,7 @@ public class DirectoryController {
|
||||
List<byte[]> tokens = new LinkedList<>();
|
||||
|
||||
for (String encodedContact : contacts.getContacts()) {
|
||||
tokens.add(Base64.decodeWithoutPadding(encodedContact));
|
||||
tokens.add(decodeToken(encodedContact));
|
||||
}
|
||||
|
||||
List<ClientContact> intersection = directory.get(tokens);
|
||||
@@ -110,4 +110,8 @@ public class DirectoryController {
|
||||
throw new WebApplicationException(Response.status(400).build());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeToken(String encoded) throws IOException {
|
||||
return Base64.decodeWithoutPadding(encoded.replace('-', '+').replace('_', '/'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ public class FederationControllerV1 extends FederationController {
|
||||
|
||||
for (Account account : accountList) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
|
||||
|
||||
if (!account.isActive()) {
|
||||
clientContact.setInactive(true);
|
||||
|
||||
@@ -1,19 +1,54 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
import org.whispersystems.websocket.session.WebSocketSession;
|
||||
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
|
||||
@Path("/v1/keepalive")
|
||||
public class KeepAliveController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(KeepAliveController.class);
|
||||
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
public KeepAliveController(PubSubManager pubSubManager) {
|
||||
this.pubSubManager = pubSubManager;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
public Response getKeepAlive() {
|
||||
public Response getKeepAlive(@Auth Account account,
|
||||
@WebSocketSession WebSocketSessionContext context)
|
||||
{
|
||||
if (account != null) {
|
||||
WebsocketAddress address = new WebsocketAddress(account.getNumber(),
|
||||
account.getAuthenticatedDevice().get().getId());
|
||||
|
||||
if (!pubSubManager.hasLocalSubscription(address)) {
|
||||
logger.warn("***** No local subscription found for: " + address);
|
||||
context.getClient().close(1000, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/provisioning")
|
||||
public Response getProvisioningKeepAlive() {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.base.Optional;
|
||||
import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
@@ -38,6 +40,8 @@ import io.dropwizard.auth.Auth;
|
||||
|
||||
public class KeysController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KeysController.class);
|
||||
|
||||
protected final RateLimiters rateLimiters;
|
||||
protected final Keys keys;
|
||||
protected final AccountsManager accounts;
|
||||
@@ -52,7 +56,6 @@ public class KeysController {
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKeyCount getStatus(@Auth Account account) {
|
||||
@@ -87,8 +90,16 @@ public class KeysController {
|
||||
throw new NoSuchUserException("Target device is inactive.");
|
||||
}
|
||||
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
for (int i=0;i<20;i++) {
|
||||
try {
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
} catch (UnableToExecuteStatementException e) {
|
||||
logger.info(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
throw new WebApplicationException(Response.status(500).build());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClient;
|
||||
@@ -34,16 +35,19 @@ import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
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.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@@ -66,17 +70,23 @@ public class MessageController {
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final PushSender pushSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
|
||||
public MessageController(RateLimiters rateLimiters,
|
||||
PushSender pushSender,
|
||||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
MessagesManager messagesManager,
|
||||
FederatedClientManager federatedClientManager)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.pushSender = pushSender;
|
||||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
}
|
||||
|
||||
@@ -90,7 +100,7 @@ public class MessageController {
|
||||
@Valid IncomingMessageList messages)
|
||||
throws IOException, RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||
rateLimiters.getMessagesLimiter().validate(source.getNumber() + "__" + destinationName);
|
||||
|
||||
try {
|
||||
boolean isSyncMessage = source.getNumber().equals(destinationName);
|
||||
@@ -118,30 +128,45 @@ public class MessageController {
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public MessageResponse sendMessageLegacy(@Auth Account source, @Valid IncomingMessageList messages)
|
||||
throws IOException, RateLimitExceededException
|
||||
public OutgoingMessageEntityList getPendingMessages(@Auth Account account) {
|
||||
return messagesManager.getMessagesForDevice(account.getNumber(),
|
||||
account.getAuthenticatedDevice().get().getId());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/{source}/{timestamp}")
|
||||
public void removePendingMessage(@Auth Account account,
|
||||
@PathParam("source") String source,
|
||||
@PathParam("timestamp") long timestamp)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
List<IncomingMessage> incomingMessages = messages.getMessages();
|
||||
validateLegacyDestinations(incomingMessages);
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
source, timestamp);
|
||||
|
||||
messages.setRelay(incomingMessages.get(0).getRelay());
|
||||
sendMessage(source, incomingMessages.get(0).getDestination(), messages);
|
||||
|
||||
return new MessageResponse(new LinkedList<String>(), new LinkedList<String>());
|
||||
} catch (ValidationException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
if (message.isPresent() && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(account,
|
||||
message.get().getSource(),
|
||||
message.get().getTimestamp(),
|
||||
Optional.fromNullable(message.get().getRelay()));
|
||||
}
|
||||
} catch (NotPushRegisteredException e) {
|
||||
logger.info("User no longer push registered for delivery receipt: " + e.getMessage());
|
||||
} catch (NoSuchUserException | TransientPushFailureException e) {
|
||||
logger.warn("Sending delivery receipt", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void sendLocalMessage(Account source,
|
||||
String destinationName,
|
||||
IncomingMessageList messages,
|
||||
boolean isSyncMessage)
|
||||
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
|
||||
throws NoSuchUserException, MismatchedDevicesException, StaleDevicesException
|
||||
{
|
||||
Account destination;
|
||||
|
||||
@@ -165,19 +190,24 @@ public class MessageController {
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
IncomingMessage incomingMessage)
|
||||
throws NoSuchUserException, IOException
|
||||
throws NoSuchUserException
|
||||
{
|
||||
try {
|
||||
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
|
||||
OutgoingMessageSignal.Builder messageBuilder = OutgoingMessageSignal.newBuilder();
|
||||
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
|
||||
Optional<byte[]> messageContent = getMessageContent(incomingMessage);
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
|
||||
messageBuilder.setType(incomingMessage.getType())
|
||||
messageBuilder.setType(Envelope.Type.valueOf(incomingMessage.getType()))
|
||||
.setSource(source.getNumber())
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
|
||||
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId());
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
messageBuilder.setMessage(ByteString.copyFrom(messageBody.get()));
|
||||
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
|
||||
}
|
||||
|
||||
if (messageContent.isPresent()) {
|
||||
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
|
||||
}
|
||||
|
||||
if (source.getRelay().isPresent()) {
|
||||
@@ -188,9 +218,6 @@ public class MessageController {
|
||||
} catch (NotPushRegisteredException e) {
|
||||
if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
|
||||
else logger.debug("Not registered", e);
|
||||
} catch (TransientPushFailureException e) {
|
||||
if (destinationDevice.isMaster()) throw new IOException(e);
|
||||
else logger.debug("Transient failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,23 +309,9 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateLegacyDestinations(List<IncomingMessage> messages)
|
||||
throws ValidationException
|
||||
{
|
||||
String destination = null;
|
||||
|
||||
for (IncomingMessage message : messages) {
|
||||
if ((message.getDestination() == null) ||
|
||||
(destination != null && !destination.equals(message.getDestination())))
|
||||
{
|
||||
throw new ValidationException("Multiple account destinations!");
|
||||
}
|
||||
|
||||
destination = message.getDestination();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<byte[]> getMessageBody(IncomingMessage message) {
|
||||
if (Util.isEmpty(message.getBody())) return Optional.absent();
|
||||
|
||||
try {
|
||||
return Optional.of(Base64.decode(message.getBody()));
|
||||
} catch (IOException ioe) {
|
||||
@@ -306,4 +319,15 @@ public class MessageController {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<byte[]> getMessageContent(IncomingMessage message) {
|
||||
if (Util.isEmpty(message.getContent())) return Optional.absent();
|
||||
|
||||
try {
|
||||
return Optional.of(Base64.decode(message.getContent()));
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Bad B64", ioe);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
@@ -37,6 +38,7 @@ public class ProvisioningController {
|
||||
@Path("/{destination}")
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void sendProvisioningMessage(@Auth Account source,
|
||||
@PathParam("destination") String destinationName,
|
||||
@Valid ProvisioningMessage message)
|
||||
@@ -44,7 +46,7 @@ public class ProvisioningController {
|
||||
{
|
||||
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||
|
||||
if (!websocketSender.sendProvisioningMessage(new ProvisioningAddress(destinationName),
|
||||
if (!websocketSender.sendProvisioningMessage(new ProvisioningAddress(destinationName, 0),
|
||||
Base64.decode(message.getBody())))
|
||||
{
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
|
||||
@@ -2,14 +2,10 @@ 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.ReceiptSender;
|
||||
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;
|
||||
@@ -18,25 +14,16 @@ 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;
|
||||
private final ReceiptSender receiptSender;
|
||||
|
||||
public ReceiptController(AccountsManager accountManager,
|
||||
FederatedClientManager federatedClientManager,
|
||||
PushSender pushSender)
|
||||
{
|
||||
this.accountManager = accountManager;
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
this.pushSender = pushSender;
|
||||
public ReceiptController(ReceiptSender receiptSender) {
|
||||
this.receiptSender = receiptSender;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -49,8 +36,7 @@ public class ReceiptController {
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
|
||||
else sendDirectReceipt(source, destination, messageId);
|
||||
receiptSender.sendReceipt(source, destination, messageId, relay);
|
||||
} catch (NoSuchUserException | NotPushRegisteredException e) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
} catch (TransientPushFailureException e) {
|
||||
@@ -58,51 +44,4 @@ public class ReceiptController {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class AccountAttributes {
|
||||
@@ -25,32 +27,39 @@ public class AccountAttributes {
|
||||
@NotEmpty
|
||||
private String signalingKey;
|
||||
|
||||
@JsonProperty
|
||||
private boolean supportsSms;
|
||||
|
||||
@JsonProperty
|
||||
private boolean fetchesMessages;
|
||||
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
@JsonProperty
|
||||
@Length(max = 50, message = "This field must be less than 50 characters")
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private boolean voice;
|
||||
|
||||
public AccountAttributes() {}
|
||||
|
||||
public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages, int registrationId) {
|
||||
@VisibleForTesting
|
||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) {
|
||||
this(signalingKey, fetchesMessages, registrationId, null, false);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.supportsSms = supportsSms;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.name = name;
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
public String getSignalingKey() {
|
||||
return signalingKey;
|
||||
}
|
||||
|
||||
public boolean getSupportsSms() {
|
||||
return supportsSms;
|
||||
}
|
||||
|
||||
public boolean getFetchesMessages() {
|
||||
return fetchesMessages;
|
||||
}
|
||||
@@ -58,4 +67,13 @@ public class AccountAttributes {
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean getVoice() {
|
||||
return voice;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,12 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class ApnMessage {
|
||||
|
||||
public static long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String apnId;
|
||||
@@ -23,12 +27,50 @@ public class ApnMessage {
|
||||
@NotEmpty
|
||||
private String message;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private boolean voip;
|
||||
|
||||
@JsonProperty
|
||||
private long expirationTime;
|
||||
|
||||
public ApnMessage() {}
|
||||
|
||||
public ApnMessage(String apnId, String number, int deviceId, String message) {
|
||||
this.apnId = apnId;
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.message = message;
|
||||
public ApnMessage(String apnId, String number, int deviceId, String message, boolean voip, long expirationTime) {
|
||||
this.apnId = apnId;
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.message = message;
|
||||
this.voip = voip;
|
||||
this.expirationTime = expirationTime;
|
||||
}
|
||||
|
||||
public ApnMessage(ApnMessage copy, String apnId, boolean voip, long expirationTime) {
|
||||
this.apnId = apnId;
|
||||
this.number = copy.number;
|
||||
this.deviceId = copy.deviceId;
|
||||
this.message = copy.message;
|
||||
this.voip = voip;
|
||||
this.expirationTime = expirationTime;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getApnId() {
|
||||
return apnId;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean isVoip() {
|
||||
return voip;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public long getExpirationTime() {
|
||||
return expirationTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,14 @@ public class ApnRegistrationId {
|
||||
@NotEmpty
|
||||
private String apnRegistrationId;
|
||||
|
||||
@JsonProperty
|
||||
private String voipRegistrationId;
|
||||
|
||||
public String getApnRegistrationId() {
|
||||
return apnRegistrationId;
|
||||
}
|
||||
|
||||
public String getVoipRegistrationId() {
|
||||
return voipRegistrationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,16 @@ public class ClientContact {
|
||||
@JsonProperty
|
||||
private byte[] token;
|
||||
|
||||
@JsonProperty
|
||||
private boolean voice;
|
||||
|
||||
private String relay;
|
||||
private boolean inactive;
|
||||
private boolean supportsSms;
|
||||
|
||||
public ClientContact(byte[] token, String relay, boolean supportsSms) {
|
||||
this.token = token;
|
||||
this.relay = relay;
|
||||
this.supportsSms = supportsSms;
|
||||
public ClientContact(byte[] token, String relay, boolean voice) {
|
||||
this.token = token;
|
||||
this.relay = relay;
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
public ClientContact() {}
|
||||
@@ -56,10 +58,6 @@ public class ClientContact {
|
||||
this.relay = relay;
|
||||
}
|
||||
|
||||
public boolean isSupportsSms() {
|
||||
return supportsSms;
|
||||
}
|
||||
|
||||
public boolean isInactive() {
|
||||
return inactive;
|
||||
}
|
||||
@@ -68,9 +66,13 @@ public class ClientContact {
|
||||
this.inactive = inactive;
|
||||
}
|
||||
|
||||
// public String toString() {
|
||||
// return new Gson().toJson(this);
|
||||
// }
|
||||
public boolean isVoice() {
|
||||
return voice;
|
||||
}
|
||||
|
||||
public void setVoice(boolean voice) {
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
@@ -81,8 +83,8 @@ public class ClientContact {
|
||||
|
||||
return
|
||||
Arrays.equals(this.token, that.token) &&
|
||||
this.supportsSms == that.supportsSms &&
|
||||
this.inactive == that.inactive &&
|
||||
this.voice == that.voice &&
|
||||
(this.relay == null ? (that.relay == null) : this.relay.equals(that.relay));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class DeviceInfo {
|
||||
@JsonProperty
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private long lastSeen;
|
||||
|
||||
@JsonProperty
|
||||
private long created;
|
||||
|
||||
public DeviceInfo(long id, String name, long lastSeen, long created) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.lastSeen = lastSeen;
|
||||
this.created = created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceInfoList {
|
||||
|
||||
@JsonProperty
|
||||
private List<DeviceInfo> devices;
|
||||
|
||||
public DeviceInfoList(List<DeviceInfo> devices) {
|
||||
this.devices = devices;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@@ -44,8 +44,7 @@ public class EncryptedOutgoingMessage {
|
||||
private final byte[] serialized;
|
||||
private final String serializedAndEncoded;
|
||||
|
||||
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
|
||||
String signalingKey)
|
||||
public EncryptedOutgoingMessage(Envelope outgoingMessage, String signalingKey)
|
||||
throws CryptoEncodingException
|
||||
{
|
||||
byte[] plaintext = outgoingMessage.toByteArray();
|
||||
|
||||
@@ -34,9 +34,11 @@ public class IncomingMessage {
|
||||
private int destinationRegistrationId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String body;
|
||||
|
||||
@JsonProperty
|
||||
private String content;
|
||||
|
||||
@JsonProperty
|
||||
private String relay;
|
||||
|
||||
@@ -67,4 +69,8 @@ public class IncomingMessage {
|
||||
public int getDestinationRegistrationId() {
|
||||
return destinationRegistrationId;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: OutgoingMessageSignal.proto
|
||||
// source: TextSecure.proto
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
@@ -8,18 +8,18 @@ public final class MessageProtos {
|
||||
public static void registerAllExtensions(
|
||||
com.google.protobuf.ExtensionRegistry registry) {
|
||||
}
|
||||
public interface OutgoingMessageSignalOrBuilder
|
||||
public interface EnvelopeOrBuilder
|
||||
extends com.google.protobuf.MessageOrBuilder {
|
||||
|
||||
// optional uint32 type = 1;
|
||||
// optional .textsecure.Envelope.Type type = 1;
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
boolean hasType();
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
int getType();
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type getType();
|
||||
|
||||
// optional string source = 2;
|
||||
/**
|
||||
@@ -64,50 +64,68 @@ public final class MessageProtos {
|
||||
// optional uint64 timestamp = 5;
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasTimestamp();
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
long getTimestamp();
|
||||
|
||||
// optional bytes message = 6;
|
||||
// optional bytes legacyMessage = 6;
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasMessage();
|
||||
boolean hasLegacyMessage();
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
com.google.protobuf.ByteString getMessage();
|
||||
com.google.protobuf.ByteString getLegacyMessage();
|
||||
|
||||
// optional bytes content = 8;
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasContent();
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
com.google.protobuf.ByteString getContent();
|
||||
}
|
||||
/**
|
||||
* Protobuf type {@code textsecure.OutgoingMessageSignal}
|
||||
* Protobuf type {@code textsecure.Envelope}
|
||||
*/
|
||||
public static final class OutgoingMessageSignal extends
|
||||
public static final class Envelope extends
|
||||
com.google.protobuf.GeneratedMessage
|
||||
implements OutgoingMessageSignalOrBuilder {
|
||||
// Use OutgoingMessageSignal.newBuilder() to construct.
|
||||
private OutgoingMessageSignal(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
|
||||
implements EnvelopeOrBuilder {
|
||||
// Use Envelope.newBuilder() to construct.
|
||||
private Envelope(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
|
||||
super(builder);
|
||||
this.unknownFields = builder.getUnknownFields();
|
||||
}
|
||||
private OutgoingMessageSignal(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
|
||||
private Envelope(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
|
||||
|
||||
private static final OutgoingMessageSignal defaultInstance;
|
||||
public static OutgoingMessageSignal getDefaultInstance() {
|
||||
private static final Envelope defaultInstance;
|
||||
public static Envelope getDefaultInstance() {
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
public OutgoingMessageSignal getDefaultInstanceForType() {
|
||||
public Envelope getDefaultInstanceForType() {
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
@@ -117,7 +135,7 @@ public final class MessageProtos {
|
||||
getUnknownFields() {
|
||||
return this.unknownFields;
|
||||
}
|
||||
private OutgoingMessageSignal(
|
||||
private Envelope(
|
||||
com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
@@ -141,8 +159,14 @@ public final class MessageProtos {
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
bitField0_ |= 0x00000001;
|
||||
type_ = input.readUInt32();
|
||||
int rawValue = input.readEnum();
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type value = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.valueOf(rawValue);
|
||||
if (value == null) {
|
||||
unknownFields.mergeVarintField(1, rawValue);
|
||||
} else {
|
||||
bitField0_ |= 0x00000001;
|
||||
type_ = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 18: {
|
||||
@@ -162,7 +186,7 @@ public final class MessageProtos {
|
||||
}
|
||||
case 50: {
|
||||
bitField0_ |= 0x00000020;
|
||||
message_ = input.readBytes();
|
||||
legacyMessage_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 56: {
|
||||
@@ -170,6 +194,11 @@ public final class MessageProtos {
|
||||
sourceDevice_ = input.readUInt32();
|
||||
break;
|
||||
}
|
||||
case 66: {
|
||||
bitField0_ |= 0x00000040;
|
||||
content_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@@ -184,33 +213,33 @@ public final class MessageProtos {
|
||||
}
|
||||
public static final com.google.protobuf.Descriptors.Descriptor
|
||||
getDescriptor() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_descriptor;
|
||||
}
|
||||
|
||||
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internalGetFieldAccessorTable() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_fieldAccessorTable
|
||||
.ensureFieldAccessorsInitialized(
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class);
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.class, org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Builder.class);
|
||||
}
|
||||
|
||||
public static com.google.protobuf.Parser<OutgoingMessageSignal> PARSER =
|
||||
new com.google.protobuf.AbstractParser<OutgoingMessageSignal>() {
|
||||
public OutgoingMessageSignal parsePartialFrom(
|
||||
public static com.google.protobuf.Parser<Envelope> PARSER =
|
||||
new com.google.protobuf.AbstractParser<Envelope>() {
|
||||
public Envelope parsePartialFrom(
|
||||
com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return new OutgoingMessageSignal(input, extensionRegistry);
|
||||
return new Envelope(input, extensionRegistry);
|
||||
}
|
||||
};
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Parser<OutgoingMessageSignal> getParserForType() {
|
||||
public com.google.protobuf.Parser<Envelope> getParserForType() {
|
||||
return PARSER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protobuf enum {@code textsecure.OutgoingMessageSignal.Type}
|
||||
* Protobuf enum {@code textsecure.Envelope.Type}
|
||||
*/
|
||||
public enum Type
|
||||
implements com.google.protobuf.ProtocolMessageEnum {
|
||||
@@ -230,14 +259,10 @@ public final class MessageProtos {
|
||||
* <code>PREKEY_BUNDLE = 3;</code>
|
||||
*/
|
||||
PREKEY_BUNDLE(3, 3),
|
||||
/**
|
||||
* <code>PLAINTEXT = 4;</code>
|
||||
*/
|
||||
PLAINTEXT(4, 4),
|
||||
/**
|
||||
* <code>RECEIPT = 5;</code>
|
||||
*/
|
||||
RECEIPT(5, 5),
|
||||
RECEIPT(4, 5),
|
||||
;
|
||||
|
||||
/**
|
||||
@@ -256,10 +281,6 @@ public final class MessageProtos {
|
||||
* <code>PREKEY_BUNDLE = 3;</code>
|
||||
*/
|
||||
public static final int PREKEY_BUNDLE_VALUE = 3;
|
||||
/**
|
||||
* <code>PLAINTEXT = 4;</code>
|
||||
*/
|
||||
public static final int PLAINTEXT_VALUE = 4;
|
||||
/**
|
||||
* <code>RECEIPT = 5;</code>
|
||||
*/
|
||||
@@ -274,7 +295,6 @@ public final class MessageProtos {
|
||||
case 1: return CIPHERTEXT;
|
||||
case 2: return KEY_EXCHANGE;
|
||||
case 3: return PREKEY_BUNDLE;
|
||||
case 4: return PLAINTEXT;
|
||||
case 5: return RECEIPT;
|
||||
default: return null;
|
||||
}
|
||||
@@ -302,7 +322,7 @@ public final class MessageProtos {
|
||||
}
|
||||
public static final com.google.protobuf.Descriptors.EnumDescriptor
|
||||
getDescriptor() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDescriptor().getEnumTypes().get(0);
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.getDescriptor().getEnumTypes().get(0);
|
||||
}
|
||||
|
||||
private static final Type[] VALUES = values();
|
||||
@@ -324,23 +344,23 @@ public final class MessageProtos {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(enum_scope:textsecure.OutgoingMessageSignal.Type)
|
||||
// @@protoc_insertion_point(enum_scope:textsecure.Envelope.Type)
|
||||
}
|
||||
|
||||
private int bitField0_;
|
||||
// optional uint32 type = 1;
|
||||
// optional .textsecure.Envelope.Type type = 1;
|
||||
public static final int TYPE_FIELD_NUMBER = 1;
|
||||
private int type_;
|
||||
private org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type type_;
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
public boolean hasType() {
|
||||
return ((bitField0_ & 0x00000001) == 0x00000001);
|
||||
}
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
public int getType() {
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type getType() {
|
||||
return type_;
|
||||
}
|
||||
|
||||
@@ -451,48 +471,73 @@ public final class MessageProtos {
|
||||
private long timestamp_;
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasTimestamp() {
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
public long getTimestamp() {
|
||||
return timestamp_;
|
||||
}
|
||||
|
||||
// optional bytes message = 6;
|
||||
public static final int MESSAGE_FIELD_NUMBER = 6;
|
||||
private com.google.protobuf.ByteString message_;
|
||||
// optional bytes legacyMessage = 6;
|
||||
public static final int LEGACYMESSAGE_FIELD_NUMBER = 6;
|
||||
private com.google.protobuf.ByteString legacyMessage_;
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasMessage() {
|
||||
public boolean hasLegacyMessage() {
|
||||
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getMessage() {
|
||||
return message_;
|
||||
public com.google.protobuf.ByteString getLegacyMessage() {
|
||||
return legacyMessage_;
|
||||
}
|
||||
|
||||
// optional bytes content = 8;
|
||||
public static final int CONTENT_FIELD_NUMBER = 8;
|
||||
private com.google.protobuf.ByteString content_;
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasContent() {
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getContent() {
|
||||
return content_;
|
||||
}
|
||||
|
||||
private void initFields() {
|
||||
type_ = 0;
|
||||
type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
|
||||
source_ = "";
|
||||
sourceDevice_ = 0;
|
||||
relay_ = "";
|
||||
timestamp_ = 0L;
|
||||
message_ = com.google.protobuf.ByteString.EMPTY;
|
||||
legacyMessage_ = com.google.protobuf.ByteString.EMPTY;
|
||||
content_ = com.google.protobuf.ByteString.EMPTY;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@@ -507,7 +552,7 @@ public final class MessageProtos {
|
||||
throws java.io.IOException {
|
||||
getSerializedSize();
|
||||
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
||||
output.writeUInt32(1, type_);
|
||||
output.writeEnum(1, type_.getNumber());
|
||||
}
|
||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
||||
output.writeBytes(2, getSourceBytes());
|
||||
@@ -519,11 +564,14 @@ public final class MessageProtos {
|
||||
output.writeUInt64(5, timestamp_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
output.writeBytes(6, message_);
|
||||
output.writeBytes(6, legacyMessage_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
output.writeUInt32(7, sourceDevice_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
output.writeBytes(8, content_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@@ -535,7 +583,7 @@ public final class MessageProtos {
|
||||
size = 0;
|
||||
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(1, type_);
|
||||
.computeEnumSize(1, type_.getNumber());
|
||||
}
|
||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
@@ -551,12 +599,16 @@ public final class MessageProtos {
|
||||
}
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(6, message_);
|
||||
.computeBytesSize(6, legacyMessage_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(7, sourceDevice_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(8, content_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@@ -569,53 +621,53 @@ public final class MessageProtos {
|
||||
return super.writeReplace();
|
||||
}
|
||||
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
|
||||
com.google.protobuf.ByteString data)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope 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.entities.MessageProtos.OutgoingMessageSignal parseFrom(byte[] data)
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(byte[] data)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
|
||||
byte[] data,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data, extensionRegistry);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(java.io.InputStream input)
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(java.io.InputStream input)
|
||||
throws java.io.IOException {
|
||||
return PARSER.parseFrom(input);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
|
||||
java.io.InputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
return PARSER.parseFrom(input, extensionRegistry);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(java.io.InputStream input)
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseDelimitedFrom(java.io.InputStream input)
|
||||
throws java.io.IOException {
|
||||
return PARSER.parseDelimitedFrom(input);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseDelimitedFrom(
|
||||
java.io.InputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
return PARSER.parseDelimitedFrom(input, extensionRegistry);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
|
||||
com.google.protobuf.CodedInputStream input)
|
||||
throws java.io.IOException {
|
||||
return PARSER.parseFrom(input);
|
||||
}
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(
|
||||
public static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parseFrom(
|
||||
com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
@@ -624,7 +676,7 @@ public final class MessageProtos {
|
||||
|
||||
public static Builder newBuilder() { return Builder.create(); }
|
||||
public Builder newBuilderForType() { return newBuilder(); }
|
||||
public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal prototype) {
|
||||
public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope prototype) {
|
||||
return newBuilder().mergeFrom(prototype);
|
||||
}
|
||||
public Builder toBuilder() { return newBuilder(this); }
|
||||
@@ -636,24 +688,24 @@ public final class MessageProtos {
|
||||
return builder;
|
||||
}
|
||||
/**
|
||||
* Protobuf type {@code textsecure.OutgoingMessageSignal}
|
||||
* Protobuf type {@code textsecure.Envelope}
|
||||
*/
|
||||
public static final class Builder extends
|
||||
com.google.protobuf.GeneratedMessage.Builder<Builder>
|
||||
implements org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignalOrBuilder {
|
||||
implements org.whispersystems.textsecuregcm.entities.MessageProtos.EnvelopeOrBuilder {
|
||||
public static final com.google.protobuf.Descriptors.Descriptor
|
||||
getDescriptor() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_descriptor;
|
||||
}
|
||||
|
||||
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internalGetFieldAccessorTable() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_fieldAccessorTable
|
||||
.ensureFieldAccessorsInitialized(
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class);
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.class, org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Builder.class);
|
||||
}
|
||||
|
||||
// Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.newBuilder()
|
||||
// Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.newBuilder()
|
||||
private Builder() {
|
||||
maybeForceBuilderInitialization();
|
||||
}
|
||||
@@ -673,7 +725,7 @@ public final class MessageProtos {
|
||||
|
||||
public Builder clear() {
|
||||
super.clear();
|
||||
type_ = 0;
|
||||
type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
source_ = "";
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
@@ -683,8 +735,10 @@ public final class MessageProtos {
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
timestamp_ = 0L;
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
message_ = com.google.protobuf.ByteString.EMPTY;
|
||||
legacyMessage_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000020);
|
||||
content_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -694,23 +748,23 @@ public final class MessageProtos {
|
||||
|
||||
public com.google.protobuf.Descriptors.Descriptor
|
||||
getDescriptorForType() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor;
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_Envelope_descriptor;
|
||||
}
|
||||
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal getDefaultInstanceForType() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance();
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope getDefaultInstanceForType() {
|
||||
return org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.getDefaultInstance();
|
||||
}
|
||||
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal build() {
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial();
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope build() {
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope result = buildPartial();
|
||||
if (!result.isInitialized()) {
|
||||
throw newUninitializedMessageException(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildPartial() {
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = new org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal(this);
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope buildPartial() {
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope result = new org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope(this);
|
||||
int from_bitField0_ = bitField0_;
|
||||
int to_bitField0_ = 0;
|
||||
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
|
||||
@@ -736,23 +790,27 @@ public final class MessageProtos {
|
||||
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
to_bitField0_ |= 0x00000020;
|
||||
}
|
||||
result.message_ = message_;
|
||||
result.legacyMessage_ = legacyMessage_;
|
||||
if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
to_bitField0_ |= 0x00000040;
|
||||
}
|
||||
result.content_ = content_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
}
|
||||
|
||||
public Builder mergeFrom(com.google.protobuf.Message other) {
|
||||
if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) {
|
||||
return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal)other);
|
||||
if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope) {
|
||||
return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope)other);
|
||||
} else {
|
||||
super.mergeFrom(other);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal other) {
|
||||
if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance()) return this;
|
||||
public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope other) {
|
||||
if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.getDefaultInstance()) return this;
|
||||
if (other.hasType()) {
|
||||
setType(other.getType());
|
||||
}
|
||||
@@ -772,8 +830,11 @@ public final class MessageProtos {
|
||||
if (other.hasTimestamp()) {
|
||||
setTimestamp(other.getTimestamp());
|
||||
}
|
||||
if (other.hasMessage()) {
|
||||
setMessage(other.getMessage());
|
||||
if (other.hasLegacyMessage()) {
|
||||
setLegacyMessage(other.getLegacyMessage());
|
||||
}
|
||||
if (other.hasContent()) {
|
||||
setContent(other.getContent());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
@@ -787,11 +848,11 @@ public final class MessageProtos {
|
||||
com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parsedMessage = null;
|
||||
org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope parsedMessage = null;
|
||||
try {
|
||||
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
parsedMessage = (org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) e.getUnfinishedMessage();
|
||||
parsedMessage = (org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope) e.getUnfinishedMessage();
|
||||
throw e;
|
||||
} finally {
|
||||
if (parsedMessage != null) {
|
||||
@@ -802,35 +863,38 @@ public final class MessageProtos {
|
||||
}
|
||||
private int bitField0_;
|
||||
|
||||
// optional uint32 type = 1;
|
||||
private int type_ ;
|
||||
// optional .textsecure.Envelope.Type type = 1;
|
||||
private org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
public boolean hasType() {
|
||||
return ((bitField0_ & 0x00000001) == 0x00000001);
|
||||
}
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
public int getType() {
|
||||
public org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type getType() {
|
||||
return type_;
|
||||
}
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
public Builder setType(int value) {
|
||||
public Builder setType(org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000001;
|
||||
type_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* <code>optional uint32 type = 1;</code>
|
||||
* <code>optional .textsecure.Envelope.Type type = 1;</code>
|
||||
*/
|
||||
public Builder clearType() {
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
type_ = 0;
|
||||
type_ = org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type.UNKNOWN;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
@@ -1020,30 +1084,18 @@ public final class MessageProtos {
|
||||
private long timestamp_ ;
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasTimestamp() {
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
public long getTimestamp() {
|
||||
return timestamp_;
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setTimestamp(long value) {
|
||||
bitField0_ |= 0x00000010;
|
||||
@@ -1053,10 +1105,6 @@ public final class MessageProtos {
|
||||
}
|
||||
/**
|
||||
* <code>optional uint64 timestamp = 5;</code>
|
||||
*
|
||||
* <pre>
|
||||
* repeated string destinations = 4;
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearTimestamp() {
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
@@ -1065,51 +1113,119 @@ public final class MessageProtos {
|
||||
return this;
|
||||
}
|
||||
|
||||
// optional bytes message = 6;
|
||||
private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY;
|
||||
// optional bytes legacyMessage = 6;
|
||||
private com.google.protobuf.ByteString legacyMessage_ = com.google.protobuf.ByteString.EMPTY;
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasMessage() {
|
||||
public boolean hasLegacyMessage() {
|
||||
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getMessage() {
|
||||
return message_;
|
||||
public com.google.protobuf.ByteString getLegacyMessage() {
|
||||
return legacyMessage_;
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setMessage(com.google.protobuf.ByteString value) {
|
||||
public Builder setLegacyMessage(com.google.protobuf.ByteString value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000020;
|
||||
message_ = value;
|
||||
legacyMessage_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes message = 6;</code>
|
||||
* <code>optional bytes legacyMessage = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted DataMessage XXX -- Remove after 10/01/15
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearMessage() {
|
||||
public Builder clearLegacyMessage() {
|
||||
bitField0_ = (bitField0_ & ~0x00000020);
|
||||
message_ = getDefaultInstance().getMessage();
|
||||
legacyMessage_ = getDefaultInstance().getLegacyMessage();
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:textsecure.OutgoingMessageSignal)
|
||||
// optional bytes content = 8;
|
||||
private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasContent() {
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getContent() {
|
||||
return content_;
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setContent(com.google.protobuf.ByteString value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000040;
|
||||
content_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* <code>optional bytes content = 8;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Contains an encrypted Content
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearContent() {
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
content_ = getDefaultInstance().getContent();
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:textsecure.Envelope)
|
||||
}
|
||||
|
||||
static {
|
||||
defaultInstance = new OutgoingMessageSignal(true);
|
||||
defaultInstance = new Envelope(true);
|
||||
defaultInstance.initFields();
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(class_scope:textsecure.OutgoingMessageSignal)
|
||||
// @@protoc_insertion_point(class_scope:textsecure.Envelope)
|
||||
}
|
||||
|
||||
public interface ProvisioningUuidOrBuilder
|
||||
@@ -1584,10 +1700,10 @@ public final class MessageProtos {
|
||||
}
|
||||
|
||||
private static com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_textsecure_OutgoingMessageSignal_descriptor;
|
||||
internal_static_textsecure_Envelope_descriptor;
|
||||
private static
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
|
||||
internal_static_textsecure_Envelope_fieldAccessorTable;
|
||||
private static com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_textsecure_ProvisioningUuid_descriptor;
|
||||
private static
|
||||
@@ -1602,28 +1718,28 @@ public final class MessageProtos {
|
||||
descriptor;
|
||||
static {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n\033OutgoingMessageSignal.proto\022\ntextsecur" +
|
||||
"e\"\344\001\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(" +
|
||||
"\r\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceDevice\030\007 \001(\r\022" +
|
||||
"\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mes" +
|
||||
"sage\030\006 \001(\014\"d\n\004Type\022\013\n\007UNKNOWN\020\000\022\016\n\nCIPHE" +
|
||||
"RTEXT\020\001\022\020\n\014KEY_EXCHANGE\020\002\022\021\n\rPREKEY_BUND" +
|
||||
"LE\020\003\022\r\n\tPLAINTEXT\020\004\022\013\n\007RECEIPT\020\005\" \n\020Prov" +
|
||||
"isioningUuid\022\014\n\004uuid\030\001 \001(\tB:\n)org.whispe" +
|
||||
"rsystems.textsecuregcm.entitiesB\rMessage" +
|
||||
"Protos"
|
||||
"\n\020TextSecure.proto\022\ntextsecure\"\372\001\n\010Envel" +
|
||||
"ope\022\'\n\004type\030\001 \001(\0162\031.textsecure.Envelope." +
|
||||
"Type\022\016\n\006source\030\002 \001(\t\022\024\n\014sourceDevice\030\007 \001" +
|
||||
"(\r\022\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(\004\022\025\n\r" +
|
||||
"legacyMessage\030\006 \001(\014\022\017\n\007content\030\010 \001(\014\"U\n\004" +
|
||||
"Type\022\013\n\007UNKNOWN\020\000\022\016\n\nCIPHERTEXT\020\001\022\020\n\014KEY" +
|
||||
"_EXCHANGE\020\002\022\021\n\rPREKEY_BUNDLE\020\003\022\013\n\007RECEIP" +
|
||||
"T\020\005\" \n\020ProvisioningUuid\022\014\n\004uuid\030\001 \001(\tB:\n" +
|
||||
")org.whispersystems.textsecuregcm.entiti" +
|
||||
"esB\rMessageProtos"
|
||||
};
|
||||
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_OutgoingMessageSignal_descriptor =
|
||||
internal_static_textsecure_Envelope_descriptor =
|
||||
getDescriptor().getMessageTypes().get(0);
|
||||
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable = new
|
||||
internal_static_textsecure_Envelope_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_textsecure_OutgoingMessageSignal_descriptor,
|
||||
new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "Message", });
|
||||
internal_static_textsecure_Envelope_descriptor,
|
||||
new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "LegacyMessage", "Content", });
|
||||
internal_static_textsecure_ProvisioningUuid_descriptor =
|
||||
getDescriptor().getMessageTypes().get(1);
|
||||
internal_static_textsecure_ProvisioningUuid_fieldAccessorTable = new
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class OutgoingMessageEntity {
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private int type;
|
||||
|
||||
@JsonProperty
|
||||
private String relay;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
@JsonProperty
|
||||
private String source;
|
||||
|
||||
@JsonProperty
|
||||
private int sourceDevice;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] message;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] content;
|
||||
|
||||
public OutgoingMessageEntity() {}
|
||||
|
||||
public OutgoingMessageEntity(long id, int type, String relay, long timestamp,
|
||||
String source, int sourceDevice, byte[] message,
|
||||
byte[] content)
|
||||
{
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.relay = relay;
|
||||
this.timestamp = timestamp;
|
||||
this.source = source;
|
||||
this.sourceDevice = sourceDevice;
|
||||
this.message = message;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getRelay() {
|
||||
return relay;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public int getSourceDevice() {
|
||||
return sourceDevice;
|
||||
}
|
||||
|
||||
public byte[] getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public byte[] getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingMessageEntityList {
|
||||
|
||||
@JsonProperty
|
||||
private List<OutgoingMessageEntity> messages;
|
||||
|
||||
@JsonProperty
|
||||
private boolean more;
|
||||
|
||||
public OutgoingMessageEntityList() {}
|
||||
|
||||
public OutgoingMessageEntityList(List<OutgoingMessageEntity> messages, boolean more) {
|
||||
this.messages = messages;
|
||||
this.more = more;
|
||||
}
|
||||
|
||||
public List<OutgoingMessageEntity> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public boolean hasMore() {
|
||||
return more;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ public class UnregisteredEvent {
|
||||
@NotEmpty
|
||||
private String registrationId;
|
||||
|
||||
@JsonProperty
|
||||
private String canonicalId;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String number;
|
||||
@@ -26,6 +29,10 @@ public class UnregisteredEvent {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public String getCanonicalId() {
|
||||
return canonicalId;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
@@ -18,17 +18,15 @@ package org.whispersystems.textsecuregcm.federation;
|
||||
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.sun.jersey.api.client.Client;
|
||||
import com.sun.jersey.api.client.ClientHandlerException;
|
||||
import com.sun.jersey.api.client.ClientResponse;
|
||||
import com.sun.jersey.api.client.UniformInterfaceException;
|
||||
import com.sun.jersey.api.client.WebResource;
|
||||
import com.sun.jersey.api.client.config.ClientConfig;
|
||||
import com.sun.jersey.api.client.config.DefaultClientConfig;
|
||||
import com.sun.jersey.api.json.JSONConfiguration;
|
||||
import com.sun.jersey.client.urlconnection.HTTPSProperties;
|
||||
import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.bouncycastle.openssl.PEMReader;
|
||||
import org.glassfish.jersey.client.ClientProperties;
|
||||
import org.glassfish.jersey.client.RequestEntityProcessing;
|
||||
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
||||
@@ -38,11 +36,13 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -57,7 +57,10 @@ import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.dropwizard.client.JerseyClientBuilder;
|
||||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
import io.dropwizard.setup.Environment;
|
||||
|
||||
public class FederatedClient {
|
||||
|
||||
@@ -73,15 +76,14 @@ public class FederatedClient {
|
||||
|
||||
private final FederatedPeer peer;
|
||||
private final Client client;
|
||||
private final String authorizationHeader;
|
||||
|
||||
public FederatedClient(String federationName, FederatedPeer peer)
|
||||
public FederatedClient(Environment environment, JerseyClientConfiguration configuration,
|
||||
String federationName, FederatedPeer peer)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
this.client = Client.create(getClientConfig(peer));
|
||||
this.peer = peer;
|
||||
this.authorizationHeader = getAuthorizationHeader(federationName, peer);
|
||||
this.client = createClient(environment, configuration, federationName, peer);
|
||||
this.peer = peer;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (KeyStoreException | KeyManagementException | CertificateException e) {
|
||||
@@ -91,20 +93,14 @@ public class FederatedClient {
|
||||
|
||||
public URL getSignedAttachmentUri(long attachmentId) throws IOException {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl())
|
||||
.path(String.format(ATTACHMENT_URI_PATH, attachmentId));
|
||||
AttachmentUri response = client.target(peer.getUrl())
|
||||
.path(String.format(ATTACHMENT_URI_PATH, attachmentId))
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(AttachmentUri.class);
|
||||
|
||||
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return response.getEntity(AttachmentUri.class).getLocation();
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
return response.getLocation();
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("Bad URI", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
@@ -112,19 +108,14 @@ public class FederatedClient {
|
||||
|
||||
public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device));
|
||||
PreKeyResponseV1 response = client.target(peer.getUrl())
|
||||
.path(String.format(PREKEY_PATH_DEVICE_V1, destination, device))
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(PreKeyResponseV1.class);
|
||||
|
||||
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return Optional.of(response.getEntity(PreKeyResponseV1.class));
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
return Optional.of(response);
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("PreKey", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
@@ -132,34 +123,29 @@ public class FederatedClient {
|
||||
|
||||
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device));
|
||||
PreKeyResponseV2 response = client.target(peer.getUrl())
|
||||
.path(String.format(PREKEY_PATH_DEVICE_V2, destination, device))
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(PreKeyResponseV2.class);
|
||||
|
||||
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(ClientResponse.class);
|
||||
|
||||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return Optional.of(response.getEntity(PreKeyResponseV2.class));
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
return Optional.of(response);
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("PreKey", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getUserCount() {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
|
||||
AccountCount count = resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(AccountCount.class);
|
||||
AccountCount count = client.target(peer.getUrl())
|
||||
.path(USER_COUNT_PATH)
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(AccountCount.class);
|
||||
|
||||
return count.getCount();
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("User Count", e);
|
||||
return 0;
|
||||
}
|
||||
@@ -167,13 +153,14 @@ public class FederatedClient {
|
||||
|
||||
public List<ClientContact> getUserTokens(int offset) {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(USER_TOKENS_PATH, offset));
|
||||
ClientContacts contacts = resource.accept(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.get(ClientContacts.class);
|
||||
ClientContacts contacts = client.target(peer.getUrl())
|
||||
.path(String.format(USER_TOKENS_PATH, offset))
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(ClientContacts.class);
|
||||
|
||||
return contacts.getContacts();
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("User Tokens", e);
|
||||
return null;
|
||||
}
|
||||
@@ -182,46 +169,53 @@ public class FederatedClient {
|
||||
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
|
||||
throws IOException
|
||||
{
|
||||
Response response = null;
|
||||
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination));
|
||||
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", authorizationHeader)
|
||||
.entity(messages)
|
||||
.put(ClientResponse.class);
|
||||
response = client.target(peer.getUrl())
|
||||
.path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination))
|
||||
.request()
|
||||
.put(Entity.json(messages));
|
||||
|
||||
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
|
||||
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
|
||||
}
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("sendMessage", e);
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
if (response != null) response.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
|
||||
throws IOException
|
||||
{
|
||||
Response response = null;
|
||||
|
||||
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);
|
||||
response = client.target(peer.getUrl())
|
||||
.path(String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId))
|
||||
.request()
|
||||
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||
.put(Entity.entity("", MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
|
||||
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
|
||||
}
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("sendMessage", e);
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
if (response != null) response.close();
|
||||
}
|
||||
}
|
||||
|
||||
private String getAuthorizationHeader(String federationName, FederatedPeer peer) {
|
||||
return "Basic " + Base64.encodeBytes((federationName + ":" + peer.getAuthenticationToken()).getBytes());
|
||||
}
|
||||
|
||||
private ClientConfig getClientConfig(FederatedPeer peer)
|
||||
private Client createClient(Environment environment, JerseyClientConfiguration configuration,
|
||||
String federationName, FederatedPeer peer)
|
||||
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, CertificateException
|
||||
{
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
|
||||
@@ -230,12 +224,19 @@ public class FederatedClient {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagerFactory.getTrustManagers(), SecureRandom.getInstance("SHA1PRNG"));
|
||||
|
||||
ClientConfig config = new DefaultClientConfig();
|
||||
config.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES,
|
||||
new HTTPSProperties(new StrictHostnameVerifier(), sslContext));
|
||||
config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
|
||||
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier());
|
||||
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionSocketFactory).build();
|
||||
|
||||
return config;
|
||||
Client client = new JerseyClientBuilder(environment).using(configuration)
|
||||
.using(registry)
|
||||
.build("FederatedClient");
|
||||
|
||||
client.property(ClientProperties.CONNECT_TIMEOUT, 5000);
|
||||
client.property(ClientProperties.READ_TIMEOUT, 10000);
|
||||
client.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
|
||||
client.register(HttpAuthenticationFeature.basic(federationName, peer.getAuthenticationToken()));
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private KeyStore initializeTrustStore(String name, String pemCertificate)
|
||||
@@ -261,19 +262,6 @@ public class FederatedClient {
|
||||
}
|
||||
}
|
||||
|
||||
private Response clientResponseToResponse(ClientResponse r) {
|
||||
Response.ResponseBuilder rb = Response.status(r.getStatus());
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : r.getHeaders().entrySet()) {
|
||||
for (String value : entry.getValue()) {
|
||||
rb.header(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
|
||||
rb.entity(r.getEntityInputStream());
|
||||
return rb.build();
|
||||
}
|
||||
|
||||
public String getPeerName() {
|
||||
return peer.getName();
|
||||
}
|
||||
|
||||
@@ -25,13 +25,18 @@ import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
import io.dropwizard.setup.Environment;
|
||||
|
||||
public class FederatedClientManager {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederatedClientManager.class);
|
||||
|
||||
private final HashMap<String, FederatedClient> clients = new HashMap<>();
|
||||
|
||||
public FederatedClientManager(FederationConfiguration federationConfig)
|
||||
public FederatedClientManager(Environment environment,
|
||||
JerseyClientConfiguration clientConfig,
|
||||
FederationConfiguration federationConfig)
|
||||
throws IOException
|
||||
{
|
||||
List<FederatedPeer> peers = federationConfig.getPeers();
|
||||
@@ -40,7 +45,7 @@ public class FederatedClientManager {
|
||||
if (peers != null) {
|
||||
for (FederatedPeer peer : peers) {
|
||||
logger.info("Adding peer: " + peer.getName());
|
||||
clients.put(peer.getName(), new FederatedClient(identity, peer));
|
||||
clients.put(peer.getName(), new FederatedClient(environment, clientConfig, identity, peer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, System.currentTimeMillis()));
|
||||
return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "NA"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ 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.db.PooledDataSourceFactory;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
import liquibase.Liquibase;
|
||||
import liquibase.exception.LiquibaseException;
|
||||
@@ -40,10 +40,8 @@ public abstract class AbstractLiquibaseCommand<T extends Configuration> extends
|
||||
@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);
|
||||
final PooledDataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration);
|
||||
dbConfig.asSingleConnectionPool();
|
||||
|
||||
try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) {
|
||||
run(namespace, liquibase);
|
||||
@@ -53,7 +51,7 @@ public abstract class AbstractLiquibaseCommand<T extends Configuration> extends
|
||||
}
|
||||
}
|
||||
|
||||
private CloseableLiquibase openLiquibase(final DataSourceFactory dataSourceFactory, final Namespace namespace)
|
||||
private CloseableLiquibase openLiquibase(final PooledDataSourceFactory dataSourceFactory, final Namespace namespace)
|
||||
throws ClassNotFoundException, SQLException, LiquibaseException
|
||||
{
|
||||
final ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(), "liquibase");
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.whispersystems.textsecuregcm.mappers;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceLimitExceededException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class DeviceLimitExceededExceptionMapper implements ExceptionMapper<DeviceLimitExceededException> {
|
||||
@Override
|
||||
public Response toResponse(DeviceLimitExceededException exception) {
|
||||
return Response.status(411)
|
||||
.entity(new DeviceLimitExceededDetails(exception.getCurrentDevices(),
|
||||
exception.getMaxDevices()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class DeviceLimitExceededDetails {
|
||||
@JsonProperty
|
||||
private int current;
|
||||
@JsonProperty
|
||||
private int max;
|
||||
|
||||
public DeviceLimitExceededDetails(int current, int max) {
|
||||
this.current = current;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class FileDescriptorGauge implements Gauge<Integer> {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
File file = new File("/proc/self/fd");
|
||||
|
||||
if (file.isDirectory() && file.exists()) {
|
||||
return file.list().length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,14 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.providers;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
|
||||
import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
@@ -25,29 +31,40 @@ import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.Protocol;
|
||||
|
||||
public class RedisClientFactory {
|
||||
public class RedisClientFactory implements RedisPubSubConnectionFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RedisClientFactory.class);
|
||||
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final JedisPool jedisPool;
|
||||
|
||||
public RedisClientFactory(String url) throws URISyntaxException {
|
||||
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
||||
poolConfig.setTestOnBorrow(true);
|
||||
|
||||
URI redisURI = new URI(url);
|
||||
String redisHost = redisURI.getHost();
|
||||
int redisPort = redisURI.getPort();
|
||||
String redisPassword = null;
|
||||
URI redisURI = new URI(url);
|
||||
|
||||
if (!Util.isEmpty(redisURI.getUserInfo())) {
|
||||
redisPassword = redisURI.getUserInfo().split(":",2)[1];
|
||||
}
|
||||
|
||||
this.jedisPool = new JedisPool(poolConfig, redisHost, redisPort,
|
||||
Protocol.DEFAULT_TIMEOUT, redisPassword);
|
||||
this.host = redisURI.getHost();
|
||||
this.port = redisURI.getPort();
|
||||
this.jedisPool = new JedisPool(poolConfig, host, port,
|
||||
Protocol.DEFAULT_TIMEOUT, null);
|
||||
}
|
||||
|
||||
public JedisPool getRedisClientPool() {
|
||||
return jedisPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubSubConnection connect() {
|
||||
while (true) {
|
||||
try {
|
||||
Socket socket = new Socket(host, port);
|
||||
return new PubSubConnection(socket);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error connecting", e);
|
||||
Util.sleep(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,7 @@ public class RedisHealthCheck extends HealthCheck {
|
||||
|
||||
@Override
|
||||
protected Result check() throws Exception {
|
||||
Jedis client = clientPool.getResource();
|
||||
|
||||
try {
|
||||
try (Jedis client = clientPool.getResource()) {
|
||||
client.set("HEALTH", "test");
|
||||
|
||||
if (!"test".equals(client.get("HEALTH"))) {
|
||||
@@ -41,8 +39,6 @@ public class RedisHealthCheck extends HealthCheck {
|
||||
}
|
||||
|
||||
return Result.healthy();
|
||||
} finally {
|
||||
clientPool.returnResource(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.RatioGauge;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.DispatchChannel;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketConnectionInfo;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
|
||||
public class ApnFallbackManager implements Managed, Runnable, DispatchChannel {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApnFallbackManager.class);
|
||||
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Meter voipOneSuccess = metricRegistry.meter(name(ApnFallbackManager.class, "voip_one_success"));
|
||||
private static final Meter voipOneDelivery = metricRegistry.meter(name(ApnFallbackManager.class, "voip_one_failure"));
|
||||
private static final Histogram voipOneSuccessHistogram = metricRegistry.histogram(name(ApnFallbackManager.class, "voip_one_success_histogram"));
|
||||
|
||||
static {
|
||||
metricRegistry.register(name(ApnFallbackManager.class, "voip_one_success_ratio"), new VoipRatioGauge(voipOneSuccess, voipOneDelivery));
|
||||
}
|
||||
|
||||
private final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
|
||||
|
||||
private final PushServiceClient pushServiceClient;
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
public ApnFallbackManager(PushServiceClient pushServiceClient, PubSubManager pubSubManager) {
|
||||
this.pushServiceClient = pushServiceClient;
|
||||
this.pubSubManager = pubSubManager;
|
||||
}
|
||||
|
||||
public void schedule(final WebsocketAddress address, ApnFallbackTask task) {
|
||||
voipOneDelivery.mark();
|
||||
|
||||
if (taskQueue.put(address, task)) {
|
||||
pubSubManager.subscribe(new WebSocketConnectionInfo(address), this);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancel(WebsocketAddress address) {
|
||||
ApnFallbackTask task = taskQueue.remove(address);
|
||||
|
||||
if (task != null) {
|
||||
pubSubManager.unsubscribe(new WebSocketConnectionInfo(address), this);
|
||||
voipOneSuccess.mark();
|
||||
voipOneSuccessHistogram.update(System.currentTimeMillis() - task.getScheduledTime());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
new Thread(this).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
Entry<WebsocketAddress, ApnFallbackTask> taskEntry = taskQueue.get();
|
||||
ApnFallbackTask task = taskEntry.getValue();
|
||||
|
||||
pubSubManager.unsubscribe(new WebSocketConnectionInfo(taskEntry.getKey()), this);
|
||||
pushServiceClient.send(new ApnMessage(task.getMessage(), task.getApnId(),
|
||||
false, ApnMessage.MAX_EXPIRATION));
|
||||
} catch (Throwable e) {
|
||||
logger.warn("ApnFallbackThread", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDispatchMessage(String channel, byte[] message) {
|
||||
try {
|
||||
PubSubMessage notification = PubSubMessage.parseFrom(message);
|
||||
|
||||
if (notification.getType().getNumber() == PubSubMessage.Type.CONNECTED_VALUE) {
|
||||
WebSocketConnectionInfo address = new WebSocketConnectionInfo(channel);
|
||||
cancel(address.getWebsocketAddress());
|
||||
} else {
|
||||
logger.warn("Got strange pubsub type: " + notification.getType().getNumber());
|
||||
}
|
||||
|
||||
} catch (WebSocketConnectionInfo.FormattingException e) {
|
||||
logger.warn("Bad formatting?", e);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.warn("Bad protobuf", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDispatchSubscribed(String channel) {}
|
||||
|
||||
@Override
|
||||
public void onDispatchUnsubscribed(String channel) {}
|
||||
|
||||
public static class ApnFallbackTask {
|
||||
|
||||
private final long delay;
|
||||
private final long scheduledTime;
|
||||
private final String apnId;
|
||||
private final ApnMessage message;
|
||||
|
||||
public ApnFallbackTask(String apnId, ApnMessage message) {
|
||||
this(apnId, message, TimeUnit.SECONDS.toMillis(30));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ApnFallbackTask(String apnId, ApnMessage message, long delay) {
|
||||
this.scheduledTime = System.currentTimeMillis();
|
||||
this.delay = delay;
|
||||
this.apnId = apnId;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getApnId() {
|
||||
return apnId;
|
||||
}
|
||||
|
||||
public ApnMessage getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public long getScheduledTime() {
|
||||
return scheduledTime;
|
||||
}
|
||||
|
||||
public long getExecutionTime() {
|
||||
return scheduledTime + delay;
|
||||
}
|
||||
|
||||
public long getDelay() {
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static class ApnFallbackTaskQueue {
|
||||
|
||||
private final LinkedHashMap<WebsocketAddress, ApnFallbackTask> tasks = new LinkedHashMap<>();
|
||||
|
||||
public Entry<WebsocketAddress, ApnFallbackTask> get() {
|
||||
while (true) {
|
||||
long timeDelta;
|
||||
|
||||
synchronized (tasks) {
|
||||
while (tasks.isEmpty()) Util.wait(tasks);
|
||||
|
||||
Iterator<Entry<WebsocketAddress, ApnFallbackTask>> iterator = tasks.entrySet().iterator();
|
||||
Entry<WebsocketAddress, ApnFallbackTask> nextTask = iterator.next();
|
||||
|
||||
timeDelta = nextTask.getValue().getExecutionTime() - System.currentTimeMillis();
|
||||
|
||||
if (timeDelta <= 0) {
|
||||
iterator.remove();
|
||||
return nextTask;
|
||||
}
|
||||
}
|
||||
|
||||
Util.sleep(timeDelta);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean put(WebsocketAddress address, ApnFallbackTask task) {
|
||||
synchronized (tasks) {
|
||||
ApnFallbackTask previous = tasks.put(address, task);
|
||||
tasks.notifyAll();
|
||||
|
||||
return previous == null;
|
||||
}
|
||||
}
|
||||
|
||||
public ApnFallbackTask remove(WebsocketAddress address) {
|
||||
synchronized (tasks) {
|
||||
return tasks.remove(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class VoipRatioGauge extends RatioGauge {
|
||||
|
||||
private final Meter success;
|
||||
private final Meter attempts;
|
||||
|
||||
private VoipRatioGauge(Meter success, Meter attempts) {
|
||||
this.success = success;
|
||||
this.attempts = attempts;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Ratio getRatio() {
|
||||
return Ratio.of(success.getFiveMinuteRate(), attempts.getFiveMinuteRate());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,10 +73,17 @@ public class FeedbackHandler implements Managed, Runnable {
|
||||
if (event.getRegistrationId().equals(device.get().getGcmId())) {
|
||||
logger.info("GCM Unregister GCM ID matches!");
|
||||
if (device.get().getPushTimestamp() == 0 ||
|
||||
event.getTimestamp() > device.get().getPushTimestamp())
|
||||
event.getTimestamp() > (device.get().getPushTimestamp() + TimeUnit.SECONDS.toMillis(10)))
|
||||
{
|
||||
logger.info("GCM Unregister Timestamp matches!");
|
||||
device.get().setGcmId(null);
|
||||
|
||||
if (event.getCanonicalId() != null && !event.getCanonicalId().isEmpty()) {
|
||||
logger.info("It's a canonical ID update...");
|
||||
device.get().setGcmId(event.getCanonicalId());
|
||||
} else {
|
||||
device.get().setGcmId(null);
|
||||
device.get().setFetchesMessages(false);
|
||||
}
|
||||
accountsManager.update(account.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,101 +16,159 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmMessage;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask;
|
||||
import org.whispersystems.textsecuregcm.push.WebsocketSender.DeliveryStatus;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.BlockingThreadPoolExecutor;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PushSender {
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
|
||||
public class PushSender implements Managed {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
|
||||
|
||||
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"badge\":%d,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
|
||||
|
||||
private final PushServiceClient pushServiceClient;
|
||||
private final WebsocketSender webSocketSender;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final PushServiceClient pushServiceClient;
|
||||
private final WebsocketSender webSocketSender;
|
||||
private final BlockingThreadPoolExecutor executor;
|
||||
private final int queueSize;
|
||||
|
||||
public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
|
||||
this.pushServiceClient = pushServiceClient;
|
||||
this.webSocketSender = websocketSender;
|
||||
public PushSender(ApnFallbackManager apnFallbackManager, PushServiceClient pushServiceClient,
|
||||
WebsocketSender websocketSender, int queueSize)
|
||||
{
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
this.pushServiceClient = pushServiceClient;
|
||||
this.webSocketSender = websocketSender;
|
||||
this.queueSize = queueSize;
|
||||
this.executor = new BlockingThreadPoolExecutor(50, queueSize);
|
||||
|
||||
SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME)
|
||||
.register(name(PushSender.class, "send_queue_depth"),
|
||||
new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return executor.getSize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||
public void sendMessage(final Account account, final Device device, final Envelope message)
|
||||
throws NotPushRegisteredException
|
||||
{
|
||||
if (device.getGcmId() == null && device.getApnId() == null && !device.getFetchesMessages()) {
|
||||
throw new NotPushRegisteredException("No delivery possible!");
|
||||
}
|
||||
|
||||
if (queueSize > 0) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendSynchronousMessage(account, device, message);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sendSynchronousMessage(account, device, message);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendQueuedNotification(Account account, Device device, int messageQueueDepth)
|
||||
throws NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
|
||||
else if (device.getApnId() != null) sendApnMessage(account, device, message);
|
||||
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message);
|
||||
else throw new NotPushRegisteredException("No delivery possible!");
|
||||
if (device.getGcmId() != null) sendGcmNotification(account, device);
|
||||
else if (device.getApnId() != null) sendApnNotification(account, device, messageQueueDepth);
|
||||
else if (!device.getFetchesMessages()) throw new NotPushRegisteredException("No notification possible!");
|
||||
}
|
||||
|
||||
public WebsocketSender getWebSocketSender() {
|
||||
return webSocketSender;
|
||||
}
|
||||
|
||||
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 sendSynchronousMessage(Account account, Device device, Envelope message) {
|
||||
if (device.getGcmId() != null) sendGcmMessage(account, device, message);
|
||||
else if (device.getApnId() != null) sendApnMessage(account, device, message);
|
||||
else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message);
|
||||
else throw new AssertionError();
|
||||
}
|
||||
|
||||
private void sendPayloadGcmMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||
throws TransientPushFailureException, NotPushRegisteredException
|
||||
{
|
||||
try {
|
||||
String number = account.getNumber();
|
||||
long deviceId = device.getId();
|
||||
String registrationId = device.getGcmId();
|
||||
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
|
||||
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
|
||||
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int) deviceId,
|
||||
encryptedMessage.toEncodedString(), isReceipt, false);
|
||||
|
||||
pushServiceClient.send(gcmMessage);
|
||||
} catch (CryptoEncodingException e) {
|
||||
throw new NotPushRegisteredException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendNotificationGcmMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||
throws TransientPushFailureException
|
||||
{
|
||||
private void sendGcmMessage(Account account, Device device, Envelope message) {
|
||||
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, message, WebsocketSender.Type.GCM);
|
||||
|
||||
if (!deliveryStatus.isDelivered()) {
|
||||
sendGcmNotification(account, device);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendGcmNotification(Account account, Device device) {
|
||||
try {
|
||||
GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(),
|
||||
(int)device.getId(), "", false, true);
|
||||
|
||||
pushServiceClient.send(gcmMessage);
|
||||
} else {
|
||||
logger.warn("Delivered!");
|
||||
} catch (TransientPushFailureException e) {
|
||||
logger.warn("SILENT PUSH LOSS", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void sendApnMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
|
||||
throws TransientPushFailureException
|
||||
{
|
||||
private void sendApnMessage(Account account, Device device, Envelope outgoingMessage) {
|
||||
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);
|
||||
if (!deliveryStatus.isDelivered() && outgoingMessage.getType() != Envelope.Type.RECEIPT) {
|
||||
sendApnNotification(account, device, deliveryStatus.getMessageQueueDepth());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendWebSocketMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
|
||||
private void sendApnNotification(Account account, Device device, int messageQueueDepth) {
|
||||
ApnMessage apnMessage;
|
||||
|
||||
if (!Util.isEmpty(device.getVoipApnId())) {
|
||||
apnMessage = new ApnMessage(device.getVoipApnId(), account.getNumber(), (int)device.getId(),
|
||||
String.format(APN_PAYLOAD, messageQueueDepth),
|
||||
true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30));
|
||||
|
||||
apnFallbackManager.schedule(new WebsocketAddress(account.getNumber(), device.getId()),
|
||||
new ApnFallbackTask(device.getApnId(), apnMessage));
|
||||
} else {
|
||||
apnMessage = new ApnMessage(device.getApnId(), account.getNumber(), (int)device.getId(),
|
||||
String.format(APN_PAYLOAD, messageQueueDepth),
|
||||
false, ApnMessage.MAX_EXPIRATION);
|
||||
}
|
||||
|
||||
try {
|
||||
pushServiceClient.send(apnMessage);
|
||||
} catch (TransientPushFailureException e) {
|
||||
logger.warn("SILENT PUSH LOSS", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendWebSocketMessage(Account account, Device device, Envelope outgoingMessage)
|
||||
{
|
||||
webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.WEB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(5, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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;
|
||||
@@ -13,7 +9,11 @@ import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
|
||||
import org.whispersystems.textsecuregcm.entities.UnregisteredEventList;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@@ -57,16 +57,17 @@ public class PushServiceClient {
|
||||
|
||||
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);
|
||||
Response response = client.target("http://" + host + ":" + port)
|
||||
.path(path)
|
||||
.request()
|
||||
.header("Authorization", authorization)
|
||||
.put(Entity.entity(entity, MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
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) {
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("Push error: ", e);
|
||||
throw new TransientPushFailureException(e);
|
||||
}
|
||||
@@ -74,12 +75,14 @@ public class PushServiceClient {
|
||||
|
||||
private List<UnregisteredEvent> getFeedback(String path) throws IOException {
|
||||
try {
|
||||
UnregisteredEventList unregisteredEvents = client.resource("http://" + host + ":" + port + path)
|
||||
UnregisteredEventList unregisteredEvents = client.target("http://" + host + ":" + port)
|
||||
.path(path)
|
||||
.request()
|
||||
.header("Authorization", authorization)
|
||||
.get(UnregisteredEventList.class);
|
||||
|
||||
return unregisteredEvents.getDevices();
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
} catch (ProcessingException e) {
|
||||
logger.warn("Request error:", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||
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.Set;
|
||||
|
||||
public class ReceiptSender {
|
||||
|
||||
private final PushSender pushSender;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
private final AccountsManager accountManager;
|
||||
|
||||
public ReceiptSender(AccountsManager accountManager,
|
||||
PushSender pushSender,
|
||||
FederatedClientManager federatedClientManager)
|
||||
{
|
||||
this.federatedClientManager = federatedClientManager;
|
||||
this.accountManager = accountManager;
|
||||
this.pushSender = pushSender;
|
||||
}
|
||||
|
||||
public void sendReceipt(Account source, String destination,
|
||||
long messageId, Optional<String> relay)
|
||||
throws IOException, NoSuchUserException,
|
||||
NotPushRegisteredException, TransientPushFailureException
|
||||
{
|
||||
if (relay.isPresent() && !relay.get().isEmpty()) {
|
||||
sendRelayedReceipt(source, destination, messageId, relay.get());
|
||||
} else {
|
||||
sendDirectReceipt(source, destination, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
Envelope.Builder message = Envelope.newBuilder()
|
||||
.setSource(source.getNumber())
|
||||
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
|
||||
.setTimestamp(messageId)
|
||||
.setType(Envelope.Type.RECEIPT);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||
|
||||
public class WebsocketSender {
|
||||
@@ -46,6 +46,7 @@ public class WebsocketSender {
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
|
||||
private final Meter websocketRequeueMeter = metricRegistry.meter(name(getClass(), "ws_requeue"));
|
||||
private final Meter websocketOnlineMeter = metricRegistry.meter(name(getClass(), "ws_online" ));
|
||||
private final Meter websocketOfflineMeter = metricRegistry.meter(name(getClass(), "ws_offline" ));
|
||||
|
||||
@@ -66,7 +67,7 @@ public class WebsocketSender {
|
||||
this.pubSubManager = pubSubManager;
|
||||
}
|
||||
|
||||
public DeliveryStatus sendMessage(Account account, Device device, OutgoingMessageSignal message, Type channel) {
|
||||
public DeliveryStatus sendMessage(Account account, Device device, Envelope message, Type channel) {
|
||||
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
|
||||
.setType(PubSubMessage.Type.DELIVER)
|
||||
@@ -84,15 +85,24 @@ public class WebsocketSender {
|
||||
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());
|
||||
|
||||
int queueDepth = queueMessage(account, device, message);
|
||||
return new DeliveryStatus(false, queueDepth);
|
||||
}
|
||||
}
|
||||
|
||||
public int queueMessage(Account account, Device device, Envelope message) {
|
||||
websocketRequeueMeter.mark();
|
||||
|
||||
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||
int queueDepth = messagesManager.insert(account.getNumber(), device.getId(), message);
|
||||
|
||||
pubSubManager.publish(address, PubSubMessage.newBuilder()
|
||||
.setType(PubSubMessage.Type.QUERY_DB)
|
||||
.build());
|
||||
|
||||
return queueDepth;
|
||||
}
|
||||
|
||||
public boolean sendProvisioningMessage(ProvisioningAddress address, byte[] body) {
|
||||
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
|
||||
.setType(PubSubMessage.Type.DELIVER)
|
||||
|
||||
@@ -1,91 +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.sms;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class NexmoSmsSender {
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
|
||||
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
|
||||
private final Logger logger = LoggerFactory.getLogger(NexmoSmsSender.class);
|
||||
|
||||
private static final String NEXMO_SMS_URL =
|
||||
"https://rest.nexmo.com/sms/json?api_key=%s&api_secret=%s&from=%s&to=%s&text=%s";
|
||||
|
||||
private static final String NEXMO_VOX_URL =
|
||||
"https://rest.nexmo.com/tts/json?api_key=%s&api_secret=%s&to=%s&text=%s";
|
||||
|
||||
private final String apiKey;
|
||||
private final String apiSecret;
|
||||
private final String number;
|
||||
|
||||
public NexmoSmsSender(NexmoConfiguration config) {
|
||||
this.apiKey = config.getApiKey();
|
||||
this.apiSecret = config.getApiSecret();
|
||||
this.number = config.getNumber();
|
||||
}
|
||||
|
||||
public void deliverSmsVerification(String destination, String verificationCode) throws IOException {
|
||||
URL url = new URL(String.format(NEXMO_SMS_URL, apiKey, apiSecret, number, destination,
|
||||
URLEncoder.encode(SmsSender.SMS_VERIFICATION_TEXT + verificationCode, "UTF-8")));
|
||||
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
while (reader.readLine() != null) {}
|
||||
reader.close();
|
||||
smsMeter.mark();
|
||||
}
|
||||
|
||||
public void deliverVoxVerification(String destination, String message) throws IOException {
|
||||
URL url = new URL(String.format(NEXMO_VOX_URL, apiKey, apiSecret, destination,
|
||||
URLEncoder.encode(SmsSender.VOX_VERIFICATION_TEXT + message, "UTF-8")));
|
||||
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
logger.debug(line);
|
||||
}
|
||||
reader.close();
|
||||
voxMeter.mark();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -26,59 +26,41 @@ import java.io.IOException;
|
||||
|
||||
public class SmsSender {
|
||||
|
||||
static final String SMS_VERIFICATION_TEXT = "Your TextSecure verification code: ";
|
||||
static final String VOX_VERIFICATION_TEXT = "Your TextSecure verification code is: ";
|
||||
static final String SMS_IOS_VERIFICATION_TEXT = "Your Signal verification code: %s\n\nOr tap: sgnl://verify/%s";
|
||||
static final String SMS_VERIFICATION_TEXT = "Your TextSecure verification code: %s";
|
||||
static final String VOX_VERIFICATION_TEXT = "Your Signal verification code is: ";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SmsSender.class);
|
||||
|
||||
private final TwilioSmsSender twilioSender;
|
||||
private final Optional<NexmoSmsSender> nexmoSender;
|
||||
private final boolean isTwilioInternational;
|
||||
private final TwilioSmsSender twilioSender;
|
||||
|
||||
public SmsSender(TwilioSmsSender twilioSender,
|
||||
Optional<NexmoSmsSender> nexmoSender,
|
||||
boolean isTwilioInternational)
|
||||
public SmsSender(TwilioSmsSender twilioSender)
|
||||
{
|
||||
this.isTwilioInternational = isTwilioInternational;
|
||||
this.twilioSender = twilioSender;
|
||||
this.nexmoSender = nexmoSender;
|
||||
this.twilioSender = twilioSender;
|
||||
}
|
||||
|
||||
public void deliverSmsVerification(String destination, String verificationCode)
|
||||
public void deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode)
|
||||
throws IOException
|
||||
{
|
||||
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) {
|
||||
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
|
||||
} else {
|
||||
try {
|
||||
twilioSender.deliverSmsVerification(destination, verificationCode);
|
||||
} catch (TwilioRestException e) {
|
||||
logger.info("Twilio SMS Failed: " + e.getErrorMessage());
|
||||
if (nexmoSender.isPresent()) {
|
||||
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
|
||||
}
|
||||
}
|
||||
// Fix up mexico numbers to 'mobile' format just for SMS delivery.
|
||||
if (destination.startsWith("+42") && !destination.startsWith("+421")) {
|
||||
destination = "+421" + destination.substring(3);
|
||||
}
|
||||
|
||||
try {
|
||||
twilioSender.deliverSmsVerification(destination, clientType, verificationCode);
|
||||
} catch (TwilioRestException e) {
|
||||
logger.info("Twilio SMS Failed: " + e.getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void deliverVoxVerification(String destination, String verificationCode)
|
||||
throws IOException
|
||||
{
|
||||
if (!isTwilioDestination(destination) && nexmoSender.isPresent()) {
|
||||
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
|
||||
} else {
|
||||
try {
|
||||
twilioSender.deliverVoxVerification(destination, verificationCode);
|
||||
} catch (TwilioRestException e) {
|
||||
logger.info("Twilio Vox Failed: " + e.getErrorMessage());
|
||||
if (nexmoSender.isPresent()) {
|
||||
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
|
||||
}
|
||||
}
|
||||
try {
|
||||
twilioSender.deliverVoxVerification(destination, verificationCode);
|
||||
} catch (TwilioRestException e) {
|
||||
logger.info("Twilio Vox Failed: " + e.getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTwilioDestination(String number) {
|
||||
return isTwilioInternational || number.length() == 12 && number.startsWith("+1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.whispersystems.textsecuregcm.sms;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.google.common.base.Optional;
|
||||
import com.twilio.sdk.TwilioRestClient;
|
||||
import com.twilio.sdk.TwilioRestException;
|
||||
import com.twilio.sdk.resource.factory.CallFactory;
|
||||
@@ -29,10 +30,12 @@ import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
@@ -40,35 +43,42 @@ public class TwilioSmsSender {
|
||||
|
||||
public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<Response>\n" +
|
||||
" <Say voice=\"woman\" language=\"en\">" + SmsSender.VOX_VERIFICATION_TEXT + "%s</Say>\n" +
|
||||
" <Say voice=\"woman\" language=\"en\" loop=\"3\">" + SmsSender.VOX_VERIFICATION_TEXT + "%s.</Say>\n" +
|
||||
"</Response>";
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered"));
|
||||
private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered"));
|
||||
|
||||
private final String accountId;
|
||||
private final String accountToken;
|
||||
private final String number;
|
||||
private final String localDomain;
|
||||
private final String accountId;
|
||||
private final String accountToken;
|
||||
private final ArrayList<String> numbers;
|
||||
private final String localDomain;
|
||||
private final Random random;
|
||||
|
||||
public TwilioSmsSender(TwilioConfiguration config) {
|
||||
this.accountId = config.getAccountId();
|
||||
this.accountToken = config.getAccountToken();
|
||||
this.number = config.getNumber();
|
||||
this.numbers = new ArrayList<>(config.getNumbers());
|
||||
this.localDomain = config.getLocalDomain();
|
||||
this.random = new Random(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public void deliverSmsVerification(String destination, String verificationCode)
|
||||
public void deliverSmsVerification(String destination, Optional<String> clientType, String verificationCode)
|
||||
throws IOException, TwilioRestException
|
||||
{
|
||||
TwilioRestClient client = new TwilioRestClient(accountId, accountToken);
|
||||
MessageFactory messageFactory = client.getAccount().getMessageFactory();
|
||||
List<NameValuePair> messageParams = new LinkedList<>();
|
||||
messageParams.add(new BasicNameValuePair("To", destination));
|
||||
messageParams.add(new BasicNameValuePair("From", number));
|
||||
messageParams.add(new BasicNameValuePair("Body", SmsSender.SMS_VERIFICATION_TEXT + verificationCode));
|
||||
|
||||
messageParams.add(new BasicNameValuePair("From", getRandom(random, numbers)));
|
||||
|
||||
if ("ios".equals(clientType.orNull())) {
|
||||
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_IOS_VERIFICATION_TEXT, verificationCode, verificationCode)));
|
||||
} else {
|
||||
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_VERIFICATION_TEXT, verificationCode)));
|
||||
}
|
||||
|
||||
try {
|
||||
messageFactory.create(messageParams);
|
||||
} catch (RuntimeException damnYouTwilio) {
|
||||
@@ -85,7 +95,7 @@ public class TwilioSmsSender {
|
||||
CallFactory callFactory = client.getAccount().getCallFactory();
|
||||
Map<String, String> callParams = new HashMap<>();
|
||||
callParams.put("To", destination);
|
||||
callParams.put("From", number);
|
||||
callParams.put("From", getRandom(random, numbers));
|
||||
callParams.put("Url", "https://" + localDomain + "/v1/accounts/voice/twiml/" + verificationCode);
|
||||
|
||||
try {
|
||||
@@ -96,4 +106,9 @@ public class TwilioSmsSender {
|
||||
|
||||
voxMeter.mark();
|
||||
}
|
||||
|
||||
private String getRandom(Random random, ArrayList<String> elements) {
|
||||
return elements.get(random.nextInt(elements.size()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,9 +32,6 @@ public class Account {
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
private boolean supportsSms;
|
||||
|
||||
@JsonProperty
|
||||
private Set<Device> devices = new HashSet<>();
|
||||
|
||||
@@ -47,10 +44,9 @@ public class Account {
|
||||
public Account() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account(String number, boolean supportsSms, Set<Device> devices) {
|
||||
this.number = number;
|
||||
this.supportsSms = supportsSms;
|
||||
this.devices = devices;
|
||||
public Account(String number, Set<Device> devices) {
|
||||
this.number = number;
|
||||
this.devices = devices;
|
||||
}
|
||||
|
||||
public Optional<Device> getAuthenticatedDevice() {
|
||||
@@ -69,19 +65,15 @@ public class Account {
|
||||
return number;
|
||||
}
|
||||
|
||||
public boolean getSupportsSms() {
|
||||
return supportsSms;
|
||||
}
|
||||
|
||||
public void setSupportsSms(boolean supportsSms) {
|
||||
this.supportsSms = supportsSms;
|
||||
}
|
||||
|
||||
public void addDevice(Device device) {
|
||||
this.devices.remove(device);
|
||||
this.devices.add(device);
|
||||
}
|
||||
|
||||
public void removeDevice(long deviceId) {
|
||||
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, false, "NA"));
|
||||
}
|
||||
|
||||
public Set<Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
@@ -100,6 +92,16 @@ public class Account {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public boolean isVoiceSupported() {
|
||||
for (Device device : devices) {
|
||||
if (device.isActive() && device.isVoiceSupported()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return
|
||||
getMasterDevice().isPresent() &&
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.skife.jdbi.v2.SQLStatement;
|
||||
|
||||
@@ -100,7 +100,7 @@ public class AccountsManager {
|
||||
private void updateDirectory(Account account) {
|
||||
if (account.isActive()) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
|
||||
directory.add(clientContact);
|
||||
} else {
|
||||
directory.remove(account.getNumber());
|
||||
|
||||
@@ -31,6 +31,9 @@ public class Device {
|
||||
@JsonProperty
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String authToken;
|
||||
|
||||
@@ -46,6 +49,9 @@ public class Device {
|
||||
@JsonProperty
|
||||
private String apnId;
|
||||
|
||||
@JsonProperty
|
||||
private String voipApnId;
|
||||
|
||||
@JsonProperty
|
||||
private long pushTimestamp;
|
||||
|
||||
@@ -61,23 +67,39 @@ public class Device {
|
||||
@JsonProperty
|
||||
private long lastSeen;
|
||||
|
||||
@JsonProperty
|
||||
private long created;
|
||||
|
||||
@JsonProperty
|
||||
private boolean voice;
|
||||
|
||||
@JsonProperty
|
||||
private String userAgent;
|
||||
|
||||
public Device() {}
|
||||
|
||||
public Device(long id, String authToken, String salt,
|
||||
public Device(long id, String name, String authToken, String salt,
|
||||
String signalingKey, String gcmId, String apnId,
|
||||
boolean fetchesMessages, int registrationId,
|
||||
SignedPreKey signedPreKey, long lastSeen)
|
||||
String voipApnId, boolean fetchesMessages,
|
||||
int registrationId, SignedPreKey signedPreKey,
|
||||
long lastSeen, long created, boolean voice,
|
||||
String userAgent)
|
||||
{
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.authToken = authToken;
|
||||
this.salt = salt;
|
||||
this.signalingKey = signalingKey;
|
||||
this.gcmId = gcmId;
|
||||
this.apnId = apnId;
|
||||
this.voipApnId = voipApnId;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.signedPreKey = signedPreKey;
|
||||
this.lastSeen = lastSeen;
|
||||
this.created = created;
|
||||
this.voice = voice;
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public String getApnId() {
|
||||
@@ -92,6 +114,14 @@ public class Device {
|
||||
}
|
||||
}
|
||||
|
||||
public String getVoipApnId() {
|
||||
return voipApnId;
|
||||
}
|
||||
|
||||
public void setVoipApnId(String voipApnId) {
|
||||
this.voipApnId = voipApnId;
|
||||
}
|
||||
|
||||
public void setLastSeen(long lastSeen) {
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
@@ -100,6 +130,14 @@ public class Device {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public void setCreated(long created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public long getCreated() {
|
||||
return this.created;
|
||||
}
|
||||
|
||||
public String getGcmId() {
|
||||
return gcmId;
|
||||
}
|
||||
@@ -120,6 +158,22 @@ public class Device {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public boolean isVoiceSupported() {
|
||||
return voice;
|
||||
}
|
||||
|
||||
public void setVoiceSupported(boolean voice) {
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
|
||||
this.authToken = credentials.getHashedAuthenticationToken();
|
||||
this.salt = credentials.getSalt();
|
||||
@@ -176,6 +230,14 @@ public class Device {
|
||||
return pushTimestamp;
|
||||
}
|
||||
|
||||
public void setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return this.userAgent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof Device)) return false;
|
||||
|
||||
@@ -61,9 +61,9 @@ public class DirectoryManager {
|
||||
}
|
||||
|
||||
public void remove(byte[] token) {
|
||||
Jedis jedis = redisPool.getResource();
|
||||
jedis.hdel(DIRECTORY_KEY, token);
|
||||
redisPool.returnResource(jedis);
|
||||
try (Jedis jedis = redisPool.getResource()) {
|
||||
jedis.hdel(DIRECTORY_KEY, token);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(BatchOperationHandle handle, byte[] token) {
|
||||
@@ -72,7 +72,7 @@ public class DirectoryManager {
|
||||
}
|
||||
|
||||
public void add(ClientContact contact) {
|
||||
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
|
||||
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice());
|
||||
|
||||
try (Jedis jedis = redisPool.getResource()) {
|
||||
jedis.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
|
||||
@@ -84,7 +84,7 @@ public class DirectoryManager {
|
||||
public void add(BatchOperationHandle handle, ClientContact contact) {
|
||||
try {
|
||||
Pipeline pipeline = handle.pipeline;
|
||||
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
|
||||
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice());
|
||||
|
||||
pipeline.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
|
||||
} catch (JsonProcessingException e) {
|
||||
@@ -106,7 +106,7 @@ public class DirectoryManager {
|
||||
}
|
||||
|
||||
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
|
||||
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
|
||||
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice));
|
||||
} catch (IOException e) {
|
||||
logger.warn("JSON Error", e);
|
||||
return Optional.absent();
|
||||
@@ -133,7 +133,7 @@ public class DirectoryManager {
|
||||
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);
|
||||
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.voice);
|
||||
|
||||
results.add(clientContact);
|
||||
}
|
||||
@@ -175,14 +175,14 @@ public class DirectoryManager {
|
||||
@JsonProperty(value = "r")
|
||||
private String relay;
|
||||
|
||||
@JsonProperty(value = "s")
|
||||
private boolean supportsSms;
|
||||
@JsonProperty(value = "v")
|
||||
private boolean voice;
|
||||
|
||||
public TokenValue() {}
|
||||
|
||||
public TokenValue(String relay, boolean supportsSms) {
|
||||
this.relay = relay;
|
||||
this.supportsSms = supportsSms;
|
||||
public TokenValue(String relay, boolean voice) {
|
||||
this.relay = relay;
|
||||
this.voice = voice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public class DirectoryManager {
|
||||
}
|
||||
|
||||
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
|
||||
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
|
||||
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
@@ -11,8 +10,8 @@ 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 org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
@@ -25,6 +24,8 @@ import java.util.List;
|
||||
|
||||
public abstract class Messages {
|
||||
|
||||
public static final int RESULT_SET_CHUNK_SIZE = 100;
|
||||
|
||||
private static final String ID = "id";
|
||||
private static final String TYPE = "type";
|
||||
private static final String RELAY = "relay";
|
||||
@@ -34,42 +35,65 @@ public abstract class Messages {
|
||||
private static final String DESTINATION = "destination";
|
||||
private static final String DESTINATION_DEVICE = "destination_device";
|
||||
private static final String MESSAGE = "message";
|
||||
private static final String CONTENT = "content";
|
||||
|
||||
@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,
|
||||
@SqlQuery("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ", " + CONTENT + ") " +
|
||||
"VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message, :content) " +
|
||||
"RETURNING (SELECT COUNT(id) FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + TYPE + " != " + Envelope.Type.RECEIPT_VALUE + ")")
|
||||
abstract int store(@MessageBinder Envelope 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);
|
||||
@SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC LIMIT " + RESULT_SET_CHUNK_SIZE)
|
||||
abstract List<OutgoingMessageEntity> load(@Bind("destination") String destination,
|
||||
@Bind("destination_device") long destinationDevice);
|
||||
|
||||
@SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id")
|
||||
abstract void remove(@Bind("id") long id);
|
||||
@Mapper(MessageMapper.class)
|
||||
@SqlQuery("DELETE FROM messages WHERE " + ID + " IN (SELECT " + ID + " FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + SOURCE + " = :source AND " + TIMESTAMP + " = :timestamp ORDER BY " + ID + " LIMIT 1) RETURNING *")
|
||||
abstract OutgoingMessageEntity remove(@Bind("destination") String destination,
|
||||
@Bind("destination_device") long destinationDevice,
|
||||
@Bind("source") String source,
|
||||
@Bind("timestamp") long timestamp);
|
||||
|
||||
@Mapper(MessageMapper.class)
|
||||
@SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id AND " + DESTINATION + " = :destination")
|
||||
abstract void remove(@Bind("destination") String destination, @Bind("id") long id);
|
||||
|
||||
@SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination")
|
||||
abstract void clear(@Bind("destination") String destination);
|
||||
|
||||
@SqlUpdate("DELETE FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device")
|
||||
abstract void clear(@Bind("destination") String destination, @Bind("destination_device") long destinationDevice);
|
||||
|
||||
@SqlUpdate("DELETE FROM messages WHERE " + TIMESTAMP + " < :timestamp")
|
||||
public abstract void removeOld(@Bind("timestamp") long timestamp);
|
||||
|
||||
@SqlUpdate("VACUUM messages")
|
||||
public abstract void vacuum();
|
||||
|
||||
public static class MessageMapper implements ResultSetMapper<Pair<Long, OutgoingMessageSignal>> {
|
||||
public static class MessageMapper implements ResultSetMapper<OutgoingMessageEntity> {
|
||||
@Override
|
||||
public Pair<Long, OutgoingMessageSignal> map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||
public OutgoingMessageEntity 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());
|
||||
|
||||
int type = resultSet.getInt(TYPE);
|
||||
byte[] legacyMessage = resultSet.getBytes(MESSAGE);
|
||||
|
||||
if (type == Envelope.Type.RECEIPT_VALUE && legacyMessage == null) {
|
||||
/// XXX - REMOVE AFTER 10/01/15
|
||||
legacyMessage = new byte[0];
|
||||
}
|
||||
|
||||
return new OutgoingMessageEntity(resultSet.getLong(ID),
|
||||
type,
|
||||
resultSet.getString(RELAY),
|
||||
resultSet.getLong(TIMESTAMP),
|
||||
resultSet.getString(SOURCE),
|
||||
resultSet.getInt(SOURCE_DEVICE),
|
||||
legacyMessage,
|
||||
resultSet.getBytes(CONTENT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,18 +104,19 @@ public abstract class Messages {
|
||||
public static class AccountBinderFactory implements BinderFactory {
|
||||
@Override
|
||||
public Binder build(Annotation annotation) {
|
||||
return new Binder<MessageBinder, OutgoingMessageSignal>() {
|
||||
return new Binder<MessageBinder, Envelope>() {
|
||||
@Override
|
||||
public void bind(SQLStatement<?> sql,
|
||||
MessageBinder accountBinder,
|
||||
OutgoingMessageSignal message)
|
||||
Envelope message)
|
||||
{
|
||||
sql.bind(TYPE, message.getType());
|
||||
sql.bind(TYPE, message.getType().getNumber());
|
||||
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());
|
||||
sql.bind(MESSAGE, message.hasLegacyMessage() ? message.getLegacyMessage().toByteArray() : null);
|
||||
sql.bind(CONTENT, message.hasContent() ? message.getContent().toByteArray() : null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import com.google.common.base.Optional;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -14,19 +16,29 @@ public class MessagesManager {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public int insert(String destination, long destinationDevice, OutgoingMessageSignal message) {
|
||||
public int insert(String destination, long destinationDevice, Envelope 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 OutgoingMessageEntityList getMessagesForDevice(String destination, long destinationDevice) {
|
||||
List<OutgoingMessageEntity> messages = this.messages.load(destination, destinationDevice);
|
||||
return new OutgoingMessageEntityList(messages, messages.size() >= Messages.RESULT_SET_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
public void clear(String destination) {
|
||||
this.messages.clear(destination);
|
||||
}
|
||||
|
||||
public void delete(long id) {
|
||||
this.messages.remove(id);
|
||||
public void clear(String destination, long deviceId) {
|
||||
this.messages.clear(destination, deviceId);
|
||||
}
|
||||
|
||||
public Optional<OutgoingMessageEntity> delete(String destination, long destinationDevice, String source, long timestamp)
|
||||
{
|
||||
return Optional.fromNullable(this.messages.remove(destination, destinationDevice, source, timestamp));
|
||||
}
|
||||
|
||||
public void delete(String destination, long id) {
|
||||
this.messages.remove(destination, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
public interface PubSubAddress {
|
||||
public String serialize();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||
|
||||
public interface PubSubListener {
|
||||
|
||||
public void onPubSubMessage(PubSubMessage outgoingMessage);
|
||||
|
||||
}
|
||||
@@ -1,129 +1,90 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||
import org.whispersystems.dispatch.DispatchChannel;
|
||||
import org.whispersystems.dispatch.DispatchManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||
import redis.clients.jedis.BinaryJedisPubSub;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
public class PubSubManager {
|
||||
public class PubSubManager implements Managed {
|
||||
|
||||
private static final byte[] KEEPALIVE_CHANNEL = "KEEPALIVE".getBytes();
|
||||
private static final String KEEPALIVE_CHANNEL = "KEEPALIVE";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
|
||||
private final SubscriptionListener baseListener = new SubscriptionListener();
|
||||
private final Map<String, PubSubListener> listeners = new HashMap<>();
|
||||
private final Logger logger = LoggerFactory.getLogger(PubSubManager.class);
|
||||
|
||||
private final DispatchManager dispatchManager;
|
||||
private final JedisPool jedisPool;
|
||||
|
||||
private final JedisPool jedisPool;
|
||||
private boolean subscribed = false;
|
||||
|
||||
public PubSubManager(JedisPool jedisPool) {
|
||||
this.jedisPool = jedisPool;
|
||||
initializePubSubWorker();
|
||||
waitForSubscription();
|
||||
public PubSubManager(JedisPool jedisPool, DispatchManager dispatchManager) {
|
||||
this.dispatchManager = dispatchManager;
|
||||
this.jedisPool = jedisPool;
|
||||
}
|
||||
|
||||
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
|
||||
String serializedAddress = address.serialize();
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
this.dispatchManager.start();
|
||||
|
||||
listeners.put(serializedAddress, listener);
|
||||
baseListener.subscribe(serializedAddress.getBytes());
|
||||
}
|
||||
KeepaliveDispatchChannel keepaliveDispatchChannel = new KeepaliveDispatchChannel();
|
||||
this.dispatchManager.subscribe(KEEPALIVE_CHANNEL, keepaliveDispatchChannel);
|
||||
|
||||
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
|
||||
String serializedAddress = address.serialize();
|
||||
|
||||
if (listeners.get(serializedAddress) == listener) {
|
||||
listeners.remove(serializedAddress);
|
||||
baseListener.unsubscribe(serializedAddress.getBytes());
|
||||
synchronized (this) {
|
||||
while (!subscribed) wait(0);
|
||||
}
|
||||
|
||||
new KeepaliveSender().start();
|
||||
}
|
||||
|
||||
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
dispatchManager.shutdown();
|
||||
}
|
||||
|
||||
public void subscribe(PubSubAddress address, DispatchChannel channel) {
|
||||
dispatchManager.subscribe(address.serialize(), channel);
|
||||
}
|
||||
|
||||
public void unsubscribe(PubSubAddress address, DispatchChannel dispatchChannel) {
|
||||
dispatchManager.unsubscribe(address.serialize(), dispatchChannel);
|
||||
}
|
||||
|
||||
public boolean hasLocalSubscription(PubSubAddress address) {
|
||||
return dispatchManager.hasSubscription(address.serialize());
|
||||
}
|
||||
|
||||
public boolean publish(PubSubAddress address, PubSubMessage message) {
|
||||
return publish(address.serialize().getBytes(), message);
|
||||
}
|
||||
|
||||
private synchronized boolean publish(byte[] channel, PubSubMessage message) {
|
||||
private boolean publish(byte[] channel, PubSubMessage message) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
return jedis.publish(channel, message.toByteArray()) != 0;
|
||||
long result = jedis.publish(channel, message.toByteArray());
|
||||
|
||||
if (result < 0) {
|
||||
logger.warn("**** Jedis publish result < 0");
|
||||
}
|
||||
|
||||
return result > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void waitForSubscription() {
|
||||
try {
|
||||
while (!subscribed) {
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePubSubWorker() {
|
||||
new Thread("PubSubListener") {
|
||||
@Override
|
||||
public void run() {
|
||||
for (;;) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.subscribe(baseListener, KEEPALIVE_CHANNEL);
|
||||
logger.warn("**** Unsubscribed from holding channel!!! ******");
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
new Thread("PubSubKeepAlive") {
|
||||
@Override
|
||||
public void run() {
|
||||
for (;;) {
|
||||
try {
|
||||
Thread.sleep(20000);
|
||||
publish(KEEPALIVE_CHANNEL, PubSubMessage.newBuilder()
|
||||
.setType(PubSubMessage.Type.KEEPALIVE)
|
||||
.build());
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private class SubscriptionListener extends BinaryJedisPubSub {
|
||||
private class KeepaliveDispatchChannel implements DispatchChannel {
|
||||
|
||||
@Override
|
||||
public void onMessage(byte[] channel, byte[] message) {
|
||||
try {
|
||||
PubSubListener listener;
|
||||
|
||||
synchronized (PubSubManager.this) {
|
||||
listener = listeners.get(new String(channel));
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onPubSubMessage(PubSubMessage.parseFrom(message));
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.warn("Error parsing PubSub protobuf", e);
|
||||
}
|
||||
public void onDispatchMessage(String channel, byte[] message) {
|
||||
// Good
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPMessage(byte[] s, byte[] s2, byte[] s3) {
|
||||
logger.warn("Received PMessage!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(byte[] channel, int count) {
|
||||
if (Arrays.equals(KEEPALIVE_CHANNEL, channel)) {
|
||||
public void onDispatchSubscribed(String channel) {
|
||||
if (KEEPALIVE_CHANNEL.equals(channel)) {
|
||||
synchronized (PubSubManager.this) {
|
||||
subscribed = true;
|
||||
PubSubManager.this.notifyAll();
|
||||
@@ -132,12 +93,24 @@ public class PubSubManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnsubscribe(byte[] s, int i) {}
|
||||
public void onDispatchUnsubscribed(String channel) {
|
||||
logger.warn("***** KEEPALIVE CHANNEL UNSUBSCRIBED *****");
|
||||
}
|
||||
}
|
||||
|
||||
private class KeepaliveSender extends Thread {
|
||||
@Override
|
||||
public void onPUnsubscribe(byte[] s, int i) {}
|
||||
|
||||
@Override
|
||||
public void onPSubscribe(byte[] s, int i) {}
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
Thread.sleep(20000);
|
||||
publish(KEEPALIVE_CHANNEL.getBytes(), PubSubMessage.newBuilder()
|
||||
.setType(PubSubMessage.Type.KEEPALIVE)
|
||||
.build());
|
||||
} catch (Throwable e) {
|
||||
logger.warn("***** KEEPALIVE EXCEPTION ******", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,10 @@ public final class PubSubProtos {
|
||||
* <code>CLOSE = 4;</code>
|
||||
*/
|
||||
CLOSE(4, 4),
|
||||
/**
|
||||
* <code>CONNECTED = 5;</code>
|
||||
*/
|
||||
CONNECTED(5, 5),
|
||||
;
|
||||
|
||||
/**
|
||||
@@ -184,6 +188,10 @@ public final class PubSubProtos {
|
||||
* <code>CLOSE = 4;</code>
|
||||
*/
|
||||
public static final int CLOSE_VALUE = 4;
|
||||
/**
|
||||
* <code>CONNECTED = 5;</code>
|
||||
*/
|
||||
public static final int CONNECTED_VALUE = 5;
|
||||
|
||||
|
||||
public final int getNumber() { return value; }
|
||||
@@ -195,6 +203,7 @@ public final class PubSubProtos {
|
||||
case 2: return DELIVER;
|
||||
case 3: return KEEPALIVE;
|
||||
case 4: return CLOSE;
|
||||
case 5: return CONNECTED;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -620,13 +629,13 @@ public final class PubSubProtos {
|
||||
descriptor;
|
||||
static {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n\023PubSubMessage.proto\022\ntextsecure\"\230\001\n\rPu" +
|
||||
"\n\023PubSubMessage.proto\022\ntextsecure\"\247\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" +
|
||||
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"W\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"
|
||||
"\020\002\022\r\n\tKEEPALIVE\020\003\022\t\n\005CLOSE\020\004\022\r\n\tCONNECTE" +
|
||||
"D\020\005B8\n(org.whispersystems.textsecuregcm." +
|
||||
"storageB\014PubSubProtos"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {
|
||||
|
||||
private final Semaphore semaphore;
|
||||
|
||||
public BlockingThreadPoolExecutor(int threads, int bound) {
|
||||
super(threads, threads, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
|
||||
this.semaphore = new Semaphore(bound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable task) {
|
||||
semaphore.acquireUninterruptibly();
|
||||
|
||||
try {
|
||||
super.execute(task);
|
||||
} catch (Throwable t) {
|
||||
semaphore.release();
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterExecute(Runnable r, Throwable t) {
|
||||
semaphore.release();
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return ((LinkedBlockingQueue)getQueue()).size();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class SystemMapper {
|
||||
@@ -11,6 +12,7 @@ public class SystemMapper {
|
||||
static {
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
public static ObjectMapper getMapper() {
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -115,12 +116,24 @@ public class Util {
|
||||
return parts;
|
||||
}
|
||||
|
||||
public static void sleep(int i) {
|
||||
public static void sleep(long i) {
|
||||
try {
|
||||
Thread.sleep(i);
|
||||
} catch (InterruptedException ie) {}
|
||||
}
|
||||
|
||||
public static void wait(Object object) {
|
||||
try {
|
||||
object.wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static int hashCode(Object... objects) {
|
||||
return Arrays.hashCode(objects);
|
||||
}
|
||||
|
||||
public static long todayInMillis() {
|
||||
return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
@@ -1,55 +1,72 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
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.storage.PubSubProtos.PubSubMessage;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
||||
import org.whispersystems.websocket.setup.WebSocketConnectListener;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Histogram durationHistogram = metricRegistry.histogram(name(WebSocketConnection.class, "connected_duration"));
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final PushSender pushSender;
|
||||
private final MessagesManager messagesManager;
|
||||
private final PubSubManager pubSubManager;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final PushSender pushSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final MessagesManager messagesManager;
|
||||
private final PubSubManager pubSubManager;
|
||||
|
||||
public AuthenticatedConnectListener(AccountsManager accountsManager, PushSender pushSender,
|
||||
MessagesManager messagesManager, PubSubManager pubSubManager)
|
||||
ReceiptSender receiptSender, MessagesManager messagesManager,
|
||||
PubSubManager pubSubManager, ApnFallbackManager apnFallbackManager)
|
||||
{
|
||||
this.accountsManager = accountsManager;
|
||||
this.pushSender = pushSender;
|
||||
this.messagesManager = messagesManager;
|
||||
this.pubSubManager = pubSubManager;
|
||||
this.accountsManager = accountsManager;
|
||||
this.pushSender = pushSender;
|
||||
this.receiptSender = receiptSender;
|
||||
this.messagesManager = messagesManager;
|
||||
this.pubSubManager = pubSubManager;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(WebSocketSessionContext context) {
|
||||
Account account = context.getAuthenticated(Account.class).get();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
final Account account = context.getAuthenticated(Account.class);
|
||||
final Device device = account.getAuthenticatedDevice().get();
|
||||
final long connectTime = System.currentTimeMillis();
|
||||
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||
final WebSocketConnectionInfo info = new WebSocketConnectionInfo(address);
|
||||
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
|
||||
messagesManager, account, device,
|
||||
context.getClient());
|
||||
|
||||
pubSubManager.publish(info, PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED).build());
|
||||
updateLastSeen(account, device);
|
||||
closeExistingDeviceConnection(account, device);
|
||||
|
||||
final WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender,
|
||||
messagesManager, pubSubManager,
|
||||
account, device,
|
||||
context.getClient());
|
||||
|
||||
connection.onConnected();
|
||||
pubSubManager.subscribe(address, connection);
|
||||
|
||||
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
|
||||
@Override
|
||||
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
|
||||
connection.onConnectionLost();
|
||||
pubSubManager.unsubscribe(address, connection);
|
||||
durationHistogram.update(System.currentTimeMillis() - connectTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -60,12 +77,5 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.DispatchChannel;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||
|
||||
public class DeadLetterHandler implements DispatchChannel {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DeadLetterHandler.class);
|
||||
|
||||
private final MessagesManager messagesManager;
|
||||
|
||||
public DeadLetterHandler(MessagesManager messagesManager) {
|
||||
this.messagesManager = messagesManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDispatchMessage(String channel, byte[] data) {
|
||||
if (!WebSocketConnectionInfo.isType(channel)) {
|
||||
try {
|
||||
logger.info("Handling dead letter to: " + channel);
|
||||
|
||||
WebsocketAddress address = new WebsocketAddress(channel);
|
||||
PubSubMessage pubSubMessage = PubSubMessage.parseFrom(data);
|
||||
|
||||
switch (pubSubMessage.getType().getNumber()) {
|
||||
case PubSubMessage.Type.DELIVER_VALUE:
|
||||
Envelope message = Envelope.parseFrom(pubSubMessage.getContent());
|
||||
messagesManager.insert(address.getNumber(), address.getDeviceId(), message);
|
||||
break;
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.warn("Bad pubsub message", e);
|
||||
} catch (InvalidWebsocketAddressException e) {
|
||||
logger.warn("Invalid websocket address", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDispatchSubscribed(String channel) {
|
||||
logger.warn("DeadLetterHandler subscription notice! " + channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDispatchUnsubscribed(String channel) {
|
||||
logger.warn("DeadLetterHandler unsubscribe notice! " + channel);
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,16 @@ import java.security.SecureRandom;
|
||||
|
||||
public class ProvisioningAddress extends WebsocketAddress {
|
||||
|
||||
private final String address;
|
||||
public ProvisioningAddress(String address, int id) throws InvalidWebsocketAddressException {
|
||||
super(address, id);
|
||||
}
|
||||
|
||||
public ProvisioningAddress(String address) throws InvalidWebsocketAddressException {
|
||||
super(address, 0);
|
||||
this.address = address;
|
||||
public ProvisioningAddress(String serialized) throws InvalidWebsocketAddressException {
|
||||
super(serialized);
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
return getNumber();
|
||||
}
|
||||
|
||||
public static ProvisioningAddress generate() {
|
||||
@@ -24,7 +25,7 @@ public class ProvisioningAddress extends WebsocketAddress {
|
||||
SecureRandom.getInstance("SHA1PRNG").nextBytes(random);
|
||||
|
||||
return new ProvisioningAddress(Base64.encodeBytesWithoutPadding(random)
|
||||
.replace('+', '-').replace('/', '_'));
|
||||
.replace('+', '-').replace('/', '_'), 0);
|
||||
} catch (NoSuchAlgorithmException | InvalidWebsocketAddressException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,15 @@ public class ProvisioningConnectListener implements WebSocketConnectListener {
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(WebSocketSessionContext context) {
|
||||
final ProvisioningConnection connection = new ProvisioningConnection(pubSubManager, context.getClient());
|
||||
connection.onConnected();
|
||||
final ProvisioningConnection connection = new ProvisioningConnection(context.getClient());
|
||||
final ProvisioningAddress provisioningAddress = ProvisioningAddress.generate();
|
||||
|
||||
pubSubManager.subscribe(provisioningAddress, connection);
|
||||
|
||||
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
|
||||
@Override
|
||||
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
|
||||
connection.onConnectionLost();
|
||||
pubSubManager.unsubscribe(provisioningAddress, connection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,58 +4,68 @@ 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.dispatch.DispatchChannel;
|
||||
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 {
|
||||
public class ProvisioningConnection implements DispatchChannel {
|
||||
|
||||
private final PubSubManager pubSubManager;
|
||||
private final ProvisioningAddress provisioningAddress;
|
||||
private final WebSocketClient client;
|
||||
private final Logger logger = LoggerFactory.getLogger(ProvisioningConnection.class);
|
||||
|
||||
public ProvisioningConnection(PubSubManager pubSubManager, WebSocketClient client) {
|
||||
this.pubSubManager = pubSubManager;
|
||||
this.client = client;
|
||||
this.provisioningAddress = ProvisioningAddress.generate();
|
||||
private final WebSocketClient client;
|
||||
|
||||
public ProvisioningConnection(WebSocketClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPubSubMessage(PubSubMessage outgoingMessage) {
|
||||
if (outgoingMessage.getType() == PubSubMessage.Type.DELIVER) {
|
||||
Optional<byte[]> body = Optional.of(outgoingMessage.getContent().toByteArray());
|
||||
public void onDispatchMessage(String channel, byte[] message) {
|
||||
try {
|
||||
PubSubMessage outgoingMessage = PubSubMessage.parseFrom(message);
|
||||
|
||||
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/v1/message", body);
|
||||
if (outgoingMessage.getType() == PubSubMessage.Type.DELIVER) {
|
||||
Optional<byte[]> body = Optional.of(outgoingMessage.getContent().toByteArray());
|
||||
|
||||
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
|
||||
@Override
|
||||
public void onSuccess(WebSocketResponseMessage webSocketResponseMessage) {
|
||||
pubSubManager.unsubscribe(provisioningAddress, ProvisioningConnection.this);
|
||||
client.close(1001, "All you get.");
|
||||
}
|
||||
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/v1/message", body);
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
pubSubManager.unsubscribe(provisioningAddress, ProvisioningConnection.this);
|
||||
client.close(1001, "That's all!");
|
||||
}
|
||||
});
|
||||
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
|
||||
@Override
|
||||
public void onSuccess(WebSocketResponseMessage webSocketResponseMessage) {
|
||||
client.close(1001, "All you get.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
client.close(1001, "That's all!");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.warn("Protobuf Error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void onConnected() {
|
||||
this.pubSubManager.subscribe(provisioningAddress, this);
|
||||
this.client.sendRequest("PUT", "/v1/address", Optional.of(ProvisioningUuid.newBuilder()
|
||||
.setUuid(provisioningAddress.getAddress())
|
||||
.build()
|
||||
.toByteArray()));
|
||||
@Override
|
||||
public void onDispatchSubscribed(String channel) {
|
||||
try {
|
||||
ProvisioningAddress address = new ProvisioningAddress(channel);
|
||||
this.client.sendRequest("PUT", "/v1/address", Optional.of(ProvisioningUuid.newBuilder()
|
||||
.setUuid(address.getAddress())
|
||||
.build()
|
||||
.toByteArray()));
|
||||
} catch (InvalidWebsocketAddressException e) {
|
||||
logger.warn("Badly formatted address", e);
|
||||
this.client.close(1001, "Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
public void onConnectionLost() {
|
||||
this.pubSubManager.unsubscribe(provisioningAddress, this);
|
||||
this.client.close(1001, "Done");
|
||||
@Override
|
||||
public void onDispatchUnsubscribed(String channel) {
|
||||
this.client.close(1001, "Closed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.websocket.auth.AuthenticationException;
|
||||
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
@@ -24,18 +25,18 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Acc
|
||||
@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");
|
||||
Map<String, List<String>> parameters = request.getParameterMap();
|
||||
List<String> usernames = parameters.get("login");
|
||||
List<String> passwords = parameters.get("password");
|
||||
|
||||
if (usernames == null || usernames.length == 0 ||
|
||||
passwords == null || passwords.length == 0)
|
||||
if (usernames == null || usernames.size() == 0 ||
|
||||
passwords == null || passwords.size() == 0)
|
||||
{
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
BasicCredentials credentials = new BasicCredentials(usernames[0].replace(" ", "+"),
|
||||
passwords[0].replace(" ", "+"));
|
||||
BasicCredentials credentials = new BasicCredentials(usernames.get(0).replace(" ", "+"),
|
||||
passwords.get(0).replace(" ", "+"));
|
||||
|
||||
return accountAuthenticator.authenticate(credentials);
|
||||
} catch (io.dropwizard.auth.AuthenticationException e) {
|
||||
|
||||
@@ -4,85 +4,73 @@ 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.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.DispatchChannel;
|
||||
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
|
||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
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 javax.ws.rs.WebApplicationException;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||
|
||||
public class WebSocketConnection implements PubSubListener {
|
||||
public class WebSocketConnection implements DispatchChannel {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final ReceiptSender receiptSender;
|
||||
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,
|
||||
public WebSocketConnection(PushSender pushSender,
|
||||
ReceiptSender receiptSender,
|
||||
MessagesManager messagesManager,
|
||||
PubSubManager pubSubManager,
|
||||
Account account,
|
||||
Device device,
|
||||
WebSocketClient client)
|
||||
{
|
||||
this.accountsManager = accountsManager;
|
||||
this.pushSender = pushSender;
|
||||
this.receiptSender = receiptSender;
|
||||
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) {
|
||||
public void onDispatchMessage(String channel, byte[] message) {
|
||||
try {
|
||||
PubSubMessage pubSubMessage = PubSubMessage.parseFrom(message);
|
||||
|
||||
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);
|
||||
sendMessage(Envelope.parseFrom(pubSubMessage.getContent()), Optional.<Long>absent(), false);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Unknown pubsub message: " + pubSubMessage.getType().getNumber());
|
||||
@@ -92,8 +80,18 @@ public class WebSocketConnection implements PubSubListener {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(final OutgoingMessageSignal message,
|
||||
final Optional<Long> storedMessageId)
|
||||
@Override
|
||||
public void onDispatchUnsubscribed(String channel) {
|
||||
client.close(1000, "OK");
|
||||
}
|
||||
|
||||
public void onDispatchSubscribed(String channel) {
|
||||
processStoredMessages();
|
||||
}
|
||||
|
||||
private void sendMessage(final Envelope message,
|
||||
final Optional<Long> storedMessageId,
|
||||
final boolean requery)
|
||||
{
|
||||
try {
|
||||
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
|
||||
@@ -103,12 +101,13 @@ public class WebSocketConnection implements PubSubListener {
|
||||
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable WebSocketResponseMessage response) {
|
||||
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
|
||||
boolean isReceipt = message.getType() == Envelope.Type.RECEIPT;
|
||||
|
||||
if (isSuccessResponse(response)) {
|
||||
if (storedMessageId.isPresent()) messagesManager.delete(storedMessageId.get());
|
||||
if (storedMessageId.isPresent()) messagesManager.delete(account.getNumber(), storedMessageId.get());
|
||||
if (!isReceipt) sendDeliveryReceiptFor(message);
|
||||
} else if (!isSuccessResponse(response) & !storedMessageId.isPresent()) {
|
||||
if (requery) processStoredMessages();
|
||||
} else if (!isSuccessResponse(response) && !storedMessageId.isPresent()) {
|
||||
requeueMessage(message);
|
||||
}
|
||||
}
|
||||
@@ -127,45 +126,55 @@ public class WebSocketConnection implements PubSubListener {
|
||||
}
|
||||
}
|
||||
|
||||
private void requeueMessage(OutgoingMessageSignal message) {
|
||||
private void requeueMessage(Envelope message) {
|
||||
int queueDepth = pushSender.getWebSocketSender().queueMessage(account, device, message);
|
||||
|
||||
try {
|
||||
pushSender.sendMessage(account, device, message);
|
||||
pushSender.sendQueuedNotification(account, device, queueDepth);
|
||||
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
||||
logger.warn("requeueMessage", e);
|
||||
messagesManager.insert(account.getNumber(), device.getId(), message);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDeliveryReceiptFor(OutgoingMessageSignal message) {
|
||||
private void sendDeliveryReceiptFor(Envelope 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);
|
||||
receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp(),
|
||||
message.hasRelay() ? Optional.of(message.getRelay()) :
|
||||
Optional.<String>absent());
|
||||
} catch (NoSuchUserException | NotPushRegisteredException e) {
|
||||
logger.info("No longer registered " + e.getMessage());
|
||||
} catch(IOException | TransientPushFailureException e) {
|
||||
logger.warn("Something wrong while sending receipt", e);
|
||||
} catch (WebApplicationException e) {
|
||||
logger.warn("Bad federated response for receipt: " + e.getResponse().getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private void processStoredMessages() {
|
||||
List<Pair<Long, OutgoingMessageSignal>> messages = messagesManager.getMessagesForDevice(account.getNumber(),
|
||||
device.getId());
|
||||
OutgoingMessageEntityList messages = messagesManager.getMessagesForDevice(account.getNumber(), device.getId());
|
||||
Iterator<OutgoingMessageEntity> iterator = messages.getMessages().iterator();
|
||||
|
||||
for (Pair<Long, OutgoingMessageSignal> message : messages) {
|
||||
sendMessage(message.second(), Optional.of(message.first()));
|
||||
while (iterator.hasNext()) {
|
||||
OutgoingMessageEntity message = iterator.next();
|
||||
Envelope.Builder builder = Envelope.newBuilder()
|
||||
.setType(Envelope.Type.valueOf(message.getType()))
|
||||
.setSourceDevice(message.getSourceDevice())
|
||||
.setSource(message.getSource())
|
||||
.setTimestamp(message.getTimestamp());
|
||||
|
||||
if (message.getMessage() != null) {
|
||||
builder.setLegacyMessage(ByteString.copyFrom(message.getMessage()));
|
||||
}
|
||||
|
||||
if (message.getContent() != null) {
|
||||
builder.setContent(ByteString.copyFrom(message.getContent()));
|
||||
}
|
||||
|
||||
if (message.getRelay() != null && !message.getRelay().isEmpty()) {
|
||||
builder.setRelay(message.getRelay());
|
||||
}
|
||||
|
||||
sendMessage(builder.build(), Optional.of(message.getId()), !iterator.hasNext() && messages.hasMore());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubAddress;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class WebSocketConnectionInfo implements PubSubAddress {
|
||||
|
||||
private final WebsocketAddress address;
|
||||
|
||||
public WebSocketConnectionInfo(WebsocketAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public WebSocketConnectionInfo(String serialized) throws FormattingException {
|
||||
String[] parts = serialized.split("[:]", 3);
|
||||
|
||||
if (parts.length != 3 || !"c".equals(parts[2])) {
|
||||
throw new FormattingException("Bad address: " + serialized);
|
||||
}
|
||||
|
||||
try {
|
||||
this.address = new WebsocketAddress(parts[0], Long.parseLong(parts[1]));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new FormattingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return address.serialize() + ":c";
|
||||
}
|
||||
|
||||
public WebsocketAddress getWebsocketAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public static boolean isType(String address) {
|
||||
return address.endsWith(":c");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return
|
||||
other != null &&
|
||||
other instanceof WebSocketConnectionInfo
|
||||
&& ((WebSocketConnectionInfo)other).address.equals(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Util.hashCode(address, "c");
|
||||
}
|
||||
|
||||
public static class FormattingException extends Exception {
|
||||
public FormattingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FormattingException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.whispersystems.textsecuregcm.websocket;
|
||||
|
||||
public class WebsocketAddress {
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubAddress;
|
||||
|
||||
public class WebsocketAddress implements PubSubAddress {
|
||||
|
||||
private final String number;
|
||||
private final long deviceId;
|
||||
@@ -10,6 +12,29 @@ public class WebsocketAddress {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public WebsocketAddress(String serialized) throws InvalidWebsocketAddressException {
|
||||
try {
|
||||
String[] parts = serialized.split(":", 2);
|
||||
|
||||
if (parts.length != 2) {
|
||||
throw new InvalidWebsocketAddressException("Bad address: " + serialized);
|
||||
}
|
||||
|
||||
this.number = parts[0];
|
||||
this.deviceId = Long.parseLong(parts[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new InvalidWebsocketAddressException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return number + ":" + deviceId;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.workers;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import org.skife.jdbi.v2.DBI;
|
||||
import org.slf4j.Logger;
|
||||
@@ -27,31 +28,42 @@ import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
|
||||
import io.dropwizard.Application;
|
||||
import io.dropwizard.cli.ConfiguredCommand;
|
||||
import io.dropwizard.cli.EnvironmentCommand;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.jdbi.ImmutableListContainerFactory;
|
||||
import io.dropwizard.jdbi.ImmutableSetContainerFactory;
|
||||
import io.dropwizard.jdbi.OptionalContainerFactory;
|
||||
import io.dropwizard.jdbi.args.OptionalArgumentFactory;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
import io.dropwizard.setup.Environment;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfiguration> {
|
||||
public class DirectoryCommand extends EnvironmentCommand<WhisperServerConfiguration> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class);
|
||||
|
||||
public DirectoryCommand() {
|
||||
super("directory", "Update directory from DB and peers.");
|
||||
super(new Application<WhisperServerConfiguration>() {
|
||||
@Override
|
||||
public void run(WhisperServerConfiguration configuration, Environment environment)
|
||||
throws Exception
|
||||
{
|
||||
|
||||
}
|
||||
}, "directory", "Update directory from DB and peers.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Bootstrap<WhisperServerConfiguration> bootstrap,
|
||||
Namespace namespace,
|
||||
WhisperServerConfiguration config)
|
||||
protected void run(Environment environment, Namespace namespace,
|
||||
WhisperServerConfiguration configuration)
|
||||
throws Exception
|
||||
{
|
||||
try {
|
||||
DataSourceFactory dbConfig = config.getDataSourceFactory();
|
||||
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
DataSourceFactory dbConfig = configuration.getDataSourceFactory();
|
||||
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
|
||||
|
||||
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
|
||||
@@ -60,11 +72,13 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
|
||||
dbi.registerContainerFactory(new OptionalContainerFactory());
|
||||
|
||||
Accounts accounts = dbi.onDemand(Accounts.class);
|
||||
JedisPool cacheClient = new RedisClientFactory(config.getCacheConfiguration().getUrl()).getRedisClientPool();
|
||||
JedisPool redisClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
||||
JedisPool cacheClient = new RedisClientFactory(configuration.getCacheConfiguration().getUrl()).getRedisClientPool();
|
||||
JedisPool redisClient = new RedisClientFactory(configuration.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
|
||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||
FederatedClientManager federatedClientManager = new FederatedClientManager(environment,
|
||||
configuration.getJerseyClientConfiguration(),
|
||||
configuration.getFederationConfiguration());
|
||||
|
||||
DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory);
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -73,7 +72,7 @@ public class DirectoryUpdater {
|
||||
for (Account account : accounts) {
|
||||
if (account.isActive()) {
|
||||
byte[] token = Util.getContactToken(account.getNumber());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
||||
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported());
|
||||
|
||||
directory.add(batchOperation, clientContact);
|
||||
contactsAdded++;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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.Messages;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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 TrimMessagesCommand extends ConfiguredCommand<WhisperServerConfiguration> {
|
||||
private final Logger logger = LoggerFactory.getLogger(VacuumCommand.class);
|
||||
|
||||
public TrimMessagesCommand() {
|
||||
super("trim", "Trim Messages Database");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(Bootstrap<WhisperServerConfiguration> bootstrap,
|
||||
Namespace namespace,
|
||||
WhisperServerConfiguration config)
|
||||
throws Exception
|
||||
{
|
||||
DataSourceFactory messageDbConfig = config.getMessageStoreConfiguration();
|
||||
DBI messageDbi = new DBI(messageDbConfig.getUrl(), messageDbConfig.getUser(), messageDbConfig.getPassword());
|
||||
|
||||
messageDbi.registerArgumentFactory(new OptionalArgumentFactory(messageDbConfig.getDriverClass()));
|
||||
messageDbi.registerContainerFactory(new ImmutableListContainerFactory());
|
||||
messageDbi.registerContainerFactory(new ImmutableSetContainerFactory());
|
||||
messageDbi.registerContainerFactory(new OptionalContainerFactory());
|
||||
|
||||
Messages messages = messageDbi.onDemand(Messages.class);
|
||||
long timestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60);
|
||||
|
||||
logger.info("Trimming old messages: " + timestamp + "...");
|
||||
messages.removeOld(timestamp);
|
||||
|
||||
Thread.sleep(3000);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import io.dropwizard.setup.Bootstrap;
|
||||
|
||||
public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DirectoryCommand.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(VacuumCommand.class);
|
||||
|
||||
public VacuumCommand() {
|
||||
super("vacuum", "Vacuum Postgres Tables");
|
||||
@@ -51,18 +51,18 @@ public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration>
|
||||
Accounts accounts = dbi.onDemand(Accounts.class );
|
||||
Keys keys = dbi.onDemand(Keys.class );
|
||||
PendingAccounts pendingAccounts = dbi.onDemand(PendingAccounts.class);
|
||||
Messages messages = dbi.onDemand(Messages.class );
|
||||
Messages messages = messageDbi.onDemand(Messages.class );
|
||||
|
||||
logger.warn("Vacuuming accounts...");
|
||||
logger.info("Vacuuming accounts...");
|
||||
accounts.vacuum();
|
||||
|
||||
logger.warn("Vacuuming pending_accounts...");
|
||||
logger.info("Vacuuming pending_accounts...");
|
||||
pendingAccounts.vacuum();
|
||||
|
||||
logger.warn("Vacuuming keys...");
|
||||
logger.info("Vacuuming keys...");
|
||||
keys.vacuum();
|
||||
|
||||
logger.warn("Vacuuming messages...");
|
||||
logger.info("Vacuuming messages...");
|
||||
messages.vacuum();
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
@@ -57,4 +57,22 @@
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="2" author="moxie">
|
||||
<addColumn tableName="messages">
|
||||
<column name="content" type="bytea"/>
|
||||
</addColumn>
|
||||
|
||||
<dropNotNullConstraint tableName="messages" columnName="message"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="3" author="moxie">
|
||||
<sql>CREATE RULE bounded_message_queue AS ON INSERT TO messages DO ALSO DELETE FROM messages WHERE id IN (SELECT id FROM messages WHERE destination = NEW.destination AND destination_device = NEW.destination_device ORDER BY timestamp DESC OFFSET 5000);</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="4" author="moxie">
|
||||
<sql>DROP RULE bounded_message_queue ON messages;</sql>
|
||||
<sql>CREATE RULE bounded_message_queue AS ON INSERT TO messages DO ALSO DELETE FROM messages WHERE id IN (SELECT id FROM messages WHERE destination = NEW.destination AND destination_device = NEW.destination_device ORDER BY timestamp DESC OFFSET 1000);</sql>
|
||||
</changeSet>
|
||||
|
||||
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.whispersystems.dispatch;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExternalResource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
|
||||
import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
import org.whispersystems.dispatch.redis.PubSubReply;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class DispatchManagerTest {
|
||||
|
||||
private PubSubConnection pubSubConnection;
|
||||
private RedisPubSubConnectionFactory socketFactory;
|
||||
private DispatchManager dispatchManager;
|
||||
private PubSubReplyInputStream pubSubReplyInputStream;
|
||||
|
||||
@Rule
|
||||
public ExternalResource resource = new ExternalResource() {
|
||||
@Override
|
||||
protected void before() throws Throwable {
|
||||
pubSubConnection = mock(PubSubConnection.class );
|
||||
socketFactory = mock(RedisPubSubConnectionFactory.class);
|
||||
pubSubReplyInputStream = new PubSubReplyInputStream();
|
||||
|
||||
when(socketFactory.connect()).thenReturn(pubSubConnection);
|
||||
when(pubSubConnection.read()).thenAnswer(new Answer<PubSubReply>() {
|
||||
@Override
|
||||
public PubSubReply answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
return pubSubReplyInputStream.read();
|
||||
}
|
||||
});
|
||||
|
||||
dispatchManager = new DispatchManager(socketFactory, Optional.<DispatchChannel>absent());
|
||||
dispatchManager.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testConnect() {
|
||||
verify(socketFactory).connect();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubscribe() throws IOException {
|
||||
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
|
||||
dispatchManager.subscribe("foo", dispatchChannel);
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
|
||||
verify(dispatchChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubscribeUnsubscribe() throws IOException {
|
||||
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
|
||||
dispatchManager.subscribe("foo", dispatchChannel);
|
||||
dispatchManager.unsubscribe("foo", dispatchChannel);
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
|
||||
verify(dispatchChannel, timeout(1000)).onDispatchUnsubscribed(eq("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessages() throws IOException {
|
||||
DispatchChannel fooChannel = mock(DispatchChannel.class);
|
||||
DispatchChannel barChannel = mock(DispatchChannel.class);
|
||||
|
||||
dispatchManager.subscribe("foo", fooChannel);
|
||||
dispatchManager.subscribe("bar", barChannel);
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "bar", Optional.<byte[]>absent()));
|
||||
|
||||
verify(fooChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
|
||||
verify(barChannel, timeout(1000)).onDispatchSubscribed(eq("bar"));
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.MESSAGE, "foo", Optional.of("hello".getBytes())));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.MESSAGE, "bar", Optional.of("there".getBytes())));
|
||||
|
||||
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
|
||||
verify(fooChannel, timeout(1000)).onDispatchMessage(eq("foo"), captor.capture());
|
||||
|
||||
assertArrayEquals("hello".getBytes(), captor.getValue());
|
||||
|
||||
verify(barChannel, timeout(1000)).onDispatchMessage(eq("bar"), captor.capture());
|
||||
|
||||
assertArrayEquals("there".getBytes(), captor.getValue());
|
||||
}
|
||||
|
||||
private static class PubSubReplyInputStream {
|
||||
|
||||
private final List<PubSubReply> pubSubReplyList = new LinkedList<>();
|
||||
|
||||
public synchronized PubSubReply read() {
|
||||
try {
|
||||
while (pubSubReplyList.isEmpty()) wait();
|
||||
return pubSubReplyList.remove(0);
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void write(PubSubReply pubSubReply) {
|
||||
pubSubReplyList.add(pubSubReply);
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class PubSubConnectionTest {
|
||||
|
||||
private static final String REPLY = "*3\r\n" +
|
||||
"$9\r\n" +
|
||||
"subscribe\r\n" +
|
||||
"$5\r\n" +
|
||||
"abcde\r\n" +
|
||||
":1\r\n" +
|
||||
"*3\r\n" +
|
||||
"$9\r\n" +
|
||||
"subscribe\r\n" +
|
||||
"$5\r\n" +
|
||||
"fghij\r\n" +
|
||||
":2\r\n" +
|
||||
"*3\r\n" +
|
||||
"$9\r\n" +
|
||||
"subscribe\r\n" +
|
||||
"$5\r\n" +
|
||||
"klmno\r\n" +
|
||||
":2\r\n" +
|
||||
"*3\r\n" +
|
||||
"$7\r\n" +
|
||||
"message\r\n" +
|
||||
"$5\r\n" +
|
||||
"abcde\r\n" +
|
||||
"$10\r\n" +
|
||||
"1234567890\r\n" +
|
||||
"*3\r\n" +
|
||||
"$7\r\n" +
|
||||
"message\r\n" +
|
||||
"$5\r\n" +
|
||||
"klmno\r\n" +
|
||||
"$10\r\n" +
|
||||
"0987654321\r\n";
|
||||
|
||||
|
||||
@Test
|
||||
public void testSubscribe() throws IOException {
|
||||
// ByteChannel byteChannel = mock(ByteChannel.class);
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
PubSubConnection connection = new PubSubConnection(socket);
|
||||
|
||||
connection.subscribe("foobar");
|
||||
|
||||
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
|
||||
verify(outputStream).write(captor.capture());
|
||||
|
||||
assertArrayEquals(captor.getValue(), "SUBSCRIBE foobar\r\n".getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsubscribe() throws IOException {
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
PubSubConnection connection = new PubSubConnection(socket);
|
||||
|
||||
connection.unsubscribe("bazbar");
|
||||
|
||||
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
|
||||
verify(outputStream).write(captor.capture());
|
||||
|
||||
assertArrayEquals(captor.getValue(), "UNSUBSCRIBE bazbar\r\n".getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTricklyResponse() throws Exception {
|
||||
InputStream inputStream = mockInputStreamFor(new TrickleInputStream(REPLY.getBytes()));
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
when(socket.getInputStream()).thenReturn(inputStream);
|
||||
|
||||
PubSubConnection pubSubConnection = new PubSubConnection(socket);
|
||||
readResponses(pubSubConnection);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullResponse() throws Exception {
|
||||
InputStream inputStream = mockInputStreamFor(new FullInputStream(REPLY.getBytes()));
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
when(socket.getInputStream()).thenReturn(inputStream);
|
||||
|
||||
PubSubConnection pubSubConnection = new PubSubConnection(socket);
|
||||
readResponses(pubSubConnection);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRandomLengthResponse() throws Exception {
|
||||
InputStream inputStream = mockInputStreamFor(new RandomInputStream(REPLY.getBytes()));
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
when(socket.getInputStream()).thenReturn(inputStream);
|
||||
|
||||
PubSubConnection pubSubConnection = new PubSubConnection(socket);
|
||||
readResponses(pubSubConnection);
|
||||
}
|
||||
|
||||
private InputStream mockInputStreamFor(final MockInputStream stub) throws IOException {
|
||||
InputStream result = mock(InputStream.class);
|
||||
|
||||
when(result.read()).thenAnswer(new Answer<Integer>() {
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
return stub.read();
|
||||
}
|
||||
});
|
||||
|
||||
when(result.read(any(byte[].class))).thenAnswer(new Answer<Integer>() {
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
byte[] buffer = (byte[])invocationOnMock.getArguments()[0];
|
||||
return stub.read(buffer, 0, buffer.length);
|
||||
}
|
||||
});
|
||||
|
||||
when(result.read(any(byte[].class), anyInt(), anyInt())).thenAnswer(new Answer<Integer>() {
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
byte[] buffer = (byte[]) invocationOnMock.getArguments()[0];
|
||||
int offset = (int) invocationOnMock.getArguments()[1];
|
||||
int length = (int) invocationOnMock.getArguments()[2];
|
||||
|
||||
return stub.read(buffer, offset, length);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void readResponses(PubSubConnection pubSubConnection) throws Exception {
|
||||
PubSubReply reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
|
||||
assertEquals(reply.getChannel(), "abcde");
|
||||
assertFalse(reply.getContent().isPresent());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
|
||||
assertEquals(reply.getChannel(), "fghij");
|
||||
assertFalse(reply.getContent().isPresent());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
|
||||
assertEquals(reply.getChannel(), "klmno");
|
||||
assertFalse(reply.getContent().isPresent());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.MESSAGE);
|
||||
assertEquals(reply.getChannel(), "abcde");
|
||||
assertArrayEquals(reply.getContent().get(), "1234567890".getBytes());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.MESSAGE);
|
||||
assertEquals(reply.getChannel(), "klmno");
|
||||
assertArrayEquals(reply.getContent().get(), "0987654321".getBytes());
|
||||
}
|
||||
|
||||
private interface MockInputStream {
|
||||
public int read();
|
||||
public int read(byte[] input, int offset, int length);
|
||||
}
|
||||
|
||||
private static class TrickleInputStream implements MockInputStream {
|
||||
|
||||
private final byte[] data;
|
||||
private int index = 0;
|
||||
|
||||
private TrickleInputStream(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int read() {
|
||||
return data[index++];
|
||||
}
|
||||
|
||||
public int read(byte[] input, int offset, int length) {
|
||||
input[offset] = data[index++];
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class FullInputStream implements MockInputStream {
|
||||
|
||||
private final byte[] data;
|
||||
private int index = 0;
|
||||
|
||||
private FullInputStream(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int read() {
|
||||
return data[index++];
|
||||
}
|
||||
|
||||
public int read(byte[] input, int offset, int length) {
|
||||
int amount = Math.min(data.length - index, length);
|
||||
System.arraycopy(data, index, input, offset, amount);
|
||||
index += length;
|
||||
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RandomInputStream implements MockInputStream {
|
||||
private final byte[] data;
|
||||
private int index = 0;
|
||||
|
||||
private RandomInputStream(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int read() {
|
||||
return data[index++];
|
||||
}
|
||||
|
||||
public int read(byte[] input, int offset, int length) {
|
||||
try {
|
||||
int maxCopy = Math.min(data.length - index, length);
|
||||
int randomCopy = SecureRandom.getInstance("SHA1PRNG").nextInt(maxCopy) + 1;
|
||||
int copyAmount = Math.min(maxCopy, randomCopy);
|
||||
|
||||
System.arraycopy(data, index, input, offset, copyAmount);
|
||||
index += copyAmount;
|
||||
|
||||
return copyAmount;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ArrayReplyHeaderTest {
|
||||
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testNull() throws IOException {
|
||||
new ArrayReplyHeader(null);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadPrefix() throws IOException {
|
||||
new ArrayReplyHeader(":3");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testEmpty() throws IOException {
|
||||
new ArrayReplyHeader("");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testTruncated() throws IOException {
|
||||
new ArrayReplyHeader("*");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadNumber() throws IOException {
|
||||
new ArrayReplyHeader("*ABC");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValid() throws IOException {
|
||||
assertEquals(4, new ArrayReplyHeader("*4").getElementCount());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IntReplyHeaderTest {
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testNull() throws IOException {
|
||||
new IntReply(null);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testEmpty() throws IOException {
|
||||
new IntReply("");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadNumber() throws IOException {
|
||||
new IntReply(":A");
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadFormat() throws IOException {
|
||||
new IntReply("*");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValid() throws IOException {
|
||||
assertEquals(23, new IntReply(":23").getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class StringReplyHeaderTest {
|
||||
|
||||
@Test
|
||||
public void testNull() {
|
||||
try {
|
||||
new StringReplyHeader(null);
|
||||
throw new AssertionError();
|
||||
} catch (IOException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadNumber() {
|
||||
try {
|
||||
new StringReplyHeader("$100A");
|
||||
throw new AssertionError();
|
||||
} catch (IOException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadPrefix() {
|
||||
try {
|
||||
new StringReplyHeader("*");
|
||||
throw new AssertionError();
|
||||
} catch (IOException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValid() throws IOException {
|
||||
assertEquals(1000, new StringReplyHeader("$1000").getStringLength());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
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.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
@@ -17,13 +18,16 @@ 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 org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.HashMap;
|
||||
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -42,14 +46,18 @@ public class AccountControllerTest {
|
||||
|
||||
@Rule
|
||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||
.addProvider(AuthHelper.getAuthenticator())
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder())
|
||||
.setMapper(SystemMapper.getMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new AccountController(pendingAccountsManager,
|
||||
accountsManager,
|
||||
rateLimiters,
|
||||
smsSender,
|
||||
storedMessages,
|
||||
timeProvider,
|
||||
Optional.of(authorizationKey)))
|
||||
Optional.of(authorizationKey),
|
||||
new HashMap<String, Integer>()))
|
||||
.build();
|
||||
|
||||
|
||||
@@ -66,23 +74,40 @@ public class AccountControllerTest {
|
||||
|
||||
@Test
|
||||
public void testSendCode() throws Exception {
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/sms/code/%s", SENDER))
|
||||
.get(ClientResponse.class);
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
|
||||
.request()
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), anyString());
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.<String>absent()), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendiOSCode() throws Exception {
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/sms/code/%s", SENDER))
|
||||
.queryParam("client", "ios")
|
||||
.request()
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("ios")), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVerifyCode() throws Exception {
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/code/%s", "1234"))
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 2222))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/code/%s", "1234"))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
@@ -91,12 +116,13 @@ public class AccountControllerTest {
|
||||
|
||||
@Test
|
||||
public void testVerifyBadCode() throws Exception {
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/code/%s", "1111"))
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 3333))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/code/%s", "1111"))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
@@ -109,12 +135,13 @@ public class AccountControllerTest {
|
||||
|
||||
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);
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/token/%s", token))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
|
||||
@@ -127,12 +154,13 @@ public class AccountControllerTest {
|
||||
|
||||
String token = SENDER + ":1415906574:af4f046107c21721224a";
|
||||
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/token/%s", token))
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/token/%s", token))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
@@ -145,12 +173,13 @@ public class AccountControllerTest {
|
||||
|
||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
||||
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/token/%s", token))
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/token/%s", token))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
@@ -163,12 +192,13 @@ public class AccountControllerTest {
|
||||
|
||||
String token = SENDER + ":1415906573:af4f046107c21721224a";
|
||||
|
||||
ClientResponse response =
|
||||
resources.client().resource(String.format("/v1/accounts/token/%s", token))
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/accounts/token/%s", token))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar"))
|
||||
.entity(new AccountAttributes("keykeykeykey", false, false, 4444))
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.put(ClientResponse.class);
|
||||
.put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444),
|
||||
MediaType.APPLICATION_JSON_TYPE));
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user