mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-17 02:10:36 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
887f49760f | ||
|
|
be77f2291b | ||
|
|
b585b849a1 | ||
|
|
0c94e3d994 | ||
|
|
4a93658d0f | ||
|
|
6da19c6254 | ||
|
|
289058be81 | ||
|
|
864675ecde | ||
|
|
c79d7e3e30 | ||
|
|
549cc6f492 | ||
|
|
aa84ab66af | ||
|
|
1fef812c67 | ||
|
|
9170f74887 | ||
|
|
c9bd700d31 | ||
|
|
0928e4c035 | ||
|
|
75aec0a8d4 | ||
|
|
1f5ee36a6b | ||
|
|
45a0b74b89 | ||
|
|
f7132bdbbc | ||
|
|
d2dbff173a | ||
|
|
79f83babb3 | ||
|
|
715181f830 |
@@ -1,32 +1,16 @@
|
|||||||
twilio:
|
twilio: # Twilio SMS gateway configuration
|
||||||
accountId:
|
accountId:
|
||||||
accountToken:
|
accountToken:
|
||||||
number:
|
number:
|
||||||
localDomain: # The domain Twilio can call back to.
|
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
|
push: # GCM/APN push server configuration
|
||||||
# voice verification if twilio.international is false. Otherwise,
|
host:
|
||||||
# Nexmo, if specified, Nexmo will only be used as a fallback
|
port:
|
||||||
# for failed Twilio deliveries.
|
username:
|
||||||
nexmo:
|
password:
|
||||||
apiKey:
|
|
||||||
apiSecret:
|
|
||||||
number:
|
|
||||||
|
|
||||||
gcm:
|
s3: # AWS S3 configuration
|
||||||
senderId:
|
|
||||||
apiKey:
|
|
||||||
|
|
||||||
# Optional. Only if iOS clients are supported.
|
|
||||||
apn:
|
|
||||||
# In PEM format.
|
|
||||||
certificate:
|
|
||||||
|
|
||||||
# In PEM format.
|
|
||||||
key:
|
|
||||||
|
|
||||||
s3:
|
|
||||||
accessKey:
|
accessKey:
|
||||||
accessSecret:
|
accessSecret:
|
||||||
|
|
||||||
@@ -35,13 +19,37 @@ s3:
|
|||||||
# correct permissions.
|
# correct permissions.
|
||||||
attachmentsBucket:
|
attachmentsBucket:
|
||||||
|
|
||||||
memcache:
|
directory: # Redis server configuration for TS directory
|
||||||
servers:
|
url:
|
||||||
user:
|
|
||||||
password:
|
|
||||||
|
|
||||||
redis:
|
cache: # Redis server configuration for general purpose caching
|
||||||
url:
|
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:
|
federation:
|
||||||
name:
|
name:
|
||||||
@@ -52,24 +60,3 @@ federation:
|
|||||||
authenticationToken: foo
|
authenticationToken: foo
|
||||||
certificate: in pem format
|
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
|
|
||||||
|
|||||||
17
pom.xml
17
pom.xml
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<groupId>org.whispersystems.textsecure</groupId>
|
<groupId>org.whispersystems.textsecure</groupId>
|
||||||
<artifactId>TextSecureServer</artifactId>
|
<artifactId>TextSecureServer</artifactId>
|
||||||
<version>0.29</version>
|
<version>0.50</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<dropwizard.version>0.7.1</dropwizard.version>
|
<dropwizard.version>0.7.1</dropwizard.version>
|
||||||
@@ -53,11 +53,6 @@
|
|||||||
<artifactId>dropwizard-metrics-graphite</artifactId>
|
<artifactId>dropwizard-metrics-graphite</artifactId>
|
||||||
<version>${dropwizard.version}</version>
|
<version>${dropwizard.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>io.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-client</artifactId>
|
|
||||||
<version>${dropwizard.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.dcsquare</groupId>
|
<groupId>com.dcsquare</groupId>
|
||||||
<artifactId>dropwizard-papertrail</artifactId>
|
<artifactId>dropwizard-papertrail</artifactId>
|
||||||
@@ -91,11 +86,6 @@
|
|||||||
<artifactId>gcm-server</artifactId>
|
<artifactId>gcm-server</artifactId>
|
||||||
<version>1.0.2</version>
|
<version>1.0.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>net.spy</groupId>
|
|
||||||
<artifactId>spymemcached</artifactId>
|
|
||||||
<version>2.10.1</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.notnoop.apns</groupId>
|
<groupId>com.notnoop.apns</groupId>
|
||||||
<artifactId>apns</artifactId>
|
<artifactId>apns</artifactId>
|
||||||
@@ -116,7 +106,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>2.6.1</version>
|
<version>2.6.2</version>
|
||||||
<type>jar</type>
|
<type>jar</type>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@@ -139,10 +129,9 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.whispersystems</groupId>
|
<groupId>org.whispersystems</groupId>
|
||||||
<artifactId>websocket-resources</artifactId>
|
<artifactId>websocket-resources</artifactId>
|
||||||
<version>0.2.0</version>
|
<version>0.2.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
|
|
||||||
all:
|
all:
|
||||||
protoc --java_out=../src/main/java/ OutgoingMessageSignal.proto PubSubMessage.proto StoredMessage.proto
|
protoc --java_out=../src/main/java/ OutgoingMessageSignal.proto PubSubMessage.proto
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
* Copyright (C) 2013 - 2015 Open WhisperSystems
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
@@ -25,7 +25,7 @@ message OutgoingMessageSignal {
|
|||||||
CIPHERTEXT = 1;
|
CIPHERTEXT = 1;
|
||||||
KEY_EXCHANGE = 2;
|
KEY_EXCHANGE = 2;
|
||||||
PREKEY_BUNDLE = 3;
|
PREKEY_BUNDLE = 3;
|
||||||
PLAINTEXT = 4;
|
// PLAINTEXT = 4;
|
||||||
RECEIPT = 5;
|
RECEIPT = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,3 +37,7 @@ message OutgoingMessageSignal {
|
|||||||
optional uint64 timestamp = 5;
|
optional uint64 timestamp = 5;
|
||||||
optional bytes message = 6;
|
optional bytes message = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ProvisioningUuid {
|
||||||
|
optional string uuid = 1;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ message PubSubMessage {
|
|||||||
QUERY_DB = 1;
|
QUERY_DB = 1;
|
||||||
DELIVER = 2;
|
DELIVER = 2;
|
||||||
KEEPALIVE = 3;
|
KEEPALIVE = 3;
|
||||||
|
CLOSE = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Type type = 1;
|
optional Type type = 1;
|
||||||
|
|||||||
@@ -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.warn("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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,22 +17,26 @@
|
|||||||
package org.whispersystems.textsecuregcm;
|
package org.whispersystems.textsecuregcm;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.MessageStoreConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotNull;
|
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.Configuration;
|
||||||
import io.dropwizard.client.JerseyClientConfiguration;
|
import io.dropwizard.client.JerseyClientConfiguration;
|
||||||
import io.dropwizard.db.DataSourceFactory;
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
@@ -60,17 +64,22 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private MemcacheConfiguration memcache;
|
private RedisConfiguration cache;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private DirectoryConfiguration directory;
|
private RedisConfiguration directory;
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Valid
|
@Valid
|
||||||
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private MessageStoreConfiguration messageStore;
|
private DataSourceFactory messageStore;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@@ -127,15 +136,15 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
return s3;
|
return s3;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MemcacheConfiguration getMemcacheConfiguration() {
|
public RedisConfiguration getCacheConfiguration() {
|
||||||
return memcache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DirectoryConfiguration getDirectoryConfiguration() {
|
public RedisConfiguration getDirectoryConfiguration() {
|
||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageStoreConfiguration getMessageStoreConfiguration() {
|
public DataSourceFactory getMessageStoreConfiguration() {
|
||||||
return messageStore;
|
return messageStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,4 +167,15 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
public RedPhoneConfiguration getRedphoneConfiguration() {
|
public RedPhoneConfiguration getRedphoneConfiguration() {
|
||||||
return redphone;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ import com.codahale.metrics.graphite.GraphiteReporter;
|
|||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.sun.jersey.api.client.Client;
|
import com.sun.jersey.api.client.Client;
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||||
import org.skife.jdbi.v2.DBI;
|
import org.skife.jdbi.v2.DBI;
|
||||||
|
import org.whispersystems.dispatch.DispatchChannel;
|
||||||
|
import org.whispersystems.dispatch.DispatchManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
|
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
|
import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider;
|
||||||
@@ -39,25 +40,26 @@ import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
|||||||
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
|
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
|
||||||
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
|
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
|
||||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
|
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
|
||||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
||||||
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
|
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
|
||||||
import org.whispersystems.textsecuregcm.metrics.JsonMetricsReporter;
|
|
||||||
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
|
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
|
||||||
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
||||||
import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
|
|
||||||
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||||
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
||||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||||
import org.whispersystems.textsecuregcm.push.FeedbackHandler;
|
import org.whispersystems.textsecuregcm.push.FeedbackHandler;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
import org.whispersystems.textsecuregcm.push.PushServiceClient;
|
import org.whispersystems.textsecuregcm.push.PushServiceClient;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
|
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
@@ -67,15 +69,18 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Messages;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingDevices;
|
import org.whispersystems.textsecuregcm.storage.PendingDevices;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Constants;
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||||
import org.whispersystems.textsecuregcm.websocket.ConnectListener;
|
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.websocket.WebSocketAccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||||
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
|
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
|
||||||
@@ -95,7 +100,6 @@ import io.dropwizard.client.JerseyClientBuilder;
|
|||||||
import io.dropwizard.db.DataSourceFactory;
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
import io.dropwizard.jdbi.DBIFactory;
|
import io.dropwizard.jdbi.DBIFactory;
|
||||||
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
|
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
|
||||||
import io.dropwizard.migrations.MigrationsBundle;
|
|
||||||
import io.dropwizard.setup.Bootstrap;
|
import io.dropwizard.setup.Bootstrap;
|
||||||
import io.dropwizard.setup.Environment;
|
import io.dropwizard.setup.Environment;
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
@@ -110,12 +114,19 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||||||
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||||
bootstrap.addCommand(new DirectoryCommand());
|
bootstrap.addCommand(new DirectoryCommand());
|
||||||
bootstrap.addCommand(new VacuumCommand());
|
bootstrap.addCommand(new VacuumCommand());
|
||||||
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
|
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
|
||||||
@Override
|
@Override
|
||||||
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||||
return configuration.getDataSourceFactory();
|
return configuration.getDataSourceFactory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("messagedb", "messagedb.xml") {
|
||||||
|
@Override
|
||||||
|
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||||
|
return configuration.getMessageStoreConfiguration();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -131,74 +142,95 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||||||
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
DBIFactory dbiFactory = new DBIFactory();
|
DBIFactory dbiFactory = new DBIFactory();
|
||||||
DBI jdbi = dbiFactory.build(environment, config.getDataSourceFactory(), "postgresql");
|
DBI database = dbiFactory.build(environment, config.getDataSourceFactory(), "accountdb");
|
||||||
|
DBI messagedb = dbiFactory.build(environment, config.getMessageStoreConfiguration(), "messagedb");
|
||||||
|
|
||||||
Accounts accounts = jdbi.onDemand(Accounts.class);
|
Accounts accounts = database.onDemand(Accounts.class);
|
||||||
PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class);
|
PendingAccounts pendingAccounts = database.onDemand(PendingAccounts.class);
|
||||||
PendingDevices pendingDevices = jdbi.onDemand(PendingDevices.class);
|
PendingDevices pendingDevices = database.onDemand(PendingDevices.class);
|
||||||
Keys keys = jdbi.onDemand(Keys.class);
|
Keys keys = database.onDemand(Keys.class);
|
||||||
|
Messages messages = messagedb.onDemand(Messages.class);
|
||||||
|
|
||||||
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
|
RedisClientFactory cacheClientFactory = new RedisClientFactory(config.getCacheConfiguration().getUrl());
|
||||||
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
JedisPool cacheClient = cacheClientFactory.getRedisClientPool();
|
||||||
JedisPool messageStoreClient = new RedisClientFactory(config.getMessageStoreConfiguration().getUrl()).getRedisClientPool();
|
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
||||||
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
|
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
|
||||||
.build(getName());
|
.build(getName());
|
||||||
|
|
||||||
DirectoryManager directory = new DirectoryManager(directoryClient);
|
DirectoryManager directory = new DirectoryManager(directoryClient);
|
||||||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
|
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
|
||||||
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, memcachedClient );
|
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient);
|
||||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
|
||||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||||
StoredMessages storedMessages = new StoredMessages(messageStoreClient);
|
MessagesManager messagesManager = new MessagesManager(messages);
|
||||||
PubSubManager pubSubManager = new PubSubManager(messageStoreClient);
|
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());
|
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
|
||||||
WebsocketSender websocketSender = new WebsocketSender(storedMessages, pubSubManager);
|
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
|
||||||
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
|
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
|
||||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
|
||||||
|
|
||||||
|
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient);
|
||||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||||
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
||||||
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
|
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
|
||||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||||
PushSender pushSender = new PushSender(pushServiceClient, websocketSender);
|
PushSender pushSender = new PushSender(apnFallbackManager, pushServiceClient, websocketSender);
|
||||||
|
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
||||||
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
|
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
|
||||||
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
|
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
|
||||||
|
|
||||||
|
environment.lifecycle().manage(apnFallbackManager);
|
||||||
|
environment.lifecycle().manage(pubSubManager);
|
||||||
environment.lifecycle().manage(feedbackHandler);
|
environment.lifecycle().manage(feedbackHandler);
|
||||||
|
|
||||||
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
||||||
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
|
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||||
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
|
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||||
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()),
|
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||||
FederatedPeer.class,
|
FederatedPeer.class,
|
||||||
deviceAuthenticator,
|
deviceAuthenticator,
|
||||||
Device.class, "WhisperServer"));
|
Device.class, "WhisperServer"));
|
||||||
|
|
||||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, storedMessages, new TimeProvider(), authorizationKey));
|
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey, config.getTestDevices()));
|
||||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
|
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
|
||||||
environment.jersey().register(new DirectoryController(rateLimiters, directory));
|
environment.jersey().register(new DirectoryController(rateLimiters, directory));
|
||||||
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
|
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
|
||||||
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
|
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(attachmentController);
|
||||||
environment.jersey().register(keysControllerV1);
|
environment.jersey().register(keysControllerV1);
|
||||||
environment.jersey().register(keysControllerV2);
|
environment.jersey().register(keysControllerV2);
|
||||||
environment.jersey().register(messageController);
|
environment.jersey().register(messageController);
|
||||||
|
|
||||||
if (config.getWebsocketConfiguration().isEnabled()) {
|
if (config.getWebsocketConfiguration().isEnabled()) {
|
||||||
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config);
|
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config, 90000);
|
||||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
|
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
|
||||||
webSocketEnvironment.setConnectListener(new ConnectListener(accountsManager, pushSender, storedMessages, pubSubManager));
|
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, messagesManager, pubSubManager, apnFallbackManager));
|
||||||
webSocketEnvironment.jersey().register(new KeepAliveController());
|
webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager));
|
||||||
|
|
||||||
WebSocketResourceProviderFactory servlet = new WebSocketResourceProviderFactory(webSocketEnvironment);
|
|
||||||
|
|
||||||
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", servlet);
|
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config);
|
||||||
websocket.addMapping("/v1/websocket/*");
|
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
|
||||||
|
provisioningEnvironment.jersey().register(new KeepAliveController(pubSubManager));
|
||||||
|
|
||||||
|
WebSocketResourceProviderFactory webSocketServlet = new WebSocketResourceProviderFactory(webSocketEnvironment );
|
||||||
|
WebSocketResourceProviderFactory provisioningServlet = new WebSocketResourceProviderFactory(provisioningEnvironment);
|
||||||
|
|
||||||
|
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet );
|
||||||
|
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
|
||||||
|
|
||||||
|
websocket.addMapping("/v1/websocket/");
|
||||||
websocket.setAsyncSupported(true);
|
websocket.setAsyncSupported(true);
|
||||||
servlet.start();
|
|
||||||
|
provisioning.addMapping("/v1/websocket/provisioning/");
|
||||||
|
provisioning.setAsyncSupported(true);
|
||||||
|
|
||||||
|
webSocketServlet.start();
|
||||||
|
provisioningServlet.start();
|
||||||
|
|
||||||
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
|
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
|
||||||
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
|
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
|
||||||
@@ -210,8 +242,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
|
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
|
||||||
environment.healthChecks().register("messagestore", new RedisHealthCheck(messageStoreClient));
|
environment.healthChecks().register("cache", new RedisHealthCheck(cacheClient));
|
||||||
environment.healthChecks().register("memcache", new MemcacheHealthCheck(memcachedClient));
|
|
||||||
|
|
||||||
environment.jersey().register(new IOExceptionMapper());
|
environment.jersey().register(new IOExceptionMapper());
|
||||||
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||||
@@ -242,5 +273,4 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
new WhisperServerService().run(args);
|
new WhisperServerService().run(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +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;
|
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
|
||||||
|
|
||||||
public class MemcacheConfiguration {
|
|
||||||
|
|
||||||
@NotEmpty
|
|
||||||
@JsonProperty
|
|
||||||
private String servers;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private String user;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
|
|
||||||
public String getServers() {
|
|
||||||
return servers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUser() {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
import org.hibernate.validator.constraints.URL;
|
import org.hibernate.validator.constraints.URL;
|
||||||
|
|
||||||
public class DirectoryConfiguration {
|
public class RedisConfiguration {
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,11 +23,11 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
|
||||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
|
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
@@ -35,11 +35,10 @@ import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
|||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
@@ -57,6 +56,7 @@ import javax.ws.rs.core.Response;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
@@ -69,25 +69,28 @@ public class AccountController {
|
|||||||
private final AccountsManager accounts;
|
private final AccountsManager accounts;
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final SmsSender smsSender;
|
private final SmsSender smsSender;
|
||||||
private final StoredMessages storedMessages;
|
private final MessagesManager messagesManager;
|
||||||
private final TimeProvider timeProvider;
|
private final TimeProvider timeProvider;
|
||||||
private final Optional<byte[]> authorizationKey;
|
private final Optional<byte[]> authorizationKey;
|
||||||
|
private final Map<String, Integer> testDevices;
|
||||||
|
|
||||||
public AccountController(PendingAccountsManager pendingAccounts,
|
public AccountController(PendingAccountsManager pendingAccounts,
|
||||||
AccountsManager accounts,
|
AccountsManager accounts,
|
||||||
RateLimiters rateLimiters,
|
RateLimiters rateLimiters,
|
||||||
SmsSender smsSenderFactory,
|
SmsSender smsSenderFactory,
|
||||||
StoredMessages storedMessages,
|
MessagesManager messagesManager,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
Optional<byte[]> authorizationKey)
|
Optional<byte[]> authorizationKey,
|
||||||
|
Map<String, Integer> testDevices)
|
||||||
{
|
{
|
||||||
this.pendingAccounts = pendingAccounts;
|
this.pendingAccounts = pendingAccounts;
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.smsSender = smsSenderFactory;
|
this.smsSender = smsSenderFactory;
|
||||||
this.storedMessages = storedMessages;
|
this.messagesManager = messagesManager;
|
||||||
this.timeProvider = timeProvider;
|
this.timeProvider = timeProvider;
|
||||||
this.authorizationKey = authorizationKey;
|
this.authorizationKey = authorizationKey;
|
||||||
|
this.testDevices = testDevices;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@@ -113,10 +116,12 @@ public class AccountController {
|
|||||||
throw new WebApplicationException(Response.status(422).build());
|
throw new WebApplicationException(Response.status(422).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
VerificationCode verificationCode = generateVerificationCode();
|
VerificationCode verificationCode = generateVerificationCode(number);
|
||||||
pendingAccounts.store(number, verificationCode.getVerificationCode());
|
pendingAccounts.store(number, verificationCode.getVerificationCode());
|
||||||
|
|
||||||
if (transport.equals("sms")) {
|
if (testDevices.containsKey(number)) {
|
||||||
|
// noop
|
||||||
|
} else if (transport.equals("sms")) {
|
||||||
smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
|
smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
|
||||||
} else if (transport.equals("voice")) {
|
} else if (transport.equals("voice")) {
|
||||||
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
|
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
|
||||||
@@ -198,10 +203,15 @@ public class AccountController {
|
|||||||
@PUT
|
@PUT
|
||||||
@Path("/gcm/")
|
@Path("/gcm/")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
|
public void setGcmRegistrationId(@Auth Account account, @Valid GcmRegistrationId registrationId) {
|
||||||
Device device = account.getAuthenticatedDevice().get();
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
device.setApnId(null);
|
device.setApnId(null);
|
||||||
|
device.setVoipApnId(null);
|
||||||
device.setGcmId(registrationId.getGcmRegistrationId());
|
device.setGcmId(registrationId.getGcmRegistrationId());
|
||||||
|
|
||||||
|
if (registrationId.isWebSocketChannel()) device.setFetchesMessages(true);
|
||||||
|
else device.setFetchesMessages(false);
|
||||||
|
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +221,7 @@ public class AccountController {
|
|||||||
public void deleteGcmRegistrationId(@Auth Account account) {
|
public void deleteGcmRegistrationId(@Auth Account account) {
|
||||||
Device device = account.getAuthenticatedDevice().get();
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
device.setGcmId(null);
|
device.setGcmId(null);
|
||||||
|
device.setFetchesMessages(false);
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +232,9 @@ public class AccountController {
|
|||||||
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
|
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
|
||||||
Device device = account.getAuthenticatedDevice().get();
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
device.setApnId(registrationId.getApnRegistrationId());
|
device.setApnId(registrationId.getApnRegistrationId());
|
||||||
|
device.setVoipApnId(registrationId.getVoipRegistrationId());
|
||||||
device.setGcmId(null);
|
device.setGcmId(null);
|
||||||
|
device.setFetchesMessages(true);
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +244,25 @@ public class AccountController {
|
|||||||
public void deleteApnRegistrationId(@Auth Account account) {
|
public void deleteApnRegistrationId(@Auth Account account) {
|
||||||
Device device = account.getAuthenticatedDevice().get();
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
device.setApnId(null);
|
device.setApnId(null);
|
||||||
|
device.setFetchesMessages(false);
|
||||||
|
accounts.update(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Path("/wsc/")
|
||||||
|
public void setWebSocketChannelSupported(@Auth Account account) {
|
||||||
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
device.setFetchesMessages(true);
|
||||||
|
accounts.update(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@DELETE
|
||||||
|
@Path("/wsc/")
|
||||||
|
public void deleteWebSocketChannel(@Auth Account account) {
|
||||||
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
device.setFetchesMessages(false);
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,14 +289,18 @@ public class AccountController {
|
|||||||
account.addDevice(device);
|
account.addDevice(device);
|
||||||
|
|
||||||
accounts.create(account);
|
accounts.create(account);
|
||||||
storedMessages.clear(new WebsocketAddress(number, Device.MASTER_ID));
|
messagesManager.clear(number);
|
||||||
pendingAccounts.remove(number);
|
pendingAccounts.remove(number);
|
||||||
|
|
||||||
logger.debug("Stored device...");
|
logger.debug("Stored device...");
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
@VisibleForTesting protected VerificationCode generateVerificationCode(String number) {
|
||||||
try {
|
try {
|
||||||
|
if (testDevices.containsKey(number)) {
|
||||||
|
return new VerificationCode(testDevices.get(number));
|
||||||
|
}
|
||||||
|
|
||||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||||
int randomInt = 100000 + random.nextInt(900000);
|
int randomInt = 100000 + random.nextInt(900000);
|
||||||
return new VerificationCode(randomInt);
|
return new VerificationCode(randomInt);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
|||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
@@ -44,6 +45,7 @@ import javax.ws.rs.Produces;
|
|||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ public class DeviceController {
|
|||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Path("/provisioning_code")
|
@Path("/provisioning/code")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public VerificationCode createDeviceToken(@Auth Account account)
|
public VerificationCode createDeviceToken(@Auth Account account)
|
||||||
throws RateLimitExceededException
|
throws RateLimitExceededException
|
||||||
@@ -102,7 +104,7 @@ public class DeviceController {
|
|||||||
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
|
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
|
||||||
|
|
||||||
if (!storedVerificationCode.isPresent() ||
|
if (!storedVerificationCode.isPresent() ||
|
||||||
!verificationCode.equals(storedVerificationCode.get()))
|
!MessageDigest.isEqual(verificationCode.getBytes(), storedVerificationCode.get().getBytes()))
|
||||||
{
|
{
|
||||||
throw new WebApplicationException(Response.status(403).build());
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
}
|
}
|
||||||
@@ -118,6 +120,8 @@ public class DeviceController {
|
|||||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||||
device.setId(account.get().getNextDeviceId());
|
device.setId(account.get().getNextDeviceId());
|
||||||
|
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||||
|
device.setLastSeen(Util.todayInMillis());
|
||||||
|
|
||||||
account.get().addDevice(device);
|
account.get().addDevice(device);
|
||||||
accounts.update(account.get());
|
accounts.update(account.get());
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class DirectoryController {
|
|||||||
rateLimiters.getContactsLimiter().validate(account.getNumber());
|
rateLimiters.getContactsLimiter().validate(account.getNumber());
|
||||||
|
|
||||||
try {
|
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();
|
if (contact.isPresent()) return Response.ok().entity(contact.get()).build();
|
||||||
else return Response.status(404).build();
|
else return Response.status(404).build();
|
||||||
@@ -100,7 +100,7 @@ public class DirectoryController {
|
|||||||
List<byte[]> tokens = new LinkedList<>();
|
List<byte[]> tokens = new LinkedList<>();
|
||||||
|
|
||||||
for (String encodedContact : contacts.getContacts()) {
|
for (String encodedContact : contacts.getContacts()) {
|
||||||
tokens.add(Base64.decodeWithoutPadding(encodedContact));
|
tokens.add(decodeToken(encodedContact));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ClientContact> intersection = directory.get(tokens);
|
List<ClientContact> intersection = directory.get(tokens);
|
||||||
@@ -110,4 +110,8 @@ public class DirectoryController {
|
|||||||
throw new WebApplicationException(Response.status(400).build());
|
throw new WebApplicationException(Response.status(400).build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] decodeToken(String encoded) throws IOException {
|
||||||
|
return Base64.decodeWithoutPadding(encoded.replace('-', '+').replace('_', '/'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
public class InvalidDestinationException extends Exception {
|
||||||
|
public InvalidDestinationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,47 @@
|
|||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import com.codahale.metrics.annotation.Timed;
|
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.GET;
|
||||||
import javax.ws.rs.PUT;
|
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
|
||||||
@Path("/v1/keepalive")
|
@Path("/v1/keepalive")
|
||||||
public class KeepAliveController {
|
public class KeepAliveController {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(KeepAliveController.class);
|
||||||
|
|
||||||
|
private final PubSubManager pubSubManager;
|
||||||
|
|
||||||
|
public KeepAliveController(PubSubManager pubSubManager) {
|
||||||
|
this.pubSubManager = pubSubManager;
|
||||||
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
public Response getKeepAlive() {
|
public Response getKeepAlive(@Auth(required = false) 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();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
|||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageResponse;
|
import org.whispersystems.textsecuregcm.entities.MessageResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
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.entities.StaleDevices;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClient;
|
import org.whispersystems.textsecuregcm.federation.FederatedClient;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
@@ -33,14 +36,19 @@ import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
|||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.util.Base64;
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DELETE;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
@@ -64,17 +72,23 @@ public class MessageController {
|
|||||||
|
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final PushSender pushSender;
|
private final PushSender pushSender;
|
||||||
|
private final ReceiptSender receiptSender;
|
||||||
private final FederatedClientManager federatedClientManager;
|
private final FederatedClientManager federatedClientManager;
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
|
private final MessagesManager messagesManager;
|
||||||
|
|
||||||
public MessageController(RateLimiters rateLimiters,
|
public MessageController(RateLimiters rateLimiters,
|
||||||
PushSender pushSender,
|
PushSender pushSender,
|
||||||
|
ReceiptSender receiptSender,
|
||||||
AccountsManager accountsManager,
|
AccountsManager accountsManager,
|
||||||
|
MessagesManager messagesManager,
|
||||||
FederatedClientManager federatedClientManager)
|
FederatedClientManager federatedClientManager)
|
||||||
{
|
{
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.pushSender = pushSender;
|
this.pushSender = pushSender;
|
||||||
|
this.receiptSender = receiptSender;
|
||||||
this.accountsManager = accountsManager;
|
this.accountsManager = accountsManager;
|
||||||
|
this.messagesManager = messagesManager;
|
||||||
this.federatedClientManager = federatedClientManager;
|
this.federatedClientManager = federatedClientManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,16 +96,21 @@ public class MessageController {
|
|||||||
@Path("/{destination}")
|
@Path("/{destination}")
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public void sendMessage(@Auth Account source,
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@PathParam("destination") String destinationName,
|
public SendMessageResponse sendMessage(@Auth Account source,
|
||||||
@Valid IncomingMessageList messages)
|
@PathParam("destination") String destinationName,
|
||||||
|
@Valid IncomingMessageList messages)
|
||||||
throws IOException, RateLimitExceededException
|
throws IOException, RateLimitExceededException
|
||||||
{
|
{
|
||||||
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
rateLimiters.getMessagesLimiter().validate(source.getNumber() + "__" + destinationName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages);
|
boolean isSyncMessage = source.getNumber().equals(destinationName);
|
||||||
else sendRelayMessage(source, destinationName, messages);
|
|
||||||
|
if (Util.isEmpty(messages.getRelay())) sendLocalMessage(source, destinationName, messages, isSyncMessage);
|
||||||
|
else sendRelayMessage(source, destinationName, messages, isSyncMessage);
|
||||||
|
|
||||||
|
return new SendMessageResponse(!isSyncMessage && source.getActiveDeviceCount() > 1);
|
||||||
} catch (NoSuchUserException e) {
|
} catch (NoSuchUserException e) {
|
||||||
throw new WebApplicationException(Response.status(404).build());
|
throw new WebApplicationException(Response.status(404).build());
|
||||||
} catch (MismatchedDevicesException e) {
|
} catch (MismatchedDevicesException e) {
|
||||||
@@ -105,6 +124,8 @@ public class MessageController {
|
|||||||
.type(MediaType.APPLICATION_JSON)
|
.type(MediaType.APPLICATION_JSON)
|
||||||
.entity(new StaleDevices(e.getStaleDevices()))
|
.entity(new StaleDevices(e.getStaleDevices()))
|
||||||
.build());
|
.build());
|
||||||
|
} catch (InvalidDestinationException e) {
|
||||||
|
throw new WebApplicationException(Response.status(400).build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,14 +149,50 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public OutgoingMessageEntityList getPendingMessages(@Auth Account account) {
|
||||||
|
return new OutgoingMessageEntityList(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 {
|
||||||
|
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(), source, timestamp);
|
||||||
|
|
||||||
|
if (message.isPresent() && message.get().getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) {
|
||||||
|
receiptSender.sendReceipt(account,
|
||||||
|
message.get().getSource(),
|
||||||
|
message.get().getTimestamp(),
|
||||||
|
Optional.fromNullable(message.get().getRelay()));
|
||||||
|
}
|
||||||
|
} catch (NoSuchUserException | NotPushRegisteredException | TransientPushFailureException e) {
|
||||||
|
logger.warn("Sending delivery receipt", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void sendLocalMessage(Account source,
|
private void sendLocalMessage(Account source,
|
||||||
String destinationName,
|
String destinationName,
|
||||||
IncomingMessageList messages)
|
IncomingMessageList messages,
|
||||||
|
boolean isSyncMessage)
|
||||||
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
|
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
|
||||||
{
|
{
|
||||||
Account destination = getDestinationAccount(destinationName);
|
Account destination;
|
||||||
|
|
||||||
validateCompleteDeviceList(destination, messages.getMessages());
|
if (!isSyncMessage) destination = getDestinationAccount(destinationName);
|
||||||
|
else destination = source;
|
||||||
|
|
||||||
|
validateCompleteDeviceList(destination, messages.getMessages(), isSyncMessage);
|
||||||
validateRegistrationIds(destination, messages.getMessages());
|
validateRegistrationIds(destination, messages.getMessages());
|
||||||
|
|
||||||
for (IncomingMessage incomingMessage : messages.getMessages()) {
|
for (IncomingMessage incomingMessage : messages.getMessages()) {
|
||||||
@@ -183,9 +240,12 @@ public class MessageController {
|
|||||||
|
|
||||||
private void sendRelayMessage(Account source,
|
private void sendRelayMessage(Account source,
|
||||||
String destinationName,
|
String destinationName,
|
||||||
IncomingMessageList messages)
|
IncomingMessageList messages,
|
||||||
throws IOException, NoSuchUserException
|
boolean isSyncMessage)
|
||||||
|
throws IOException, NoSuchUserException, InvalidDestinationException
|
||||||
{
|
{
|
||||||
|
if (isSyncMessage) throw new InvalidDestinationException("Transcript messages can't be relayed!");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
|
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
|
||||||
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
|
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
|
||||||
@@ -228,7 +288,9 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateCompleteDeviceList(Account account, List<IncomingMessage> messages)
|
private void validateCompleteDeviceList(Account account,
|
||||||
|
List<IncomingMessage> messages,
|
||||||
|
boolean isSyncMessage)
|
||||||
throws MismatchedDevicesException
|
throws MismatchedDevicesException
|
||||||
{
|
{
|
||||||
Set<Long> messageDeviceIds = new HashSet<>();
|
Set<Long> messageDeviceIds = new HashSet<>();
|
||||||
@@ -242,7 +304,9 @@ public class MessageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Device device : account.getDevices()) {
|
for (Device device : account.getDevices()) {
|
||||||
if (device.isActive()) {
|
if (device.isActive() &&
|
||||||
|
!(isSyncMessage && device.getId() == account.getAuthenticatedDevice().get().getId()))
|
||||||
|
{
|
||||||
accountDeviceIds.add(device.getId());
|
accountDeviceIds.add(device.getId());
|
||||||
|
|
||||||
if (!messageDeviceIds.contains(device.getId())) {
|
if (!messageDeviceIds.contains(device.getId())) {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v1/provisioning")
|
||||||
|
public class ProvisioningController {
|
||||||
|
|
||||||
|
private final RateLimiters rateLimiters;
|
||||||
|
private final WebsocketSender websocketSender;
|
||||||
|
|
||||||
|
public ProvisioningController(RateLimiters rateLimiters, PushSender pushSender) {
|
||||||
|
this.rateLimiters = rateLimiters;
|
||||||
|
this.websocketSender = pushSender.getWebSocketSender();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@Path("/{destination}")
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public void sendProvisioningMessage(@Auth Account source,
|
||||||
|
@PathParam("destination") String destinationName,
|
||||||
|
@Valid ProvisioningMessage message)
|
||||||
|
throws RateLimitExceededException, InvalidWebsocketAddressException, IOException
|
||||||
|
{
|
||||||
|
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||||
|
|
||||||
|
if (!websocketSender.sendProvisioningMessage(new ProvisioningAddress(destinationName, 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.codahale.metrics.annotation.Timed;
|
||||||
import com.google.common.base.Optional;
|
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.NotPushRegisteredException;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
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.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
@@ -18,25 +14,16 @@ import javax.ws.rs.QueryParam;
|
|||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
|
||||||
|
|
||||||
@Path("/v1/receipt")
|
@Path("/v1/receipt")
|
||||||
public class ReceiptController {
|
public class ReceiptController {
|
||||||
|
|
||||||
private final AccountsManager accountManager;
|
private final ReceiptSender receiptSender;
|
||||||
private final PushSender pushSender;
|
|
||||||
private final FederatedClientManager federatedClientManager;
|
|
||||||
|
|
||||||
public ReceiptController(AccountsManager accountManager,
|
public ReceiptController(ReceiptSender receiptSender) {
|
||||||
FederatedClientManager federatedClientManager,
|
this.receiptSender = receiptSender;
|
||||||
PushSender pushSender)
|
|
||||||
{
|
|
||||||
this.accountManager = accountManager;
|
|
||||||
this.federatedClientManager = federatedClientManager;
|
|
||||||
this.pushSender = pushSender;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@@ -49,8 +36,7 @@ public class ReceiptController {
|
|||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
|
receiptSender.sendReceipt(source, destination, messageId, relay);
|
||||||
else sendDirectReceipt(source, destination, messageId);
|
|
||||||
} catch (NoSuchUserException | NotPushRegisteredException e) {
|
} catch (NoSuchUserException | NotPushRegisteredException e) {
|
||||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||||
} catch (TransientPushFailureException e) {
|
} 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);
|
|
||||||
List<Device> destinationDevices = destinationAccount.getDevices();
|
|
||||||
|
|
||||||
OutgoingMessageSignal.Builder message =
|
|
||||||
OutgoingMessageSignal.newBuilder()
|
|
||||||
.setSource(source.getNumber())
|
|
||||||
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
|
|
||||||
.setTimestamp(messageId)
|
|
||||||
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
|
|
||||||
|
|
||||||
if (source.getRelay().isPresent()) {
|
|
||||||
message.setRelay(source.getRelay().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Device destinationDevice : destinationDevices) {
|
|
||||||
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Account getDestinationAccount(String destination)
|
|
||||||
throws NoSuchUserException
|
|
||||||
{
|
|
||||||
Optional<Account> account = accountManager.get(destination);
|
|
||||||
|
|
||||||
if (!account.isPresent()) {
|
|
||||||
throw new NoSuchUserException(destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
return account.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import com.google.common.annotations.VisibleForTesting;
|
|||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
import javax.validation.constraints.Min;
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
public class ApnMessage {
|
public class ApnMessage {
|
||||||
|
|
||||||
|
public static long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String apnId;
|
private String apnId;
|
||||||
@@ -23,12 +27,50 @@ public class ApnMessage {
|
|||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private boolean voip;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long expirationTime;
|
||||||
|
|
||||||
public ApnMessage() {}
|
public ApnMessage() {}
|
||||||
|
|
||||||
public ApnMessage(String apnId, String number, int deviceId, String message) {
|
public ApnMessage(String apnId, String number, int deviceId, String message, boolean voip, long expirationTime) {
|
||||||
this.apnId = apnId;
|
this.apnId = apnId;
|
||||||
this.number = number;
|
this.number = number;
|
||||||
this.deviceId = deviceId;
|
this.deviceId = deviceId;
|
||||||
this.message = message;
|
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
|
@NotEmpty
|
||||||
private String apnRegistrationId;
|
private String apnRegistrationId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String voipRegistrationId;
|
||||||
|
|
||||||
public String getApnRegistrationId() {
|
public String getApnRegistrationId() {
|
||||||
return apnRegistrationId;
|
return apnRegistrationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getVoipRegistrationId() {
|
||||||
|
return voipRegistrationId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,20 +20,23 @@ public class GcmMessage {
|
|||||||
private int deviceId;
|
private int deviceId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotEmpty
|
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private boolean receipt;
|
private boolean receipt;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean notification;
|
||||||
|
|
||||||
public GcmMessage() {}
|
public GcmMessage() {}
|
||||||
|
|
||||||
public GcmMessage(String gcmId, String number, int deviceId, String message, boolean receipt) {
|
public GcmMessage(String gcmId, String number, int deviceId, String message, boolean receipt, boolean notification) {
|
||||||
this.gcmId = gcmId;
|
this.gcmId = gcmId;
|
||||||
this.number = number;
|
this.number = number;
|
||||||
this.deviceId = deviceId;
|
this.deviceId = deviceId;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.receipt = receipt;
|
this.receipt = receipt;
|
||||||
|
this.notification = notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,15 @@ public class GcmRegistrationId {
|
|||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String gcmRegistrationId;
|
private String gcmRegistrationId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean webSocketChannel;
|
||||||
|
|
||||||
public String getGcmRegistrationId() {
|
public String getGcmRegistrationId() {
|
||||||
return gcmRegistrationId;
|
return gcmRegistrationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isWebSocketChannel() {
|
||||||
|
return webSocketChannel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1112,11 +1112,487 @@ public final class MessageProtos {
|
|||||||
// @@protoc_insertion_point(class_scope:textsecure.OutgoingMessageSignal)
|
// @@protoc_insertion_point(class_scope:textsecure.OutgoingMessageSignal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ProvisioningUuidOrBuilder
|
||||||
|
extends com.google.protobuf.MessageOrBuilder {
|
||||||
|
|
||||||
|
// optional string uuid = 1;
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
boolean hasUuid();
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
java.lang.String getUuid();
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
com.google.protobuf.ByteString
|
||||||
|
getUuidBytes();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Protobuf type {@code textsecure.ProvisioningUuid}
|
||||||
|
*/
|
||||||
|
public static final class ProvisioningUuid extends
|
||||||
|
com.google.protobuf.GeneratedMessage
|
||||||
|
implements ProvisioningUuidOrBuilder {
|
||||||
|
// Use ProvisioningUuid.newBuilder() to construct.
|
||||||
|
private ProvisioningUuid(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
|
||||||
|
super(builder);
|
||||||
|
this.unknownFields = builder.getUnknownFields();
|
||||||
|
}
|
||||||
|
private ProvisioningUuid(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
|
||||||
|
|
||||||
|
private static final ProvisioningUuid defaultInstance;
|
||||||
|
public static ProvisioningUuid getDefaultInstance() {
|
||||||
|
return defaultInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProvisioningUuid getDefaultInstanceForType() {
|
||||||
|
return defaultInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final com.google.protobuf.UnknownFieldSet unknownFields;
|
||||||
|
@java.lang.Override
|
||||||
|
public final com.google.protobuf.UnknownFieldSet
|
||||||
|
getUnknownFields() {
|
||||||
|
return this.unknownFields;
|
||||||
|
}
|
||||||
|
private ProvisioningUuid(
|
||||||
|
com.google.protobuf.CodedInputStream input,
|
||||||
|
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||||
|
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||||
|
initFields();
|
||||||
|
int mutable_bitField0_ = 0;
|
||||||
|
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
|
||||||
|
com.google.protobuf.UnknownFieldSet.newBuilder();
|
||||||
|
try {
|
||||||
|
boolean done = false;
|
||||||
|
while (!done) {
|
||||||
|
int tag = input.readTag();
|
||||||
|
switch (tag) {
|
||||||
|
case 0:
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
if (!parseUnknownField(input, unknownFields,
|
||||||
|
extensionRegistry, tag)) {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 10: {
|
||||||
|
bitField0_ |= 0x00000001;
|
||||||
|
uuid_ = input.readBytes();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||||
|
throw e.setUnfinishedMessage(this);
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
throw new com.google.protobuf.InvalidProtocolBufferException(
|
||||||
|
e.getMessage()).setUnfinishedMessage(this);
|
||||||
|
} finally {
|
||||||
|
this.unknownFields = unknownFields.build();
|
||||||
|
makeExtensionsImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static final com.google.protobuf.Descriptors.Descriptor
|
||||||
|
getDescriptor() {
|
||||||
|
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_ProvisioningUuid_descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||||
|
internalGetFieldAccessorTable() {
|
||||||
|
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_ProvisioningUuid_fieldAccessorTable
|
||||||
|
.ensureFieldAccessorsInitialized(
|
||||||
|
org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid.class, org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid.Builder.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static com.google.protobuf.Parser<ProvisioningUuid> PARSER =
|
||||||
|
new com.google.protobuf.AbstractParser<ProvisioningUuid>() {
|
||||||
|
public ProvisioningUuid parsePartialFrom(
|
||||||
|
com.google.protobuf.CodedInputStream input,
|
||||||
|
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||||
|
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||||
|
return new ProvisioningUuid(input, extensionRegistry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@java.lang.Override
|
||||||
|
public com.google.protobuf.Parser<ProvisioningUuid> getParserForType() {
|
||||||
|
return PARSER;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int bitField0_;
|
||||||
|
// optional string uuid = 1;
|
||||||
|
public static final int UUID_FIELD_NUMBER = 1;
|
||||||
|
private java.lang.Object uuid_;
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public boolean hasUuid() {
|
||||||
|
return ((bitField0_ & 0x00000001) == 0x00000001);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public java.lang.String getUuid() {
|
||||||
|
java.lang.Object ref = uuid_;
|
||||||
|
if (ref instanceof java.lang.String) {
|
||||||
|
return (java.lang.String) ref;
|
||||||
|
} else {
|
||||||
|
com.google.protobuf.ByteString bs =
|
||||||
|
(com.google.protobuf.ByteString) ref;
|
||||||
|
java.lang.String s = bs.toStringUtf8();
|
||||||
|
if (bs.isValidUtf8()) {
|
||||||
|
uuid_ = s;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public com.google.protobuf.ByteString
|
||||||
|
getUuidBytes() {
|
||||||
|
java.lang.Object ref = uuid_;
|
||||||
|
if (ref instanceof java.lang.String) {
|
||||||
|
com.google.protobuf.ByteString b =
|
||||||
|
com.google.protobuf.ByteString.copyFromUtf8(
|
||||||
|
(java.lang.String) ref);
|
||||||
|
uuid_ = b;
|
||||||
|
return b;
|
||||||
|
} else {
|
||||||
|
return (com.google.protobuf.ByteString) ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initFields() {
|
||||||
|
uuid_ = "";
|
||||||
|
}
|
||||||
|
private byte memoizedIsInitialized = -1;
|
||||||
|
public final boolean isInitialized() {
|
||||||
|
byte isInitialized = memoizedIsInitialized;
|
||||||
|
if (isInitialized != -1) return isInitialized == 1;
|
||||||
|
|
||||||
|
memoizedIsInitialized = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTo(com.google.protobuf.CodedOutputStream output)
|
||||||
|
throws java.io.IOException {
|
||||||
|
getSerializedSize();
|
||||||
|
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
||||||
|
output.writeBytes(1, getUuidBytes());
|
||||||
|
}
|
||||||
|
getUnknownFields().writeTo(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int memoizedSerializedSize = -1;
|
||||||
|
public int getSerializedSize() {
|
||||||
|
int size = memoizedSerializedSize;
|
||||||
|
if (size != -1) return size;
|
||||||
|
|
||||||
|
size = 0;
|
||||||
|
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
||||||
|
size += com.google.protobuf.CodedOutputStream
|
||||||
|
.computeBytesSize(1, getUuidBytes());
|
||||||
|
}
|
||||||
|
size += getUnknownFields().getSerializedSize();
|
||||||
|
memoizedSerializedSize = size;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 0L;
|
||||||
|
@java.lang.Override
|
||||||
|
protected java.lang.Object writeReplace()
|
||||||
|
throws java.io.ObjectStreamException {
|
||||||
|
return super.writeReplace();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid parseFrom(
|
||||||
|
com.google.protobuf.ByteString data)
|
||||||
|
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||||
|
return PARSER.parseFrom(data);
|
||||||
|
}
|
||||||
|
public static org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid 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.ProvisioningUuid parseFrom(byte[] data)
|
||||||
|
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||||
|
return PARSER.parseFrom(data);
|
||||||
|
}
|
||||||
|
public static org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid 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.ProvisioningUuid parseFrom(java.io.InputStream input)
|
||||||
|
throws java.io.IOException {
|
||||||
|
return PARSER.parseFrom(input);
|
||||||
|
}
|
||||||
|
public static org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid 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.ProvisioningUuid parseDelimitedFrom(java.io.InputStream input)
|
||||||
|
throws java.io.IOException {
|
||||||
|
return PARSER.parseDelimitedFrom(input);
|
||||||
|
}
|
||||||
|
public static org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid 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.ProvisioningUuid parseFrom(
|
||||||
|
com.google.protobuf.CodedInputStream input)
|
||||||
|
throws java.io.IOException {
|
||||||
|
return PARSER.parseFrom(input);
|
||||||
|
}
|
||||||
|
public static org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid parseFrom(
|
||||||
|
com.google.protobuf.CodedInputStream input,
|
||||||
|
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||||
|
throws java.io.IOException {
|
||||||
|
return PARSER.parseFrom(input, extensionRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() { return Builder.create(); }
|
||||||
|
public Builder newBuilderForType() { return newBuilder(); }
|
||||||
|
public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid prototype) {
|
||||||
|
return newBuilder().mergeFrom(prototype);
|
||||||
|
}
|
||||||
|
public Builder toBuilder() { return newBuilder(this); }
|
||||||
|
|
||||||
|
@java.lang.Override
|
||||||
|
protected Builder newBuilderForType(
|
||||||
|
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
|
||||||
|
Builder builder = new Builder(parent);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Protobuf type {@code textsecure.ProvisioningUuid}
|
||||||
|
*/
|
||||||
|
public static final class Builder extends
|
||||||
|
com.google.protobuf.GeneratedMessage.Builder<Builder>
|
||||||
|
implements org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuidOrBuilder {
|
||||||
|
public static final com.google.protobuf.Descriptors.Descriptor
|
||||||
|
getDescriptor() {
|
||||||
|
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_ProvisioningUuid_descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||||
|
internalGetFieldAccessorTable() {
|
||||||
|
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_ProvisioningUuid_fieldAccessorTable
|
||||||
|
.ensureFieldAccessorsInitialized(
|
||||||
|
org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid.class, org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid.Builder.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid.newBuilder()
|
||||||
|
private Builder() {
|
||||||
|
maybeForceBuilderInitialization();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder(
|
||||||
|
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
|
||||||
|
super(parent);
|
||||||
|
maybeForceBuilderInitialization();
|
||||||
|
}
|
||||||
|
private void maybeForceBuilderInitialization() {
|
||||||
|
if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static Builder create() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clear() {
|
||||||
|
super.clear();
|
||||||
|
uuid_ = "";
|
||||||
|
bitField0_ = (bitField0_ & ~0x00000001);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder clone() {
|
||||||
|
return create().mergeFrom(buildPartial());
|
||||||
|
}
|
||||||
|
|
||||||
|
public com.google.protobuf.Descriptors.Descriptor
|
||||||
|
getDescriptorForType() {
|
||||||
|
return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_ProvisioningUuid_descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid getDefaultInstanceForType() {
|
||||||
|
return org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid.getDefaultInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid build() {
|
||||||
|
org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid result = buildPartial();
|
||||||
|
if (!result.isInitialized()) {
|
||||||
|
throw newUninitializedMessageException(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid buildPartial() {
|
||||||
|
org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid result = new org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid(this);
|
||||||
|
int from_bitField0_ = bitField0_;
|
||||||
|
int to_bitField0_ = 0;
|
||||||
|
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
|
||||||
|
to_bitField0_ |= 0x00000001;
|
||||||
|
}
|
||||||
|
result.uuid_ = uuid_;
|
||||||
|
result.bitField0_ = to_bitField0_;
|
||||||
|
onBuilt();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder mergeFrom(com.google.protobuf.Message other) {
|
||||||
|
if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid) {
|
||||||
|
return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid)other);
|
||||||
|
} else {
|
||||||
|
super.mergeFrom(other);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid other) {
|
||||||
|
if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid.getDefaultInstance()) return this;
|
||||||
|
if (other.hasUuid()) {
|
||||||
|
bitField0_ |= 0x00000001;
|
||||||
|
uuid_ = other.uuid_;
|
||||||
|
onChanged();
|
||||||
|
}
|
||||||
|
this.mergeUnknownFields(other.getUnknownFields());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final boolean isInitialized() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder mergeFrom(
|
||||||
|
com.google.protobuf.CodedInputStream input,
|
||||||
|
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||||
|
throws java.io.IOException {
|
||||||
|
org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid parsedMessage = null;
|
||||||
|
try {
|
||||||
|
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
|
||||||
|
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||||
|
parsedMessage = (org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid) e.getUnfinishedMessage();
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (parsedMessage != null) {
|
||||||
|
mergeFrom(parsedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
private int bitField0_;
|
||||||
|
|
||||||
|
// optional string uuid = 1;
|
||||||
|
private java.lang.Object uuid_ = "";
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public boolean hasUuid() {
|
||||||
|
return ((bitField0_ & 0x00000001) == 0x00000001);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public java.lang.String getUuid() {
|
||||||
|
java.lang.Object ref = uuid_;
|
||||||
|
if (!(ref instanceof java.lang.String)) {
|
||||||
|
java.lang.String s = ((com.google.protobuf.ByteString) ref)
|
||||||
|
.toStringUtf8();
|
||||||
|
uuid_ = s;
|
||||||
|
return s;
|
||||||
|
} else {
|
||||||
|
return (java.lang.String) ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public com.google.protobuf.ByteString
|
||||||
|
getUuidBytes() {
|
||||||
|
java.lang.Object ref = uuid_;
|
||||||
|
if (ref instanceof String) {
|
||||||
|
com.google.protobuf.ByteString b =
|
||||||
|
com.google.protobuf.ByteString.copyFromUtf8(
|
||||||
|
(java.lang.String) ref);
|
||||||
|
uuid_ = b;
|
||||||
|
return b;
|
||||||
|
} else {
|
||||||
|
return (com.google.protobuf.ByteString) ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public Builder setUuid(
|
||||||
|
java.lang.String value) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
bitField0_ |= 0x00000001;
|
||||||
|
uuid_ = value;
|
||||||
|
onChanged();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public Builder clearUuid() {
|
||||||
|
bitField0_ = (bitField0_ & ~0x00000001);
|
||||||
|
uuid_ = getDefaultInstance().getUuid();
|
||||||
|
onChanged();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional string uuid = 1;</code>
|
||||||
|
*/
|
||||||
|
public Builder setUuidBytes(
|
||||||
|
com.google.protobuf.ByteString value) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
bitField0_ |= 0x00000001;
|
||||||
|
uuid_ = value;
|
||||||
|
onChanged();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @@protoc_insertion_point(builder_scope:textsecure.ProvisioningUuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
defaultInstance = new ProvisioningUuid(true);
|
||||||
|
defaultInstance.initFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @@protoc_insertion_point(class_scope:textsecure.ProvisioningUuid)
|
||||||
|
}
|
||||||
|
|
||||||
private static com.google.protobuf.Descriptors.Descriptor
|
private static com.google.protobuf.Descriptors.Descriptor
|
||||||
internal_static_textsecure_OutgoingMessageSignal_descriptor;
|
internal_static_textsecure_OutgoingMessageSignal_descriptor;
|
||||||
private static
|
private static
|
||||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||||
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
|
internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable;
|
||||||
|
private static com.google.protobuf.Descriptors.Descriptor
|
||||||
|
internal_static_textsecure_ProvisioningUuid_descriptor;
|
||||||
|
private static
|
||||||
|
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||||
|
internal_static_textsecure_ProvisioningUuid_fieldAccessorTable;
|
||||||
|
|
||||||
public static com.google.protobuf.Descriptors.FileDescriptor
|
public static com.google.protobuf.Descriptors.FileDescriptor
|
||||||
getDescriptor() {
|
getDescriptor() {
|
||||||
@@ -1132,9 +1608,10 @@ public final class MessageProtos {
|
|||||||
"\r\n\005relay\030\003 \001(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mes" +
|
"\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" +
|
"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" +
|
"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\005B:\n)org." +
|
"LE\020\003\022\r\n\tPLAINTEXT\020\004\022\013\n\007RECEIPT\020\005\" \n\020Prov" +
|
||||||
"whispersystems.textsecuregcm.entitiesB\rM" +
|
"isioningUuid\022\014\n\004uuid\030\001 \001(\tB:\n)org.whispe" +
|
||||||
"essageProtos"
|
"rsystems.textsecuregcm.entitiesB\rMessage" +
|
||||||
|
"Protos"
|
||||||
};
|
};
|
||||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||||
@@ -1147,6 +1624,12 @@ public final class MessageProtos {
|
|||||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||||
internal_static_textsecure_OutgoingMessageSignal_descriptor,
|
internal_static_textsecure_OutgoingMessageSignal_descriptor,
|
||||||
new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "Message", });
|
new java.lang.String[] { "Type", "Source", "SourceDevice", "Relay", "Timestamp", "Message", });
|
||||||
|
internal_static_textsecure_ProvisioningUuid_descriptor =
|
||||||
|
getDescriptor().getMessageTypes().get(1);
|
||||||
|
internal_static_textsecure_ProvisioningUuid_fieldAccessorTable = new
|
||||||
|
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||||
|
internal_static_textsecure_ProvisioningUuid_descriptor,
|
||||||
|
new java.lang.String[] { "Uuid", });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public OutgoingMessageEntity() {}
|
||||||
|
|
||||||
|
public OutgoingMessageEntity(long id, int type, String relay, long timestamp,
|
||||||
|
String source, int sourceDevice, byte[] message)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
this.relay = relay;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.source = source;
|
||||||
|
this.sourceDevice = sourceDevice;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public OutgoingMessageEntityList() {}
|
||||||
|
|
||||||
|
public OutgoingMessageEntityList(List<OutgoingMessageEntity> messages) {
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public List<OutgoingMessageEntity> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.entities;
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
@@ -42,7 +43,19 @@ public class PreKeyResponseV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public List<PreKeyResponseItemV2> getDevices() {
|
@JsonIgnore
|
||||||
return devices;
|
public PreKeyResponseItemV2 getDevice(int deviceId) {
|
||||||
|
for (PreKeyResponseItemV2 device : devices) {
|
||||||
|
if (device.getDeviceId() == deviceId) return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@JsonIgnore
|
||||||
|
public int getDevicesCount() {
|
||||||
|
return devices.size();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
public class ProvisioningMessage {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class SendMessageResponse {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean needsSync;
|
||||||
|
|
||||||
|
public SendMessageResponse() {}
|
||||||
|
|
||||||
|
public SendMessageResponse(boolean needsSync) {
|
||||||
|
this.needsSync = needsSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ public class UnregisteredEvent {
|
|||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String registrationId;
|
private String registrationId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String canonicalId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String number;
|
private String number;
|
||||||
@@ -26,6 +29,10 @@ public class UnregisteredEvent {
|
|||||||
return registrationId;
|
return registrationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCanonicalId() {
|
||||||
|
return canonicalId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getNumber() {
|
public String getNumber() {
|
||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Device> getAuthenticatedDevice() {
|
public Optional<Device> getAuthenticatedDevice() {
|
||||||
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null));
|
return Optional.of(new Device(deviceId, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.limits;
|
package org.whispersystems.textsecuregcm.limits;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
public class LeakyBucket implements Serializable {
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class LeakyBucket {
|
||||||
|
|
||||||
private final int bucketSize;
|
private final int bucketSize;
|
||||||
private final double leakRatePerMillis;
|
private final double leakRatePerMillis;
|
||||||
@@ -27,10 +31,14 @@ public class LeakyBucket implements Serializable {
|
|||||||
private long lastUpdateTimeMillis;
|
private long lastUpdateTimeMillis;
|
||||||
|
|
||||||
public LeakyBucket(int bucketSize, double leakRatePerMillis) {
|
public LeakyBucket(int bucketSize, double leakRatePerMillis) {
|
||||||
|
this(bucketSize, leakRatePerMillis, bucketSize, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private LeakyBucket(int bucketSize, double leakRatePerMillis, int spaceRemaining, long lastUpdateTimeMillis) {
|
||||||
this.bucketSize = bucketSize;
|
this.bucketSize = bucketSize;
|
||||||
this.leakRatePerMillis = leakRatePerMillis;
|
this.leakRatePerMillis = leakRatePerMillis;
|
||||||
this.spaceRemaining = bucketSize;
|
this.spaceRemaining = spaceRemaining;
|
||||||
this.lastUpdateTimeMillis = System.currentTimeMillis();
|
this.lastUpdateTimeMillis = lastUpdateTimeMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean add(int amount) {
|
public boolean add(int amount) {
|
||||||
@@ -50,4 +58,40 @@ public class LeakyBucket implements Serializable {
|
|||||||
return Math.min(this.bucketSize,
|
return Math.min(this.bucketSize,
|
||||||
(int)Math.floor(this.spaceRemaining + (elapsedTime * this.leakRatePerMillis)));
|
(int)Math.floor(this.spaceRemaining + (elapsedTime * this.leakRatePerMillis)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String serialize(ObjectMapper mapper) throws JsonProcessingException {
|
||||||
|
return mapper.writeValueAsString(new LeakyBucketEntity(bucketSize, leakRatePerMillis, spaceRemaining, lastUpdateTimeMillis));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LeakyBucket fromSerialized(ObjectMapper mapper, String serialized) throws IOException {
|
||||||
|
LeakyBucketEntity entity = mapper.readValue(serialized, LeakyBucketEntity.class);
|
||||||
|
|
||||||
|
return new LeakyBucket(entity.bucketSize, entity.leakRatePerMillis,
|
||||||
|
entity.spaceRemaining, entity.lastUpdateTimeMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LeakyBucketEntity {
|
||||||
|
@JsonProperty
|
||||||
|
private int bucketSize;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private double leakRatePerMillis;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int spaceRemaining;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long lastUpdateTimeMillis;
|
||||||
|
|
||||||
|
public LeakyBucketEntity() {}
|
||||||
|
|
||||||
|
private LeakyBucketEntity(int bucketSize, double leakRatePerMillis,
|
||||||
|
int spaceRemaining, long lastUpdateTimeMillis)
|
||||||
|
{
|
||||||
|
this.bucketSize = bucketSize;
|
||||||
|
this.leakRatePerMillis = leakRatePerMillis;
|
||||||
|
this.spaceRemaining = spaceRemaining;
|
||||||
|
this.lastUpdateTimeMillis = lastUpdateTimeMillis;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,27 +19,38 @@ package org.whispersystems.textsecuregcm.limits;
|
|||||||
import com.codahale.metrics.Meter;
|
import com.codahale.metrics.Meter;
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.codahale.metrics.SharedMetricRegistries;
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
import net.spy.memcached.MemcachedClient;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||||
import org.whispersystems.textsecuregcm.util.Constants;
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
public class RateLimiter {
|
public class RateLimiter {
|
||||||
|
|
||||||
private final Meter meter;
|
private final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
|
||||||
private final MemcachedClient memcachedClient;
|
private final ObjectMapper mapper = SystemMapper.getMapper();
|
||||||
private final String name;
|
|
||||||
private final int bucketSize;
|
|
||||||
private final double leakRatePerMillis;
|
|
||||||
|
|
||||||
public RateLimiter(MemcachedClient memcachedClient, String name,
|
private final Meter meter;
|
||||||
|
private final JedisPool cacheClient;
|
||||||
|
private final String name;
|
||||||
|
private final int bucketSize;
|
||||||
|
private final double leakRatePerMillis;
|
||||||
|
|
||||||
|
public RateLimiter(JedisPool cacheClient, String name,
|
||||||
int bucketSize, double leakRatePerMinute)
|
int bucketSize, double leakRatePerMinute)
|
||||||
{
|
{
|
||||||
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
|
||||||
this.meter = metricRegistry.meter(name(getClass(), name, "exceeded"));
|
this.meter = metricRegistry.meter(name(getClass(), name, "exceeded"));
|
||||||
this.memcachedClient = memcachedClient;
|
this.cacheClient = cacheClient;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.bucketSize = bucketSize;
|
this.bucketSize = bucketSize;
|
||||||
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
|
this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0);
|
||||||
@@ -61,21 +72,29 @@ public class RateLimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setBucket(String key, LeakyBucket bucket) {
|
private void setBucket(String key, LeakyBucket bucket) {
|
||||||
memcachedClient.set(getBucketName(key),
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
(int)Math.ceil((bucketSize / leakRatePerMillis) / 1000), bucket);
|
String serialized = bucket.serialize(mapper);
|
||||||
}
|
jedis.setex(getBucketName(key), (int) Math.ceil((bucketSize / leakRatePerMillis) / 1000), serialized);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
private LeakyBucket getBucket(String key) {
|
throw new IllegalArgumentException(e);
|
||||||
LeakyBucket bucket = (LeakyBucket)memcachedClient.get(getBucketName(key));
|
|
||||||
|
|
||||||
if (bucket == null) {
|
|
||||||
return new LeakyBucket(bucketSize, leakRatePerMillis);
|
|
||||||
} else {
|
|
||||||
return bucket;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LeakyBucket getBucket(String key) {
|
||||||
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
|
String serialized = jedis.get(getBucketName(key));
|
||||||
|
|
||||||
|
if (serialized != null) {
|
||||||
|
return LeakyBucket.fromSerialized(mapper, serialized);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Deserialization error", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LeakyBucket(bucketSize, leakRatePerMillis);
|
||||||
|
}
|
||||||
|
|
||||||
private String getBucketName(String key) {
|
private String getBucketName(String key) {
|
||||||
return LeakyBucket.class.getSimpleName() + name + key;
|
return "leaky_bucket::" + name + "::" + key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@
|
|||||||
package org.whispersystems.textsecuregcm.limits;
|
package org.whispersystems.textsecuregcm.limits;
|
||||||
|
|
||||||
|
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||||
|
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
public class RateLimiters {
|
public class RateLimiters {
|
||||||
|
|
||||||
private final RateLimiter smsDestinationLimiter;
|
private final RateLimiter smsDestinationLimiter;
|
||||||
@@ -34,40 +35,40 @@ public class RateLimiters {
|
|||||||
private final RateLimiter allocateDeviceLimiter;
|
private final RateLimiter allocateDeviceLimiter;
|
||||||
private final RateLimiter verifyDeviceLimiter;
|
private final RateLimiter verifyDeviceLimiter;
|
||||||
|
|
||||||
public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) {
|
public RateLimiters(RateLimitsConfiguration config, JedisPool cacheClient) {
|
||||||
this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination",
|
this.smsDestinationLimiter = new RateLimiter(cacheClient, "smsDestination",
|
||||||
config.getSmsDestination().getBucketSize(),
|
config.getSmsDestination().getBucketSize(),
|
||||||
config.getSmsDestination().getLeakRatePerMinute());
|
config.getSmsDestination().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.voiceDestinationLimiter = new RateLimiter(memcachedClient, "voxDestination",
|
this.voiceDestinationLimiter = new RateLimiter(cacheClient, "voxDestination",
|
||||||
config.getVoiceDestination().getBucketSize(),
|
config.getVoiceDestination().getBucketSize(),
|
||||||
config.getVoiceDestination().getLeakRatePerMinute());
|
config.getVoiceDestination().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.verifyLimiter = new RateLimiter(memcachedClient, "verify",
|
this.verifyLimiter = new RateLimiter(cacheClient, "verify",
|
||||||
config.getVerifyNumber().getBucketSize(),
|
config.getVerifyNumber().getBucketSize(),
|
||||||
config.getVerifyNumber().getLeakRatePerMinute());
|
config.getVerifyNumber().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.attachmentLimiter = new RateLimiter(memcachedClient, "attachmentCreate",
|
this.attachmentLimiter = new RateLimiter(cacheClient, "attachmentCreate",
|
||||||
config.getAttachments().getBucketSize(),
|
config.getAttachments().getBucketSize(),
|
||||||
config.getAttachments().getLeakRatePerMinute());
|
config.getAttachments().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.contactsLimiter = new RateLimiter(memcachedClient, "contactsQuery",
|
this.contactsLimiter = new RateLimiter(cacheClient, "contactsQuery",
|
||||||
config.getContactQueries().getBucketSize(),
|
config.getContactQueries().getBucketSize(),
|
||||||
config.getContactQueries().getLeakRatePerMinute());
|
config.getContactQueries().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.preKeysLimiter = new RateLimiter(memcachedClient, "prekeys",
|
this.preKeysLimiter = new RateLimiter(cacheClient, "prekeys",
|
||||||
config.getPreKeys().getBucketSize(),
|
config.getPreKeys().getBucketSize(),
|
||||||
config.getPreKeys().getLeakRatePerMinute());
|
config.getPreKeys().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.messagesLimiter = new RateLimiter(memcachedClient, "messages",
|
this.messagesLimiter = new RateLimiter(cacheClient, "messages",
|
||||||
config.getMessages().getBucketSize(),
|
config.getMessages().getBucketSize(),
|
||||||
config.getMessages().getLeakRatePerMinute());
|
config.getMessages().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.allocateDeviceLimiter = new RateLimiter(memcachedClient, "allocateDevice",
|
this.allocateDeviceLimiter = new RateLimiter(cacheClient, "allocateDevice",
|
||||||
config.getAllocateDevice().getBucketSize(),
|
config.getAllocateDevice().getBucketSize(),
|
||||||
config.getAllocateDevice().getLeakRatePerMinute());
|
config.getAllocateDevice().getLeakRatePerMinute());
|
||||||
|
|
||||||
this.verifyDeviceLimiter = new RateLimiter(memcachedClient, "verifyDevice",
|
this.verifyDeviceLimiter = new RateLimiter(cacheClient, "verifyDevice",
|
||||||
config.getVerifyDevice().getBucketSize(),
|
config.getVerifyDevice().getBucketSize(),
|
||||||
config.getVerifyDevice().getLeakRatePerMinute());
|
config.getVerifyDevice().getLeakRatePerMinute());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.liquibase;
|
||||||
|
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
import io.dropwizard.Configuration;
|
||||||
|
import io.dropwizard.cli.ConfiguredCommand;
|
||||||
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
|
import io.dropwizard.db.DatabaseConfiguration;
|
||||||
|
import io.dropwizard.db.ManagedDataSource;
|
||||||
|
import io.dropwizard.setup.Bootstrap;
|
||||||
|
import liquibase.Liquibase;
|
||||||
|
import liquibase.exception.LiquibaseException;
|
||||||
|
import liquibase.exception.ValidationFailedException;
|
||||||
|
|
||||||
|
public abstract class AbstractLiquibaseCommand<T extends Configuration> extends ConfiguredCommand<T> {
|
||||||
|
|
||||||
|
private final DatabaseConfiguration<T> strategy;
|
||||||
|
private final Class<T> configurationClass;
|
||||||
|
private final String migrations;
|
||||||
|
|
||||||
|
protected AbstractLiquibaseCommand(String name,
|
||||||
|
String description,
|
||||||
|
String migrations,
|
||||||
|
DatabaseConfiguration<T> strategy,
|
||||||
|
Class<T> configurationClass) {
|
||||||
|
super(name, description);
|
||||||
|
this.migrations = migrations;
|
||||||
|
this.strategy = strategy;
|
||||||
|
this.configurationClass = configurationClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<T> getConfigurationClass() {
|
||||||
|
return configurationClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("UseOfSystemOutOrSystemErr")
|
||||||
|
protected void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception {
|
||||||
|
final DataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration);
|
||||||
|
dbConfig.setMaxSize(1);
|
||||||
|
dbConfig.setMinSize(1);
|
||||||
|
dbConfig.setInitialSize(1);
|
||||||
|
|
||||||
|
try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) {
|
||||||
|
run(namespace, liquibase);
|
||||||
|
} catch (ValidationFailedException e) {
|
||||||
|
e.printDescriptiveError(System.err);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CloseableLiquibase openLiquibase(final DataSourceFactory dataSourceFactory, final Namespace namespace)
|
||||||
|
throws ClassNotFoundException, SQLException, LiquibaseException
|
||||||
|
{
|
||||||
|
final ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(), "liquibase");
|
||||||
|
return new CloseableLiquibase(dataSource, migrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void run(Namespace namespace, Liquibase liquibase) throws Exception;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.liquibase;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
import io.dropwizard.db.ManagedDataSource;
|
||||||
|
import liquibase.Liquibase;
|
||||||
|
import liquibase.database.jvm.JdbcConnection;
|
||||||
|
import liquibase.exception.LiquibaseException;
|
||||||
|
import liquibase.resource.ClassLoaderResourceAccessor;
|
||||||
|
|
||||||
|
|
||||||
|
public class CloseableLiquibase extends Liquibase implements AutoCloseable {
|
||||||
|
private final ManagedDataSource dataSource;
|
||||||
|
|
||||||
|
public CloseableLiquibase(ManagedDataSource dataSource, String migrations)
|
||||||
|
throws LiquibaseException, ClassNotFoundException, SQLException
|
||||||
|
{
|
||||||
|
super(migrations,
|
||||||
|
new ClassLoaderResourceAccessor(),
|
||||||
|
new JdbcConnection(dataSource.getConnection()));
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
dataSource.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.liquibase;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import net.sourceforge.argparse4j.impl.Arguments;
|
||||||
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.dropwizard.Configuration;
|
||||||
|
import io.dropwizard.db.DatabaseConfiguration;
|
||||||
|
import liquibase.Liquibase;
|
||||||
|
|
||||||
|
public class DbMigrateCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
|
||||||
|
|
||||||
|
public DbMigrateCommand(String migration, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
|
||||||
|
super("migrate", "Apply all pending change sets.", migration, strategy, configurationClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(Subparser subparser) {
|
||||||
|
super.configure(subparser);
|
||||||
|
|
||||||
|
subparser.addArgument("-n", "--dry-run")
|
||||||
|
.action(Arguments.storeTrue())
|
||||||
|
.dest("dry-run")
|
||||||
|
.setDefault(Boolean.FALSE)
|
||||||
|
.help("output the DDL to stdout, don't run it");
|
||||||
|
|
||||||
|
subparser.addArgument("-c", "--count")
|
||||||
|
.type(Integer.class)
|
||||||
|
.dest("count")
|
||||||
|
.help("only apply the next N change sets");
|
||||||
|
|
||||||
|
subparser.addArgument("-i", "--include")
|
||||||
|
.action(Arguments.append())
|
||||||
|
.dest("contexts")
|
||||||
|
.help("include change sets from the given context");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("UseOfSystemOutOrSystemErr")
|
||||||
|
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
|
||||||
|
final String context = getContext(namespace);
|
||||||
|
final Integer count = namespace.getInt("count");
|
||||||
|
final Boolean dryRun = namespace.getBoolean("dry-run");
|
||||||
|
if (count != null) {
|
||||||
|
if (dryRun) {
|
||||||
|
liquibase.update(count, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
|
||||||
|
} else {
|
||||||
|
liquibase.update(count, context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dryRun) {
|
||||||
|
liquibase.update(context, new OutputStreamWriter(System.out, Charsets.UTF_8));
|
||||||
|
} else {
|
||||||
|
liquibase.update(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getContext(Namespace namespace) {
|
||||||
|
final List<Object> contexts = namespace.getList("contexts");
|
||||||
|
if (contexts == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Joiner.on(',').join(contexts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.liquibase;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import net.sourceforge.argparse4j.impl.Arguments;
|
||||||
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.dropwizard.Configuration;
|
||||||
|
import io.dropwizard.db.DatabaseConfiguration;
|
||||||
|
import liquibase.Liquibase;
|
||||||
|
|
||||||
|
public class DbStatusCommand <T extends Configuration> extends AbstractLiquibaseCommand<T> {
|
||||||
|
|
||||||
|
public DbStatusCommand(String migrations, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
|
||||||
|
super("status", "Check for pending change sets.", migrations, strategy, configurationClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(Subparser subparser) {
|
||||||
|
super.configure(subparser);
|
||||||
|
|
||||||
|
subparser.addArgument("-v", "--verbose")
|
||||||
|
.action(Arguments.storeTrue())
|
||||||
|
.dest("verbose")
|
||||||
|
.help("Output verbose information");
|
||||||
|
subparser.addArgument("-i", "--include")
|
||||||
|
.action(Arguments.append())
|
||||||
|
.dest("contexts")
|
||||||
|
.help("include change sets from the given context");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("UseOfSystemOutOrSystemErr")
|
||||||
|
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
|
||||||
|
liquibase.reportStatus(namespace.getBoolean("verbose"),
|
||||||
|
getContext(namespace),
|
||||||
|
new OutputStreamWriter(System.out, Charsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getContext(Namespace namespace) {
|
||||||
|
final List<Object> contexts = namespace.getList("contexts");
|
||||||
|
if (contexts == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Joiner.on(',').join(contexts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.liquibase;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
import io.dropwizard.Configuration;
|
||||||
|
import io.dropwizard.db.DatabaseConfiguration;
|
||||||
|
import liquibase.Liquibase;
|
||||||
|
|
||||||
|
public class NameableDbCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
|
||||||
|
private static final String COMMAND_NAME_ATTR = "subcommand";
|
||||||
|
private final SortedMap<String, AbstractLiquibaseCommand<T>> subcommands;
|
||||||
|
|
||||||
|
public NameableDbCommand(String name, String migrations, DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
|
||||||
|
super(name, "Run database migrations tasks", migrations, strategy, configurationClass);
|
||||||
|
this.subcommands = Maps.newTreeMap();
|
||||||
|
addSubcommand(new DbMigrateCommand<>(migrations, strategy, configurationClass));
|
||||||
|
addSubcommand(new DbStatusCommand<>(migrations, strategy, configurationClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSubcommand(AbstractLiquibaseCommand<T> subcommand) {
|
||||||
|
subcommands.put(subcommand.getName(), subcommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(Subparser subparser) {
|
||||||
|
for (AbstractLiquibaseCommand<T> subcommand : subcommands.values()) {
|
||||||
|
final Subparser cmdParser = subparser.addSubparsers()
|
||||||
|
.addParser(subcommand.getName())
|
||||||
|
.setDefault(COMMAND_NAME_ATTR, subcommand.getName())
|
||||||
|
.description(subcommand.getDescription());
|
||||||
|
subcommand.configure(cmdParser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(Namespace namespace, Liquibase liquibase) throws Exception {
|
||||||
|
final AbstractLiquibaseCommand<T> subcommand = subcommands.get(namespace.getString(COMMAND_NAME_ATTR));
|
||||||
|
subcommand.run(namespace, liquibase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.liquibase;
|
||||||
|
|
||||||
|
import io.dropwizard.Bundle;
|
||||||
|
import io.dropwizard.Configuration;
|
||||||
|
import io.dropwizard.db.DatabaseConfiguration;
|
||||||
|
import io.dropwizard.setup.Bootstrap;
|
||||||
|
import io.dropwizard.setup.Environment;
|
||||||
|
import io.dropwizard.util.Generics;
|
||||||
|
|
||||||
|
public abstract class NameableMigrationsBundle<T extends Configuration> implements Bundle, DatabaseConfiguration<T> {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final String migrations;
|
||||||
|
|
||||||
|
public NameableMigrationsBundle(String name, String migrations) {
|
||||||
|
this.name = name;
|
||||||
|
this.migrations = migrations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void initialize(Bootstrap<?> bootstrap) {
|
||||||
|
Class klass = Generics.getTypeParameter(this.getClass(), Configuration.class);
|
||||||
|
bootstrap.addCommand(new NameableDbCommand(name, migrations, this, klass));
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void run(Environment environment) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.mappers;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.ext.ExceptionMapper;
|
||||||
|
import javax.ws.rs.ext.Provider;
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
public class InvalidWebsocketAddressExceptionMapper implements ExceptionMapper<InvalidWebsocketAddressException> {
|
||||||
|
@Override
|
||||||
|
public Response toResponse(InvalidWebsocketAddressException exception) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.whispersystems.textsecuregcm.providers;
|
|
||||||
|
|
||||||
import com.codahale.metrics.health.HealthCheck;
|
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
public class MemcacheHealthCheck extends HealthCheck {
|
|
||||||
|
|
||||||
private final MemcachedClient client;
|
|
||||||
|
|
||||||
public MemcacheHealthCheck(MemcachedClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Result check() throws Exception {
|
|
||||||
if (client == null) {
|
|
||||||
return Result.unhealthy("not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
int random = SecureRandom.getInstance("SHA1PRNG").nextInt();
|
|
||||||
int value = SecureRandom.getInstance("SHA1PRNG").nextInt();
|
|
||||||
|
|
||||||
this.client.set("HEALTH" + random, 2000, String.valueOf(value));
|
|
||||||
String result = (String)this.client.get("HEALTH" + random);
|
|
||||||
|
|
||||||
if (result == null || Integer.parseInt(result) != value) {
|
|
||||||
return Result.unhealthy("Fetch failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.delete("HEALTH" + random);
|
|
||||||
|
|
||||||
return Result.healthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.whispersystems.textsecuregcm.providers;
|
|
||||||
|
|
||||||
import net.spy.memcached.AddrUtil;
|
|
||||||
import net.spy.memcached.ConnectionFactoryBuilder;
|
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
import net.spy.memcached.auth.AuthDescriptor;
|
|
||||||
import net.spy.memcached.auth.PlainCallbackHandler;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class MemcachedClientFactory {
|
|
||||||
|
|
||||||
private final MemcachedClient client;
|
|
||||||
|
|
||||||
public MemcachedClientFactory(MemcacheConfiguration config) throws IOException {
|
|
||||||
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
|
|
||||||
builder.setProtocol(ConnectionFactoryBuilder.Protocol.BINARY);
|
|
||||||
|
|
||||||
if (!Util.isEmpty(config.getUser())) {
|
|
||||||
AuthDescriptor ad = new AuthDescriptor(new String[] { "PLAIN" },
|
|
||||||
new PlainCallbackHandler(config.getUser(),
|
|
||||||
config.getPassword()));
|
|
||||||
|
|
||||||
builder.setAuthDescriptor(ad);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.client = new MemcachedClient(builder.build(),
|
|
||||||
AddrUtil.getAddresses(config.getServers()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public MemcachedClient getClient() {
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,9 +16,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.providers;
|
package org.whispersystems.textsecuregcm.providers;
|
||||||
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
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 org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
@@ -26,29 +31,40 @@ import redis.clients.jedis.JedisPool;
|
|||||||
import redis.clients.jedis.JedisPoolConfig;
|
import redis.clients.jedis.JedisPoolConfig;
|
||||||
import redis.clients.jedis.Protocol;
|
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;
|
private final JedisPool jedisPool;
|
||||||
|
|
||||||
public RedisClientFactory(String url) throws URISyntaxException {
|
public RedisClientFactory(String url) throws URISyntaxException {
|
||||||
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
||||||
poolConfig.setTestOnBorrow(true);
|
poolConfig.setTestOnBorrow(true);
|
||||||
|
|
||||||
URI redisURI = new URI(url);
|
URI redisURI = new URI(url);
|
||||||
String redisHost = redisURI.getHost();
|
|
||||||
int redisPort = redisURI.getPort();
|
|
||||||
String redisPassword = null;
|
|
||||||
|
|
||||||
if (!Util.isEmpty(redisURI.getUserInfo())) {
|
this.host = redisURI.getHost();
|
||||||
redisPassword = redisURI.getUserInfo().split(":",2)[1];
|
this.port = redisURI.getPort();
|
||||||
}
|
this.jedisPool = new JedisPool(poolConfig, host, port,
|
||||||
|
Protocol.DEFAULT_TIMEOUT, null);
|
||||||
this.jedisPool = new JedisPool(poolConfig, redisHost, redisPort,
|
|
||||||
Protocol.DEFAULT_TIMEOUT, redisPassword);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public JedisPool getRedisClientPool() {
|
public JedisPool getRedisClientPool() {
|
||||||
return jedisPool;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
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 org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ApnMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
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 {
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public ApnFallbackManager(PushServiceClient pushServiceClient) {
|
||||||
|
this.pushServiceClient = pushServiceClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void schedule(final WebsocketAddress address, ApnFallbackTask task) {
|
||||||
|
voipOneDelivery.mark();
|
||||||
|
taskQueue.put(address, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel(WebsocketAddress address) {
|
||||||
|
ApnFallbackTask task = taskQueue.remove(address);
|
||||||
|
|
||||||
|
if (task != null) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
pushServiceClient.send(new ApnMessage(task.getMessage(), task.getApnId(),
|
||||||
|
false, ApnMessage.MAX_EXPIRATION));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.warn("ApnFallbackThread", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 void put(WebsocketAddress address, ApnFallbackTask task) {
|
||||||
|
synchronized (tasks) {
|
||||||
|
tasks.put(address, task);
|
||||||
|
tasks.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -76,7 +76,13 @@ public class FeedbackHandler implements Managed, Runnable {
|
|||||||
event.getTimestamp() > device.get().getPushTimestamp())
|
event.getTimestamp() > device.get().getPushTimestamp())
|
||||||
{
|
{
|
||||||
logger.info("GCM Unregister Timestamp matches!");
|
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);
|
||||||
|
}
|
||||||
accountsManager.update(account.get());
|
accountsManager.update(account.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,23 +22,33 @@ import org.whispersystems.textsecuregcm.entities.ApnMessage;
|
|||||||
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
import org.whispersystems.textsecuregcm.entities.CryptoEncodingException;
|
||||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||||
import org.whispersystems.textsecuregcm.entities.GcmMessage;
|
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.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.dropwizard.lifecycle.Managed;
|
||||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
|
|
||||||
public class PushSender {
|
public class PushSender {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
|
private final Logger logger = LoggerFactory.getLogger(PushSender.class);
|
||||||
|
|
||||||
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"},\"content-available\":1,\"category\":\"Signal_Message\"}}";
|
private static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"badge\":%d,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
|
||||||
|
|
||||||
private final PushServiceClient pushServiceClient;
|
private final ApnFallbackManager apnFallbackManager;
|
||||||
private final WebsocketSender webSocketSender;
|
private final PushServiceClient pushServiceClient;
|
||||||
|
private final WebsocketSender webSocketSender;
|
||||||
|
|
||||||
public PushSender(PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
|
public PushSender(ApnFallbackManager apnFallbackManager, PushServiceClient pushServiceClient, WebsocketSender websocketSender) {
|
||||||
this.pushServiceClient = pushServiceClient;
|
this.apnFallbackManager = apnFallbackManager;
|
||||||
this.webSocketSender = websocketSender;
|
this.pushServiceClient = pushServiceClient;
|
||||||
|
this.webSocketSender = websocketSender;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
|
public void sendMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||||
@@ -50,8 +60,19 @@ public class PushSender {
|
|||||||
else throw new NotPushRegisteredException("No delivery possible!");
|
else throw new NotPushRegisteredException("No delivery possible!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WebsocketSender getWebSocketSender() {
|
||||||
|
return webSocketSender;
|
||||||
|
}
|
||||||
|
|
||||||
private void sendGcmMessage(Account account, Device device, OutgoingMessageSignal message)
|
private void sendGcmMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||||
throws TransientPushFailureException, NotPushRegisteredException
|
throws TransientPushFailureException, NotPushRegisteredException
|
||||||
|
{
|
||||||
|
if (device.getFetchesMessages()) sendNotificationGcmMessage(account, device, message);
|
||||||
|
else sendPayloadGcmMessage(account, device, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPayloadGcmMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||||
|
throws TransientPushFailureException, NotPushRegisteredException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
String number = account.getNumber();
|
String number = account.getNumber();
|
||||||
@@ -60,7 +81,7 @@ public class PushSender {
|
|||||||
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
|
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
|
||||||
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
|
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
|
||||||
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int) deviceId,
|
GcmMessage gcmMessage = new GcmMessage(registrationId, number, (int) deviceId,
|
||||||
encryptedMessage.toEncodedString(), isReceipt);
|
encryptedMessage.toEncodedString(), isReceipt, false);
|
||||||
|
|
||||||
pushServiceClient.send(gcmMessage);
|
pushServiceClient.send(gcmMessage);
|
||||||
} catch (CryptoEncodingException e) {
|
} catch (CryptoEncodingException e) {
|
||||||
@@ -68,20 +89,46 @@ public class PushSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendNotificationGcmMessage(Account account, Device device, OutgoingMessageSignal message)
|
||||||
|
throws TransientPushFailureException
|
||||||
|
{
|
||||||
|
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, message, WebsocketSender.Type.GCM);
|
||||||
|
|
||||||
|
if (!deliveryStatus.isDelivered()) {
|
||||||
|
GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(),
|
||||||
|
(int)device.getId(), "", false, true);
|
||||||
|
|
||||||
|
pushServiceClient.send(gcmMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void sendApnMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
|
private void sendApnMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
|
||||||
throws TransientPushFailureException
|
throws TransientPushFailureException
|
||||||
{
|
{
|
||||||
boolean online = webSocketSender.sendMessage(account, device, outgoingMessage, true);
|
DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.APN);
|
||||||
|
|
||||||
|
if (!deliveryStatus.isDelivered() && outgoingMessage.getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) {
|
||||||
|
ApnMessage apnMessage;
|
||||||
|
|
||||||
|
if (!Util.isEmpty(device.getVoipApnId())) {
|
||||||
|
apnMessage = new ApnMessage(device.getVoipApnId(), account.getNumber(), (int)device.getId(),
|
||||||
|
String.format(APN_PAYLOAD, deliveryStatus.getMessageQueueDepth()),
|
||||||
|
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, deliveryStatus.getMessageQueueDepth()),
|
||||||
|
false, ApnMessage.MAX_EXPIRATION);
|
||||||
|
}
|
||||||
|
|
||||||
if (!online && outgoingMessage.getType() != OutgoingMessageSignal.Type.RECEIPT_VALUE) {
|
|
||||||
ApnMessage apnMessage = new ApnMessage(device.getApnId(), account.getNumber(),
|
|
||||||
(int)device.getId(), APN_PAYLOAD);
|
|
||||||
pushServiceClient.send(apnMessage);
|
pushServiceClient.send(apnMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendWebSocketMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
|
private void sendWebSocketMessage(Account account, Device device, OutgoingMessageSignal outgoingMessage)
|
||||||
{
|
{
|
||||||
webSocketSender.sendMessage(account, device, outgoingMessage, false);
|
webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.WEB);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||||
|
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();
|
||||||
|
|
||||||
|
MessageProtos.OutgoingMessageSignal.Builder message =
|
||||||
|
MessageProtos.OutgoingMessageSignal.newBuilder()
|
||||||
|
.setSource(source.getNumber())
|
||||||
|
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
|
||||||
|
.setTimestamp(messageId)
|
||||||
|
.setType(MessageProtos.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,13 +19,15 @@ package org.whispersystems.textsecuregcm.push;
|
|||||||
import com.codahale.metrics.Meter;
|
import com.codahale.metrics.Meter;
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.codahale.metrics.SharedMetricRegistries;
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Constants;
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
|
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
@@ -34,6 +36,12 @@ import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessag
|
|||||||
|
|
||||||
public class WebsocketSender {
|
public class WebsocketSender {
|
||||||
|
|
||||||
|
public static enum Type {
|
||||||
|
APN,
|
||||||
|
GCM,
|
||||||
|
WEB
|
||||||
|
}
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class);
|
private static final Logger logger = LoggerFactory.getLogger(WebsocketSender.class);
|
||||||
|
|
||||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
@@ -44,15 +52,21 @@ public class WebsocketSender {
|
|||||||
private final Meter apnOnlineMeter = metricRegistry.meter(name(getClass(), "apn_online" ));
|
private final Meter apnOnlineMeter = metricRegistry.meter(name(getClass(), "apn_online" ));
|
||||||
private final Meter apnOfflineMeter = metricRegistry.meter(name(getClass(), "apn_offline"));
|
private final Meter apnOfflineMeter = metricRegistry.meter(name(getClass(), "apn_offline"));
|
||||||
|
|
||||||
private final StoredMessages storedMessages;
|
private final Meter gcmOnlineMeter = metricRegistry.meter(name(getClass(), "gcm_online" ));
|
||||||
private final PubSubManager pubSubManager;
|
private final Meter gcmOfflineMeter = metricRegistry.meter(name(getClass(), "gcm_offline"));
|
||||||
|
|
||||||
public WebsocketSender(StoredMessages storedMessages, PubSubManager pubSubManager) {
|
private final Meter provisioningOnlineMeter = metricRegistry.meter(name(getClass(), "provisioning_online" ));
|
||||||
this.storedMessages = storedMessages;
|
private final Meter provisioningOfflineMeter = metricRegistry.meter(name(getClass(), "provisioning_offline"));
|
||||||
this.pubSubManager = pubSubManager;
|
|
||||||
|
private final MessagesManager messagesManager;
|
||||||
|
private final PubSubManager pubSubManager;
|
||||||
|
|
||||||
|
public WebsocketSender(MessagesManager messagesManager, PubSubManager pubSubManager) {
|
||||||
|
this.messagesManager = messagesManager;
|
||||||
|
this.pubSubManager = pubSubManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean sendMessage(Account account, Device device, OutgoingMessageSignal message, boolean apn) {
|
public DeliveryStatus sendMessage(Account account, Device device, OutgoingMessageSignal message, Type channel) {
|
||||||
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||||
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
|
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
|
||||||
.setType(PubSubMessage.Type.DELIVER)
|
.setType(PubSubMessage.Type.DELIVER)
|
||||||
@@ -60,20 +74,56 @@ public class WebsocketSender {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
if (pubSubManager.publish(address, pubSubMessage)) {
|
if (pubSubManager.publish(address, pubSubMessage)) {
|
||||||
if (apn) apnOnlineMeter.mark();
|
if (channel == Type.APN) apnOnlineMeter.mark();
|
||||||
else websocketOnlineMeter.mark();
|
else if (channel == Type.GCM) gcmOnlineMeter.mark();
|
||||||
|
else websocketOnlineMeter.mark();
|
||||||
|
|
||||||
return true;
|
return new DeliveryStatus(true, 0);
|
||||||
} else {
|
} else {
|
||||||
if (apn) apnOfflineMeter.mark();
|
if (channel == Type.APN) apnOfflineMeter.mark();
|
||||||
else websocketOfflineMeter.mark();
|
else if (channel == Type.GCM) gcmOfflineMeter.mark();
|
||||||
|
else websocketOfflineMeter.mark();
|
||||||
|
|
||||||
storedMessages.insert(address, message);
|
int queueDepth = messagesManager.insert(account.getNumber(), device.getId(), message);
|
||||||
pubSubManager.publish(address, PubSubMessage.newBuilder()
|
pubSubManager.publish(address, PubSubMessage.newBuilder()
|
||||||
.setType(PubSubMessage.Type.QUERY_DB)
|
.setType(PubSubMessage.Type.QUERY_DB)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
return new DeliveryStatus(false, queueDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sendProvisioningMessage(ProvisioningAddress address, byte[] body) {
|
||||||
|
PubSubMessage pubSubMessage = PubSubMessage.newBuilder()
|
||||||
|
.setType(PubSubMessage.Type.DELIVER)
|
||||||
|
.setContent(ByteString.copyFrom(body))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if (pubSubManager.publish(address, pubSubMessage)) {
|
||||||
|
provisioningOnlineMeter.mark();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
provisioningOfflineMeter.mark();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class DeliveryStatus {
|
||||||
|
|
||||||
|
private final boolean delivered;
|
||||||
|
private final int messageQueueDepth;
|
||||||
|
|
||||||
|
public DeliveryStatus(boolean delivered, int messageQueueDepth) {
|
||||||
|
this.delivered = delivered;
|
||||||
|
this.messageQueueDepth = messageQueueDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDelivered() {
|
||||||
|
return delivered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMessageQueueDepth() {
|
||||||
|
return messageQueueDepth;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class SmsSender {
|
|||||||
try {
|
try {
|
||||||
twilioSender.deliverSmsVerification(destination, verificationCode);
|
twilioSender.deliverSmsVerification(destination, verificationCode);
|
||||||
} catch (TwilioRestException e) {
|
} catch (TwilioRestException e) {
|
||||||
logger.info("Twilio SMS Fallback", e);
|
logger.info("Twilio SMS Failed: " + e.getErrorMessage());
|
||||||
if (nexmoSender.isPresent()) {
|
if (nexmoSender.isPresent()) {
|
||||||
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
|
nexmoSender.get().deliverSmsVerification(destination, verificationCode);
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ public class SmsSender {
|
|||||||
try {
|
try {
|
||||||
twilioSender.deliverVoxVerification(destination, verificationCode);
|
twilioSender.deliverVoxVerification(destination, verificationCode);
|
||||||
} catch (TwilioRestException e) {
|
} catch (TwilioRestException e) {
|
||||||
logger.info("Twilio Vox Fallback", e);
|
logger.info("Twilio Vox Failed: " + e.getErrorMessage());
|
||||||
if (nexmoSender.isPresent()) {
|
if (nexmoSender.isPresent()) {
|
||||||
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
|
nexmoSender.get().deliverVoxVerification(destination, verificationCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class TwilioSmsSender {
|
|||||||
|
|
||||||
public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||||
"<Response>\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>";
|
"</Response>";
|
||||||
|
|
||||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.Set;
|
||||||
|
|
||||||
public class Account {
|
public class Account {
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ public class Account {
|
|||||||
private boolean supportsSms;
|
private boolean supportsSms;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private List<Device> devices = new LinkedList<>();
|
private Set<Device> devices = new HashSet<>();
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String identityKey;
|
private String identityKey;
|
||||||
@@ -47,7 +47,7 @@ public class Account {
|
|||||||
public Account() {}
|
public Account() {}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public Account(String number, boolean supportsSms, List<Device> devices) {
|
public Account(String number, boolean supportsSms, Set<Device> devices) {
|
||||||
this.number = number;
|
this.number = number;
|
||||||
this.supportsSms = supportsSms;
|
this.supportsSms = supportsSms;
|
||||||
this.devices = devices;
|
this.devices = devices;
|
||||||
@@ -78,14 +78,11 @@ public class Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addDevice(Device device) {
|
public void addDevice(Device device) {
|
||||||
|
this.devices.remove(device);
|
||||||
this.devices.add(device);
|
this.devices.add(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDevices(List<Device> devices) {
|
public Set<Device> getDevices() {
|
||||||
this.devices = devices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Device> getDevices() {
|
|
||||||
return devices;
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +110,9 @@ public class Account {
|
|||||||
long highestDevice = Device.MASTER_ID;
|
long highestDevice = Device.MASTER_ID;
|
||||||
|
|
||||||
for (Device device : devices) {
|
for (Device device : devices) {
|
||||||
if (device.getId() > highestDevice) {
|
if (!device.isActive()) {
|
||||||
|
return device.getId();
|
||||||
|
} else if (device.getId() > highestDevice) {
|
||||||
highestDevice = device.getId();
|
highestDevice = device.getId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,6 +120,16 @@ public class Account {
|
|||||||
return highestDevice + 1;
|
return highestDevice + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getActiveDeviceCount() {
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (Device device : devices) {
|
||||||
|
if (device.isActive()) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isRateLimited() {
|
public boolean isRateLimited() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,9 @@
|
|||||||
package org.whispersystems.textsecuregcm.storage;
|
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.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||||
@@ -33,23 +30,26 @@ import java.io.IOException;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
public class AccountsManager {
|
public class AccountsManager {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
|
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
|
||||||
|
|
||||||
private final Accounts accounts;
|
private final Accounts accounts;
|
||||||
private final MemcachedClient memcachedClient;
|
private final JedisPool cacheClient;
|
||||||
private final DirectoryManager directory;
|
private final DirectoryManager directory;
|
||||||
private final ObjectMapper mapper;
|
private final ObjectMapper mapper;
|
||||||
|
|
||||||
public AccountsManager(Accounts accounts,
|
public AccountsManager(Accounts accounts,
|
||||||
DirectoryManager directory,
|
DirectoryManager directory,
|
||||||
MemcachedClient memcachedClient)
|
JedisPool cacheClient)
|
||||||
{
|
{
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
this.memcachedClient = memcachedClient;
|
this.cacheClient = cacheClient;
|
||||||
this.mapper = SystemMapper.getMapper();
|
this.mapper = SystemMapper.getMapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getCount() {
|
public long getCount() {
|
||||||
@@ -112,25 +112,19 @@ public class AccountsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void memcacheSet(String number, Account account) {
|
private void memcacheSet(String number, Account account) {
|
||||||
if (memcachedClient != null) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
try {
|
jedis.set(getKey(number), mapper.writeValueAsString(account));
|
||||||
String json = mapper.writeValueAsString(account);
|
} catch (JsonProcessingException e) {
|
||||||
memcachedClient.set(getKey(number), 0, json);
|
throw new IllegalArgumentException(e);
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Account> memcacheGet(String number) {
|
private Optional<Account> memcacheGet(String number) {
|
||||||
if (memcachedClient == null) return Optional.absent();
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
|
String json = jedis.get(getKey(number));
|
||||||
try {
|
|
||||||
String json = (String)memcachedClient.get(getKey(number));
|
|
||||||
|
|
||||||
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
|
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
|
||||||
else return Optional.absent();
|
else return Optional.absent();
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.warn("AccountsManager", "Deserialization error", e);
|
logger.warn("AccountsManager", "Deserialization error", e);
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
|||||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class Device {
|
public class Device {
|
||||||
|
|
||||||
public static final long MASTER_ID = 1;
|
public static final long MASTER_ID = 1;
|
||||||
@@ -44,6 +46,9 @@ public class Device {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String apnId;
|
private String apnId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String voipApnId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private long pushTimestamp;
|
private long pushTimestamp;
|
||||||
|
|
||||||
@@ -56,12 +61,15 @@ public class Device {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private SignedPreKey signedPreKey;
|
private SignedPreKey signedPreKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long lastSeen;
|
||||||
|
|
||||||
public Device() {}
|
public Device() {}
|
||||||
|
|
||||||
public Device(long id, String authToken, String salt,
|
public Device(long id, String authToken, String salt,
|
||||||
String signalingKey, String gcmId, String apnId,
|
String signalingKey, String gcmId, String apnId,
|
||||||
boolean fetchesMessages, int registrationId,
|
String voipApnId, boolean fetchesMessages,
|
||||||
SignedPreKey signedPreKey)
|
int registrationId, SignedPreKey signedPreKey, long lastSeen)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
@@ -69,9 +77,11 @@ public class Device {
|
|||||||
this.signalingKey = signalingKey;
|
this.signalingKey = signalingKey;
|
||||||
this.gcmId = gcmId;
|
this.gcmId = gcmId;
|
||||||
this.apnId = apnId;
|
this.apnId = apnId;
|
||||||
|
this.voipApnId = voipApnId;
|
||||||
this.fetchesMessages = fetchesMessages;
|
this.fetchesMessages = fetchesMessages;
|
||||||
this.registrationId = registrationId;
|
this.registrationId = registrationId;
|
||||||
this.signedPreKey = signedPreKey;
|
this.signedPreKey = signedPreKey;
|
||||||
|
this.lastSeen = lastSeen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getApnId() {
|
public String getApnId() {
|
||||||
@@ -86,6 +96,22 @@ public class Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getVoipApnId() {
|
||||||
|
return voipApnId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVoipApnId(String voipApnId) {
|
||||||
|
this.voipApnId = voipApnId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastSeen(long lastSeen) {
|
||||||
|
this.lastSeen = lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastSeen() {
|
||||||
|
return lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
public String getGcmId() {
|
public String getGcmId() {
|
||||||
return gcmId;
|
return gcmId;
|
||||||
}
|
}
|
||||||
@@ -124,7 +150,10 @@ public class Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
|
boolean hasChannel = fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
|
||||||
|
|
||||||
|
return (id == MASTER_ID && hasChannel) ||
|
||||||
|
(id != MASTER_ID && hasChannel && signedPreKey != null && lastSeen > (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getFetchesMessages() {
|
public boolean getFetchesMessages() {
|
||||||
@@ -158,4 +187,17 @@ public class Device {
|
|||||||
public long getPushTimestamp() {
|
public long getPushTimestamp() {
|
||||||
return pushTimestamp;
|
return pushTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (other == null || !(other instanceof Device)) return false;
|
||||||
|
|
||||||
|
Device that = (Device)other;
|
||||||
|
return this.id == that.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return (int)this.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import org.skife.jdbi.v2.SQLStatement;
|
||||||
|
import org.skife.jdbi.v2.StatementContext;
|
||||||
|
import org.skife.jdbi.v2.sqlobject.Bind;
|
||||||
|
import org.skife.jdbi.v2.sqlobject.Binder;
|
||||||
|
import org.skife.jdbi.v2.sqlobject.BinderFactory;
|
||||||
|
import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
|
||||||
|
import org.skife.jdbi.v2.sqlobject.SqlQuery;
|
||||||
|
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
|
||||||
|
import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
|
||||||
|
import org.skife.jdbi.v2.tweak.ResultSetMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class Messages {
|
||||||
|
|
||||||
|
private static final String ID = "id";
|
||||||
|
private static final String TYPE = "type";
|
||||||
|
private static final String RELAY = "relay";
|
||||||
|
private static final String TIMESTAMP = "timestamp";
|
||||||
|
private static final String SOURCE = "source";
|
||||||
|
private static final String SOURCE_DEVICE = "source_device";
|
||||||
|
private static final String DESTINATION = "destination";
|
||||||
|
private static final String DESTINATION_DEVICE = "destination_device";
|
||||||
|
private static final String MESSAGE = "message";
|
||||||
|
|
||||||
|
@SqlQuery("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ") " +
|
||||||
|
"VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message) " +
|
||||||
|
"RETURNING (SELECT COUNT(id) FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device AND " + TYPE + " != " + OutgoingMessageSignal.Type.RECEIPT_VALUE + ")")
|
||||||
|
abstract int store(@MessageBinder OutgoingMessageSignal message,
|
||||||
|
@Bind("destination") String destination,
|
||||||
|
@Bind("destination_device") long destinationDevice);
|
||||||
|
|
||||||
|
@Mapper(MessageMapper.class)
|
||||||
|
@SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC")
|
||||||
|
abstract List<OutgoingMessageEntity> load(@Bind("destination") String destination,
|
||||||
|
@Bind("destination_device") long destinationDevice);
|
||||||
|
|
||||||
|
@Mapper(MessageMapper.class)
|
||||||
|
@SqlQuery("DELETE FROM messages WHERE " + ID + " IN (SELECT " + ID + " FROM messages WHERE " + DESTINATION + " = :destination AND " + SOURCE + " = :source AND " + TIMESTAMP + " = :timestamp ORDER BY " + ID + " LIMIT 1) RETURNING *")
|
||||||
|
abstract OutgoingMessageEntity remove(@Bind("destination") String destination, @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("VACUUM messages")
|
||||||
|
public abstract void vacuum();
|
||||||
|
|
||||||
|
public static class MessageMapper implements ResultSetMapper<OutgoingMessageEntity> {
|
||||||
|
@Override
|
||||||
|
public OutgoingMessageEntity map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||||
|
throws SQLException
|
||||||
|
{
|
||||||
|
return new OutgoingMessageEntity(resultSet.getLong(ID),
|
||||||
|
resultSet.getInt(TYPE),
|
||||||
|
resultSet.getString(RELAY),
|
||||||
|
resultSet.getLong(TIMESTAMP),
|
||||||
|
resultSet.getString(SOURCE),
|
||||||
|
resultSet.getInt(SOURCE_DEVICE),
|
||||||
|
resultSet.getBytes(MESSAGE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAnnotation(MessageBinder.AccountBinderFactory.class)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.PARAMETER})
|
||||||
|
public @interface MessageBinder {
|
||||||
|
public static class AccountBinderFactory implements BinderFactory {
|
||||||
|
@Override
|
||||||
|
public Binder build(Annotation annotation) {
|
||||||
|
return new Binder<MessageBinder, OutgoingMessageSignal>() {
|
||||||
|
@Override
|
||||||
|
public void bind(SQLStatement<?> sql,
|
||||||
|
MessageBinder accountBinder,
|
||||||
|
OutgoingMessageSignal message)
|
||||||
|
{
|
||||||
|
sql.bind(TYPE, message.getType());
|
||||||
|
sql.bind(RELAY, message.getRelay());
|
||||||
|
sql.bind(TIMESTAMP, message.getTimestamp());
|
||||||
|
sql.bind(SOURCE, message.getSource());
|
||||||
|
sql.bind(SOURCE_DEVICE, message.getSourceDevice());
|
||||||
|
sql.bind(MESSAGE, message.getMessage().toByteArray());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MessagesManager {
|
||||||
|
|
||||||
|
private final Messages messages;
|
||||||
|
|
||||||
|
public MessagesManager(Messages messages) {
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int insert(String destination, long destinationDevice, OutgoingMessageSignal message) {
|
||||||
|
return this.messages.store(message, destination, destinationDevice) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OutgoingMessageEntity> getMessagesForDevice(String destination, long destinationDevice) {
|
||||||
|
return this.messages.load(destination, destinationDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(String destination) {
|
||||||
|
this.messages.clear(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<OutgoingMessageEntity> delete(String destination, String source, long timestamp) {
|
||||||
|
return Optional.fromNullable(this.messages.remove(destination, source, timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String destination, long id) {
|
||||||
|
this.messages.remove(destination, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,20 +17,21 @@
|
|||||||
package org.whispersystems.textsecuregcm.storage;
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
public class PendingAccountsManager {
|
public class PendingAccountsManager {
|
||||||
|
|
||||||
private static final String MEMCACHE_PREFIX = "pending_account";
|
private static final String CACHE_PREFIX = "pending_account::";
|
||||||
|
|
||||||
private final PendingAccounts pendingAccounts;
|
private final PendingAccounts pendingAccounts;
|
||||||
private final MemcachedClient memcachedClient;
|
private final JedisPool cacheClient;
|
||||||
|
|
||||||
public PendingAccountsManager(PendingAccounts pendingAccounts,
|
public PendingAccountsManager(PendingAccounts pendingAccounts, JedisPool cacheClient)
|
||||||
MemcachedClient memcachedClient)
|
|
||||||
{
|
{
|
||||||
this.pendingAccounts = pendingAccounts;
|
this.pendingAccounts = pendingAccounts;
|
||||||
this.memcachedClient = memcachedClient;
|
this.cacheClient = cacheClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void store(String number, String code) {
|
public void store(String number, String code) {
|
||||||
@@ -58,22 +59,20 @@ public class PendingAccountsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void memcacheSet(String number, String code) {
|
private void memcacheSet(String number, String code) {
|
||||||
if (memcachedClient != null) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
jedis.set(CACHE_PREFIX + number, code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<String> memcacheGet(String number) {
|
private Optional<String> memcacheGet(String number) {
|
||||||
if (memcachedClient != null) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
|
return Optional.fromNullable(jedis.get(CACHE_PREFIX + number));
|
||||||
} else {
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void memcacheDelete(String number) {
|
private void memcacheDelete(String number) {
|
||||||
if (memcachedClient != null) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
jedis.del(CACHE_PREFIX + number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,20 +17,22 @@
|
|||||||
package org.whispersystems.textsecuregcm.storage;
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
public class PendingDevicesManager {
|
public class PendingDevicesManager {
|
||||||
|
|
||||||
private static final String MEMCACHE_PREFIX = "pending_devices";
|
private static final String CACHE_PREFIX = "pending_devices::";
|
||||||
|
|
||||||
private final PendingDevices pendingDevices;
|
private final PendingDevices pendingDevices;
|
||||||
private final MemcachedClient memcachedClient;
|
private final JedisPool cacheClient;
|
||||||
|
|
||||||
public PendingDevicesManager(PendingDevices pendingDevices,
|
public PendingDevicesManager(PendingDevices pendingDevices,
|
||||||
MemcachedClient memcachedClient)
|
JedisPool cacheClient)
|
||||||
{
|
{
|
||||||
this.pendingDevices = pendingDevices;
|
this.pendingDevices = pendingDevices;
|
||||||
this.memcachedClient = memcachedClient;
|
this.cacheClient = cacheClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void store(String number, String code) {
|
public void store(String number, String code) {
|
||||||
@@ -58,22 +60,20 @@ public class PendingDevicesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void memcacheSet(String number, String code) {
|
private void memcacheSet(String number, String code) {
|
||||||
if (memcachedClient != null) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
memcachedClient.set(MEMCACHE_PREFIX + number, 0, code);
|
jedis.set(CACHE_PREFIX + number, code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<String> memcacheGet(String number) {
|
private Optional<String> memcacheGet(String number) {
|
||||||
if (memcachedClient != null) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
return Optional.fromNullable((String)memcachedClient.get(MEMCACHE_PREFIX + number));
|
return Optional.fromNullable(jedis.get(CACHE_PREFIX + number));
|
||||||
} else {
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void memcacheDelete(String number) {
|
private void memcacheDelete(String number) {
|
||||||
if (memcachedClient != null) {
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
memcachedClient.delete(MEMCACHE_PREFIX + number);
|
jedis.del(CACHE_PREFIX + number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,88 @@
|
|||||||
package org.whispersystems.textsecuregcm.storage;
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.ByteString;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.dispatch.DispatchChannel;
|
||||||
|
import org.whispersystems.dispatch.DispatchManager;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
import io.dropwizard.lifecycle.Managed;
|
||||||
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||||
import redis.clients.jedis.BinaryJedisPubSub;
|
|
||||||
import redis.clients.jedis.Jedis;
|
import redis.clients.jedis.Jedis;
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
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 Logger logger = LoggerFactory.getLogger(PubSubManager.class);
|
||||||
private final SubscriptionListener baseListener = new SubscriptionListener();
|
|
||||||
private final Map<String, PubSubListener> listeners = new HashMap<>();
|
private final DispatchManager dispatchManager;
|
||||||
|
private final JedisPool jedisPool;
|
||||||
|
|
||||||
private final JedisPool jedisPool;
|
|
||||||
private boolean subscribed = false;
|
private boolean subscribed = false;
|
||||||
|
|
||||||
public PubSubManager(JedisPool jedisPool) {
|
public PubSubManager(JedisPool jedisPool, DispatchManager dispatchManager) {
|
||||||
this.jedisPool = jedisPool;
|
this.dispatchManager = dispatchManager;
|
||||||
initializePubSubWorker();
|
this.jedisPool = jedisPool;
|
||||||
waitForSubscription();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void subscribe(WebsocketAddress address, PubSubListener listener) {
|
@Override
|
||||||
String serializedAddress = address.serialize();
|
public void start() throws Exception {
|
||||||
|
this.dispatchManager.start();
|
||||||
|
|
||||||
listeners.put(serializedAddress, listener);
|
KeepaliveDispatchChannel keepaliveDispatchChannel = new KeepaliveDispatchChannel();
|
||||||
baseListener.subscribe(serializedAddress.getBytes());
|
this.dispatchManager.subscribe(KEEPALIVE_CHANNEL, keepaliveDispatchChannel);
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void unsubscribe(WebsocketAddress address, PubSubListener listener) {
|
synchronized (this) {
|
||||||
String serializedAddress = address.serialize();
|
while (!subscribed) wait(0);
|
||||||
|
|
||||||
if (listeners.get(serializedAddress) == listener) {
|
|
||||||
listeners.remove(serializedAddress);
|
|
||||||
baseListener.unsubscribe(serializedAddress.getBytes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new KeepaliveSender().start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized boolean publish(WebsocketAddress address, PubSubMessage message) {
|
@Override
|
||||||
|
public void stop() throws Exception {
|
||||||
|
dispatchManager.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void subscribe(WebsocketAddress address, DispatchChannel channel) {
|
||||||
|
String serializedAddress = address.serialize();
|
||||||
|
dispatchManager.subscribe(serializedAddress, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unsubscribe(WebsocketAddress address, DispatchChannel dispatchChannel) {
|
||||||
|
String serializedAddress = address.serialize();
|
||||||
|
dispatchManager.unsubscribe(serializedAddress, dispatchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasLocalSubscription(WebsocketAddress address) {
|
||||||
|
return dispatchManager.hasSubscription(address.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean publish(WebsocketAddress address, PubSubMessage message) {
|
||||||
return publish(address.serialize().getBytes(), 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()) {
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
return jedis.publish(channel, message.toByteArray()) != 0;
|
return jedis.publish(channel, message.toByteArray()) != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void waitForSubscription() {
|
private class KeepaliveDispatchChannel implements DispatchChannel {
|
||||||
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 {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(byte[] channel, byte[] message) {
|
public void onDispatchMessage(String channel, byte[] message) {
|
||||||
try {
|
// Good
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPMessage(byte[] s, byte[] s2, byte[] s3) {
|
public void onDispatchSubscribed(String channel) {
|
||||||
logger.warn("Received PMessage!");
|
if (KEEPALIVE_CHANNEL.equals(channel)) {
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSubscribe(byte[] channel, int count) {
|
|
||||||
if (Arrays.equals(KEEPALIVE_CHANNEL, channel)) {
|
|
||||||
synchronized (PubSubManager.this) {
|
synchronized (PubSubManager.this) {
|
||||||
subscribed = true;
|
subscribed = true;
|
||||||
PubSubManager.this.notifyAll();
|
PubSubManager.this.notifyAll();
|
||||||
@@ -132,12 +91,24 @@ public class PubSubManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUnsubscribe(byte[] s, int i) {}
|
public void onDispatchUnsubscribed(String channel) {
|
||||||
|
logger.warn("***** KEEPALIVE CHANNEL UNSUBSCRIBED *****");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KeepaliveSender extends Thread {
|
||||||
@Override
|
@Override
|
||||||
public void onPUnsubscribe(byte[] s, int i) {}
|
public void run() {
|
||||||
|
while (true) {
|
||||||
@Override
|
try {
|
||||||
public void onPSubscribe(byte[] s, int i) {}
|
Thread.sleep(20000);
|
||||||
|
publish(KEEPALIVE_CHANNEL.getBytes(), PubSubMessage.newBuilder()
|
||||||
|
.setType(PubSubMessage.Type.KEEPALIVE)
|
||||||
|
.build());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.warn("***** KEEPALIVE EXCEPTION ******", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,10 @@ public final class PubSubProtos {
|
|||||||
* <code>KEEPALIVE = 3;</code>
|
* <code>KEEPALIVE = 3;</code>
|
||||||
*/
|
*/
|
||||||
KEEPALIVE(3, 3),
|
KEEPALIVE(3, 3),
|
||||||
|
/**
|
||||||
|
* <code>CLOSE = 4;</code>
|
||||||
|
*/
|
||||||
|
CLOSE(4, 4),
|
||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,6 +180,10 @@ public final class PubSubProtos {
|
|||||||
* <code>KEEPALIVE = 3;</code>
|
* <code>KEEPALIVE = 3;</code>
|
||||||
*/
|
*/
|
||||||
public static final int KEEPALIVE_VALUE = 3;
|
public static final int KEEPALIVE_VALUE = 3;
|
||||||
|
/**
|
||||||
|
* <code>CLOSE = 4;</code>
|
||||||
|
*/
|
||||||
|
public static final int CLOSE_VALUE = 4;
|
||||||
|
|
||||||
|
|
||||||
public final int getNumber() { return value; }
|
public final int getNumber() { return value; }
|
||||||
@@ -186,6 +194,7 @@ public final class PubSubProtos {
|
|||||||
case 1: return QUERY_DB;
|
case 1: return QUERY_DB;
|
||||||
case 2: return DELIVER;
|
case 2: return DELIVER;
|
||||||
case 3: return KEEPALIVE;
|
case 3: return KEEPALIVE;
|
||||||
|
case 4: return CLOSE;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -611,12 +620,13 @@ public final class PubSubProtos {
|
|||||||
descriptor;
|
descriptor;
|
||||||
static {
|
static {
|
||||||
java.lang.String[] descriptorData = {
|
java.lang.String[] descriptorData = {
|
||||||
"\n\023PubSubMessage.proto\022\ntextsecure\"\215\001\n\rPu" +
|
"\n\023PubSubMessage.proto\022\ntextsecure\"\230\001\n\rPu" +
|
||||||
"bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" +
|
"bSubMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.P" +
|
||||||
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"=\n\004Ty" +
|
"ubSubMessage.Type\022\017\n\007content\030\002 \001(\014\"H\n\004Ty" +
|
||||||
"pe\022\013\n\007UNKNOWN\020\000\022\014\n\010QUERY_DB\020\001\022\013\n\007DELIVER" +
|
"pe\022\013\n\007UNKNOWN\020\000\022\014\n\010QUERY_DB\020\001\022\013\n\007DELIVER" +
|
||||||
"\020\002\022\r\n\tKEEPALIVE\020\003B8\n(org.whispersystems." +
|
"\020\002\022\r\n\tKEEPALIVE\020\003\022\t\n\005CLOSE\020\004B8\n(org.whis" +
|
||||||
"textsecuregcm.storageB\014PubSubProtos"
|
"persystems.textsecuregcm.storageB\014PubSub" +
|
||||||
|
"Protos"
|
||||||
};
|
};
|
||||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||||
|
|||||||
@@ -1,624 +0,0 @@
|
|||||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
||||||
// source: StoredMessage.proto
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.storage;
|
|
||||||
|
|
||||||
public final class StoredMessageProtos {
|
|
||||||
private StoredMessageProtos() {}
|
|
||||||
public static void registerAllExtensions(
|
|
||||||
com.google.protobuf.ExtensionRegistry registry) {
|
|
||||||
}
|
|
||||||
public interface StoredMessageOrBuilder
|
|
||||||
extends com.google.protobuf.MessageOrBuilder {
|
|
||||||
|
|
||||||
// optional .textsecure.StoredMessage.Type type = 1;
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
boolean hasType();
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type getType();
|
|
||||||
|
|
||||||
// optional bytes content = 2;
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
boolean hasContent();
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
com.google.protobuf.ByteString getContent();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Protobuf type {@code textsecure.StoredMessage}
|
|
||||||
*/
|
|
||||||
public static final class StoredMessage extends
|
|
||||||
com.google.protobuf.GeneratedMessage
|
|
||||||
implements StoredMessageOrBuilder {
|
|
||||||
// Use StoredMessage.newBuilder() to construct.
|
|
||||||
private StoredMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
|
|
||||||
super(builder);
|
|
||||||
this.unknownFields = builder.getUnknownFields();
|
|
||||||
}
|
|
||||||
private StoredMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
|
|
||||||
|
|
||||||
private static final StoredMessage defaultInstance;
|
|
||||||
public static StoredMessage getDefaultInstance() {
|
|
||||||
return defaultInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StoredMessage getDefaultInstanceForType() {
|
|
||||||
return defaultInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private final com.google.protobuf.UnknownFieldSet unknownFields;
|
|
||||||
@java.lang.Override
|
|
||||||
public final com.google.protobuf.UnknownFieldSet
|
|
||||||
getUnknownFields() {
|
|
||||||
return this.unknownFields;
|
|
||||||
}
|
|
||||||
private StoredMessage(
|
|
||||||
com.google.protobuf.CodedInputStream input,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
|
||||||
initFields();
|
|
||||||
int mutable_bitField0_ = 0;
|
|
||||||
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
|
|
||||||
com.google.protobuf.UnknownFieldSet.newBuilder();
|
|
||||||
try {
|
|
||||||
boolean done = false;
|
|
||||||
while (!done) {
|
|
||||||
int tag = input.readTag();
|
|
||||||
switch (tag) {
|
|
||||||
case 0:
|
|
||||||
done = true;
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
if (!parseUnknownField(input, unknownFields,
|
|
||||||
extensionRegistry, tag)) {
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 8: {
|
|
||||||
int rawValue = input.readEnum();
|
|
||||||
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type value = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.valueOf(rawValue);
|
|
||||||
if (value == null) {
|
|
||||||
unknownFields.mergeVarintField(1, rawValue);
|
|
||||||
} else {
|
|
||||||
bitField0_ |= 0x00000001;
|
|
||||||
type_ = value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 18: {
|
|
||||||
bitField0_ |= 0x00000002;
|
|
||||||
content_ = input.readBytes();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
|
||||||
throw e.setUnfinishedMessage(this);
|
|
||||||
} catch (java.io.IOException e) {
|
|
||||||
throw new com.google.protobuf.InvalidProtocolBufferException(
|
|
||||||
e.getMessage()).setUnfinishedMessage(this);
|
|
||||||
} finally {
|
|
||||||
this.unknownFields = unknownFields.build();
|
|
||||||
makeExtensionsImmutable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static final com.google.protobuf.Descriptors.Descriptor
|
|
||||||
getDescriptor() {
|
|
||||||
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
|
||||||
internalGetFieldAccessorTable() {
|
|
||||||
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_fieldAccessorTable
|
|
||||||
.ensureFieldAccessorsInitialized(
|
|
||||||
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.class, org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Builder.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static com.google.protobuf.Parser<StoredMessage> PARSER =
|
|
||||||
new com.google.protobuf.AbstractParser<StoredMessage>() {
|
|
||||||
public StoredMessage parsePartialFrom(
|
|
||||||
com.google.protobuf.CodedInputStream input,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
|
||||||
return new StoredMessage(input, extensionRegistry);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
public com.google.protobuf.Parser<StoredMessage> getParserForType() {
|
|
||||||
return PARSER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Protobuf enum {@code textsecure.StoredMessage.Type}
|
|
||||||
*/
|
|
||||||
public enum Type
|
|
||||||
implements com.google.protobuf.ProtocolMessageEnum {
|
|
||||||
/**
|
|
||||||
* <code>UNKNOWN = 0;</code>
|
|
||||||
*/
|
|
||||||
UNKNOWN(0, 0),
|
|
||||||
/**
|
|
||||||
* <code>MESSAGE = 1;</code>
|
|
||||||
*/
|
|
||||||
MESSAGE(1, 1),
|
|
||||||
;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <code>UNKNOWN = 0;</code>
|
|
||||||
*/
|
|
||||||
public static final int UNKNOWN_VALUE = 0;
|
|
||||||
/**
|
|
||||||
* <code>MESSAGE = 1;</code>
|
|
||||||
*/
|
|
||||||
public static final int MESSAGE_VALUE = 1;
|
|
||||||
|
|
||||||
|
|
||||||
public final int getNumber() { return value; }
|
|
||||||
|
|
||||||
public static Type valueOf(int value) {
|
|
||||||
switch (value) {
|
|
||||||
case 0: return UNKNOWN;
|
|
||||||
case 1: return MESSAGE;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static com.google.protobuf.Internal.EnumLiteMap<Type>
|
|
||||||
internalGetValueMap() {
|
|
||||||
return internalValueMap;
|
|
||||||
}
|
|
||||||
private static com.google.protobuf.Internal.EnumLiteMap<Type>
|
|
||||||
internalValueMap =
|
|
||||||
new com.google.protobuf.Internal.EnumLiteMap<Type>() {
|
|
||||||
public Type findValueByNumber(int number) {
|
|
||||||
return Type.valueOf(number);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public final com.google.protobuf.Descriptors.EnumValueDescriptor
|
|
||||||
getValueDescriptor() {
|
|
||||||
return getDescriptor().getValues().get(index);
|
|
||||||
}
|
|
||||||
public final com.google.protobuf.Descriptors.EnumDescriptor
|
|
||||||
getDescriptorForType() {
|
|
||||||
return getDescriptor();
|
|
||||||
}
|
|
||||||
public static final com.google.protobuf.Descriptors.EnumDescriptor
|
|
||||||
getDescriptor() {
|
|
||||||
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.getDescriptor().getEnumTypes().get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Type[] VALUES = values();
|
|
||||||
|
|
||||||
public static Type valueOf(
|
|
||||||
com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
|
|
||||||
if (desc.getType() != getDescriptor()) {
|
|
||||||
throw new java.lang.IllegalArgumentException(
|
|
||||||
"EnumValueDescriptor is not for this type.");
|
|
||||||
}
|
|
||||||
return VALUES[desc.getIndex()];
|
|
||||||
}
|
|
||||||
|
|
||||||
private final int index;
|
|
||||||
private final int value;
|
|
||||||
|
|
||||||
private Type(int index, int value) {
|
|
||||||
this.index = index;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @@protoc_insertion_point(enum_scope:textsecure.StoredMessage.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
private int bitField0_;
|
|
||||||
// optional .textsecure.StoredMessage.Type type = 1;
|
|
||||||
public static final int TYPE_FIELD_NUMBER = 1;
|
|
||||||
private org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type type_;
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
public boolean hasType() {
|
|
||||||
return ((bitField0_ & 0x00000001) == 0x00000001);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type getType() {
|
|
||||||
return type_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// optional bytes content = 2;
|
|
||||||
public static final int CONTENT_FIELD_NUMBER = 2;
|
|
||||||
private com.google.protobuf.ByteString content_;
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
public boolean hasContent() {
|
|
||||||
return ((bitField0_ & 0x00000002) == 0x00000002);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
public com.google.protobuf.ByteString getContent() {
|
|
||||||
return content_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initFields() {
|
|
||||||
type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
|
|
||||||
content_ = com.google.protobuf.ByteString.EMPTY;
|
|
||||||
}
|
|
||||||
private byte memoizedIsInitialized = -1;
|
|
||||||
public final boolean isInitialized() {
|
|
||||||
byte isInitialized = memoizedIsInitialized;
|
|
||||||
if (isInitialized != -1) return isInitialized == 1;
|
|
||||||
|
|
||||||
memoizedIsInitialized = 1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeTo(com.google.protobuf.CodedOutputStream output)
|
|
||||||
throws java.io.IOException {
|
|
||||||
getSerializedSize();
|
|
||||||
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
|
||||||
output.writeEnum(1, type_.getNumber());
|
|
||||||
}
|
|
||||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
|
||||||
output.writeBytes(2, content_);
|
|
||||||
}
|
|
||||||
getUnknownFields().writeTo(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int memoizedSerializedSize = -1;
|
|
||||||
public int getSerializedSize() {
|
|
||||||
int size = memoizedSerializedSize;
|
|
||||||
if (size != -1) return size;
|
|
||||||
|
|
||||||
size = 0;
|
|
||||||
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
|
||||||
size += com.google.protobuf.CodedOutputStream
|
|
||||||
.computeEnumSize(1, type_.getNumber());
|
|
||||||
}
|
|
||||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
|
||||||
size += com.google.protobuf.CodedOutputStream
|
|
||||||
.computeBytesSize(2, content_);
|
|
||||||
}
|
|
||||||
size += getUnknownFields().getSerializedSize();
|
|
||||||
memoizedSerializedSize = size;
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 0L;
|
|
||||||
@java.lang.Override
|
|
||||||
protected java.lang.Object writeReplace()
|
|
||||||
throws java.io.ObjectStreamException {
|
|
||||||
return super.writeReplace();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
|
|
||||||
com.google.protobuf.ByteString data)
|
|
||||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
|
||||||
return PARSER.parseFrom(data);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
|
|
||||||
com.google.protobuf.ByteString data,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
|
||||||
return PARSER.parseFrom(data, extensionRegistry);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(byte[] data)
|
|
||||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
|
||||||
return PARSER.parseFrom(data);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
|
|
||||||
byte[] data,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
|
||||||
return PARSER.parseFrom(data, extensionRegistry);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(java.io.InputStream input)
|
|
||||||
throws java.io.IOException {
|
|
||||||
return PARSER.parseFrom(input);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
|
|
||||||
java.io.InputStream input,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws java.io.IOException {
|
|
||||||
return PARSER.parseFrom(input, extensionRegistry);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseDelimitedFrom(java.io.InputStream input)
|
|
||||||
throws java.io.IOException {
|
|
||||||
return PARSER.parseDelimitedFrom(input);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseDelimitedFrom(
|
|
||||||
java.io.InputStream input,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws java.io.IOException {
|
|
||||||
return PARSER.parseDelimitedFrom(input, extensionRegistry);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
|
|
||||||
com.google.protobuf.CodedInputStream input)
|
|
||||||
throws java.io.IOException {
|
|
||||||
return PARSER.parseFrom(input);
|
|
||||||
}
|
|
||||||
public static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parseFrom(
|
|
||||||
com.google.protobuf.CodedInputStream input,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws java.io.IOException {
|
|
||||||
return PARSER.parseFrom(input, extensionRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder newBuilder() { return Builder.create(); }
|
|
||||||
public Builder newBuilderForType() { return newBuilder(); }
|
|
||||||
public static Builder newBuilder(org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage prototype) {
|
|
||||||
return newBuilder().mergeFrom(prototype);
|
|
||||||
}
|
|
||||||
public Builder toBuilder() { return newBuilder(this); }
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected Builder newBuilderForType(
|
|
||||||
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
|
|
||||||
Builder builder = new Builder(parent);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Protobuf type {@code textsecure.StoredMessage}
|
|
||||||
*/
|
|
||||||
public static final class Builder extends
|
|
||||||
com.google.protobuf.GeneratedMessage.Builder<Builder>
|
|
||||||
implements org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessageOrBuilder {
|
|
||||||
public static final com.google.protobuf.Descriptors.Descriptor
|
|
||||||
getDescriptor() {
|
|
||||||
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
|
||||||
internalGetFieldAccessorTable() {
|
|
||||||
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_fieldAccessorTable
|
|
||||||
.ensureFieldAccessorsInitialized(
|
|
||||||
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.class, org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Builder.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct using org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.newBuilder()
|
|
||||||
private Builder() {
|
|
||||||
maybeForceBuilderInitialization();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Builder(
|
|
||||||
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
|
|
||||||
super(parent);
|
|
||||||
maybeForceBuilderInitialization();
|
|
||||||
}
|
|
||||||
private void maybeForceBuilderInitialization() {
|
|
||||||
if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private static Builder create() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder clear() {
|
|
||||||
super.clear();
|
|
||||||
type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
|
|
||||||
bitField0_ = (bitField0_ & ~0x00000001);
|
|
||||||
content_ = com.google.protobuf.ByteString.EMPTY;
|
|
||||||
bitField0_ = (bitField0_ & ~0x00000002);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder clone() {
|
|
||||||
return create().mergeFrom(buildPartial());
|
|
||||||
}
|
|
||||||
|
|
||||||
public com.google.protobuf.Descriptors.Descriptor
|
|
||||||
getDescriptorForType() {
|
|
||||||
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.internal_static_textsecure_StoredMessage_descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage getDefaultInstanceForType() {
|
|
||||||
return org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.getDefaultInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage build() {
|
|
||||||
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage result = buildPartial();
|
|
||||||
if (!result.isInitialized()) {
|
|
||||||
throw newUninitializedMessageException(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage buildPartial() {
|
|
||||||
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage result = new org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage(this);
|
|
||||||
int from_bitField0_ = bitField0_;
|
|
||||||
int to_bitField0_ = 0;
|
|
||||||
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
|
|
||||||
to_bitField0_ |= 0x00000001;
|
|
||||||
}
|
|
||||||
result.type_ = type_;
|
|
||||||
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
|
|
||||||
to_bitField0_ |= 0x00000002;
|
|
||||||
}
|
|
||||||
result.content_ = content_;
|
|
||||||
result.bitField0_ = to_bitField0_;
|
|
||||||
onBuilt();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder mergeFrom(com.google.protobuf.Message other) {
|
|
||||||
if (other instanceof org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage) {
|
|
||||||
return mergeFrom((org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage)other);
|
|
||||||
} else {
|
|
||||||
super.mergeFrom(other);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder mergeFrom(org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage other) {
|
|
||||||
if (other == org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.getDefaultInstance()) return this;
|
|
||||||
if (other.hasType()) {
|
|
||||||
setType(other.getType());
|
|
||||||
}
|
|
||||||
if (other.hasContent()) {
|
|
||||||
setContent(other.getContent());
|
|
||||||
}
|
|
||||||
this.mergeUnknownFields(other.getUnknownFields());
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final boolean isInitialized() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder mergeFrom(
|
|
||||||
com.google.protobuf.CodedInputStream input,
|
|
||||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
|
||||||
throws java.io.IOException {
|
|
||||||
org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage parsedMessage = null;
|
|
||||||
try {
|
|
||||||
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
|
|
||||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
|
||||||
parsedMessage = (org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage) e.getUnfinishedMessage();
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
if (parsedMessage != null) {
|
|
||||||
mergeFrom(parsedMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
private int bitField0_;
|
|
||||||
|
|
||||||
// optional .textsecure.StoredMessage.Type type = 1;
|
|
||||||
private org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
public boolean hasType() {
|
|
||||||
return ((bitField0_ & 0x00000001) == 0x00000001);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
public org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type getType() {
|
|
||||||
return type_;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
public Builder setType(org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type value) {
|
|
||||||
if (value == null) {
|
|
||||||
throw new NullPointerException();
|
|
||||||
}
|
|
||||||
bitField0_ |= 0x00000001;
|
|
||||||
type_ = value;
|
|
||||||
onChanged();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional .textsecure.StoredMessage.Type type = 1;</code>
|
|
||||||
*/
|
|
||||||
public Builder clearType() {
|
|
||||||
bitField0_ = (bitField0_ & ~0x00000001);
|
|
||||||
type_ = org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage.Type.UNKNOWN;
|
|
||||||
onChanged();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// optional bytes content = 2;
|
|
||||||
private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
public boolean hasContent() {
|
|
||||||
return ((bitField0_ & 0x00000002) == 0x00000002);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
public com.google.protobuf.ByteString getContent() {
|
|
||||||
return content_;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
public Builder setContent(com.google.protobuf.ByteString value) {
|
|
||||||
if (value == null) {
|
|
||||||
throw new NullPointerException();
|
|
||||||
}
|
|
||||||
bitField0_ |= 0x00000002;
|
|
||||||
content_ = value;
|
|
||||||
onChanged();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional bytes content = 2;</code>
|
|
||||||
*/
|
|
||||||
public Builder clearContent() {
|
|
||||||
bitField0_ = (bitField0_ & ~0x00000002);
|
|
||||||
content_ = getDefaultInstance().getContent();
|
|
||||||
onChanged();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @@protoc_insertion_point(builder_scope:textsecure.StoredMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
defaultInstance = new StoredMessage(true);
|
|
||||||
defaultInstance.initFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @@protoc_insertion_point(class_scope:textsecure.StoredMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static com.google.protobuf.Descriptors.Descriptor
|
|
||||||
internal_static_textsecure_StoredMessage_descriptor;
|
|
||||||
private static
|
|
||||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
|
||||||
internal_static_textsecure_StoredMessage_fieldAccessorTable;
|
|
||||||
|
|
||||||
public static com.google.protobuf.Descriptors.FileDescriptor
|
|
||||||
getDescriptor() {
|
|
||||||
return descriptor;
|
|
||||||
}
|
|
||||||
private static com.google.protobuf.Descriptors.FileDescriptor
|
|
||||||
descriptor;
|
|
||||||
static {
|
|
||||||
java.lang.String[] descriptorData = {
|
|
||||||
"\n\023StoredMessage.proto\022\ntextsecure\"p\n\rSto" +
|
|
||||||
"redMessage\022,\n\004type\030\001 \001(\0162\036.textsecure.St" +
|
|
||||||
"oredMessage.Type\022\017\n\007content\030\002 \001(\014\" \n\004Typ" +
|
|
||||||
"e\022\013\n\007UNKNOWN\020\000\022\013\n\007MESSAGE\020\001B?\n(org.whisp" +
|
|
||||||
"ersystems.textsecuregcm.storageB\023StoredM" +
|
|
||||||
"essageProtos"
|
|
||||||
};
|
|
||||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
|
||||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
|
||||||
public com.google.protobuf.ExtensionRegistry assignDescriptors(
|
|
||||||
com.google.protobuf.Descriptors.FileDescriptor root) {
|
|
||||||
descriptor = root;
|
|
||||||
internal_static_textsecure_StoredMessage_descriptor =
|
|
||||||
getDescriptor().getMessageTypes().get(0);
|
|
||||||
internal_static_textsecure_StoredMessage_fieldAccessorTable = new
|
|
||||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
|
||||||
internal_static_textsecure_StoredMessage_descriptor,
|
|
||||||
new java.lang.String[] { "Type", "Content", });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
com.google.protobuf.Descriptors.FileDescriptor
|
|
||||||
.internalBuildGeneratedFileFrom(descriptorData,
|
|
||||||
new com.google.protobuf.Descriptors.FileDescriptor[] {
|
|
||||||
}, assigner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @@protoc_insertion_point(outer_class_scope)
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2014 Open WhisperSystems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.whispersystems.textsecuregcm.storage;
|
|
||||||
|
|
||||||
import com.codahale.metrics.Histogram;
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
|
||||||
import com.codahale.metrics.SharedMetricRegistries;
|
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Constants;
|
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
|
||||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
|
||||||
import static org.whispersystems.textsecuregcm.storage.StoredMessageProtos.StoredMessage;
|
|
||||||
import redis.clients.jedis.Jedis;
|
|
||||||
import redis.clients.jedis.JedisPool;
|
|
||||||
|
|
||||||
public class StoredMessages {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(StoredMessages.class);
|
|
||||||
|
|
||||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
|
||||||
private final Histogram queueSizeHistogram = metricRegistry.histogram(name(getClass(), "queue_size"));
|
|
||||||
|
|
||||||
private static final String QUEUE_PREFIX = "msgs";
|
|
||||||
|
|
||||||
private final JedisPool jedisPool;
|
|
||||||
|
|
||||||
public StoredMessages(JedisPool jedisPool) {
|
|
||||||
this.jedisPool = jedisPool;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear(WebsocketAddress address) {
|
|
||||||
try (Jedis jedis = jedisPool.getResource()) {
|
|
||||||
jedis.del(getKey(address));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insert(WebsocketAddress address, OutgoingMessageSignal message) {
|
|
||||||
try (Jedis jedis = jedisPool.getResource()) {
|
|
||||||
byte[] queue = getKey(address);
|
|
||||||
StoredMessage storedMessage = StoredMessage.newBuilder()
|
|
||||||
.setType(StoredMessage.Type.MESSAGE)
|
|
||||||
.setContent(message.toByteString())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
long queueSize = jedis.lpush(queue, storedMessage.toByteArray());
|
|
||||||
queueSizeHistogram.update(queueSize);
|
|
||||||
|
|
||||||
jedis.expireAt(queue, (System.currentTimeMillis() / 1000) + TimeUnit.DAYS.toSeconds(30));
|
|
||||||
|
|
||||||
if (queueSize > 1000) {
|
|
||||||
jedis.ltrim(getKey(address), 0, 999);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<OutgoingMessageSignal> getMessagesForDevice(WebsocketAddress address) {
|
|
||||||
List<OutgoingMessageSignal> messages = new LinkedList<>();
|
|
||||||
|
|
||||||
try (Jedis jedis = jedisPool.getResource()) {
|
|
||||||
byte[] message;
|
|
||||||
|
|
||||||
while ((message = jedis.rpop(getKey(address))) != null) {
|
|
||||||
try {
|
|
||||||
StoredMessage storedMessage = StoredMessage.parseFrom(message);
|
|
||||||
|
|
||||||
if (storedMessage.getType().getNumber() == StoredMessage.Type.MESSAGE_VALUE) {
|
|
||||||
messages.add(OutgoingMessageSignal.parseFrom(storedMessage.getContent()));
|
|
||||||
} else {
|
|
||||||
logger.warn("Unkown stored message type: " + storedMessage.getType().getNumber());
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (InvalidProtocolBufferException e) {
|
|
||||||
logger.warn("Error parsing protobuf", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] getKey(WebsocketAddress address) {
|
|
||||||
return (QUEUE_PREFIX + ":" + address.serialize()).getBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,7 @@ import java.net.URLEncoder;
|
|||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class Util {
|
public class Util {
|
||||||
|
|
||||||
@@ -114,9 +115,21 @@ public class Util {
|
|||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void sleep(int i) {
|
public static void sleep(long i) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(i);
|
Thread.sleep(i);
|
||||||
} catch (InterruptedException ie) {}
|
} catch (InterruptedException ie) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void wait(Object object) {
|
||||||
|
try {
|
||||||
|
object.wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long todayInMillis() {
|
||||||
|
return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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.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 MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
private static final Histogram durationHistogram = metricRegistry.histogram(name(WebSocketConnection.class, "connected_duration"));
|
||||||
|
|
||||||
|
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,
|
||||||
|
ReceiptSender receiptSender, MessagesManager messagesManager,
|
||||||
|
PubSubManager pubSubManager, ApnFallbackManager apnFallbackManager)
|
||||||
|
{
|
||||||
|
this.accountsManager = accountsManager;
|
||||||
|
this.pushSender = pushSender;
|
||||||
|
this.receiptSender = receiptSender;
|
||||||
|
this.messagesManager = messagesManager;
|
||||||
|
this.pubSubManager = pubSubManager;
|
||||||
|
this.apnFallbackManager = apnFallbackManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketConnect(WebSocketSessionContext context) {
|
||||||
|
final Account account = context.getAuthenticated(Account.class).get();
|
||||||
|
final Device device = account.getAuthenticatedDevice().get();
|
||||||
|
final long connectTime = System.currentTimeMillis();
|
||||||
|
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
|
||||||
|
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
|
||||||
|
messagesManager, account, device,
|
||||||
|
context.getClient());
|
||||||
|
|
||||||
|
apnFallbackManager.cancel(address);
|
||||||
|
updateLastSeen(account, device);
|
||||||
|
pubSubManager.subscribe(address, connection);
|
||||||
|
|
||||||
|
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
|
||||||
|
pubSubManager.unsubscribe(address, connection);
|
||||||
|
durationHistogram.update(System.currentTimeMillis() - connectTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLastSeen(Account account, Device device) {
|
||||||
|
if (device.getLastSeen() != Util.todayInMillis()) {
|
||||||
|
device.setLastSeen(Util.todayInMillis());
|
||||||
|
accountsManager.update(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package org.whispersystems.textsecuregcm.websocket;
|
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
|
||||||
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
|
||||||
import org.whispersystems.websocket.setup.WebSocketConnectListener;
|
|
||||||
|
|
||||||
public class ConnectListener implements WebSocketConnectListener {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
|
||||||
|
|
||||||
private final AccountsManager accountsManager;
|
|
||||||
private final PushSender pushSender;
|
|
||||||
private final StoredMessages storedMessages;
|
|
||||||
private final PubSubManager pubSubManager;
|
|
||||||
|
|
||||||
public ConnectListener(AccountsManager accountsManager, PushSender pushSender,
|
|
||||||
StoredMessages storedMessages, PubSubManager pubSubManager)
|
|
||||||
{
|
|
||||||
this.accountsManager = accountsManager;
|
|
||||||
this.pushSender = pushSender;
|
|
||||||
this.storedMessages = storedMessages;
|
|
||||||
this.pubSubManager = pubSubManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWebSocketConnect(WebSocketSessionContext context) {
|
|
||||||
Optional<Account> account = context.getAuthenticated(Account.class);
|
|
||||||
|
|
||||||
if (!account.isPresent()) {
|
|
||||||
logger.debug("WS Connection with no authentication...");
|
|
||||||
context.getClient().close(4001, "Authentication failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<Device> device = account.get().getAuthenticatedDevice();
|
|
||||||
|
|
||||||
if (!device.isPresent()) {
|
|
||||||
logger.debug("WS Connection with no authenticated device...");
|
|
||||||
context.getClient().close(4001, "Device authentication failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender,
|
|
||||||
storedMessages, pubSubManager,
|
|
||||||
account.get(), device.get(),
|
|
||||||
context.getClient());
|
|
||||||
|
|
||||||
connection.onConnected();
|
|
||||||
|
|
||||||
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
|
|
||||||
@Override
|
|
||||||
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
|
|
||||||
connection.onConnectionLost();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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.OutgoingMessageSignal;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
logger.warn("Handling dead letter to: " + channel);
|
||||||
|
|
||||||
|
WebsocketAddress address = new WebsocketAddress(channel);
|
||||||
|
PubSubMessage pubSubMessage = PubSubMessage.parseFrom(data);
|
||||||
|
|
||||||
|
switch (pubSubMessage.getType().getNumber()) {
|
||||||
|
case PubSubMessage.Type.DELIVER_VALUE:
|
||||||
|
OutgoingMessageSignal message = OutgoingMessageSignal.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.websocket;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
public class ProvisioningAddress extends WebsocketAddress {
|
||||||
|
|
||||||
|
public ProvisioningAddress(String address, int id) throws InvalidWebsocketAddressException {
|
||||||
|
super(address, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProvisioningAddress(String serialized) throws InvalidWebsocketAddressException {
|
||||||
|
super(serialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return getNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProvisioningAddress generate() {
|
||||||
|
try {
|
||||||
|
byte[] random = new byte[16];
|
||||||
|
SecureRandom.getInstance("SHA1PRNG").nextBytes(random);
|
||||||
|
|
||||||
|
return new ProvisioningAddress(Base64.encodeBytesWithoutPadding(random)
|
||||||
|
.replace('+', '-').replace('/', '_'), 0);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidWebsocketAddressException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.websocket;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
|
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
||||||
|
import org.whispersystems.websocket.setup.WebSocketConnectListener;
|
||||||
|
|
||||||
|
public class ProvisioningConnectListener implements WebSocketConnectListener {
|
||||||
|
|
||||||
|
private final PubSubManager pubSubManager;
|
||||||
|
|
||||||
|
public ProvisioningConnectListener(PubSubManager pubSubManager) {
|
||||||
|
this.pubSubManager = pubSubManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWebSocketConnect(WebSocketSessionContext context) {
|
||||||
|
final ProvisioningConnection connection = new ProvisioningConnection(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) {
|
||||||
|
pubSubManager.unsubscribe(provisioningAddress, connection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.websocket;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.dispatch.DispatchChannel;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.MessageProtos.ProvisioningUuid;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
||||||
|
import org.whispersystems.websocket.WebSocketClient;
|
||||||
|
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||||
|
|
||||||
|
public class ProvisioningConnection implements DispatchChannel {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ProvisioningConnection.class);
|
||||||
|
|
||||||
|
private final WebSocketClient client;
|
||||||
|
|
||||||
|
public ProvisioningConnection(WebSocketClient client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDispatchMessage(String channel, byte[] message) {
|
||||||
|
try {
|
||||||
|
PubSubMessage outgoingMessage = PubSubMessage.parseFrom(message);
|
||||||
|
|
||||||
|
if (outgoingMessage.getType() == PubSubMessage.Type.DELIVER) {
|
||||||
|
Optional<byte[]> body = Optional.of(outgoingMessage.getContent().toByteArray());
|
||||||
|
|
||||||
|
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/v1/message", body);
|
||||||
|
|
||||||
|
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(WebSocketResponseMessage webSocketResponseMessage) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDispatchUnsubscribed(String channel) {
|
||||||
|
this.client.close(1001, "Closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,9 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Acc
|
|||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicCredentials credentials = new BasicCredentials(usernames[0], passwords[0]);
|
BasicCredentials credentials = new BasicCredentials(usernames[0].replace(" ", "+"),
|
||||||
|
passwords[0].replace(" ", "+"));
|
||||||
|
|
||||||
return accountAuthenticator.authenticate(credentials);
|
return accountAuthenticator.authenticate(credentials);
|
||||||
} catch (io.dropwizard.auth.AuthenticationException e) {
|
} catch (io.dropwizard.auth.AuthenticationException e) {
|
||||||
throw new AuthenticationException(e);
|
throw new AuthenticationException(e);
|
||||||
|
|||||||
@@ -4,80 +4,72 @@ import com.google.common.base.Optional;
|
|||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.CryptoEncodingException;
|
||||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubListener;
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
|
||||||
import org.whispersystems.websocket.WebSocketClient;
|
import org.whispersystems.websocket.WebSocketClient;
|
||||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
|
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 static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
|
||||||
|
|
||||||
private final AccountsManager accountsManager;
|
private final ReceiptSender receiptSender;
|
||||||
private final PushSender pushSender;
|
private final PushSender pushSender;
|
||||||
private final StoredMessages storedMessages;
|
private final MessagesManager messagesManager;
|
||||||
private final PubSubManager pubSubManager;
|
|
||||||
|
|
||||||
private final Account account;
|
private final Account account;
|
||||||
private final Device device;
|
private final Device device;
|
||||||
private final WebsocketAddress address;
|
|
||||||
private final WebSocketClient client;
|
private final WebSocketClient client;
|
||||||
|
|
||||||
public WebSocketConnection(AccountsManager accountsManager,
|
public WebSocketConnection(PushSender pushSender,
|
||||||
PushSender pushSender,
|
ReceiptSender receiptSender,
|
||||||
StoredMessages storedMessages,
|
MessagesManager messagesManager,
|
||||||
PubSubManager pubSubManager,
|
|
||||||
Account account,
|
Account account,
|
||||||
Device device,
|
Device device,
|
||||||
WebSocketClient client)
|
WebSocketClient client)
|
||||||
{
|
{
|
||||||
this.accountsManager = accountsManager;
|
|
||||||
this.pushSender = pushSender;
|
this.pushSender = pushSender;
|
||||||
this.storedMessages = storedMessages;
|
this.receiptSender = receiptSender;
|
||||||
this.pubSubManager = pubSubManager;
|
this.messagesManager = messagesManager;
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.client = client;
|
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
|
@Override
|
||||||
public void onPubSubMessage(PubSubMessage pubSubMessage) {
|
public void onDispatchMessage(String channel, byte[] message) {
|
||||||
try {
|
try {
|
||||||
|
PubSubMessage pubSubMessage = PubSubMessage.parseFrom(message);
|
||||||
|
|
||||||
switch (pubSubMessage.getType().getNumber()) {
|
switch (pubSubMessage.getType().getNumber()) {
|
||||||
case PubSubMessage.Type.QUERY_DB_VALUE:
|
case PubSubMessage.Type.QUERY_DB_VALUE:
|
||||||
processStoredMessages();
|
processStoredMessages();
|
||||||
break;
|
break;
|
||||||
case PubSubMessage.Type.DELIVER_VALUE:
|
case PubSubMessage.Type.DELIVER_VALUE:
|
||||||
sendMessage(OutgoingMessageSignal.parseFrom(pubSubMessage.getContent()));
|
sendMessage(OutgoingMessageSignal.parseFrom(pubSubMessage.getContent()), Optional.<Long>absent());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
logger.warn("Unknown pubsub message: " + pubSubMessage.getType().getNumber());
|
logger.warn("Unknown pubsub message: " + pubSubMessage.getType().getNumber());
|
||||||
@@ -87,7 +79,18 @@ public class WebSocketConnection implements PubSubListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessage(final OutgoingMessageSignal message) {
|
@Override
|
||||||
|
public void onDispatchUnsubscribed(String channel) {
|
||||||
|
client.close(1000, "OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDispatchSubscribed(String channel) {
|
||||||
|
processStoredMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage(final OutgoingMessageSignal message,
|
||||||
|
final Optional<Long> storedMessageId)
|
||||||
|
{
|
||||||
try {
|
try {
|
||||||
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
|
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
|
||||||
Optional<byte[]> body = Optional.fromNullable(encryptedMessage.toByteArray());
|
Optional<byte[]> body = Optional.fromNullable(encryptedMessage.toByteArray());
|
||||||
@@ -98,16 +101,17 @@ public class WebSocketConnection implements PubSubListener {
|
|||||||
public void onSuccess(@Nullable WebSocketResponseMessage response) {
|
public void onSuccess(@Nullable WebSocketResponseMessage response) {
|
||||||
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
|
boolean isReceipt = message.getType() == OutgoingMessageSignal.Type.RECEIPT_VALUE;
|
||||||
|
|
||||||
if (isSuccessResponse(response) && !isReceipt) {
|
if (isSuccessResponse(response)) {
|
||||||
sendDeliveryReceiptFor(message);
|
if (storedMessageId.isPresent()) messagesManager.delete(account.getNumber(), storedMessageId.get());
|
||||||
} else if (!isSuccessResponse(response)) {
|
if (!isReceipt) sendDeliveryReceiptFor(message);
|
||||||
|
} else if (!isSuccessResponse(response) && !storedMessageId.isPresent()) {
|
||||||
requeueMessage(message);
|
requeueMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@Nonnull Throwable throwable) {
|
public void onFailure(@Nonnull Throwable throwable) {
|
||||||
requeueMessage(message);
|
if (!storedMessageId.isPresent()) requeueMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSuccessResponse(WebSocketResponseMessage response) {
|
private boolean isSuccessResponse(WebSocketResponseMessage response) {
|
||||||
@@ -124,39 +128,40 @@ public class WebSocketConnection implements PubSubListener {
|
|||||||
pushSender.sendMessage(account, device, message);
|
pushSender.sendMessage(account, device, message);
|
||||||
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
||||||
logger.warn("requeueMessage", e);
|
logger.warn("requeueMessage", e);
|
||||||
storedMessages.insert(address, message);
|
messagesManager.insert(account.getNumber(), device.getId(), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendDeliveryReceiptFor(OutgoingMessageSignal message) {
|
private void sendDeliveryReceiptFor(OutgoingMessageSignal message) {
|
||||||
try {
|
try {
|
||||||
Optional<Account> source = accountsManager.get(message.getSource());
|
receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp(),
|
||||||
|
message.hasRelay() ? Optional.of(message.getRelay()) :
|
||||||
if (!source.isPresent()) {
|
Optional.<String>absent());
|
||||||
logger.warn("Source account disappeared? (%s)", message.getSource());
|
} catch (IOException | NoSuchUserException | TransientPushFailureException | NotPushRegisteredException e) {
|
||||||
return;
|
logger.warn("sendDeliveryReceiptFor", e);
|
||||||
}
|
} catch (WebApplicationException e) {
|
||||||
|
logger.warn("Bad federated response for receipt: " + e.getResponse().getStatus());
|
||||||
OutgoingMessageSignal.Builder receipt =
|
|
||||||
OutgoingMessageSignal.newBuilder()
|
|
||||||
.setSource(account.getNumber())
|
|
||||||
.setSourceDevice((int) device.getId())
|
|
||||||
.setTimestamp(message.getTimestamp())
|
|
||||||
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
|
|
||||||
|
|
||||||
for (Device device : source.get().getDevices()) {
|
|
||||||
pushSender.sendMessage(source.get(), device, receipt.build());
|
|
||||||
}
|
|
||||||
} catch (NotPushRegisteredException | TransientPushFailureException e) {
|
|
||||||
logger.warn("sendDeliveryReceiptFor", "Delivery receipet", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processStoredMessages() {
|
private void processStoredMessages() {
|
||||||
List<OutgoingMessageSignal> messages = storedMessages.getMessagesForDevice(address);
|
List<OutgoingMessageEntity> messages = messagesManager.getMessagesForDevice(account.getNumber(), device.getId());
|
||||||
|
|
||||||
for (OutgoingMessageSignal message : messages) {
|
for (OutgoingMessageEntity message : messages) {
|
||||||
sendMessage(message);
|
OutgoingMessageSignal.Builder builder = OutgoingMessageSignal.newBuilder()
|
||||||
|
.setType(message.getType())
|
||||||
|
.setMessage(ByteString.copyFrom(message.getMessage()))
|
||||||
|
.setSourceDevice(message.getSourceDevice())
|
||||||
|
.setSource(message.getSource())
|
||||||
|
.setTimestamp(message.getTimestamp());
|
||||||
|
|
||||||
|
if (message.getRelay() != null && !message.getRelay().isEmpty()) {
|
||||||
|
builder.setRelay(message.getRelay());
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(builder.build(), Optional.of(message.getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,29 @@ public class WebsocketAddress {
|
|||||||
this.deviceId = deviceId;
|
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() {
|
public String serialize() {
|
||||||
return number + ":" + deviceId;
|
return number + ":" + deviceId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,11 @@
|
|||||||
package org.whispersystems.textsecuregcm.workers;
|
package org.whispersystems.textsecuregcm.workers;
|
||||||
|
|
||||||
import net.sourceforge.argparse4j.inf.Namespace;
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
import org.skife.jdbi.v2.DBI;
|
import org.skife.jdbi.v2.DBI;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
@@ -62,10 +60,10 @@ public class DirectoryCommand extends ConfiguredCommand<WhisperServerConfigurati
|
|||||||
dbi.registerContainerFactory(new OptionalContainerFactory());
|
dbi.registerContainerFactory(new OptionalContainerFactory());
|
||||||
|
|
||||||
Accounts accounts = dbi.onDemand(Accounts.class);
|
Accounts accounts = dbi.onDemand(Accounts.class);
|
||||||
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
|
JedisPool cacheClient = new RedisClientFactory(config.getCacheConfiguration().getUrl()).getRedisClientPool();
|
||||||
JedisPool redisClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
JedisPool redisClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
||||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
DirectoryManager directory = new DirectoryManager(redisClient);
|
||||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
|
||||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||||
|
|
||||||
DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory);
|
DirectoryUpdater update = new DirectoryUpdater(accountsManager, federatedClientManager, directory);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Messages;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
||||||
|
|
||||||
import io.dropwizard.cli.ConfiguredCommand;
|
import io.dropwizard.cli.ConfiguredCommand;
|
||||||
@@ -32,17 +33,25 @@ public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration>
|
|||||||
WhisperServerConfiguration config)
|
WhisperServerConfiguration config)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
DataSourceFactory dbConfig = config.getDataSourceFactory();
|
DataSourceFactory dbConfig = config.getDataSourceFactory();
|
||||||
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword());
|
DataSourceFactory messageDbConfig = config.getMessageStoreConfiguration();
|
||||||
|
DBI dbi = new DBI(dbConfig.getUrl(), dbConfig.getUser(), dbConfig.getPassword() );
|
||||||
|
DBI messageDbi = new DBI(messageDbConfig.getUrl(), messageDbConfig.getUser(), messageDbConfig.getPassword());
|
||||||
|
|
||||||
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
|
dbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
|
||||||
dbi.registerContainerFactory(new ImmutableListContainerFactory());
|
dbi.registerContainerFactory(new ImmutableListContainerFactory());
|
||||||
dbi.registerContainerFactory(new ImmutableSetContainerFactory());
|
dbi.registerContainerFactory(new ImmutableSetContainerFactory());
|
||||||
dbi.registerContainerFactory(new OptionalContainerFactory());
|
dbi.registerContainerFactory(new OptionalContainerFactory());
|
||||||
|
|
||||||
|
messageDbi.registerArgumentFactory(new OptionalArgumentFactory(dbConfig.getDriverClass()));
|
||||||
|
messageDbi.registerContainerFactory(new ImmutableListContainerFactory());
|
||||||
|
messageDbi.registerContainerFactory(new ImmutableSetContainerFactory());
|
||||||
|
messageDbi.registerContainerFactory(new OptionalContainerFactory());
|
||||||
|
|
||||||
Accounts accounts = dbi.onDemand(Accounts.class );
|
Accounts accounts = dbi.onDemand(Accounts.class );
|
||||||
Keys keys = dbi.onDemand(Keys.class );
|
Keys keys = dbi.onDemand(Keys.class );
|
||||||
PendingAccounts pendingAccounts = dbi.onDemand(PendingAccounts.class);
|
PendingAccounts pendingAccounts = dbi.onDemand(PendingAccounts.class);
|
||||||
|
Messages messages = dbi.onDemand(Messages.class );
|
||||||
|
|
||||||
logger.warn("Vacuuming accounts...");
|
logger.warn("Vacuuming accounts...");
|
||||||
accounts.vacuum();
|
accounts.vacuum();
|
||||||
@@ -53,6 +62,9 @@ public class VacuumCommand extends ConfiguredCommand<WhisperServerConfiguration>
|
|||||||
logger.warn("Vacuuming keys...");
|
logger.warn("Vacuuming keys...");
|
||||||
keys.vacuum();
|
keys.vacuum();
|
||||||
|
|
||||||
|
logger.warn("Vacuuming messages...");
|
||||||
|
messages.vacuum();
|
||||||
|
|
||||||
Thread.sleep(3000);
|
Thread.sleep(3000);
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/main/resources/messagedb.xml
Normal file
60
src/main/resources/messagedb.xml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
|
||||||
|
|
||||||
|
<changeSet id="1" author="moxie">
|
||||||
|
<createTable tableName="messages">
|
||||||
|
<column name="id" type="bigint" autoIncrement="true">
|
||||||
|
<constraints primaryKey="true" nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="type" type="tinyint">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="relay" type="text">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="timestamp" type="bigint">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="source" type="text">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="source_device" type="int">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="destination" type="text">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="destination_device" type="int">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="message" type="bytea">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<createIndex tableName="messages" indexName="destination_index">
|
||||||
|
<column name="destination"></column>
|
||||||
|
<column name="destination_device"></column>
|
||||||
|
</createIndex>
|
||||||
|
|
||||||
|
<createIndex tableName="messages" indexName="destination_and_type_index">
|
||||||
|
<column name="destination"></column>
|
||||||
|
<column name="destination_device"></column>
|
||||||
|
<column name="type"></column>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,12 +15,15 @@ import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
|||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
//import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static org.fest.assertions.api.Assertions.assertThat;
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
import static org.mockito.Matchers.anyString;
|
import static org.mockito.Matchers.anyString;
|
||||||
@@ -35,7 +38,7 @@ public class AccountControllerTest {
|
|||||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
private SmsSender smsSender = mock(SmsSender.class );
|
private SmsSender smsSender = mock(SmsSender.class );
|
||||||
private StoredMessages storedMessages = mock(StoredMessages.class );
|
private MessagesManager storedMessages = mock(MessagesManager.class );
|
||||||
private TimeProvider timeProvider = mock(TimeProvider.class );
|
private TimeProvider timeProvider = mock(TimeProvider.class );
|
||||||
private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f");
|
private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f");
|
||||||
|
|
||||||
@@ -48,7 +51,8 @@ public class AccountControllerTest {
|
|||||||
smsSender,
|
smsSender,
|
||||||
storedMessages,
|
storedMessages,
|
||||||
timeProvider,
|
timeProvider,
|
||||||
Optional.of(authorizationKey)))
|
Optional.of(authorizationKey),
|
||||||
|
new HashMap<String, Integer>()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class DeviceControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validDeviceRegisterTest() throws Exception {
|
public void validDeviceRegisterTest() throws Exception {
|
||||||
VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning_code")
|
VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning/code")
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(VerificationCode.class);
|
.get(VerificationCode.class);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package org.whispersystems.textsecuregcm.tests.controllers;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.sun.jersey.api.client.ClientResponse;
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
import org.hamcrest.CoreMatchers;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -21,14 +20,17 @@ import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
|||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.Set;
|
||||||
|
|
||||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
@@ -45,8 +47,10 @@ public class FederatedControllerTest {
|
|||||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||||
|
|
||||||
private PushSender pushSender = mock(PushSender.class );
|
private PushSender pushSender = mock(PushSender.class );
|
||||||
|
private ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||||
private FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
private FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
||||||
private AccountsManager accountsManager = mock(AccountsManager.class );
|
private AccountsManager accountsManager = mock(AccountsManager.class );
|
||||||
|
private MessagesManager messagesManager = mock(MessagesManager.class);
|
||||||
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
private RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||||
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
private RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
|
|
||||||
@@ -55,7 +59,7 @@ public class FederatedControllerTest {
|
|||||||
|
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
private final MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
|
private final MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager);
|
||||||
private final KeysControllerV2 keysControllerV2 = mock(KeysControllerV2.class);
|
private final KeysControllerV2 keysControllerV2 = mock(KeysControllerV2.class);
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@@ -69,13 +73,13 @@ public class FederatedControllerTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() throws Exception {
|
public void setup() throws Exception {
|
||||||
List<Device> singleDeviceList = new LinkedList<Device>() {{
|
Set<Device> singleDeviceList = new HashSet<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis()));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
List<Device> multiDeviceList = new LinkedList<Device>() {{
|
Set<Device> multiDeviceList = new HashSet<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis()));
|
||||||
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
|
add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis()));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import org.whispersystems.textsecuregcm.storage.Keys;
|
|||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static org.fest.assertions.api.Assertions.assertThat;
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
@@ -73,7 +75,7 @@ public class KeyControllerTest {
|
|||||||
final Device sampleDevice3 = mock(Device.class);
|
final Device sampleDevice3 = mock(Device.class);
|
||||||
final Device sampleDevice4 = mock(Device.class);
|
final Device sampleDevice4 = mock(Device.class);
|
||||||
|
|
||||||
List<Device> allDevices = new LinkedList<Device>() {{
|
Set<Device> allDevices = new HashSet<Device>() {{
|
||||||
add(sampleDevice);
|
add(sampleDevice);
|
||||||
add(sampleDevice2);
|
add(sampleDevice2);
|
||||||
add(sampleDevice3);
|
add(sampleDevice3);
|
||||||
@@ -198,10 +200,10 @@ public class KeyControllerTest {
|
|||||||
.get(PreKeyResponseV2.class);
|
.get(PreKeyResponseV2.class);
|
||||||
|
|
||||||
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
assertThat(result.getDevices().size()).isEqualTo(1);
|
assertThat(result.getDevicesCount()).isEqualTo(1);
|
||||||
assertThat(result.getDevices().get(0).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
assertThat(result.getDevice(1).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
||||||
assertThat(result.getDevices().get(0).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
assertThat(result.getDevice(1).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
||||||
assertThat(result.getDevices().get(0).getSignedPreKey()).isEqualTo(existsAccount.getDevice(1).get().getSignedPreKey());
|
assertThat(result.getDevice(1).getSignedPreKey()).isEqualTo(existsAccount.getDevice(1).get().getSignedPreKey());
|
||||||
|
|
||||||
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
|
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
|
||||||
verifyNoMoreInteractions(keys);
|
verifyNoMoreInteractions(keys);
|
||||||
@@ -245,13 +247,13 @@ public class KeyControllerTest {
|
|||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
.get(PreKeyResponseV2.class);
|
.get(PreKeyResponseV2.class);
|
||||||
|
|
||||||
assertThat(results.getDevices().size()).isEqualTo(3);
|
assertThat(results.getDevicesCount()).isEqualTo(3);
|
||||||
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
|
||||||
|
|
||||||
PreKeyV2 signedPreKey = results.getDevices().get(0).getSignedPreKey();
|
PreKeyV2 signedPreKey = results.getDevice(1).getSignedPreKey();
|
||||||
PreKeyV2 preKey = results.getDevices().get(0).getPreKey();
|
PreKeyV2 preKey = results.getDevice(1).getPreKey();
|
||||||
long registrationId = results.getDevices().get(0).getRegistrationId();
|
long registrationId = results.getDevice(1).getRegistrationId();
|
||||||
long deviceId = results.getDevices().get(0).getDeviceId();
|
long deviceId = results.getDevice(1).getDeviceId();
|
||||||
|
|
||||||
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
|
||||||
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
|
||||||
@@ -260,10 +262,10 @@ public class KeyControllerTest {
|
|||||||
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY.getPublicKey());
|
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY.getPublicKey());
|
||||||
assertThat(deviceId).isEqualTo(1);
|
assertThat(deviceId).isEqualTo(1);
|
||||||
|
|
||||||
signedPreKey = results.getDevices().get(1).getSignedPreKey();
|
signedPreKey = results.getDevice(2).getSignedPreKey();
|
||||||
preKey = results.getDevices().get(1).getPreKey();
|
preKey = results.getDevice(2).getPreKey();
|
||||||
registrationId = results.getDevices().get(1).getRegistrationId();
|
registrationId = results.getDevice(2).getRegistrationId();
|
||||||
deviceId = results.getDevices().get(1).getDeviceId();
|
deviceId = results.getDevice(2).getDeviceId();
|
||||||
|
|
||||||
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
|
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
|
||||||
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
|
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
|
||||||
@@ -272,10 +274,10 @@ public class KeyControllerTest {
|
|||||||
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY2.getPublicKey());
|
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY2.getPublicKey());
|
||||||
assertThat(deviceId).isEqualTo(2);
|
assertThat(deviceId).isEqualTo(2);
|
||||||
|
|
||||||
signedPreKey = results.getDevices().get(2).getSignedPreKey();
|
signedPreKey = results.getDevice(4).getSignedPreKey();
|
||||||
preKey = results.getDevices().get(2).getPreKey();
|
preKey = results.getDevice(4).getPreKey();
|
||||||
registrationId = results.getDevices().get(2).getRegistrationId();
|
registrationId = results.getDevice(4).getRegistrationId();
|
||||||
deviceId = results.getDevices().get(2).getDeviceId();
|
deviceId = results.getDevice(4).getDeviceId();
|
||||||
|
|
||||||
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId());
|
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId());
|
||||||
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey());
|
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.whispersystems.textsecuregcm.tests.controllers;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.sun.jersey.api.client.ClientResponse;
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
|
import org.hamcrest.CoreMatchers;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -10,24 +11,34 @@ import org.whispersystems.textsecuregcm.controllers.MessageController;
|
|||||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
@@ -40,8 +51,10 @@ public class MessageControllerTest {
|
|||||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||||
|
|
||||||
private final PushSender pushSender = mock(PushSender.class );
|
private final PushSender pushSender = mock(PushSender.class );
|
||||||
|
private final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||||
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
||||||
private final AccountsManager accountsManager = mock(AccountsManager.class );
|
private final AccountsManager accountsManager = mock(AccountsManager.class );
|
||||||
|
private final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||||
private final RateLimiters rateLimiters = mock(RateLimiters.class );
|
private final RateLimiters rateLimiters = mock(RateLimiters.class );
|
||||||
private final RateLimiter rateLimiter = mock(RateLimiter.class );
|
private final RateLimiter rateLimiter = mock(RateLimiter.class );
|
||||||
|
|
||||||
@@ -50,20 +63,21 @@ public class MessageControllerTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
.addProvider(AuthHelper.getAuthenticator())
|
.addProvider(AuthHelper.getAuthenticator())
|
||||||
.addResource(new MessageController(rateLimiters, pushSender, accountsManager,
|
.addResource(new MessageController(rateLimiters, pushSender, receiptSender, accountsManager,
|
||||||
federatedClientManager))
|
messagesManager, federatedClientManager))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() throws Exception {
|
public void setup() throws Exception {
|
||||||
List<Device> singleDeviceList = new LinkedList<Device>() {{
|
Set<Device> singleDeviceList = new HashSet<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis()));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
List<Device> multiDeviceList = new LinkedList<Device>() {{
|
Set<Device> multiDeviceList = new HashSet<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis()));
|
||||||
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
|
add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis()));
|
||||||
|
add(new Device(3, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
||||||
@@ -98,7 +112,7 @@ public class MessageControllerTest {
|
|||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
.put(ClientResponse.class);
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
assertThat("Good Response", response.getStatus(), is(equalTo(204)));
|
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
||||||
}
|
}
|
||||||
@@ -148,7 +162,7 @@ public class MessageControllerTest {
|
|||||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
.put(ClientResponse.class);
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
assertThat("Good Response Code", response.getStatus(), is(equalTo(200)));
|
||||||
|
|
||||||
verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class));
|
||||||
}
|
}
|
||||||
@@ -172,4 +186,75 @@ public class MessageControllerTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public synchronized void testGetMessages() throws Exception {
|
||||||
|
|
||||||
|
final long timestampOne = 313377;
|
||||||
|
final long timestampTwo = 313388;
|
||||||
|
|
||||||
|
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
|
||||||
|
add(new OutgoingMessageEntity(1L, MessageProtos.OutgoingMessageSignal.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes()));
|
||||||
|
add(new OutgoingMessageEntity(2L, MessageProtos.OutgoingMessageSignal.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null));
|
||||||
|
}};
|
||||||
|
|
||||||
|
when(messagesManager.getMessagesForDevice(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(messages);
|
||||||
|
|
||||||
|
OutgoingMessageEntityList response =
|
||||||
|
resources.client().resource("/v1/messages/")
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.accept(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.get(OutgoingMessageEntityList.class);
|
||||||
|
|
||||||
|
|
||||||
|
assertEquals(response.getMessages().size(), 2);
|
||||||
|
|
||||||
|
assertEquals(response.getMessages().get(0).getId(), 0);
|
||||||
|
assertEquals(response.getMessages().get(1).getId(), 0);
|
||||||
|
|
||||||
|
assertEquals(response.getMessages().get(0).getTimestamp(), timestampOne);
|
||||||
|
assertEquals(response.getMessages().get(1).getTimestamp(), timestampTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public synchronized void testDeleteMessages() throws Exception {
|
||||||
|
long timestamp = System.currentTimeMillis();
|
||||||
|
when(messagesManager.delete(AuthHelper.VALID_NUMBER, "+14152222222", 31337))
|
||||||
|
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L,
|
||||||
|
MessageProtos.OutgoingMessageSignal.Type.CIPHERTEXT_VALUE,
|
||||||
|
null, timestamp,
|
||||||
|
"+14152222222", 1, "hi".getBytes())));
|
||||||
|
|
||||||
|
when(messagesManager.delete(AuthHelper.VALID_NUMBER, "+14152222222", 31338))
|
||||||
|
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L,
|
||||||
|
MessageProtos.OutgoingMessageSignal.Type.RECEIPT_VALUE,
|
||||||
|
null, System.currentTimeMillis(),
|
||||||
|
"+14152222222", 1, null)));
|
||||||
|
|
||||||
|
|
||||||
|
when(messagesManager.delete(AuthHelper.VALID_NUMBER, "+14152222222", 31339))
|
||||||
|
.thenReturn(Optional.<OutgoingMessageEntity>absent());
|
||||||
|
|
||||||
|
ClientResponse response = resources.client().resource(String.format("/v1/messages/%s/%d", "+14152222222", 31337))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.delete(ClientResponse.class);
|
||||||
|
|
||||||
|
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
||||||
|
verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp), eq(Optional.<String>absent()));
|
||||||
|
|
||||||
|
response = resources.client().resource(String.format("/v1/messages/%s/%d", "+14152222222", 31338))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.delete(ClientResponse.class);
|
||||||
|
|
||||||
|
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
||||||
|
verifyNoMoreInteractions(receiptSender);
|
||||||
|
|
||||||
|
response = resources.client().resource(String.format("/v1/messages/%s/%d", "+14152222222", 31339))
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.delete(ClientResponse.class);
|
||||||
|
|
||||||
|
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
|
||||||
|
verifyNoMoreInteractions(receiptSender);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,25 +6,21 @@ import com.sun.jersey.api.client.ClientResponse;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
|
||||||
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
|
import org.whispersystems.textsecuregcm.controllers.ReceiptController;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.Set;
|
||||||
|
|
||||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
import static org.fest.assertions.api.Assertions.assertThat;
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
|
||||||
import static org.hamcrest.core.IsEqual.equalTo;
|
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@@ -37,23 +33,25 @@ public class ReceiptControllerTest {
|
|||||||
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
|
||||||
private final AccountsManager accountsManager = mock(AccountsManager.class );
|
private final AccountsManager accountsManager = mock(AccountsManager.class );
|
||||||
|
|
||||||
|
private final ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
|
||||||
|
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final ResourceTestRule resources = ResourceTestRule.builder()
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
.addProvider(AuthHelper.getAuthenticator())
|
.addProvider(AuthHelper.getAuthenticator())
|
||||||
.addResource(new ReceiptController(accountsManager, federatedClientManager, pushSender))
|
.addResource(new ReceiptController(receiptSender))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() throws Exception {
|
public void setup() throws Exception {
|
||||||
List<Device> singleDeviceList = new LinkedList<Device>() {{
|
Set<Device> singleDeviceList = new HashSet<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis()));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
List<Device> multiDeviceList = new LinkedList<Device>() {{
|
Set<Device> multiDeviceList = new HashSet<Device>() {{
|
||||||
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
|
add(new Device(1, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis()));
|
||||||
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
|
add(new Device(2, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis()));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.tests.push;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ApnMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushServiceClient;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
public class ApnFallbackManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFullFallback() throws Exception {
|
||||||
|
PushServiceClient pushServiceClient = mock(PushServiceClient.class);
|
||||||
|
WebsocketAddress address = mock(WebsocketAddress.class );
|
||||||
|
ApnMessage message = new ApnMessage("bar", "123", 1, "hmm", true, 1111);
|
||||||
|
ApnFallbackTask task = new ApnFallbackTask("foo", message, 500);
|
||||||
|
|
||||||
|
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient);
|
||||||
|
apnFallbackManager.start();
|
||||||
|
|
||||||
|
apnFallbackManager.schedule(address, task);
|
||||||
|
|
||||||
|
Util.sleep(1100);
|
||||||
|
|
||||||
|
ArgumentCaptor<ApnMessage> captor = ArgumentCaptor.forClass(ApnMessage.class);
|
||||||
|
verify(pushServiceClient, times(1)).send(captor.capture());
|
||||||
|
|
||||||
|
assertEquals(captor.getValue().getMessage(), message.getMessage());
|
||||||
|
assertEquals(captor.getValue().getApnId(), task.getApnId());
|
||||||
|
assertFalse(captor.getValue().isVoip());
|
||||||
|
assertEquals(captor.getValue().getExpirationTime(), Integer.MAX_VALUE * 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoFallback() throws Exception {
|
||||||
|
PushServiceClient pushServiceClient = mock(PushServiceClient.class);
|
||||||
|
WebsocketAddress address = mock(WebsocketAddress.class );
|
||||||
|
ApnMessage message = new ApnMessage("bar", "123", 1, "hmm", true, 5555);
|
||||||
|
ApnFallbackTask task = new ApnFallbackTask ("foo", message, 500);
|
||||||
|
|
||||||
|
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushServiceClient);
|
||||||
|
apnFallbackManager.start();
|
||||||
|
|
||||||
|
apnFallbackManager.schedule(address, task);
|
||||||
|
apnFallbackManager.cancel(address);
|
||||||
|
|
||||||
|
Util.sleep(1100);
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(pushServiceClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.tests.push;
|
||||||
|
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTaskQueue;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ApnFallbackTaskQueueTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBlocking() {
|
||||||
|
final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
|
||||||
|
|
||||||
|
final WebsocketAddress address = mock(WebsocketAddress.class);
|
||||||
|
final ApnFallbackTask task = mock(ApnFallbackTask.class );
|
||||||
|
|
||||||
|
when(task.getExecutionTime()).thenReturn(System.currentTimeMillis() - 1000);
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Util.sleep(500);
|
||||||
|
taskQueue.put(address, task);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
Map.Entry<WebsocketAddress, ApnFallbackTask> result = taskQueue.get();
|
||||||
|
|
||||||
|
assertEquals(result.getKey(), address);
|
||||||
|
assertEquals(result.getValue(), task);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testElapsedTime() {
|
||||||
|
final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
|
||||||
|
final WebsocketAddress address = mock(WebsocketAddress.class);
|
||||||
|
final ApnFallbackTask task = mock(ApnFallbackTask.class );
|
||||||
|
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
when(task.getExecutionTime()).thenReturn(currentTime + 1000);
|
||||||
|
|
||||||
|
taskQueue.put(address, task);
|
||||||
|
Map.Entry<WebsocketAddress, ApnFallbackTask> result = taskQueue.get();
|
||||||
|
|
||||||
|
assertTrue(System.currentTimeMillis() >= currentTime + 1000);
|
||||||
|
assertEquals(result.getKey(), address);
|
||||||
|
assertEquals(result.getValue(), task);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCanceled() {
|
||||||
|
final ApnFallbackTaskQueue taskQueue = new ApnFallbackTaskQueue();
|
||||||
|
final WebsocketAddress addressOne = mock(WebsocketAddress.class);
|
||||||
|
final ApnFallbackTask taskOne = mock(ApnFallbackTask.class );
|
||||||
|
final WebsocketAddress addressTwo = mock(WebsocketAddress.class);
|
||||||
|
final ApnFallbackTask taskTwo = mock(ApnFallbackTask.class );
|
||||||
|
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
when(taskOne.getExecutionTime()).thenReturn(currentTime + 1000);
|
||||||
|
when(taskTwo.getExecutionTime()).thenReturn(currentTime + 2000);
|
||||||
|
|
||||||
|
taskQueue.put(addressOne, taskOne);
|
||||||
|
taskQueue.put(addressTwo, taskTwo);
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Util.sleep(300);
|
||||||
|
taskQueue.remove(addressOne);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
Map.Entry<WebsocketAddress, ApnFallbackTask> result = taskQueue.get();
|
||||||
|
|
||||||
|
assertTrue(System.currentTimeMillis() >= currentTime + 2000);
|
||||||
|
assertEquals(result.getKey(), addressTwo);
|
||||||
|
assertEquals(result.getValue(), taskTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,30 +5,41 @@ import com.google.common.util.concurrent.SettableFuture;
|
|||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
import org.mockito.stubbing.Answer;
|
import org.mockito.stubbing.Answer;
|
||||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.StoredMessages;
|
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
|
||||||
import org.whispersystems.textsecuregcm.util.Base64;
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
import org.whispersystems.textsecuregcm.websocket.ConnectListener;
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||||
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
|
||||||
import org.whispersystems.websocket.WebSocketClient;
|
import org.whispersystems.websocket.WebSocketClient;
|
||||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||||
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
||||||
|
import org.whispersystems.websocket.setup.WebSocketConnectListener;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import io.dropwizard.auth.basic.BasicCredentials;
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
@@ -36,29 +47,27 @@ import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMe
|
|||||||
|
|
||||||
public class WebSocketConnectionTest {
|
public class WebSocketConnectionTest {
|
||||||
|
|
||||||
// private static final ObjectMapper mapper = new ObjectMapper();
|
|
||||||
|
|
||||||
private static final String VALID_USER = "+14152222222";
|
private static final String VALID_USER = "+14152222222";
|
||||||
private static final String INVALID_USER = "+14151111111";
|
private static final String INVALID_USER = "+14151111111";
|
||||||
|
|
||||||
private static final String VALID_PASSWORD = "secure";
|
private static final String VALID_PASSWORD = "secure";
|
||||||
private static final String INVALID_PASSWORD = "insecure";
|
private static final String INVALID_PASSWORD = "insecure";
|
||||||
|
|
||||||
// private static final StoredMessages storedMessages = mock(StoredMessages.class);
|
|
||||||
private static final AccountAuthenticator accountAuthenticator = mock(AccountAuthenticator.class);
|
private static final AccountAuthenticator accountAuthenticator = mock(AccountAuthenticator.class);
|
||||||
private static final AccountsManager accountsManager = mock(AccountsManager.class);
|
private static final AccountsManager accountsManager = mock(AccountsManager.class);
|
||||||
private static final PubSubManager pubSubManager = mock(PubSubManager.class );
|
private static final PubSubManager pubSubManager = mock(PubSubManager.class );
|
||||||
private static final Account account = mock(Account.class );
|
private static final Account account = mock(Account.class );
|
||||||
private static final Device device = mock(Device.class );
|
private static final Device device = mock(Device.class );
|
||||||
private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class );
|
private static final UpgradeRequest upgradeRequest = mock(UpgradeRequest.class );
|
||||||
// private static final Session session = mock(Session.class );
|
|
||||||
private static final PushSender pushSender = mock(PushSender.class);
|
private static final PushSender pushSender = mock(PushSender.class);
|
||||||
|
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||||
|
private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCredentials() throws Exception {
|
public void testCredentials() throws Exception {
|
||||||
StoredMessages storedMessages = mock(StoredMessages.class);
|
MessagesManager storedMessages = mock(MessagesManager.class);
|
||||||
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
|
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
|
||||||
ConnectListener connectListener = new ConnectListener(accountsManager, pushSender, storedMessages, pubSubManager);
|
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(accountsManager, pushSender, receiptSender, storedMessages, pubSubManager, apnFallbackManager);
|
||||||
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
|
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
|
||||||
|
|
||||||
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
|
when(accountAuthenticator.authenticate(eq(new BasicCredentials(VALID_USER, VALID_PASSWORD))))
|
||||||
@@ -69,10 +78,6 @@ public class WebSocketConnectionTest {
|
|||||||
|
|
||||||
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
|
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
// when(session.getUpgradeRequest()).thenReturn(upgradeRequest);
|
|
||||||
//
|
|
||||||
// WebsocketController controller = new WebsocketController(accountAuthenticator, accountsManager, pushSender, pubSubManager, storedMessages);
|
|
||||||
|
|
||||||
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
|
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
|
||||||
put("login", new String[] {VALID_USER});
|
put("login", new String[] {VALID_USER});
|
||||||
put("password", new String[] {VALID_PASSWORD});
|
put("password", new String[] {VALID_PASSWORD});
|
||||||
@@ -85,38 +90,23 @@ public class WebSocketConnectionTest {
|
|||||||
|
|
||||||
verify(sessionContext).addListener(any(WebSocketSessionContext.WebSocketEventListener.class));
|
verify(sessionContext).addListener(any(WebSocketSessionContext.WebSocketEventListener.class));
|
||||||
|
|
||||||
//
|
|
||||||
// controller.onWebSocketConnect(session);
|
|
||||||
|
|
||||||
// verify(session, never()).close();
|
|
||||||
// verify(session, never()).close(any(CloseStatus.class));
|
|
||||||
// verify(session, never()).close(anyInt(), anyString());
|
|
||||||
|
|
||||||
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
|
when(upgradeRequest.getParameterMap()).thenReturn(new HashMap<String, String[]>() {{
|
||||||
put("login", new String[] {INVALID_USER});
|
put("login", new String[] {INVALID_USER});
|
||||||
put("password", new String[] {INVALID_PASSWORD});
|
put("password", new String[] {INVALID_PASSWORD});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
account = webSocketAuthenticator.authenticate(upgradeRequest);
|
account = webSocketAuthenticator.authenticate(upgradeRequest);
|
||||||
when(sessionContext.getAuthenticated(Account.class)).thenReturn(account);
|
assertFalse(account.isPresent());
|
||||||
|
|
||||||
WebSocketClient client = mock(WebSocketClient.class);
|
|
||||||
when(sessionContext.getClient()).thenReturn(client);
|
|
||||||
|
|
||||||
connectListener.onWebSocketConnect(sessionContext);
|
|
||||||
|
|
||||||
verify(sessionContext, times(1)).addListener(any(WebSocketSessionContext.WebSocketEventListener.class));
|
|
||||||
verify(client).close(eq(4001), anyString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpen() throws Exception {
|
public void testOpen() throws Exception {
|
||||||
StoredMessages storedMessages = mock(StoredMessages.class);
|
MessagesManager storedMessages = mock(MessagesManager.class);
|
||||||
|
|
||||||
List<OutgoingMessageSignal> outgoingMessages = new LinkedList<OutgoingMessageSignal>() {{
|
List<OutgoingMessageEntity> outgoingMessages = new LinkedList<OutgoingMessageEntity> () {{
|
||||||
add(createMessage("sender1", 1111, false, "first"));
|
add(createMessage(1L, "sender1", 1111, false, "first"));
|
||||||
add(createMessage("sender1", 2222, false, "second"));
|
add(createMessage(2L, "sender1", 2222, false, "second"));
|
||||||
add(createMessage("sender2", 3333, false, "third"));
|
add(createMessage(3L, "sender2", 3333, false, "third"));
|
||||||
}};
|
}};
|
||||||
|
|
||||||
when(device.getId()).thenReturn(2L);
|
when(device.getId()).thenReturn(2L);
|
||||||
@@ -127,7 +117,7 @@ public class WebSocketConnectionTest {
|
|||||||
|
|
||||||
final Device sender1device = mock(Device.class);
|
final Device sender1device = mock(Device.class);
|
||||||
|
|
||||||
List<Device> sender1devices = new LinkedList<Device>() {{
|
Set<Device> sender1devices = new HashSet<Device>() {{
|
||||||
add(sender1device);
|
add(sender1device);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
@@ -137,7 +127,7 @@ public class WebSocketConnectionTest {
|
|||||||
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
|
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
|
||||||
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
|
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
|
||||||
|
|
||||||
when(storedMessages.getMessagesForDevice(new WebsocketAddress(account.getNumber(), device.getId())))
|
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
|
||||||
.thenReturn(outgoingMessages);
|
.thenReturn(outgoingMessages);
|
||||||
|
|
||||||
final List<SettableFuture<WebSocketResponseMessage>> futures = new LinkedList<>();
|
final List<SettableFuture<WebSocketResponseMessage>> futures = new LinkedList<>();
|
||||||
@@ -153,12 +143,11 @@ public class WebSocketConnectionTest {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender, storedMessages,
|
WebsocketAddress websocketAddress = new WebsocketAddress(account.getNumber(), device.getId());
|
||||||
pubSubManager, account, device, client);
|
WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender, storedMessages,
|
||||||
|
account, device, client);
|
||||||
|
|
||||||
connection.onConnected();
|
connection.onDispatchSubscribed(websocketAddress.serialize());
|
||||||
|
|
||||||
verify(pubSubManager).subscribe(eq(new WebsocketAddress("+14152222222", 2L)), eq((connection)));
|
|
||||||
verify(client, times(3)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class));
|
verify(client, times(3)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class));
|
||||||
|
|
||||||
assertTrue(futures.size() == 3);
|
assertTrue(futures.size() == 3);
|
||||||
@@ -170,26 +159,102 @@ public class WebSocketConnectionTest {
|
|||||||
futures.get(0).setException(new IOException());
|
futures.get(0).setException(new IOException());
|
||||||
futures.get(2).setException(new IOException());
|
futures.get(2).setException(new IOException());
|
||||||
|
|
||||||
List<OutgoingMessageSignal> pending = new LinkedList<OutgoingMessageSignal>() {{
|
verify(storedMessages, times(1)).delete(eq(account.getNumber()), eq(2L));
|
||||||
add(createMessage("sender1", 1111, false, "first"));
|
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender1"), eq(2222L), eq(Optional.<String>absent()));
|
||||||
add(createMessage("sender2", 3333, false, "third"));
|
|
||||||
}};
|
|
||||||
|
|
||||||
verify(pushSender, times(2)).sendMessage(eq(account), eq(device), any(OutgoingMessageSignal.class));
|
connection.onDispatchUnsubscribed(websocketAddress.serialize());
|
||||||
verify(pushSender, times(1)).sendMessage(eq(sender1), eq(sender1device), any(OutgoingMessageSignal.class));
|
verify(client).close(anyInt(), anyString());
|
||||||
|
|
||||||
connection.onConnectionLost();
|
|
||||||
verify(pubSubManager).unsubscribe(eq(new WebsocketAddress("+14152222222", 2L)), eq(connection));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private OutgoingMessageSignal createMessage(String sender, long timestamp, boolean receipt, String content) {
|
@Test
|
||||||
return OutgoingMessageSignal.newBuilder()
|
public void testOnlineSend() throws Exception {
|
||||||
.setSource(sender)
|
MessagesManager storedMessages = mock(MessagesManager.class);
|
||||||
.setSourceDevice(1)
|
OutgoingMessageSignal firstMessage = OutgoingMessageSignal.newBuilder()
|
||||||
.setType(receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE)
|
.setMessage(ByteString.copyFrom("first".getBytes()))
|
||||||
.setTimestamp(timestamp)
|
.setSource("sender1")
|
||||||
.setMessage(ByteString.copyFrom(content.getBytes()))
|
.setTimestamp(System.currentTimeMillis())
|
||||||
.build();
|
.setSourceDevice(1)
|
||||||
|
.setType(OutgoingMessageSignal.Type.CIPHERTEXT_VALUE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OutgoingMessageSignal secondMessage = OutgoingMessageSignal.newBuilder()
|
||||||
|
.setMessage(ByteString.copyFrom("second".getBytes()))
|
||||||
|
.setSource("sender2")
|
||||||
|
.setTimestamp(System.currentTimeMillis())
|
||||||
|
.setSourceDevice(2)
|
||||||
|
.setType(OutgoingMessageSignal.Type.CIPHERTEXT_VALUE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<OutgoingMessageEntity> pendingMessages = new LinkedList<>();
|
||||||
|
|
||||||
|
when(device.getId()).thenReturn(2L);
|
||||||
|
when(device.getSignalingKey()).thenReturn(Base64.encodeBytes(new byte[52]));
|
||||||
|
|
||||||
|
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
|
||||||
|
when(account.getNumber()).thenReturn("+14152222222");
|
||||||
|
|
||||||
|
final Device sender1device = mock(Device.class);
|
||||||
|
|
||||||
|
Set<Device> sender1devices = new HashSet<Device>() {{
|
||||||
|
add(sender1device);
|
||||||
|
}};
|
||||||
|
|
||||||
|
Account sender1 = mock(Account.class);
|
||||||
|
when(sender1.getDevices()).thenReturn(sender1devices);
|
||||||
|
|
||||||
|
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
|
||||||
|
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
|
||||||
|
|
||||||
|
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
|
||||||
|
.thenReturn(pendingMessages);
|
||||||
|
|
||||||
|
final List<SettableFuture<WebSocketResponseMessage>> futures = new LinkedList<>();
|
||||||
|
final WebSocketClient client = mock(WebSocketClient.class);
|
||||||
|
|
||||||
|
when(client.sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class)))
|
||||||
|
.thenAnswer(new Answer<SettableFuture<WebSocketResponseMessage>>() {
|
||||||
|
@Override
|
||||||
|
public SettableFuture<WebSocketResponseMessage> answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||||
|
SettableFuture<WebSocketResponseMessage> future = SettableFuture.create();
|
||||||
|
futures.add(future);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WebsocketAddress websocketAddress = new WebsocketAddress(account.getNumber(), device.getId());
|
||||||
|
WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender, storedMessages,
|
||||||
|
account, device, client);
|
||||||
|
|
||||||
|
connection.onDispatchSubscribed(websocketAddress.serialize());
|
||||||
|
connection.onDispatchMessage(websocketAddress.serialize(), PubSubProtos.PubSubMessage.newBuilder()
|
||||||
|
.setType(PubSubProtos.PubSubMessage.Type.DELIVER)
|
||||||
|
.setContent(ByteString.copyFrom(firstMessage.toByteArray()))
|
||||||
|
.build().toByteArray());
|
||||||
|
|
||||||
|
connection.onDispatchMessage(websocketAddress.serialize(), PubSubProtos.PubSubMessage.newBuilder()
|
||||||
|
.setType(PubSubProtos.PubSubMessage.Type.DELIVER)
|
||||||
|
.setContent(ByteString.copyFrom(secondMessage.toByteArray()))
|
||||||
|
.build().toByteArray());
|
||||||
|
|
||||||
|
verify(client, times(2)).sendRequest(eq("PUT"), eq("/api/v1/message"), any(Optional.class));
|
||||||
|
|
||||||
|
assertEquals(futures.size(), 2);
|
||||||
|
|
||||||
|
WebSocketResponseMessage response = mock(WebSocketResponseMessage.class);
|
||||||
|
when(response.getStatus()).thenReturn(200);
|
||||||
|
futures.get(1).set(response);
|
||||||
|
futures.get(0).setException(new IOException());
|
||||||
|
|
||||||
|
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()), eq(Optional.<String>absent()));
|
||||||
|
verify(pushSender, times(1)).sendMessage(eq(account), eq(device), any(OutgoingMessageSignal.class));
|
||||||
|
|
||||||
|
connection.onDispatchUnsubscribed(websocketAddress.serialize());
|
||||||
|
verify(client).close(anyInt(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private OutgoingMessageEntity createMessage(long id, String sender, long timestamp, boolean receipt, String content) {
|
||||||
|
return new OutgoingMessageEntity(id, receipt ? OutgoingMessageSignal.Type.RECEIPT_VALUE : OutgoingMessageSignal.Type.CIPHERTEXT_VALUE,
|
||||||
|
null, timestamp, sender, 1, content.getBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user