mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-11 01:40:22 +00:00
Compare commits
498 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef9a7fda9a | ||
|
|
13447df1e0 | ||
|
|
3608c5bfb0 | ||
|
|
34dbff6786 | ||
|
|
a6066bfc2f | ||
|
|
8579190cdf | ||
|
|
917f667229 | ||
|
|
317a551bdb | ||
|
|
27e9271473 | ||
|
|
11dff6c546 | ||
|
|
e6712937ca | ||
|
|
cf8887bb5a | ||
|
|
696340f780 | ||
|
|
86ddcbaa08 | ||
|
|
2144d2a8d8 | ||
|
|
f7af861b31 | ||
|
|
208a09b3ae | ||
|
|
831023e41d | ||
|
|
ff627793d6 | ||
|
|
f971c76a99 | ||
|
|
8f41176c76 | ||
|
|
31bbbbb5e0 | ||
|
|
effcd6038d | ||
|
|
12be7d49c2 | ||
|
|
14863b575e | ||
|
|
32a95f96ff | ||
|
|
b757d4b334 | ||
|
|
bd03d910fe | ||
|
|
01ef855157 | ||
|
|
817866caf3 | ||
|
|
158d65c6a7 | ||
|
|
62022c7de1 | ||
|
|
a824b5575d | ||
|
|
78819d5382 | ||
|
|
d128bc782a | ||
|
|
530b2a310f | ||
|
|
d5b0d99a54 | ||
|
|
43be72d076 | ||
|
|
9558944e22 | ||
|
|
0f6c866c8d | ||
|
|
bac78e9291 | ||
|
|
c22ea78672 | ||
|
|
a85afe827d | ||
|
|
abaed821ec | ||
|
|
6fa9dcd954 | ||
|
|
819d59cd79 | ||
|
|
2f88f0eedb | ||
|
|
74ff491671 | ||
|
|
eac48a6617 | ||
|
|
19617c14f8 | ||
|
|
fc7291c3e8 | ||
|
|
88db808298 | ||
|
|
5193abdab3 | ||
|
|
a315c9be92 | ||
|
|
fc1541591a | ||
|
|
ae97c4db9f | ||
|
|
26bc5973b5 | ||
|
|
e52b8c8423 | ||
|
|
7395489bac | ||
|
|
b384ed7f5c | ||
|
|
e3afcae7d3 | ||
|
|
9faeed7b20 | ||
|
|
49adcca80e | ||
|
|
49c43a6816 | ||
|
|
84f85ae098 | ||
|
|
3d581941ab | ||
|
|
d2d39baede | ||
|
|
111f5ba024 | ||
|
|
ce3fb7fa99 | ||
|
|
fc421d3f21 | ||
|
|
71bea759c6 | ||
|
|
bf1dd791a5 | ||
|
|
4c99577c08 | ||
|
|
5d5c63e6d4 | ||
|
|
42ff3f8432 | ||
|
|
be6ef76486 | ||
|
|
bc297e6d34 | ||
|
|
3a526dcbd7 | ||
|
|
7883352b74 | ||
|
|
982d122d18 | ||
|
|
d8d94407c6 | ||
|
|
28cfc54170 | ||
|
|
2ee7279743 | ||
|
|
eb1b073385 | ||
|
|
c634185b6f | ||
|
|
827a3af419 | ||
|
|
2c33d22a30 | ||
|
|
b41ed9d810 | ||
|
|
58d3a12eff | ||
|
|
88c4b2be97 | ||
|
|
6cbd57f19f | ||
|
|
5522376584 | ||
|
|
5089c37d28 | ||
|
|
1ccf24e68c | ||
|
|
411f7298f2 | ||
|
|
5b0214c6f2 | ||
|
|
735573e61b | ||
|
|
c545cff1b3 | ||
|
|
cbd9681e3e | ||
|
|
ca876e40ca | ||
|
|
76f5a71727 | ||
|
|
117de2382d | ||
|
|
25e7036451 | ||
|
|
3131bd3dd9 | ||
|
|
1cf9397bbd | ||
|
|
c97be15e79 | ||
|
|
164fc40990 | ||
|
|
6456af6284 | ||
|
|
81212cc13a | ||
|
|
6f0750790c | ||
|
|
3e61b5c49d | ||
|
|
50c4df4f45 | ||
|
|
1eb946f5fe | ||
|
|
7bd402b48d | ||
|
|
90444d5b91 | ||
|
|
5ee093f87c | ||
|
|
623743286c | ||
|
|
67067f1d2d | ||
|
|
07f9bb112e | ||
|
|
417d48c452 | ||
|
|
358412c78a | ||
|
|
215621a9b0 | ||
|
|
c3f53c4dd9 | ||
|
|
01514f83a0 | ||
|
|
c10b64c367 | ||
|
|
722055c8b5 | ||
|
|
680e501f83 | ||
|
|
f13f7a5ff4 | ||
|
|
5290656c3b | ||
|
|
93fbb87741 | ||
|
|
ce76c5c117 | ||
|
|
e663e1b0a6 | ||
|
|
20cdd09171 | ||
|
|
f98dd80941 | ||
|
|
f84736bd32 | ||
|
|
9995f271c8 | ||
|
|
cf59d849b0 | ||
|
|
ee3b91e4fb | ||
|
|
77f134ddca | ||
|
|
8913192b7e | ||
|
|
94ac3f6cc8 | ||
|
|
b89de860d3 | ||
|
|
f8c623074b | ||
|
|
1160af9522 | ||
|
|
3056ea8cbc | ||
|
|
28e3b23e8c | ||
|
|
fbaf4a09e2 | ||
|
|
cfa8cbedc1 | ||
|
|
be4c46e674 | ||
|
|
bacf524ae6 | ||
|
|
aa65d34c36 | ||
|
|
0cd3640f13 | ||
|
|
c595d9415c | ||
|
|
1a604d8c79 | ||
|
|
f76e6705c0 | ||
|
|
10cd60738a | ||
|
|
89470ff536 | ||
|
|
79b8202452 | ||
|
|
d252e579f4 | ||
|
|
30b2c2b5ad | ||
|
|
282f39141e | ||
|
|
85e4de6933 | ||
|
|
0b993098a8 | ||
|
|
1880773fb9 | ||
|
|
00c9023e74 | ||
|
|
d59eabd9d7 | ||
|
|
2a3ea13c9e | ||
|
|
6906336dfb | ||
|
|
514b94a5cb | ||
|
|
df01be2dca | ||
|
|
10c6f885fd | ||
|
|
224e6dac31 | ||
|
|
3b1eb3a9db | ||
|
|
e320626c6e | ||
|
|
03dac2bf7e | ||
|
|
730303567f | ||
|
|
57ff9f86f5 | ||
|
|
bfd2c32d4e | ||
|
|
e9a3d52d7f | ||
|
|
ac7eb88194 | ||
|
|
d45154f2aa | ||
|
|
760462f8fb | ||
|
|
1999bd2bcb | ||
|
|
46110d4d65 | ||
|
|
5752853bba | ||
|
|
02d06af3fc | ||
|
|
09e0934eaf | ||
|
|
b100f09205 | ||
|
|
670b69df24 | ||
|
|
03a531e1b0 | ||
|
|
13ecbe7e53 | ||
|
|
17047513c3 | ||
|
|
7bd7d0e84e | ||
|
|
4571042814 | ||
|
|
9cb89b42bf | ||
|
|
9a4453c414 | ||
|
|
5fa22bc073 | ||
|
|
bf32b766a5 | ||
|
|
f0a8b5a54a | ||
|
|
8e68e0e037 | ||
|
|
b81b811400 | ||
|
|
b41f97233e | ||
|
|
350de1c759 | ||
|
|
055e8d80a1 | ||
|
|
dfb8a419e7 | ||
|
|
030a791d69 | ||
|
|
cf495ef7cf | ||
|
|
8fdbcbef44 | ||
|
|
30c9968928 | ||
|
|
f357ad098f | ||
|
|
1a8c40c02a | ||
|
|
20677d4be1 | ||
|
|
c448c37cc9 | ||
|
|
f117d9ff4d | ||
|
|
2dbd7ffc75 | ||
|
|
fac4538f6f | ||
|
|
01e526af25 | ||
|
|
7e805d1592 | ||
|
|
c63bebb3e7 | ||
|
|
0e6cfb460d | ||
|
|
cd6b2512e1 | ||
|
|
4f6b132449 | ||
|
|
b7c611a466 | ||
|
|
0163242c8a | ||
|
|
7fa17e33e9 | ||
|
|
e4dbb8efe7 | ||
|
|
89256fb5b3 | ||
|
|
59e401f41e | ||
|
|
4b42dd1db3 | ||
|
|
6196856a7c | ||
|
|
0e8d4f9a61 | ||
|
|
97d2d97ee7 | ||
|
|
62315f423c | ||
|
|
5ee56b022c | ||
|
|
6c37b658ac | ||
|
|
1f53900345 | ||
|
|
deece33a0d | ||
|
|
13053da97f | ||
|
|
4c019aef15 | ||
|
|
bab5e5769b | ||
|
|
f68390e96f | ||
|
|
76cbf734ad | ||
|
|
17ba630014 | ||
|
|
7057476048 | ||
|
|
3121867f72 | ||
|
|
435410b004 | ||
|
|
f190462879 | ||
|
|
7c0ff67625 | ||
|
|
ac72c8b2de | ||
|
|
20208ae528 | ||
|
|
6c6f073bc2 | ||
|
|
0663fe30df | ||
|
|
2c0a75586b | ||
|
|
b6cb23cbb5 | ||
|
|
ee555285ed | ||
|
|
b75456acf3 | ||
|
|
be6d6351b9 | ||
|
|
abafa2ccac | ||
|
|
53e6f419b6 | ||
|
|
b75dec40ac | ||
|
|
0f4f775ee2 | ||
|
|
5974328d9c | ||
|
|
a472774734 | ||
|
|
166d203e8e | ||
|
|
3b3764535c | ||
|
|
f2a1a65a45 | ||
|
|
b7c56108ca | ||
|
|
52478e7de0 | ||
|
|
ae9fd090de | ||
|
|
59bbd0c43c | ||
|
|
f6c9b2b6e7 | ||
|
|
0c0e33bc0e | ||
|
|
4d33ba48cc | ||
|
|
18fb23f27c | ||
|
|
92c25a8373 | ||
|
|
14f5271c20 | ||
|
|
37bda0b035 | ||
|
|
675785a4fd | ||
|
|
0572951c8a | ||
|
|
7d766ee39e | ||
|
|
1f24c913a6 | ||
|
|
2a8806ec2e | ||
|
|
ffcabe6fc4 | ||
|
|
365ad3a4f8 | ||
|
|
2cb788ceb7 | ||
|
|
257fef9734 | ||
|
|
37e0730d2a | ||
|
|
dea359ef91 | ||
|
|
64c9648dd8 | ||
|
|
6dfd13118d | ||
|
|
2f6105f9bc | ||
|
|
5c23f62cec | ||
|
|
ab4e94edab | ||
|
|
9589b7758c | ||
|
|
681cdf8eff | ||
|
|
ad6c271f9d | ||
|
|
c8414a63fb | ||
|
|
c10d9603ad | ||
|
|
91bd061110 | ||
|
|
83aa59f4dd | ||
|
|
3745a0b81d | ||
|
|
e2b093abce | ||
|
|
7e29ed1cc7 | ||
|
|
5965f0fd22 | ||
|
|
c3c46f2f74 | ||
|
|
a816aa0186 | ||
|
|
a7bad20eae | ||
|
|
089b6b1644 | ||
|
|
7509520883 | ||
|
|
9778775046 | ||
|
|
e5ae0572c5 | ||
|
|
63dac3bd9f | ||
|
|
19295eef46 | ||
|
|
0bc1369e04 | ||
|
|
ca2f7d2eed | ||
|
|
3ea535a412 | ||
|
|
a288b9df8e | ||
|
|
8b6012f8a8 | ||
|
|
1e5d7582da | ||
|
|
ad838b4827 | ||
|
|
25f603efc9 | ||
|
|
152c927929 | ||
|
|
b5bd16c6a9 | ||
|
|
14bfa83bb8 | ||
|
|
5dc8086968 | ||
|
|
7118340f12 | ||
|
|
efe7f2e4c1 | ||
|
|
fb2fc2335a | ||
|
|
345e116699 | ||
|
|
e50a1c0646 | ||
|
|
a6fd1aa06c | ||
|
|
3cdc58200a | ||
|
|
933dd81d82 | ||
|
|
a1434524a4 | ||
|
|
cdc6afefe2 | ||
|
|
738ec2a38e | ||
|
|
07886a9722 | ||
|
|
fde1b49729 | ||
|
|
58210141f4 | ||
|
|
e1f35102aa | ||
|
|
af2a8548c3 | ||
|
|
1faedd3870 | ||
|
|
f57a4171ba | ||
|
|
df9dc82de5 | ||
|
|
0573f09285 | ||
|
|
eb6fe11da1 | ||
|
|
823025f3b3 | ||
|
|
0ee3f0a5b5 | ||
|
|
6bff564129 | ||
|
|
7dabc92447 | ||
|
|
78bbe8855b | ||
|
|
5354104128 | ||
|
|
a5118e4daa | ||
|
|
b5ade5dc12 | ||
|
|
fff8c72f42 | ||
|
|
06ca5f14fc | ||
|
|
8c9d871268 | ||
|
|
5951ead1b6 | ||
|
|
4a0a0e10d2 | ||
|
|
7266eeee7a | ||
|
|
5839ce3e1a | ||
|
|
f85c6bf828 | ||
|
|
9af9e21e05 | ||
|
|
6d16ad2763 | ||
|
|
447fba1594 | ||
|
|
93f845610d | ||
|
|
aa8525385a | ||
|
|
ec783133c1 | ||
|
|
f630bddb19 | ||
|
|
71f0aab2c6 | ||
|
|
ae8de67271 | ||
|
|
6142998b87 | ||
|
|
142376f360 | ||
|
|
ae329e735f | ||
|
|
2dbab70c8c | ||
|
|
47916ecb0f | ||
|
|
635f669a32 | ||
|
|
5f49772ca6 | ||
|
|
6332552346 | ||
|
|
4fb7afcf7b | ||
|
|
ff448950ed | ||
|
|
d6319aeb92 | ||
|
|
8fc6f9c442 | ||
|
|
fdcf317963 | ||
|
|
e9ea79cc8e | ||
|
|
ad32555cc9 | ||
|
|
be8a1acca9 | ||
|
|
477615fc66 | ||
|
|
e0ed8fa0b8 | ||
|
|
dcbf285fae | ||
|
|
ceda459942 | ||
|
|
28fe44aea4 | ||
|
|
71510a8199 | ||
|
|
9cd121c8f6 | ||
|
|
03f14475ff | ||
|
|
2f105ed0a4 | ||
|
|
b4350ec77b | ||
|
|
0fa6eb4e31 | ||
|
|
704d54dd01 | ||
|
|
bee9b61831 | ||
|
|
9c6ce08db0 | ||
|
|
6c0de89de8 | ||
|
|
aa99e202b4 | ||
|
|
04728ea4bc | ||
|
|
6865cdfce3 | ||
|
|
d09b36b1d5 | ||
|
|
a5dd4f5fac | ||
|
|
9936b2967e | ||
|
|
0971613ac0 | ||
|
|
98f9bc3fc1 | ||
|
|
f5f2da11d1 | ||
|
|
f7d855c59e | ||
|
|
b6dba2cbe9 | ||
|
|
2fe743649d | ||
|
|
a015237fd2 | ||
|
|
e1f4deaacc | ||
|
|
1dceee3fa0 | ||
|
|
3a17a7c98f | ||
|
|
3298db8683 | ||
|
|
d4d9403829 | ||
|
|
426e6923ac | ||
|
|
b413f665d8 | ||
|
|
5e1a572bd8 | ||
|
|
3036a149bb | ||
|
|
0dcb4b645c | ||
|
|
d71082b491 | ||
|
|
fc4c8d6054 | ||
|
|
1a27c7eabc | ||
|
|
b2e9602aba | ||
|
|
408b959441 | ||
|
|
35fc98a188 | ||
|
|
92f6a79e1f | ||
|
|
5a9c8e304c | ||
|
|
8f94ed68a3 | ||
|
|
a4cd30451c | ||
|
|
ce1a4b94cb | ||
|
|
92a0deffcf | ||
|
|
97b6f6028b | ||
|
|
99e300a640 | ||
|
|
611e8c39ee | ||
|
|
af55287dee | ||
|
|
01f1c263a6 | ||
|
|
24ea6a9f1d | ||
|
|
46c800b8b7 | ||
|
|
f10be893ce | ||
|
|
c606c1664f | ||
|
|
90a938fe2b | ||
|
|
225932b4c9 | ||
|
|
6b850b9894 | ||
|
|
d8ef796a46 | ||
|
|
943a5d1036 | ||
|
|
e600e9c583 | ||
|
|
b25da8ceaa | ||
|
|
f7388f6492 | ||
|
|
10cdb7387d | ||
|
|
dd436dd1dd | ||
|
|
13b84635b5 | ||
|
|
144d1ea280 | ||
|
|
27534d408f | ||
|
|
b80a2921aa | ||
|
|
0a23ce870a | ||
|
|
ba1e100b42 | ||
|
|
2bc237468d | ||
|
|
c355ef8d53 | ||
|
|
3052d88164 | ||
|
|
1feb23ba99 | ||
|
|
767f650e6f | ||
|
|
59a0fd0799 | ||
|
|
00b5cfcf17 | ||
|
|
f7217944e7 | ||
|
|
9e342f253d | ||
|
|
20c48b6bb2 | ||
|
|
572004d37a | ||
|
|
4f9e7bb572 | ||
|
|
df9b692a32 | ||
|
|
0a322d5a9f | ||
|
|
59eb6d10c1 | ||
|
|
affb219d72 | ||
|
|
a57ce1dd17 | ||
|
|
4e7ace3b48 | ||
|
|
b100b3c36b | ||
|
|
b64b27e5ea | ||
|
|
81c1ba6eef | ||
|
|
46b981bb2f | ||
|
|
93ae4d1ee6 | ||
|
|
9c53d818f4 | ||
|
|
efb2a1d913 | ||
|
|
e5a2c1ab10 | ||
|
|
550c0c7625 | ||
|
|
0abc269a3e | ||
|
|
6b3cbe7882 | ||
|
|
67ed035b36 | ||
|
|
ca25105f13 | ||
|
|
ad30786f4a | ||
|
|
ff0bdcd0c2 | ||
|
|
2e01da5ec1 | ||
|
|
8fb37a0024 | ||
|
|
9412a7424c |
1396
.editorconfig
Normal file
1396
.editorconfig
Normal file
File diff suppressed because it is too large
Load Diff
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright 2021 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
custom: https://signal.org/donate/
|
||||
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Service CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -9,8 +9,16 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
- name: Cache local Maven repository
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
- name: Build with Maven
|
||||
run: mvn -e -B package
|
||||
run: mvn -e -B verify
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -9,10 +9,13 @@ config/production.yml
|
||||
config/federated.yml
|
||||
config/staging.yml
|
||||
config/testing.yml
|
||||
service/config/production.yml
|
||||
service/config/federated.yml
|
||||
service/config/staging.yml
|
||||
service/config/testing.yml
|
||||
config/deploy.properties
|
||||
/service/config/production.yml
|
||||
/service/config/federated.yml
|
||||
/service/config/staging.yml
|
||||
/service/config/testing.yml
|
||||
/service/config/deploy.properties
|
||||
/service/dependency-reduced-pom.xml
|
||||
.opsmanage
|
||||
put.sh
|
||||
deployer-staging.properties
|
||||
|
||||
9
.mvn/extensions.xml
Normal file
9
.mvn/extensions.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
|
||||
<extension>
|
||||
<groupId>fr.brouillard.oss</groupId>
|
||||
<artifactId>jgitver-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
14
.mvn/jgitver.config.xml
Normal file
14
.mvn/jgitver.config.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<configuration xmlns="http://jgitver.github.io/maven/configuration/1.1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://jgitver.github.io/maven/configuration/1.1.0 https://jgitver.github.io/maven/configuration/jgitver-configuration-v1_1_0.xsd">
|
||||
<useDirty>true</useDirty>
|
||||
<useDefaultBranchingPolicy>false</useDefaultBranchingPolicy>
|
||||
<branchPolicies>
|
||||
<branchPolicy>
|
||||
<pattern>(.*)</pattern>
|
||||
<transformations>
|
||||
<transformation>IGNORE</transformation>
|
||||
</transformations>
|
||||
</branchPolicy>
|
||||
</branchPolicies>
|
||||
</configuration>
|
||||
@@ -21,6 +21,6 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
License
|
||||
---------------------
|
||||
|
||||
Copyright 2013-2020 Signal Messenger, LLC
|
||||
Copyright 2013-2021 Signal Messenger, LLC
|
||||
|
||||
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
@@ -1,47 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>gcm-sender-async</artifactId>
|
||||
<version>${TextSecureServer.version}</version>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-retry</artifactId>
|
||||
<version>${resilience4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk16</artifactId>
|
||||
<version>1.46</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>gcm-sender-async</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-retry</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-nop</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
|
||||
@@ -4,31 +4,41 @@
|
||||
*/
|
||||
package org.whispersystems.gcm.server;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.any;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.whispersystems.gcm.server.util.FixtureHelpers.fixture;
|
||||
import static org.whispersystems.gcm.server.util.JsonHelpers.jsonFixture;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.squareup.okhttp.mockwebserver.MockResponse;
|
||||
import com.squareup.okhttp.mockwebserver.RecordedRequest;
|
||||
import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.github.tomakehurst.wiremock.client.CountMatchingStrategy;
|
||||
import com.github.tomakehurst.wiremock.junit.WireMockRule;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.whispersystems.gcm.server.util.FixtureHelpers.fixture;
|
||||
import static org.whispersystems.gcm.server.util.JsonHelpers.jsonFixture;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SenderTest {
|
||||
|
||||
@Rule
|
||||
public MockWebServerRule server = new MockWebServerRule();
|
||||
public WireMockRule wireMock = new WireMockRule(options().dynamicPort().dynamicHttpsPort());
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@@ -40,11 +50,13 @@ public class SenderTest {
|
||||
|
||||
@Test
|
||||
public void testSuccess() throws InterruptedException, ExecutionException, TimeoutException, IOException {
|
||||
MockResponse successResponse = new MockResponse().setResponseCode(200)
|
||||
.setBody(fixture("fixtures/response-success.json"));
|
||||
server.enqueue(successResponse);
|
||||
wireMock.stubFor(any(anyUrl())
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withBody(fixture("fixtures/response-success.json"))));
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper, 10, server.getUrl("/gcm/send").toExternalForm());
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper, 10, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
CompletableFuture<Result> future = sender.send(Message.newBuilder().withDestination("1").build());
|
||||
|
||||
Result result = future.get(10, TimeUnit.SECONDS);
|
||||
@@ -56,20 +68,19 @@ public class SenderTest {
|
||||
assertNull(result.getError());
|
||||
assertNull(result.getCanonicalRegistrationId());
|
||||
|
||||
RecordedRequest request = server.takeRequest();
|
||||
assertEquals(request.getPath(), "/gcm/send");
|
||||
assertEquals(new String(request.getBody()), jsonFixture("fixtures/message-minimal.json"));
|
||||
assertEquals(request.getHeader("Authorization"), "key=foobarbaz");
|
||||
assertEquals(request.getHeader("Content-Type"), "application/json");
|
||||
assertEquals(server.getRequestCount(), 1);
|
||||
verify(1, postRequestedFor(urlEqualTo("/gcm/send"))
|
||||
.withHeader("Authorization", equalTo("key=foobarbaz"))
|
||||
.withHeader("Content-Type", equalTo("application/json"))
|
||||
.withRequestBody(equalTo(jsonFixture("fixtures/message-minimal.json"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadApiKey() throws InterruptedException, TimeoutException {
|
||||
MockResponse unauthorizedResponse = new MockResponse().setResponseCode(401);
|
||||
server.enqueue(unauthorizedResponse);
|
||||
wireMock.stubFor(any(anyUrl())
|
||||
.willReturn(aResponse()
|
||||
.withStatus(401)));
|
||||
|
||||
Sender sender = new Sender("foobar", mapper, 10, server.getUrl("/gcm/send").toExternalForm());
|
||||
Sender sender = new Sender("foobar", mapper, 10, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
CompletableFuture<Result> future = sender.send(Message.newBuilder().withDestination("1").build());
|
||||
|
||||
try {
|
||||
@@ -79,15 +90,16 @@ public class SenderTest {
|
||||
assertTrue(ee.getCause() instanceof AuthenticationFailedException);
|
||||
}
|
||||
|
||||
assertEquals(server.getRequestCount(), 1);
|
||||
verify(1, anyRequestedFor(anyUrl()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRequest() throws TimeoutException, InterruptedException {
|
||||
MockResponse malformed = new MockResponse().setResponseCode(400);
|
||||
server.enqueue(malformed);
|
||||
wireMock.stubFor(any(anyUrl())
|
||||
.willReturn(aResponse()
|
||||
.withStatus(400)));
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper, 10, server.getUrl("/gcm/send").toExternalForm());
|
||||
Sender sender = new Sender("foobarbaz", mapper, 10, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
CompletableFuture<Result> future = sender.send(Message.newBuilder().withDestination("1").build());
|
||||
|
||||
try {
|
||||
@@ -97,17 +109,16 @@ public class SenderTest {
|
||||
assertTrue(e.getCause() instanceof InvalidRequestException);
|
||||
}
|
||||
|
||||
assertEquals(server.getRequestCount(), 1);
|
||||
verify(1, anyRequestedFor(anyUrl()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerError() throws TimeoutException, InterruptedException {
|
||||
MockResponse error = new MockResponse().setResponseCode(503);
|
||||
server.enqueue(error);
|
||||
server.enqueue(error);
|
||||
server.enqueue(error);
|
||||
wireMock.stubFor(any(anyUrl())
|
||||
.willReturn(aResponse()
|
||||
.withStatus(503)));
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper, 3, server.getUrl("/gcm/send").toExternalForm());
|
||||
Sender sender = new Sender("foobarbaz", mapper, 3, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
CompletableFuture<Result> future = sender.send(Message.newBuilder().withDestination("1").build());
|
||||
|
||||
try {
|
||||
@@ -117,27 +128,29 @@ public class SenderTest {
|
||||
assertTrue(ee.getCause() instanceof ServerFailedException);
|
||||
}
|
||||
|
||||
assertEquals(server.getRequestCount(), 3);
|
||||
verify(3, anyRequestedFor(anyUrl()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerErrorRecovery() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
MockResponse success = new MockResponse().setResponseCode(200)
|
||||
.setBody(fixture("fixtures/response-success.json"));
|
||||
|
||||
MockResponse error = new MockResponse().setResponseCode(503);
|
||||
wireMock.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(503)));
|
||||
|
||||
server.enqueue(error);
|
||||
server.enqueue(error);
|
||||
server.enqueue(error);
|
||||
server.enqueue(success);
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper, 4, server.getUrl("/gcm/send").toExternalForm());
|
||||
Sender sender = new Sender("foobarbaz", mapper, 4, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
CompletableFuture<Result> future = sender.send(Message.newBuilder().withDestination("1").build());
|
||||
|
||||
// up to three failures can happen, with 100ms exponential backoff
|
||||
// if we end up using the fourth, and finaly try, it would be after ~700 ms
|
||||
CompletableFuture.delayedExecutor(300, TimeUnit.MILLISECONDS).execute(() ->
|
||||
wireMock.stubFor(any(anyUrl())
|
||||
.willReturn(aResponse()
|
||||
.withStatus(200)
|
||||
.withBody(fixture("fixtures/response-success.json"))))
|
||||
);
|
||||
|
||||
Result result = future.get(10, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals(server.getRequestCount(), 4);
|
||||
verify(new CountMatchingStrategy(CountMatchingStrategy.GREATER_THAN, 1), anyRequestedFor(anyUrl()));
|
||||
assertTrue(result.isSuccess());
|
||||
assertFalse(result.isThrottled());
|
||||
assertFalse(result.isUnregistered());
|
||||
@@ -147,17 +160,14 @@ public class SenderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNetworkError() throws TimeoutException, InterruptedException, IOException {
|
||||
MockResponse response = new MockResponse().setResponseCode(200)
|
||||
.setBody(fixture("fixtures/response-success.json"));
|
||||
public void testNetworkError() throws TimeoutException, InterruptedException {
|
||||
|
||||
server.enqueue(response);
|
||||
server.enqueue(response);
|
||||
server.enqueue(response);
|
||||
wireMock.stubFor(any(anyUrl())
|
||||
.willReturn(ok()));
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper ,2, server.getUrl("/gcm/send").toExternalForm());
|
||||
Sender sender = new Sender("foobarbaz", mapper ,2, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
|
||||
server.get().shutdown();
|
||||
wireMock.stop();
|
||||
|
||||
CompletableFuture<Result> future = sender.send(Message.newBuilder().withDestination("1").build());
|
||||
|
||||
@@ -170,12 +180,11 @@ public class SenderTest {
|
||||
|
||||
@Test
|
||||
public void testNotRegistered() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
MockResponse response = new MockResponse().setResponseCode(200)
|
||||
.setBody(fixture("fixtures/response-not-registered.json"));
|
||||
|
||||
server.enqueue(response);
|
||||
wireMock.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200)
|
||||
.withBody(fixture("fixtures/response-not-registered.json"))));
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper,2, server.getUrl("/gcm/send").toExternalForm());
|
||||
Sender sender = new Sender("foobarbaz", mapper,2, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
CompletableFuture<Result> future = sender.send(Message.newBuilder()
|
||||
.withDestination("2")
|
||||
.withDataPart("message", "new message!")
|
||||
|
||||
@@ -4,30 +4,34 @@
|
||||
*/
|
||||
package org.whispersystems.gcm.server;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.whispersystems.gcm.server.util.FixtureHelpers.fixture;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||
import com.github.tomakehurst.wiremock.junit.WireMockRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.whispersystems.gcm.server.util.FixtureHelpers.fixture;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SimultaneousSenderTest {
|
||||
|
||||
@Rule
|
||||
public WireMockRule wireMock = new WireMockRule(8089);
|
||||
public WireMockRule wireMock = new WireMockRule(WireMockConfiguration.options().dynamicPort().dynamicHttpsPort());
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@@ -44,7 +48,7 @@ public class SimultaneousSenderTest {
|
||||
.withStatus(200)
|
||||
.withBody(fixture("fixtures/response-success.json"))));
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper, 2, "http://localhost:8089/gcm/send");
|
||||
Sender sender = new Sender("foobarbaz", mapper, 2, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
List<CompletableFuture<Result>> results = new LinkedList<>();
|
||||
|
||||
for (int i=0;i<1000;i++) {
|
||||
@@ -61,12 +65,13 @@ public class SimultaneousSenderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testSimultaneousFailure() throws TimeoutException, InterruptedException {
|
||||
stubFor(post(urlPathEqualTo("/gcm/send"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(503)));
|
||||
|
||||
Sender sender = new Sender("foobarbaz", mapper, 2, "http://localhost:8089/gcm/send");
|
||||
Sender sender = new Sender("foobarbaz", mapper, 2, "http://localhost:" + wireMock.port() + "/gcm/send");
|
||||
List<CompletableFuture<Result>> futures = new LinkedList<>();
|
||||
|
||||
for (int i=0;i<1000;i++) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!-- Turning down the wiremock logging -->
|
||||
<logger name="com.github.tomakehurst.wiremock" level="WARN"/>
|
||||
<logger name="wiremock.org" level="ERROR"/>
|
||||
<logger name="WireMock" level="WARN"/>
|
||||
<!-- wiremock has per endpoint servlet logging -->
|
||||
<logger name="/" level="WARN"/>
|
||||
<configuration>
|
||||
<!-- Turning down the wiremock logging -->
|
||||
<logger name="com.github.tomakehurst.wiremock" level="WARN"/>
|
||||
<logger name="wiremock.org" level="ERROR"/>
|
||||
<logger name="WireMock" level="WARN"/>
|
||||
<!-- wiremock has per endpoint servlet logging -->
|
||||
<logger name="/" level="WARN"/>
|
||||
</configuration>
|
||||
|
||||
531
pom.xml
531
pom.xml
@@ -1,166 +1,397 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>pom</packaging>
|
||||
<prerequisites>
|
||||
<maven>3.0.0</maven>
|
||||
</prerequisites>
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>redis-dispatch</module>
|
||||
<module>websocket-resources</module>
|
||||
<module>gcm-sender-async</module>
|
||||
<module>service</module>
|
||||
</modules>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<name>Central Repository</name>
|
||||
<url>https://repo.maven.apache.org/maven2</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>dynamodb-local-oregon</id>
|
||||
<name>DynamoDB Local Release Repository</name>
|
||||
<url>https://s3-us-west-2.amazonaws.com/dynamodb-local/release</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<properties>
|
||||
<dropwizard.version>2.0.13</dropwizard.version>
|
||||
<resilience4j.version>1.5.0</resilience4j.version>
|
||||
<mockito.version>2.25.1</mockito.version>
|
||||
<modules>
|
||||
<module>redis-dispatch</module>
|
||||
<module>websocket-resources</module>
|
||||
<module>gcm-sender-async</module>
|
||||
<module>service</module>
|
||||
</modules>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<TextSecureServer.version>4.93</TextSecureServer.version>
|
||||
</properties>
|
||||
<properties>
|
||||
<aws.sdk.version>1.11.939</aws.sdk.version>
|
||||
<aws.sdk2.version>2.16.66</aws.sdk2.version>
|
||||
<commons-codec.version>1.15</commons-codec.version>
|
||||
<commons-csv.version>1.8</commons-csv.version>
|
||||
<commons-io.version>2.9.0</commons-io.version>
|
||||
<dropwizard.version>2.0.22</dropwizard.version>
|
||||
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
|
||||
<guava.version>30.1.1-jre</guava.version>
|
||||
<jaxb.version>2.3.1</jaxb.version>
|
||||
<jedis.version>2.9.0</jedis.version>
|
||||
<lettuce.version>6.0.4.RELEASE</lettuce.version>
|
||||
<libphonenumber.version>8.12.23</libphonenumber.version>
|
||||
<logstash.logback.version>6.6</logstash.logback.version>
|
||||
<micrometer.version>1.5.3</micrometer.version>
|
||||
<mockito.version>3.11.1</mockito.version>
|
||||
<netty.version>4.1.65.Final</netty.version>
|
||||
<netty.tcnative-boringssl-static.version>2.0.39.Final</netty.tcnative-boringssl-static.version>
|
||||
<opentest4j.version>1.2.0</opentest4j.version>
|
||||
<postgresql.version>9.4-1201-jdbc41</postgresql.version>
|
||||
<protobuf.version>3.17.1</protobuf.version>
|
||||
<pushy.version>0.14.2</pushy.version>
|
||||
<resilience4j.version>1.5.0</resilience4j.version>
|
||||
<semver4j.version>3.1.0</semver4j.version>
|
||||
<slf4j.version>1.7.30</slf4j.version>
|
||||
|
||||
<parent>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<version>JGITVER</version>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-dependencies</artifactId>
|
||||
<version>2.0.13</version>
|
||||
</parent>
|
||||
<version>${dropwizard.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-bom</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-bom</artifactId>
|
||||
<version>${aws.sdk.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>bom</artifactId>
|
||||
<version>${aws.sdk2.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-bom</artifactId>
|
||||
<version>${resilience4j.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-bom</artifactId>
|
||||
<version>${micrometer.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<version>1.0</version>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-jdbi3</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>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>2.3.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>2.3.1</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-servlets</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-all</artifactId>
|
||||
<version>1.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>2.26.2</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.15.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.eatthepath</groupId>
|
||||
<artifactId>pushy</artifactId>
|
||||
<version>${pushy.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.eatthepath</groupId>
|
||||
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
||||
<version>${pushy.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.libphonenumber</groupId>
|
||||
<artifactId>libphonenumber</artifactId>
|
||||
<version>${libphonenumber.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vdurmont</groupId>
|
||||
<artifactId>semver4j</artifactId>
|
||||
<version>${semver4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>${commons-codec.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
<version>${lettuce.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
<version>${netty.tcnative-boringssl-static.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.logstash.logback</groupId>
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
<version>${logstash.logback.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-csv</artifactId>
|
||||
<version>${commons-csv.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.coursera</groupId>
|
||||
<artifactId>dropwizard-metrics-datadog</artifactId>
|
||||
<version>${dropwizard-metrics-datadog.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>${jaxb.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.opentest4j</groupId>
|
||||
<artifactId>opentest4j</artifactId>
|
||||
<version>${opentest4j.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgresql.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-nop</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>${jedis.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-all</artifactId>
|
||||
<version>1.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>2.28.1</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
</plugin>
|
||||
</dependencies>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<configuration>
|
||||
<rules><dependencyConvergence/></rules>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.7.0</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:3.17.2:exe:${os.detected.classifier}</protocArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeScope>test</includeScope>
|
||||
<includeTypes>so,dll,dylib</includeTypes>
|
||||
<outputDirectory>${project.build.directory}/lib</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<systemProperties>
|
||||
<property>
|
||||
<name>sqlite4java.library.path</name>
|
||||
<value>${project.build.directory}/lib</value>
|
||||
</property>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>3.0.0-M3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<dependencyConvergence/>
|
||||
<requireMavenVersion>
|
||||
<version>3.0.0</version>
|
||||
</requireMavenVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>redis-dispatch</artifactId>
|
||||
|
||||
<artifactId>redis-dispatch</artifactId>
|
||||
<version>${TextSecureServer.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
package org.whispersystems.dispatch;
|
||||
|
||||
public interface DispatchChannel {
|
||||
public void onDispatchMessage(String channel, byte[] message);
|
||||
public void onDispatchSubscribed(String channel);
|
||||
public void onDispatchUnsubscribed(String channel);
|
||||
void onDispatchMessage(String channel, byte[] message);
|
||||
void onDispatchSubscribed(String channel);
|
||||
void onDispatchUnsubscribed(String channel);
|
||||
}
|
||||
|
||||
@@ -59,9 +59,7 @@ public class DispatchManager extends Thread {
|
||||
logger.warn("Subscription error", e);
|
||||
}
|
||||
|
||||
if (previous.isPresent()) {
|
||||
dispatchUnsubscription(name, previous.get());
|
||||
}
|
||||
previous.ifPresent(channel -> dispatchUnsubscription(name, channel));
|
||||
}
|
||||
|
||||
public synchronized void unsubscribe(String name, DispatchChannel channel) {
|
||||
@@ -132,46 +130,28 @@ public class DispatchManager extends Thread {
|
||||
}
|
||||
|
||||
private void resubscribeAll() {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (DispatchManager.this) {
|
||||
try {
|
||||
for (String name : subscriptions.keySet()) {
|
||||
pubSubConnection.subscribe(name);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
|
||||
new Thread(() -> {
|
||||
synchronized (DispatchManager.this) {
|
||||
try {
|
||||
for (String name : subscriptions.keySet()) {
|
||||
pubSubConnection.subscribe(name);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void dispatchMessage(final String name, final DispatchChannel channel, final byte[] message) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
channel.onDispatchMessage(name, message);
|
||||
}
|
||||
});
|
||||
executor.execute(() -> channel.onDispatchMessage(name, message));
|
||||
}
|
||||
|
||||
private void dispatchSubscription(final String name, final DispatchChannel channel) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
channel.onDispatchSubscribed(name);
|
||||
}
|
||||
});
|
||||
executor.execute(() -> channel.onDispatchSubscribed(name));
|
||||
}
|
||||
|
||||
private void dispatchUnsubscription(final String name, final DispatchChannel channel) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
channel.onDispatchUnsubscribed(name);
|
||||
}
|
||||
});
|
||||
executor.execute(() -> channel.onDispatchUnsubscribed(name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
|
||||
public interface RedisPubSubConnectionFactory {
|
||||
|
||||
public PubSubConnection connect();
|
||||
PubSubConnection connect();
|
||||
|
||||
}
|
||||
|
||||
@@ -4,21 +4,24 @@
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
public class PubSubConnectionTest {
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
|
||||
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
|
||||
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
|
||||
<id>bin</id>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<formats>
|
||||
@@ -18,8 +18,8 @@
|
||||
<directory>${project.build.directory}</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<includes>
|
||||
<include>${parent.artifactId}-${TextSecureServer.version}.jar</include>
|
||||
<include>${parent.artifactId}-${project.version}.jar</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
</assembly>
|
||||
|
||||
@@ -1,42 +1,26 @@
|
||||
twilio: # Twilio gateway configuration
|
||||
accountId:
|
||||
accountToken:
|
||||
numbers: # Numbers allocated in Twilio
|
||||
- # First number
|
||||
- # Second number
|
||||
- # Third number
|
||||
- # ...
|
||||
- # Nth number
|
||||
nanpaMessagingServiceSid: # Twilio SID for the messaging service to use for NANPA.
|
||||
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
|
||||
verifyServiceSid: # Twilio SID for a Verify service
|
||||
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
|
||||
iosVerificationText: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||
androidNgVerificationText: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202001VerificationText: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
genericVerificationText: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||
senderId:
|
||||
defaultSenderId: # Sender ID to use for country codes not found in either the overrides or omitted lists.
|
||||
countryCodesWithoutSenderId:
|
||||
- # First country code
|
||||
- # Second country code
|
||||
- # ...
|
||||
- # Nth country code
|
||||
countrySpecificSenderIds:
|
||||
- countryCode: # First country code
|
||||
senderId: # Sender ID to use for this country
|
||||
- countryCode: # Second country code
|
||||
senderId: # Sender ID to use for this country
|
||||
- countryCode: # ...
|
||||
senderId: # ...
|
||||
- countryCode: # Nth country code
|
||||
senderId: # Sender ID to use for this country
|
||||
defaultClientVerificationTexts:
|
||||
ios: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||
androidNg: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202001: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202103: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
generic: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||
regionalClientVerificationTexts: # Map of country codes to custom texts
|
||||
999: # example country code
|
||||
ios:
|
||||
# … all keys from defaultClientVerificationTexts are required
|
||||
androidAppHash: # Hash appended to Android
|
||||
verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable
|
||||
|
||||
push:
|
||||
queueSize: # Size of push pending queue
|
||||
|
||||
redphone:
|
||||
authKey: # Deprecated
|
||||
|
||||
turn: # TURN server configuration
|
||||
secret: # TURN server secret
|
||||
uris:
|
||||
@@ -49,23 +33,37 @@ cacheCluster: # Redis server configuration for cache cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
clientPresenceCluster: # Redis server configuration for client presence cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
pubsub: # Redis server configuration for pubsub cluster
|
||||
url: redis://redis.example.com:6379/
|
||||
replicaUrls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
rateLimitersCluster: # Redis server configuration for rate limiters cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
directory:
|
||||
redis: # Redis server configuration for directory cluster
|
||||
url:
|
||||
replicaUrls:
|
||||
client: # Configuration for interfacing with Contact Discovery Service cluster
|
||||
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
|
||||
userAuthenticationTokenUserIdSecret: # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
|
||||
sqs:
|
||||
accessKey: # AWS SQS accessKey
|
||||
accessSecret: # AWS SQS accessSecret
|
||||
queueUrl: # AWS SQS queue url
|
||||
server:
|
||||
replicationUrl: # CDS replication endpoint base url
|
||||
replicationPassword: # CDS replication endpoint password
|
||||
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
|
||||
reconciliationChunkSize: # CDS reconciliation chunk size
|
||||
reconciliationChunkIntervalMs: # CDS reconciliation chunk interval, in milliseconds
|
||||
queueUrls: # AWS SQS queue urls
|
||||
- https://sqs.example.com/directory.fifo
|
||||
server: # One or more CDS servers
|
||||
- replicationName: # CDS replication name
|
||||
replicationUrl: # CDS replication endpoint base url
|
||||
replicationPassword: # CDS replication endpoint password
|
||||
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes:
|
||||
@@ -74,16 +72,44 @@ messageCache: # Redis server configuration for message store cache
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
messageStore: # Postgresql database configuration for message store
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
|
||||
metricsCluster:
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
messageDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
keysDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
accountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
phoneNumberTableName:
|
||||
|
||||
deletedAccountsDynamoDb: # DynamoDb table configuration
|
||||
region:
|
||||
tableName:
|
||||
needsReconciliationIndexName:
|
||||
|
||||
migrationDeletedAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
migrationRetryAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
pushChallengeDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
reportMessageDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
awsAttachments: # AWS S3 configuration
|
||||
accessKey:
|
||||
accessSecret:
|
||||
@@ -97,18 +123,22 @@ gcpAttachments: # GCP Storage configuration
|
||||
pathPrefix:
|
||||
rsaSigningKey:
|
||||
|
||||
profiles: # AWS S3 configuration
|
||||
accessKey:
|
||||
accessSecret:
|
||||
bucket:
|
||||
region:
|
||||
|
||||
database: # Postgresql database configuration
|
||||
abuseDatabase: # Postgresql database configuration
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
|
||||
accountsDatabase: # Postgresql database configuration
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
|
||||
accountDatabaseCrawler:
|
||||
chunkSize: # accounts per run
|
||||
chunkIntervalMs: # time per run
|
||||
|
||||
apn: # Apple Push Notifications configuration
|
||||
sandbox: true
|
||||
bundleId:
|
||||
@@ -120,11 +150,52 @@ gcm: # GCM Configuration
|
||||
senderId:
|
||||
apiKey:
|
||||
|
||||
micrometer: # Micrometer metrics config
|
||||
- name: "example"
|
||||
- uri: "https://metrics.example.com/"
|
||||
- apiKey:
|
||||
- accountId:
|
||||
cdn:
|
||||
accessKey: # AWS Access Key ID
|
||||
accessSecret: # AWS Access Secret
|
||||
bucket: # S3 Bucket name
|
||||
region: # AWS region
|
||||
|
||||
wavefront: # Wavefront micrometer metrics config
|
||||
uri: # Wavefront proxy endpoint
|
||||
batchSize: # Number of measurements to send per request
|
||||
|
||||
datadog:
|
||||
apiKey:
|
||||
environment:
|
||||
|
||||
unidentifiedDelivery:
|
||||
certificate:
|
||||
privateKey:
|
||||
expiresDays:
|
||||
|
||||
voiceVerification:
|
||||
url: https://cdn-ca.signal.org/verification/
|
||||
locales:
|
||||
- en
|
||||
|
||||
recaptcha:
|
||||
secret:
|
||||
|
||||
storageService:
|
||||
uri:
|
||||
userAuthenticationTokenSharedSecret:
|
||||
storageCaCertificate:
|
||||
|
||||
backupService:
|
||||
uri:
|
||||
userAuthenticationTokenSharedSecret:
|
||||
backupCaCertificate:
|
||||
|
||||
zkConfig:
|
||||
serverPublic:
|
||||
serverSecret:
|
||||
enabled:
|
||||
|
||||
appConfig:
|
||||
application:
|
||||
environment:
|
||||
configuration:
|
||||
|
||||
remoteConfig:
|
||||
authorizedTokens:
|
||||
@@ -134,12 +205,35 @@ remoteConfig:
|
||||
- # Nth authorized token
|
||||
globalConfig: # keys and values that are given to clients on GET /v1/config
|
||||
|
||||
featureFlag:
|
||||
authorizedTokens:
|
||||
- # 1st authorized token
|
||||
- # 2nd authorized token
|
||||
- # ...
|
||||
- # Nth authorized token
|
||||
|
||||
paymentService:
|
||||
paymentsService:
|
||||
userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
|
||||
torExitNodeList:
|
||||
s3Region:
|
||||
s3Bucket:
|
||||
objectKey:
|
||||
maxSize:
|
||||
|
||||
asnTable:
|
||||
s3Region:
|
||||
s3Bucket:
|
||||
objectKey:
|
||||
maxSize:
|
||||
|
||||
donation:
|
||||
uri: # value
|
||||
apiKey: # value
|
||||
supportedCurrencies:
|
||||
- # 1st supported currency
|
||||
- # 2nd supported currency
|
||||
- # ...
|
||||
- # Nth supported currency
|
||||
circuitBreaker:
|
||||
failureRateThreshold: # value
|
||||
ringBufferSizeInHalfOpenState: # value
|
||||
ringBufferSizeInClosedState: # value
|
||||
waitDurationInOpenStateInSeconds: # value
|
||||
retry:
|
||||
maxAttempts: # value
|
||||
waitDuration: # value
|
||||
|
||||
799
service/pom.xml
799
service/pom.xml
@@ -1,265 +1,558 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>service</artifactId>
|
||||
|
||||
<artifactId>service</artifactId>
|
||||
<version>${TextSecureServer.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.ws.rs</groupId>
|
||||
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>redis-dispatch</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>websocket-resources</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>gcm-sender-async</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>zkgroup-java</artifactId>
|
||||
<version>0.7.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
<artifactId>curve25519-java</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>redis-dispatch</artifactId>
|
||||
<version>${TextSecureServer.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>websocket-resources</artifactId>
|
||||
<version>${TextSecureServer.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>gcm-sender-async</artifactId>
|
||||
<version>${TextSecureServer.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>zkgroup-java</artifactId>
|
||||
<version>0.7.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-jdbi3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-auth</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-db</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-logging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-metrics</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-util</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-servlets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-lifecycle</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-jersey</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-jetty</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-migrations</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-access</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.logstash.logback</groupId>
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jdbi</groupId>
|
||||
<artifactId>jdbi3-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-jdbi3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-healthchecks</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-annotation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
<artifactId>jersey-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
<artifactId>jersey-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
<artifactId>jersey-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-csv</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-circuitbreaker</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-retry</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-wavefront</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-datadog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.coursera</groupId>
|
||||
<artifactId>dropwizard-metrics-datadog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.jaxrs</groupId>
|
||||
<artifactId>jackson-jaxrs-json-provider</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>sts</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>sqs</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>dynamodb</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>appconfig</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>dynamodb-lock-client</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.eatthepath</groupId>
|
||||
<artifactId>pushy</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.eatthepath</groupId>
|
||||
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.vdurmont</groupId>
|
||||
<artifactId>semver4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.googlecode.libphonenumber</groupId>
|
||||
<artifactId>libphonenumber</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.argparse4j</groupId>
|
||||
<artifactId>argparse4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.test-framework</groupId>
|
||||
<artifactId>jersey-test-framework-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
|
||||
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.opentable.components</groupId>
|
||||
<artifactId>otj-pg-embedded</artifactId>
|
||||
<version>0.13.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.almworks.sqlite4java</groupId>
|
||||
<artifactId>sqlite4java</artifactId>
|
||||
<version>1.0.392</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<version>3.3.16.RELEASE</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>embedded-redis</artifactId>
|
||||
<version>0.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.uuid</groupId>
|
||||
<artifactId>java-uuid-generator</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>DynamoDBLocal</artifactId>
|
||||
<version>1.16.0</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>antlr4-runtime</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>pl.pragmatists</groupId>
|
||||
<artifactId>JUnitParams</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk16</artifactId>
|
||||
<version>1.46</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.syslog4j</groupId>
|
||||
<artifactId>syslog4j</artifactId>
|
||||
<version>0.9.30</version>
|
||||
</dependency>
|
||||
<build>
|
||||
<finalName>${project.parent.artifactId}-${project.version}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||
<phase>package</phase> <!-- bind to the packaging phase -->
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-circuitbreaker</artifactId>
|
||||
<version>${resilience4j.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-retry</artifactId>
|
||||
<version>${resilience4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-wavefront</artifactId>
|
||||
<version>1.5.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
<version>1.11.366</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-sqs</artifactId>
|
||||
<version>1.11.366</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>2.9.0</version>
|
||||
<type>jar</type>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
<version>6.0.1.RELEASE</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>9.4-1201-jdbc41</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
<artifactId>curve25519-java</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>com.eatthepath</groupId>
|
||||
<artifactId>pushy</artifactId>
|
||||
<version>0.14.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.eatthepath</groupId>
|
||||
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
||||
<version>0.14.2</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
<version>2.0.34.Final</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.vdurmont</groupId>
|
||||
<artifactId>semver4j</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.googlecode.libphonenumber</groupId>
|
||||
<artifactId>libphonenumber</artifactId>
|
||||
<version>8.11.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
|
||||
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
|
||||
<version>2.30</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.opentable.components</groupId>
|
||||
<artifactId>otj-pg-embedded</artifactId>
|
||||
<version>0.13.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>embedded-redis</artifactId>
|
||||
<version>0.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.uuid</groupId>
|
||||
<artifactId>java-uuid-generator</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>pl.pragmatists</groupId>
|
||||
<artifactId>JUnitParams</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<build>
|
||||
<finalName>${parent.artifactId}-${TextSecureServer.version}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.4</version>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||
<phase>package</phase> <!-- bind to the packaging phase -->
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>read-deploy-configuration</id>
|
||||
<phase>deploy</phase>
|
||||
<goals>
|
||||
<goal>read-project-properties</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<files>${project.basedir}/config/deploy.properties</files>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.bazaarvoice.maven.plugins</groupId>
|
||||
<artifactId>s3-upload-maven-plugin</artifactId>
|
||||
<version>1.5</version>
|
||||
<configuration>
|
||||
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
|
||||
<bucketName>${deploy.bucketName}</bucketName>
|
||||
<destination>${project.build.finalName}-bin.tar.gz</destination>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>deploy-to-s3</id>
|
||||
<phase>deploy</phase>
|
||||
<goals>
|
||||
<goal>s3-upload</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>templating-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>filter-src</id>
|
||||
<goals>
|
||||
<goal>filter-sources</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/main/java/ TextSecure.proto PubSubMessage.proto
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
public class WhisperServerVersion {
|
||||
|
||||
private static final String VERSION = "${project.version}";
|
||||
|
||||
public static String getServerVersion() {
|
||||
return VERSION;
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,31 @@ package org.whispersystems.textsecuregcm;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.dropwizard.Configuration;
|
||||
import io.dropwizard.client.JerseyClientConfiguration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AccountsDatabaseConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AccountsDynamoDbConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DeletedAccountsDynamoDbConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.FeatureFlagConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MicrometerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MessageDynamoDbConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.WavefrontConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
@@ -29,6 +42,7 @@ import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MonitoredS3ObjectConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
||||
@@ -36,13 +50,6 @@ import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfigura
|
||||
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
|
||||
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** @noinspection MismatchedQueryAndUpdateOfCollection, WeakerAccess */
|
||||
public class WhisperServerConfiguration extends Configuration {
|
||||
|
||||
@@ -74,7 +81,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private MicrometerConfiguration micrometer;
|
||||
private WavefrontConfiguration wavefront;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DatadogConfiguration datadog;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -104,17 +116,77 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisConfiguration pushScheduler;
|
||||
private RedisClusterConfiguration pushSchedulerCluster;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration rateLimitersCluster;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private MessageCacheConfiguration messageCache;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration clientPresenceCluster;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DatabaseConfiguration messageStore;
|
||||
private MessageDynamoDbConfiguration messageDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration keysDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private AccountsDynamoDbConfiguration accountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration migrationDeletedAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration migrationRetryAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DeletedAccountsDynamoDbConfiguration deletedAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration deletedAccountsLockDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration pushChallengeDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration reportMessageDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration pendingAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration pendingDevicesDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@@ -134,7 +206,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DatabaseConfiguration accountsDatabase;
|
||||
private AccountsDatabaseConfiguration accountsDatabase;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@@ -209,7 +281,22 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private FeatureFlagConfiguration featureFlag;
|
||||
private AppConfigConfiguration appConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private MonitoredS3ObjectConfiguration torExitNodeList;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private MonitoredS3ObjectConfiguration asnTable;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DonationConfiguration donation;
|
||||
|
||||
private Map<String, String> transparentDataIndex = new HashMap<>();
|
||||
|
||||
@@ -273,19 +360,51 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return messageCache;
|
||||
}
|
||||
|
||||
public RedisConfiguration getPushScheduler() {
|
||||
return pushScheduler;
|
||||
public RedisClusterConfiguration getClientPresenceClusterConfiguration() {
|
||||
return clientPresenceCluster;
|
||||
}
|
||||
|
||||
public DatabaseConfiguration getMessageStoreConfiguration() {
|
||||
return messageStore;
|
||||
public RedisClusterConfiguration getPushSchedulerCluster() {
|
||||
return pushSchedulerCluster;
|
||||
}
|
||||
|
||||
public RedisClusterConfiguration getRateLimitersCluster() {
|
||||
return rateLimitersCluster;
|
||||
}
|
||||
|
||||
public MessageDynamoDbConfiguration getMessageDynamoDbConfiguration() {
|
||||
return messageDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getKeysDynamoDbConfiguration() {
|
||||
return keysDynamoDb;
|
||||
}
|
||||
|
||||
public AccountsDynamoDbConfiguration getAccountsDynamoDbConfiguration() {
|
||||
return accountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getMigrationDeletedAccountsDynamoDbConfiguration() {
|
||||
return migrationDeletedAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getMigrationRetryAccountsDynamoDbConfiguration() {
|
||||
return migrationRetryAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
|
||||
return deletedAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getDeletedAccountsLockDynamoDbConfiguration() {
|
||||
return deletedAccountsLockDynamoDb;
|
||||
}
|
||||
|
||||
public DatabaseConfiguration getAbuseDatabaseConfiguration() {
|
||||
return abuseDatabase;
|
||||
}
|
||||
|
||||
public DatabaseConfiguration getAccountsDatabaseConfiguration() {
|
||||
public AccountsDatabaseConfiguration getAccountsDatabaseConfiguration() {
|
||||
return accountsDatabase;
|
||||
}
|
||||
|
||||
@@ -309,8 +428,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return cdn;
|
||||
}
|
||||
|
||||
public MicrometerConfiguration getMicrometerConfiguration() {
|
||||
return micrometer;
|
||||
public WavefrontConfiguration getWavefrontConfiguration() {
|
||||
return wavefront;
|
||||
}
|
||||
|
||||
public DatadogConfiguration getDatadogConfiguration() {
|
||||
return datadog;
|
||||
}
|
||||
|
||||
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
||||
@@ -359,7 +482,35 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return remoteConfig;
|
||||
}
|
||||
|
||||
public FeatureFlagConfiguration getFeatureFlagConfiguration() {
|
||||
return featureFlag;
|
||||
public AppConfigConfiguration getAppConfig() {
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPushChallengeDynamoDbConfiguration() {
|
||||
return pushChallengeDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getReportMessageDynamoDbConfiguration() {
|
||||
return reportMessageDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPendingAccountsDynamoDbConfiguration() {
|
||||
return pendingAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPendingDevicesDynamoDbConfiguration() {
|
||||
return pendingDevicesDynamoDb;
|
||||
}
|
||||
|
||||
public MonitoredS3ObjectConfiguration getTorExitNodeListConfiguration() {
|
||||
return torExitNodeList;
|
||||
}
|
||||
|
||||
public MonitoredS3ObjectConfiguration getAsnTableConfiguration() {
|
||||
return asnTable;
|
||||
}
|
||||
|
||||
public DonationConfiguration getDonationConfiguration() {
|
||||
return donation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.amazonaws.ClientConfiguration;
|
||||
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.jdbi3.strategies.DefaultNameStrategy;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
@@ -30,16 +30,38 @@ import io.dropwizard.setup.Bootstrap;
|
||||
import io.dropwizard.setup.Environment;
|
||||
import io.lettuce.core.resource.ClientResources;
|
||||
import io.micrometer.core.instrument.Clock;
|
||||
import io.micrometer.core.instrument.Meter.Id;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import io.micrometer.core.instrument.config.MeterFilter;
|
||||
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
|
||||
import io.micrometer.datadog.DatadogMeterRegistry;
|
||||
import io.micrometer.wavefront.WavefrontConfig;
|
||||
import io.micrometer.wavefront.WavefrontMeterRegistry;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.FilterRegistration;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.jdbi.v3.core.Jdbi;
|
||||
import org.signal.zkgroup.ServerSecretParams;
|
||||
import org.signal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.DispatchManager;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
@@ -53,9 +75,10 @@ import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
|
||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
import org.whispersystems.textsecuregcm.controllers.FeatureFlagsController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DonationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
@@ -67,13 +90,25 @@ import org.whispersystems.textsecuregcm.controllers.SecureBackupController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||
import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController;
|
||||
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
||||
import org.whispersystems.textsecuregcm.currency.FtxClient;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
|
||||
import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.PushChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitResetMetricsManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitChallengeExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RetryLaterExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.metrics.BufferPoolGauges;
|
||||
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge;
|
||||
@@ -81,14 +116,15 @@ import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.GarbageCollectionGauges;
|
||||
import org.whispersystems.textsecuregcm.metrics.MaxFileDescriptorGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsRequestEventListener;
|
||||
import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.OperatingSystemMemoryGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
|
||||
import org.whispersystems.textsecuregcm.metrics.TrafficSource;
|
||||
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisClusterHealthCheck;
|
||||
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
|
||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
@@ -102,8 +138,11 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
|
||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -112,72 +151,71 @@ import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawler;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerCache;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDbMigrator;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ActiveUserCounter;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsDirectoryReconciler;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsTableCrawler;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||
import org.whispersystems.textsecuregcm.storage.FeatureFlags;
|
||||
import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagePersister;
|
||||
import org.whispersystems.textsecuregcm.storage.Messages;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevices;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.MigrationDeletedAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.MigrationRetryAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.MigrationRetryAccountsTableCrawler;
|
||||
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PubSubManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.PushFeedbackProcessor;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationLockVersionCounter;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Usernames;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationCodeStore;
|
||||
import org.whispersystems.textsecuregcm.util.AsnManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
|
||||
import org.whispersystems.textsecuregcm.util.HostnameUtil;
|
||||
import org.whispersystems.textsecuregcm.util.TorExitNodeManager;
|
||||
import org.whispersystems.textsecuregcm.util.logging.LoggingUnhandledExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.DisableRequestLoggingTask;
|
||||
import org.whispersystems.textsecuregcm.workers.EnableRequestLoggingTask;
|
||||
import org.whispersystems.textsecuregcm.workers.GetRedisCommandStatsCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.GetRedisSlowlogCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.SetCrawlerAccelerationTask;
|
||||
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
|
||||
import org.whispersystems.textsecuregcm.workers.VacuumCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ZkParamsCommand;
|
||||
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.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
|
||||
public class WhisperServerService extends Application<WhisperServerConfiguration> {
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
private static final Logger log = LoggerFactory.getLogger(WhisperServerService.class);
|
||||
|
||||
@Override
|
||||
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||
@@ -185,8 +223,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
bootstrap.addCommand(new DeleteUserCommand());
|
||||
bootstrap.addCommand(new CertificateCommand());
|
||||
bootstrap.addCommand(new ZkParamsCommand());
|
||||
bootstrap.addCommand(new GetRedisSlowlogCommand());
|
||||
bootstrap.addCommand(new GetRedisCommandStatsCommand());
|
||||
bootstrap.addCommand(new ServerVersionCommand());
|
||||
bootstrap.addCommand(new CheckDynamicConfigurationCommand());
|
||||
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
|
||||
@Override
|
||||
@@ -196,13 +234,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
});
|
||||
|
||||
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("messagedb", "messagedb.xml") {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
return configuration.getMessageStoreConfiguration();
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("abusedb", "abusedb.xml") {
|
||||
@Override
|
||||
public PooledDataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
@@ -218,11 +249,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
@Override
|
||||
public void run(WhisperServerConfiguration config, Environment environment)
|
||||
throws Exception
|
||||
{
|
||||
throws Exception {
|
||||
|
||||
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
|
||||
|
||||
Metrics.addRegistry(new WavefrontMeterRegistry(new WavefrontConfig() {
|
||||
final DistributionStatisticConfig defaultDistributionStatisticConfig = DistributionStatisticConfig.builder()
|
||||
.percentiles(.75, .95, .99, .999)
|
||||
.build();
|
||||
|
||||
final WavefrontConfig wavefrontConfig = new WavefrontConfig() {
|
||||
@Override
|
||||
public String get(final String key) {
|
||||
return null;
|
||||
@@ -230,141 +265,266 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
@Override
|
||||
public String uri() {
|
||||
return config.getMicrometerConfiguration().getUri();
|
||||
return config.getWavefrontConfiguration().getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchSize() {
|
||||
return config.getMicrometerConfiguration().getBatchSize();
|
||||
return config.getWavefrontConfiguration().getBatchSize();
|
||||
}
|
||||
}, Clock.SYSTEM) {
|
||||
};
|
||||
|
||||
Metrics.addRegistry(new WavefrontMeterRegistry(wavefrontConfig, Clock.SYSTEM) {
|
||||
@Override
|
||||
protected DistributionStatisticConfig defaultHistogramConfig() {
|
||||
return DistributionStatisticConfig.builder()
|
||||
.percentiles(.75, .95, .99, .999)
|
||||
.build()
|
||||
.merge(super.defaultHistogramConfig());
|
||||
return defaultDistributionStatisticConfig.merge(super.defaultHistogramConfig());
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
final DatadogMeterRegistry datadogMeterRegistry = new DatadogMeterRegistry(config.getDatadogConfiguration(), Clock.SYSTEM);
|
||||
|
||||
datadogMeterRegistry.config().commonTags(
|
||||
Tags.of(
|
||||
"service", "chat",
|
||||
"host", HostnameUtil.getLocalHostname(),
|
||||
"version", WhisperServerVersion.getServerVersion(),
|
||||
"env", config.getDatadogConfiguration().getEnvironment()))
|
||||
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.REQUEST_COUNTER_NAME))
|
||||
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.ANDROID_REQUEST_COUNTER_NAME))
|
||||
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.DESKTOP_REQUEST_COUNTER_NAME))
|
||||
.meterFilter(MeterFilter.denyNameStartsWith(MetricsRequestEventListener.IOS_REQUEST_COUNTER_NAME))
|
||||
.meterFilter(new MeterFilter() {
|
||||
@Override
|
||||
public DistributionStatisticConfig configure(final Id id, final DistributionStatisticConfig config) {
|
||||
return defaultDistributionStatisticConfig.merge(config);
|
||||
}
|
||||
});
|
||||
|
||||
Metrics.addRegistry(datadogMeterRegistry);
|
||||
}
|
||||
|
||||
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY);
|
||||
Jdbi accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb");
|
||||
Jdbi messageJdbi = jdbiFactory.build(environment, config.getMessageStoreConfiguration(), "messagedb" );
|
||||
Jdbi abuseJdbi = jdbiFactory.build(environment, config.getAbuseDatabaseConfiguration(), "abusedb" );
|
||||
|
||||
FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("accounts_database", accountJdbi, config.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration());
|
||||
FaultTolerantDatabase messageDatabase = new FaultTolerantDatabase("message_database", messageJdbi, config.getMessageStoreConfiguration().getCircuitBreakerConfiguration());
|
||||
FaultTolerantDatabase abuseDatabase = new FaultTolerantDatabase("abuse_database", abuseJdbi, config.getAbuseDatabaseConfiguration().getCircuitBreakerConfiguration());
|
||||
|
||||
DynamoDbClient messageDynamoDb = DynamoDbFromConfig.client(config.getMessageDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient preKeyDynamoDb = DynamoDbFromConfig.client(config.getKeysDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
// The thread pool core & max sizes are set via dynamic configuration within AccountsDynamoDb
|
||||
ThreadPoolExecutor accountsDynamoDbMigrationThreadPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
|
||||
new LinkedBlockingDeque<>());
|
||||
|
||||
DynamoDbAsyncClient accountsDynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(config.getAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create(),
|
||||
accountsDynamoDbMigrationThreadPool);
|
||||
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient recentlyDeletedAccountsDynamoDb = DynamoDbFromConfig.client(config.getMigrationDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pushChallengeDynamoDbClient = DynamoDbFromConfig.client(config.getPushChallengeDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient reportMessageDynamoDbClient = DynamoDbFromConfig.client(config.getReportMessageDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient migrationRetryAccountsDynamoDb = DynamoDbFromConfig.client(config.getMigrationRetryAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pendingAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getPendingAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pendingDevicesDynamoDbClient = DynamoDbFromConfig.client(config.getPendingDevicesDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
AmazonDynamoDB deletedAccountsLockDynamoDbClient = AmazonDynamoDBClientBuilder.standard()
|
||||
.withRegion(config.getDeletedAccountsLockDynamoDbConfiguration().getRegion())
|
||||
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) config.getDeletedAccountsLockDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
|
||||
.withRequestTimeout((int) config.getDeletedAccountsLockDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
|
||||
.withCredentials(InstanceProfileCredentialsProvider.getInstance())
|
||||
.build();
|
||||
|
||||
DeletedAccounts deletedAccounts = new DeletedAccounts(deletedAccountsDynamoDbClient, config.getDeletedAccountsDynamoDbConfiguration().getTableName(), config.getDeletedAccountsDynamoDbConfiguration().getNeedsReconciliationIndexName());
|
||||
MigrationDeletedAccounts migrationDeletedAccounts = new MigrationDeletedAccounts(recentlyDeletedAccountsDynamoDb, config.getMigrationDeletedAccountsDynamoDbConfiguration().getTableName());
|
||||
MigrationRetryAccounts migrationRetryAccounts = new MigrationRetryAccounts(migrationRetryAccountsDynamoDb, config.getMigrationRetryAccountsDynamoDbConfiguration().getTableName());
|
||||
|
||||
Accounts accounts = new Accounts(accountDatabase);
|
||||
PendingAccounts pendingAccounts = new PendingAccounts(accountDatabase);
|
||||
PendingDevices pendingDevices = new PendingDevices (accountDatabase);
|
||||
AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, accountsDynamoDbAsyncClient, accountsDynamoDbMigrationThreadPool, config.getAccountsDynamoDbConfiguration().getTableName(), config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), migrationDeletedAccounts, migrationRetryAccounts);
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
Keys keys = new Keys(accountDatabase);
|
||||
Messages messages = new Messages(messageDatabase);
|
||||
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeyDynamoDb, config.getKeysDynamoDbConfiguration().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb, config.getMessageDynamoDbConfiguration().getTableName(), config.getMessageDynamoDbConfiguration().getTimeToLive());
|
||||
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
|
||||
RemoteConfigs remoteConfigs = new RemoteConfigs(accountDatabase);
|
||||
FeatureFlags featureFlags = new FeatureFlags(accountDatabase);
|
||||
|
||||
RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
|
||||
RedisClientFactory directoryClientFactory = new RedisClientFactory("directory_cache", config.getDirectoryConfiguration().getRedisConfiguration().getUrl(), config.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls(), config.getDirectoryConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration());
|
||||
RedisClientFactory pushSchedulerClientFactory = new RedisClientFactory("push_scheduler_cache", config.getPushScheduler().getUrl(), config.getPushScheduler().getReplicaUrls(), config.getPushScheduler().getCircuitBreakerConfiguration());
|
||||
PushChallengeDynamoDb pushChallengeDynamoDb = new PushChallengeDynamoDb(pushChallengeDynamoDbClient, config.getPushChallengeDynamoDbConfiguration().getTableName());
|
||||
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessageDynamoDbClient, config.getReportMessageDynamoDbConfiguration().getTableName());
|
||||
VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient, config.getPendingAccountsDynamoDbConfiguration().getTableName());
|
||||
VerificationCodeStore pendingDevices = new VerificationCodeStore(pendingDevicesDynamoDbClient, config.getPendingDevicesDynamoDbConfiguration().getTableName());
|
||||
|
||||
RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
|
||||
ReplicatedJedisPool pubsubClient = pubSubClientFactory.getRedisClientPool();
|
||||
ReplicatedJedisPool directoryClient = directoryClientFactory.getRedisClientPool();
|
||||
ReplicatedJedisPool pushSchedulerClient = pushSchedulerClientFactory.getRedisClientPool();
|
||||
|
||||
ClientResources redisClusterClientResources = ClientResources.builder().build();
|
||||
ConnectionEventLogger.logConnectionEvents(redisClusterClientResources);
|
||||
ClientResources generalCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources messageCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources presenceClientResources = ClientResources.builder().build();
|
||||
ClientResources metricsCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources pushSchedulerCacheClientResources = ClientResources.builder().ioThreadPoolSize(4).build();
|
||||
ClientResources rateLimitersCacheClientResources = ClientResources.builder().build();
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClusterClientResources);
|
||||
FaultTolerantRedisCluster messagesCacheCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources);
|
||||
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClusterClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(generalCacheClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(messageCacheClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(presenceClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(metricsCacheClientResources);
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), generalCacheClientResources);
|
||||
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), messageCacheClientResources);
|
||||
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), presenceClientResources);
|
||||
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), metricsCacheClientResources);
|
||||
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), pushSchedulerCacheClientResources);
|
||||
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), rateLimitersCacheClientResources);
|
||||
|
||||
BlockingQueue<Runnable> keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(10_000);
|
||||
Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(), keyspaceNotificationDispatchQueue);
|
||||
|
||||
ScheduledExecutorService recurringJobExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(2).build();
|
||||
ScheduledExecutorService recurringJobExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(3).build();
|
||||
ScheduledExecutorService declinedMessageReceiptExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "declined-receipt-%d")).threads(2).build();
|
||||
ScheduledExecutorService retrySchedulingExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "retry-%d")).threads(2).build();
|
||||
ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle().executorService(name(getClass(), "keyspaceNotification-%d")).maxThreads(16).workQueue(keyspaceNotificationDispatchQueue).build();
|
||||
ExecutorService apnSenderExecutor = environment.lifecycle().executorService(name(getClass(), "apnSender-%d")).maxThreads(1).minThreads(1).build();
|
||||
ExecutorService gcmSenderExecutor = environment.lifecycle().executorService(name(getClass(), "gcmSender-%d")).maxThreads(1).minThreads(1).build();
|
||||
|
||||
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(messagesCacheCluster, recurringJobExecutor, keyspaceNotificationDispatchExecutor);
|
||||
DirectoryManager directory = new DirectoryManager(directoryClient);
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration());
|
||||
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheCluster);
|
||||
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, cacheCluster);
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
|
||||
MessagesCache messagesCache = new MessagesCache(messagesCacheCluster, keyspaceNotificationDispatchExecutor);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
|
||||
MessagesManager messagesManager = new MessagesManager(messages, messagesCache, pushLatencyManager);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheCluster, directoryQueue, keys, messagesManager, usernamesManager, profilesManager);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
FeatureFlagsManager featureFlagsManager = new FeatureFlagsManager(featureFlags, recurringJobExecutor);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
|
||||
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler));
|
||||
PubSubManager pubSubManager = new PubSubManager(pubsubClient, dispatchManager);
|
||||
APNSender apnSender = new APNSender(apnSenderExecutor, accountsManager, config.getApnConfiguration());
|
||||
GCMSender gcmSender = new GCMSender(gcmSenderExecutor, accountsManager, config.getGcmConfiguration().getApiKey());
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheCluster);
|
||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||
|
||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
ExecutorService backupServiceExecutor = environment.lifecycle().executorService(name(getClass(), "backupService-%d")).maxThreads(1).minThreads(1).build();
|
||||
ExecutorService storageServiceExecutor = environment.lifecycle().executorService(name(getClass(), "storageService-%d")).maxThreads(1).minThreads(1).build();
|
||||
ExecutorService donationExecutor = environment.lifecycle().executorService(name(getClass(), "donation-%d")).maxThreads(1).minThreads(1).build();
|
||||
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
|
||||
true);
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
|
||||
true);
|
||||
|
||||
DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager(config.getAppConfig().getApplication(), config.getAppConfig().getEnvironment(), config.getAppConfig().getConfigurationName());
|
||||
dynamicConfigurationManager.start();
|
||||
|
||||
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
|
||||
|
||||
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = new TwilioVerifyExperimentEnrollmentManager(
|
||||
config.getVoiceVerificationConfiguration(), experimentEnrollmentManager);
|
||||
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
|
||||
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
|
||||
ExternalServiceCredentialGenerator paymentsCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerClient, apnSender, accountsManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
|
||||
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator, backupServiceExecutor, config.getSecureBackupServiceConfiguration());
|
||||
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, config.getSecureStorageServiceConfiguration());
|
||||
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor, keyspaceNotificationDispatchExecutor);
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
StoredVerificationCodeManager pendingDevicesManager = new StoredVerificationCodeManager(pendingDevices);
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
|
||||
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
|
||||
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, Metrics.globalRegistry);
|
||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
|
||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts, deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, accountsDynamoDb, cacheCluster, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, pendingAccountsManager, secureStorageClient, secureBackupClient, experimentEnrollmentManager, dynamicConfigurationManager);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
|
||||
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler));
|
||||
PubSubManager pubSubManager = new PubSubManager(pubsubClient, dispatchManager);
|
||||
APNSender apnSender = new APNSender(apnSenderExecutor, accountsManager, config.getApnConfiguration());
|
||||
GCMSender gcmSender = new GCMSender(gcmSenderExecutor, accountsManager, config.getGcmConfiguration().getApiKey());
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), dynamicConfigurationManager, rateLimitersCluster);
|
||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||
TorExitNodeManager torExitNodeManager = new TorExitNodeManager(recurringJobExecutor, config.getTorExitNodeListConfiguration());
|
||||
AsnManager asnManager = new AsnManager(recurringJobExecutor, config.getAsnTableConfiguration());
|
||||
|
||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
|
||||
RateLimitResetMetricsManager rateLimitResetMetricsManager = new RateLimitResetMetricsManager(metricsCluster, Metrics.globalRegistry);
|
||||
|
||||
UnsealedSenderRateLimiter unsealedSenderRateLimiter = new UnsealedSenderRateLimiter(rateLimiters, rateLimitersCluster, dynamicConfigurationManager, rateLimitResetMetricsManager);
|
||||
PreKeyRateLimiter preKeyRateLimiter = new PreKeyRateLimiter(rateLimiters, dynamicConfigurationManager, rateLimitResetMetricsManager);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerCluster, apnSender, accountsManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration(), dynamicConfigurationManager);
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||
MessageSender messageSender = new MessageSender(apnFallbackManager, clientPresenceManager, messagesManager, gcmSender, apnSender, pushLatencyManager);
|
||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender);
|
||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
|
||||
RecaptchaClient recaptchaClient = new RecaptchaClient(config.getRecaptchaConfiguration().getSecret());
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, gcmSender, pushChallengeDynamoDb);
|
||||
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager, recaptchaClient, preKeyRateLimiter, unsealedSenderRateLimiter, rateLimiters, dynamicConfigurationManager);
|
||||
|
||||
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
|
||||
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
|
||||
|
||||
final List<DeletedAccountsDirectoryReconciler> deletedAccountsDirectoryReconcilers = new ArrayList<>();
|
||||
final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = new ArrayList<>();
|
||||
accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager, directoryQueue));
|
||||
accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager));
|
||||
accountDatabaseCrawlerListeners.add(new ActiveUserCounter(config.getMetricsFactory(), cacheCluster));
|
||||
for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration().getDirectoryServerConfiguration()) {
|
||||
final DirectoryReconciliationClient directoryReconciliationClient = new DirectoryReconciliationClient(directoryServerConfiguration);
|
||||
final DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryServerConfiguration.isReplicationPrimary(), directoryReconciliationClient, directory);
|
||||
final DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
|
||||
accountDatabaseCrawlerListeners.add(directoryReconciler);
|
||||
|
||||
final DeletedAccountsDirectoryReconciler deletedAccountsDirectoryReconciler = new DeletedAccountsDirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
|
||||
deletedAccountsDirectoryReconcilers.add(deletedAccountsDirectoryReconciler);
|
||||
}
|
||||
accountDatabaseCrawlerListeners.add(new AccountCleaner(accountsManager));
|
||||
accountDatabaseCrawlerListeners.add(new RegistrationLockVersionCounter(metricsCluster, config.getMetricsFactory()));
|
||||
accountDatabaseCrawlerListeners.add(new AccountsDynamoDbMigrator(accountsDynamoDb, dynamicConfigurationManager));
|
||||
|
||||
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
||||
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().getFixerApiKey());
|
||||
FtxClient ftxClient = new FtxClient(currencyClient);
|
||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
||||
|
||||
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster);
|
||||
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs());
|
||||
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs(), dynamicConfigurationManager);
|
||||
|
||||
DeletedAccountsTableCrawler deletedAccountsTableCrawler = new DeletedAccountsTableCrawler(deletedAccountsManager, deletedAccountsDirectoryReconcilers, cacheCluster, recurringJobExecutor);
|
||||
MigrationRetryAccountsTableCrawler migrationRetryAccountsTableCrawler = new MigrationRetryAccountsTableCrawler(migrationRetryAccounts, accountsManager, accountsDynamoDb, cacheCluster, recurringJobExecutor);
|
||||
|
||||
apnSender.setApnFallbackManager(apnFallbackManager);
|
||||
environment.lifecycle().manage(apnFallbackManager);
|
||||
environment.lifecycle().manage(pubSubManager);
|
||||
environment.lifecycle().manage(messageSender);
|
||||
environment.lifecycle().manage(accountDatabaseCrawler);
|
||||
environment.lifecycle().manage(deletedAccountsTableCrawler);
|
||||
environment.lifecycle().manage(migrationRetryAccountsTableCrawler);
|
||||
environment.lifecycle().manage(remoteConfigsManager);
|
||||
environment.lifecycle().manage(messagesCache);
|
||||
environment.lifecycle().manage(messagePersister);
|
||||
environment.lifecycle().manage(clientPresenceManager);
|
||||
environment.lifecycle().manage(featureFlagsManager);
|
||||
environment.lifecycle().manage(currencyManager);
|
||||
environment.lifecycle().manage(torExitNodeManager);
|
||||
environment.lifecycle().manage(asnManager);
|
||||
environment.lifecycle().manage(directoryQueue);
|
||||
|
||||
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
|
||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||
AmazonS3 cdnS3Client = AmazonS3Client.builder().withCredentials(credentialsProvider).withRegion(config.getCdnConfiguration().getRegion()).build();
|
||||
StaticCredentialsProvider cdnCredentialsProvider = StaticCredentialsProvider
|
||||
.create(AwsBasicCredentials.create(
|
||||
config.getCdnConfiguration().getAccessKey(),
|
||||
config.getCdnConfiguration().getAccessSecret()));
|
||||
S3Client cdnS3Client = S3Client.builder()
|
||||
.credentialsProvider(cdnCredentialsProvider)
|
||||
.region(Region.of(config.getCdnConfiguration().getRegion()))
|
||||
.build();
|
||||
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey());
|
||||
PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion());
|
||||
|
||||
@@ -373,58 +533,55 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
|
||||
boolean isZkEnabled = config.getZkConfig().isEnabled();
|
||||
|
||||
AttachmentControllerV1 attachmentControllerV1 = new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket());
|
||||
AttachmentControllerV2 attachmentControllerV2 = new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket());
|
||||
AttachmentControllerV3 attachmentControllerV3 = new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey());
|
||||
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, directoryQueue);
|
||||
MessageController messageController = new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
|
||||
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, isZkEnabled);
|
||||
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
|
||||
RemoteConfigController remoteConfigController = new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().getAuthorizedTokens(), config.getRemoteConfigConfiguration().getGlobalConfig());
|
||||
FeatureFlagsController featureFlagsController = new FeatureFlagsController(featureFlagsManager, config.getFeatureFlagConfiguration().getAuthorizedTokens());
|
||||
|
||||
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
|
||||
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
|
||||
|
||||
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
|
||||
environment.servlets().addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
|
||||
environment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
|
||||
environment.jersey().register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(Account.class, accountAuthFilter,
|
||||
DisabledPermittedAccount.class, disabledPermittedAccountAuthFilter)));
|
||||
environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)));
|
||||
|
||||
environment.jersey().register(new TimestampResponseFilter());
|
||||
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator));
|
||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
|
||||
environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator));
|
||||
environment.jersey().register(new ProvisioningController(rateLimiters, provisioningManager));
|
||||
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, isZkEnabled));
|
||||
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales()));
|
||||
environment.jersey().register(new SecureStorageController(storageCredentialsGenerator));
|
||||
environment.jersey().register(new SecureBackupController(backupCredentialsGenerator));
|
||||
environment.jersey().register(new PaymentsController(paymentsCredentialsGenerator));
|
||||
environment.jersey().register(attachmentControllerV1);
|
||||
environment.jersey().register(attachmentControllerV2);
|
||||
environment.jersey().register(attachmentControllerV3);
|
||||
environment.jersey().register(keysController);
|
||||
environment.jersey().register(messageController);
|
||||
environment.jersey().register(profileController);
|
||||
environment.jersey().register(stickerController);
|
||||
environment.jersey().register(remoteConfigController);
|
||||
environment.jersey().register(featureFlagsController);
|
||||
|
||||
///
|
||||
WebSocketEnvironment<Account> webSocketEnvironment = new WebSocketEnvironment<>(environment, config.getWebSocketConfiguration(), 90000);
|
||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
|
||||
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(receiptSender, messagesManager, messageSender, apnFallbackManager, clientPresenceManager));
|
||||
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(receiptSender, messagesManager, messageSender, apnFallbackManager, clientPresenceManager, retrySchedulingExecutor));
|
||||
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
|
||||
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||
webSocketEnvironment.jersey().register(messageController);
|
||||
webSocketEnvironment.jersey().register(profileController);
|
||||
webSocketEnvironment.jersey().register(attachmentControllerV1);
|
||||
webSocketEnvironment.jersey().register(attachmentControllerV2);
|
||||
webSocketEnvironment.jersey().register(attachmentControllerV3);
|
||||
webSocketEnvironment.jersey().register(remoteConfigController);
|
||||
|
||||
// these should be common, but use @Auth DisabledPermittedAccount, which isn’t supported yet on websocket
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator, verifyExperimentEnrollmentManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, dynamicConfigurationManager, rateLimitChallengeManager));
|
||||
|
||||
final List<Object> commonControllers = List.of(
|
||||
new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()),
|
||||
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket()),
|
||||
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey()),
|
||||
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, isZkEnabled),
|
||||
new ChallengeController(rateLimitChallengeManager),
|
||||
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keysDynamoDb, rateLimiters, config.getMaxDevices()),
|
||||
new DirectoryController(directoryCredentialsGenerator),
|
||||
new DonationController(donationExecutor, config.getDonationConfiguration()),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager, dynamicConfigurationManager, rateLimitChallengeManager, reportMessageManager, metricsCluster, declinedMessageReceiptExecutor),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, dynamicConfigurationManager, cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, isZkEnabled),
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().getAuthorizedTokens(), config.getRemoteConfigConfiguration().getGlobalConfig()),
|
||||
new SecureBackupController(backupCredentialsGenerator),
|
||||
new SecureStorageController(storageCredentialsGenerator),
|
||||
new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket())
|
||||
);
|
||||
|
||||
for (Object controller : commonControllers) {
|
||||
environment.jersey().register(controller);
|
||||
webSocketEnvironment.jersey().register(controller);
|
||||
}
|
||||
|
||||
WebSocketEnvironment<Account> provisioningEnvironment = new WebSocketEnvironment<>(environment, webSocketEnvironment.getRequestLog(), 60000);
|
||||
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
|
||||
@@ -434,6 +591,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
registerCorsFilter(environment);
|
||||
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
||||
|
||||
RateLimitChallengeExceptionMapper rateLimitChallengeExceptionMapper = new RateLimitChallengeExceptionMapper(rateLimitChallengeManager);
|
||||
|
||||
environment.jersey().register(rateLimitChallengeExceptionMapper);
|
||||
webSocketEnvironment.jersey().register(rateLimitChallengeExceptionMapper);
|
||||
provisioningEnvironment.jersey().register(rateLimitChallengeExceptionMapper);
|
||||
|
||||
WebSocketResourceProviderFactory<Account> webSocketServlet = new WebSocketResourceProviderFactory<>(webSocketEnvironment, Account.class);
|
||||
WebSocketResourceProviderFactory<Account> provisioningServlet = new WebSocketResourceProviderFactory<>(provisioningEnvironment, Account.class);
|
||||
|
||||
@@ -446,12 +609,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
provisioning.addMapping("/v1/websocket/provisioning/");
|
||||
provisioning.setAsyncSupported(true);
|
||||
|
||||
environment.admin().addTask(new EnableRequestLoggingTask());
|
||||
environment.admin().addTask(new DisableRequestLoggingTask());
|
||||
environment.admin().addTask(new SetRequestLoggingEnabledTask());
|
||||
environment.admin().addTask(new SetCrawlerAccelerationTask(accountDatabaseCrawlerCache));
|
||||
|
||||
///
|
||||
|
||||
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
|
||||
environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster));
|
||||
|
||||
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge(3, TimeUnit.SECONDS));
|
||||
@@ -468,20 +630,26 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
}
|
||||
|
||||
private void registerExceptionMappers(Environment environment, WebSocketEnvironment<Account> webSocketEnvironment, WebSocketEnvironment<Account> provisioningEnvironment) {
|
||||
environment.jersey().register(new LoggingUnhandledExceptionMapper());
|
||||
environment.jersey().register(new IOExceptionMapper());
|
||||
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
|
||||
environment.jersey().register(new DeviceLimitExceededExceptionMapper());
|
||||
environment.jersey().register(new RetryLaterExceptionMapper());
|
||||
|
||||
webSocketEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
|
||||
webSocketEnvironment.jersey().register(new IOExceptionMapper());
|
||||
webSocketEnvironment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
webSocketEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
|
||||
webSocketEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
|
||||
webSocketEnvironment.jersey().register(new RetryLaterExceptionMapper());
|
||||
|
||||
provisioningEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new IOExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new RetryLaterExceptionMapper());
|
||||
}
|
||||
|
||||
private void registerCorsFilter(Environment environment) {
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import org.apache.http.auth.AUTH;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
|
||||
@@ -37,4 +37,9 @@ public class AmbiguousIdentifier {
|
||||
public boolean hasNumber() {
|
||||
return number != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return hasUuid() ? uuid.toString() : number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.util.Base64;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Anonymous {
|
||||
|
||||
@@ -17,8 +15,8 @@ public class Anonymous {
|
||||
|
||||
public Anonymous(String header) {
|
||||
try {
|
||||
this.unidentifiedSenderAccessKey = Base64.decode(header);
|
||||
} catch (IOException e) {
|
||||
this.unidentifiedSenderAccessKey = Base64.getDecoder().decode(header);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new WebApplicationException(e, Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class AuthorizationHeader {
|
||||
|
||||
@@ -49,7 +49,7 @@ public class AuthorizationHeader {
|
||||
throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + headerParts[0]);
|
||||
}
|
||||
|
||||
String concatenatedValues = new String(Base64.decode(headerParts[1]));
|
||||
String concatenatedValues = new String(Base64.getDecoder().decode(headerParts[1]));
|
||||
|
||||
if (Util.isEmpty(concatenatedValues)) {
|
||||
throw new InvalidAuthorizationHeaderException("Bad decoded value: " + concatenatedValues);
|
||||
@@ -62,8 +62,8 @@ public class AuthorizationHeader {
|
||||
}
|
||||
|
||||
return fromUserAndPassword(credentialParts[0], credentialParts[1]);
|
||||
} catch (IOException ioe) {
|
||||
throw new InvalidAuthorizationHeaderException(ioe);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new InvalidAuthorizationHeaderException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,38 +5,32 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class BaseAccountAuthenticator {
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
|
||||
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded" ));
|
||||
private final Meter noSuchAccountMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchAccount" ));
|
||||
private final Meter noSuchDeviceMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchDevice" ));
|
||||
private final Meter accountDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "accountDisabled"));
|
||||
private final Meter deviceDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "deviceDisabled" ));
|
||||
private final Meter invalidAuthHeaderMeter = metricRegistry.meter(name(getClass(), "authentication", "invalidHeader" ));
|
||||
private static final String AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "authentication");
|
||||
private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
|
||||
private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
|
||||
private static final String AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME = "enabledRequired";
|
||||
private static final String AUTHENTICATION_CREDENTIAL_TYPE_TAG_NAME = "credentialType";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
|
||||
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
|
||||
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final Clock clock;
|
||||
@@ -52,58 +46,81 @@ public class BaseAccountAuthenticator {
|
||||
}
|
||||
|
||||
public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
|
||||
boolean succeeded = false;
|
||||
String failureReason = null;
|
||||
String credentialType = null;
|
||||
|
||||
try {
|
||||
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
|
||||
Optional<Account> account = accountsManager.get(authorizationHeader.getIdentifier());
|
||||
|
||||
if (!account.isPresent()) {
|
||||
noSuchAccountMeter.mark();
|
||||
credentialType = authorizationHeader.getIdentifier().hasNumber() ? "e164" : "uuid";
|
||||
|
||||
if (account.isEmpty()) {
|
||||
failureReason = "noSuchAccount";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
|
||||
|
||||
if (!device.isPresent()) {
|
||||
noSuchDeviceMeter.mark();
|
||||
if (device.isEmpty()) {
|
||||
failureReason = "noSuchDevice";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (enabledRequired) {
|
||||
if (!device.get().isEnabled()) {
|
||||
deviceDisabledMeter.mark();
|
||||
failureReason = "deviceDisabled";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!account.get().isEnabled()) {
|
||||
accountDisabledMeter.mark();
|
||||
failureReason = "accountDisabled";
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
|
||||
authenticationSucceededMeter.mark();
|
||||
account.get().setAuthenticatedDevice(device.get());
|
||||
updateLastSeen(account.get(), device.get());
|
||||
return account;
|
||||
succeeded = true;
|
||||
final Account authenticatedAccount = updateLastSeen(account.get(), device.get());
|
||||
authenticatedAccount.setAuthenticatedDevice(device.get());
|
||||
return Optional.of(authenticatedAccount);
|
||||
}
|
||||
|
||||
authenticationFailedMeter.mark();
|
||||
return Optional.empty();
|
||||
} catch (IllegalArgumentException | InvalidAuthorizationHeaderException iae) {
|
||||
invalidAuthHeaderMeter.mark();
|
||||
failureReason = "invalidHeader";
|
||||
return Optional.empty();
|
||||
} finally {
|
||||
Tags tags = Tags.of(
|
||||
AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded),
|
||||
AUTHENTICATION_ENABLED_REQUIRED_TAG_NAME, String.valueOf(enabledRequired));
|
||||
|
||||
if (StringUtils.isNotBlank(failureReason)) {
|
||||
tags = tags.and(AUTHENTICATION_FAILURE_REASON_TAG_NAME, failureReason);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(credentialType)) {
|
||||
tags = tags.and(AUTHENTICATION_CREDENTIAL_TYPE_TAG_NAME, credentialType);
|
||||
}
|
||||
|
||||
Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void updateLastSeen(Account account, Device device) {
|
||||
public Account updateLastSeen(Account account, Device device) {
|
||||
final long lastSeenOffsetSeconds = Math.abs(account.getUuid().getLeastSignificantBits()) % ChronoUnit.DAYS.getDuration().toSeconds();
|
||||
final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated());
|
||||
|
||||
if (device.getLastSeen() < todayInMillisWithOffset) {
|
||||
device.setLastSeen(Util.todayInMillis(clock));
|
||||
accountsManager.update(account);
|
||||
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
|
||||
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
|
||||
|
||||
return accountsManager.updateDevice(account, device.getId(), d -> d.setLastSeen(Util.todayInMillis(clock)));
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CertificateGenerator {
|
||||
@@ -33,11 +33,11 @@ public class CertificateGenerator {
|
||||
this.serverCertificate = ServerCertificate.parseFrom(serverCertificate);
|
||||
}
|
||||
|
||||
public byte[] createFor(Account account, Device device, boolean includeE164) throws IOException, InvalidKeyException {
|
||||
public byte[] createFor(Account account, Device device, boolean includeE164) throws InvalidKeyException {
|
||||
SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder()
|
||||
.setSenderDevice(Math.toIntExact(device.getId()))
|
||||
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
|
||||
.setIdentityKey(ByteString.copyFrom(Base64.decode(account.getIdentityKey())))
|
||||
.setIdentityKey(ByteString.copyFrom(Base64.getDecoder().decode(account.getIdentityKey())))
|
||||
.setSigner(serverCertificate)
|
||||
.setSenderUuid(account.getUuid().toString());
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.Base64;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
public class CombinedUnidentifiedSenderAccessKeys {
|
||||
private final byte[] combinedUnidentifiedSenderAccessKeys;
|
||||
|
||||
public CombinedUnidentifiedSenderAccessKeys(String header) {
|
||||
try {
|
||||
this.combinedUnidentifiedSenderAccessKeys = Base64.getDecoder().decode(header);
|
||||
if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != 16) {
|
||||
throw new WebApplicationException("Invalid combined unidentified sender access keys", Status.UNAUTHORIZED);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new WebApplicationException(e, Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getAccessKeys() {
|
||||
return combinedUnidentifiedSenderAccessKeys;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ public class OptionalAccess {
|
||||
}
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (requestAccount.isPresent() && (!targetAccount.isPresent() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
|
||||
if (requestAccount.isPresent() && (targetAccount.isEmpty() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,30 +5,42 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class StoredVerificationCode {
|
||||
|
||||
@JsonProperty
|
||||
private String code;
|
||||
private final String code;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
private final long timestamp;
|
||||
|
||||
@JsonProperty
|
||||
private String pushCode;
|
||||
private final String pushCode;
|
||||
|
||||
public StoredVerificationCode() {}
|
||||
@JsonProperty
|
||||
@Nullable
|
||||
private final String twilioVerificationSid;
|
||||
|
||||
public StoredVerificationCode(String code, long timestamp, String pushCode) {
|
||||
this.code = code;
|
||||
public static final Duration EXPIRATION = Duration.ofMinutes(10);
|
||||
|
||||
@JsonCreator
|
||||
public StoredVerificationCode(
|
||||
@JsonProperty("code") final String code,
|
||||
@JsonProperty("timestamp") final long timestamp,
|
||||
@JsonProperty("pushCode") final String pushCode,
|
||||
@JsonProperty("twilioVerificationSid") @Nullable final String twilioVerificationSid) {
|
||||
|
||||
this.code = code;
|
||||
this.timestamp = timestamp;
|
||||
this.pushCode = pushCode;
|
||||
this.pushCode = pushCode;
|
||||
this.twilioVerificationSid = twilioVerificationSid;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
@@ -43,19 +55,18 @@ public class StoredVerificationCode {
|
||||
return pushCode;
|
||||
}
|
||||
|
||||
public boolean isValid(String theirCodeString) {
|
||||
if (timestamp + TimeUnit.MINUTES.toMillis(10) < System.currentTimeMillis()) {
|
||||
return false;
|
||||
}
|
||||
public Optional<String> getTwilioVerificationSid() {
|
||||
return Optional.ofNullable(twilioVerificationSid);
|
||||
}
|
||||
|
||||
public boolean isValid(String theirCodeString) {
|
||||
if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] ourCode = code.getBytes();
|
||||
byte[] ourCode = code.getBytes();
|
||||
byte[] theirCode = theirCodeString.getBytes();
|
||||
|
||||
return MessageDigest.isEqual(ourCode, theirCode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -34,7 +34,7 @@ public class TurnTokenGenerator {
|
||||
String userTime = validUntilSeconds + ":" + user;
|
||||
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA1"));
|
||||
String password = Base64.encodeBytes(mac.doFinal(userTime.getBytes()));
|
||||
String password = Base64.getEncoder().encodeToString(mac.doFinal(userTime.getBytes()));
|
||||
|
||||
return new TurnToken(userTime, password, urls);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@@ -23,7 +22,7 @@ public class UnidentifiedAccessChecksum {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(unidentifiedAccessKey.get(), "HmacSHA256"));
|
||||
|
||||
return Base64.encodeBytes(mac.doFinal(new byte[32]));
|
||||
return Base64.getEncoder().encodeToString(mac.doFinal(new byte[32]));
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class AccountsDatabaseConfiguration extends DatabaseConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private RetryConfiguration keyOperationRetry = new RetryConfiguration();
|
||||
|
||||
public RetryConfiguration getKeyOperationRetryConfiguration() {
|
||||
return keyOperationRetry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
|
||||
@NotNull
|
||||
private String phoneNumberTableName;
|
||||
|
||||
public String getPhoneNumberTableName() {
|
||||
return phoneNumberTableName;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
|
||||
public class ApnConfiguration {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class AppConfigConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String application;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String environment;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String configuration;
|
||||
|
||||
public String getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public String getEnvironment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
public String getConfigurationName() {
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class AwsAttachmentsConfiguration {
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class CdnConfiguration {
|
||||
@NotEmpty
|
||||
|
||||
@@ -7,15 +7,15 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
|
||||
|
||||
public class CircuitBreakerConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@@ -39,6 +39,9 @@ public class CircuitBreakerConfiguration {
|
||||
@Min(1)
|
||||
private long waitDurationInOpenStateInSeconds = 10;
|
||||
|
||||
@JsonProperty
|
||||
private List<String> ignoredExceptions = Collections.emptyList();
|
||||
|
||||
|
||||
public int getFailureRateThreshold() {
|
||||
return failureRateThreshold;
|
||||
@@ -56,6 +59,18 @@ public class CircuitBreakerConfiguration {
|
||||
return waitDurationInOpenStateInSeconds;
|
||||
}
|
||||
|
||||
public List<Class> getIgnoredExceptions() {
|
||||
return ignoredExceptions.stream()
|
||||
.map(name -> {
|
||||
try {
|
||||
return Class.forName(name);
|
||||
} catch (final ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setFailureRateThreshold(int failureRateThreshold) {
|
||||
this.failureRateThreshold = failureRateThreshold;
|
||||
@@ -76,9 +91,15 @@ public class CircuitBreakerConfiguration {
|
||||
this.waitDurationInOpenStateInSeconds = seconds;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setIgnoredExceptions(final List<String> ignoredExceptions) {
|
||||
this.ignoredExceptions = ignoredExceptions;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfig toCircuitBreakerConfig() {
|
||||
return CircuitBreakerConfig.custom()
|
||||
.failureRateThreshold(getFailureRateThreshold())
|
||||
.ignoreExceptions(getIgnoredExceptions().toArray(new Class[0]))
|
||||
.ringBufferSizeInHalfOpenState(getRingBufferSizeInHalfOpenState())
|
||||
.waitDurationInOpenState(Duration.ofSeconds(getWaitDurationInOpenStateInSeconds()))
|
||||
.ringBufferSizeInClosedState(getRingBufferSizeInClosedState())
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.micrometer.datadog.DatadogConfig;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
|
||||
public class DatadogConfiguration implements DatadogConfig {
|
||||
|
||||
@JsonProperty
|
||||
@NotBlank
|
||||
private String apiKey;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Duration step = Duration.ofSeconds(10);
|
||||
|
||||
@JsonProperty
|
||||
@NotBlank
|
||||
private String environment;
|
||||
|
||||
@JsonProperty
|
||||
@Min(1)
|
||||
private int batchSize = 5_000;
|
||||
|
||||
@Override
|
||||
public String apiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration step() {
|
||||
return step;
|
||||
}
|
||||
|
||||
public String getEnvironment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchSize() {
|
||||
return batchSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hostTag() {
|
||||
return "host";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(final String key) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DeletedAccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
|
||||
@NotNull
|
||||
private String needsReconciliationIndexName;
|
||||
|
||||
public String getNeedsReconciliationIndexName() {
|
||||
return needsReconciliationIndexName;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class DirectoryClientConfiguration {
|
||||
|
||||
|
||||
@@ -12,11 +12,6 @@ import java.util.List;
|
||||
|
||||
public class DirectoryConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private RedisConfiguration redis;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -32,10 +27,6 @@ public class DirectoryConfiguration {
|
||||
@Valid
|
||||
private List<DirectoryServerConfiguration> server;
|
||||
|
||||
public RedisConfiguration getRedisConfiguration() {
|
||||
return redis;
|
||||
}
|
||||
|
||||
public SqsConfiguration getSqsConfiguration() {
|
||||
return sqs;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class DirectoryServerConfiguration {
|
||||
|
||||
@@ -13,9 +13,6 @@ public class DirectoryServerConfiguration {
|
||||
@JsonProperty
|
||||
private String replicationName;
|
||||
|
||||
@JsonProperty
|
||||
private boolean replicationPrimary;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String replicationUrl;
|
||||
@@ -32,10 +29,6 @@ public class DirectoryServerConfiguration {
|
||||
return replicationName;
|
||||
}
|
||||
|
||||
public boolean isReplicationPrimary() {
|
||||
return replicationPrimary;
|
||||
}
|
||||
|
||||
public String getReplicationUrl() {
|
||||
return replicationUrl;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DonationConfiguration {
|
||||
|
||||
private String uri;
|
||||
private String apiKey;
|
||||
private String description;
|
||||
private Set<String> supportedCurrencies;
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
private RetryConfiguration retry = new RetryConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setUri(final String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setApiKey(final String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
public Set<String> getSupportedCurrencies() {
|
||||
return supportedCurrencies;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setSupportedCurrencies(final Set<String> supportedCurrencies) {
|
||||
this.supportedCurrencies = supportedCurrencies;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
public CircuitBreakerConfiguration getCircuitBreaker() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setCircuitBreaker(final CircuitBreakerConfiguration circuitBreaker) {
|
||||
this.circuitBreaker = circuitBreaker;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
public RetryConfiguration getRetry() {
|
||||
return retry;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setRetry(final RetryConfiguration retry) {
|
||||
this.retry = retry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.time.Duration;
|
||||
|
||||
public class DynamoDbConfiguration {
|
||||
|
||||
private String region;
|
||||
private String tableName;
|
||||
private Duration clientExecutionTimeout = Duration.ofSeconds(30);
|
||||
private Duration clientRequestTimeout = Duration.ofSeconds(10);
|
||||
|
||||
@Valid
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
public String getTableName() {
|
||||
return tableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public Duration getClientExecutionTimeout() {
|
||||
return clientExecutionTimeout;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public Duration getClientRequestTimeout() {
|
||||
return clientRequestTimeout;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class FeatureFlagConfiguration {
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private List<String> authorizedTokens = new LinkedList<>();
|
||||
|
||||
public List<String> getAuthorizedTokens() {
|
||||
return authorizedTokens;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class GcmConfiguration {
|
||||
|
||||
@@ -8,9 +8,8 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.dropwizard.util.Strings;
|
||||
import io.dropwizard.validation.ValidationMethod;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class GcpAttachmentsConfiguration {
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class MaxDeviceConfiguration {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.time.Duration;
|
||||
import javax.validation.Valid;
|
||||
|
||||
public class MessageDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
|
||||
private Duration timeToLive = Duration.ofDays(14);
|
||||
|
||||
@Valid
|
||||
public Duration getTimeToLive() {
|
||||
return timeToLive;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.time.Duration;
|
||||
|
||||
public class MonitoredS3ObjectConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotBlank
|
||||
private String s3Region;
|
||||
|
||||
@JsonProperty
|
||||
@NotBlank
|
||||
private String s3Bucket;
|
||||
|
||||
@JsonProperty
|
||||
@NotBlank
|
||||
private String objectKey;
|
||||
|
||||
@JsonProperty
|
||||
private long maxSize = 16 * 1024 * 1024;
|
||||
|
||||
@JsonProperty
|
||||
private Duration refreshInterval = Duration.ofMinutes(5);
|
||||
|
||||
public String getS3Region() {
|
||||
return s3Region;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setS3Region(final String s3Region) {
|
||||
this.s3Region = s3Region;
|
||||
}
|
||||
|
||||
public String getS3Bucket() {
|
||||
return s3Bucket;
|
||||
}
|
||||
|
||||
public String getObjectKey() {
|
||||
return objectKey;
|
||||
}
|
||||
|
||||
public long getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setMaxSize(final long maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public Duration getRefreshInterval() {
|
||||
return refreshInterval;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.List;
|
||||
|
||||
public class PaymentsServiceConfiguration {
|
||||
|
||||
@@ -17,8 +18,23 @@ public class PaymentsServiceConfiguration {
|
||||
@JsonProperty
|
||||
private String userAuthenticationTokenSharedSecret;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private String fixerApiKey;
|
||||
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
private List<String> paymentCurrencies;
|
||||
|
||||
public byte[] getUserAuthenticationTokenSharedSecret() throws DecoderException {
|
||||
return Hex.decodeHex(userAuthenticationTokenSharedSecret.toCharArray());
|
||||
}
|
||||
|
||||
public String getFixerApiKey() {
|
||||
return fixerApiKey;
|
||||
}
|
||||
|
||||
public List<String> getPaymentCurrencies() {
|
||||
return paymentCurrencies;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
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 {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Duration;
|
||||
|
||||
public class RateLimitsConfiguration {
|
||||
|
||||
@@ -35,12 +36,6 @@ public class RateLimitsConfiguration {
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration attachments = new RateLimitConfiguration(50, 50);
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration contactQueries = new RateLimitConfiguration(50000, 50000);
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration contactIpQueries = new RateLimitConfiguration(200, (100.0 / 60.0));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration prekeys = new RateLimitConfiguration(3, 1.0 / 10.0);
|
||||
|
||||
@@ -88,14 +83,6 @@ public class RateLimitsConfiguration {
|
||||
return prekeys;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getContactQueries() {
|
||||
return contactQueries;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getContactIpQueries() {
|
||||
return contactIpQueries;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
@@ -170,4 +157,28 @@ public class RateLimitsConfiguration {
|
||||
return leakRatePerMinute;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CardinalityRateLimitConfiguration {
|
||||
@JsonProperty
|
||||
private int maxCardinality;
|
||||
|
||||
@JsonProperty
|
||||
private Duration ttl;
|
||||
|
||||
public CardinalityRateLimitConfiguration() {
|
||||
}
|
||||
|
||||
public CardinalityRateLimitConfiguration(int maxCardinality, Duration ttl) {
|
||||
this.maxCardinality = maxCardinality;
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
public int getMaxCardinality() {
|
||||
return maxCardinality;
|
||||
}
|
||||
|
||||
public Duration getTtl() {
|
||||
return ttl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class RecaptchaConfiguration {
|
||||
|
||||
|
||||
@@ -6,13 +6,11 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class RedisConfiguration {
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
public class SecureBackupServiceConfiguration {
|
||||
|
||||
@@ -16,8 +20,51 @@ public class SecureBackupServiceConfiguration {
|
||||
@JsonProperty
|
||||
private String userAuthenticationTokenSharedSecret;
|
||||
|
||||
@NotBlank
|
||||
@JsonProperty
|
||||
private String uri;
|
||||
|
||||
@NotBlank
|
||||
@JsonProperty
|
||||
private String backupCaCertificate;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RetryConfiguration retry = new RetryConfiguration();
|
||||
|
||||
public byte[] getUserAuthenticationTokenSharedSecret() throws DecoderException {
|
||||
return Hex.decodeHex(userAuthenticationTokenSharedSecret.toCharArray());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setUri(final String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setBackupCaCertificate(final String backupCaCertificate) {
|
||||
this.backupCaCertificate = backupCaCertificate;
|
||||
}
|
||||
|
||||
public String getBackupCaCertificate() {
|
||||
return backupCaCertificate;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
public RetryConfiguration getRetryConfiguration() {
|
||||
return retry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
public class SecureStorageServiceConfiguration {
|
||||
|
||||
@@ -25,6 +24,10 @@ public class SecureStorageServiceConfiguration {
|
||||
@JsonProperty
|
||||
private String uri;
|
||||
|
||||
@NotBlank
|
||||
@JsonProperty
|
||||
private String storageCaCertificate;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -48,6 +51,15 @@ public class SecureStorageServiceConfiguration {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setStorageCaCertificate(final String certificatePem) {
|
||||
this.storageCaCertificate = certificatePem;
|
||||
}
|
||||
|
||||
public String getStorageCaCertificate() {
|
||||
return storageCaCertificate;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class SqsConfiguration {
|
||||
@NotEmpty
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class TestDeviceConfiguration {
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class TurnConfiguration {
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
public class TwilioConfiguration {
|
||||
|
||||
@@ -19,15 +19,18 @@ public class TwilioConfiguration {
|
||||
@NotEmpty
|
||||
private String accountToken;
|
||||
|
||||
@NotNull
|
||||
private List<String> numbers;
|
||||
|
||||
@NotEmpty
|
||||
private String localDomain;
|
||||
|
||||
@NotEmpty
|
||||
private String messagingServiceSid;
|
||||
|
||||
@NotEmpty
|
||||
private String nanpaMessagingServiceSid;
|
||||
|
||||
@NotEmpty
|
||||
private String verifyServiceSid;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
@@ -36,21 +39,17 @@ public class TwilioConfiguration {
|
||||
@Valid
|
||||
private RetryConfiguration retry = new RetryConfiguration();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private TwilioSenderIdConfiguration senderId = new TwilioSenderIdConfiguration();
|
||||
private TwilioVerificationTextConfiguration defaultClientVerificationTexts;
|
||||
|
||||
@Valid
|
||||
private Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts = Collections.emptyMap();
|
||||
|
||||
@NotEmpty
|
||||
private String iosVerificationText;
|
||||
private String androidAppHash;
|
||||
|
||||
@NotEmpty
|
||||
private String androidNgVerificationText;
|
||||
|
||||
@NotEmpty
|
||||
private String android202001VerificationText;
|
||||
|
||||
@NotEmpty
|
||||
private String genericVerificationText;
|
||||
private String verifyServiceFriendlyName;
|
||||
|
||||
public String getAccountId() {
|
||||
return accountId;
|
||||
@@ -69,16 +68,6 @@ public class TwilioConfiguration {
|
||||
public void setAccountToken(String accountToken) {
|
||||
this.accountToken = accountToken;
|
||||
}
|
||||
|
||||
public List<String> getNumbers() {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setNumbers(List<String> numbers) {
|
||||
this.numbers = numbers;
|
||||
}
|
||||
|
||||
public String getLocalDomain() {
|
||||
return localDomain;
|
||||
}
|
||||
@@ -106,6 +95,15 @@ public class TwilioConfiguration {
|
||||
this.nanpaMessagingServiceSid = nanpaMessagingServiceSid;
|
||||
}
|
||||
|
||||
public String getVerifyServiceSid() {
|
||||
return verifyServiceSid;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setVerifyServiceSid(String verifyServiceSid) {
|
||||
this.verifyServiceSid = verifyServiceSid;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreaker() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
@@ -124,48 +122,38 @@ public class TwilioConfiguration {
|
||||
this.retry = retry;
|
||||
}
|
||||
|
||||
public TwilioSenderIdConfiguration getSenderId() {
|
||||
return senderId;
|
||||
public TwilioVerificationTextConfiguration getDefaultClientVerificationTexts() {
|
||||
return defaultClientVerificationTexts;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setSenderId(TwilioSenderIdConfiguration senderId) {
|
||||
this.senderId = senderId;
|
||||
public void setDefaultClientVerificationTexts(TwilioVerificationTextConfiguration defaultClientVerificationTexts) {
|
||||
this.defaultClientVerificationTexts = defaultClientVerificationTexts;
|
||||
}
|
||||
|
||||
public String getIosVerificationText() {
|
||||
return iosVerificationText;
|
||||
|
||||
public Map<String,TwilioVerificationTextConfiguration> getRegionalClientVerificationTexts() {
|
||||
return regionalClientVerificationTexts;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setIosVerificationText(String iosVerificationText) {
|
||||
this.iosVerificationText = iosVerificationText;
|
||||
public void setRegionalClientVerificationTexts(final Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts) {
|
||||
this.regionalClientVerificationTexts = regionalClientVerificationTexts;
|
||||
}
|
||||
|
||||
public String getAndroidNgVerificationText() {
|
||||
return androidNgVerificationText;
|
||||
public String getAndroidAppHash() {
|
||||
return androidAppHash;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAndroidNgVerificationText(String androidNgVerificationText) {
|
||||
this.androidNgVerificationText = androidNgVerificationText;
|
||||
public void setAndroidAppHash(String androidAppHash) {
|
||||
this.androidAppHash = androidAppHash;
|
||||
}
|
||||
|
||||
public String getAndroid202001VerificationText() {
|
||||
return android202001VerificationText;
|
||||
public void setVerifyServiceFriendlyName(String serviceFriendlyName) {
|
||||
this.verifyServiceFriendlyName = serviceFriendlyName;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAndroid202001VerificationText(String android202001VerificationText) {
|
||||
this.android202001VerificationText = android202001VerificationText;
|
||||
}
|
||||
|
||||
public String getGenericVerificationText() {
|
||||
return genericVerificationText;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setGenericVerificationText(String genericVerificationText) {
|
||||
this.genericVerificationText = genericVerificationText;
|
||||
public String getVerifyServiceFriendlyName() {
|
||||
return verifyServiceFriendlyName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class TwilioSenderIdConfiguration {
|
||||
@NotEmpty
|
||||
private String defaultSenderId;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private List<TwilioCountrySenderIdConfiguration> countrySpecificSenderIds = new ArrayList<>();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Set<String> countryCodesWithoutSenderId = new HashSet<>();
|
||||
|
||||
public String getDefaultSenderId() {
|
||||
return defaultSenderId;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setDefaultSenderId(String defaultSenderId) {
|
||||
this.defaultSenderId = defaultSenderId;
|
||||
}
|
||||
|
||||
public List<TwilioCountrySenderIdConfiguration> getCountrySpecificSenderIds() {
|
||||
return countrySpecificSenderIds;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setCountrySpecificSenderIds(List<TwilioCountrySenderIdConfiguration> countrySpecificSenderIds) {
|
||||
this.countrySpecificSenderIds = countrySpecificSenderIds;
|
||||
}
|
||||
|
||||
public Set<String> getCountryCodesWithoutSenderId() {
|
||||
return countryCodesWithoutSenderId;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setCountryCodesWithoutSenderId(Set<String> countryCodesWithoutSenderId) {
|
||||
this.countryCodesWithoutSenderId = countryCodesWithoutSenderId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class TwilioVerificationTextConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String ios;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String androidNg;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String android202001;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String android202103;
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String generic;
|
||||
|
||||
public String getIosText() {
|
||||
return ios;
|
||||
}
|
||||
|
||||
public void setIosText(String ios) {
|
||||
this.ios = ios;
|
||||
}
|
||||
|
||||
public String getAndroidNgText() {
|
||||
return androidNg;
|
||||
}
|
||||
|
||||
public void setAndroidNgText(final String androidNg) {
|
||||
this.androidNg = androidNg;
|
||||
}
|
||||
|
||||
public String getAndroid202001Text() {
|
||||
return android202001;
|
||||
}
|
||||
|
||||
public void setAndroid202001Text(final String android202001) {
|
||||
this.android202001 = android202001;
|
||||
}
|
||||
|
||||
public String getAndroid202103Text() {
|
||||
return android202103;
|
||||
}
|
||||
|
||||
public void setAndroid202103Text(final String android202103) {
|
||||
this.android202103 = android202103;
|
||||
}
|
||||
|
||||
public String getGenericText() {
|
||||
return generic;
|
||||
}
|
||||
|
||||
public void setGenericText(final String generic) {
|
||||
this.generic = generic;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,12 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class VoiceVerificationConfiguration {
|
||||
|
||||
|
||||
@@ -7,10 +7,9 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Positive;
|
||||
|
||||
public class MicrometerConfiguration {
|
||||
public class WavefrontConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private String uri;
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
public class DynamicAccountsDynamoDbMigrationConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
boolean backgroundMigrationEnabled;
|
||||
|
||||
@JsonProperty
|
||||
int backgroundMigrationExecutorThreads = 1;
|
||||
|
||||
@JsonProperty
|
||||
boolean deleteEnabled;
|
||||
|
||||
@JsonProperty
|
||||
boolean writeEnabled;
|
||||
|
||||
@JsonProperty
|
||||
boolean readEnabled;
|
||||
|
||||
@JsonProperty
|
||||
boolean logMismatches;
|
||||
|
||||
@JsonProperty
|
||||
boolean dynamoCrawlerEnabled;
|
||||
|
||||
@JsonProperty
|
||||
int dynamoCrawlerScanPageSize = 10;
|
||||
|
||||
public boolean isBackgroundMigrationEnabled() {
|
||||
return backgroundMigrationEnabled;
|
||||
}
|
||||
|
||||
public int getBackgroundMigrationExecutorThreads() {
|
||||
return backgroundMigrationExecutorThreads;
|
||||
}
|
||||
|
||||
public void setDeleteEnabled(boolean deleteEnabled) {
|
||||
this.deleteEnabled = deleteEnabled;
|
||||
}
|
||||
|
||||
public boolean isDeleteEnabled() {
|
||||
return deleteEnabled;
|
||||
}
|
||||
|
||||
public void setWriteEnabled(boolean writeEnabled) {
|
||||
this.writeEnabled = writeEnabled;
|
||||
}
|
||||
|
||||
public boolean isWriteEnabled() {
|
||||
return writeEnabled;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setReadEnabled(boolean readEnabled) {
|
||||
this.readEnabled = readEnabled;
|
||||
}
|
||||
|
||||
public boolean isReadEnabled() {
|
||||
return readEnabled;
|
||||
}
|
||||
|
||||
public boolean isLogMismatches() {
|
||||
return logMismatches;
|
||||
}
|
||||
|
||||
public boolean isDynamoCrawlerEnabled() {
|
||||
return dynamoCrawlerEnabled;
|
||||
}
|
||||
|
||||
public int getDynamoCrawlerScanPageSize() {
|
||||
return dynamoCrawlerScanPageSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
|
||||
public class DynamicConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Map<String, DynamicExperimentEnrollmentConfiguration> experiments = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Map<String, DynamicPreRegistrationExperimentEnrollmentConfiguration> preRegistrationExperiments = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicRateLimitsConfiguration limits = new DynamicRateLimitsConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicRemoteDeprecationConfiguration remoteDeprecation = new DynamicRemoteDeprecationConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicMessageRateConfiguration messageRate = new DynamicMessageRateConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicPaymentsConfiguration payments = new DynamicPaymentsConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
private Set<String> featureFlags = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicTwilioConfiguration twilio = new DynamicTwilioConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
private DynamicSignupCaptchaConfiguration signupCaptcha = new DynamicSignupCaptchaConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
private DynamicAccountsDynamoDbMigrationConfiguration accountsDynamoDbMigration = new DynamicAccountsDynamoDbMigrationConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicRateLimitChallengeConfiguration rateLimitChallenge = new DynamicRateLimitChallengeConfiguration();
|
||||
|
||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||
final String experimentName) {
|
||||
return Optional.ofNullable(experiments.get(experimentName));
|
||||
}
|
||||
|
||||
public Optional<DynamicPreRegistrationExperimentEnrollmentConfiguration> getPreRegistrationEnrollmentConfiguration(
|
||||
final String experimentName) {
|
||||
return Optional.ofNullable(preRegistrationExperiments.get(experimentName));
|
||||
}
|
||||
|
||||
public DynamicRateLimitsConfiguration getLimits() {
|
||||
return limits;
|
||||
}
|
||||
|
||||
public DynamicRemoteDeprecationConfiguration getRemoteDeprecationConfiguration() {
|
||||
return remoteDeprecation;
|
||||
}
|
||||
|
||||
public DynamicMessageRateConfiguration getMessageRateConfiguration() {
|
||||
return messageRate;
|
||||
}
|
||||
|
||||
public DynamicPaymentsConfiguration getPaymentsConfiguration() {
|
||||
return payments;
|
||||
}
|
||||
|
||||
public Set<String> getActiveFeatureFlags() {
|
||||
return featureFlags;
|
||||
}
|
||||
|
||||
public DynamicTwilioConfiguration getTwilioConfiguration() {
|
||||
return twilio;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setTwilioConfiguration(DynamicTwilioConfiguration twilioConfiguration) {
|
||||
this.twilio = twilioConfiguration;
|
||||
}
|
||||
|
||||
public DynamicSignupCaptchaConfiguration getSignupCaptchaConfiguration() {
|
||||
return signupCaptcha;
|
||||
}
|
||||
|
||||
public DynamicAccountsDynamoDbMigrationConfiguration getAccountsDynamoDbMigrationConfiguration() {
|
||||
return accountsDynamoDbMigration;
|
||||
}
|
||||
|
||||
public DynamicRateLimitChallengeConfiguration getRateLimitChallengeConfiguration() {
|
||||
return rateLimitChallenge;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DynamicExperimentEnrollmentConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Set<UUID> enrolledUuids = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
private int enrollmentPercentage = 0;
|
||||
|
||||
public Set<UUID> getEnrolledUuids() {
|
||||
return enrolledUuids;
|
||||
}
|
||||
|
||||
public int getEnrollmentPercentage() {
|
||||
return enrollmentPercentage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public class DynamicMessageRateConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private boolean enforceUnsealedSenderRateLimit = false;
|
||||
|
||||
@JsonProperty
|
||||
private Set<String> rateLimitedCountryCodes = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
private Set<String> rateLimitedHosts = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
private Duration responseDelay = Duration.ofNanos(1_200_000);
|
||||
|
||||
@JsonProperty
|
||||
private Duration responseDelayJitter = Duration.ofNanos(500_000);
|
||||
|
||||
@JsonProperty
|
||||
private Duration receiptDelay = Duration.ofMillis(1_200);
|
||||
|
||||
@JsonProperty
|
||||
private Duration receiptDelayJitter = Duration.ofMillis(800);
|
||||
|
||||
@JsonProperty
|
||||
private double receiptProbability = 0.82;
|
||||
|
||||
|
||||
public boolean isEnforceUnsealedSenderRateLimit() {
|
||||
return enforceUnsealedSenderRateLimit;
|
||||
}
|
||||
|
||||
public Set<String> getRateLimitedCountryCodes() {
|
||||
return rateLimitedCountryCodes;
|
||||
}
|
||||
|
||||
public Set<String> getRateLimitedHosts() {
|
||||
return rateLimitedHosts;
|
||||
}
|
||||
|
||||
public Duration getResponseDelay() {
|
||||
return responseDelay;
|
||||
}
|
||||
|
||||
public Duration getResponseDelayJitter() {
|
||||
return responseDelayJitter;
|
||||
}
|
||||
|
||||
public Duration getReceiptDelay() {
|
||||
return receiptDelay;
|
||||
}
|
||||
|
||||
public Duration getReceiptDelayJitter() {
|
||||
return receiptDelayJitter;
|
||||
}
|
||||
|
||||
public double getReceiptProbability() {
|
||||
return receiptProbability;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public class DynamicPaymentsConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private Set<String> allowedCountryCodes = Collections.emptySet();
|
||||
|
||||
public Set<String> getAllowedCountryCodes() {
|
||||
return allowedCountryCodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
public class DynamicPreRegistrationExperimentEnrollmentConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Set<String> enrolledE164s = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Set<String> excludedCountryCodes = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Set<String> includedCountryCodes = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
private int enrollmentPercentage = 0;
|
||||
|
||||
public Set<String> getEnrolledE164s() {
|
||||
return enrolledE164s;
|
||||
}
|
||||
|
||||
public Set<String> getExcludedCountryCodes() {
|
||||
return excludedCountryCodes;
|
||||
}
|
||||
|
||||
public Set<String> getIncludedCountryCodes() {
|
||||
return includedCountryCodes;
|
||||
}
|
||||
|
||||
public int getEnrollmentPercentage() {
|
||||
return enrollmentPercentage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DynamicRateLimitChallengeConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private boolean preKeyLimitEnforced = false;
|
||||
|
||||
@JsonProperty
|
||||
boolean unsealedSenderLimitEnforced = false;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Map<ClientPlatform, Semver> clientSupportedVersions = Collections.emptyMap();
|
||||
|
||||
@VisibleForTesting
|
||||
Map<ClientPlatform, Semver> getClientSupportedVersions() {
|
||||
return clientSupportedVersions;
|
||||
}
|
||||
|
||||
public Optional<Semver> getMinimumSupportedVersion(final ClientPlatform platform) {
|
||||
return Optional.ofNullable(clientSupportedVersions.get(platform));
|
||||
}
|
||||
|
||||
public boolean isPreKeyLimitEnforced() {
|
||||
return preKeyLimitEnforced;
|
||||
}
|
||||
|
||||
public boolean isUnsealedSenderLimitEnforced() {
|
||||
return unsealedSenderLimitEnforced;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.CardinalityRateLimitConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.RateLimitConfiguration;
|
||||
import java.time.Duration;
|
||||
|
||||
public class DynamicRateLimitsConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private CardinalityRateLimitConfiguration unsealedSenderNumber = new CardinalityRateLimitConfiguration(100, Duration.ofDays(1));
|
||||
|
||||
@JsonProperty
|
||||
private int unsealedSenderDefaultCardinalityLimit = 100;
|
||||
|
||||
@JsonProperty
|
||||
private int unsealedSenderPermitIncrement = 50;
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration unsealedSenderIp = new RateLimitConfiguration(120, 2.0 / 60);
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration rateLimitReset = new RateLimitConfiguration(2, 2.0 / (60 * 24));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration recaptchaChallengeAttempt = new RateLimitConfiguration(10, 10.0 / (60 * 24));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration recaptchaChallengeSuccess = new RateLimitConfiguration(2, 2.0 / (60 * 24));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration pushChallengeAttempt = new RateLimitConfiguration(10, 10.0 / (60 * 24));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration pushChallengeSuccess = new RateLimitConfiguration(2, 2.0 / (60 * 24));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration dailyPreKeys = new RateLimitConfiguration(50, 50.0 / (24.0 * 60));
|
||||
|
||||
public RateLimitConfiguration getUnsealedSenderIp() {
|
||||
return unsealedSenderIp;
|
||||
}
|
||||
|
||||
public CardinalityRateLimitConfiguration getUnsealedSenderNumber() {
|
||||
return unsealedSenderNumber;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getRateLimitReset() {
|
||||
return rateLimitReset;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getRecaptchaChallengeAttempt() {
|
||||
return recaptchaChallengeAttempt;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getRecaptchaChallengeSuccess() {
|
||||
return recaptchaChallengeSuccess;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getPushChallengeAttempt() {
|
||||
return pushChallengeAttempt;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getPushChallengeSuccess() {
|
||||
return pushChallengeSuccess;
|
||||
}
|
||||
|
||||
public int getUnsealedSenderDefaultCardinalityLimit() {
|
||||
return unsealedSenderDefaultCardinalityLimit;
|
||||
}
|
||||
|
||||
public int getUnsealedSenderPermitIncrement() {
|
||||
return unsealedSenderPermitIncrement;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getDailyPreKeys() {
|
||||
return dailyPreKeys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class DynamicRemoteDeprecationConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private Map<ClientPlatform, Semver> minimumVersions = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
private Map<ClientPlatform, Semver> versionsPendingDeprecation = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
private Map<ClientPlatform, Set<Semver>> blockedVersions = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
private Map<ClientPlatform, Set<Semver>> versionsPendingBlock = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
private boolean unrecognizedUserAgentAllowed = true;
|
||||
|
||||
@VisibleForTesting
|
||||
public void setMinimumVersions(final Map<ClientPlatform, Semver> minimumVersions) {
|
||||
this.minimumVersions = minimumVersions;
|
||||
}
|
||||
|
||||
public Map<ClientPlatform, Semver> getMinimumVersions() {
|
||||
return minimumVersions;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setVersionsPendingDeprecation(final Map<ClientPlatform, Semver> versionsPendingDeprecation) {
|
||||
this.versionsPendingDeprecation = versionsPendingDeprecation;
|
||||
}
|
||||
|
||||
public Map<ClientPlatform, Semver> getVersionsPendingDeprecation() {
|
||||
return versionsPendingDeprecation;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setUnrecognizedUserAgentAllowed(final boolean allowUnrecognizedUserAgents) {
|
||||
this.unrecognizedUserAgentAllowed = allowUnrecognizedUserAgents;
|
||||
}
|
||||
|
||||
public boolean isUnrecognizedUserAgentAllowed() {
|
||||
return unrecognizedUserAgentAllowed;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setBlockedVersions(final Map<ClientPlatform, Set<Semver>> blockedVersions) {
|
||||
this.blockedVersions = blockedVersions;
|
||||
}
|
||||
|
||||
public Map<ClientPlatform, Set<Semver>> getBlockedVersions() {
|
||||
return blockedVersions;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setVersionsPendingBlock(final Map<ClientPlatform, Set<Semver>> versionsPendingBlock) {
|
||||
this.versionsPendingBlock = versionsPendingBlock;
|
||||
}
|
||||
|
||||
public Map<ClientPlatform, Set<Semver>> getVersionsPendingBlock() {
|
||||
return versionsPendingBlock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DynamicSignupCaptchaConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Set<String> countryCodes = Collections.emptySet();
|
||||
|
||||
public Set<String> getCountryCodes() {
|
||||
return countryCodes;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setCountryCodes(Set<String> numbers) {
|
||||
this.countryCodes = numbers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class DynamicTwilioConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private List<String> numbers = Collections.emptyList();
|
||||
|
||||
public List<String> getNumbers() {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setNumbers(List<String> numbers) {
|
||||
this.numbers = numbers;
|
||||
}
|
||||
}
|
||||
@@ -4,54 +4,26 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Auth;
|
||||
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.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountCreationResult;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.DeprecatedPin;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceName;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.push.GCMSender;
|
||||
import org.whispersystems.textsecuregcm.push.GcmMessage;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.storage.AbusiveHostRule;
|
||||
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PaymentAddressList;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
@@ -65,14 +37,49 @@ import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
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.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnToken;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicSignupCaptchaConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountCreationResult;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.DeprecatedPin;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceName;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.push.GCMSender;
|
||||
import org.whispersystems.textsecuregcm.push.GcmMessage;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.storage.AbusiveHostRule;
|
||||
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/accounts")
|
||||
@@ -80,23 +87,34 @@ public class AccountController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter newUserMeter = metricRegistry.meter(name(AccountController.class, "brand_new_user" ));
|
||||
private final Meter blockedHostMeter = metricRegistry.meter(name(AccountController.class, "blocked_host" ));
|
||||
private final Meter filteredHostMeter = metricRegistry.meter(name(AccountController.class, "filtered_host" ));
|
||||
private final Meter rateLimitedHostMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_host" ));
|
||||
private final Meter rateLimitedPrefixMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_prefix"));
|
||||
private final Meter captchaRequiredMeter = metricRegistry.meter(name(AccountController.class, "captcha_required" ));
|
||||
private final Meter captchaSuccessMeter = metricRegistry.meter(name(AccountController.class, "captcha_success" ));
|
||||
private final Meter captchaFailureMeter = metricRegistry.meter(name(AccountController.class, "captcha_failure" ));
|
||||
|
||||
private static final String PUSH_CHALLENGE_COUNTER_NAME = name(AccountController.class, "pushChallenge");
|
||||
private static final String ACCOUNT_CREATE_COUNTER_NAME = name(AccountController.class, "create");
|
||||
private static final String ACCOUNT_VERIFY_COUNTER_NAME = name(AccountController.class, "verify");
|
||||
|
||||
private final PendingAccountsManager pendingAccounts;
|
||||
private static final String TWILIO_VERIFY_ERROR_COUNTER_NAME = name(AccountController.class, "twilioVerifyError");
|
||||
|
||||
private static final String CHALLENGE_PRESENT_TAG_NAME = "present";
|
||||
private static final String CHALLENGE_MATCH_TAG_NAME = "matches";
|
||||
private static final String COUNTRY_CODE_TAG_NAME = "countryCode";
|
||||
private static final String VERFICATION_TRANSPORT_TAG_NAME = "transport";
|
||||
|
||||
private static final String VERIFY_EXPERIMENT_TAG_NAME = "twilioVerify";
|
||||
|
||||
private final StoredVerificationCodeManager pendingAccounts;
|
||||
private final AccountsManager accounts;
|
||||
private final UsernamesManager usernames;
|
||||
private final AbusiveHostRules abusiveHostRules;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final MessagesManager messagesManager;
|
||||
private final DynamicConfigurationManager dynamicConfigurationManager;
|
||||
private final TurnTokenGenerator turnTokenGenerator;
|
||||
private final Map<String, Integer> testDevices;
|
||||
private final RecaptchaClient recaptchaClient;
|
||||
@@ -104,20 +122,22 @@ public class AccountController {
|
||||
private final APNSender apnSender;
|
||||
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
|
||||
|
||||
public AccountController(PendingAccountsManager pendingAccounts,
|
||||
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
|
||||
|
||||
public AccountController(StoredVerificationCodeManager pendingAccounts,
|
||||
AccountsManager accounts,
|
||||
UsernamesManager usernames,
|
||||
AbusiveHostRules abusiveHostRules,
|
||||
RateLimiters rateLimiters,
|
||||
SmsSender smsSenderFactory,
|
||||
DirectoryQueue directoryQueue,
|
||||
MessagesManager messagesManager,
|
||||
DynamicConfigurationManager dynamicConfigurationManager,
|
||||
TurnTokenGenerator turnTokenGenerator,
|
||||
Map<String, Integer> testDevices,
|
||||
RecaptchaClient recaptchaClient,
|
||||
GCMSender gcmSender,
|
||||
APNSender apnSender,
|
||||
ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
|
||||
ExternalServiceCredentialGenerator backupServiceCredentialGenerator,
|
||||
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager)
|
||||
{
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
@@ -125,14 +145,14 @@ public class AccountController {
|
||||
this.abusiveHostRules = abusiveHostRules;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.messagesManager = messagesManager;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.testDevices = testDevices;
|
||||
this.turnTokenGenerator = turnTokenGenerator;
|
||||
this.recaptchaClient = recaptchaClient;
|
||||
this.gcmSender = gcmSender;
|
||||
this.apnSender = apnSender;
|
||||
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
|
||||
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -140,7 +160,8 @@ public class AccountController {
|
||||
@Path("/{type}/preauth/{token}/{number}")
|
||||
public Response getPreAuth(@PathParam("type") String pushType,
|
||||
@PathParam("token") String pushToken,
|
||||
@PathParam("number") String number)
|
||||
@PathParam("number") String number,
|
||||
@QueryParam("voip") Optional<Boolean> useVoip)
|
||||
{
|
||||
if (!"apn".equals(pushType) && !"fcm".equals(pushType)) {
|
||||
return Response.status(400).build();
|
||||
@@ -153,14 +174,15 @@ public class AccountController {
|
||||
String pushChallenge = generatePushChallenge();
|
||||
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null,
|
||||
System.currentTimeMillis(),
|
||||
pushChallenge);
|
||||
pushChallenge,
|
||||
null);
|
||||
|
||||
pendingAccounts.store(number, storedVerificationCode);
|
||||
|
||||
if ("fcm".equals(pushType)) {
|
||||
gcmSender.sendMessage(new GcmMessage(pushToken, number, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
||||
} else if ("apn".equals(pushType)) {
|
||||
apnSender.sendMessage(new ApnMessage(pushToken, number, 0, true, Optional.of(storedVerificationCode.getPushCode())));
|
||||
apnSender.sendMessage(new ApnMessage(pushToken, number, 0, useVoip.orElse(true), ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -174,27 +196,26 @@ public class AccountController {
|
||||
public Response createAccount(@PathParam("transport") String transport,
|
||||
@PathParam("number") String number,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor,
|
||||
@HeaderParam("User-Agent") List<String> userAgent,
|
||||
@HeaderParam("Accept-Language") Optional<String> locale,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@HeaderParam("Accept-Language") Optional<String> acceptLanguage,
|
||||
@QueryParam("client") Optional<String> client,
|
||||
@QueryParam("captcha") Optional<String> captcha,
|
||||
@QueryParam("challenge") Optional<String> pushChallenge)
|
||||
throws RateLimitExceededException
|
||||
throws RateLimitExceededException, RetryLaterException
|
||||
{
|
||||
if (!Util.isValidNumber(number)) {
|
||||
logger.info("Invalid number: " + number);
|
||||
throw new WebApplicationException(Response.status(400).build());
|
||||
}
|
||||
|
||||
String requester = Arrays.stream(forwardedFor.split(","))
|
||||
.map(String::trim)
|
||||
.reduce((a, b) -> b)
|
||||
.orElseThrow();
|
||||
String requester = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
|
||||
|
||||
Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number);
|
||||
CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, requester, captcha, storedChallenge, pushChallenge);
|
||||
|
||||
if (requirement.isCaptchaRequired()) {
|
||||
captchaRequiredMeter.mark();
|
||||
|
||||
if (requirement.isAutoBlock() && shouldAutoBlock(requester)) {
|
||||
logger.info("Auto-block: " + requester);
|
||||
abusiveHostRules.setBlockedHost(requester, "Auto-Block");
|
||||
@@ -203,35 +224,99 @@ public class AccountController {
|
||||
return Response.status(402).build();
|
||||
}
|
||||
|
||||
switch (transport) {
|
||||
case "sms":
|
||||
rateLimiters.getSmsDestinationLimiter().validate(number);
|
||||
break;
|
||||
case "voice":
|
||||
rateLimiters.getVoiceDestinationLimiter().validate(number);
|
||||
rateLimiters.getVoiceDestinationDailyLimiter().validate(number);
|
||||
break;
|
||||
default:
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
try {
|
||||
switch (transport) {
|
||||
case "sms":
|
||||
rateLimiters.getSmsDestinationLimiter().validate(number);
|
||||
break;
|
||||
case "voice":
|
||||
rateLimiters.getVoiceDestinationLimiter().validate(number);
|
||||
rateLimiters.getVoiceDestinationDailyLimiter().validate(number);
|
||||
break;
|
||||
default:
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
} catch (RateLimitExceededException e) {
|
||||
if (!e.getRetryDuration().isNegative()) {
|
||||
throw new RetryLaterException(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
VerificationCode verificationCode = generateVerificationCode(number);
|
||||
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
|
||||
System.currentTimeMillis(),
|
||||
storedChallenge.map(StoredVerificationCode::getPushCode).orElse(null));
|
||||
System.currentTimeMillis(),
|
||||
storedChallenge.map(StoredVerificationCode::getPushCode).orElse(null),
|
||||
storedChallenge.flatMap(StoredVerificationCode::getTwilioVerificationSid).orElse(null));
|
||||
|
||||
pendingAccounts.store(number, storedVerificationCode);
|
||||
|
||||
if (testDevices.containsKey(number)) {
|
||||
// noop
|
||||
} else if (transport.equals("sms")) {
|
||||
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
|
||||
} else if (transport.equals("voice")) {
|
||||
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), locale);
|
||||
final List<Locale.LanguageRange> languageRanges;
|
||||
|
||||
try {
|
||||
languageRanges = acceptLanguage.map(Locale.LanguageRange::parse).orElse(Collections.emptyList());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
return Response.status(400).build();
|
||||
}
|
||||
|
||||
final boolean enrolledInVerifyExperiment = verifyExperimentEnrollmentManager.isEnrolled(client, number, languageRanges, transport);
|
||||
final CompletableFuture<Optional<String>> sendVerificationWithTwilioVerifyFuture;
|
||||
|
||||
if (testDevices.containsKey(number)) {
|
||||
// noop
|
||||
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
|
||||
} else if (transport.equals("sms")) {
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
sendVerificationWithTwilioVerifyFuture = smsSender.deliverSmsVerificationWithTwilioVerify(number, client, verificationCode.getVerificationCode(), languageRanges);
|
||||
} else {
|
||||
smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay());
|
||||
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
|
||||
}
|
||||
} else if (transport.equals("voice")) {
|
||||
|
||||
if (enrolledInVerifyExperiment) {
|
||||
sendVerificationWithTwilioVerifyFuture = smsSender.deliverVoxVerificationWithTwilioVerify(number, verificationCode.getVerificationCode(), languageRanges);
|
||||
} else {
|
||||
smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), languageRanges);
|
||||
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
|
||||
}
|
||||
|
||||
} else {
|
||||
sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty());
|
||||
}
|
||||
|
||||
sendVerificationWithTwilioVerifyFuture.whenComplete((maybeVerificationSid, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Metrics.counter(TWILIO_VERIFY_ERROR_COUNTER_NAME).increment();
|
||||
|
||||
logger.warn("Error with Twilio Verify", throwable);
|
||||
return;
|
||||
}
|
||||
maybeVerificationSid.ifPresent(twilioVerificationSid -> {
|
||||
StoredVerificationCode storedVerificationCodeWithVerificationSid = new StoredVerificationCode(
|
||||
storedVerificationCode.getCode(),
|
||||
storedVerificationCode.getTimestamp(),
|
||||
storedVerificationCode.getPushCode(),
|
||||
twilioVerificationSid);
|
||||
pendingAccounts.store(number, storedVerificationCodeWithVerificationSid);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO Remove this meter when external dependencies have been resolved
|
||||
metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark();
|
||||
|
||||
{
|
||||
final List<Tag> tags = new ArrayList<>();
|
||||
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)));
|
||||
tags.add(Tag.of(VERFICATION_TRANSPORT_TAG_NAME, transport));
|
||||
tags.add(UserAgentTagUtil.getPlatformTag(userAgent));
|
||||
tags.add(Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(enrolledInVerifyExperiment)));
|
||||
|
||||
Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, tags).increment();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@@ -242,7 +327,8 @@ public class AccountController {
|
||||
@Path("/code/{verification_code}")
|
||||
public AccountCreationResult verifyAccount(@PathParam("verification_code") String verificationCode,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@HeaderParam("X-Signal-Agent") String signalAgent,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@QueryParam("transfer") Optional<Boolean> availableForTransfer,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
@@ -264,6 +350,9 @@ public class AccountController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
Optional<Account> existingAccount = accounts.get(number);
|
||||
Optional<StoredRegistrationLock> existingRegistrationLock = existingAccount.map(Account::getRegistrationLock);
|
||||
Optional<ExternalServiceCredentials> existingBackupCredentials = existingAccount.map(Account::getUuid)
|
||||
@@ -290,9 +379,21 @@ public class AccountController {
|
||||
throw new WebApplicationException(Response.status(409).build());
|
||||
}
|
||||
|
||||
Account account = createAccount(number, password, userAgent, accountAttributes);
|
||||
Account account = accounts.create(number, password, signalAgent, accountAttributes);
|
||||
|
||||
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
|
||||
{
|
||||
metricRegistry.meter(name(AccountController.class, "verify", Util.getCountryCode(number))).mark();
|
||||
|
||||
final List<Tag> tags = new ArrayList<>();
|
||||
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)));
|
||||
tags.add(UserAgentTagUtil.getPlatformTag(userAgent));
|
||||
tags.add(Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(storedVerificationCode.get().getTwilioVerificationSid().isPresent())));
|
||||
|
||||
Metrics.counter(ACCOUNT_VERIFY_COUNTER_NAME, tags).increment();
|
||||
|
||||
Metrics.timer(name(AccountController.class, "verifyDuration"), tags)
|
||||
.record(Instant.now().toEpochMilli() - storedVerificationCode.get().getTimestamp(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
return new AccountCreationResult(account.getUuid(), existingAccount.map(Account::isStorageSupported).orElse(false));
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
@@ -317,7 +418,6 @@ public class AccountController {
|
||||
public void setGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid GcmRegistrationId registrationId) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
boolean wasAccountEnabled = account.isEnabled();
|
||||
|
||||
if (device.getGcmId() != null &&
|
||||
device.getGcmId().equals(registrationId.getGcmRegistrationId()))
|
||||
@@ -325,16 +425,12 @@ public class AccountController {
|
||||
return;
|
||||
}
|
||||
|
||||
device.setApnId(null);
|
||||
device.setVoipApnId(null);
|
||||
device.setGcmId(registrationId.getGcmRegistrationId());
|
||||
device.setFetchesMessages(false);
|
||||
|
||||
accounts.update(account);
|
||||
|
||||
if (!wasAccountEnabled && account.isEnabled()) {
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
}
|
||||
accounts.updateDevice(account, device.getId(), d -> {
|
||||
d.setApnId(null);
|
||||
d.setVoipApnId(null);
|
||||
d.setGcmId(registrationId.getGcmRegistrationId());
|
||||
d.setFetchesMessages(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -343,11 +439,12 @@ public class AccountController {
|
||||
public void deleteGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setGcmId(null);
|
||||
device.setFetchesMessages(false);
|
||||
|
||||
accounts.update(account);
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
accounts.updateDevice(account, device.getId(), d -> {
|
||||
d.setGcmId(null);
|
||||
d.setFetchesMessages(false);
|
||||
d.setUserAgent("OWA");
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -357,17 +454,13 @@ public class AccountController {
|
||||
public void setApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid ApnRegistrationId registrationId) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
boolean wasAccountEnabled = account.isEnabled();
|
||||
|
||||
device.setApnId(registrationId.getApnRegistrationId());
|
||||
device.setVoipApnId(registrationId.getVoipRegistrationId());
|
||||
device.setGcmId(null);
|
||||
device.setFetchesMessages(false);
|
||||
accounts.update(account);
|
||||
|
||||
if (!wasAccountEnabled && account.isEnabled()) {
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
}
|
||||
accounts.updateDevice(account, device.getId(), d -> {
|
||||
d.setApnId(registrationId.getApnRegistrationId());
|
||||
d.setVoipApnId(registrationId.getVoipRegistrationId());
|
||||
d.setGcmId(null);
|
||||
d.setFetchesMessages(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -376,11 +469,16 @@ public class AccountController {
|
||||
public void deleteApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setApnId(null);
|
||||
device.setFetchesMessages(false);
|
||||
|
||||
accounts.update(account);
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
accounts.updateDevice(account, device.getId(), d -> {
|
||||
d.setApnId(null);
|
||||
d.setFetchesMessages(false);
|
||||
if (d.getId() == 1) {
|
||||
d.setUserAgent("OWI");
|
||||
} else {
|
||||
d.setUserAgent("OWP");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -389,37 +487,43 @@ public class AccountController {
|
||||
@Path("/registration_lock")
|
||||
public void setRegistrationLock(@Auth Account account, @Valid RegistrationLock accountLock) {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
account.setPin(null);
|
||||
|
||||
accounts.update(account);
|
||||
accounts.update(account, a -> {
|
||||
a.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
a.setPin(null);
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/registration_lock")
|
||||
public void removeRegistrationLock(@Auth Account account) {
|
||||
account.setRegistrationLock(null, null);
|
||||
accounts.update(account);
|
||||
accounts.update(account, a -> a.setRegistrationLock(null, null));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/pin/")
|
||||
public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock) {
|
||||
account.setPin(accountLock.getPin());
|
||||
account.setRegistrationLock(null, null);
|
||||
public void setPin(@Auth Account account, @Valid DeprecatedPin accountLock, @HeaderParam("User-Agent") String userAgent) {
|
||||
// TODO Remove once PIN-based reglocks have been deprecated
|
||||
logger.info("PIN set by User-Agent: {}", userAgent);
|
||||
|
||||
accounts.update(account);
|
||||
accounts.update(account, a -> {
|
||||
a.setPin(accountLock.getPin());
|
||||
a.setRegistrationLock(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/pin/")
|
||||
public void removePin(@Auth Account account) {
|
||||
account.setPin(null);
|
||||
accounts.update(account);
|
||||
|
||||
public void removePin(@Auth Account account, @HeaderParam("User-Agent") String userAgent) {
|
||||
// TODO Remove once PIN-based reglocks have been deprecated
|
||||
logger.info("PIN removed by User-Agent: {}", userAgent);
|
||||
|
||||
accounts.update(account, a -> a.setPin(null));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -427,17 +531,14 @@ public class AccountController {
|
||||
@Path("/name/")
|
||||
public void setName(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid DeviceName deviceName) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
account.getAuthenticatedDevice().get().setName(deviceName.getDeviceName());
|
||||
accounts.update(account);
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
accounts.updateDevice(account, device.getId(), d -> d.setName(deviceName.getDeviceName()));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/signaling_key")
|
||||
public void removeSignalingKey(@Auth DisabledPermittedAccount disabledPermittedAccount) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
account.getAuthenticatedDevice().get().setSignalingKey(null);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -449,30 +550,25 @@ public class AccountController {
|
||||
@Valid AccountAttributes attributes)
|
||||
{
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
long deviceId = account.getAuthenticatedDevice().get().getId();
|
||||
|
||||
device.setFetchesMessages(attributes.getFetchesMessages());
|
||||
device.setName(attributes.getName());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setCapabilities(attributes.getCapabilities());
|
||||
device.setRegistrationId(attributes.getRegistrationId());
|
||||
device.setSignalingKey(attributes.getSignalingKey());
|
||||
device.setUserAgent(userAgent);
|
||||
accounts.update(account, a-> {
|
||||
|
||||
setAccountRegistrationLockFromAttributes(account, attributes);
|
||||
a.getDevice(deviceId).ifPresent(d -> {
|
||||
d.setFetchesMessages(attributes.getFetchesMessages());
|
||||
d.setName(attributes.getName());
|
||||
d.setLastSeen(Util.todayInMillis());
|
||||
d.setCapabilities(attributes.getCapabilities());
|
||||
d.setRegistrationId(attributes.getRegistrationId());
|
||||
d.setUserAgent(userAgent);
|
||||
});
|
||||
|
||||
final boolean hasDiscoverabilityChange = (account.isDiscoverableByPhoneNumber() != attributes.isDiscoverableByPhoneNumber());
|
||||
a.setRegistrationLockFromAttributes(attributes);
|
||||
|
||||
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
|
||||
account.setPayments(attributes.getPayments());
|
||||
account.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
|
||||
|
||||
accounts.update(account);
|
||||
|
||||
if (hasDiscoverabilityChange) {
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
}
|
||||
a.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
|
||||
a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
|
||||
a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
|
||||
});
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -519,16 +615,6 @@ public class AccountController {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/payments")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setPayments(@Auth Account account, @Valid PaymentAddressList payments) {
|
||||
account.setPayments(payments.getPayments());
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor,
|
||||
String requester,
|
||||
Optional<String> captchaToken,
|
||||
@@ -536,68 +622,91 @@ public class AccountController {
|
||||
Optional<String> pushChallenge)
|
||||
{
|
||||
|
||||
if (captchaToken.isPresent()) {
|
||||
boolean validToken = recaptchaClient.verify(captchaToken.get(), requester);
|
||||
|
||||
if (validToken) {
|
||||
captchaSuccessMeter.mark();
|
||||
return new CaptchaRequirement(false, false);
|
||||
} else {
|
||||
captchaFailureMeter.mark();
|
||||
return new CaptchaRequirement(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
final String countryCode = Util.getCountryCode(number);
|
||||
{
|
||||
final List<Tag> tags = new ArrayList<>();
|
||||
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, countryCode));
|
||||
|
||||
try {
|
||||
if (pushChallenge.isPresent()) {
|
||||
tags.add(Tag.of(CHALLENGE_PRESENT_TAG_NAME, "true"));
|
||||
|
||||
Optional<String> storedPushChallenge = storedVerificationCode.map(StoredVerificationCode::getPushCode);
|
||||
|
||||
if (!pushChallenge.get().equals(storedPushChallenge.orElse(null))) {
|
||||
tags.add(Tag.of(CHALLENGE_MATCH_TAG_NAME, "false"));
|
||||
return new CaptchaRequirement(true, false);
|
||||
} else {
|
||||
tags.add(Tag.of(CHALLENGE_MATCH_TAG_NAME, "true"));
|
||||
}
|
||||
} else {
|
||||
tags.add(Tag.of(CHALLENGE_PRESENT_TAG_NAME, "false"));
|
||||
|
||||
return new CaptchaRequirement(true, false);
|
||||
}
|
||||
} finally {
|
||||
Metrics.counter(PUSH_CHALLENGE_COUNTER_NAME, tags).increment();
|
||||
}
|
||||
}
|
||||
|
||||
List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(requester);
|
||||
|
||||
for (AbusiveHostRule abuseRule : abuseRules) {
|
||||
if (abuseRule.isBlocked()) {
|
||||
logger.info("Blocked host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
blockedHostMeter.mark();
|
||||
return new CaptchaRequirement(true, false);
|
||||
}
|
||||
|
||||
if (!abuseRule.getRegions().isEmpty()) {
|
||||
if (abuseRule.getRegions().stream().noneMatch(number::startsWith)) {
|
||||
logger.info("Restricted host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
filteredHostMeter.mark();
|
||||
return new CaptchaRequirement(true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
rateLimiters.getSmsVoiceIpLimiter().validate(requester);
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.info("Rate limited exceeded: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
rateLimitedHostMeter.mark();
|
||||
return new CaptchaRequirement(true, true);
|
||||
}
|
||||
|
||||
try {
|
||||
rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number));
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.info("Prefix rate limit exceeded: " + transport + ", " + number + ", (" + forwardedFor + ")");
|
||||
rateLimitedPrefixMeter.mark();
|
||||
return new CaptchaRequirement(true, true);
|
||||
}
|
||||
|
||||
DynamicSignupCaptchaConfiguration signupCaptchaConfig = dynamicConfigurationManager.getConfiguration().getSignupCaptchaConfiguration();
|
||||
if (signupCaptchaConfig.getCountryCodes().contains(countryCode)) {
|
||||
return new CaptchaRequirement(true, false);
|
||||
}
|
||||
|
||||
return new CaptchaRequirement(false, false);
|
||||
// if (captchaToken.isPresent()) {
|
||||
// boolean validToken = recaptchaClient.verify(captchaToken.get(), requester);
|
||||
//
|
||||
// if (validToken) {
|
||||
// captchaSuccessMeter.mark();
|
||||
// return new CaptchaRequirement(false, false);
|
||||
// } else {
|
||||
// captchaFailureMeter.mark();
|
||||
// return new CaptchaRequirement(true, false);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (pushChallenge.isPresent()) {
|
||||
// Optional<String> storedPushChallenge = storedVerificationCode.map(StoredVerificationCode::getPushCode);
|
||||
//
|
||||
// if (!pushChallenge.get().equals(storedPushChallenge.orElse(null))) {
|
||||
// return new CaptchaRequirement(true, false);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(requester);
|
||||
//
|
||||
// for (AbusiveHostRule abuseRule : abuseRules) {
|
||||
// if (abuseRule.isBlocked()) {
|
||||
// logger.info("Blocked host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
// blockedHostMeter.mark();
|
||||
// return new CaptchaRequirement(true, false);
|
||||
// }
|
||||
//
|
||||
// if (!abuseRule.getRegions().isEmpty()) {
|
||||
// if (abuseRule.getRegions().stream().noneMatch(number::startsWith)) {
|
||||
// logger.info("Restricted host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
// filteredHostMeter.mark();
|
||||
// return new CaptchaRequirement(true, false);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// rateLimiters.getSmsVoiceIpLimiter().validate(requester);
|
||||
// } catch (RateLimitExceededException e) {
|
||||
// logger.info("Rate limited exceeded: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
// rateLimitedHostMeter.mark();
|
||||
// return new CaptchaRequirement(true, true);
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number));
|
||||
// } catch (RateLimitExceededException e) {
|
||||
// logger.info("Prefix rate limit exceeded: " + transport + ", " + number + ", (" + forwardedFor + ")");
|
||||
// rateLimitedPrefixMeter.mark();
|
||||
// return new CaptchaRequirement(true, true);
|
||||
// }
|
||||
//
|
||||
// return new CaptchaRequirement(false, false);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/me")
|
||||
public void deleteAccount(@Auth Account account) {
|
||||
public void deleteAccount(@Auth Account account) throws InterruptedException {
|
||||
accounts.delete(account, AccountsManager.DeletionReason.USER_REQUEST);
|
||||
}
|
||||
|
||||
@@ -611,54 +720,6 @@ public class AccountController {
|
||||
return false;
|
||||
}
|
||||
|
||||
private Account createAccount(String number, String password, String userAgent, AccountAttributes accountAttributes) {
|
||||
Optional<Account> maybeExistingAccount = accounts.get(number);
|
||||
|
||||
Device device = new Device();
|
||||
device.setId(Device.MASTER_ID);
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setCapabilities(accountAttributes.getCapabilities());
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setUserAgent(userAgent);
|
||||
|
||||
Account account = new Account();
|
||||
account.setNumber(number);
|
||||
account.setUuid(UUID.randomUUID());
|
||||
account.addDevice(device);
|
||||
setAccountRegistrationLockFromAttributes(account, accountAttributes);
|
||||
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
|
||||
account.setPayments(accountAttributes.getPayments());
|
||||
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
|
||||
|
||||
if (accounts.create(account)) {
|
||||
newUserMeter.mark();
|
||||
}
|
||||
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
messagesManager.clear(number, maybeExistingAccount.map(Account::getUuid).orElse(null));
|
||||
pendingAccounts.remove(number);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
private void setAccountRegistrationLockFromAttributes(Account account, @Valid AccountAttributes attributes) {
|
||||
if (!Util.isEmpty(attributes.getPin())) {
|
||||
account.setPin(attributes.getPin());
|
||||
} else if (!Util.isEmpty(attributes.getRegistrationLock())) {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials(attributes.getRegistrationLock());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
} else {
|
||||
account.setPin(null);
|
||||
account.setRegistrationLock(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting protected
|
||||
VerificationCode generateVerificationCode(String number) {
|
||||
if (testDevices.containsKey(number)) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Base64;
|
||||
@@ -45,7 +46,7 @@ public class AttachmentControllerV3 extends AttachmentControllerBase {
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull String domain, @Nonnull String email, int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
|
||||
throws IOException, InvalidKeyException {
|
||||
throws IOException, InvalidKeyException, InvalidKeySpecException {
|
||||
this.rateLimiter = rateLimiters.getAttachmentLimiter();
|
||||
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
|
||||
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
|
||||
|
||||
@@ -5,17 +5,16 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.signal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@@ -24,22 +23,24 @@ import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.signal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.GroupCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/certificate")
|
||||
public class CertificateController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(CertificateController.class);
|
||||
|
||||
private final CertificateGenerator certificateGenerator;
|
||||
private final ServerZkAuthOperations serverZkAuthOperations;
|
||||
private final boolean isZkEnabled;
|
||||
|
||||
private static final String GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME = name(CertificateGenerator.class, "generateCertificate");
|
||||
private static final String INCLUDE_E164_TAG_NAME = "includeE164";
|
||||
|
||||
public CertificateController(CertificateGenerator certificateGenerator, ServerZkAuthOperations serverZkAuthOperations, boolean isZkEnabled) {
|
||||
this.certificateGenerator = certificateGenerator;
|
||||
this.serverZkAuthOperations = serverZkAuthOperations;
|
||||
@@ -51,8 +52,8 @@ public class CertificateController {
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/delivery")
|
||||
public DeliveryCertificate getDeliveryCertificate(@Auth Account account,
|
||||
@QueryParam("includeE164") Optional<Boolean> includeE164)
|
||||
throws IOException, InvalidKeyException
|
||||
@QueryParam("includeE164") Optional<Boolean> maybeIncludeE164)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (account.getAuthenticatedDevice().isEmpty()) {
|
||||
throw new AssertionError();
|
||||
@@ -61,7 +62,11 @@ public class CertificateController {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), includeE164.orElse(true)));
|
||||
final boolean includeE164 = maybeIncludeE164.orElse(true);
|
||||
|
||||
Metrics.counter(GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME, INCLUDE_E164_TAG_NAME, String.valueOf(includeE164)).increment();
|
||||
|
||||
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get(), includeE164));
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import java.util.NoSuchElementException;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.whispersystems.textsecuregcm.entities.AnswerChallengeRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.AnswerPushChallengeRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.AnswerRecaptchaChallengeRequest;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
|
||||
|
||||
@Path("/v1/challenge")
|
||||
public class ChallengeController {
|
||||
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
|
||||
public ChallengeController(final RateLimitChallengeManager rateLimitChallengeManager) {
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response handleChallengeResponse(@Auth final Account account,
|
||||
@Valid final AnswerChallengeRequest answerRequest,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor) throws RetryLaterException {
|
||||
|
||||
try {
|
||||
if (answerRequest instanceof AnswerPushChallengeRequest) {
|
||||
final AnswerPushChallengeRequest pushChallengeRequest = (AnswerPushChallengeRequest) answerRequest;
|
||||
|
||||
rateLimitChallengeManager.answerPushChallenge(account, pushChallengeRequest.getChallenge());
|
||||
} else if (answerRequest instanceof AnswerRecaptchaChallengeRequest) {
|
||||
try {
|
||||
|
||||
final AnswerRecaptchaChallengeRequest recaptchaChallengeRequest = (AnswerRecaptchaChallengeRequest) answerRequest;
|
||||
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
|
||||
|
||||
rateLimitChallengeManager.answerRecaptchaChallenge(account, recaptchaChallengeRequest.getCaptcha(), mostRecentProxy);
|
||||
|
||||
} catch (final NoSuchElementException e) {
|
||||
return Response.status(400).build();
|
||||
}
|
||||
}
|
||||
} catch (final RateLimitExceededException e) {
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
|
||||
return Response.status(200).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/push")
|
||||
public Response requestPushChallenge(@Auth final Account account) {
|
||||
try {
|
||||
rateLimitChallengeManager.sendPushChallenge(account);
|
||||
return Response.status(200).build();
|
||||
} catch (final NotPushRegisteredException e) {
|
||||
return Response.status(404).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,9 @@ import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
@@ -55,24 +56,24 @@ public class DeviceController {
|
||||
|
||||
private static final int MAX_DEVICES = 6;
|
||||
|
||||
private final PendingDevicesManager pendingDevices;
|
||||
private final StoredVerificationCodeManager pendingDevices;
|
||||
private final AccountsManager accounts;
|
||||
private final MessagesManager messages;
|
||||
private final KeysDynamoDb keys;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Map<String, Integer> maxDeviceConfiguration;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
|
||||
public DeviceController(PendingDevicesManager pendingDevices,
|
||||
public DeviceController(StoredVerificationCodeManager pendingDevices,
|
||||
AccountsManager accounts,
|
||||
MessagesManager messages,
|
||||
DirectoryQueue directoryQueue,
|
||||
KeysDynamoDb keys,
|
||||
RateLimiters rateLimiters,
|
||||
Map<String, Integer> maxDeviceConfiguration)
|
||||
{
|
||||
this.pendingDevices = pendingDevices;
|
||||
this.accounts = accounts;
|
||||
this.messages = messages;
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.keys = keys;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.maxDeviceConfiguration = maxDeviceConfiguration;
|
||||
}
|
||||
@@ -99,10 +100,11 @@ public class DeviceController {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
account.removeDevice(deviceId);
|
||||
accounts.update(account);
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
messages.clear(account.getNumber(), account.getUuid(), deviceId);
|
||||
messages.clear(account.getUuid(), deviceId);
|
||||
account = accounts.update(account, a -> a.removeDevice(deviceId));
|
||||
keys.delete(account, deviceId);
|
||||
// ensure any messages that came in after the first clear() are also removed
|
||||
messages.clear(account.getUuid(), deviceId);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -131,6 +133,7 @@ public class DeviceController {
|
||||
VerificationCode verificationCode = generateVerificationCode();
|
||||
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
|
||||
System.currentTimeMillis(),
|
||||
null,
|
||||
null);
|
||||
|
||||
pendingDevices.store(account.getNumber(), storedVerificationCode);
|
||||
@@ -188,17 +191,17 @@ public class DeviceController {
|
||||
Device device = new Device();
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setId(account.get().getNextDeviceId());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
device.setCapabilities(accountAttributes.getCapabilities());
|
||||
|
||||
account.get().addDevice(device);
|
||||
messages.clear(account.get().getNumber(), account.get().getUuid(), device.getId());
|
||||
accounts.update(account.get());
|
||||
accounts.update(account.get(), a -> {
|
||||
device.setId(account.get().getNextDeviceId());
|
||||
messages.clear(account.get().getUuid(), device.getId());
|
||||
a.addDevice(device);
|
||||
});;
|
||||
|
||||
pendingDevices.remove(number);
|
||||
|
||||
@@ -222,8 +225,8 @@ public class DeviceController {
|
||||
@Path("/capabilities")
|
||||
public void setCapabiltities(@Auth Account account, @Valid DeviceCapabilities capabilities) {
|
||||
assert(account.getAuthenticatedDevice().isPresent());
|
||||
account.getAuthenticatedDevice().get().setCapabilities(capabilities);
|
||||
accounts.update(account);
|
||||
final long deviceId = account.getAuthenticatedDevice().get().getId();
|
||||
accounts.updateDevice(account, deviceId, d -> d.setCapabilities(capabilities));
|
||||
}
|
||||
|
||||
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||
@@ -235,9 +238,9 @@ public class DeviceController {
|
||||
private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) {
|
||||
boolean isDowngrade = false;
|
||||
|
||||
if (account.isGv1MigrationSupported() && !capabilities.isGv1Migration()) {
|
||||
isDowngrade = true;
|
||||
}
|
||||
isDowngrade |= account.isAnnouncementGroupSupported() && !capabilities.isAnnouncementGroup();
|
||||
isDowngrade |= account.isSenderKeySupported() && !capabilities.isSenderKey();
|
||||
isDowngrade |= account.isGv1MigrationSupported() && !capabilities.isGv1Migration();
|
||||
|
||||
if (account.isGroupsV2Supported()) {
|
||||
try {
|
||||
|
||||
@@ -4,92 +4,25 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.entities.DirectoryFeedbackRequest;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
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 javax.ws.rs.core.Response.Status;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/directory")
|
||||
public class DirectoryController {
|
||||
|
||||
private static final String[] FEEDBACK_STATUSES = {
|
||||
"ok",
|
||||
"mismatch",
|
||||
"attestation-error",
|
||||
"unexpected-error",
|
||||
};
|
||||
|
||||
private static final String[] FRONTED_REGIONS = {"+20", "+971", "+968", "+974"};
|
||||
|
||||
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 Meter contactsMeter = metricRegistry.meter(name(getClass(), "contactRate"));
|
||||
|
||||
private final Map<String, Meter> iosFeedbackMeters = new HashMap<String, Meter>() {{
|
||||
for (String status : FEEDBACK_STATUSES) {
|
||||
put(status, metricRegistry.meter(name(DirectoryController.class, "feedback-v2", "ios", status)));
|
||||
}
|
||||
}};
|
||||
private final Map<String, Meter> androidFeedbackMeters = new HashMap<String, Meter>() {{
|
||||
for (String status : FEEDBACK_STATUSES) {
|
||||
put(status, metricRegistry.meter(name(DirectoryController.class, "feedback-v2", "android", status)));
|
||||
}
|
||||
}};
|
||||
private final Map<String, Meter> unknownFeedbackMeters = new HashMap<String, Meter>() {{
|
||||
for (String status : FEEDBACK_STATUSES) {
|
||||
put(status, metricRegistry.meter(name(DirectoryController.class, "feedback-v2", "unknown", status)));
|
||||
}
|
||||
}};
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final DirectoryManager directory;
|
||||
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
|
||||
|
||||
public DirectoryController(RateLimiters rateLimiters,
|
||||
DirectoryManager directory,
|
||||
ExternalServiceCredentialGenerator userTokenGenerator)
|
||||
{
|
||||
this.directory = directory;
|
||||
this.rateLimiters = rateLimiters;
|
||||
public DirectoryController(ExternalServiceCredentialGenerator userTokenGenerator) {
|
||||
this.directoryServiceTokenGenerator = userTokenGenerator;
|
||||
}
|
||||
|
||||
@@ -105,29 +38,8 @@ public class DirectoryController {
|
||||
@Path("/feedback-v3/{status}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response setFeedback(@Auth Account account,
|
||||
@PathParam("status") String status,
|
||||
@Valid DirectoryFeedbackRequest request)
|
||||
{
|
||||
Map<String, Meter> platformFeedbackMeters = unknownFeedbackMeters;
|
||||
|
||||
Optional<Device> masterDevice = account.getMasterDevice();
|
||||
if (masterDevice.isPresent()) {
|
||||
if (masterDevice.get().getApnId() != null) {
|
||||
platformFeedbackMeters = iosFeedbackMeters;
|
||||
} else if (masterDevice.get().getGcmId() != null) {
|
||||
platformFeedbackMeters = androidFeedbackMeters;
|
||||
}
|
||||
}
|
||||
|
||||
Optional<Meter> meter = Optional.ofNullable(platformFeedbackMeters.get(status));
|
||||
if (meter.isPresent()) {
|
||||
meter.get().mark();
|
||||
|
||||
return Response.ok().build();
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
public Response setFeedback(@Auth Account account) {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@@ -135,21 +47,8 @@ public class DirectoryController {
|
||||
@GET
|
||||
@Path("/{token}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getTokenPresence(@Auth Account account, @PathParam("token") String token)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getContactsLimiter().validate(account.getNumber());
|
||||
|
||||
try {
|
||||
Optional<ClientContact> contact = directory.get(decodeToken(token));
|
||||
|
||||
if (contact.isPresent()) return Response.ok().entity(contact.get()).build();
|
||||
else return Response.status(404).build();
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.info("Bad token", e);
|
||||
return Response.status(404).build();
|
||||
}
|
||||
public Response getTokenPresence(@Auth Account account) {
|
||||
return Response.status(429).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -157,40 +56,7 @@ public class DirectoryController {
|
||||
@Path("/tokens")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public ClientContacts getContactIntersection(@Auth Account account,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor,
|
||||
@Valid ClientContactTokens contacts)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
String requester = Arrays.stream(forwardedFor.split(","))
|
||||
.map(String::trim)
|
||||
.reduce((a, b) -> b)
|
||||
.orElseThrow();
|
||||
|
||||
if (Stream.of(FRONTED_REGIONS).noneMatch(region -> account.getNumber().startsWith(region))) {
|
||||
rateLimiters.getContactsIpLimiter().validate(requester);
|
||||
}
|
||||
|
||||
rateLimiters.getContactsLimiter().validate(account.getNumber(), contacts.getContacts().size());
|
||||
contactsHistogram.update(contacts.getContacts().size());
|
||||
contactsMeter.mark(contacts.getContacts().size());
|
||||
|
||||
try {
|
||||
List<byte[]> tokens = new LinkedList<>();
|
||||
|
||||
for (String encodedContact : contacts.getContacts()) {
|
||||
tokens.add(decodeToken(encodedContact));
|
||||
}
|
||||
|
||||
List<ClientContact> intersection = directory.get(tokens);
|
||||
return new ClientContacts(intersection);
|
||||
} catch (IOException e) {
|
||||
logger.info("Bad token", e);
|
||||
throw new WebApplicationException(Response.status(400).build());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeToken(String encoded) throws IOException {
|
||||
return Base64.decodeWithoutPadding(encoded.replace('-', '+').replace('_', '/'));
|
||||
public Response getContactIntersection(@Auth Account account) {
|
||||
return Response.status(429).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.util.Strings;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationResponse;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@Path("/v1/donation")
|
||||
public class DonationController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DonationController.class);
|
||||
|
||||
private final URI uri;
|
||||
private final String apiKey;
|
||||
private final String description;
|
||||
private final Set<String> supportedCurrencies;
|
||||
private final FaultTolerantHttpClient httpClient;
|
||||
|
||||
public DonationController(final Executor executor, final DonationConfiguration configuration) {
|
||||
this.uri = URI.create(configuration.getUri());
|
||||
this.apiKey = configuration.getApiKey();
|
||||
this.description = configuration.getDescription();
|
||||
this.supportedCurrencies = configuration.getSupportedCurrencies();
|
||||
this.httpClient = FaultTolerantHttpClient.newBuilder()
|
||||
.withCircuitBreaker(configuration.getCircuitBreaker())
|
||||
.withRetry(configuration.getRetry())
|
||||
.withVersion(Version.HTTP_2)
|
||||
.withConnectTimeout(Duration.ofSeconds(10))
|
||||
.withRedirect(Redirect.NEVER)
|
||||
.withExecutor(executor)
|
||||
.withName("donation")
|
||||
.withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_3)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/authorize-apple-pay")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getApplePayAuthorization(@Auth Account account, @Valid ApplePayAuthorizationRequest request) {
|
||||
if (!supportedCurrencies.contains(request.getCurrency())) {
|
||||
return CompletableFuture.completedFuture(Response.status(422).build());
|
||||
}
|
||||
|
||||
final Map<String, String> formData = new HashMap<>();
|
||||
formData.put("amount", Long.toString(request.getAmount()));
|
||||
formData.put("currency", request.getCurrency());
|
||||
if (!Strings.isNullOrEmpty(description)) {
|
||||
formData.put("description", description);
|
||||
}
|
||||
final HttpRequest httpRequest = HttpRequest.newBuilder()
|
||||
.uri(uri)
|
||||
.POST(FormDataBodyPublisher.of(formData))
|
||||
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString(
|
||||
(apiKey + ":").getBytes(StandardCharsets.UTF_8)))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.build();
|
||||
return httpClient.sendAsync(httpRequest, BodyHandlers.ofString())
|
||||
.thenApply(this::processApplePayAuthorizationRemoteResponse);
|
||||
}
|
||||
|
||||
private Response processApplePayAuthorizationRemoteResponse(HttpResponse<String> response) {
|
||||
ObjectMapper mapper = SystemMapper.getMapper();
|
||||
|
||||
if (response.statusCode() >= 200 && response.statusCode() < 300 &&
|
||||
MediaType.APPLICATION_JSON.equalsIgnoreCase(response.headers().firstValue("Content-Type").orElse(null))) {
|
||||
try {
|
||||
final JsonNode jsonResponse = mapper.readTree(response.body());
|
||||
final String id = jsonResponse.get("id").asText(null);
|
||||
final String clientSecret = jsonResponse.get("client_secret").asText(null);
|
||||
if (Strings.isNullOrEmpty(id) || Strings.isNullOrEmpty(clientSecret)) {
|
||||
logger.warn("missing fields in json response in donation controller");
|
||||
return Response.status(500).build();
|
||||
}
|
||||
final String responseJson = mapper.writeValueAsString(new ApplePayAuthorizationResponse(id, clientSecret));
|
||||
return Response.ok(responseJson, MediaType.APPLICATION_JSON_TYPE).build();
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("json processing error in donation controller", e);
|
||||
return Response.status(500).build();
|
||||
}
|
||||
} else {
|
||||
logger.warn("unexpected response code returned to donation controller");
|
||||
return Response.status(500).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.storage.FeatureFlagsManager;
|
||||
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.FormParam;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/v1/featureflag")
|
||||
public class FeatureFlagsController {
|
||||
|
||||
private final FeatureFlagsManager featureFlagsManager;
|
||||
private final List<byte[]> authorizedTokens;
|
||||
|
||||
public FeatureFlagsController(final FeatureFlagsManager featureFlagsManager, final List<String> authorizedTokens) {
|
||||
this.featureFlagsManager = featureFlagsManager;
|
||||
this.authorizedTokens = authorizedTokens.stream().map(token -> token.getBytes(StandardCharsets.UTF_8)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Map<String, Boolean> get(@HeaderParam("Token") final String token) {
|
||||
if (!isAuthorized(token)) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return featureFlagsManager.getAllFlags();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/{featureFlag}")
|
||||
public void set(@HeaderParam("Token") final String token, @PathParam("featureFlag") final String featureFlag, @FormParam("active") final boolean active) {
|
||||
if (!isAuthorized(token)) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
featureFlagsManager.setFeatureFlag(featureFlag, active);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/{featureFlag}")
|
||||
public void delete(@HeaderParam("Token") final String token, @PathParam("featureFlag") final String featureFlag) {
|
||||
if (!isAuthorized(token)) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
featureFlagsManager.deleteFeatureFlag(featureFlag);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean isAuthorized(final String token) {
|
||||
if (token == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final byte[] tokenBytes = token.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
boolean authorized = false;
|
||||
|
||||
for (final byte[] authorizedToken : authorizedTokens) {
|
||||
//noinspection IfStatementMissingBreakInLoop
|
||||
if (MessageDigest.isEqual(authorizedToken, tokenBytes)) {
|
||||
authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
return authorized;
|
||||
}
|
||||
}
|
||||
@@ -4,28 +4,17 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
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 io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -37,32 +26,64 @@ import javax.ws.rs.Produces;
|
||||
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 java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v2/keys")
|
||||
public class KeysController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KeysController.class);
|
||||
private final RateLimiters rateLimiters;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final AccountsManager accounts;
|
||||
private final PreKeyRateLimiter preKeyRateLimiter;
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final DynamicConfigurationManager dynamicConfigurationManager;
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts, DirectoryQueue directoryQueue) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
this.directoryQueue = directoryQueue;
|
||||
private static final String PREKEY_REQUEST_COUNTER_NAME = name(KeysController.class, "preKeyGet");
|
||||
private static final String RATE_LIMITED_GET_PREKEYS_COUNTER_NAME = name(KeysController.class, "rateLimitedGetPreKeys");
|
||||
|
||||
private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
|
||||
private static final String INTERNATIONAL_TAG_NAME = "international";
|
||||
private static final String PREKEY_TARGET_IDENTIFIER_TAG_NAME = "identifierType";
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts,
|
||||
PreKeyRateLimiter preKeyRateLimiter,
|
||||
DynamicConfigurationManager dynamicConfigurationManager,
|
||||
RateLimitChallengeManager rateLimitChallengeManager) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keysDynamoDb = keysDynamoDb;
|
||||
this.accounts = accounts;
|
||||
this.preKeyRateLimiter = preKeyRateLimiter;
|
||||
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKeyCount getStatus(@Auth Account account) {
|
||||
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
|
||||
int count = keysDynamoDb.getCount(account, account.getAuthenticatedDevice().get().getId());
|
||||
|
||||
if (count > 0) {
|
||||
count = count - 1;
|
||||
@@ -77,40 +98,36 @@ public class KeysController {
|
||||
public void setKeys(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid PreKeyState preKeys) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
boolean wasAccountEnabled = account.isEnabled();
|
||||
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);
|
||||
|
||||
if (!wasAccountEnabled && account.isEnabled()) {
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
}
|
||||
account = accounts.update(account, a -> {
|
||||
a.getDevice(device.getId()).ifPresent(d -> d.setSignedPreKey(preKeys.getSignedPreKey()));
|
||||
a.setIdentityKey(preKeys.getIdentityKey());
|
||||
});
|
||||
}
|
||||
|
||||
keys.store(account.getNumber(), device.getId(), preKeys.getPreKeys());
|
||||
keysDynamoDb.store(account, device.getId(), preKeys.getPreKeys());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/{identifier}/{device_id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Optional<PreKeyResponse> getDeviceKeys(@Auth Optional<Account> account,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("identifier") AmbiguousIdentifier targetName,
|
||||
@PathParam("device_id") String deviceId)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
public Response getDeviceKeys(@Auth Optional<Account> account,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("identifier") AmbiguousIdentifier targetName,
|
||||
@PathParam("device_id") String deviceId,
|
||||
@HeaderParam("User-Agent") String userAgent)
|
||||
throws RateLimitExceededException, RateLimitChallengeException {
|
||||
if (!account.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
@@ -120,32 +137,53 @@ public class KeysController {
|
||||
|
||||
assert(target.isPresent());
|
||||
|
||||
if (account.isPresent()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(account.get().getNumber() + "." + account.get().getAuthenticatedDevice().get().getId() + "__" + target.get().getNumber() + "." + deviceId);
|
||||
{
|
||||
final String sourceCountryCode = account.map(a -> Util.getCountryCode(a.getNumber())).orElse("0");
|
||||
final String targetCountryCode = target.map(a -> Util.getCountryCode(a.getNumber())).orElseThrow();
|
||||
|
||||
Metrics.counter(PREKEY_REQUEST_COUNTER_NAME, Tags.of(
|
||||
SOURCE_COUNTRY_TAG_NAME, sourceCountryCode,
|
||||
INTERNATIONAL_TAG_NAME, String.valueOf(!sourceCountryCode.equals(targetCountryCode)),
|
||||
PREKEY_TARGET_IDENTIFIER_TAG_NAME, targetName.hasNumber() ? "number" : "uuid"
|
||||
)).increment();
|
||||
}
|
||||
|
||||
List<KeyRecord> targetKeys = getLocalKeys(target.get(), deviceId);
|
||||
List<PreKeyResponseItem> devices = new LinkedList<>();
|
||||
if (account.isPresent()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(account.get().getNumber() + "." + account.get().getAuthenticatedDevice().get().getId() + "__" + target.get().getNumber() + "." + deviceId);
|
||||
|
||||
for (Device device : target.get().getDevices()) {
|
||||
if (device.isEnabled() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
PreKey preKey = null;
|
||||
try {
|
||||
preKeyRateLimiter.validate(account.get());
|
||||
} catch (RateLimitExceededException e) {
|
||||
|
||||
for (KeyRecord keyRecord : targetKeys) {
|
||||
if (keyRecord.getDeviceId() == device.getId()) {
|
||||
preKey = new PreKey(keyRecord.getKeyId(), keyRecord.getPublicKey());
|
||||
}
|
||||
}
|
||||
final boolean enforceLimit = rateLimitChallengeManager.shouldIssueRateLimitChallenge(userAgent);
|
||||
|
||||
if (signedPreKey != null || preKey != null) {
|
||||
devices.add(new PreKeyResponseItem(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
|
||||
Metrics.counter(RATE_LIMITED_GET_PREKEYS_COUNTER_NAME,
|
||||
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.get().getNumber()),
|
||||
"enforced", String.valueOf(enforceLimit))
|
||||
.increment();
|
||||
|
||||
if (enforceLimit) {
|
||||
throw new RateLimitChallengeException(account.get(), e.getRetryDuration());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) return Optional.empty();
|
||||
else return Optional.of(new PreKeyResponse(target.get().getIdentityKey(), devices));
|
||||
Map<Long, PreKey> preKeysByDeviceId = getLocalKeys(target.get(), deviceId);
|
||||
List<PreKeyResponseItem> responseItems = new LinkedList<>();
|
||||
|
||||
for (Device device : target.get().getDevices()) {
|
||||
if (device.isEnabled() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
PreKey preKey = preKeysByDeviceId.get(device.getId());
|
||||
|
||||
if (signedPreKey != null || preKey != null) {
|
||||
responseItems.add(new PreKeyResponseItem(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (responseItems.isEmpty()) return Response.status(404).build();
|
||||
else return Response.ok().entity(new PreKeyResponse(target.get().getIdentityKey(), responseItems)).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -153,15 +191,9 @@ public class KeysController {
|
||||
@Path("/signed")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setSignedKey(@Auth Account account, @Valid SignedPreKey signedPreKey) {
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
boolean wasAccountEnabled = account.isEnabled();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
|
||||
device.setSignedPreKey(signedPreKey);
|
||||
accounts.update(account);
|
||||
|
||||
if (!wasAccountEnabled && account.isEnabled()) {
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
}
|
||||
accounts.updateDevice(account, device.getId(), d -> d.setSignedPreKey(signedPreKey));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -176,15 +208,17 @@ public class KeysController {
|
||||
else return Optional.empty();
|
||||
}
|
||||
|
||||
private List<KeyRecord> getLocalKeys(Account destination, String deviceIdSelector) {
|
||||
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector) {
|
||||
try {
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
return keys.get(destination.getNumber());
|
||||
return keysDynamoDb.take(destination);
|
||||
}
|
||||
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
|
||||
return keys.get(destination.getNumber(), deviceId);
|
||||
return keysDynamoDb.take(destination, deviceId)
|
||||
.map(preKey -> Map.of(deviceId, preKey))
|
||||
.orElse(Collections.emptyMap());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
|
||||
@@ -4,67 +4,108 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.lettuce.core.ScriptOutputType;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.CombinedUnidentifiedSenderAccessKeys;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicMessageRateConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountMismatchedDevices;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountStaleDevices;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type;
|
||||
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
|
||||
import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage.Recipient;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMultiRecipientMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/messages")
|
||||
@@ -74,35 +115,74 @@ public class MessageController {
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter unidentifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "unidentified"));
|
||||
private final Meter identifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "identified" ));
|
||||
private final Meter rejectOversizeMessageMeter = metricRegistry.meter(name(getClass(), "rejectOversizeMessage"));
|
||||
private final Meter rejectOver256kibMessageMeter = metricRegistry.meter(name(getClass(), "rejectOver256kibMessage"));
|
||||
private final Timer sendMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendMessageInternal"));
|
||||
private final Timer sendCommonMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendCommonMessageInternal"));
|
||||
private final Histogram outgoingMessageListSizeHistogram = metricRegistry.histogram(name(getClass(), "outgoingMessageListSize"));
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final MessageSender messageSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final MessageSender messageSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final UnsealedSenderRateLimiter unsealedSenderRateLimiter;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final DynamicConfigurationManager dynamicConfigurationManager;
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
private final ReportMessageManager reportMessageManager;
|
||||
private final ScheduledExecutorService receiptExecutorService;
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
private final ClusterLuaScript recordInternationalUnsealedSenderMetricsScript;
|
||||
|
||||
private static final String LEGACY_MESSAGE_SENT_COUNTER = name(MessageController.class, "legacyMessageSent");
|
||||
private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages");
|
||||
private static final String REJECT_UNSEALED_SENDER_COUNTER_NAME = name(MessageController.class, "rejectUnsealedSenderLimit");
|
||||
private static final String INTERNATIONAL_UNSEALED_SENDER_COUNTER_NAME = name(MessageController.class, "internationalUnsealedSender");
|
||||
private static final String UNSEALED_SENDER_WITHOUT_PUSH_TOKEN_COUNTER_NAME = name(MessageController.class, "unsealedSenderWithoutPushToken");
|
||||
private static final String DECLINED_DELIVERY_COUNTER = name(MessageController.class, "declinedDelivery");
|
||||
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = name(MessageController.class, "messageContentSize");
|
||||
private static final String OUTGOING_MESSAGE_LIST_SIZE_BYTES_DISTRIBUTION_NAME = name(MessageController.class, "outgoingMessageListSizeBytes");
|
||||
|
||||
private static final int MAX_MESSAGE_SIZE = 1024 * 1024;
|
||||
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
||||
private static final String SENDER_TYPE_TAG_NAME = "senderType";
|
||||
private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry";
|
||||
private static final String DESTINATION_TYPE_TAG_NAME = "destinationType";
|
||||
|
||||
private static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes();
|
||||
|
||||
public MessageController(RateLimiters rateLimiters,
|
||||
MessageSender messageSender,
|
||||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
MessagesManager messagesManager,
|
||||
ApnFallbackManager apnFallbackManager)
|
||||
MessageSender messageSender,
|
||||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
MessagesManager messagesManager,
|
||||
UnsealedSenderRateLimiter unsealedSenderRateLimiter,
|
||||
ApnFallbackManager apnFallbackManager,
|
||||
DynamicConfigurationManager dynamicConfigurationManager,
|
||||
RateLimitChallengeManager rateLimitChallengeManager,
|
||||
ReportMessageManager reportMessageManager,
|
||||
FaultTolerantRedisCluster metricsCluster,
|
||||
ScheduledExecutorService receiptExecutorService)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.messageSender = messageSender;
|
||||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.messageSender = messageSender;
|
||||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.unsealedSenderRateLimiter = unsealedSenderRateLimiter;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
this.reportMessageManager = reportMessageManager;
|
||||
this.receiptExecutorService = receiptExecutorService;
|
||||
|
||||
try {
|
||||
recordInternationalUnsealedSenderMetricsScript = ClusterLuaScript.fromResource(metricsCluster, "lua/record_international_unsealed_sender_metrics.lua", ScriptOutputType.MULTI);
|
||||
} catch (IOException e) {
|
||||
// This should never happen for a script included in our own resource bundle
|
||||
throw new AssertionError("Failed to load script", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -110,25 +190,38 @@ public class MessageController {
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public SendMessageResponse sendMessage(@Auth Optional<Account> source,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@PathParam("destination") AmbiguousIdentifier destinationName,
|
||||
@Valid IncomingMessageList messages)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (!source.isPresent() && !accessKey.isPresent()) {
|
||||
public Response sendMessage(@Auth Optional<Account> source,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor,
|
||||
@PathParam("destination") AmbiguousIdentifier destinationName,
|
||||
@Valid IncomingMessageList messages)
|
||||
throws RateLimitExceededException, RateLimitChallengeException {
|
||||
if (source.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (source.isPresent() && !source.get().isFor(destinationName)) {
|
||||
rateLimiters.getMessagesLimiter().validate(source.get().getNumber() + "__" + destinationName);
|
||||
assert source.get().getMasterDevice().isPresent();
|
||||
|
||||
final Device masterDevice = source.get().getMasterDevice().get();
|
||||
final String senderCountryCode = Util.getCountryCode(source.get().getNumber());
|
||||
|
||||
if (StringUtils.isAllBlank(masterDevice.getApnId(), masterDevice.getVoipApnId(), masterDevice.getGcmId()) || masterDevice.getUninstalledFeedbackTimestamp() > 0) {
|
||||
Metrics.counter(UNSEALED_SENDER_WITHOUT_PUSH_TOKEN_COUNTER_NAME, SENDER_COUNTRY_TAG_NAME, senderCountryCode).increment();
|
||||
}
|
||||
}
|
||||
|
||||
final String senderType;
|
||||
|
||||
if (source.isPresent() && !source.get().isFor(destinationName)) {
|
||||
identifiedMeter.mark();
|
||||
} else if (!source.isPresent()) {
|
||||
senderType = "identified";
|
||||
} else if (source.isEmpty()) {
|
||||
unidentifiedMeter.mark();
|
||||
senderType = "unidentified";
|
||||
} else {
|
||||
senderType = "self";
|
||||
}
|
||||
|
||||
for (final IncomingMessage message : messages.getMessages()) {
|
||||
@@ -142,11 +235,11 @@ public class MessageController {
|
||||
contentLength += message.getBody().length();
|
||||
}
|
||||
|
||||
Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, UserAgentTagUtil.getUserAgentTags(userAgent)).record(contentLength);
|
||||
Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(contentLength);
|
||||
|
||||
if (contentLength > MAX_MESSAGE_SIZE) {
|
||||
// TODO Reject the request
|
||||
rejectOversizeMessageMeter.mark();
|
||||
rejectOver256kibMessageMeter.mark();
|
||||
return Response.status(Response.Status.REQUEST_ENTITY_TOO_LARGE).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,49 +254,265 @@ public class MessageController {
|
||||
OptionalAccess.verify(source, accessKey, destination);
|
||||
assert(destination.isPresent());
|
||||
|
||||
if (source.isPresent() && !source.get().isFor(destinationName)) {
|
||||
rateLimiters.getMessagesLimiter().validate(source.get().getNumber() + "__" + destination.get().getUuid());
|
||||
|
||||
final String senderCountryCode = Util.getCountryCode(source.get().getNumber());
|
||||
|
||||
try {
|
||||
unsealedSenderRateLimiter.validate(source.get(), destination.get());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final boolean enforceLimit = rateLimitChallengeManager.shouldIssueRateLimitChallenge(userAgent);
|
||||
|
||||
Metrics.counter(REJECT_UNSEALED_SENDER_COUNTER_NAME,
|
||||
SENDER_COUNTRY_TAG_NAME, senderCountryCode,
|
||||
"enforced", String.valueOf(enforceLimit))
|
||||
.increment();
|
||||
|
||||
if (enforceLimit) {
|
||||
logger.debug("Rejected unsealed sender limit from: {}", source.get().getNumber());
|
||||
|
||||
throw new RateLimitChallengeException(source.get(), e.getRetryDuration());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
final String destinationCountryCode = Util.getCountryCode(destination.get().getNumber());
|
||||
final Device masterDevice = source.get().getMasterDevice().get();
|
||||
|
||||
if (!senderCountryCode.equals(destinationCountryCode)) {
|
||||
recordInternationalUnsealedSenderMetrics(forwardedFor, senderCountryCode, destination.get().getNumber());
|
||||
|
||||
if (StringUtils.isAllBlank(masterDevice.getApnId(), masterDevice.getVoipApnId(), masterDevice.getGcmId()) || masterDevice.getUninstalledFeedbackTimestamp() > 0) {
|
||||
if (dynamicConfigurationManager.getConfiguration().getMessageRateConfiguration().getRateLimitedCountryCodes().contains(senderCountryCode)) {
|
||||
|
||||
final boolean isRateLimitedHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor)
|
||||
.map(proxy -> dynamicConfigurationManager.getConfiguration().getMessageRateConfiguration().getRateLimitedHosts().contains(proxy))
|
||||
.orElse(false);
|
||||
|
||||
if (isRateLimitedHost) {
|
||||
return declineDelivery(messages, source.get(), destination.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateCompleteDeviceList(destination.get(), messages.getMessages(), isSyncMessage);
|
||||
validateRegistrationIds(destination.get(), messages.getMessages());
|
||||
|
||||
final List<Tag> tags = List.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(messages.isOnline())),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, senderType),
|
||||
Tag.of(DESTINATION_TYPE_TAG_NAME, destinationName.hasNumber() ? "e164" : "uuid"));
|
||||
|
||||
for (IncomingMessage incomingMessage : messages.getMessages()) {
|
||||
Optional<Device> destinationDevice = destination.get().getDevice(incomingMessage.getDestinationDeviceId());
|
||||
|
||||
if (destinationDevice.isPresent()) {
|
||||
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment();
|
||||
sendMessage(source, destination.get(), destinationDevice.get(), messages.getTimestamp(), messages.isOnline(), incomingMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return new SendMessageResponse(!isSyncMessage && source.isPresent() && source.get().getEnabledDeviceCount() > 1);
|
||||
return Response.ok(new SendMessageResponse(!isSyncMessage && source.isPresent() && source.get().getEnabledDeviceCount() > 1)).build();
|
||||
} catch (NoSuchUserException e) {
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
} catch (MismatchedDevicesException e) {
|
||||
throw new WebApplicationException(Response.status(409)
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.entity(new MismatchedDevices(e.getMissingDevices(),
|
||||
e.getExtraDevices()))
|
||||
.build());
|
||||
.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());
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(new StaleDevices(e.getStaleDevices()))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@Path("/multi_recipient")
|
||||
@PUT
|
||||
@Consumes(MultiRecipientMessageProvider.MEDIA_TYPE)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response sendMultiRecipientMessage(
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) CombinedUnidentifiedSenderAccessKeys accessKeys,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor,
|
||||
@QueryParam("online") boolean online,
|
||||
@QueryParam("ts") long timestamp,
|
||||
@Valid MultiRecipientMessage multiRecipientMessage) {
|
||||
|
||||
unidentifiedMeter.mark(multiRecipientMessage.getRecipients().length);
|
||||
|
||||
Map<UUID, Account> uuidToAccountMap = Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(Recipient::getUuid)
|
||||
.distinct()
|
||||
.collect(Collectors.toMap(Function.identity(), uuid -> {
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
if (account.isEmpty()) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
return account.get();
|
||||
}));
|
||||
checkAccessKeys(accessKeys, uuidToAccountMap);
|
||||
|
||||
final Map<Account, HashSet<Pair<Long, Integer>>> accountToDeviceIdAndRegistrationIdMap =
|
||||
Arrays
|
||||
.stream(multiRecipientMessage.getRecipients())
|
||||
.collect(Collectors.toMap(
|
||||
recipient -> uuidToAccountMap.get(recipient.getUuid()),
|
||||
recipient -> new HashSet<>(
|
||||
Collections.singletonList(new Pair<>(recipient.getDeviceId(), recipient.getRegistrationId()))),
|
||||
(a, b) -> {
|
||||
a.addAll(b);
|
||||
return a;
|
||||
}
|
||||
));
|
||||
|
||||
Collection<AccountMismatchedDevices> accountMismatchedDevices = new ArrayList<>();
|
||||
Collection<AccountStaleDevices> accountStaleDevices = new ArrayList<>();
|
||||
uuidToAccountMap.values().forEach(account -> {
|
||||
final Set<Pair<Long, Integer>> deviceIdAndRegistrationIdSet = accountToDeviceIdAndRegistrationIdMap.get(account);
|
||||
final Set<Long> deviceIds = deviceIdAndRegistrationIdSet.stream().map(Pair::first).collect(Collectors.toSet());
|
||||
try {
|
||||
validateCompleteDeviceList(account, deviceIds, false);
|
||||
validateRegistrationIds(account, deviceIdAndRegistrationIdSet.stream());
|
||||
} catch (MismatchedDevicesException e) {
|
||||
accountMismatchedDevices.add(new AccountMismatchedDevices(account.getUuid(),
|
||||
new MismatchedDevices(e.getMissingDevices(), e.getExtraDevices())));
|
||||
} catch (StaleDevicesException e) {
|
||||
accountStaleDevices.add(new AccountStaleDevices(account.getUuid(), new StaleDevices(e.getStaleDevices())));
|
||||
}
|
||||
});
|
||||
if (!accountMismatchedDevices.isEmpty()) {
|
||||
return Response
|
||||
.status(409)
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.entity(accountMismatchedDevices)
|
||||
.build();
|
||||
}
|
||||
if (!accountStaleDevices.isEmpty()) {
|
||||
return Response
|
||||
.status(410)
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(accountStaleDevices)
|
||||
.build();
|
||||
}
|
||||
|
||||
List<Tag> tags = List.of(
|
||||
UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, "unidentified"));
|
||||
List<UUID> uuids404 = new ArrayList<>();
|
||||
for (Recipient recipient : multiRecipientMessage.getRecipients()) {
|
||||
|
||||
Account destinationAccount = uuidToAccountMap.get(recipient.getUuid());
|
||||
// we asserted this must be true in validateCompleteDeviceList
|
||||
//noinspection OptionalGetWithoutIsPresent
|
||||
Device destinationDevice = destinationAccount.getDevice(recipient.getDeviceId()).get();
|
||||
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment();
|
||||
try {
|
||||
sendMessage(destinationAccount, destinationDevice, timestamp, online, recipient,
|
||||
multiRecipientMessage.getCommonPayload());
|
||||
} catch (NoSuchUserException e) {
|
||||
uuids404.add(destinationAccount.getUuid());
|
||||
}
|
||||
}
|
||||
return Response.ok(new SendMultiRecipientMessageResponse(uuids404)).build();
|
||||
}
|
||||
|
||||
private void checkAccessKeys(CombinedUnidentifiedSenderAccessKeys accessKeys, Map<UUID, Account> uuidToAccountMap) {
|
||||
AtomicBoolean throwUnauthorized = new AtomicBoolean(false);
|
||||
byte[] empty = new byte[16];
|
||||
byte[] combinedUnknownAccessKeys = uuidToAccountMap.values().stream()
|
||||
.map(Account::getUnidentifiedAccessKey)
|
||||
.map(accessKey -> {
|
||||
if (accessKey.isEmpty()) {
|
||||
throwUnauthorized.set(true);
|
||||
return empty;
|
||||
}
|
||||
return accessKey.get();
|
||||
})
|
||||
.reduce(new byte[16], (bytes, bytes2) -> {
|
||||
if (bytes.length != bytes2.length) {
|
||||
throwUnauthorized.set(true);
|
||||
return bytes;
|
||||
}
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] ^= bytes2[i];
|
||||
}
|
||||
return bytes;
|
||||
});
|
||||
if (throwUnauthorized.get()
|
||||
|| !MessageDigest.isEqual(combinedUnknownAccessKeys, accessKeys.getAccessKeys())) {
|
||||
throw new WebApplicationException(Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
private Response declineDelivery(final IncomingMessageList messages, final Account source, final Account destination) {
|
||||
Metrics.counter(DECLINED_DELIVERY_COUNTER, SENDER_COUNTRY_TAG_NAME, Util.getCountryCode(source.getNumber())).increment();
|
||||
|
||||
final DynamicMessageRateConfiguration messageRateConfiguration = dynamicConfigurationManager.getConfiguration().getMessageRateConfiguration();
|
||||
|
||||
{
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
|
||||
for (final IncomingMessage message : messages.getMessages()) {
|
||||
final long jitterNanos = random.nextInt((int) messageRateConfiguration.getReceiptDelayJitter().toNanos());
|
||||
final Duration receiptDelay = messageRateConfiguration.getReceiptDelay().plusNanos(jitterNanos);
|
||||
|
||||
if (random.nextDouble() <= messageRateConfiguration.getReceiptProbability()) {
|
||||
receiptExecutorService.schedule(() -> {
|
||||
try {
|
||||
receiptSender.sendReceipt(destination, source.getNumber(), timestamp);
|
||||
} catch (final NoSuchUserException ignored) {
|
||||
}
|
||||
}, receiptDelay.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Duration responseDelay = Duration.ZERO;
|
||||
|
||||
for (int i = 0; i < messages.getMessages().size(); i++) {
|
||||
final long jitterNanos = random.nextInt((int) messageRateConfiguration.getResponseDelayJitter().toNanos());
|
||||
|
||||
responseDelay = responseDelay.plus(
|
||||
messageRateConfiguration.getResponseDelay()).plusNanos(jitterNanos);
|
||||
}
|
||||
|
||||
Util.sleep(responseDelay.toMillis());
|
||||
}
|
||||
|
||||
return Response.ok(new SendMessageResponse(source.getEnabledDeviceCount() > 1)).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public OutgoingMessageEntityList getPendingMessages(@Auth Account account, @HeaderParam("User-Agent") String userAgent) {
|
||||
assert account.getAuthenticatedDevice().isPresent();
|
||||
|
||||
// TODO Remove once PIN-based reglocks have been deprecated
|
||||
if (account.getRegistrationLock().requiresClientRegistrationLock() && account.getRegistrationLock().hasDeprecatedPin()) {
|
||||
logger.info("User-Agent with deprecated PIN-based registration lock: {}", userAgent);
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(account.getAuthenticatedDevice().get().getApnId())) {
|
||||
RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, account.getAuthenticatedDevice().get()));
|
||||
}
|
||||
|
||||
final OutgoingMessageEntityList outgoingMessages = messagesManager.getMessagesForDevice(account.getNumber(),
|
||||
account.getUuid(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
userAgent,
|
||||
false);
|
||||
final OutgoingMessageEntityList outgoingMessages = messagesManager.getMessagesForDevice(
|
||||
account.getUuid(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
userAgent,
|
||||
false);
|
||||
|
||||
outgoingMessageListSizeHistogram.update(outgoingMessages.getMessages().size());
|
||||
|
||||
@@ -244,12 +553,12 @@ public class MessageController {
|
||||
{
|
||||
try {
|
||||
WebSocketConnection.recordMessageDeliveryDuration(timestamp, account.getAuthenticatedDevice().get());
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(),
|
||||
account.getUuid(),
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(
|
||||
account.getUuid(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
source, timestamp);
|
||||
|
||||
if (message.isPresent() && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
|
||||
if (message.isPresent() && message.get().getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(account,
|
||||
message.get().getSource(),
|
||||
message.get().getTimestamp());
|
||||
@@ -264,14 +573,14 @@ public class MessageController {
|
||||
@Path("/uuid/{uuid}")
|
||||
public void removePendingMessage(@Auth Account account, @PathParam("uuid") UUID uuid) {
|
||||
try {
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(),
|
||||
account.getUuid(),
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(
|
||||
account.getUuid(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
uuid);
|
||||
|
||||
if (message.isPresent()) {
|
||||
WebSocketConnection.recordMessageDeliveryDuration(message.get().getTimestamp(), account.getAuthenticatedDevice().get());
|
||||
if (!Util.isEmpty(message.get().getSource()) && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
|
||||
if (!Util.isEmpty(message.get().getSource()) && message.get().getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(account, message.get().getSource(), message.get().getTimestamp());
|
||||
}
|
||||
}
|
||||
@@ -281,6 +590,17 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/report/{sourceNumber}/{messageGuid}")
|
||||
public Response reportMessage(@Auth Account account, @PathParam("sourceNumber") String sourceNumber, @PathParam("messageGuid") UUID messageGuid) {
|
||||
|
||||
reportMessageManager.report(sourceNumber, messageGuid);
|
||||
|
||||
return Response.status(Status.ACCEPTED)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void sendMessage(Optional<Account> source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
@@ -294,7 +614,7 @@ public class MessageController {
|
||||
Optional<byte[]> messageContent = getMessageContent(incomingMessage);
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
|
||||
messageBuilder.setType(Envelope.Type.valueOf(incomingMessage.getType()))
|
||||
messageBuilder.setType(Envelope.Type.forNumber(incomingMessage.getType()))
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setServerTimestamp(System.currentTimeMillis());
|
||||
|
||||
@@ -305,6 +625,7 @@ public class MessageController {
|
||||
}
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
Metrics.counter(LEGACY_MESSAGE_SENT_COUNTER).increment();
|
||||
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
|
||||
}
|
||||
|
||||
@@ -319,43 +640,76 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRegistrationIds(Account account, List<IncomingMessage> messages)
|
||||
throws StaleDevicesException
|
||||
{
|
||||
List<Long> staleDevices = new LinkedList<>();
|
||||
private void sendMessage(Account destinationAccount, Device destinationDevice, long timestamp, boolean online,
|
||||
Recipient recipient, byte[] commonPayload) throws NoSuchUserException {
|
||||
try (final Timer.Context ignored = sendCommonMessageInternalTimer.time()) {
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
long serverTimestamp = System.currentTimeMillis();
|
||||
byte[] recipientKeyMaterial = recipient.getPerRecipientKeyMaterial();
|
||||
|
||||
for (IncomingMessage message : messages) {
|
||||
Optional<Device> device = account.getDevice(message.getDestinationDeviceId());
|
||||
byte[] payload = new byte[1 + recipientKeyMaterial.length + commonPayload.length];
|
||||
payload[0] = MultiRecipientMessageProvider.VERSION;
|
||||
System.arraycopy(recipientKeyMaterial, 0, payload, 1, recipientKeyMaterial.length);
|
||||
System.arraycopy(commonPayload, 0, payload, 1 + recipientKeyMaterial.length, commonPayload.length);
|
||||
|
||||
if (device.isPresent() &&
|
||||
message.getDestinationRegistrationId() > 0 &&
|
||||
message.getDestinationRegistrationId() != device.get().getRegistrationId())
|
||||
{
|
||||
staleDevices.add(device.get().getId());
|
||||
messageBuilder
|
||||
.setType(Type.UNIDENTIFIED_SENDER)
|
||||
.setTimestamp(timestamp == 0 ? serverTimestamp : timestamp)
|
||||
.setServerTimestamp(serverTimestamp)
|
||||
.setContent(ByteString.copyFrom(payload));
|
||||
|
||||
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
if (destinationDevice.isMaster()) {
|
||||
throw new NoSuchUserException(e);
|
||||
} else {
|
||||
logger.debug("Not registered", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void validateRegistrationIds(Account account, List<IncomingMessage> messages)
|
||||
throws StaleDevicesException {
|
||||
final Stream<Pair<Long, Integer>> deviceIdAndRegistrationIdStream = messages
|
||||
.stream()
|
||||
.map(message -> new Pair<>(message.getDestinationDeviceId(), message.getDestinationRegistrationId()));
|
||||
validateRegistrationIds(account, deviceIdAndRegistrationIdStream);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void validateRegistrationIds(Account account, Stream<Pair<Long, Integer>> deviceIdAndRegistrationIdStream)
|
||||
throws StaleDevicesException {
|
||||
final List<Long> staleDevices = deviceIdAndRegistrationIdStream
|
||||
.filter(deviceIdAndRegistrationId -> deviceIdAndRegistrationId.second() > 0)
|
||||
.filter(deviceIdAndRegistrationId -> {
|
||||
Optional<Device> device = account.getDevice(deviceIdAndRegistrationId.first());
|
||||
return device.isPresent() && deviceIdAndRegistrationId.second() != device.get().getRegistrationId();
|
||||
})
|
||||
.map(Pair::first)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!staleDevices.isEmpty()) {
|
||||
throw new StaleDevicesException(staleDevices);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCompleteDeviceList(Account account,
|
||||
List<IncomingMessage> messages,
|
||||
boolean isSyncMessage)
|
||||
throws MismatchedDevicesException
|
||||
{
|
||||
Set<Long> messageDeviceIds = new HashSet<>();
|
||||
@VisibleForTesting
|
||||
public static void validateCompleteDeviceList(Account account, List<IncomingMessage> messages, boolean isSyncMessage)
|
||||
throws MismatchedDevicesException {
|
||||
Set<Long> messageDeviceIds = messages.stream().map(IncomingMessage::getDestinationDeviceId).collect(Collectors.toSet());
|
||||
validateCompleteDeviceList(account, messageDeviceIds, isSyncMessage);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void validateCompleteDeviceList(Account account, Set<Long> messageDeviceIds, boolean isSyncMessage)
|
||||
throws MismatchedDevicesException {
|
||||
Set<Long> accountDeviceIds = new HashSet<>();
|
||||
|
||||
List<Long> missingDeviceIds = new LinkedList<>();
|
||||
List<Long> extraDeviceIds = new LinkedList<>();
|
||||
|
||||
for (IncomingMessage message : messages) {
|
||||
messageDeviceIds.add(message.getDestinationDeviceId());
|
||||
}
|
||||
|
||||
for (Device device : account.getDevices()) {
|
||||
for (Device device : account.getDevices()) {
|
||||
if (device.isEnabled() &&
|
||||
!(isSyncMessage && device.getId() == account.getAuthenticatedDevice().get().getId()))
|
||||
{
|
||||
@@ -367,9 +721,9 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
for (IncomingMessage message : messages) {
|
||||
if (!accountDeviceIds.contains(message.getDestinationDeviceId())) {
|
||||
extraDeviceIds.add(message.getDestinationDeviceId());
|
||||
for (Long deviceId : messageDeviceIds) {
|
||||
if (!accountDeviceIds.contains(deviceId)) {
|
||||
extraDeviceIds.add(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,9 +736,9 @@ public class MessageController {
|
||||
if (Util.isEmpty(message.getBody())) return Optional.empty();
|
||||
|
||||
try {
|
||||
return Optional.of(Base64.decode(message.getBody()));
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Bad B64", ioe);
|
||||
return Optional.of(Base64.getDecoder().decode(message.getBody()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("Bad B64", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -393,10 +747,34 @@ public class MessageController {
|
||||
if (Util.isEmpty(message.getContent())) return Optional.empty();
|
||||
|
||||
try {
|
||||
return Optional.of(Base64.decode(message.getContent()));
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Bad B64", ioe);
|
||||
return Optional.of(Base64.getDecoder().decode(message.getContent()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("Bad B64", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void recordInternationalUnsealedSenderMetrics(final String forwardedFor, final String senderCountryCode, final String destinationNumber) {
|
||||
ForwardedIpUtil.getMostRecentProxy(forwardedFor).ifPresent(senderIp -> {
|
||||
final String destinationSetKey = getDestinationSetKey(senderIp);
|
||||
final String messageCountKey = getMessageCountKey(senderIp);
|
||||
|
||||
recordInternationalUnsealedSenderMetricsScript.execute(
|
||||
List.of(destinationSetKey, messageCountKey),
|
||||
List.of(destinationNumber));
|
||||
});
|
||||
|
||||
Metrics.counter(INTERNATIONAL_UNSEALED_SENDER_COUNTER_NAME, SENDER_COUNTRY_TAG_NAME, senderCountryCode).increment();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static String getDestinationSetKey(final String senderIp) {
|
||||
return "international_unsealed_sender_destinations::{" + senderIp + "}";
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static String getMessageCountKey(final String senderIp) {
|
||||
return "international_unsealed_sender_message_count::{" + senderIp + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,22 +6,27 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/payments")
|
||||
public class PaymentsController {
|
||||
|
||||
private final ExternalServiceCredentialGenerator paymentsServiceCredentialGenerator;
|
||||
private final CurrencyConversionManager currencyManager;
|
||||
|
||||
public PaymentsController(ExternalServiceCredentialGenerator paymentsServiceCredentialGenerator) {
|
||||
public PaymentsController(CurrencyConversionManager currencyManager, ExternalServiceCredentialGenerator paymentsServiceCredentialGenerator) {
|
||||
this.currencyManager = currencyManager;
|
||||
this.paymentsServiceCredentialGenerator = paymentsServiceCredentialGenerator;
|
||||
}
|
||||
|
||||
@@ -32,4 +37,12 @@ public class PaymentsController {
|
||||
public ExternalServiceCredentials getAuth(@Auth Account account) {
|
||||
return paymentsServiceCredentialGenerator.generateFor(account.getUuid().toString());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/conversions")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CurrencyConversionEntityList getConversions(@Auth Account account) {
|
||||
return currencyManager.getCurrencyConversions().orElseThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,32 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.valueextraction.Unwrapping;
|
||||
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.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
@@ -32,30 +52,15 @@ import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.valueextraction.Unwrapping;
|
||||
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.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/profile")
|
||||
@@ -67,30 +72,33 @@ public class ProfileController {
|
||||
private final ProfilesManager profilesManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final DynamicConfigurationManager dynamicConfigurationManager;
|
||||
|
||||
private final PolicySigner policySigner;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
private final ServerZkProfileOperations zkProfileOperations;
|
||||
private final boolean isZkEnabled;
|
||||
|
||||
private final AmazonS3 s3client;
|
||||
private final S3Client s3client;
|
||||
private final String bucket;
|
||||
|
||||
public ProfileController(RateLimiters rateLimiters,
|
||||
AccountsManager accountsManager,
|
||||
ProfilesManager profilesManager,
|
||||
UsernamesManager usernamesManager,
|
||||
AmazonS3 s3client,
|
||||
PostPolicyGenerator policyGenerator,
|
||||
PolicySigner policySigner,
|
||||
String bucket,
|
||||
ServerZkProfileOperations zkProfileOperations,
|
||||
boolean isZkEnabled)
|
||||
AccountsManager accountsManager,
|
||||
ProfilesManager profilesManager,
|
||||
UsernamesManager usernamesManager,
|
||||
DynamicConfigurationManager dynamicConfigurationManager,
|
||||
S3Client s3client,
|
||||
PostPolicyGenerator policyGenerator,
|
||||
PolicySigner policySigner,
|
||||
String bucket,
|
||||
ServerZkProfileOperations zkProfileOperations,
|
||||
boolean isZkEnabled)
|
||||
{
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.zkProfileOperations = zkProfileOperations;
|
||||
this.bucket = bucket;
|
||||
this.s3client = s3client;
|
||||
@@ -106,11 +114,28 @@ public class ProfileController {
|
||||
public Response setProfile(@Auth Account account, @Valid CreateProfileRequest request) {
|
||||
if (!isZkEnabled) throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
|
||||
final Set<String> allowedPaymentsCountryCodes =
|
||||
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getAllowedCountryCodes();
|
||||
|
||||
if (StringUtils.isNotBlank(request.getPaymentAddress()) &&
|
||||
!allowedPaymentsCountryCodes.contains(Util.getCountryCode(account.getNumber()))) {
|
||||
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
Optional<VersionedProfile> currentProfile = profilesManager.get(account.getUuid(), request.getVersion());
|
||||
String avatar = request.isAvatar() ? generateAvatarObjectName() : null;
|
||||
Optional<ProfileAvatarUploadAttributes> response = Optional.empty();
|
||||
|
||||
profilesManager.set(account.getUuid(), new VersionedProfile(request.getVersion(), request.getName(), avatar, request.getCommitment().serialize()));
|
||||
profilesManager.set(account.getUuid(),
|
||||
new VersionedProfile(
|
||||
request.getVersion(),
|
||||
request.getName(),
|
||||
avatar,
|
||||
request.getAboutEmoji(),
|
||||
request.getAbout(),
|
||||
request.getPaymentAddress(),
|
||||
request.getCommitment().serialize()));
|
||||
|
||||
if (request.isAvatar()) {
|
||||
Optional<String> currentAvatar = Optional.empty();
|
||||
@@ -123,14 +148,19 @@ public class ProfileController {
|
||||
currentAvatar = Optional.of(account.getAvatar());
|
||||
}
|
||||
|
||||
currentAvatar.ifPresent(s -> s3client.deleteObject(bucket, s));
|
||||
currentAvatar.ifPresent(s -> s3client.deleteObject(DeleteObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(s)
|
||||
.build()));
|
||||
|
||||
response = Optional.of(generateAvatarUploadForm(avatar));
|
||||
}
|
||||
|
||||
account.setProfileName(request.getName());
|
||||
account.setAvatar(avatar);
|
||||
accountsManager.update(account);
|
||||
accountsManager.update(account, a -> {
|
||||
a.setProfileName(request.getName());
|
||||
a.setAvatar(avatar);
|
||||
a.setCurrentProfileVersion(request.getVersion());
|
||||
});
|
||||
|
||||
if (response.isPresent()) return Response.ok(response).build();
|
||||
else return Response.ok().build();
|
||||
@@ -165,7 +195,6 @@ public class ProfileController {
|
||||
return getVersionedProfile(requestAccount, accessKey, uuid, version, Optional.of(credentialRequest));
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalIsPresent")
|
||||
private Optional<Profile> getVersionedProfile(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
UUID uuid,
|
||||
@@ -176,7 +205,7 @@ public class ProfileController {
|
||||
if (!isZkEnabled) throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
|
||||
try {
|
||||
if (!requestAccount.isPresent() && !accessKey.isPresent()) {
|
||||
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -189,24 +218,36 @@ public class ProfileController {
|
||||
|
||||
assert(accountProfile.isPresent());
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
|
||||
|
||||
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
|
||||
String about = profile.map(VersionedProfile::getAbout).orElse(null);
|
||||
String aboutEmoji = profile.map(VersionedProfile::getAboutEmoji).orElse(null);
|
||||
String avatar = profile.map(VersionedProfile::getAvatar).orElse(accountProfile.get().getAvatar());
|
||||
Optional<String> currentProfileVersion = accountProfile.get().getCurrentProfileVersion();
|
||||
|
||||
// Allow requests where either the version matches the latest version on Account or the latest version on Account
|
||||
// is empty to read the payment address.
|
||||
final String paymentAddress = profile
|
||||
.filter(p -> currentProfileVersion.map(v -> v.equals(version)).orElse(true))
|
||||
.map(VersionedProfile::getPaymentAddress)
|
||||
.orElse(null);
|
||||
|
||||
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
|
||||
String avatar = profile.map(VersionedProfile::getAvatar).orElse(accountProfile.get().getAvatar());
|
||||
|
||||
Optional<ProfileKeyCredentialResponse> credential = getProfileCredential(credentialRequest, profile, uuid);
|
||||
|
||||
return Optional.of(new Profile(name,
|
||||
about,
|
||||
aboutEmoji,
|
||||
avatar,
|
||||
paymentAddress,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
new UserCapabilities(accountProfile.get().isGroupsV2Supported(), accountProfile.get().isGv1MigrationSupported()),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username.orElse(null),
|
||||
null,
|
||||
credential.orElse(null),
|
||||
accountProfile.get().getPayments()));
|
||||
credential.orElse(null)));
|
||||
} catch (InvalidInputException e) {
|
||||
logger.info("Bad profile request", e);
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
@@ -225,26 +266,28 @@ public class ProfileController {
|
||||
|
||||
Optional<UUID> uuid = usernamesManager.get(username);
|
||||
|
||||
if (!uuid.isPresent()) {
|
||||
if (uuid.isEmpty()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid.get());
|
||||
|
||||
if (!accountProfile.isPresent()) {
|
||||
if (accountProfile.isEmpty()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
return new Profile(accountProfile.get().getProfileName(),
|
||||
null,
|
||||
null,
|
||||
accountProfile.get().getAvatar(),
|
||||
null,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
new UserCapabilities(accountProfile.get().isGroupsV2Supported(), accountProfile.get().isGv1MigrationSupported()),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username,
|
||||
accountProfile.get().getUuid(),
|
||||
null,
|
||||
accountProfile.get().getPayments());
|
||||
null);
|
||||
}
|
||||
|
||||
private Optional<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> encodedProfileCredentialRequest,
|
||||
@@ -252,8 +295,8 @@ public class ProfileController {
|
||||
UUID uuid)
|
||||
throws InvalidInputException
|
||||
{
|
||||
if (!encodedProfileCredentialRequest.isPresent()) return Optional.empty();
|
||||
if (!profile.isPresent()) return Optional.empty();
|
||||
if (encodedProfileCredentialRequest.isEmpty()) return Optional.empty();
|
||||
if (profile.isEmpty()) return Optional.empty();
|
||||
|
||||
try {
|
||||
ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.get().getCommitment());
|
||||
@@ -275,8 +318,7 @@ public class ProfileController {
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/name/{name}")
|
||||
public void setProfile(@Auth Account account, @PathParam("name") @ExactlySize(value = {72, 108}, payload = {Unwrapping.Unwrap.class}) Optional<String> name) {
|
||||
account.setProfileName(name.orElse(null));
|
||||
accountsManager.update(account);
|
||||
accountsManager.update(account, a -> a.setProfileName(name.orElse(null)));
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@@ -290,7 +332,7 @@ public class ProfileController {
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (!requestAccount.isPresent() && !accessKey.isPresent()) {
|
||||
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -309,15 +351,17 @@ public class ProfileController {
|
||||
}
|
||||
|
||||
return new Profile(accountProfile.get().getProfileName(),
|
||||
null,
|
||||
null,
|
||||
accountProfile.get().getAvatar(),
|
||||
null,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
new UserCapabilities(accountProfile.get().isGroupsV2Supported(), accountProfile.get().isGv1MigrationSupported()),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username.orElse(null),
|
||||
null,
|
||||
null,
|
||||
accountProfile.get().getPayments());
|
||||
null);
|
||||
}
|
||||
|
||||
|
||||
@@ -332,11 +376,13 @@ public class ProfileController {
|
||||
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
|
||||
|
||||
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
|
||||
s3client.deleteObject(bucket, previousAvatar);
|
||||
s3client.deleteObject(DeleteObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(previousAvatar)
|
||||
.build());
|
||||
}
|
||||
|
||||
account.setAvatar(objectName);
|
||||
accountsManager.update(account);
|
||||
accountsManager.update(account, a -> a.setAvatar(objectName));
|
||||
|
||||
return profileAvatarUploadAttributes;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
|
||||
|
||||
@@ -24,6 +23,7 @@ import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ProvisioningController {
|
||||
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||
|
||||
if (!provisioningManager.sendProvisioningMessage(new ProvisioningAddress(destinationName, 0),
|
||||
Base64.decode(message.getBody())))
|
||||
Base64.getDecoder().decode(message.getBody())))
|
||||
{
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,20 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class RateLimitExceededException extends Exception {
|
||||
public RateLimitExceededException(String number) {
|
||||
super(number);
|
||||
|
||||
private final Duration retryDuration;
|
||||
|
||||
public RateLimitExceededException(final Duration retryDuration) {
|
||||
this(null, retryDuration);
|
||||
}
|
||||
|
||||
public RateLimitExceededException(final String message, final Duration retryDuration) {
|
||||
super(message, null, true, false);
|
||||
this.retryDuration = retryDuration;
|
||||
}
|
||||
|
||||
public Duration getRetryDuration() { return retryDuration; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class RetryLaterException extends Exception {
|
||||
private final Duration backoffDuration;
|
||||
|
||||
public RetryLaterException(RateLimitExceededException e) {
|
||||
super(null, e, true, false);
|
||||
this.backoffDuration = e.getRetryDuration();
|
||||
}
|
||||
|
||||
public Duration getBackoffDuration() { return backoffDuration; }
|
||||
}
|
||||
@@ -8,7 +8,7 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class StaleDevicesException extends Throwable {
|
||||
public class StaleDevicesException extends Exception {
|
||||
private final List<Long> staleDevices;
|
||||
|
||||
public StaleDevicesException(List<Long> staleDevices) {
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale.LanguageRange;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@@ -13,8 +17,7 @@ import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/voice/")
|
||||
@@ -47,6 +50,8 @@ public class VoiceVerificationController {
|
||||
" <Play>%s</Play>\n" +
|
||||
"</Response>";
|
||||
|
||||
private static final String DEFAULT_LOCALE = "en-US";
|
||||
|
||||
|
||||
private final String baseUrl;
|
||||
private final Set<String> supportedLocales;
|
||||
@@ -59,22 +64,29 @@ public class VoiceVerificationController {
|
||||
@POST
|
||||
@Path("/description/{code}")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public Response getDescription(@PathParam("code") String code, @QueryParam("l") String locale) {
|
||||
public Response getDescription(@PathParam("code") String code, @QueryParam("l") List<String> locales) {
|
||||
code = code.replaceAll("[^0-9]", "");
|
||||
|
||||
if (code.length() != 6) {
|
||||
return Response.status(400).build();
|
||||
}
|
||||
|
||||
if (locale != null && supportedLocales.contains(locale)) {
|
||||
return getLocalizedDescription(code, locale);
|
||||
if (locales == null) {
|
||||
locales = Collections.emptyList();
|
||||
}
|
||||
|
||||
if (locale != null && locale.split("-").length >= 1 && supportedLocales.contains(locale.split("-")[0])) {
|
||||
return getLocalizedDescription(code, locale.split("-")[0]);
|
||||
final List<LanguageRange> priorityList;
|
||||
try {
|
||||
priorityList = locales.stream()
|
||||
.map(LanguageRange::new)
|
||||
.collect(Collectors.toList());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
return Response.status(400).build();
|
||||
}
|
||||
|
||||
return getLocalizedDescription(code, "en-US");
|
||||
final String localeMatch = Util.findBestLocale(priorityList, supportedLocales).orElse(DEFAULT_LOCALE);
|
||||
|
||||
return getLocalizedDescription(code, localeMatch);
|
||||
}
|
||||
|
||||
private Response getLocalizedDescription(String code, String locale) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.whispersystems.textsecuregcm.currency;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
|
||||
public class CurrencyConversionManager implements Managed {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CurrencyConversionManager.class);
|
||||
|
||||
private static final long FIXER_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
private static final long FTX_INTERVAL = TimeUnit.MINUTES.toMillis(5);
|
||||
|
||||
private final FixerClient fixerClient;
|
||||
private final FtxClient ftxClient;
|
||||
private final List<String> currencies;
|
||||
|
||||
private AtomicReference<CurrencyConversionEntityList> cached = new AtomicReference<>(null);
|
||||
|
||||
private long fixerUpdatedTimestamp;
|
||||
private long ftxUpdatedTimestamp;
|
||||
|
||||
private Map<String, Double> cachedFixerValues;
|
||||
private Map<String, Double> cachedFtxValues;
|
||||
|
||||
public CurrencyConversionManager(FixerClient fixerClient, FtxClient ftxClient, List<String> currencies) {
|
||||
this.fixerClient = fixerClient;
|
||||
this.ftxClient = ftxClient;
|
||||
this.currencies = currencies;
|
||||
}
|
||||
|
||||
public Optional<CurrencyConversionEntityList> getCurrencyConversions() {
|
||||
return Optional.ofNullable(cached.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
new Thread(() -> {
|
||||
for (;;) {
|
||||
try {
|
||||
updateCacheIfNecessary();
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Error updating currency conversions", t);
|
||||
}
|
||||
|
||||
Util.sleep(15000);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateCacheIfNecessary() throws IOException {
|
||||
if (System.currentTimeMillis() - fixerUpdatedTimestamp > FIXER_INTERVAL || cachedFixerValues == null) {
|
||||
this.cachedFixerValues = new HashMap<>(fixerClient.getConversionsForBase("USD"));
|
||||
this.fixerUpdatedTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - ftxUpdatedTimestamp > FTX_INTERVAL || cachedFtxValues == null) {
|
||||
Map<String, Double> cachedFtxValues = new HashMap<>();
|
||||
|
||||
for (String currency : currencies) {
|
||||
cachedFtxValues.put(currency, ftxClient.getSpotPrice(currency, "USD"));
|
||||
}
|
||||
|
||||
this.cachedFtxValues = cachedFtxValues;
|
||||
this.ftxUpdatedTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
List<CurrencyConversionEntity> entities = new LinkedList<>();
|
||||
|
||||
for (Map.Entry<String, Double> currency : cachedFtxValues.entrySet()) {
|
||||
double usdValue = currency.getValue();
|
||||
|
||||
Map<String, Double> values = new HashMap<>();
|
||||
values.put("USD", usdValue);
|
||||
|
||||
for (Map.Entry<String, Double> conversion : cachedFixerValues.entrySet()) {
|
||||
values.put(conversion.getKey(), conversion.getValue() * usdValue);
|
||||
}
|
||||
|
||||
entities.add(new CurrencyConversionEntity(currency.getKey(), values));
|
||||
}
|
||||
|
||||
|
||||
this.cached.set(new CurrencyConversionEntityList(entities, ftxUpdatedTimestamp));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setFixerUpdatedTimestamp(long timestamp) {
|
||||
this.fixerUpdatedTimestamp = timestamp;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setFtxUpdatedTimestamp(long timestamp) {
|
||||
this.ftxUpdatedTimestamp = timestamp;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.whispersystems.textsecuregcm.currency;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Map;
|
||||
|
||||
public class FixerClient {
|
||||
|
||||
private final String apiKey;
|
||||
private final HttpClient client;
|
||||
|
||||
public FixerClient(HttpClient client, String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Map<String, Double> getConversionsForBase(String base) throws FixerException {
|
||||
try {
|
||||
URI uri = URI.create("https://data.fixer.io/api/latest?access_key=" + apiKey + "&base=" + base);
|
||||
|
||||
HttpResponse<String> response = client.send(HttpRequest.newBuilder()
|
||||
.GET()
|
||||
.uri(uri)
|
||||
.build(),
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new FixerException("Bad response: " + response.statusCode() + " " + response.toString());
|
||||
}
|
||||
|
||||
FixerResponse parsedResponse = SystemMapper.getMapper().readValue(response.body(), FixerResponse.class);
|
||||
|
||||
if (parsedResponse.success) return parsedResponse.rates;
|
||||
else throw new FixerException("Got failed response!");
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new FixerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FixerResponse {
|
||||
|
||||
@JsonProperty
|
||||
private boolean success;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
@JsonProperty
|
||||
private String base;
|
||||
|
||||
@JsonProperty
|
||||
private String date;
|
||||
|
||||
@JsonProperty
|
||||
private Map<String, Double> rates;
|
||||
|
||||
}
|
||||
|
||||
public static class FixerException extends IOException {
|
||||
public FixerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FixerException(Exception exception) {
|
||||
super(exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user