mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-13 01:50:34 +00:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9bd700d31 | ||
|
|
0928e4c035 | ||
|
|
75aec0a8d4 | ||
|
|
1f5ee36a6b | ||
|
|
45a0b74b89 | ||
|
|
f7132bdbbc | ||
|
|
d2dbff173a | ||
|
|
79f83babb3 | ||
|
|
715181f830 | ||
|
|
5c1c80dad3 | ||
|
|
32c0712715 | ||
|
|
fa4e492d1c | ||
|
|
4711fa2a9a | ||
|
|
08291502eb | ||
|
|
1f0acd0622 | ||
|
|
e88b732715 | ||
|
|
dafda85c36 | ||
|
|
8441fa9687 | ||
|
|
77800dfb01 | ||
|
|
41d15b738b | ||
|
|
aa2a5ff929 | ||
|
|
56d3c1e73f | ||
|
|
f401f9a674 | ||
|
|
30933d792b | ||
|
|
905717977e | ||
|
|
b802994809 | ||
|
|
ac96f906b3 | ||
|
|
cc395e914f | ||
|
|
f8063f8faf | ||
|
|
958ada9110 | ||
|
|
3452ea29b8 | ||
|
|
675b6f4b5e | ||
|
|
4fab67b0f5 | ||
|
|
8a2131416d | ||
|
|
2525304215 | ||
|
|
fdb35d4f77 | ||
|
|
222c7ea641 | ||
|
|
8f2722263f | ||
|
|
fd662e3401 | ||
|
|
bc65461ecb | ||
|
|
30017371df | ||
|
|
b944b86bf8 | ||
|
|
6ba8352fa6 | ||
|
|
aadf76692e | ||
|
|
c9a1386a55 | ||
|
|
4eb88a3e02 | ||
|
|
160c0bfe14 | ||
|
|
4cd098af1d | ||
|
|
362abd618f | ||
|
|
69de9f6684 | ||
|
|
2aa379bf21 | ||
|
|
820a2f1a63 | ||
|
|
6fac7614f5 | ||
|
|
b724ea8d3b | ||
|
|
06f80c320d | ||
|
|
d9de015eab | ||
|
|
dd36c861ba | ||
|
|
b34e46af93 | ||
|
|
405802c492 | ||
|
|
e15f3c9d2b | ||
|
|
885af064c9 | ||
|
|
40529dc41f | ||
|
|
2452f6ef8a | ||
|
|
4c543e6f06 | ||
|
|
bc5fd5d441 | ||
|
|
7a33cef27e | ||
|
|
b433b9c879 | ||
|
|
5d169c523f | ||
|
|
98d277368f | ||
|
|
3bd58bf25e | ||
|
|
ba05e577ae | ||
|
|
4206f6af45 | ||
|
|
0c5da1cc47 | ||
|
|
d9bd1c679e | ||
|
|
437eb8de37 | ||
|
|
f14c181840 | ||
|
|
d46c9fb157 | ||
|
|
6913e4dfd2 | ||
|
|
aea3f299a0 | ||
|
|
5667476780 | ||
|
|
b263f47826 | ||
|
|
21723d6313 | ||
|
|
a63cdc76b0 | ||
|
|
129e372613 | ||
|
|
53de38fc06 | ||
|
|
67e5794722 | ||
|
|
6aaca59020 | ||
|
|
f4ecb5d7be | ||
|
|
35e212a30f | ||
|
|
a6463df5bb | ||
|
|
a9994ef5aa | ||
|
|
6e0ae70f02 | ||
|
|
a0889130e5 | ||
|
|
8e763f62f5 | ||
|
|
866f8bf1ef | ||
|
|
7bb505db4c | ||
|
|
519f982604 | ||
|
|
2f85cd214e | ||
|
|
74f71fd8a6 | ||
|
|
6f9226dcf9 | ||
|
|
eedaa8b3f4 | ||
|
|
7af3c51cc4 | ||
|
|
d3830a7fd4 | ||
|
|
ce9d3548e4 | ||
|
|
0bd82784a0 | ||
|
|
542bf73a75 | ||
|
|
bd6cf10402 | ||
|
|
5a837d4481 | ||
|
|
b08eb0df5c | ||
|
|
e39016ad35 | ||
|
|
8c74ad073b | ||
|
|
918ef4a7ca | ||
|
|
2473505d4e | ||
|
|
591d26981e | ||
|
|
605e88d4bf | ||
|
|
48fe609d53 | ||
|
|
a0768e219a | ||
|
|
40a988c0cd | ||
|
|
5845d2dedd | ||
|
|
cb185a6552 | ||
|
|
2dc5857645 | ||
|
|
7d8336fd30 | ||
|
|
f9d7c1de57 | ||
|
|
648812a267 | ||
|
|
ef1160eda8 | ||
|
|
4cd1082a4a | ||
|
|
cae5cf7024 | ||
|
|
96435648d3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ run.sh
|
|||||||
local.yml
|
local.yml
|
||||||
config/production.yml
|
config/production.yml
|
||||||
config/federated.yml
|
config/federated.yml
|
||||||
|
config/staging.yml
|
||||||
|
.opsmanage
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ whispersystems@lists.riseup.net
|
|||||||
|
|
||||||
https://lists.riseup.net/www/info/whispersystems
|
https://lists.riseup.net/www/info/whispersystems
|
||||||
|
|
||||||
|
Current BitHub Payment Per Commit:
|
||||||
|
=================
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
Cryptography Notice
|
Cryptography Notice
|
||||||
------------
|
------------
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ twilio:
|
|||||||
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
|
# Optional. If specified, Nexmo will be used for non-US SMS and
|
||||||
# voice verification.
|
# voice verification if twilio.international is false. Otherwise,
|
||||||
|
# Nexmo, if specified, Nexmo will only be used as a fallback
|
||||||
|
# for failed Twilio deliveries.
|
||||||
nexmo:
|
nexmo:
|
||||||
apiKey:
|
apiKey:
|
||||||
apiSecret:
|
apiSecret:
|
||||||
number:
|
number:
|
||||||
|
|
||||||
gcm:
|
gcm:
|
||||||
|
senderId:
|
||||||
apiKey:
|
apiKey:
|
||||||
|
|
||||||
# Optional. Only if iOS clients are supported.
|
# Optional. Only if iOS clients are supported.
|
||||||
@@ -53,9 +57,6 @@ graphite:
|
|||||||
host:
|
host:
|
||||||
port:
|
port:
|
||||||
|
|
||||||
http:
|
|
||||||
shutdownGracePeriod: 0s
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
# the name of your JDBC driver
|
# the name of your JDBC driver
|
||||||
driverClass: org.postgresql.Driver
|
driverClass: org.postgresql.Driver
|
||||||
@@ -72,24 +73,3 @@ database:
|
|||||||
# any properties specific to your JDBC driver:
|
# any properties specific to your JDBC driver:
|
||||||
properties:
|
properties:
|
||||||
charSet: UTF-8
|
charSet: UTF-8
|
||||||
|
|
||||||
# the maximum amount of time to wait on an empty pool before throwing an exception
|
|
||||||
maxWaitForConnection: 1s
|
|
||||||
|
|
||||||
# the SQL query to run when validating a connection's liveness
|
|
||||||
validationQuery: "/* MyService Health Check */ SELECT 1"
|
|
||||||
|
|
||||||
# the minimum number of connections to keep open
|
|
||||||
minSize: 8
|
|
||||||
|
|
||||||
# the maximum number of connections to keep open
|
|
||||||
maxSize: 32
|
|
||||||
|
|
||||||
# whether or not idle connections should be validated
|
|
||||||
checkConnectionWhileIdle: false
|
|
||||||
|
|
||||||
# how long a connection must be held before it can be validated
|
|
||||||
checkConnectionHealthWhenIdleFor: 10s
|
|
||||||
|
|
||||||
# the maximum lifetime of an idle connection
|
|
||||||
closeConnectionIfIdleFor: 1 minute
|
|
||||||
|
|||||||
145
pom.xml
145
pom.xml
@@ -9,39 +9,83 @@
|
|||||||
|
|
||||||
<groupId>org.whispersystems.textsecure</groupId>
|
<groupId>org.whispersystems.textsecure</groupId>
|
||||||
<artifactId>TextSecureServer</artifactId>
|
<artifactId>TextSecureServer</artifactId>
|
||||||
<version>0.2</version>
|
<version>0.30</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<dropwizard.version>0.7.1</dropwizard.version>
|
||||||
|
<jackson.api.version>2.3.3</jackson.api.version>
|
||||||
|
<commons-codec.version>1.6</commons-codec.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-core</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-jdbi</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-auth</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-client</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-migrations</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-testing</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-metrics-graphite</artifactId>
|
||||||
|
<version>${dropwizard.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.dcsquare</groupId>
|
||||||
|
<artifactId>dropwizard-papertrail</artifactId>
|
||||||
|
<version>1.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.sun.jersey</groupId>
|
||||||
|
<artifactId>jersey-json</artifactId>
|
||||||
|
<version>1.18.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.codahale.metrics</groupId>
|
||||||
|
<artifactId>metrics-graphite</artifactId>
|
||||||
|
<version>3.0.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||||
|
<artifactId>websocket-server</artifactId>
|
||||||
|
<version>9.0.7.v20131107</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>bouncycastle</groupId>
|
<groupId>bouncycastle</groupId>
|
||||||
<artifactId>bcprov-jdk16</artifactId>
|
<artifactId>bcprov-jdk16</artifactId>
|
||||||
<version>140</version>
|
<version>140</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-core</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.metrics</groupId>
|
|
||||||
<artifactId>metrics-graphite</artifactId>
|
|
||||||
<version>2.2.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.android.gcm</groupId>
|
<groupId>com.google.android.gcm</groupId>
|
||||||
<artifactId>gcm-server</artifactId>
|
<artifactId>gcm-server</artifactId>
|
||||||
<version>1.0.2</version>
|
<version>1.0.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.code.gson</groupId>
|
|
||||||
<artifactId>gson</artifactId>
|
|
||||||
<version>2.2.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<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>
|
||||||
@@ -56,45 +100,20 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.protobuf</groupId>
|
<groupId>com.google.protobuf</groupId>
|
||||||
<artifactId>protobuf-java</artifactId>
|
<artifactId>protobuf-java</artifactId>
|
||||||
<version>2.4.1</version>
|
<version>2.5.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>2.2.1</version>
|
<version>2.6.1</version>
|
||||||
<type>jar</type>
|
<type>jar</type>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-jdbi</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-auth</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-client</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-migrations</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
|
||||||
<artifactId>dropwizard-testing</artifactId>
|
|
||||||
<version>0.6.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.twilio.sdk</groupId>
|
<groupId>com.twilio.sdk</groupId>
|
||||||
<artifactId>twilio-java-sdk</artifactId>
|
<artifactId>twilio-java-sdk</artifactId>
|
||||||
<version>3.4.1</version>
|
<version>3.4.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -102,15 +121,35 @@
|
|||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>9.1-901.jdbc4</version>
|
<version>9.1-901.jdbc4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.sun.jersey</groupId>
|
<groupId>org.igniterealtime.smack</groupId>
|
||||||
<artifactId>jersey-json</artifactId>
|
<artifactId>smack-tcp</artifactId>
|
||||||
<version>1.17.1</version>
|
<version>4.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.whispersystems</groupId>
|
||||||
|
<artifactId>websocket-resources</artifactId>
|
||||||
|
<version>0.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.api.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-codec</groupId>
|
||||||
|
<artifactId>commons-codec</artifactId>
|
||||||
|
<version>${commons-codec.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
|
|
||||||
all:
|
all:
|
||||||
protoc --java_out=../src/main/java/ OutgoingMessageSignal.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
|
||||||
@@ -20,10 +20,24 @@ option java_package = "org.whispersystems.textsecuregcm.entities";
|
|||||||
option java_outer_classname = "MessageProtos";
|
option java_outer_classname = "MessageProtos";
|
||||||
|
|
||||||
message OutgoingMessageSignal {
|
message OutgoingMessageSignal {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
CIPHERTEXT = 1;
|
||||||
|
KEY_EXCHANGE = 2;
|
||||||
|
PREKEY_BUNDLE = 3;
|
||||||
|
PLAINTEXT = 4;
|
||||||
|
RECEIPT = 5;
|
||||||
|
}
|
||||||
|
|
||||||
optional uint32 type = 1;
|
optional uint32 type = 1;
|
||||||
optional string source = 2;
|
optional string source = 2;
|
||||||
|
optional uint32 sourceDevice = 7;
|
||||||
optional string relay = 3;
|
optional string relay = 3;
|
||||||
repeated string destinations = 4;
|
// repeated string destinations = 4;
|
||||||
optional uint64 timestamp = 5;
|
optional uint64 timestamp = 5;
|
||||||
optional bytes message = 6;
|
optional bytes message = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ProvisioningUuid {
|
||||||
|
optional string uuid = 1;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
||||||
@@ -14,33 +14,19 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
package textsecure;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
option java_package = "org.whispersystems.textsecuregcm.storage";
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
option java_outer_classname = "PubSubProtos";
|
||||||
|
|
||||||
public class MemcacheConfiguration {
|
message PubSubMessage {
|
||||||
|
enum Type {
|
||||||
@NotEmpty
|
UNKNOWN = 0;
|
||||||
@JsonProperty
|
QUERY_DB = 1;
|
||||||
private String servers;
|
DELIVER = 2;
|
||||||
|
KEEPALIVE = 3;
|
||||||
@JsonProperty
|
|
||||||
private String user;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
|
|
||||||
public String getServers() {
|
|
||||||
return servers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUser() {
|
optional Type type = 1;
|
||||||
return user;
|
optional bytes content = 2;
|
||||||
}
|
|
||||||
|
|
||||||
public String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
30
protobuf/StoredMessage.proto
Normal file
30
protobuf/StoredMessage.proto
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package textsecure;
|
||||||
|
|
||||||
|
option java_package = "org.whispersystems.textsecuregcm.storage";
|
||||||
|
option java_outer_classname = "StoredMessageProtos";
|
||||||
|
|
||||||
|
message StoredMessage {
|
||||||
|
enum Type {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
MESSAGE = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
optional bytes content = 2;
|
||||||
|
}
|
||||||
@@ -17,22 +17,24 @@
|
|||||||
package org.whispersystems.textsecuregcm;
|
package org.whispersystems.textsecuregcm;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.yammer.dropwizard.config.Configuration;
|
|
||||||
import com.yammer.dropwizard.db.DatabaseConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.GraphiteConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import io.dropwizard.Configuration;
|
||||||
|
import io.dropwizard.client.JerseyClientConfiguration;
|
||||||
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
|
|
||||||
public class WhisperServerConfiguration extends Configuration {
|
public class WhisperServerConfiguration extends Configuration {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -44,8 +46,9 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
private NexmoConfiguration nexmo;
|
private NexmoConfiguration nexmo;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private GcmConfiguration gcm;
|
private PushConfiguration push;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
@@ -55,15 +58,18 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private MemcacheConfiguration memcache;
|
private RedisConfiguration cache;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private RedisConfiguration redis;
|
private RedisConfiguration directory;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private ApnConfiguration apn = new ApnConfiguration();
|
private DataSourceFactory messageStore;
|
||||||
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@@ -72,7 +78,7 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private DatabaseConfiguration database = new DatabaseConfiguration();
|
private DataSourceFactory database = new DataSourceFactory();
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -83,6 +89,23 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private GraphiteConfiguration graphite = new GraphiteConfiguration();
|
private GraphiteConfiguration graphite = new GraphiteConfiguration();
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private WebsocketConfiguration websocket = new WebsocketConfiguration();
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
|
||||||
|
|
||||||
|
|
||||||
|
public WebsocketConfiguration getWebsocketConfiguration() {
|
||||||
|
return websocket;
|
||||||
|
}
|
||||||
|
|
||||||
public TwilioConfiguration getTwilioConfiguration() {
|
public TwilioConfiguration getTwilioConfiguration() {
|
||||||
return twilio;
|
return twilio;
|
||||||
}
|
}
|
||||||
@@ -91,27 +114,31 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
return nexmo;
|
return nexmo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GcmConfiguration getGcmConfiguration() {
|
public PushConfiguration getPushConfiguration() {
|
||||||
return gcm;
|
return push;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApnConfiguration getApnConfiguration() {
|
public JerseyClientConfiguration getJerseyClientConfiguration() {
|
||||||
return apn;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public S3Configuration getS3Configuration() {
|
public S3Configuration getS3Configuration() {
|
||||||
return s3;
|
return s3;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MemcacheConfiguration getMemcacheConfiguration() {
|
public RedisConfiguration getCacheConfiguration() {
|
||||||
return memcache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RedisConfiguration getRedisConfiguration() {
|
public RedisConfiguration getDirectoryConfiguration() {
|
||||||
return redis;
|
return directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DatabaseConfiguration getDatabaseConfiguration() {
|
public DataSourceFactory getMessageStoreConfiguration() {
|
||||||
|
return messageStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataSourceFactory getDataSourceFactory() {
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,4 +153,8 @@ public class WhisperServerConfiguration extends Configuration {
|
|||||||
public GraphiteConfiguration getGraphiteConfiguration() {
|
public GraphiteConfiguration getGraphiteConfiguration() {
|
||||||
return graphite;
|
return graphite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RedPhoneConfiguration getRedphoneConfiguration() {
|
||||||
|
return redphone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,52 +16,90 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm;
|
package org.whispersystems.textsecuregcm;
|
||||||
|
|
||||||
import com.yammer.dropwizard.Service;
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
import com.yammer.dropwizard.config.Bootstrap;
|
import com.codahale.metrics.graphite.GraphiteReporter;
|
||||||
import com.yammer.dropwizard.config.Environment;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.yammer.dropwizard.db.DatabaseConfiguration;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.jdbi.DBIFactory;
|
import com.sun.jersey.api.client.Client;
|
||||||
import com.yammer.dropwizard.migrations.MigrationsBundle;
|
|
||||||
import com.yammer.metrics.reporting.GraphiteReporter;
|
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||||
import org.skife.jdbi.v2.DBI;
|
import org.skife.jdbi.v2.DBI;
|
||||||
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;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
|
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.FederationController;
|
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
|
||||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
|
||||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||||
|
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.providers.MemcacheHealthCheck;
|
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
||||||
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
|
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
||||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||||
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
||||||
|
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.push.FeedbackHandler;
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.SenderFactory;
|
import org.whispersystems.textsecuregcm.push.PushServiceClient;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||||
|
import org.whispersystems.textsecuregcm.sms.NexmoSmsSender;
|
||||||
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
|
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
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.PendingDevicesManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||||
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
|
||||||
|
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||||
|
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||||
|
|
||||||
|
import javax.servlet.DispatcherType;
|
||||||
|
import javax.servlet.FilterRegistration;
|
||||||
|
import javax.servlet.ServletRegistration;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import io.dropwizard.Application;
|
||||||
|
import io.dropwizard.client.JerseyClientBuilder;
|
||||||
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
|
import io.dropwizard.jdbi.DBIFactory;
|
||||||
|
import io.dropwizard.metrics.graphite.GraphiteReporterFactory;
|
||||||
|
import io.dropwizard.setup.Bootstrap;
|
||||||
|
import io.dropwizard.setup.Environment;
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
|
|
||||||
public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
public class WhisperServerService extends Application<WhisperServerConfiguration> {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
@@ -69,67 +107,154 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||||
bootstrap.setName("whisper-server");
|
|
||||||
bootstrap.addCommand(new DirectoryCommand());
|
bootstrap.addCommand(new DirectoryCommand());
|
||||||
bootstrap.addBundle(new MigrationsBundle<WhisperServerConfiguration>() {
|
bootstrap.addCommand(new VacuumCommand());
|
||||||
|
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
|
||||||
@Override
|
@Override
|
||||||
public DatabaseConfiguration getDatabaseConfiguration(WhisperServerConfiguration configuration) {
|
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||||
return configuration.getDatabaseConfiguration();
|
return configuration.getDataSourceFactory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("messagedb", "messagedb.xml") {
|
||||||
|
@Override
|
||||||
|
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||||
|
return configuration.getMessageStoreConfiguration();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "whisper-server";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(WhisperServerConfiguration config, Environment environment)
|
public void run(WhisperServerConfiguration config, Environment environment)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
|
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
|
||||||
|
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
DBIFactory dbiFactory = new DBIFactory();
|
DBIFactory dbiFactory = new DBIFactory();
|
||||||
DBI jdbi = dbiFactory.build(environment, config.getDatabaseConfiguration(), "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);
|
||||||
Keys keys = jdbi.onDemand(Keys.class);
|
PendingDevices pendingDevices = database.onDemand(PendingDevices.class);
|
||||||
|
Keys keys = database.onDemand(Keys.class);
|
||||||
|
Messages messages = messagedb.onDemand(Messages.class);
|
||||||
|
|
||||||
MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient();
|
JedisPool cacheClient = new RedisClientFactory(config.getCacheConfiguration().getUrl()).getRedisClientPool();
|
||||||
JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool();
|
JedisPool directoryClient = new RedisClientFactory(config.getDirectoryConfiguration().getUrl()).getRedisClientPool();
|
||||||
|
Client httpClient = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
|
||||||
|
.build(getName());
|
||||||
|
|
||||||
DirectoryManager directory = new DirectoryManager(redisClient);
|
DirectoryManager directory = new DirectoryManager(directoryClient);
|
||||||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient);
|
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
|
||||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient);
|
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient);
|
||||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
|
||||||
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration());
|
||||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
|
MessagesManager messagesManager = new MessagesManager(messages);
|
||||||
SenderFactory senderFactory = new SenderFactory(config.getTwilioConfiguration(), config.getNexmoConfiguration());
|
PubSubManager pubSubManager = new PubSubManager(cacheClient);
|
||||||
|
PushServiceClient pushServiceClient = new PushServiceClient(httpClient, config.getPushConfiguration());
|
||||||
|
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
|
||||||
|
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
|
||||||
|
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
|
||||||
|
|
||||||
|
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||||
|
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
|
||||||
|
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
|
||||||
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
|
||||||
PushSender pushSender = new PushSender(config.getGcmConfiguration(),
|
PushSender pushSender = new PushSender(pushServiceClient, websocketSender);
|
||||||
config.getApnConfiguration(),
|
FeedbackHandler feedbackHandler = new FeedbackHandler(pushServiceClient, accountsManager);
|
||||||
accountsManager, directory);
|
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
|
||||||
|
|
||||||
environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
environment.lifecycle().manage(feedbackHandler);
|
||||||
|
|
||||||
|
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
|
||||||
|
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||||
|
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
|
||||||
|
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
|
||||||
|
|
||||||
|
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
|
||||||
FederatedPeer.class,
|
FederatedPeer.class,
|
||||||
accountAuthenticator,
|
deviceAuthenticator,
|
||||||
Account.class, "WhisperServer"));
|
Device.class, "WhisperServer"));
|
||||||
|
|
||||||
environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, senderFactory));
|
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, messagesManager, new TimeProvider(), authorizationKey));
|
||||||
environment.addResource(new DirectoryController(rateLimiters, directory));
|
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
|
||||||
environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner));
|
environment.jersey().register(new DirectoryController(rateLimiters, directory));
|
||||||
environment.addResource(new KeysController(rateLimiters, keys, federatedClientManager));
|
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));
|
||||||
environment.addResource(new FederationController(keys, accountsManager, pushSender, urlSigner));
|
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysControllerV2));
|
||||||
|
environment.jersey().register(new ReceiptController(accountsManager, federatedClientManager, pushSender));
|
||||||
|
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
|
||||||
|
environment.jersey().register(attachmentController);
|
||||||
|
environment.jersey().register(keysControllerV1);
|
||||||
|
environment.jersey().register(keysControllerV2);
|
||||||
|
environment.jersey().register(messageController);
|
||||||
|
|
||||||
environment.addServlet(new MessageController(rateLimiters, accountAuthenticator,
|
if (config.getWebsocketConfiguration().isEnabled()) {
|
||||||
pushSender, federatedClientManager),
|
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config);
|
||||||
MessageController.PATH);
|
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(deviceAuthenticator));
|
||||||
|
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(accountsManager, pushSender, messagesManager, pubSubManager));
|
||||||
|
webSocketEnvironment.jersey().register(new KeepAliveController());
|
||||||
|
|
||||||
environment.addHealthCheck(new RedisHealthCheck(redisClient));
|
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, config);
|
||||||
environment.addHealthCheck(new MemcacheHealthCheck(memcachedClient));
|
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
|
||||||
|
provisioningEnvironment.jersey().register(new KeepAliveController());
|
||||||
|
|
||||||
environment.addProvider(new IOExceptionMapper());
|
WebSocketResourceProviderFactory webSocketServlet = new WebSocketResourceProviderFactory(webSocketEnvironment );
|
||||||
environment.addProvider(new RateLimitExceededExceptionMapper());
|
WebSocketResourceProviderFactory provisioningServlet = new WebSocketResourceProviderFactory(provisioningEnvironment);
|
||||||
|
|
||||||
|
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet );
|
||||||
|
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
|
||||||
|
|
||||||
|
websocket.addMapping("/v1/websocket/");
|
||||||
|
websocket.setAsyncSupported(true);
|
||||||
|
|
||||||
|
provisioning.addMapping("/v1/websocket/provisioning/");
|
||||||
|
provisioning.setAsyncSupported(true);
|
||||||
|
|
||||||
|
webSocketServlet.start();
|
||||||
|
provisioningServlet.start();
|
||||||
|
|
||||||
|
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
|
||||||
|
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
|
||||||
|
filter.setInitParameter("allowedOrigins", "*");
|
||||||
|
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin");
|
||||||
|
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
|
||||||
|
filter.setInitParameter("preflightMaxAge", "5184000");
|
||||||
|
filter.setInitParameter("allowCredentials", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
|
||||||
|
environment.healthChecks().register("cache", new RedisHealthCheck(cacheClient));
|
||||||
|
|
||||||
|
environment.jersey().register(new IOExceptionMapper());
|
||||||
|
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||||
|
|
||||||
|
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
|
||||||
|
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
|
||||||
|
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
|
||||||
|
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
|
||||||
|
|
||||||
if (config.getGraphiteConfiguration().isEnabled()) {
|
if (config.getGraphiteConfiguration().isEnabled()) {
|
||||||
GraphiteReporter.enable(15, TimeUnit.SECONDS,
|
GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory();
|
||||||
config.getGraphiteConfiguration().getHost(),
|
graphiteReporterFactory.setHost(config.getGraphiteConfiguration().getHost());
|
||||||
config.getGraphiteConfiguration().getPort());
|
graphiteReporterFactory.setPort(config.getGraphiteConfiguration().getPort());
|
||||||
|
|
||||||
|
GraphiteReporter graphiteReporter = (GraphiteReporter) graphiteReporterFactory.build(environment.metrics());
|
||||||
|
graphiteReporter.start(15, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<NexmoSmsSender> initializeNexmoSmsSender(NexmoConfiguration configuration) {
|
||||||
|
if (configuration == null) {
|
||||||
|
return Optional.absent();
|
||||||
|
} else {
|
||||||
|
return Optional.of(new NexmoSmsSender(configuration));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,28 +16,27 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.auth;
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.auth.AuthenticationException;
|
|
||||||
import com.yammer.dropwizard.auth.Authenticator;
|
|
||||||
import com.yammer.dropwizard.auth.basic.BasicCredentials;
|
|
||||||
import com.yammer.metrics.Metrics;
|
|
||||||
import com.yammer.metrics.core.Meter;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.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.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import io.dropwizard.auth.AuthenticationException;
|
||||||
|
import io.dropwizard.auth.Authenticator;
|
||||||
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
|
|
||||||
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
|
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
|
||||||
|
|
||||||
private final Meter authenticationFailedMeter = Metrics.newMeter(AccountAuthenticator.class,
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
"authentication", "failed",
|
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
|
||||||
TimeUnit.MINUTES);
|
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded"));
|
||||||
|
|
||||||
private final Meter authenticationSucceededMeter = Metrics.newMeter(AccountAuthenticator.class,
|
|
||||||
"authentication", "succeeded",
|
|
||||||
TimeUnit.MINUTES);
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
|
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
|
||||||
|
|
||||||
@@ -51,18 +50,30 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
|
|||||||
public Optional<Account> authenticate(BasicCredentials basicCredentials)
|
public Optional<Account> authenticate(BasicCredentials basicCredentials)
|
||||||
throws AuthenticationException
|
throws AuthenticationException
|
||||||
{
|
{
|
||||||
Optional<Account> account = accountsManager.get(basicCredentials.getUsername());
|
try {
|
||||||
|
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
|
||||||
|
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber());
|
||||||
|
|
||||||
if (!account.isPresent()) {
|
if (!account.isPresent()) {
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
|
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
|
||||||
|
|
||||||
|
if (!device.isPresent()) {
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
|
||||||
authenticationSucceededMeter.mark();
|
authenticationSucceededMeter.mark();
|
||||||
|
account.get().setAuthenticatedDevice(device.get());
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticationFailedMeter.mark();
|
authenticationFailedMeter.mark();
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
|
} catch (InvalidAuthorizationHeaderException iahe) {
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,28 @@ import java.io.IOException;
|
|||||||
|
|
||||||
public class AuthorizationHeader {
|
public class AuthorizationHeader {
|
||||||
|
|
||||||
private final String user;
|
private final String number;
|
||||||
|
private final long accountId;
|
||||||
private final String password;
|
private final String password;
|
||||||
|
|
||||||
public AuthorizationHeader(String header) throws InvalidAuthorizationHeaderException {
|
private AuthorizationHeader(String number, long accountId, String password) {
|
||||||
|
this.number = number;
|
||||||
|
this.accountId = accountId;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {
|
||||||
|
try {
|
||||||
|
String[] numberAndId = user.split("\\.");
|
||||||
|
return new AuthorizationHeader(numberAndId[0],
|
||||||
|
numberAndId.length > 1 ? Long.parseLong(numberAndId[1]) : 1,
|
||||||
|
password);
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
throw new InvalidAuthorizationHeaderException(nfe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthorizationHeader fromFullHeader(String header) throws InvalidAuthorizationHeaderException {
|
||||||
try {
|
try {
|
||||||
if (header == null) {
|
if (header == null) {
|
||||||
throw new InvalidAuthorizationHeaderException("Null header");
|
throw new InvalidAuthorizationHeaderException("Null header");
|
||||||
@@ -55,16 +73,18 @@ public class AuthorizationHeader {
|
|||||||
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
|
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.user = credentialParts[0];
|
return fromUserAndPassword(credentialParts[0], credentialParts[1]);
|
||||||
this.password = credentialParts[1];
|
|
||||||
|
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
throw new InvalidAuthorizationHeaderException(ioe);
|
throw new InvalidAuthorizationHeaderException(ioe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserName() {
|
public String getNumber() {
|
||||||
return user;
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDeviceId() {
|
||||||
|
return accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
|
||||||
|
import org.apache.commons.codec.DecoderException;
|
||||||
|
import org.apache.commons.codec.binary.Hex;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class AuthorizationToken {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
|
||||||
|
|
||||||
|
private final String token;
|
||||||
|
private final byte[] key;
|
||||||
|
|
||||||
|
public AuthorizationToken(String token, byte[] key) {
|
||||||
|
this.token = token;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid(String number, long currentTimeMillis) {
|
||||||
|
String[] parts = token.split(":");
|
||||||
|
|
||||||
|
if (parts.length != 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!number.equals(parts[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidTime(parts[1], currentTimeMillis)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValidSignature(parts[0] + ":" + parts[1], parts[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidTime(String timeString, long currentTimeMillis) {
|
||||||
|
try {
|
||||||
|
long tokenTime = Long.parseLong(timeString);
|
||||||
|
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
|
||||||
|
|
||||||
|
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.warn("Number Format", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidSignature(String prefix, String suffix) {
|
||||||
|
try {
|
||||||
|
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||||
|
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||||
|
|
||||||
|
byte[] ourSuffix = Util.truncate(hmac.doFinal(prefix.getBytes()), 10);
|
||||||
|
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
|
||||||
|
|
||||||
|
return MessageDigest.isEqual(ourSuffix, theirSuffix);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (DecoderException e) {
|
||||||
|
logger.warn("Authorizationtoken", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,30 +16,35 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.auth;
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.auth.AuthenticationException;
|
|
||||||
import com.yammer.dropwizard.auth.Authenticator;
|
|
||||||
import com.yammer.dropwizard.auth.basic.BasicCredentials;
|
|
||||||
import com.yammer.metrics.Metrics;
|
|
||||||
import com.yammer.metrics.core.Meter;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import io.dropwizard.auth.AuthenticationException;
|
||||||
|
import io.dropwizard.auth.Authenticator;
|
||||||
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
|
|
||||||
|
|
||||||
public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> {
|
public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> {
|
||||||
|
|
||||||
private final Meter authenticationFailedMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
"authentication", "failed",
|
|
||||||
TimeUnit.MINUTES);
|
|
||||||
|
|
||||||
private final Meter authenticationSucceededMeter = Metrics.newMeter(FederatedPeerAuthenticator.class,
|
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(),
|
||||||
"authentication", "succeeded",
|
"authentication",
|
||||||
TimeUnit.MINUTES);
|
"failed"));
|
||||||
|
|
||||||
|
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(),
|
||||||
|
"authentication",
|
||||||
|
"succeeded"));
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(FederatedPeerAuthenticator.class);
|
private final Logger logger = LoggerFactory.getLogger(FederatedPeerAuthenticator.class);
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,14 @@ import com.sun.jersey.core.spi.component.ComponentContext;
|
|||||||
import com.sun.jersey.core.spi.component.ComponentScope;
|
import com.sun.jersey.core.spi.component.ComponentScope;
|
||||||
import com.sun.jersey.spi.inject.Injectable;
|
import com.sun.jersey.spi.inject.Injectable;
|
||||||
import com.sun.jersey.spi.inject.InjectableProvider;
|
import com.sun.jersey.spi.inject.InjectableProvider;
|
||||||
import com.yammer.dropwizard.auth.Auth;
|
|
||||||
import com.yammer.dropwizard.auth.Authenticator;
|
import io.dropwizard.auth.Auth;
|
||||||
import com.yammer.dropwizard.auth.basic.BasicAuthProvider;
|
import io.dropwizard.auth.Authenticator;
|
||||||
import com.yammer.dropwizard.auth.basic.BasicCredentials;
|
import io.dropwizard.auth.basic.BasicAuthProvider;
|
||||||
import org.slf4j.Logger;
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
|
public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, Parameter> {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(MultiBasicAuthProvider.class);
|
|
||||||
|
|
||||||
private final BasicAuthProvider<T1> provider1;
|
private final BasicAuthProvider<T1> provider1;
|
||||||
private final BasicAuthProvider<T2> provider2;
|
private final BasicAuthProvider<T2> provider2;
|
||||||
|
|
||||||
@@ -44,8 +41,8 @@ public class MultiBasicAuthProvider<T1,T2> implements InjectableProvider<Auth, P
|
|||||||
Class<?> clazz2,
|
Class<?> clazz2,
|
||||||
String realm)
|
String realm)
|
||||||
{
|
{
|
||||||
this.provider1 = new BasicAuthProvider<T1>(authenticator1, realm);
|
this.provider1 = new BasicAuthProvider<>(authenticator1, realm);
|
||||||
this.provider2 = new BasicAuthProvider<T2>(authenticator2, realm);
|
this.provider2 = new BasicAuthProvider<>(authenticator2, realm);
|
||||||
this.clazz1 = clazz1;
|
this.clazz1 = clazz1;
|
||||||
this.clazz2 = clazz2;
|
this.clazz2 = clazz2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,8 @@ package org.whispersystems.textsecuregcm.configuration;
|
|||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class FederationConfiguration {
|
public class FederationConfiguration {
|
||||||
@@ -34,31 +30,7 @@ public class FederationConfiguration {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private String herokuPeers;
|
|
||||||
|
|
||||||
public List<FederatedPeer> getPeers() {
|
public List<FederatedPeer> getPeers() {
|
||||||
if (peers != null) {
|
|
||||||
return peers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (herokuPeers != null) {
|
|
||||||
List<FederatedPeer> peers = new LinkedList<>();
|
|
||||||
JsonElement root = new JsonParser().parse(herokuPeers);
|
|
||||||
JsonArray peerElements = root.getAsJsonArray();
|
|
||||||
|
|
||||||
for (JsonElement peer : peerElements) {
|
|
||||||
String name = peer.getAsJsonObject().get("name").getAsString();
|
|
||||||
String url = peer.getAsJsonObject().get("url").getAsString();
|
|
||||||
String authenticationToken = peer.getAsJsonObject().get("authenticationToken").getAsString();
|
|
||||||
String certificate = peer.getAsJsonObject().get("certificate").getAsString();
|
|
||||||
|
|
||||||
peers.add(new FederatedPeer(name, url, authenticationToken, certificate));
|
|
||||||
}
|
|
||||||
|
|
||||||
return peers;
|
|
||||||
}
|
|
||||||
|
|
||||||
return peers;
|
return peers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,14 @@ package org.whispersystems.textsecuregcm.configuration;
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
public class GcmConfiguration {
|
public class GcmConfiguration {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private long senderId;
|
||||||
|
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
@@ -28,4 +34,8 @@ public class GcmConfiguration {
|
|||||||
public String getApiKey() {
|
public String getApiKey() {
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getSenderId() {
|
||||||
|
return senderId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
public class MessageStoreConfiguration {
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
|
||||||
|
public class PushConfiguration {
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Min(1)
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,20 @@ public class RateLimitsConfiguration {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private RateLimitConfiguration messages = new RateLimitConfiguration(60, 60);
|
private RateLimitConfiguration messages = new RateLimitConfiguration(60, 60);
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private RateLimitConfiguration allocateDevice = new RateLimitConfiguration(2, 1.0 / 2.0);
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private RateLimitConfiguration verifyDevice = new RateLimitConfiguration(2, 2);
|
||||||
|
|
||||||
|
public RateLimitConfiguration getAllocateDevice() {
|
||||||
|
return allocateDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateLimitConfiguration getVerifyDevice() {
|
||||||
|
return verifyDevice;
|
||||||
|
}
|
||||||
|
|
||||||
public RateLimitConfiguration getMessages() {
|
public RateLimitConfiguration getMessages() {
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.apache.commons.codec.DecoderException;
|
||||||
|
import org.apache.commons.codec.binary.Hex;
|
||||||
|
|
||||||
|
public class RedPhoneConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String authKey;
|
||||||
|
|
||||||
|
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
|
||||||
|
if (authKey == null || authKey.trim().length() == 0) {
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,9 @@ public class TwilioConfiguration {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String localDomain;
|
private String localDomain;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean international;
|
||||||
|
|
||||||
public String getAccountId() {
|
public String getAccountId() {
|
||||||
return accountId;
|
return accountId;
|
||||||
}
|
}
|
||||||
@@ -52,4 +55,8 @@ public class TwilioConfiguration {
|
|||||||
public String getLocalDomain() {
|
public String getLocalDomain() {
|
||||||
return localDomain;
|
return localDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isInternational() {
|
||||||
|
return international;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class WebsocketConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,22 +16,26 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.auth.Auth;
|
|
||||||
import com.yammer.metrics.annotation.Timed;
|
|
||||||
import org.slf4j.Logger;
|
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.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.sms.SenderFactory;
|
import org.whispersystems.textsecuregcm.providers.TimeProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
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.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||||
@@ -53,6 +57,8 @@ import java.io.IOException;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
@Path("/v1/accounts")
|
@Path("/v1/accounts")
|
||||||
public class AccountController {
|
public class AccountController {
|
||||||
|
|
||||||
@@ -61,17 +67,26 @@ public class AccountController {
|
|||||||
private final PendingAccountsManager pendingAccounts;
|
private final PendingAccountsManager pendingAccounts;
|
||||||
private final AccountsManager accounts;
|
private final AccountsManager accounts;
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final SenderFactory senderFactory;
|
private final SmsSender smsSender;
|
||||||
|
private final MessagesManager messagesManager;
|
||||||
|
private final TimeProvider timeProvider;
|
||||||
|
private final Optional<byte[]> authorizationKey;
|
||||||
|
|
||||||
public AccountController(PendingAccountsManager pendingAccounts,
|
public AccountController(PendingAccountsManager pendingAccounts,
|
||||||
AccountsManager accounts,
|
AccountsManager accounts,
|
||||||
RateLimiters rateLimiters,
|
RateLimiters rateLimiters,
|
||||||
SenderFactory smsSenderFactory)
|
SmsSender smsSenderFactory,
|
||||||
|
MessagesManager messagesManager,
|
||||||
|
TimeProvider timeProvider,
|
||||||
|
Optional<byte[]> authorizationKey)
|
||||||
{
|
{
|
||||||
this.pendingAccounts = pendingAccounts;
|
this.pendingAccounts = pendingAccounts;
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.senderFactory = smsSenderFactory;
|
this.smsSender = smsSenderFactory;
|
||||||
|
this.messagesManager = messagesManager;
|
||||||
|
this.timeProvider = timeProvider;
|
||||||
|
this.authorizationKey = authorizationKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@@ -94,16 +109,16 @@ public class AccountController {
|
|||||||
rateLimiters.getVoiceDestinationLimiter().validate(number);
|
rateLimiters.getVoiceDestinationLimiter().validate(number);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new WebApplicationException(Response.status(415).build());
|
throw new WebApplicationException(Response.status(422).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
VerificationCode verificationCode = generateVerificationCode();
|
VerificationCode verificationCode = generateVerificationCode();
|
||||||
pendingAccounts.store(number, verificationCode.getVerificationCode());
|
pendingAccounts.store(number, verificationCode.getVerificationCode());
|
||||||
|
|
||||||
if (transport.equals("sms")) {
|
if (transport.equals("sms")) {
|
||||||
senderFactory.getSmsSender(number).deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
|
smsSender.deliverSmsVerification(number, verificationCode.getVerificationCodeDisplay());
|
||||||
} else if (transport.equals("voice")) {
|
} else if (transport.equals("voice")) {
|
||||||
senderFactory.getVoxSender(number).deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
|
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCodeSpeech());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
@@ -119,8 +134,8 @@ public class AccountController {
|
|||||||
throws RateLimitExceededException
|
throws RateLimitExceededException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
AuthorizationHeader header = new AuthorizationHeader(authorizationHeader);
|
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||||
String number = header.getUserName();
|
String number = header.getNumber();
|
||||||
String password = header.getPassword();
|
String password = header.getPassword();
|
||||||
|
|
||||||
rateLimiters.getVerifyLimiter().validate(number);
|
rateLimiters.getVerifyLimiter().validate(number);
|
||||||
@@ -133,28 +148,59 @@ public class AccountController {
|
|||||||
throw new WebApplicationException(Response.status(403).build());
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
Account account = new Account();
|
if (accounts.isRelayListed(number)) {
|
||||||
account.setNumber(number);
|
throw new WebApplicationException(Response.status(417).build());
|
||||||
account.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
}
|
||||||
account.setSignalingKey(accountAttributes.getSignalingKey());
|
|
||||||
account.setSupportsSms(accountAttributes.getSupportsSms());
|
|
||||||
|
|
||||||
accounts.create(account);
|
|
||||||
logger.debug("Stored account...");
|
|
||||||
|
|
||||||
|
createAccount(number, password, accountAttributes);
|
||||||
} catch (InvalidAuthorizationHeaderException e) {
|
} catch (InvalidAuthorizationHeaderException e) {
|
||||||
logger.info("Bad Authorization Header", e);
|
logger.info("Bad Authorization Header", e);
|
||||||
throw new WebApplicationException(Response.status(401).build());
|
throw new WebApplicationException(Response.status(401).build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/token/{verification_token}")
|
||||||
|
public void verifyToken(@PathParam("verification_token") String verificationToken,
|
||||||
|
@HeaderParam("Authorization") String authorizationHeader,
|
||||||
|
@Valid AccountAttributes accountAttributes)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||||
|
String number = header.getNumber();
|
||||||
|
String password = header.getPassword();
|
||||||
|
|
||||||
|
rateLimiters.getVerifyLimiter().validate(number);
|
||||||
|
|
||||||
|
if (!authorizationKey.isPresent()) {
|
||||||
|
logger.debug("Attempt to authorize with key but not configured...");
|
||||||
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationToken token = new AuthorizationToken(verificationToken, authorizationKey.get());
|
||||||
|
|
||||||
|
if (!token.isValid(number, timeProvider.getCurrentTimeMillis())) {
|
||||||
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccount(number, password, accountAttributes);
|
||||||
|
} catch (InvalidAuthorizationHeaderException e) {
|
||||||
|
logger.info("Bad authorization header", e);
|
||||||
|
throw new WebApplicationException(Response.status(401).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@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) {
|
||||||
account.setApnRegistrationId(null);
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
account.setGcmRegistrationId(registrationId.getGcmRegistrationId());
|
device.setApnId(null);
|
||||||
|
device.setGcmId(registrationId.getGcmRegistrationId());
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +208,8 @@ public class AccountController {
|
|||||||
@DELETE
|
@DELETE
|
||||||
@Path("/gcm/")
|
@Path("/gcm/")
|
||||||
public void deleteGcmRegistrationId(@Auth Account account) {
|
public void deleteGcmRegistrationId(@Auth Account account) {
|
||||||
account.setGcmRegistrationId(null);
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
device.setGcmId(null);
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +218,9 @@ public class AccountController {
|
|||||||
@Path("/apn/")
|
@Path("/apn/")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
|
public void setApnRegistrationId(@Auth Account account, @Valid ApnRegistrationId registrationId) {
|
||||||
account.setApnRegistrationId(registrationId.getApnRegistrationId());
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
account.setGcmRegistrationId(null);
|
device.setApnId(registrationId.getApnRegistrationId());
|
||||||
|
device.setGcmId(null);
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +228,8 @@ public class AccountController {
|
|||||||
@DELETE
|
@DELETE
|
||||||
@Path("/apn/")
|
@Path("/apn/")
|
||||||
public void deleteApnRegistrationId(@Auth Account account) {
|
public void deleteApnRegistrationId(@Auth Account account) {
|
||||||
account.setApnRegistrationId(null);
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
device.setApnId(null);
|
||||||
accounts.update(account);
|
accounts.update(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,11 +239,30 @@ public class AccountController {
|
|||||||
@Produces(MediaType.APPLICATION_XML)
|
@Produces(MediaType.APPLICATION_XML)
|
||||||
public Response getTwiml(@PathParam("code") String encodedVerificationText) {
|
public Response getTwiml(@PathParam("code") String encodedVerificationText) {
|
||||||
return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML,
|
return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML,
|
||||||
SenderFactory.VoxSender.VERIFICATION_TEXT +
|
|
||||||
encodedVerificationText)).build();
|
encodedVerificationText)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private VerificationCode generateVerificationCode() {
|
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
|
||||||
|
Device device = new Device();
|
||||||
|
device.setId(Device.MASTER_ID);
|
||||||
|
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||||
|
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||||
|
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||||
|
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||||
|
|
||||||
|
Account account = new Account();
|
||||||
|
account.setNumber(number);
|
||||||
|
account.setSupportsSms(accountAttributes.getSupportsSms());
|
||||||
|
account.addDevice(device);
|
||||||
|
|
||||||
|
accounts.create(account);
|
||||||
|
messagesManager.clear(number);
|
||||||
|
pendingAccounts.remove(number);
|
||||||
|
|
||||||
|
logger.debug("Stored device...");
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||||
try {
|
try {
|
||||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||||
int randomInt = 100000 + random.nextInt(900000);
|
int randomInt = 100000 + random.nextInt(900000);
|
||||||
@@ -203,5 +271,4 @@ public class AccountController {
|
|||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import com.amazonaws.HttpMethod;
|
import com.amazonaws.HttpMethod;
|
||||||
import com.yammer.dropwizard.auth.Auth;
|
import com.codahale.metrics.annotation.Timed;
|
||||||
import com.yammer.metrics.annotation.Timed;
|
import com.google.common.base.Optional;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
|
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
|
||||||
@@ -35,14 +35,16 @@ import javax.ws.rs.Path;
|
|||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
|
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.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
|
||||||
@Path("/v1/attachments")
|
@Path("/v1/attachments")
|
||||||
public class AttachmentController {
|
public class AttachmentController {
|
||||||
@@ -65,37 +67,38 @@ public class AttachmentController {
|
|||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response allocateAttachment(@Auth Account account) throws RateLimitExceededException {
|
public AttachmentDescriptor allocateAttachment(@Auth Account account)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
if (account.isRateLimited()) {
|
||||||
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
|
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
|
||||||
|
}
|
||||||
|
|
||||||
long attachmentId = generateAttachmentId();
|
long attachmentId = generateAttachmentId();
|
||||||
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);
|
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);
|
||||||
AttachmentDescriptor descriptor = new AttachmentDescriptor(attachmentId, url.toExternalForm());
|
|
||||||
|
|
||||||
return Response.ok().entity(descriptor).build();
|
return new AttachmentDescriptor(attachmentId, url.toExternalForm());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/{attachmentId}")
|
@Path("/{attachmentId}")
|
||||||
public Response redirectToAttachment(@Auth Account account,
|
public AttachmentUri redirectToAttachment(@Auth Account account,
|
||||||
@PathParam("attachmentId") long attachmentId,
|
@PathParam("attachmentId") long attachmentId,
|
||||||
@QueryParam("relay") String relay)
|
@QueryParam("relay") Optional<String> relay)
|
||||||
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
URL url;
|
if (!relay.isPresent()) {
|
||||||
|
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET));
|
||||||
if (relay == null) url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
|
} else {
|
||||||
else url = federatedClientManager.getClient(relay).getSignedAttachmentUri(attachmentId);
|
return new AttachmentUri(federatedClientManager.getClient(relay.get()).getSignedAttachmentUri(attachmentId));
|
||||||
|
}
|
||||||
return Response.ok().entity(new AttachmentUri(url)).build();
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("No conectivity", e);
|
|
||||||
return Response.status(500).build();
|
|
||||||
} catch (NoSuchPeerException e) {
|
} catch (NoSuchPeerException e) {
|
||||||
logger.info("No such peer: " + relay);
|
logger.info("No such peer: " + relay);
|
||||||
return Response.status(404).build();
|
throw new WebApplicationException(Response.status(404).build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2013 Open WhisperSystems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.HeaderParam;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v1/devices")
|
||||||
|
public class DeviceController {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
|
||||||
|
|
||||||
|
private final PendingDevicesManager pendingDevices;
|
||||||
|
private final AccountsManager accounts;
|
||||||
|
private final RateLimiters rateLimiters;
|
||||||
|
|
||||||
|
public DeviceController(PendingDevicesManager pendingDevices,
|
||||||
|
AccountsManager accounts,
|
||||||
|
RateLimiters rateLimiters)
|
||||||
|
{
|
||||||
|
this.pendingDevices = pendingDevices;
|
||||||
|
this.accounts = accounts;
|
||||||
|
this.rateLimiters = rateLimiters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/provisioning/code")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public VerificationCode createDeviceToken(@Auth Account account)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
|
||||||
|
|
||||||
|
VerificationCode verificationCode = generateVerificationCode();
|
||||||
|
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
|
||||||
|
|
||||||
|
return verificationCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/{verification_code}")
|
||||||
|
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
||||||
|
@HeaderParam("Authorization") String authorizationHeader,
|
||||||
|
@Valid AccountAttributes accountAttributes)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||||
|
String number = header.getNumber();
|
||||||
|
String password = header.getPassword();
|
||||||
|
|
||||||
|
rateLimiters.getVerifyDeviceLimiter().validate(number);
|
||||||
|
|
||||||
|
Optional<String> storedVerificationCode = pendingDevices.getCodeForNumber(number);
|
||||||
|
|
||||||
|
if (!storedVerificationCode.isPresent() ||
|
||||||
|
!MessageDigest.isEqual(verificationCode.getBytes(), storedVerificationCode.get().getBytes()))
|
||||||
|
{
|
||||||
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<Account> account = accounts.get(number);
|
||||||
|
|
||||||
|
if (!account.isPresent()) {
|
||||||
|
throw new WebApplicationException(Response.status(403).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
Device device = new Device();
|
||||||
|
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||||
|
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||||
|
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||||
|
device.setId(account.get().getNextDeviceId());
|
||||||
|
device.setLastSeen(Util.todayInMillis());
|
||||||
|
|
||||||
|
account.get().addDevice(device);
|
||||||
|
accounts.update(account.get());
|
||||||
|
|
||||||
|
pendingDevices.remove(number);
|
||||||
|
|
||||||
|
return new DeviceResponse(device.getId());
|
||||||
|
} catch (InvalidAuthorizationHeaderException e) {
|
||||||
|
logger.info("Bad Authorization Header", e);
|
||||||
|
throw new WebApplicationException(Response.status(401).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||||
|
try {
|
||||||
|
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||||
|
int randomInt = 100000 + random.nextInt(900000);
|
||||||
|
return new VerificationCode(randomInt);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,19 +16,21 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Histogram;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.yammer.dropwizard.auth.Auth;
|
|
||||||
import com.yammer.metrics.annotation.Metered;
|
|
||||||
import com.yammer.metrics.annotation.Timed;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||||
import org.whispersystems.textsecuregcm.util.Base64;
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
@@ -44,10 +46,15 @@ import java.io.IOException;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
@Path("/v1/directory")
|
@Path("/v1/directory")
|
||||||
public class DirectoryController {
|
public class DirectoryController {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
|
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
|
||||||
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
private final Histogram contactsHistogram = metricRegistry.histogram(name(getClass(), "contacts"));
|
||||||
|
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final DirectoryManager directory;
|
private final DirectoryManager directory;
|
||||||
@@ -57,7 +64,7 @@ public class DirectoryController {
|
|||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed()
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Path("/{token}")
|
@Path("/{token}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@@ -78,7 +85,7 @@ public class DirectoryController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed()
|
@Timed
|
||||||
@PUT
|
@PUT
|
||||||
@Path("/tokens")
|
@Path("/tokens")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@@ -87,6 +94,7 @@ public class DirectoryController {
|
|||||||
throws RateLimitExceededException
|
throws RateLimitExceededException
|
||||||
{
|
{
|
||||||
rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size());
|
rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size());
|
||||||
|
contactsHistogram.update(contacts.getContacts().size());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<byte[]> tokens = new LinkedList<>();
|
List<byte[]> tokens = new LinkedList<>();
|
||||||
|
|||||||
@@ -1,157 +1,19 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import com.amazonaws.HttpMethod;
|
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
|
||||||
import com.yammer.dropwizard.auth.Auth;
|
|
||||||
import com.yammer.metrics.annotation.Timed;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
|
||||||
import org.whispersystems.textsecuregcm.push.PushSender;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
|
||||||
import org.whispersystems.textsecuregcm.util.UrlSigner;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
import javax.validation.Valid;
|
|
||||||
import javax.ws.rs.Consumes;
|
|
||||||
import javax.ws.rs.GET;
|
|
||||||
import javax.ws.rs.PUT;
|
|
||||||
import javax.ws.rs.Path;
|
|
||||||
import javax.ws.rs.PathParam;
|
|
||||||
import javax.ws.rs.Produces;
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
|
||||||
import javax.ws.rs.core.MediaType;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Path("/v1/federation")
|
|
||||||
public class FederationController {
|
public class FederationController {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(FederationController.class);
|
protected final AccountsManager accounts;
|
||||||
|
protected final AttachmentController attachmentController;
|
||||||
|
protected final MessageController messageController;
|
||||||
|
|
||||||
private static final int ACCOUNT_CHUNK_SIZE = 10000;
|
public FederationController(AccountsManager accounts,
|
||||||
|
AttachmentController attachmentController,
|
||||||
private final PushSender pushSender;
|
MessageController messageController)
|
||||||
private final Keys keys;
|
{
|
||||||
private final AccountsManager accounts;
|
|
||||||
private final UrlSigner urlSigner;
|
|
||||||
|
|
||||||
public FederationController(Keys keys, AccountsManager accounts, PushSender pushSender, UrlSigner urlSigner) {
|
|
||||||
this.keys = keys;
|
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
this.pushSender = pushSender;
|
this.attachmentController = attachmentController;
|
||||||
this.urlSigner = urlSigner;
|
this.messageController = messageController;
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
|
||||||
@GET
|
|
||||||
@Path("/attachment/{attachmentId}")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
|
|
||||||
@PathParam("attachmentId") long attachmentId)
|
|
||||||
{
|
|
||||||
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET);
|
|
||||||
return new AttachmentUri(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
|
||||||
@GET
|
|
||||||
@Path("/key/{number}")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public PreKey getKey(@Auth FederatedPeer peer,
|
|
||||||
@PathParam("number") String number)
|
|
||||||
{
|
|
||||||
PreKey preKey = keys.get(number);
|
|
||||||
|
|
||||||
if (preKey == null) {
|
|
||||||
throw new WebApplicationException(Response.status(404).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
return preKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
|
||||||
@PUT
|
|
||||||
@Path("/message")
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
public void relayMessage(@Auth FederatedPeer peer, @Valid RelayMessage message)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
OutgoingMessageSignal signal = OutgoingMessageSignal.parseFrom(message.getOutgoingMessageSignal())
|
|
||||||
.toBuilder()
|
|
||||||
.setRelay(peer.getName())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
pushSender.sendMessage(message.getDestination(), signal);
|
|
||||||
} catch (InvalidProtocolBufferException ipe) {
|
|
||||||
logger.warn("ProtoBuf", ipe);
|
|
||||||
throw new WebApplicationException(Response.status(400).build());
|
|
||||||
} catch (NoSuchUserException e) {
|
|
||||||
logger.debug("No User", e);
|
|
||||||
throw new WebApplicationException(Response.status(404).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
|
||||||
@GET
|
|
||||||
@Path("/user_count")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public AccountCount getUserCount(@Auth FederatedPeer peer) {
|
|
||||||
return new AccountCount((int)accounts.getCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Timed
|
|
||||||
@GET
|
|
||||||
@Path("/user_tokens/{offset}")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
|
|
||||||
@PathParam("offset") int offset)
|
|
||||||
{
|
|
||||||
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
|
|
||||||
List<ClientContact> clientContacts = new LinkedList<>();
|
|
||||||
|
|
||||||
for (Account account : accountList) {
|
|
||||||
byte[] token = Util.getContactToken(account.getNumber());
|
|
||||||
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
|
||||||
|
|
||||||
if (Util.isEmpty(account.getApnRegistrationId()) &&
|
|
||||||
Util.isEmpty(account.getGcmRegistrationId()))
|
|
||||||
{
|
|
||||||
clientContact.setInactive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
clientContacts.add(clientContact);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ClientContacts(clientContacts);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2013 Open WhisperSystems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v1/federation")
|
||||||
|
public class FederationControllerV1 extends FederationController {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(FederationControllerV1.class);
|
||||||
|
|
||||||
|
private static final int ACCOUNT_CHUNK_SIZE = 10000;
|
||||||
|
|
||||||
|
private final KeysControllerV1 keysControllerV1;
|
||||||
|
|
||||||
|
public FederationControllerV1(AccountsManager accounts,
|
||||||
|
AttachmentController attachmentController,
|
||||||
|
MessageController messageController,
|
||||||
|
KeysControllerV1 keysControllerV1)
|
||||||
|
{
|
||||||
|
super(accounts, attachmentController, messageController);
|
||||||
|
this.keysControllerV1 = keysControllerV1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/attachment/{attachmentId}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
|
||||||
|
@PathParam("attachmentId") long attachmentId)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||||
|
attachmentId, Optional.<String>absent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/key/{number}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Optional<PreKeyV1> getKey(@Auth FederatedPeer peer,
|
||||||
|
@PathParam("number") String number)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return keysControllerV1.get(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||||
|
number, Optional.<String>absent());
|
||||||
|
} catch (RateLimitExceededException e) {
|
||||||
|
logger.warn("Rate limiting on federated channel", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/key/{number}/{device}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Optional<PreKeyResponseV1> getKeysV1(@Auth FederatedPeer peer,
|
||||||
|
@PathParam("number") String number,
|
||||||
|
@PathParam("device") String device)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||||
|
number, device, Optional.<String>absent());
|
||||||
|
} catch (RateLimitExceededException e) {
|
||||||
|
logger.warn("Rate limiting on federated channel", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Path("/messages/{source}/{sourceDeviceId}/{destination}")
|
||||||
|
public void sendMessages(@Auth FederatedPeer peer,
|
||||||
|
@PathParam("source") String source,
|
||||||
|
@PathParam("sourceDeviceId") long sourceDeviceId,
|
||||||
|
@PathParam("destination") String destination,
|
||||||
|
@Valid IncomingMessageList messages)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
messages.setRelay(null);
|
||||||
|
messageController.sendMessage(new NonLimitedAccount(source, sourceDeviceId, peer.getName()), destination, messages);
|
||||||
|
} catch (RateLimitExceededException e) {
|
||||||
|
logger.warn("Rate limiting on federated channel", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/user_count")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public AccountCount getUserCount(@Auth FederatedPeer peer) {
|
||||||
|
return new AccountCount((int)accounts.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/user_tokens/{offset}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
|
||||||
|
@PathParam("offset") int offset)
|
||||||
|
{
|
||||||
|
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
|
||||||
|
List<ClientContact> clientContacts = new LinkedList<>();
|
||||||
|
|
||||||
|
for (Account account : accountList) {
|
||||||
|
byte[] token = Util.getContactToken(account.getNumber());
|
||||||
|
ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms());
|
||||||
|
|
||||||
|
if (!account.isActive()) {
|
||||||
|
clientContact.setInactive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
clientContacts.add(clientContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClientContacts(clientContacts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v2/federation")
|
||||||
|
public class FederationControllerV2 extends FederationController {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(FederationControllerV2.class);
|
||||||
|
|
||||||
|
private final KeysControllerV2 keysControllerV2;
|
||||||
|
|
||||||
|
public FederationControllerV2(AccountsManager accounts, AttachmentController attachmentController, MessageController messageController, KeysControllerV2 keysControllerV2) {
|
||||||
|
super(accounts, attachmentController, messageController);
|
||||||
|
this.keysControllerV2 = keysControllerV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/key/{number}/{device}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Optional<PreKeyResponseV2> getKeysV2(@Auth FederatedPeer peer,
|
||||||
|
@PathParam("number") String number,
|
||||||
|
@PathParam("device") String device)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return keysControllerV2.getDeviceKeys(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||||
|
number, device, Optional.<String>absent());
|
||||||
|
} catch (RateLimitExceededException e) {
|
||||||
|
logger.warn("Rate limiting on federated channel", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
public class InvalidDestinationException extends Exception {
|
||||||
|
public InvalidDestinationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
|
||||||
|
@Path("/v1/keepalive")
|
||||||
|
public class KeepAliveController {
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
public Response getKeepAlive() {
|
||||||
|
return Response.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
||||||
@@ -16,76 +16,100 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import com.yammer.dropwizard.auth.Auth;
|
import com.codahale.metrics.annotation.Timed;
|
||||||
import com.yammer.metrics.annotation.Timed;
|
import com.google.common.base.Optional;
|
||||||
import org.slf4j.Logger;
|
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKeyList;
|
|
||||||
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
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.KeyRecord;
|
||||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
|
||||||
import javax.ws.rs.Consumes;
|
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.PUT;
|
|
||||||
import javax.ws.rs.Path;
|
|
||||||
import javax.ws.rs.PathParam;
|
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
|
||||||
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.util.List;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
@Path("/v1/keys")
|
|
||||||
public class KeysController {
|
public class KeysController {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
protected final RateLimiters rateLimiters;
|
||||||
|
protected final Keys keys;
|
||||||
|
protected final AccountsManager accounts;
|
||||||
|
protected final FederatedClientManager federatedClientManager;
|
||||||
|
|
||||||
private final RateLimiters rateLimiters;
|
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||||
private final Keys keys;
|
|
||||||
private final FederatedClientManager federatedClientManager;
|
|
||||||
|
|
||||||
public KeysController(RateLimiters rateLimiters, Keys keys,
|
|
||||||
FederatedClientManager federatedClientManager)
|
FederatedClientManager federatedClientManager)
|
||||||
{
|
{
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
|
this.accounts = accounts;
|
||||||
this.federatedClientManager = federatedClientManager;
|
this.federatedClientManager = federatedClientManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@PUT
|
@GET
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
|
public PreKeyCount getStatus(@Auth Account account) {
|
||||||
keys.store(account.getNumber(), preKeys.getLastResortKey(), preKeys.getKeys());
|
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
count = count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed
|
return new PreKeyCount(count);
|
||||||
@GET
|
}
|
||||||
@Path("/{number}")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
|
||||||
public PreKey get(@Auth Account account,
|
throws NoSuchUserException
|
||||||
@PathParam("number") String number,
|
|
||||||
@QueryParam("relay") String relay)
|
|
||||||
throws RateLimitExceededException
|
|
||||||
{
|
{
|
||||||
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number);
|
Optional<Account> destination = accounts.get(number);
|
||||||
|
|
||||||
|
if (!destination.isPresent() || !destination.get().isActive()) {
|
||||||
|
throw new NoSuchUserException("Target account is inactive");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PreKey key;
|
if (deviceIdSelector.equals("*")) {
|
||||||
|
Optional<List<KeyRecord>> preKeys = keys.get(number);
|
||||||
|
return new TargetKeys(destination.get(), preKeys);
|
||||||
|
}
|
||||||
|
|
||||||
if (relay == null) key = keys.get(number);
|
long deviceId = Long.parseLong(deviceIdSelector);
|
||||||
else key = federatedClientManager.getClient(relay).getKey(number);
|
Optional<Device> targetDevice = destination.get().getDevice(deviceId);
|
||||||
|
|
||||||
if (key == null) throw new WebApplicationException(Response.status(404).build());
|
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
|
||||||
else return key;
|
throw new NoSuchUserException("Target device is inactive.");
|
||||||
} catch (NoSuchPeerException e) {
|
}
|
||||||
logger.info("No peer: " + relay);
|
|
||||||
throw new WebApplicationException(Response.status(404).build());
|
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
|
||||||
|
return new TargetKeys(destination.get(), preKeys);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new WebApplicationException(Response.status(422).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class TargetKeys {
|
||||||
|
private final Account destination;
|
||||||
|
private final Optional<List<KeyRecord>> keys;
|
||||||
|
|
||||||
|
public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
|
||||||
|
this.destination = destination;
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<List<KeyRecord>> getKeys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account getDestination() {
|
||||||
|
return destination;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyStateV1;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.KeyRecord;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v1/keys")
|
||||||
|
public class KeysControllerV1 extends KeysController {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(KeysControllerV1.class);
|
||||||
|
|
||||||
|
public KeysControllerV1(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||||
|
FederatedClientManager federatedClientManager)
|
||||||
|
{
|
||||||
|
super(rateLimiters, keys, accounts, federatedClientManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public void setKeys(@Auth Account account, @Valid PreKeyStateV1 preKeys) {
|
||||||
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
String identityKey = preKeys.getLastResortKey().getIdentityKey();
|
||||||
|
|
||||||
|
if (!identityKey.equals(account.getIdentityKey())) {
|
||||||
|
account.setIdentityKey(identityKey);
|
||||||
|
accounts.update(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/{number}/{device_id}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Optional<PreKeyResponseV1> getDeviceKey(@Auth Account account,
|
||||||
|
@PathParam("number") String number,
|
||||||
|
@PathParam("device_id") String deviceId,
|
||||||
|
@QueryParam("relay") Optional<String> relay)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (account.isRateLimited()) {
|
||||||
|
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay.isPresent()) {
|
||||||
|
return federatedClientManager.getClient(relay.get()).getKeysV1(number, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
TargetKeys targetKeys = getLocalKeys(number, deviceId);
|
||||||
|
|
||||||
|
if (!targetKeys.getKeys().isPresent()) {
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PreKeyV1> preKeys = new LinkedList<>();
|
||||||
|
Account destination = targetKeys.getDestination();
|
||||||
|
|
||||||
|
for (KeyRecord record : targetKeys.getKeys().get()) {
|
||||||
|
Optional<Device> device = destination.getDevice(record.getDeviceId());
|
||||||
|
if (device.isPresent() && device.get().isActive()) {
|
||||||
|
preKeys.add(new PreKeyV1(record.getDeviceId(), record.getKeyId(),
|
||||||
|
record.getPublicKey(), destination.getIdentityKey(),
|
||||||
|
device.get().getRegistrationId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preKeys.isEmpty()) return Optional.absent();
|
||||||
|
else return Optional.of(new PreKeyResponseV1(preKeys));
|
||||||
|
} catch (NoSuchPeerException | NoSuchUserException e) {
|
||||||
|
throw new WebApplicationException(Response.status(404).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/{number}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Optional<PreKeyV1> get(@Auth Account account,
|
||||||
|
@PathParam("number") String number,
|
||||||
|
@QueryParam("relay") Optional<String> relay)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
Optional<PreKeyResponseV1> results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay);
|
||||||
|
|
||||||
|
if (results.isPresent()) return Optional.of(results.get().getKeys().get(0));
|
||||||
|
else return Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItemV2;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyStateV2;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.KeyRecord;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v2/keys")
|
||||||
|
public class KeysControllerV2 extends KeysController {
|
||||||
|
|
||||||
|
public KeysControllerV2(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||||
|
FederatedClientManager federatedClientManager)
|
||||||
|
{
|
||||||
|
super(rateLimiters, keys, accounts, federatedClientManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public void setKeys(@Auth Account account, @Valid PreKeyStateV2 preKeys) {
|
||||||
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
boolean updateAccount = false;
|
||||||
|
|
||||||
|
if (!preKeys.getSignedPreKey().equals(device.getSignedPreKey())) {
|
||||||
|
device.setSignedPreKey(preKeys.getSignedPreKey());
|
||||||
|
updateAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preKeys.getIdentityKey().equals(account.getIdentityKey())) {
|
||||||
|
account.setIdentityKey(preKeys.getIdentityKey());
|
||||||
|
updateAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateAccount) {
|
||||||
|
accounts.update(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.store(account.getNumber(), device.getId(), preKeys.getPreKeys(), preKeys.getLastResortKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/{number}/{device_id}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Optional<PreKeyResponseV2> getDeviceKeys(@Auth Account account,
|
||||||
|
@PathParam("number") String number,
|
||||||
|
@PathParam("device_id") String deviceId,
|
||||||
|
@QueryParam("relay") Optional<String> relay)
|
||||||
|
throws RateLimitExceededException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (account.isRateLimited()) {
|
||||||
|
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay.isPresent()) {
|
||||||
|
return federatedClientManager.getClient(relay.get()).getKeysV2(number, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
TargetKeys targetKeys = getLocalKeys(number, deviceId);
|
||||||
|
Account destination = targetKeys.getDestination();
|
||||||
|
List<PreKeyResponseItemV2> devices = new LinkedList<>();
|
||||||
|
|
||||||
|
for (Device device : destination.getDevices()) {
|
||||||
|
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
|
||||||
|
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||||
|
PreKeyV2 preKey = null;
|
||||||
|
|
||||||
|
if (targetKeys.getKeys().isPresent()) {
|
||||||
|
for (KeyRecord keyRecord : targetKeys.getKeys().get()) {
|
||||||
|
if (keyRecord.getDeviceId() == device.getId()) {
|
||||||
|
preKey = new PreKeyV2(keyRecord.getKeyId(), keyRecord.getPublicKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signedPreKey != null || preKey != null) {
|
||||||
|
devices.add(new PreKeyResponseItemV2(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devices.isEmpty()) return Optional.absent();
|
||||||
|
else return Optional.of(new PreKeyResponseV2(destination.getIdentityKey(), devices));
|
||||||
|
} catch (NoSuchPeerException | NoSuchUserException e) {
|
||||||
|
throw new WebApplicationException(Response.status(404).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Path("/signed")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public void setSignedKey(@Auth Account account, @Valid SignedPreKey signedPreKey) {
|
||||||
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
device.setSignedPreKey(signedPreKey);
|
||||||
|
accounts.update(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Path("/signed")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Optional<SignedPreKey> getSignedKey(@Auth Account account) {
|
||||||
|
Device device = account.getAuthenticatedDevice().get();
|
||||||
|
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||||
|
|
||||||
|
if (signedPreKey != null) return Optional.of(signedPreKey);
|
||||||
|
else return Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,298 +16,293 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.codahale.metrics.annotation.Timed;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.yammer.dropwizard.auth.AuthenticationException;
|
|
||||||
import com.yammer.dropwizard.auth.basic.BasicCredentials;
|
|
||||||
import com.yammer.metrics.Metrics;
|
|
||||||
import com.yammer.metrics.core.Meter;
|
|
||||||
import com.yammer.metrics.core.Timer;
|
|
||||||
import com.yammer.metrics.core.TimerContext;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
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.SendMessageResponse;
|
||||||
|
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;
|
||||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
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.PushSender;
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
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.util.Base64;
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
import org.whispersystems.textsecuregcm.util.IterablePair;
|
|
||||||
import org.whispersystems.textsecuregcm.util.IterablePair.Pair;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
import javax.servlet.AsyncContext;
|
import javax.validation.Valid;
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.ws.rs.POST;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.ws.rs.PUT;
|
||||||
import java.io.BufferedReader;
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class MessageController extends HttpServlet {
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
public static final String PATH = "/v1/messages/";
|
@Path("/v1/messages")
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
private final Meter successMeter = Metrics.newMeter(MessageController.class, "deliver_message", "success", TimeUnit.MINUTES);
|
|
||||||
private final Meter failureMeter = Metrics.newMeter(MessageController.class, "deliver_message", "failure", TimeUnit.MINUTES);
|
|
||||||
private final Timer timer = Metrics.newTimer(MessageController.class, "deliver_message_time", TimeUnit.MILLISECONDS, TimeUnit.MINUTES);
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||||
|
|
||||||
private final RateLimiters rateLimiters;
|
private final RateLimiters rateLimiters;
|
||||||
private final AccountAuthenticator accountAuthenticator;
|
|
||||||
private final PushSender pushSender;
|
private final PushSender pushSender;
|
||||||
private final FederatedClientManager federatedClientManager;
|
private final FederatedClientManager federatedClientManager;
|
||||||
private final ObjectMapper objectMapper;
|
private final AccountsManager accountsManager;
|
||||||
private final ExecutorService executor;
|
|
||||||
|
|
||||||
public MessageController(RateLimiters rateLimiters,
|
public MessageController(RateLimiters rateLimiters,
|
||||||
AccountAuthenticator accountAuthenticator,
|
|
||||||
PushSender pushSender,
|
PushSender pushSender,
|
||||||
|
AccountsManager accountsManager,
|
||||||
FederatedClientManager federatedClientManager)
|
FederatedClientManager federatedClientManager)
|
||||||
{
|
{
|
||||||
this.rateLimiters = rateLimiters;
|
this.rateLimiters = rateLimiters;
|
||||||
this.accountAuthenticator = accountAuthenticator;
|
|
||||||
this.pushSender = pushSender;
|
this.pushSender = pushSender;
|
||||||
|
this.accountsManager = accountsManager;
|
||||||
this.federatedClientManager = federatedClientManager;
|
this.federatedClientManager = federatedClientManager;
|
||||||
this.objectMapper = new ObjectMapper();
|
|
||||||
this.executor = Executors.newFixedThreadPool(10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Timed
|
||||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
@Path("/{destination}")
|
||||||
TimerContext timerContext = timer.time();
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
try {
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
Account sender = authenticate(req);
|
public SendMessageResponse sendMessage(@Auth Account source,
|
||||||
IncomingMessageList messages = parseIncomingMessages(req);
|
@PathParam("destination") String destinationName,
|
||||||
|
@Valid IncomingMessageList messages)
|
||||||
rateLimiters.getMessagesLimiter().validate(sender.getNumber());
|
throws IOException, RateLimitExceededException
|
||||||
|
|
||||||
List<IncomingMessage> incomingMessages = messages.getMessages();
|
|
||||||
List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(),
|
|
||||||
incomingMessages);
|
|
||||||
|
|
||||||
IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages,
|
|
||||||
outgoingMessages);
|
|
||||||
|
|
||||||
handleAsyncDelivery(timerContext, req.startAsync(), listPair);
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
failureMeter.mark();
|
|
||||||
timerContext.stop();
|
|
||||||
resp.setStatus(401);
|
|
||||||
} catch (ValidationException e) {
|
|
||||||
failureMeter.mark();
|
|
||||||
timerContext.stop();
|
|
||||||
resp.setStatus(415);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("IOE", e);
|
|
||||||
failureMeter.mark();
|
|
||||||
timerContext.stop();
|
|
||||||
resp.setStatus(501);
|
|
||||||
} catch (RateLimitExceededException e) {
|
|
||||||
timerContext.stop();
|
|
||||||
failureMeter.mark();
|
|
||||||
resp.setStatus(413);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAsyncDelivery(final TimerContext timerContext,
|
|
||||||
final AsyncContext context,
|
|
||||||
final IterablePair<IncomingMessage, OutgoingMessageSignal> listPair)
|
|
||||||
{
|
{
|
||||||
executor.submit(new Runnable() {
|
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
List<String> success = new LinkedList<>();
|
|
||||||
List<String> failure = new LinkedList<>();
|
|
||||||
HttpServletResponse response = (HttpServletResponse) context.getResponse();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
|
boolean isSyncMessage = source.getNumber().equals(destinationName);
|
||||||
String destination = messagePair.first().getDestination();
|
|
||||||
String relay = messagePair.first().getRelay();
|
|
||||||
|
|
||||||
try {
|
if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages, isSyncMessage);
|
||||||
if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
|
else sendRelayMessage(source, destinationName, messages, isSyncMessage);
|
||||||
else sendRelayMessage(relay, destination, messagePair.second());
|
|
||||||
success.add(destination);
|
return new SendMessageResponse(!isSyncMessage && source.getActiveDeviceCount() > 1);
|
||||||
} catch (NoSuchUserException e) {
|
} catch (NoSuchUserException e) {
|
||||||
logger.debug("No such user", e);
|
throw new WebApplicationException(Response.status(404).build());
|
||||||
failure.add(destination);
|
} catch (MismatchedDevicesException e) {
|
||||||
|
throw new WebApplicationException(Response.status(409)
|
||||||
|
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||||
|
.entity(new MismatchedDevices(e.getMissingDevices(),
|
||||||
|
e.getExtraDevices()))
|
||||||
|
.build());
|
||||||
|
} catch (StaleDevicesException e) {
|
||||||
|
throw new WebApplicationException(Response.status(410)
|
||||||
|
.type(MediaType.APPLICATION_JSON)
|
||||||
|
.entity(new StaleDevices(e.getStaleDevices()))
|
||||||
|
.build());
|
||||||
|
} catch (InvalidDestinationException e) {
|
||||||
|
throw new WebApplicationException(Response.status(400).build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] responseData = serializeResponse(new MessageResponse(success, failure));
|
@Timed
|
||||||
response.setContentLength(responseData.length);
|
@POST
|
||||||
response.getOutputStream().write(responseData);
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
context.complete();
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
successMeter.mark();
|
public MessageResponse sendMessageLegacy(@Auth Account source, @Valid IncomingMessageList messages)
|
||||||
} catch (IOException e) {
|
throws IOException, RateLimitExceededException
|
||||||
logger.warn("Async Handler", e);
|
|
||||||
failureMeter.mark();
|
|
||||||
response.setStatus(501);
|
|
||||||
context.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
timerContext.stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendLocalMessage(String destination, OutgoingMessageSignal outgoingMessage)
|
|
||||||
throws IOException, NoSuchUserException
|
|
||||||
{
|
|
||||||
pushSender.sendMessage(destination, outgoingMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendRelayMessage(String relay, String destination, OutgoingMessageSignal outgoingMessage)
|
|
||||||
throws IOException, NoSuchUserException
|
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
FederatedClient client = federatedClientManager.getClient(relay);
|
List<IncomingMessage> incomingMessages = messages.getMessages();
|
||||||
client.sendMessage(destination, outgoingMessage);
|
validateLegacyDestinations(incomingMessages);
|
||||||
|
|
||||||
|
messages.setRelay(incomingMessages.get(0).getRelay());
|
||||||
|
sendMessage(source, incomingMessages.get(0).getDestination(), messages);
|
||||||
|
|
||||||
|
return new MessageResponse(new LinkedList<String>(), new LinkedList<String>());
|
||||||
|
} catch (ValidationException e) {
|
||||||
|
throw new WebApplicationException(Response.status(422).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendLocalMessage(Account source,
|
||||||
|
String destinationName,
|
||||||
|
IncomingMessageList messages,
|
||||||
|
boolean isSyncMessage)
|
||||||
|
throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException
|
||||||
|
{
|
||||||
|
Account destination;
|
||||||
|
|
||||||
|
if (!isSyncMessage) destination = getDestinationAccount(destinationName);
|
||||||
|
else destination = source;
|
||||||
|
|
||||||
|
validateCompleteDeviceList(destination, messages.getMessages(), isSyncMessage);
|
||||||
|
validateRegistrationIds(destination, messages.getMessages());
|
||||||
|
|
||||||
|
for (IncomingMessage incomingMessage : messages.getMessages()) {
|
||||||
|
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
|
||||||
|
|
||||||
|
if (destinationDevice.isPresent()) {
|
||||||
|
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendLocalMessage(Account source,
|
||||||
|
Account destinationAccount,
|
||||||
|
Device destinationDevice,
|
||||||
|
long timestamp,
|
||||||
|
IncomingMessage incomingMessage)
|
||||||
|
throws NoSuchUserException, IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
|
||||||
|
OutgoingMessageSignal.Builder messageBuilder = OutgoingMessageSignal.newBuilder();
|
||||||
|
|
||||||
|
messageBuilder.setType(incomingMessage.getType())
|
||||||
|
.setSource(source.getNumber())
|
||||||
|
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||||
|
.setSourceDevice((int)source.getAuthenticatedDevice().get().getId());
|
||||||
|
|
||||||
|
if (messageBody.isPresent()) {
|
||||||
|
messageBuilder.setMessage(ByteString.copyFrom(messageBody.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.getRelay().isPresent()) {
|
||||||
|
messageBuilder.setRelay(source.getRelay().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
pushSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build());
|
||||||
|
} catch (NotPushRegisteredException e) {
|
||||||
|
if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
|
||||||
|
else logger.debug("Not registered", e);
|
||||||
|
} catch (TransientPushFailureException e) {
|
||||||
|
if (destinationDevice.isMaster()) throw new IOException(e);
|
||||||
|
else logger.debug("Transient failure", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendRelayMessage(Account source,
|
||||||
|
String destinationName,
|
||||||
|
IncomingMessageList messages,
|
||||||
|
boolean isSyncMessage)
|
||||||
|
throws IOException, NoSuchUserException, InvalidDestinationException
|
||||||
|
{
|
||||||
|
if (isSyncMessage) throw new InvalidDestinationException("Transcript messages can't be relayed!");
|
||||||
|
|
||||||
|
try {
|
||||||
|
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
|
||||||
|
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
|
||||||
|
destinationName, messages);
|
||||||
} catch (NoSuchPeerException e) {
|
} catch (NoSuchPeerException e) {
|
||||||
logger.info("No such peer", e);
|
|
||||||
throw new NoSuchUserException(e);
|
throw new NoSuchUserException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<OutgoingMessageSignal> getOutgoingMessageSignals(String number,
|
private Account getDestinationAccount(String destination)
|
||||||
List<IncomingMessage> incomingMessages)
|
throws NoSuchUserException
|
||||||
{
|
{
|
||||||
List<OutgoingMessageSignal> outgoingMessages = new LinkedList<>();
|
Optional<Account> account = accountsManager.get(destination);
|
||||||
|
|
||||||
for (IncomingMessage incoming : incomingMessages) {
|
if (!account.isPresent() || !account.get().isActive()) {
|
||||||
OutgoingMessageSignal.Builder outgoingMessage = OutgoingMessageSignal.newBuilder();
|
throw new NoSuchUserException(destination);
|
||||||
outgoingMessage.setType(incoming.getType());
|
|
||||||
outgoingMessage.setSource(number);
|
|
||||||
|
|
||||||
byte[] messageBody = getMessageBody(incoming);
|
|
||||||
|
|
||||||
if (messageBody != null) {
|
|
||||||
outgoingMessage.setMessage(ByteString.copyFrom(messageBody));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outgoingMessage.setTimestamp(System.currentTimeMillis());
|
return account.get();
|
||||||
|
}
|
||||||
|
|
||||||
int index = 0;
|
private void validateRegistrationIds(Account account, List<IncomingMessage> messages)
|
||||||
|
throws StaleDevicesException
|
||||||
|
{
|
||||||
|
List<Long> staleDevices = new LinkedList<>();
|
||||||
|
|
||||||
for (IncomingMessage sub : incomingMessages) {
|
for (IncomingMessage message : messages) {
|
||||||
if (sub != incoming) {
|
Optional<Device> device = account.getDevice(message.getDestinationDeviceId());
|
||||||
outgoingMessage.setDestinations(index++, sub.getDestination());
|
|
||||||
|
if (device.isPresent() &&
|
||||||
|
message.getDestinationRegistrationId() > 0 &&
|
||||||
|
message.getDestinationRegistrationId() != device.get().getRegistrationId())
|
||||||
|
{
|
||||||
|
staleDevices.add(device.get().getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outgoingMessages.add(outgoingMessage.build());
|
if (!staleDevices.isEmpty()) {
|
||||||
|
throw new StaleDevicesException(staleDevices);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return outgoingMessages;
|
private void validateCompleteDeviceList(Account account,
|
||||||
|
List<IncomingMessage> messages,
|
||||||
|
boolean isSyncMessage)
|
||||||
|
throws MismatchedDevicesException
|
||||||
|
{
|
||||||
|
Set<Long> messageDeviceIds = new HashSet<>();
|
||||||
|
Set<Long> accountDeviceIds = new HashSet<>();
|
||||||
|
|
||||||
|
List<Long> missingDeviceIds = new LinkedList<>();
|
||||||
|
List<Long> extraDeviceIds = new LinkedList<>();
|
||||||
|
|
||||||
|
for (IncomingMessage message : messages) {
|
||||||
|
messageDeviceIds.add(message.getDestinationDeviceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getMessageBody(IncomingMessage message) {
|
for (Device device : account.getDevices()) {
|
||||||
|
if (device.isActive() &&
|
||||||
|
!(isSyncMessage && device.getId() == account.getAuthenticatedDevice().get().getId()))
|
||||||
|
{
|
||||||
|
accountDeviceIds.add(device.getId());
|
||||||
|
|
||||||
|
if (!messageDeviceIds.contains(device.getId())) {
|
||||||
|
missingDeviceIds.add(device.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IncomingMessage message : messages) {
|
||||||
|
if (!accountDeviceIds.contains(message.getDestinationDeviceId())) {
|
||||||
|
extraDeviceIds.add(message.getDestinationDeviceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!missingDeviceIds.isEmpty() || !extraDeviceIds.isEmpty()) {
|
||||||
|
throw new MismatchedDevicesException(missingDeviceIds, extraDeviceIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateLegacyDestinations(List<IncomingMessage> messages)
|
||||||
|
throws ValidationException
|
||||||
|
{
|
||||||
|
String destination = null;
|
||||||
|
|
||||||
|
for (IncomingMessage message : messages) {
|
||||||
|
if ((message.getDestination() == null) ||
|
||||||
|
(destination != null && !destination.equals(message.getDestination())))
|
||||||
|
{
|
||||||
|
throw new ValidationException("Multiple account destinations!");
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = message.getDestination();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<byte[]> getMessageBody(IncomingMessage message) {
|
||||||
try {
|
try {
|
||||||
return Base64.decode(message.getBody());
|
return Optional.of(Base64.decode(message.getBody()));
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
ioe.printStackTrace();
|
logger.debug("Bad B64", ioe);
|
||||||
return null;
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] serializeResponse(MessageResponse response) throws IOException {
|
|
||||||
try {
|
|
||||||
return objectMapper.writeValueAsBytes(response);
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IncomingMessageList parseIncomingMessages(HttpServletRequest request)
|
|
||||||
throws IOException, ValidationException
|
|
||||||
{
|
|
||||||
BufferedReader reader = request.getReader();
|
|
||||||
StringBuilder content = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
content.append(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
IncomingMessageList messages = objectMapper.readValue(content.toString(),
|
|
||||||
IncomingMessageList.class);
|
|
||||||
|
|
||||||
if (messages.getMessages() == null) {
|
|
||||||
throw new ValidationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (IncomingMessage message : messages.getMessages()) {
|
|
||||||
if (message.getBody() == null) throw new ValidationException();
|
|
||||||
if (message.getDestination() == null) throw new ValidationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Account authenticate(HttpServletRequest request) throws AuthenticationException {
|
|
||||||
try {
|
|
||||||
AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization"));
|
|
||||||
BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(),
|
|
||||||
authorizationHeader.getPassword() );
|
|
||||||
|
|
||||||
Optional<Account> account = accountAuthenticator.authenticate(credentials);
|
|
||||||
|
|
||||||
if (account.isPresent()) return account.get();
|
|
||||||
else throw new AuthenticationException("Bad credentials");
|
|
||||||
} catch (InvalidAuthorizationHeaderException e) {
|
|
||||||
throw new AuthenticationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// @Timed
|
|
||||||
// @POST
|
|
||||||
// @Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
// @Produces(MediaType.APPLICATION_JSON)
|
|
||||||
// public MessageResponse sendMessage(@Auth Account sender, IncomingMessageList messages)
|
|
||||||
// throws IOException
|
|
||||||
// {
|
|
||||||
// List<String> success = new LinkedList<>();
|
|
||||||
// List<String> failure = new LinkedList<>();
|
|
||||||
// List<IncomingMessage> incomingMessages = messages.getMessages();
|
|
||||||
// List<OutgoingMessageSignal> outgoingMessages = getOutgoingMessageSignals(sender.getNumber(), incomingMessages);
|
|
||||||
//
|
|
||||||
// IterablePair<IncomingMessage, OutgoingMessageSignal> listPair = new IterablePair<>(incomingMessages, outgoingMessages);
|
|
||||||
//
|
|
||||||
// for (Pair<IncomingMessage, OutgoingMessageSignal> messagePair : listPair) {
|
|
||||||
// String destination = messagePair.first().getDestination();
|
|
||||||
// String relay = messagePair.first().getRelay();
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second());
|
|
||||||
// else sendRelayMessage(relay, destination, messagePair.second());
|
|
||||||
// success.add(destination);
|
|
||||||
// } catch (NoSuchUserException e) {
|
|
||||||
// logger.debug("No such user", e);
|
|
||||||
// failure.add(destination);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return new MessageResponse(success, failure);
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MismatchedDevicesException extends Exception {
|
||||||
|
|
||||||
|
private final List<Long> missingDevices;
|
||||||
|
private final List<Long> extraDevices;
|
||||||
|
|
||||||
|
public MismatchedDevicesException(List<Long> missingDevices, List<Long> extraDevices) {
|
||||||
|
this.missingDevices = missingDevices;
|
||||||
|
this.extraDevices = extraDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getMissingDevices() {
|
||||||
|
return missingDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getExtraDevices() {
|
||||||
|
return extraDevices;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -27,7 +25,7 @@ public class NoSuchUserException extends Exception {
|
|||||||
|
|
||||||
public NoSuchUserException(String user) {
|
public NoSuchUserException(String user) {
|
||||||
super(user);
|
super(user);
|
||||||
missing = new LinkedList<String>();
|
missing = new LinkedList<>();
|
||||||
missing.add(user);
|
missing.add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.WebsocketSender;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v1/provisioning")
|
||||||
|
public class ProvisioningController {
|
||||||
|
|
||||||
|
private final RateLimiters rateLimiters;
|
||||||
|
private final WebsocketSender websocketSender;
|
||||||
|
|
||||||
|
public ProvisioningController(RateLimiters rateLimiters, PushSender pushSender) {
|
||||||
|
this.rateLimiters = rateLimiters;
|
||||||
|
this.websocketSender = pushSender.getWebSocketSender();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@Path("/{destination}")
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public void sendProvisioningMessage(@Auth Account source,
|
||||||
|
@PathParam("destination") String destinationName,
|
||||||
|
@Valid ProvisioningMessage message)
|
||||||
|
throws RateLimitExceededException, InvalidWebsocketAddressException, IOException
|
||||||
|
{
|
||||||
|
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||||
|
|
||||||
|
if (!websocketSender.sendProvisioningMessage(new ProvisioningAddress(destinationName),
|
||||||
|
Base64.decode(message.getBody())))
|
||||||
|
{
|
||||||
|
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||||
|
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
|
||||||
|
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
||||||
|
|
||||||
|
@Path("/v1/receipt")
|
||||||
|
public class ReceiptController {
|
||||||
|
|
||||||
|
private final AccountsManager accountManager;
|
||||||
|
private final PushSender pushSender;
|
||||||
|
private final FederatedClientManager federatedClientManager;
|
||||||
|
|
||||||
|
public ReceiptController(AccountsManager accountManager,
|
||||||
|
FederatedClientManager federatedClientManager,
|
||||||
|
PushSender pushSender)
|
||||||
|
{
|
||||||
|
this.accountManager = accountManager;
|
||||||
|
this.federatedClientManager = federatedClientManager;
|
||||||
|
this.pushSender = pushSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Path("/{destination}/{messageId}")
|
||||||
|
public void sendDeliveryReceipt(@Auth Account source,
|
||||||
|
@PathParam("destination") String destination,
|
||||||
|
@PathParam("messageId") long messageId,
|
||||||
|
@QueryParam("relay") Optional<String> relay)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (relay.isPresent()) sendRelayedReceipt(source, destination, messageId, relay.get());
|
||||||
|
else sendDirectReceipt(source, destination, messageId);
|
||||||
|
} catch (NoSuchUserException | NotPushRegisteredException e) {
|
||||||
|
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||||
|
} catch (TransientPushFailureException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
|
||||||
|
throws NoSuchUserException, IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
federatedClientManager.getClient(relay)
|
||||||
|
.sendDeliveryReceipt(source.getNumber(),
|
||||||
|
source.getAuthenticatedDevice().get().getId(),
|
||||||
|
destination, messageId);
|
||||||
|
} catch (NoSuchPeerException e) {
|
||||||
|
throw new NoSuchUserException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendDirectReceipt(Account source, String destination, long messageId)
|
||||||
|
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
|
||||||
|
{
|
||||||
|
Account destinationAccount = getDestinationAccount(destination);
|
||||||
|
Set<Device> destinationDevices = destinationAccount.getDevices();
|
||||||
|
|
||||||
|
OutgoingMessageSignal.Builder message =
|
||||||
|
OutgoingMessageSignal.newBuilder()
|
||||||
|
.setSource(source.getNumber())
|
||||||
|
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId())
|
||||||
|
.setTimestamp(messageId)
|
||||||
|
.setType(OutgoingMessageSignal.Type.RECEIPT_VALUE);
|
||||||
|
|
||||||
|
if (source.getRelay().isPresent()) {
|
||||||
|
message.setRelay(source.getRelay().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Device destinationDevice : destinationDevices) {
|
||||||
|
pushSender.sendMessage(destinationAccount, destinationDevice, message.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Account getDestinationAccount(String destination)
|
||||||
|
throws NoSuchUserException
|
||||||
|
{
|
||||||
|
Optional<Account> account = accountManager.get(destination);
|
||||||
|
|
||||||
|
if (!account.isPresent()) {
|
||||||
|
throw new NoSuchUserException(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
public class StaleDevicesException extends Throwable {
|
||||||
|
private final List<Long> staleDevices;
|
||||||
|
|
||||||
|
public StaleDevicesException(List<Long> staleDevices) {
|
||||||
|
this.staleDevices = staleDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getStaleDevices() {
|
||||||
|
return staleDevices;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,4 +18,7 @@ package org.whispersystems.textsecuregcm.controllers;
|
|||||||
|
|
||||||
|
|
||||||
public class ValidationException extends Exception {
|
public class ValidationException extends Exception {
|
||||||
|
public ValidationException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,19 @@ public class AccountAttributes {
|
|||||||
@JsonProperty
|
@JsonProperty
|
||||||
private boolean supportsSms;
|
private boolean supportsSms;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean fetchesMessages;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int registrationId;
|
||||||
|
|
||||||
public AccountAttributes() {}
|
public AccountAttributes() {}
|
||||||
|
|
||||||
public AccountAttributes(String signalingKey, boolean supportsSms) {
|
public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages, int registrationId) {
|
||||||
this.signalingKey = signalingKey;
|
this.signalingKey = signalingKey;
|
||||||
this.supportsSms = supportsSms;
|
this.supportsSms = supportsSms;
|
||||||
|
this.fetchesMessages = fetchesMessages;
|
||||||
|
this.registrationId = registrationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSignalingKey() {
|
public String getSignalingKey() {
|
||||||
@@ -43,4 +51,11 @@ public class AccountAttributes {
|
|||||||
return supportsSms;
|
return supportsSms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getFetchesMessages() {
|
||||||
|
return fetchesMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRegistrationId() {
|
||||||
|
return registrationId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class AcknowledgeWebsocketMessage extends IncomingWebsocketMessage {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
public AcknowledgeWebsocketMessage() {}
|
||||||
|
|
||||||
|
public AcknowledgeWebsocketMessage(long id) {
|
||||||
|
this.type = TYPE_ACKNOWLEDGE_MESSAGE;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
|
||||||
|
public class ApnMessage {
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String apnId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String number;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Min(1)
|
||||||
|
private int deviceId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public ApnMessage() {}
|
||||||
|
|
||||||
|
public ApnMessage(String apnId, String number, int deviceId, String message) {
|
||||||
|
this.apnId = apnId;
|
||||||
|
this.number = number;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,15 +18,10 @@ package org.whispersystems.textsecuregcm.entities;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonValue;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import com.google.gson.Gson;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Base64;
|
|
||||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||||
|
|
||||||
import javax.xml.bind.annotation.XmlRootElement;
|
|
||||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||||
@@ -73,9 +68,9 @@ public class ClientContact {
|
|||||||
this.inactive = inactive;
|
this.inactive = inactive;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
// public String toString() {
|
||||||
return new Gson().toJson(this);
|
// return new Gson().toJson(this);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object other) {
|
public boolean equals(Object other) {
|
||||||
|
|||||||
@@ -30,4 +30,11 @@ public class ClientContactTokens {
|
|||||||
public List<String> getContacts() {
|
public List<String> getContacts() {
|
||||||
return contacts;
|
return contacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientContactTokens() {}
|
||||||
|
|
||||||
|
public ClientContactTokens(List<String> contacts) {
|
||||||
|
this.contacts = contacts;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
public class CryptoEncodingException extends Exception {
|
||||||
|
|
||||||
|
public CryptoEncodingException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CryptoEncodingException(Exception e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
public class DeviceResponse {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long deviceId;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public DeviceResponse() {}
|
||||||
|
|
||||||
|
public DeviceResponse(long deviceId) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,27 +41,31 @@ public class EncryptedOutgoingMessage {
|
|||||||
private static final int MAC_KEY_SIZE = 20;
|
private static final int MAC_KEY_SIZE = 20;
|
||||||
private static final int MAC_SIZE = 10;
|
private static final int MAC_SIZE = 10;
|
||||||
|
|
||||||
private final OutgoingMessageSignal outgoingMessage;
|
private final byte[] serialized;
|
||||||
private final String signalingKey;
|
private final String serializedAndEncoded;
|
||||||
|
|
||||||
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
|
public EncryptedOutgoingMessage(OutgoingMessageSignal outgoingMessage,
|
||||||
String signalingKey)
|
String signalingKey)
|
||||||
|
throws CryptoEncodingException
|
||||||
{
|
{
|
||||||
this.outgoingMessage = outgoingMessage;
|
|
||||||
this.signalingKey = signalingKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String serialize() throws IOException {
|
|
||||||
byte[] plaintext = outgoingMessage.toByteArray();
|
byte[] plaintext = outgoingMessage.toByteArray();
|
||||||
SecretKeySpec cipherKey = getCipherKey (signalingKey);
|
SecretKeySpec cipherKey = getCipherKey (signalingKey);
|
||||||
SecretKeySpec macKey = getMacKey(signalingKey);
|
SecretKeySpec macKey = getMacKey(signalingKey);
|
||||||
byte[] ciphertext = getCiphertext(plaintext, cipherKey, macKey);
|
|
||||||
|
|
||||||
return Base64.encodeBytes(ciphertext);
|
this.serialized = getCiphertext(plaintext, cipherKey, macKey);
|
||||||
|
this.serializedAndEncoded = Base64.encodeBytes(this.serialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toEncodedString() {
|
||||||
|
return serializedAndEncoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toByteArray() {
|
||||||
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
|
private byte[] getCiphertext(byte[] plaintext, SecretKeySpec cipherKey, SecretKeySpec macKey)
|
||||||
throws IOException
|
throws CryptoEncodingException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
@@ -85,31 +89,39 @@ public class EncryptedOutgoingMessage {
|
|||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
logger.warn("Invalid Key", e);
|
logger.warn("Invalid Key", e);
|
||||||
throw new IOException("Invalid key!");
|
throw new CryptoEncodingException("Invalid key!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecretKeySpec getCipherKey(String signalingKey) throws IOException {
|
private SecretKeySpec getCipherKey(String signalingKey) throws CryptoEncodingException {
|
||||||
|
try {
|
||||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||||
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
byte[] cipherKey = new byte[CIPHER_KEY_SIZE];
|
||||||
|
|
||||||
if (signalingKeyBytes.length < CIPHER_KEY_SIZE)
|
if (signalingKeyBytes.length < CIPHER_KEY_SIZE)
|
||||||
throw new IOException("Signaling key too short!");
|
throw new CryptoEncodingException("Signaling key too short!");
|
||||||
|
|
||||||
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
System.arraycopy(signalingKeyBytes, 0, cipherKey, 0, cipherKey.length);
|
||||||
return new SecretKeySpec(cipherKey, "AES");
|
return new SecretKeySpec(cipherKey, "AES");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CryptoEncodingException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecretKeySpec getMacKey(String signalingKey) throws IOException {
|
private SecretKeySpec getMacKey(String signalingKey) throws CryptoEncodingException {
|
||||||
|
try {
|
||||||
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
byte[] signalingKeyBytes = Base64.decode(signalingKey);
|
||||||
byte[] macKey = new byte[MAC_KEY_SIZE];
|
byte[] macKey = new byte[MAC_KEY_SIZE];
|
||||||
|
|
||||||
if (signalingKeyBytes.length < CIPHER_KEY_SIZE + MAC_KEY_SIZE)
|
if (signalingKeyBytes.length < CIPHER_KEY_SIZE + MAC_KEY_SIZE)
|
||||||
throw new IOException(("Signaling key too short!"));
|
throw new CryptoEncodingException("Signaling key too short!");
|
||||||
|
|
||||||
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
System.arraycopy(signalingKeyBytes, CIPHER_KEY_SIZE, macKey, 0, macKey.length);
|
||||||
|
|
||||||
return new SecretKeySpec(macKey, "HmacSHA256");
|
return new SecretKeySpec(macKey, "HmacSHA256");
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CryptoEncodingException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
|
||||||
|
public class GcmMessage {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String gcmId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String number;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Min(1)
|
||||||
|
private int deviceId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean receipt;
|
||||||
|
|
||||||
|
public GcmMessage() {}
|
||||||
|
|
||||||
|
public GcmMessage(String gcmId, String number, int deviceId, String message, boolean receipt) {
|
||||||
|
this.gcmId = gcmId;
|
||||||
|
this.number = number;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.message = message;
|
||||||
|
this.receipt = receipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,9 +25,14 @@ public class IncomingMessage {
|
|||||||
private int type;
|
private int type;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotEmpty
|
|
||||||
private String destination;
|
private String destination;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long destinationDeviceId = 1;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int destinationRegistrationId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String body;
|
private String body;
|
||||||
@@ -36,7 +41,8 @@ public class IncomingMessage {
|
|||||||
private String relay;
|
private String relay;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private long timestamp;
|
private long timestamp; // deprecated
|
||||||
|
|
||||||
|
|
||||||
public String getDestination() {
|
public String getDestination() {
|
||||||
return destination;
|
return destination;
|
||||||
@@ -53,4 +59,12 @@ public class IncomingMessage {
|
|||||||
public String getRelay() {
|
public String getRelay() {
|
||||||
return relay;
|
return relay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDestinationDeviceId() {
|
||||||
|
return destinationDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDestinationRegistrationId() {
|
||||||
|
return destinationRegistrationId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,27 @@ public class IncomingMessageList {
|
|||||||
@Valid
|
@Valid
|
||||||
private List<IncomingMessage> messages;
|
private List<IncomingMessage> messages;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String relay;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
public IncomingMessageList() {}
|
public IncomingMessageList() {}
|
||||||
|
|
||||||
public List<IncomingMessage> getMessages() {
|
public List<IncomingMessage> getMessages() {
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRelay() {
|
||||||
|
return relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRelay(String relay) {
|
||||||
|
this.relay = relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class IncomingWebsocketMessage {
|
||||||
|
|
||||||
|
public static final int TYPE_ACKNOWLEDGE_MESSAGE = 1;
|
||||||
|
public static final int TYPE_PING_MESSAGE = 2;
|
||||||
|
public static final int TYPE_PONG_MESSAGE = 3;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
protected int type;
|
||||||
|
|
||||||
|
public IncomingWebsocketMessage() {}
|
||||||
|
|
||||||
|
public IncomingWebsocketMessage(int type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -16,15 +16,26 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.entities;
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class MessageResponse {
|
public class MessageResponse {
|
||||||
private List<String> success;
|
private List<String> success;
|
||||||
private List<String> failure;
|
private List<String> failure;
|
||||||
|
private Set<String> missingDeviceIds;
|
||||||
|
|
||||||
public MessageResponse(List<String> success, List<String> failure) {
|
public MessageResponse(List<String> success, List<String> failure) {
|
||||||
this.success = success;
|
this.success = success;
|
||||||
this.failure = failure;
|
this.failure = failure;
|
||||||
|
this.missingDeviceIds = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageResponse(Set<String> missingDeviceIds) {
|
||||||
|
this.success = new LinkedList<>();
|
||||||
|
this.failure = new LinkedList<>(missingDeviceIds);
|
||||||
|
this.missingDeviceIds = missingDeviceIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageResponse() {}
|
public MessageResponse() {}
|
||||||
@@ -33,8 +44,23 @@ public class MessageResponse {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSuccess(List<String> success) {
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getFailure() {
|
public List<String> getFailure() {
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFailure(List<String> failure) {
|
||||||
|
this.failure = failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getNumbersMissingDevices() {
|
||||||
|
return missingDeviceIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNumbersMissingDevices(Set<String> numbersMissingDevices) {
|
||||||
|
this.missingDeviceIds = numbersMissingDevices;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MismatchedDevices {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
public List<Long> missingDevices;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
public List<Long> extraDevices;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public MismatchedDevices() {}
|
||||||
|
|
||||||
|
public MismatchedDevices(List<Long> missingDevices, List<Long> extraDevices) {
|
||||||
|
this.missingDevices = missingDevices;
|
||||||
|
this.extraDevices = extraDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
public interface PreKeyBase {
|
||||||
|
|
||||||
|
public long getKeyId();
|
||||||
|
public String getPublicKey();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class PreKeyCount {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
public PreKeyCount(int count) {
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKeyCount() {}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
public class PreKeyResponseItemV2 {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long deviceId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int registrationId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private SignedPreKey signedPreKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private PreKeyV2 preKey;
|
||||||
|
|
||||||
|
public PreKeyResponseItemV2() {}
|
||||||
|
|
||||||
|
public PreKeyResponseItemV2(long deviceId, int registrationId, SignedPreKey signedPreKey, PreKeyV2 preKey) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.registrationId = registrationId;
|
||||||
|
this.signedPreKey = signedPreKey;
|
||||||
|
this.preKey = preKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public SignedPreKey getSignedPreKey() {
|
||||||
|
return signedPreKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public PreKeyV2 getPreKey() {
|
||||||
|
return preKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public int getRegistrationId() {
|
||||||
|
return registrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public long getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open WhisperSystems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PreKeyResponseV1 {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
private List<PreKeyV1> keys;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public PreKeyResponseV1() {}
|
||||||
|
|
||||||
|
public PreKeyResponseV1(PreKeyV1 preKey) {
|
||||||
|
this.keys = new LinkedList<>();
|
||||||
|
this.keys.add(preKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKeyResponseV1(List<PreKeyV1> preKeys) {
|
||||||
|
this.keys = preKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PreKeyV1> getKeys() {
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof PreKeyResponseV1) ||
|
||||||
|
((PreKeyResponseV1) o).keys.size() != keys.size())
|
||||||
|
return false;
|
||||||
|
Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
|
||||||
|
for (PreKeyV1 key : keys) {
|
||||||
|
if (!otherKeys.next().equals(key))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int hashCode() {
|
||||||
|
int ret = 0xFBA4C795 * keys.size();
|
||||||
|
for (PreKeyV1 key : keys)
|
||||||
|
ret ^= key.getPublicKey().hashCode();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PreKeyResponseV2 {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String identityKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<PreKeyResponseItemV2> devices;
|
||||||
|
|
||||||
|
public PreKeyResponseV2() {}
|
||||||
|
|
||||||
|
public PreKeyResponseV2(String identityKey, List<PreKeyResponseItemV2> devices) {
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
this.devices = devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public String getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@JsonIgnore
|
||||||
|
public PreKeyResponseItemV2 getDevice(int deviceId) {
|
||||||
|
for (PreKeyResponseItemV2 device : devices) {
|
||||||
|
if (device.getDeviceId() == deviceId) return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@JsonIgnore
|
||||||
|
public int getDevicesCount() {
|
||||||
|
return devices.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
||||||
@@ -17,28 +17,39 @@
|
|||||||
package org.whispersystems.textsecuregcm.entities;
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PreKeyList {
|
public class PreKeyStateV1 {
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
@NotNull
|
|
||||||
private PreKey lastResortKey;
|
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
private List<PreKey> keys;
|
private PreKeyV1 lastResortKey;
|
||||||
|
|
||||||
public List<PreKey> getKeys() {
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
private List<PreKeyV1> keys;
|
||||||
|
|
||||||
|
public List<PreKeyV1> getKeys() {
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreKey getLastResortKey() {
|
@VisibleForTesting
|
||||||
|
public void setKeys(List<PreKeyV1> keys) {
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKeyV1 getLastResortKey() {
|
||||||
return lastResortKey;
|
return lastResortKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public void setLastResortKey(PreKeyV1 lastResortKey) {
|
||||||
|
this.lastResortKey = lastResortKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PreKeyStateV2 {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
private List<PreKeyV2> preKeys;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
private SignedPreKey signedPreKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
private PreKeyV2 lastResortKey;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String identityKey;
|
||||||
|
|
||||||
|
public PreKeyStateV2() {}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public PreKeyStateV2(String identityKey, SignedPreKey signedPreKey,
|
||||||
|
List<PreKeyV2> keys, PreKeyV2 lastResortKey)
|
||||||
|
{
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
this.signedPreKey = signedPreKey;
|
||||||
|
this.preKeys = keys;
|
||||||
|
this.lastResortKey = lastResortKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PreKeyV2> getPreKeys() {
|
||||||
|
return preKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignedPreKey getSignedPreKey() {
|
||||||
|
return signedPreKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKeyV2 getLastResortKey() {
|
||||||
|
return lastResortKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
||||||
@@ -17,22 +17,17 @@
|
|||||||
package org.whispersystems.textsecuregcm.entities;
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import javax.xml.bind.annotation.XmlTransient;
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||||
public class PreKey {
|
public class PreKeyV1 implements PreKeyBase {
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonProperty
|
||||||
private long id;
|
private long deviceId;
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private String number;
|
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -47,70 +42,55 @@ public class PreKey {
|
|||||||
private String identityKey;
|
private String identityKey;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private boolean lastResort;
|
private int registrationId;
|
||||||
|
|
||||||
public PreKey() {}
|
public PreKeyV1() {}
|
||||||
|
|
||||||
public PreKey(long id, String number, long keyId,
|
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
|
||||||
String publicKey, String identityKey,
|
|
||||||
boolean lastResort)
|
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.deviceId = deviceId;
|
||||||
this.number = number;
|
|
||||||
this.keyId = keyId;
|
this.keyId = keyId;
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
this.identityKey = identityKey;
|
this.identityKey = identityKey;
|
||||||
this.lastResort = lastResort;
|
this.registrationId = registrationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@XmlTransient
|
@VisibleForTesting
|
||||||
public long getId() {
|
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey)
|
||||||
return id;
|
{
|
||||||
}
|
this.deviceId = deviceId;
|
||||||
|
this.keyId = keyId;
|
||||||
public void setId(long id) {
|
this.publicKey = publicKey;
|
||||||
this.id = id;
|
this.identityKey = identityKey;
|
||||||
}
|
|
||||||
|
|
||||||
@XmlTransient
|
|
||||||
public String getNumber() {
|
|
||||||
return number;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNumber(String number) {
|
|
||||||
this.number = number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getPublicKey() {
|
public String getPublicKey() {
|
||||||
return publicKey;
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPublicKey(String publicKey) {
|
@Override
|
||||||
this.publicKey = publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getKeyId() {
|
public long getKeyId() {
|
||||||
return keyId;
|
return keyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setKeyId(long keyId) {
|
|
||||||
this.keyId = keyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getIdentityKey() {
|
public String getIdentityKey() {
|
||||||
return identityKey;
|
return identityKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIdentityKey(String identityKey) {
|
public void setDeviceId(long deviceId) {
|
||||||
this.identityKey = identityKey;
|
this.deviceId = deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@XmlTransient
|
public long getDeviceId() {
|
||||||
public boolean isLastResort() {
|
return deviceId;
|
||||||
return lastResort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLastResort(boolean lastResort) {
|
public int getRegistrationId() {
|
||||||
this.lastResort = lastResort;
|
return registrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegistrationId(int registrationId) {
|
||||||
|
this.registrationId = registrationId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Open Whisper Systems
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public class PreKeyV2 implements PreKeyBase {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private long keyId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String publicKey;
|
||||||
|
|
||||||
|
public PreKeyV2() {}
|
||||||
|
|
||||||
|
public PreKeyV2(long keyId, String publicKey)
|
||||||
|
{
|
||||||
|
this.keyId = keyId;
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPublicKey() {
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicKey(String publicKey) {
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getKeyId() {
|
||||||
|
return keyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyId(long keyId) {
|
||||||
|
this.keyId = keyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object object) {
|
||||||
|
if (object == null || !(object instanceof PreKeyV2)) return false;
|
||||||
|
PreKeyV2 that = (PreKeyV2)object;
|
||||||
|
|
||||||
|
if (publicKey == null) {
|
||||||
|
return this.keyId == that.keyId && that.publicKey == null;
|
||||||
|
} else {
|
||||||
|
return this.keyId == that.keyId && this.publicKey.equals(that.publicKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
if (publicKey == null) {
|
||||||
|
return (int)this.keyId;
|
||||||
|
} else {
|
||||||
|
return ((int)this.keyId) ^ publicKey.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,10 @@ public class RelayMessage {
|
|||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String destination;
|
private String destination;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private long destinationDeviceId;
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||||
@@ -40,8 +44,9 @@ public class RelayMessage {
|
|||||||
|
|
||||||
public RelayMessage() {}
|
public RelayMessage() {}
|
||||||
|
|
||||||
public RelayMessage(String destination, byte[] outgoingMessageSignal) {
|
public RelayMessage(String destination, long destinationDeviceId, byte[] outgoingMessageSignal) {
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
|
this.destinationDeviceId = destinationDeviceId;
|
||||||
this.outgoingMessageSignal = outgoingMessageSignal;
|
this.outgoingMessageSignal = outgoingMessageSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +54,10 @@ public class RelayMessage {
|
|||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getDestinationDeviceId() {
|
||||||
|
return destinationDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getOutgoingMessageSignal() {
|
public byte[] getOutgoingMessageSignal() {
|
||||||
return outgoingMessageSignal;
|
return outgoingMessageSignal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class SignedPreKey extends PreKeyV2 {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String signature;
|
||||||
|
|
||||||
|
public SignedPreKey() {}
|
||||||
|
|
||||||
|
public SignedPreKey(long keyId, String publicKey, String signature) {
|
||||||
|
super(keyId, publicKey);
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSignature() {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object object) {
|
||||||
|
if (object == null || !(object instanceof SignedPreKey)) return false;
|
||||||
|
SignedPreKey that = (SignedPreKey) object;
|
||||||
|
|
||||||
|
if (signature == null) {
|
||||||
|
return super.equals(object) && that.signature == null;
|
||||||
|
} else {
|
||||||
|
return super.equals(object) && this.signature.equals(that.signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
if (signature == null) {
|
||||||
|
return super.hashCode();
|
||||||
|
} else {
|
||||||
|
return super.hashCode() ^ signature.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StaleDevices {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Long> staleDevices;
|
||||||
|
|
||||||
|
public StaleDevices() {}
|
||||||
|
|
||||||
|
public StaleDevices(List<Long> staleDevices) {
|
||||||
|
this.staleDevices = staleDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
|
||||||
|
public class UnregisteredEvent {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String registrationId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String number;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Min(1)
|
||||||
|
private int deviceId;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long timestamp;
|
||||||
|
|
||||||
|
public String getRegistrationId() {
|
||||||
|
return registrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNumber() {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UnregisteredEventList {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<UnregisteredEvent> devices;
|
||||||
|
|
||||||
|
public List<UnregisteredEvent> getDevices() {
|
||||||
|
if (devices == null) return new LinkedList<>();
|
||||||
|
else return devices;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
package org.whispersystems.textsecuregcm.federation;
|
package org.whispersystems.textsecuregcm.federation;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
import com.sun.jersey.api.client.Client;
|
import com.sun.jersey.api.client.Client;
|
||||||
import com.sun.jersey.api.client.ClientHandlerException;
|
import com.sun.jersey.api.client.ClientHandlerException;
|
||||||
import com.sun.jersey.api.client.ClientResponse;
|
import com.sun.jersey.api.client.ClientResponse;
|
||||||
@@ -30,19 +31,20 @@ import org.apache.http.conn.ssl.StrictHostnameVerifier;
|
|||||||
import org.bouncycastle.openssl.PEMReader;
|
import org.bouncycastle.openssl.PEMReader;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
import org.whispersystems.textsecuregcm.entities.AccountCount;
|
||||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
|
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||||
import org.whispersystems.textsecuregcm.entities.RelayMessage;
|
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||||
import org.whispersystems.textsecuregcm.util.Base64;
|
import org.whispersystems.textsecuregcm.util.Base64;
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -55,6 +57,7 @@ import java.security.SecureRandom;
|
|||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class FederatedClient {
|
public class FederatedClient {
|
||||||
|
|
||||||
@@ -62,9 +65,11 @@ public class FederatedClient {
|
|||||||
|
|
||||||
private static final String USER_COUNT_PATH = "/v1/federation/user_count";
|
private static final String USER_COUNT_PATH = "/v1/federation/user_count";
|
||||||
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
|
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
|
||||||
private static final String RELAY_MESSAGE_PATH = "/v1/federation/message";
|
private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
|
||||||
private static final String PREKEY_PATH = "/v1/federation/key/%s";
|
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
|
||||||
|
private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
|
||||||
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
|
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
|
||||||
|
private static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
|
||||||
|
|
||||||
private final FederatedPeer peer;
|
private final FederatedPeer peer;
|
||||||
private final Client client;
|
private final Client client;
|
||||||
@@ -89,28 +94,63 @@ public class FederatedClient {
|
|||||||
WebResource resource = client.resource(peer.getUrl())
|
WebResource resource = client.resource(peer.getUrl())
|
||||||
.path(String.format(ATTACHMENT_URI_PATH, attachmentId));
|
.path(String.format(ATTACHMENT_URI_PATH, attachmentId));
|
||||||
|
|
||||||
return resource.accept(MediaType.APPLICATION_JSON)
|
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||||
.header("Authorization", authorizationHeader)
|
.header("Authorization", authorizationHeader)
|
||||||
.get(AttachmentUri.class)
|
.get(ClientResponse.class);
|
||||||
.getLocation();
|
|
||||||
|
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||||
|
throw new WebApplicationException(clientResponseToResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.getEntity(AttachmentUri.class).getLocation();
|
||||||
|
|
||||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||||
logger.warn("Bad URI", e);
|
logger.warn("Bad URI", e);
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreKey getKey(String destination) {
|
public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
|
||||||
try {
|
try {
|
||||||
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination));
|
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device));
|
||||||
return resource.accept(MediaType.APPLICATION_JSON)
|
|
||||||
|
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||||
.header("Authorization", authorizationHeader)
|
.header("Authorization", authorizationHeader)
|
||||||
.get(PreKey.class);
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
|
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||||
|
throw new WebApplicationException(clientResponseToResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(response.getEntity(PreKeyResponseV1.class));
|
||||||
|
|
||||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||||
logger.warn("PreKey", e);
|
logger.warn("PreKey", e);
|
||||||
return null;
|
return Optional.absent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
|
||||||
|
try {
|
||||||
|
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device));
|
||||||
|
|
||||||
|
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.header("Authorization", authorizationHeader)
|
||||||
|
.get(ClientResponse.class);
|
||||||
|
|
||||||
|
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||||||
|
throw new WebApplicationException(clientResponseToResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(response.getEntity(PreKeyResponseV2.class));
|
||||||
|
|
||||||
|
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||||
|
logger.warn("PreKey", e);
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public int getUserCount() {
|
public int getUserCount() {
|
||||||
try {
|
try {
|
||||||
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
|
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
|
||||||
@@ -139,22 +179,37 @@ public class FederatedClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(String destination, OutgoingMessageSignal message)
|
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
|
||||||
throws IOException, NoSuchUserException
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH);
|
WebResource resource = client.resource(peer.getUrl()).path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination));
|
||||||
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
||||||
.header("Authorization", authorizationHeader)
|
.header("Authorization", authorizationHeader)
|
||||||
.entity(new RelayMessage(destination, message.toByteArray()))
|
.entity(messages)
|
||||||
.put(ClientResponse.class);
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
if (response.getStatus() == 404) {
|
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||||
throw new NoSuchUserException("No remote user: " + destination);
|
throw new WebApplicationException(clientResponseToResponse(response));
|
||||||
|
}
|
||||||
|
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||||
|
logger.warn("sendMessage", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
String path = String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId);
|
||||||
|
WebResource resource = client.resource(peer.getUrl()).path(path);
|
||||||
|
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
|
||||||
|
.header("Authorization", authorizationHeader)
|
||||||
|
.put(ClientResponse.class);
|
||||||
|
|
||||||
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
if (response.getStatus() != 200 && response.getStatus() != 204) {
|
||||||
throw new IOException("Bad response: " + response.getStatus());
|
throw new WebApplicationException(clientResponseToResponse(response));
|
||||||
}
|
}
|
||||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||||
logger.warn("sendMessage", e);
|
logger.warn("sendMessage", e);
|
||||||
@@ -206,6 +261,19 @@ public class FederatedClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Response clientResponseToResponse(ClientResponse r) {
|
||||||
|
Response.ResponseBuilder rb = Response.status(r.getStatus());
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<String>> entry : r.getHeaders().entrySet()) {
|
||||||
|
for (String value : entry.getValue()) {
|
||||||
|
rb.header(entry.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.entity(r.getEntityInputStream());
|
||||||
|
return rb.build();
|
||||||
|
}
|
||||||
|
|
||||||
public String getPeerName() {
|
public String getPeerName() {
|
||||||
return peer.getName();
|
return peer.getName();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.federation;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
|
||||||
|
public class NonLimitedAccount extends Account {
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
private final String number;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
private final String relay;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
private final long deviceId;
|
||||||
|
|
||||||
|
public NonLimitedAccount(String number, long deviceId, String relay) {
|
||||||
|
this.number = number;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.relay = relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNumber() {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRateLimited() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> getRelay() {
|
||||||
|
return Optional.of(relay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Device> getAuthenticatedDevice() {
|
||||||
|
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,27 +16,41 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.limits;
|
package org.whispersystems.textsecuregcm.limits;
|
||||||
|
|
||||||
import com.yammer.metrics.Metrics;
|
import com.codahale.metrics.Meter;
|
||||||
import com.yammer.metrics.core.Meter;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import net.spy.memcached.MemcachedClient;
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
|
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.SystemMapper;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
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 Logger logger = LoggerFactory.getLogger(RateLimiter.class);
|
||||||
|
private final ObjectMapper mapper = SystemMapper.getMapper();
|
||||||
|
|
||||||
private final Meter meter;
|
private final Meter meter;
|
||||||
private final MemcachedClient memcachedClient;
|
private final JedisPool cacheClient;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final int bucketSize;
|
private final int bucketSize;
|
||||||
private final double leakRatePerMillis;
|
private final double leakRatePerMillis;
|
||||||
|
|
||||||
public RateLimiter(MemcachedClient memcachedClient, String name,
|
public RateLimiter(JedisPool cacheClient, String name,
|
||||||
int bucketSize, double leakRatePerMinute)
|
int bucketSize, double leakRatePerMinute)
|
||||||
{
|
{
|
||||||
this.meter = Metrics.newMeter(RateLimiter.class, name, "exceeded", TimeUnit.MINUTES);
|
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
this.memcachedClient = memcachedClient;
|
|
||||||
|
this.meter = metricRegistry.meter(name(getClass(), name, "exceeded"));
|
||||||
|
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);
|
||||||
@@ -58,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) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private LeakyBucket getBucket(String key) {
|
private LeakyBucket getBucket(String key) {
|
||||||
LeakyBucket bucket = (LeakyBucket)memcachedClient.get(getBucketName(key));
|
try (Jedis jedis = cacheClient.getResource()) {
|
||||||
|
String serialized = jedis.get(getBucketName(key));
|
||||||
|
|
||||||
if (bucket == null) {
|
if (serialized != null) {
|
||||||
return new LeakyBucket(bucketSize, leakRatePerMillis);
|
return LeakyBucket.fromSerialized(mapper, serialized);
|
||||||
} else {
|
|
||||||
return bucket;
|
|
||||||
}
|
}
|
||||||
|
} 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;
|
||||||
@@ -31,34 +32,54 @@ public class RateLimiters {
|
|||||||
private final RateLimiter preKeysLimiter;
|
private final RateLimiter preKeysLimiter;
|
||||||
private final RateLimiter messagesLimiter;
|
private final RateLimiter messagesLimiter;
|
||||||
|
|
||||||
public RateLimiters(RateLimitsConfiguration config, MemcachedClient memcachedClient) {
|
private final RateLimiter allocateDeviceLimiter;
|
||||||
this.smsDestinationLimiter = new RateLimiter(memcachedClient, "smsDestination",
|
private final RateLimiter verifyDeviceLimiter;
|
||||||
|
|
||||||
|
public RateLimiters(RateLimitsConfiguration config, JedisPool cacheClient) {
|
||||||
|
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(cacheClient, "allocateDevice",
|
||||||
|
config.getAllocateDevice().getBucketSize(),
|
||||||
|
config.getAllocateDevice().getLeakRatePerMinute());
|
||||||
|
|
||||||
|
this.verifyDeviceLimiter = new RateLimiter(cacheClient, "verifyDevice",
|
||||||
|
config.getVerifyDevice().getBucketSize(),
|
||||||
|
config.getVerifyDevice().getLeakRatePerMinute());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateLimiter getAllocateDeviceLimiter() {
|
||||||
|
return allocateDeviceLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateLimiter getVerifyDeviceLimiter() {
|
||||||
|
return verifyDeviceLimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RateLimiter getMessagesLimiter() {
|
public RateLimiter getMessagesLimiter() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,9 @@ public class IOExceptionMapper implements ExceptionMapper<IOException> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response toResponse(IOException e) {
|
public Response toResponse(IOException e) {
|
||||||
|
if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) {
|
||||||
logger.warn("IOExceptionMapper", e);
|
logger.warn("IOExceptionMapper", e);
|
||||||
|
}
|
||||||
return Response.status(503).build();
|
return Response.status(503).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.metrics;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Gauge;
|
||||||
|
import com.sun.management.OperatingSystemMXBean;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
|
||||||
|
public class CpuUsageGauge implements Gauge<Integer> {
|
||||||
|
@Override
|
||||||
|
public Integer getValue() {
|
||||||
|
OperatingSystemMXBean mbean = (com.sun.management.OperatingSystemMXBean)
|
||||||
|
ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
|
||||||
|
return (int) Math.ceil(mbean.getSystemCpuLoad() * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.metrics;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Gauge;
|
||||||
|
import com.sun.management.OperatingSystemMXBean;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
|
||||||
|
public class FreeMemoryGauge implements Gauge<Long> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getValue() {
|
||||||
|
OperatingSystemMXBean mbean = (com.sun.management.OperatingSystemMXBean)
|
||||||
|
ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
|
||||||
|
return mbean.getFreePhysicalMemorySize();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.metrics;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Counter;
|
||||||
|
import com.codahale.metrics.Gauge;
|
||||||
|
import com.codahale.metrics.Histogram;
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
|
import com.codahale.metrics.Metered;
|
||||||
|
import com.codahale.metrics.MetricFilter;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.ScheduledReporter;
|
||||||
|
import com.codahale.metrics.Snapshot;
|
||||||
|
import com.codahale.metrics.Timer;
|
||||||
|
import com.fasterxml.jackson.core.JsonEncoding;
|
||||||
|
import com.fasterxml.jackson.core.JsonFactory;
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class JsonMetricsReporter extends ScheduledReporter {
|
||||||
|
|
||||||
|
private static final Pattern SIMPLE_NAMES = Pattern.compile("[^a-zA-Z0-9_.\\-~]");
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(JsonMetricsReporter.class);
|
||||||
|
private final JsonFactory factory = new JsonFactory();
|
||||||
|
|
||||||
|
private final String token;
|
||||||
|
private final String hostname;
|
||||||
|
private final String host;
|
||||||
|
|
||||||
|
public JsonMetricsReporter(MetricRegistry registry, String token, String hostname,
|
||||||
|
MetricFilter filter, TimeUnit rateUnit, TimeUnit durationUnit)
|
||||||
|
throws UnknownHostException
|
||||||
|
{
|
||||||
|
super(registry, "json-reporter", filter, rateUnit, durationUnit);
|
||||||
|
this.token = token;
|
||||||
|
this.hostname = hostname;
|
||||||
|
this.host = InetAddress.getLocalHost().getHostName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void report(SortedMap<String, Gauge> stringGaugeSortedMap,
|
||||||
|
SortedMap<String, Counter> stringCounterSortedMap,
|
||||||
|
SortedMap<String, Histogram> stringHistogramSortedMap,
|
||||||
|
SortedMap<String, Meter> stringMeterSortedMap,
|
||||||
|
SortedMap<String, Timer> stringTimerSortedMap)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
logger.debug("Reporting metrics...");
|
||||||
|
URL url = new URL("https", hostname, 443, String.format("/report/metrics?t=%s&h=%s", token, host));
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
connection.addRequestProperty("Content-Type", "application/json");
|
||||||
|
|
||||||
|
OutputStream outputStream = connection.getOutputStream();
|
||||||
|
JsonGenerator json = factory.createGenerator(outputStream, JsonEncoding.UTF8);
|
||||||
|
|
||||||
|
json.writeStartObject();
|
||||||
|
|
||||||
|
for (Map.Entry<String, Gauge> gauge : stringGaugeSortedMap.entrySet()) {
|
||||||
|
reportGauge(json, gauge.getKey(), gauge.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Counter> counter : stringCounterSortedMap.entrySet()) {
|
||||||
|
reportCounter(json, counter.getKey(), counter.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Histogram> histogram : stringHistogramSortedMap.entrySet()) {
|
||||||
|
reportHistogram(json, histogram.getKey(), histogram.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Meter> meter : stringMeterSortedMap.entrySet()) {
|
||||||
|
reportMeter(json, meter.getKey(), meter.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Timer> timer : stringTimerSortedMap.entrySet()) {
|
||||||
|
reportTimer(json, timer.getKey(), timer.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
json.writeEndObject();
|
||||||
|
json.close();
|
||||||
|
|
||||||
|
outputStream.close();
|
||||||
|
|
||||||
|
logger.debug("Metrics server response: " + connection.getResponseCode());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error sending metrics", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportGauge(JsonGenerator json, String name, Gauge gauge) throws IOException {
|
||||||
|
Object gaugeValue = evaluateGauge(gauge);
|
||||||
|
|
||||||
|
if (gaugeValue instanceof Number) {
|
||||||
|
json.writeFieldName(sanitize(name));
|
||||||
|
json.writeObject(gaugeValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportCounter(JsonGenerator json, String name, Counter counter) throws IOException {
|
||||||
|
json.writeFieldName(sanitize(name));
|
||||||
|
json.writeNumber(counter.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportHistogram(JsonGenerator json, String name, Histogram histogram) throws IOException {
|
||||||
|
Snapshot snapshot = histogram.getSnapshot();
|
||||||
|
json.writeFieldName(sanitize(name));
|
||||||
|
json.writeStartObject();
|
||||||
|
json.writeNumberField("count", histogram.getCount());
|
||||||
|
writeSnapshot(json, snapshot);
|
||||||
|
json.writeEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportMeter(JsonGenerator json, String name, Meter meter) throws IOException {
|
||||||
|
json.writeFieldName(sanitize(name));
|
||||||
|
json.writeStartObject();
|
||||||
|
writeMetered(json, meter);
|
||||||
|
json.writeEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportTimer(JsonGenerator json, String name, Timer timer) throws IOException {
|
||||||
|
json.writeFieldName(sanitize(name));
|
||||||
|
json.writeStartObject();
|
||||||
|
json.writeFieldName("rate");
|
||||||
|
json.writeStartObject();
|
||||||
|
writeMetered(json, timer);
|
||||||
|
json.writeEndObject();
|
||||||
|
json.writeFieldName("duration");
|
||||||
|
json.writeStartObject();
|
||||||
|
writeSnapshot(json, timer.getSnapshot());
|
||||||
|
json.writeEndObject();
|
||||||
|
json.writeEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object evaluateGauge(Gauge gauge) {
|
||||||
|
try {
|
||||||
|
return gauge.getValue();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
logger.warn("Error reading gauge", e);
|
||||||
|
return "error reading gauge";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeSnapshot(JsonGenerator json, Snapshot snapshot) throws IOException {
|
||||||
|
json.writeNumberField("max", convertDuration(snapshot.getMax()));
|
||||||
|
json.writeNumberField("mean", convertDuration(snapshot.getMean()));
|
||||||
|
json.writeNumberField("min", convertDuration(snapshot.getMin()));
|
||||||
|
json.writeNumberField("stddev", convertDuration(snapshot.getStdDev()));
|
||||||
|
json.writeNumberField("median", convertDuration(snapshot.getMedian()));
|
||||||
|
json.writeNumberField("p75", convertDuration(snapshot.get75thPercentile()));
|
||||||
|
json.writeNumberField("p95", convertDuration(snapshot.get95thPercentile()));
|
||||||
|
json.writeNumberField("p98", convertDuration(snapshot.get98thPercentile()));
|
||||||
|
json.writeNumberField("p99", convertDuration(snapshot.get99thPercentile()));
|
||||||
|
json.writeNumberField("p999", convertDuration(snapshot.get999thPercentile()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeMetered(JsonGenerator json, Metered meter) throws IOException {
|
||||||
|
json.writeNumberField("count", convertRate(meter.getCount()));
|
||||||
|
json.writeNumberField("mean", convertRate(meter.getMeanRate()));
|
||||||
|
json.writeNumberField("m1", convertRate(meter.getOneMinuteRate()));
|
||||||
|
json.writeNumberField("m5", convertRate(meter.getFiveMinuteRate()));
|
||||||
|
json.writeNumberField("m15", convertRate(meter.getFifteenMinuteRate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String sanitize(String metricName) {
|
||||||
|
return SIMPLE_NAMES.matcher(metricName).replaceAll("_");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder forRegistry(MetricRegistry registry) {
|
||||||
|
return new Builder(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private final MetricRegistry registry;
|
||||||
|
private MetricFilter filter = MetricFilter.ALL;
|
||||||
|
private TimeUnit rateUnit = TimeUnit.SECONDS;
|
||||||
|
private TimeUnit durationUnit = TimeUnit.MILLISECONDS;
|
||||||
|
private String token;
|
||||||
|
private String hostname;
|
||||||
|
|
||||||
|
private Builder(MetricRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
this.rateUnit = TimeUnit.SECONDS;
|
||||||
|
this.durationUnit = TimeUnit.MILLISECONDS;
|
||||||
|
this.filter = MetricFilter.ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder convertRatesTo(TimeUnit rateUnit) {
|
||||||
|
this.rateUnit = rateUnit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder convertDurationsTo(TimeUnit durationUnit) {
|
||||||
|
this.durationUnit = durationUnit;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder filter(MetricFilter filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withHostname(String hostname) {
|
||||||
|
this.hostname = hostname;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonMetricsReporter build() throws UnknownHostException {
|
||||||
|
if (hostname == null) {
|
||||||
|
throw new IllegalArgumentException("No hostname specified!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
throw new IllegalArgumentException("No token specified!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonMetricsReporter(registry, token, hostname, filter, rateUnit, durationUnit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.metrics;
|
||||||
|
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.ScheduledReporter;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
import io.dropwizard.metrics.BaseReporterFactory;
|
||||||
|
|
||||||
|
@JsonTypeName("json")
|
||||||
|
public class JsonMetricsReporterFactory extends BaseReporterFactory {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private String hostname;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScheduledReporter build(MetricRegistry metricRegistry) {
|
||||||
|
try {
|
||||||
|
return JsonMetricsReporter.forRegistry(metricRegistry)
|
||||||
|
.withHostname(hostname)
|
||||||
|
.withToken(token)
|
||||||
|
.convertRatesTo(getRateUnit())
|
||||||
|
.convertDurationsTo(getDurationUnit())
|
||||||
|
.filter(getFilter())
|
||||||
|
.build();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.metrics;
|
||||||
|
|
||||||
|
|
||||||
|
import com.codahale.metrics.Gauge;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public abstract class NetworkGauge implements Gauge<Long> {
|
||||||
|
|
||||||
|
protected Pair<Long, Long> getSentReceived() throws IOException {
|
||||||
|
File proc = new File("/proc/net/dev");
|
||||||
|
BufferedReader reader = new BufferedReader(new FileReader(proc));
|
||||||
|
String header = reader.readLine();
|
||||||
|
String header2 = reader.readLine();
|
||||||
|
|
||||||
|
long bytesSent = 0;
|
||||||
|
long bytesReceived = 0;
|
||||||
|
|
||||||
|
String interfaceStats;
|
||||||
|
|
||||||
|
while ((interfaceStats = reader.readLine()) != null) {
|
||||||
|
String[] stats = interfaceStats.split("\\s+");
|
||||||
|
|
||||||
|
if (!stats[1].equals("lo:")) {
|
||||||
|
bytesReceived += Long.parseLong(stats[2]);
|
||||||
|
bytesSent += Long.parseLong(stats[10]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Pair<>(bytesSent, bytesReceived);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.metrics;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class NetworkReceivedGauge extends NetworkGauge {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(NetworkSentGauge.class);
|
||||||
|
|
||||||
|
private long lastTimestamp;
|
||||||
|
private long lastReceived;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getValue() {
|
||||||
|
try {
|
||||||
|
long timestamp = System.currentTimeMillis();
|
||||||
|
Pair<Long, Long> sentAndReceived = getSentReceived();
|
||||||
|
long result = 0;
|
||||||
|
|
||||||
|
if (lastTimestamp != 0) {
|
||||||
|
result = sentAndReceived.second() - lastReceived;
|
||||||
|
lastReceived = sentAndReceived.second();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
return result;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("NetworkReceivedGauge", e);
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.metrics;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class NetworkSentGauge extends NetworkGauge {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(NetworkSentGauge.class);
|
||||||
|
|
||||||
|
private long lastTimestamp;
|
||||||
|
private long lastSent;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getValue() {
|
||||||
|
try {
|
||||||
|
long timestamp = System.currentTimeMillis();
|
||||||
|
Pair<Long, Long> sentAndReceived = getSentReceived();
|
||||||
|
long result = 0;
|
||||||
|
|
||||||
|
if (lastTimestamp != 0) {
|
||||||
|
result = sentAndReceived.first() - lastSent;
|
||||||
|
lastSent = sentAndReceived.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
return result;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("NetworkSentGauge", e);
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.yammer.metrics.core.HealthCheck;
|
|
||||||
import com.yammer.metrics.core.HealthCheck.Result;
|
|
||||||
import net.spy.memcached.MemcachedClient;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
public class MemcacheHealthCheck extends HealthCheck {
|
|
||||||
|
|
||||||
private final MemcachedClient client;
|
|
||||||
|
|
||||||
public MemcacheHealthCheck(MemcachedClient client) {
|
|
||||||
super("memcached");
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Result check() throws Exception {
|
|
||||||
if (client == null) {
|
|
||||||
return Result.unhealthy("not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
int random = SecureRandom.getInstance("SHA1PRNG").nextInt();
|
|
||||||
int value = SecureRandom.getInstance("SHA1PRNG").nextInt();
|
|
||||||
|
|
||||||
this.client.set("HEALTH" + random, 2000, String.valueOf(value));
|
|
||||||
String result = (String)this.client.get("HEALTH" + random);
|
|
||||||
|
|
||||||
if (result == null || Integer.parseInt(result) != value) {
|
|
||||||
return Result.unhealthy("Fetch failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.healthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.providers;
|
package org.whispersystems.textsecuregcm.providers;
|
||||||
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -30,11 +29,11 @@ public class RedisClientFactory {
|
|||||||
|
|
||||||
private final JedisPool jedisPool;
|
private final JedisPool jedisPool;
|
||||||
|
|
||||||
public RedisClientFactory(RedisConfiguration redisConfig) throws URISyntaxException {
|
public RedisClientFactory(String url) throws URISyntaxException {
|
||||||
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
||||||
poolConfig.setTestOnBorrow(true);
|
poolConfig.setTestOnBorrow(true);
|
||||||
|
|
||||||
URI redisURI = new URI(redisConfig.getUrl());
|
URI redisURI = new URI(url);
|
||||||
String redisHost = redisURI.getHost();
|
String redisHost = redisURI.getHost();
|
||||||
int redisPort = redisURI.getPort();
|
int redisPort = redisURI.getPort();
|
||||||
String redisPassword = null;
|
String redisPassword = null;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.providers;
|
package org.whispersystems.textsecuregcm.providers;
|
||||||
|
|
||||||
import com.yammer.metrics.core.HealthCheck;
|
import com.codahale.metrics.health.HealthCheck;
|
||||||
|
|
||||||
import redis.clients.jedis.Jedis;
|
import redis.clients.jedis.Jedis;
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
@@ -26,7 +26,6 @@ public class RedisHealthCheck extends HealthCheck {
|
|||||||
private final JedisPool clientPool;
|
private final JedisPool clientPool;
|
||||||
|
|
||||||
public RedisHealthCheck(JedisPool clientPool) {
|
public RedisHealthCheck(JedisPool clientPool) {
|
||||||
super("redis");
|
|
||||||
this.clientPool = clientPool;
|
this.clientPool = clientPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.providers;
|
||||||
|
|
||||||
|
public class TimeProvider {
|
||||||
|
public long getCurrentTimeMillis() {
|
||||||
|
return System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.whispersystems.textsecuregcm.push;
|
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
|
||||||
import com.notnoop.apns.APNS;
|
|
||||||
import com.notnoop.apns.ApnsService;
|
|
||||||
import com.notnoop.exceptions.NetworkIOException;
|
|
||||||
import com.yammer.metrics.Metrics;
|
|
||||||
import com.yammer.metrics.core.Meter;
|
|
||||||
import org.bouncycastle.openssl.PEMReader;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.security.KeyPair;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class APNSender {
|
|
||||||
|
|
||||||
private final Meter success = Metrics.newMeter(APNSender.class, "sent", "success", TimeUnit.MINUTES);
|
|
||||||
private final Meter failure = Metrics.newMeter(APNSender.class, "sent", "failure", TimeUnit.MINUTES);
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
|
|
||||||
|
|
||||||
private static final String MESSAGE_BODY = "m";
|
|
||||||
|
|
||||||
private final Optional<ApnsService> apnService;
|
|
||||||
|
|
||||||
public APNSender(String apnCertificate, String apnKey)
|
|
||||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException
|
|
||||||
{
|
|
||||||
if (!Util.isEmpty(apnCertificate) && !Util.isEmpty(apnKey)) {
|
|
||||||
byte[] keyStore = initializeKeyStore(apnCertificate, apnKey);
|
|
||||||
this.apnService = Optional.of(APNS.newService()
|
|
||||||
.withCert(new ByteArrayInputStream(keyStore), "insecure")
|
|
||||||
.withSandboxDestination().build());
|
|
||||||
} else {
|
|
||||||
this.apnService = Optional.absent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMessage(String registrationId, EncryptedOutgoingMessage message)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (!apnService.isPresent()) {
|
|
||||||
failure.mark();
|
|
||||||
throw new IOException("APN access not configured!");
|
|
||||||
}
|
|
||||||
|
|
||||||
String payload = APNS.newPayload()
|
|
||||||
.alertBody("Message!")
|
|
||||||
.customField(MESSAGE_BODY, message.serialize())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
logger.debug("APN Payload: " + payload);
|
|
||||||
|
|
||||||
apnService.get().push(registrationId, payload);
|
|
||||||
success.mark();
|
|
||||||
} catch (MalformedURLException mue) {
|
|
||||||
throw new AssertionError(mue);
|
|
||||||
} catch (NetworkIOException nioe) {
|
|
||||||
logger.warn("Network Error", nioe);
|
|
||||||
failure.mark();
|
|
||||||
throw new IOException("Error sending APN");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] initializeKeyStore(String pemCertificate, String pemKey)
|
|
||||||
throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException
|
|
||||||
{
|
|
||||||
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
|
|
||||||
X509Certificate certificate = (X509Certificate) reader.readObject();
|
|
||||||
Certificate[] certificateChain = {certificate};
|
|
||||||
|
|
||||||
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes())));
|
|
||||||
KeyPair keyPair = (KeyPair) reader.readObject();
|
|
||||||
|
|
||||||
KeyStore keyStore = KeyStore.getInstance("pkcs12");
|
|
||||||
keyStore.load(null);
|
|
||||||
keyStore.setEntry("apn",
|
|
||||||
new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), certificateChain),
|
|
||||||
new KeyStore.PasswordProtection("insecure".toCharArray()));
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
keyStore.store(baos, "insecure".toCharArray());
|
|
||||||
|
|
||||||
return baos.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UnregisteredEvent;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.dropwizard.lifecycle.Managed;
|
||||||
|
|
||||||
|
public class FeedbackHandler implements Managed, Runnable {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(PushServiceClient.class);
|
||||||
|
|
||||||
|
private final PushServiceClient client;
|
||||||
|
private final AccountsManager accountsManager;
|
||||||
|
|
||||||
|
private ScheduledExecutorService executor;
|
||||||
|
|
||||||
|
public FeedbackHandler(PushServiceClient client, AccountsManager accountsManager) {
|
||||||
|
this.client = client;
|
||||||
|
this.accountsManager = accountsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws Exception {
|
||||||
|
this.executor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
this.executor.scheduleAtFixedRate(this, 0, 1, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() throws Exception {
|
||||||
|
if (this.executor != null) {
|
||||||
|
this.executor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
List<UnregisteredEvent> gcmFeedback = client.getGcmFeedback();
|
||||||
|
List<UnregisteredEvent> apnFeedback = client.getApnFeedback();
|
||||||
|
|
||||||
|
for (UnregisteredEvent gcmEvent : gcmFeedback) {
|
||||||
|
handleGcmUnregistered(gcmEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UnregisteredEvent apnEvent : apnFeedback) {
|
||||||
|
handleApnUnregistered(apnEvent);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Error retrieving feedback: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleGcmUnregistered(UnregisteredEvent event) {
|
||||||
|
logger.info("Got GCM Unregistered: " + event.getNumber() + "," + event.getDeviceId());
|
||||||
|
|
||||||
|
Optional<Account> account = accountsManager.get(event.getNumber());
|
||||||
|
|
||||||
|
if (account.isPresent()) {
|
||||||
|
Optional<Device> device = account.get().getDevice(event.getDeviceId());
|
||||||
|
|
||||||
|
if (device.isPresent()) {
|
||||||
|
if (event.getRegistrationId().equals(device.get().getGcmId())) {
|
||||||
|
logger.info("GCM Unregister GCM ID matches!");
|
||||||
|
if (device.get().getPushTimestamp() == 0 ||
|
||||||
|
event.getTimestamp() > device.get().getPushTimestamp())
|
||||||
|
{
|
||||||
|
logger.info("GCM Unregister Timestamp matches!");
|
||||||
|
device.get().setGcmId(null);
|
||||||
|
accountsManager.update(account.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleApnUnregistered(UnregisteredEvent event) {
|
||||||
|
logger.info("Got APN Unregistered: " + event.getNumber() + "," + event.getDeviceId());
|
||||||
|
|
||||||
|
Optional<Account> account = accountsManager.get(event.getNumber());
|
||||||
|
|
||||||
|
if (account.isPresent()) {
|
||||||
|
Optional<Device> device = account.get().getDevice(event.getDeviceId());
|
||||||
|
|
||||||
|
if (device.isPresent()) {
|
||||||
|
if (event.getRegistrationId().equals(device.get().getApnId())) {
|
||||||
|
logger.info("APN Unregister APN ID matches!");
|
||||||
|
if (device.get().getPushTimestamp() == 0 ||
|
||||||
|
event.getTimestamp() > device.get().getPushTimestamp())
|
||||||
|
{
|
||||||
|
logger.info("APN Unregister timestamp matches!");
|
||||||
|
device.get().setApnId(null);
|
||||||
|
accountsManager.update(account.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user