mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-15 02:00:48 +00:00
Compare commits
288 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaa2a6eef1 | ||
|
|
b1f56c3324 | ||
|
|
da5c0ae4b6 | ||
|
|
1e1394560d | ||
|
|
bae0196bcf | ||
|
|
3398955c1a | ||
|
|
a1b925d1e0 | ||
|
|
31c0c3275f | ||
|
|
0a4392f700 | ||
|
|
eb86986cf4 | ||
|
|
1053a47e42 | ||
|
|
99b1f48e0e | ||
|
|
c21eb6aa50 | ||
|
|
6dddf54222 | ||
|
|
9e3eb2319e | ||
|
|
1d8dcda815 | ||
|
|
ee52a84262 | ||
|
|
eb51e81faa | ||
|
|
d41ef1df18 | ||
|
|
66d47aff2c | ||
|
|
c931103712 | ||
|
|
ad1aeea74b | ||
|
|
ae7f8af03e | ||
|
|
a52c91a665 | ||
|
|
94bf3a3902 | ||
|
|
f5a539e128 | ||
|
|
24480b2090 | ||
|
|
a124b3abe9 | ||
|
|
090d722b61 | ||
|
|
d27ec6fe8d | ||
|
|
8d34f3447b | ||
|
|
72b52965b9 | ||
|
|
ae7077c643 | ||
|
|
11598e855f | ||
|
|
534c577f59 | ||
|
|
7762afc497 | ||
|
|
a3fe4b9980 | ||
|
|
598599cd14 | ||
|
|
07cd69ab34 | ||
|
|
3b764bed7a | ||
|
|
c91d5c2fdb | ||
|
|
40f7e6e994 | ||
|
|
ee9aa9ce12 | ||
|
|
08304bf375 | ||
|
|
8b8c6237be | ||
|
|
c0837104cd | ||
|
|
fe21d014f7 | ||
|
|
75c5032cd3 | ||
|
|
f84e7aebd0 | ||
|
|
c379a3d297 | ||
|
|
eedeaaecee | ||
|
|
64eeb1e361 | ||
|
|
e07597eba7 | ||
|
|
5f2656710c | ||
|
|
1af53f2612 | ||
|
|
c89cfa4927 | ||
|
|
bbde93a3c7 | ||
|
|
b01b76d78f | ||
|
|
75c22038eb | ||
|
|
3c1705994d | ||
|
|
439d2f5df8 | ||
|
|
d2bc3c7360 | ||
|
|
9734433f00 | ||
|
|
5bd08800bb | ||
|
|
3032415141 | ||
|
|
ba58a95a0f | ||
|
|
aa4bd92fee | ||
|
|
f37c76dab1 | ||
|
|
863969c77c | ||
|
|
2383aaaa3d | ||
|
|
715d1157ad | ||
|
|
4aaae3f445 | ||
|
|
8359ef73f4 | ||
|
|
c6bb649adb | ||
|
|
e333cbd94d | ||
|
|
cc9a825279 | ||
|
|
5189cbe5c7 | ||
|
|
d1d6e5c652 | ||
|
|
3e5087e60b | ||
|
|
93c3cea912 | ||
|
|
e824b861d4 | ||
|
|
e8dd1e0bf2 | ||
|
|
533afa4c6e | ||
|
|
559026933d | ||
|
|
7864405efd | ||
|
|
a5575902de | ||
|
|
5b9bce59e1 | ||
|
|
041aed2d72 | ||
|
|
02a296e500 | ||
|
|
98e41f9a37 | ||
|
|
6a71d369e2 | ||
|
|
75661fa800 | ||
|
|
df5498e1c0 | ||
|
|
c0af911197 | ||
|
|
44bc90e5ab | ||
|
|
5c1cde1b28 | ||
|
|
3172b571c6 | ||
|
|
17e8b77e88 | ||
|
|
8011935a3b | ||
|
|
3f3052c23c | ||
|
|
8f17f45339 | ||
|
|
009e2eeb97 | ||
|
|
c70fa48835 | ||
|
|
bd5f5c407b | ||
|
|
2bc573a53d | ||
|
|
537d61d5bd | ||
|
|
09519ae942 | ||
|
|
2a67b2e610 | ||
|
|
8161f55a82 | ||
|
|
ecee189ad8 | ||
|
|
ef0900f3ac | ||
|
|
383d744bd8 | ||
|
|
c2ba8ab562 | ||
|
|
cd49ea43c0 | ||
|
|
53aa45a2bb | ||
|
|
83e0a19561 | ||
|
|
6a5d475198 | ||
|
|
49ccbba2e3 | ||
|
|
41735ed40e | ||
|
|
2d11a433c9 | ||
|
|
e79ab2521f | ||
|
|
fb1f99da87 | ||
|
|
08c6a8c2e5 | ||
|
|
ce3835e176 | ||
|
|
39f6eadbb9 | ||
|
|
16dba09b61 | ||
|
|
d5ebf2f2ed | ||
|
|
8a8e6e7b49 | ||
|
|
34e21b9f7b | ||
|
|
98a31d1474 | ||
|
|
72a0c1be0f | ||
|
|
5b25e38e41 | ||
|
|
2fb400280b | ||
|
|
79ad09524e | ||
|
|
5f8accb492 | ||
|
|
6fcadc2297 | ||
|
|
3f4e1522eb | ||
|
|
6304c84cdb | ||
|
|
894297efa9 | ||
|
|
a51a7a0901 | ||
|
|
372e131e25 | ||
|
|
6c6e6a4975 | ||
|
|
cd66a1ceb7 | ||
|
|
feb59deb28 | ||
|
|
489519a982 | ||
|
|
a96865d0f5 | ||
|
|
12e11609a9 | ||
|
|
5b404095b0 | ||
|
|
6a6555e2d5 | ||
|
|
49489a6021 | ||
|
|
8cd93d68e4 | ||
|
|
f3b9a8d97f | ||
|
|
b91a69d8b3 | ||
|
|
624e40e3b7 | ||
|
|
23a076a204 | ||
|
|
016141a05d | ||
|
|
a064b25a14 | ||
|
|
bd40e32f3b | ||
|
|
81a21c0d5f | ||
|
|
6478210330 | ||
|
|
aa1c37fe26 | ||
|
|
6ee23b0186 | ||
|
|
40eb445592 | ||
|
|
ce7d687205 | ||
|
|
758900b7a8 | ||
|
|
539b62a829 | ||
|
|
2866f1b213 | ||
|
|
fc1465c05d | ||
|
|
bc887ec6fa | ||
|
|
84b3d324bb | ||
|
|
fc10108788 | ||
|
|
fbbc1bec58 | ||
|
|
2059bb5ef8 | ||
|
|
b080a5db4d | ||
|
|
b4aabd799b | ||
|
|
92f035bc2a | ||
|
|
18a6df34bd | ||
|
|
b1274125c9 | ||
|
|
dceebc1c8d | ||
|
|
6aadb4b458 | ||
|
|
703405b874 | ||
|
|
d1735c7e57 | ||
|
|
1f815b49dd | ||
|
|
a9339b7037 | ||
|
|
f2c6ca182d | ||
|
|
b946c27a20 | ||
|
|
9fd6358518 | ||
|
|
8a8a848fac | ||
|
|
aeb9f67266 | ||
|
|
e08c5a412e | ||
|
|
a7443a9ece | ||
|
|
54fe3b9a43 | ||
|
|
ba522b1691 | ||
|
|
739c5bf22c | ||
|
|
7cdadeb791 | ||
|
|
dadf43b93e | ||
|
|
bd820e6d2e | ||
|
|
19f7b207b7 | ||
|
|
a398e2269c | ||
|
|
2e28fb97a4 | ||
|
|
5c68d83a93 | ||
|
|
0b7c3ad745 | ||
|
|
0cde06557d | ||
|
|
27844fe692 | ||
|
|
779051ef9f | ||
|
|
d13741fbd5 | ||
|
|
f7f870fe62 | ||
|
|
de59aa099d | ||
|
|
57a478b898 | ||
|
|
3e8d79e147 | ||
|
|
a46045d987 | ||
|
|
662c905b80 | ||
|
|
31022aeb79 | ||
|
|
b3e6a50dee | ||
|
|
d29764d11f | ||
|
|
f8e4f6727a | ||
|
|
63d05df8a3 | ||
|
|
52d13d1d62 | ||
|
|
f58a320223 | ||
|
|
3e01bc1174 | ||
|
|
d1ada7f998 | ||
|
|
095fc8140e | ||
|
|
73c368ea86 | ||
|
|
ce5edbb7fc | ||
|
|
a680639718 | ||
|
|
becf6afbdd | ||
|
|
1dda015c6a | ||
|
|
a0427ecf8c | ||
|
|
cfd31e98ff | ||
|
|
bcb89924b4 | ||
|
|
23f9199439 | ||
|
|
1f6318a919 | ||
|
|
b0667b258b | ||
|
|
4c3a48f5be | ||
|
|
33fb7a72de | ||
|
|
2c808e369c | ||
|
|
906d0be382 | ||
|
|
1c9a3c6105 | ||
|
|
2aaddd721f | ||
|
|
4e2284b83f | ||
|
|
d5d9978e48 | ||
|
|
d45659ac76 | ||
|
|
13a07dc6cd | ||
|
|
51b7a8d868 | ||
|
|
df9c0051c9 | ||
|
|
331ff83cd5 | ||
|
|
44838d6238 | ||
|
|
5400abb065 | ||
|
|
f47fefb73e | ||
|
|
cdef745a7a | ||
|
|
1a1eab4ec0 | ||
|
|
3a966ef345 | ||
|
|
be20c04cd8 | ||
|
|
d09dcc90fe | ||
|
|
1fd1207bf6 | ||
|
|
0117fc12c7 | ||
|
|
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 |
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -9,16 +9,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@3bc31aaf88e8fc94dc1e632d48af61be5ca8721c
|
||||
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-
|
||||
cache: 'maven'
|
||||
- name: Build with Maven
|
||||
run: mvn -e -B verify
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -21,3 +21,8 @@ put.sh
|
||||
deployer-staging.properties
|
||||
deployer-production.properties
|
||||
deployer.log
|
||||
/service/src/main/resources/org/signal/badges/Badges_*.properties
|
||||
!/service/src/main/resources/org/signal/badges/Badges_en.properties
|
||||
/service/src/main/resources/org/signal/subscriptions/Subscriptions_*.properties
|
||||
!/service/src/main/resources/org/signal/subscriptions/Subscriptions_en.properties
|
||||
/.tx/config
|
||||
|
||||
11
.gitmodules
vendored
Normal file
11
.gitmodules
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Note that the implmentation of the abusive message filter is private; internal
|
||||
# developers will need to override this URL with:
|
||||
#
|
||||
# ```
|
||||
# git config submodule.abusive-message-filter.url PRIVATE_URL
|
||||
# ```
|
||||
#
|
||||
# External developers may safely ignore this submodule.
|
||||
[submodule "abusive-message-filter"]
|
||||
path = abusive-message-filter
|
||||
url = REDACTED
|
||||
44
LICENSE
44
LICENSE
@@ -615,3 +615,47 @@ reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
1
abusive-message-filter
Submodule
1
abusive-message-filter
Submodule
Submodule abusive-message-filter added at d20873c7d7
61
pom.xml
61
pom.xml
@@ -36,11 +36,12 @@
|
||||
<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>
|
||||
<gson.version>2.8.8</gson.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>
|
||||
<libphonenumber.version>8.12.33</libphonenumber.version>
|
||||
<logstash.logback.version>6.6</logstash.logback.version>
|
||||
<micrometer.version>1.5.3</micrometer.version>
|
||||
<mockito.version>3.11.1</mockito.version>
|
||||
@@ -49,10 +50,11 @@
|
||||
<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>
|
||||
<pushy.version>0.15.0</pushy.version>
|
||||
<resilience4j.version>1.5.0</resilience4j.version>
|
||||
<semver4j.version>3.1.0</semver4j.version>
|
||||
<slf4j.version>1.7.30</slf4j.version>
|
||||
<stripe.version>20.79.0</stripe.version>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
@@ -91,6 +93,13 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>libraries-bom</artifactId>
|
||||
<version>20.9.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-bom</artifactId>
|
||||
@@ -223,6 +232,27 @@
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>${jedis.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>9.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
<version>${stripe.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -236,7 +266,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>2.28.1</version>
|
||||
<version>2.31.0</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@@ -268,6 +298,29 @@
|
||||
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>include-abusive-message-filter</id>
|
||||
<activation>
|
||||
<file>
|
||||
<exists>abusive-message-filter/pom.xml</exists>
|
||||
</file>
|
||||
</activation>
|
||||
<modules>
|
||||
<module>abusive-message-filter</module>
|
||||
</modules>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>exclude-abusive-message-filter</id>
|
||||
<activation>
|
||||
<file>
|
||||
<missing>abusive-message-filter/pom.xml</missing>
|
||||
</file>
|
||||
</activation>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
@@ -283,7 +336,7 @@
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:3.17.2:exe:${os.detected.classifier}</protocArtifact>
|
||||
<protocArtifact>com.google.protobuf:protoc:3.18.0:exe:${os.detected.classifier}</protocArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
|
||||
@@ -20,7 +20,7 @@ public class RedisInputStream {
|
||||
}
|
||||
|
||||
public String readLine() throws IOException {
|
||||
ByteArrayOutputStream boas = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
boolean foundCr = false;
|
||||
|
||||
@@ -31,14 +31,14 @@ public class RedisInputStream {
|
||||
throw new IOException("Stream closed!");
|
||||
}
|
||||
|
||||
boas.write(character);
|
||||
baos.write(character);
|
||||
|
||||
if (foundCr && character == LF) break;
|
||||
else if (character == CR) foundCr = true;
|
||||
else if (foundCr) foundCr = false;
|
||||
}
|
||||
|
||||
byte[] data = boas.toByteArray();
|
||||
byte[] data = baos.toByteArray();
|
||||
return new String(data, 0, data.length-2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
stripe:
|
||||
apiKey:
|
||||
idempotencyKeyGenerator:
|
||||
|
||||
dynamoDbClientConfiguration:
|
||||
region: # AWS Region
|
||||
|
||||
dynamoDbTables:
|
||||
issuedReceipts:
|
||||
tableName: # DDB Table Name
|
||||
expiration: # Duration of time until rows expire
|
||||
generator: # random binary sequence
|
||||
redeemedReceipts:
|
||||
tableName: # DDB Table Name
|
||||
expiration: # Duration of time until rows expire
|
||||
subscriptions:
|
||||
tableName: # DDB Table Name
|
||||
|
||||
twilio: # Twilio gateway configuration
|
||||
accountId:
|
||||
accountToken:
|
||||
@@ -65,6 +83,10 @@ directory:
|
||||
replicationPassword: # CDS replication endpoint password
|
||||
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
|
||||
|
||||
directoryV2:
|
||||
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
|
||||
userAuthenticationTokenSharedSecret: # base64-encoded secret shared with CDS to generate auth tokens for Signal users
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes:
|
||||
|
||||
@@ -94,6 +116,15 @@ deletedAccountsDynamoDb: # DynamoDb table configuration
|
||||
tableName:
|
||||
needsReconciliationIndexName:
|
||||
|
||||
deletedAccountsLockDynamoDb: # DynamoDb table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
redeemedReceiptsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
expirationTime: # ISO8601 Duration
|
||||
|
||||
migrationDeletedAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
@@ -102,6 +133,14 @@ migrationRetryAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
pendingAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
pendingDevicesDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
pushChallengeDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
@@ -139,6 +178,10 @@ accountDatabaseCrawler:
|
||||
chunkSize: # accounts per run
|
||||
chunkIntervalMs: # time per run
|
||||
|
||||
dynamoDbMigrationCrawler:
|
||||
chunkSize: # accounts per run
|
||||
chunkIntervalMs: # time per run
|
||||
|
||||
apn: # Apple Push Notifications configuration
|
||||
sandbox: true
|
||||
bundleId:
|
||||
@@ -156,10 +199,6 @@ cdn:
|
||||
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:
|
||||
@@ -177,6 +216,12 @@ voiceVerification:
|
||||
recaptcha:
|
||||
secret:
|
||||
|
||||
recaptchaV2:
|
||||
siteKey:
|
||||
scoreFloor:
|
||||
projectPath:
|
||||
credentialConfigurationJson:
|
||||
|
||||
storageService:
|
||||
uri:
|
||||
userAuthenticationTokenSharedSecret:
|
||||
@@ -208,6 +253,9 @@ remoteConfig:
|
||||
|
||||
paymentsService:
|
||||
userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
fixerApiKey:
|
||||
paymentCurrencies:
|
||||
-
|
||||
|
||||
torExitNodeList:
|
||||
s3Region:
|
||||
@@ -223,7 +271,6 @@ asnTable:
|
||||
|
||||
donation:
|
||||
uri: # value
|
||||
apiKey: # value
|
||||
supportedCurrencies:
|
||||
- # 1st supported currency
|
||||
- # 2nd supported currency
|
||||
@@ -237,3 +284,13 @@ donation:
|
||||
retry:
|
||||
maxAttempts: # value
|
||||
waitDuration: # value
|
||||
|
||||
badges:
|
||||
badges:
|
||||
- id: TEST
|
||||
imageUrl: https://example.com/test-badge
|
||||
category: other
|
||||
badgeIdsEnabledForAll:
|
||||
- TEST
|
||||
receiptLevels:
|
||||
'1': TEST
|
||||
|
||||
253
service/pom.xml
253
service/pom.xml
@@ -10,6 +10,19 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>service</artifactId>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>ossrh-snapshots</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
@@ -42,7 +55,7 @@
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>zkgroup-java</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.8.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
@@ -218,10 +231,6 @@
|
||||
<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>
|
||||
@@ -255,6 +264,10 @@
|
||||
<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>
|
||||
@@ -267,6 +280,10 @@
|
||||
<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>
|
||||
@@ -277,7 +294,14 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-appconfig</artifactId>
|
||||
<artifactId>dynamodb-lock-client</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -420,6 +444,16 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-recaptchaenterprise</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>pl.pragmatists</groupId>
|
||||
<artifactId>JUnitParams</artifactId>
|
||||
@@ -428,103 +462,112 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>exclude-abusive-message-filter</id>
|
||||
<build>
|
||||
<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>
|
||||
|
||||
<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>org.signal</groupId>
|
||||
<artifactId>s3-upload-maven-plugin</artifactId>
|
||||
<version>1.6-SNAPSHOT</version>
|
||||
<configuration>
|
||||
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
|
||||
<bucketName>${deploy.bucketName}</bucketName>
|
||||
<region>${deploy.bucketRegion}</region>
|
||||
<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>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@@ -538,6 +581,18 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.i18n;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.ResourceBundle.Control;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class HeaderControlledResourceBundleLookup {
|
||||
|
||||
private static final int MAX_LOCALES = 15;
|
||||
|
||||
private final ResourceBundleFactory resourceBundleFactory;
|
||||
|
||||
public HeaderControlledResourceBundleLookup() {
|
||||
this(ResourceBundle::getBundle);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public HeaderControlledResourceBundleLookup(
|
||||
@Nonnull final ResourceBundleFactory resourceBundleFactory) {
|
||||
this.resourceBundleFactory = Objects.requireNonNull(resourceBundleFactory);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<Locale> getAcceptableLocales(final List<Locale> acceptableLanguages) {
|
||||
return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ResourceBundle getResourceBundle(final String baseName, final List<Locale> acceptableLocales) {
|
||||
final List<Locale> deduplicatedLocales = getAcceptableLocales(acceptableLocales);
|
||||
final Locale desiredLocale = deduplicatedLocales.isEmpty() ? Locale.getDefault() : deduplicatedLocales.get(0);
|
||||
// define a control with a fallback order as specified in the header
|
||||
Control control = new Control() {
|
||||
@Override
|
||||
public List<String> getFormats(final String baseName) {
|
||||
Objects.requireNonNull(baseName);
|
||||
return Control.FORMAT_PROPERTIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale getFallbackLocale(final String baseName, final Locale locale) {
|
||||
Objects.requireNonNull(baseName);
|
||||
if (locale.equals(Locale.getDefault())) {
|
||||
return null;
|
||||
}
|
||||
final int localeIndex = deduplicatedLocales.indexOf(locale);
|
||||
if (localeIndex < 0 || localeIndex >= deduplicatedLocales.size() - 1) {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
// [0, deduplicatedLocales.size() - 2] is now the possible range for localeIndex
|
||||
return deduplicatedLocales.get(localeIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return resourceBundleFactory.createBundle(baseName, desiredLocale, control);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.i18n;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public interface ResourceBundleFactory {
|
||||
ResourceBundle createBundle(String baseName, Locale locale, ResourceBundle.Control control);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm;
|
||||
@@ -13,36 +13,44 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.AbusiveMessageFilterConfiguration;
|
||||
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.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
|
||||
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.DirectoryV2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
|
||||
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.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;
|
||||
import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RecaptchaV2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
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;
|
||||
@@ -53,6 +61,21 @@ import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||
/** @noinspection MismatchedQueryAndUpdateOfCollection, WeakerAccess */
|
||||
public class WhisperServerConfiguration extends Configuration {
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private StripeConfiguration stripe;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DynamoDbClientConfiguration dynamoDbClientConfiguration;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DynamoDbTables dynamoDbTables;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -78,11 +101,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private CdnConfiguration cdn;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private WavefrontConfiguration wavefront;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -108,6 +126,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private DirectoryConfiguration directory;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DirectoryV2Configuration directoryV2;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -151,18 +174,18 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration migrationDeletedAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration migrationRetryAccountsDynamoDb;
|
||||
private DynamoDbConfiguration phoneNumberIdentifiersDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DeletedAccountsDynamoDbConfiguration deletedAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration deletedAccountsLockDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -248,6 +271,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private RecaptchaConfiguration recaptcha;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private RecaptchaV2Configuration recaptchaV2;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -278,27 +306,57 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private AppConfigConfiguration appConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private MonitoredS3ObjectConfiguration torExitNodeList;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private MonitoredS3ObjectConfiguration asnTable;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DonationConfiguration donation;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private BadgesConfiguration badges;
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private SubscriptionConfiguration subscription;
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private BoostConfiguration boost;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ReportMessageConfiguration reportMessage = new ReportMessageConfiguration();
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AbusiveMessageFilterConfiguration abusiveMessageFilter;
|
||||
|
||||
private Map<String, String> transparentDataIndex = new HashMap<>();
|
||||
|
||||
public StripeConfiguration getStripe() {
|
||||
return stripe;
|
||||
}
|
||||
|
||||
public DynamoDbClientConfiguration getDynamoDbClientConfiguration() {
|
||||
return dynamoDbClientConfiguration;
|
||||
}
|
||||
|
||||
public DynamoDbTables getDynamoDbTables() {
|
||||
return dynamoDbTables;
|
||||
}
|
||||
|
||||
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
||||
return recaptcha;
|
||||
}
|
||||
|
||||
public RecaptchaV2Configuration getRecaptchaV2Configuration() {
|
||||
return recaptchaV2;
|
||||
}
|
||||
|
||||
public VoiceVerificationConfiguration getVoiceVerificationConfiguration() {
|
||||
return voiceVerification;
|
||||
}
|
||||
@@ -343,6 +401,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public DirectoryV2Configuration getDirectoryV2Configuration() {
|
||||
return directoryV2;
|
||||
}
|
||||
|
||||
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
|
||||
return storageService;
|
||||
}
|
||||
@@ -379,18 +441,18 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return accountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getMigrationDeletedAccountsDynamoDbConfiguration() {
|
||||
return migrationDeletedAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getMigrationRetryAccountsDynamoDbConfiguration() {
|
||||
return migrationRetryAccountsDynamoDb;
|
||||
public DynamoDbConfiguration getPhoneNumberIdentifiersDynamoDbConfiguration() {
|
||||
return phoneNumberIdentifiersDynamoDb;
|
||||
}
|
||||
|
||||
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
|
||||
return deletedAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getDeletedAccountsLockDynamoDbConfiguration() {
|
||||
return deletedAccountsLockDynamoDb;
|
||||
}
|
||||
|
||||
public DatabaseConfiguration getAbuseDatabaseConfiguration() {
|
||||
return abuseDatabase;
|
||||
}
|
||||
@@ -419,10 +481,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return cdn;
|
||||
}
|
||||
|
||||
public WavefrontConfiguration getWavefrontConfiguration() {
|
||||
return wavefront;
|
||||
}
|
||||
|
||||
public DatadogConfiguration getDatadogConfiguration() {
|
||||
return datadog;
|
||||
}
|
||||
@@ -493,15 +551,27 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return pendingDevicesDynamoDb;
|
||||
}
|
||||
|
||||
public MonitoredS3ObjectConfiguration getTorExitNodeListConfiguration() {
|
||||
return torExitNodeList;
|
||||
}
|
||||
|
||||
public MonitoredS3ObjectConfiguration getAsnTableConfiguration() {
|
||||
return asnTable;
|
||||
}
|
||||
|
||||
public DonationConfiguration getDonationConfiguration() {
|
||||
return donation;
|
||||
}
|
||||
|
||||
public BadgesConfiguration getBadges() {
|
||||
return badges;
|
||||
}
|
||||
|
||||
public SubscriptionConfiguration getSubscription() {
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public BoostConfiguration getBoost() {
|
||||
return boost;
|
||||
}
|
||||
|
||||
public ReportMessageConfiguration getReportMessageConfiguration() {
|
||||
return reportMessage;
|
||||
}
|
||||
|
||||
public AbusiveMessageFilterConfiguration getAbusiveMessageFilterConfiguration() {
|
||||
return abusiveMessageFilter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
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;
|
||||
@@ -13,6 +17,7 @@ import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import io.dropwizard.Application;
|
||||
import io.dropwizard.auth.AuthFilter;
|
||||
import io.dropwizard.auth.PolymorphicAuthDynamicFeature;
|
||||
@@ -25,47 +30,55 @@ import io.dropwizard.jdbi3.JdbiFactory;
|
||||
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 java.net.http.HttpClient;
|
||||
import java.time.Clock;
|
||||
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.ServiceLoader;
|
||||
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.glassfish.jersey.server.ServerProperties;
|
||||
import org.jdbi.v3.core.Jdbi;
|
||||
import org.signal.i18n.HeaderControlledResourceBundleLookup;
|
||||
import org.signal.zkgroup.ServerSecretParams;
|
||||
import org.signal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.DispatchManager;
|
||||
import org.whispersystems.textsecuregcm.abuse.AbusiveMessageFilter;
|
||||
import org.whispersystems.textsecuregcm.abuse.FilterAbusiveMessages;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
|
||||
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
|
||||
import org.whispersystems.textsecuregcm.badges.ResourceBundleLevelTranslator;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||
@@ -74,6 +87,7 @@ 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.DirectoryV2Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.DonationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
||||
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||
@@ -85,11 +99,13 @@ import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureBackupController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
|
||||
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.ContentLengthFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
|
||||
import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter;
|
||||
@@ -99,12 +115,17 @@ 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.CompletionExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitChallengeExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RetryLaterExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.metrics.ApplicationShutdownMonitor;
|
||||
import org.whispersystems.textsecuregcm.metrics.BufferPoolGauges;
|
||||
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge;
|
||||
@@ -128,7 +149,9 @@ import org.whispersystems.textsecuregcm.push.GCMSender;
|
||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.EnterpriseRecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.LegacyRecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.TransitionalRecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
|
||||
@@ -141,51 +164,51 @@ 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;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountCleaner;
|
||||
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.ContactDiscoveryWriter;
|
||||
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.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagePersister;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.MigrationDeletedAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.MigrationRetryAccounts;
|
||||
import org.whispersystems.textsecuregcm.storage.MigrationRetryAccountsTableCrawler;
|
||||
import org.whispersystems.textsecuregcm.storage.NonNormalizedAccountCrawlerListener;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
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.RedeemedReceiptsManager;
|
||||
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.SubscriptionManager;
|
||||
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.stripe.StripeManager;
|
||||
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.util.logging.UncaughtExceptionHandler;
|
||||
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||
@@ -196,7 +219,7 @@ import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||
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.SetUserDiscoverabilityCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ZkParamsCommand;
|
||||
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
@@ -213,12 +236,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
@Override
|
||||
public void initialize(Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||
bootstrap.addCommand(new VacuumCommand());
|
||||
bootstrap.addCommand(new DeleteUserCommand());
|
||||
bootstrap.addCommand(new CertificateCommand());
|
||||
bootstrap.addCommand(new ZkParamsCommand());
|
||||
bootstrap.addCommand(new ServerVersionCommand());
|
||||
bootstrap.addCommand(new CheckDynamicConfigurationCommand());
|
||||
bootstrap.addCommand(new SetUserDiscoverabilityCommand());
|
||||
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
|
||||
@Override
|
||||
@@ -227,7 +250,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("abusedb", "abusedb.xml") {
|
||||
@Override
|
||||
public PooledDataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
@@ -242,8 +264,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(WhisperServerConfiguration config, Environment environment)
|
||||
throws Exception {
|
||||
public void run(WhisperServerConfiguration config, Environment environment) throws Exception {
|
||||
final Clock clock = Clock.systemUTC();
|
||||
final int availableProcessors = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
UncaughtExceptionHandler.register();
|
||||
|
||||
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
|
||||
|
||||
@@ -251,32 +276,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.percentiles(.75, .95, .99, .999)
|
||||
.build();
|
||||
|
||||
final WavefrontConfig wavefrontConfig = new WavefrontConfig() {
|
||||
@Override
|
||||
public String get(final String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uri() {
|
||||
return config.getWavefrontConfiguration().getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchSize() {
|
||||
return config.getWavefrontConfiguration().getBatchSize();
|
||||
}
|
||||
};
|
||||
|
||||
Metrics.addRegistry(new WavefrontMeterRegistry(wavefrontConfig, Clock.SYSTEM) {
|
||||
@Override
|
||||
protected DistributionStatisticConfig defaultHistogramConfig() {
|
||||
return defaultDistributionStatisticConfig.merge(super.defaultHistogramConfig());
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
final DatadogMeterRegistry datadogMeterRegistry = new DatadogMeterRegistry(config.getDatadogConfiguration(), Clock.SYSTEM);
|
||||
final DatadogMeterRegistry datadogMeterRegistry = new DatadogMeterRegistry(
|
||||
config.getDatadogConfiguration(), io.micrometer.core.instrument.Clock.SYSTEM);
|
||||
|
||||
datadogMeterRegistry.config().commonTags(
|
||||
Tags.of(
|
||||
@@ -302,6 +304,13 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup =
|
||||
new HeaderControlledResourceBundleLookup();
|
||||
ConfiguredProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter(
|
||||
clock, config.getBadges(), headerControlledResourceBundleLookup);
|
||||
ResourceBundleLevelTranslator resourceBundleLevelTranslator = new ResourceBundleLevelTranslator(
|
||||
headerControlledResourceBundleLookup);
|
||||
|
||||
JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY);
|
||||
Jdbi accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb");
|
||||
Jdbi abuseJdbi = jdbiFactory.build(environment, config.getAbuseDatabaseConfiguration(), "abusedb" );
|
||||
@@ -309,6 +318,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("accounts_database", accountJdbi, config.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration());
|
||||
FaultTolerantDatabase abuseDatabase = new FaultTolerantDatabase("abuse_database", abuseJdbi, config.getAbuseDatabaseConfiguration().getCircuitBreakerConfiguration());
|
||||
|
||||
DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(
|
||||
config.getDynamoDbClientConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient messageDynamoDb = DynamoDbFromConfig.client(config.getMessageDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
@@ -318,50 +331,60 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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 phoneNumberIdentifiersDynamoDbClient =
|
||||
DynamoDbFromConfig.client(config.getPhoneNumberIdentifiersDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient recentlyDeletedAccountsDynamoDb = DynamoDbFromConfig.client(config.getMigrationDeletedAccountsDynamoDbConfiguration(),
|
||||
DynamoDbClient pushChallengeDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getPushChallengeDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pushChallengeDynamoDbClient = DynamoDbFromConfig.client(config.getPushChallengeDynamoDbConfiguration(),
|
||||
DynamoDbClient reportMessageDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getReportMessageDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient reportMessageDynamoDbClient = DynamoDbFromConfig.client(config.getReportMessageDynamoDbConfiguration(),
|
||||
DynamoDbClient pendingAccountsDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getPendingAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient migrationRetryAccountsDynamoDb = DynamoDbFromConfig.client(config.getMigrationRetryAccountsDynamoDbConfiguration(),
|
||||
DynamoDbClient pendingDevicesDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getPendingDevicesDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pendingAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getPendingAccountsDynamoDbConfiguration(),
|
||||
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();
|
||||
|
||||
DynamoDbClient pendingDevicesDynamoDbClient = DynamoDbFromConfig.client(config.getPendingDevicesDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
DeletedAccounts deletedAccounts = new DeletedAccounts(deletedAccountsDynamoDbClient,
|
||||
config.getDeletedAccountsDynamoDbConfiguration().getTableName(),
|
||||
config.getDeletedAccountsDynamoDbConfiguration().getNeedsReconciliationIndexName());
|
||||
|
||||
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);
|
||||
AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, accountsDynamoDbAsyncClient, accountsDynamoDbMigrationThreadPool, config.getAccountsDynamoDbConfiguration().getTableName(), config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), migrationDeletedAccounts, migrationRetryAccounts);
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
config.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
|
||||
config.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
|
||||
Usernames usernames = new Usernames(accountDatabase);
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
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);
|
||||
Profiles profiles = new Profiles(accountDatabase);
|
||||
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);
|
||||
PushChallengeDynamoDb pushChallengeDynamoDb = new PushChallengeDynamoDb(pushChallengeDynamoDbClient, config.getPushChallengeDynamoDbConfiguration().getTableName());
|
||||
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessageDynamoDbClient, config.getReportMessageDynamoDbConfiguration().getTableName());
|
||||
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(reportMessageDynamoDbClient, config.getReportMessageDynamoDbConfiguration().getTableName(), config.getReportMessageConfiguration().getReportTtl());
|
||||
VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient, config.getPendingAccountsDynamoDbConfiguration().getTableName());
|
||||
VerificationCodeStore pendingDevices = new VerificationCodeStore(pendingDevicesDynamoDbClient, config.getPendingDevicesDynamoDbConfiguration().getTableName());
|
||||
|
||||
@@ -390,21 +413,38 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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(3).build();
|
||||
ScheduledExecutorService declinedMessageReceiptExecutor = environment.lifecycle().scheduledExecutorService(name(getClass(), "declined-receipt-%d")).threads(2).build();
|
||||
ScheduledExecutorService recurringJobExecutor = environment.lifecycle()
|
||||
.scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(6).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();
|
||||
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();
|
||||
ExecutorService multiRecipientMessageExecutor = environment.lifecycle()
|
||||
.executorService(name(getClass(), "multiRecipientMessage-%d")).minThreads(64).maxThreads(64).build();
|
||||
ExecutorService stripeExecutor = environment.lifecycle().executorService(name(getClass(), "stripe-%d")).
|
||||
maxThreads(availableProcessors). // mostly this is IO bound so tying to number of processors is tenuous at best
|
||||
minThreads(availableProcessors). // mostly this is IO bound so tying to number of processors is tenuous at best
|
||||
allowCoreThreadTimeOut(true).
|
||||
build();
|
||||
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
|
||||
true);
|
||||
StripeManager stripeManager = new StripeManager(config.getStripe().getApiKey(), stripeExecutor,
|
||||
config.getStripe().getIdempotencyKeyGenerator());
|
||||
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret());
|
||||
ExternalServiceCredentialGenerator directoryV2CredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration()
|
||||
.getUserAuthenticationTokenSharedSecret(), false);
|
||||
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
|
||||
config.getAppConfig().getEnvironment(),
|
||||
config.getAppConfig().getConfigurationName(),
|
||||
DynamicConfiguration.class);
|
||||
|
||||
DynamicConfigurationManager dynamicConfigurationManager = new DynamicConfigurationManager(config.getAppConfig().getApplication(), config.getAppConfig().getEnvironment(), config.getAppConfig().getConfigurationName());
|
||||
dynamicConfigurationManager.start();
|
||||
|
||||
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
|
||||
@@ -412,9 +452,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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);
|
||||
ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
ExternalServiceCredentialGenerator paymentsCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
|
||||
|
||||
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator, backupServiceExecutor, config.getSecureBackupServiceConfiguration());
|
||||
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, config.getSecureStorageServiceConfiguration());
|
||||
@@ -425,11 +468,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, dynamicConfigurationManager);
|
||||
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster, Metrics.globalRegistry, config.getReportMessageConfiguration().getCounterTtl());
|
||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, accountsDynamoDb, cacheCluster, deletedAccounts, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, secureStorageClient, secureBackupClient, experimentEnrollmentManager, dynamicConfigurationManager);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
||||
deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
|
||||
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);
|
||||
@@ -437,8 +484,18 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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());
|
||||
IssuedReceiptsManager issuedReceiptsManager = new IssuedReceiptsManager(
|
||||
config.getDynamoDbTables().getIssuedReceipts().getTableName(),
|
||||
config.getDynamoDbTables().getIssuedReceipts().getExpiration(),
|
||||
dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getIssuedReceipts().getGenerator());
|
||||
RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(
|
||||
clock,
|
||||
config.getDynamoDbTables().getRedeemedReceipts().getTableName(),
|
||||
dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getRedeemedReceipts().getExpiration());
|
||||
SubscriptionManager subscriptionManager = new SubscriptionManager(
|
||||
config.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient);
|
||||
|
||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
@@ -454,27 +511,44 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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());
|
||||
LegacyRecaptchaClient legacyRecaptchaClient = new LegacyRecaptchaClient(config.getRecaptchaConfiguration().getSecret());
|
||||
EnterpriseRecaptchaClient enterpriseRecaptchaClient = new EnterpriseRecaptchaClient(
|
||||
config.getRecaptchaV2Configuration().getScoreFloor().doubleValue(),
|
||||
config.getRecaptchaV2Configuration().getSiteKey(),
|
||||
config.getRecaptchaV2Configuration().getProjectPath(),
|
||||
config.getRecaptchaV2Configuration().getCredentialConfigurationJson());
|
||||
TransitionalRecaptchaClient transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient, enterpriseRecaptchaClient);
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, gcmSender, pushChallengeDynamoDb);
|
||||
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager, recaptchaClient, preKeyRateLimiter, unsealedSenderRateLimiter, rateLimiters, dynamicConfigurationManager);
|
||||
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||
transitionalRecaptchaClient, preKeyRateLimiter, unsealedSenderRateLimiter, rateLimiters,
|
||||
dynamicConfigurationManager);
|
||||
|
||||
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
|
||||
|
||||
final List<DeletedAccountsDirectoryReconciler> deletedAccountsDirectoryReconcilers = new ArrayList<>();
|
||||
// TODO listeners must be ordered so that ones that directly update accounts come last, so that read-only ones are not working with stale data
|
||||
final List<AccountDatabaseCrawlerListener> accountDatabaseCrawlerListeners = new ArrayList<>();
|
||||
accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager, directoryQueue));
|
||||
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(), directoryReconciliationClient);
|
||||
|
||||
final List<DeletedAccountsDirectoryReconciler> deletedAccountsDirectoryReconcilers = new ArrayList<>();
|
||||
for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration()
|
||||
.getDirectoryServerConfiguration()) {
|
||||
final DirectoryReconciliationClient directoryReconciliationClient = new DirectoryReconciliationClient(
|
||||
directoryServerConfiguration);
|
||||
final DirectoryReconciler directoryReconciler = new DirectoryReconciler(
|
||||
directoryServerConfiguration.getReplicationName(), directoryReconciliationClient,
|
||||
dynamicConfigurationManager);
|
||||
// reconcilers are read-only
|
||||
accountDatabaseCrawlerListeners.add(directoryReconciler);
|
||||
|
||||
final DeletedAccountsDirectoryReconciler deletedAccountsDirectoryReconciler = new DeletedAccountsDirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
|
||||
final DeletedAccountsDirectoryReconciler deletedAccountsDirectoryReconciler = new DeletedAccountsDirectoryReconciler(
|
||||
directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
|
||||
deletedAccountsDirectoryReconcilers.add(deletedAccountsDirectoryReconciler);
|
||||
}
|
||||
accountDatabaseCrawlerListeners.add(new NonNormalizedAccountCrawlerListener(accountsManager, metricsCluster));
|
||||
accountDatabaseCrawlerListeners.add(new ContactDiscoveryWriter(accountsManager));
|
||||
// PushFeedbackProcessor may update device properties
|
||||
accountDatabaseCrawlerListeners.add(new PushFeedbackProcessor(accountsManager));
|
||||
// delete accounts last
|
||||
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());
|
||||
@@ -482,25 +556,27 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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(), dynamicConfigurationManager);
|
||||
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager,
|
||||
accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners,
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkSize(),
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()
|
||||
);
|
||||
|
||||
DeletedAccountsTableCrawler deletedAccountsTableCrawler = new DeletedAccountsTableCrawler(deletedAccounts, deletedAccountsDirectoryReconcilers, cacheCluster, recurringJobExecutor);
|
||||
MigrationRetryAccountsTableCrawler migrationRetryAccountsTableCrawler = new MigrationRetryAccountsTableCrawler(migrationRetryAccounts, accountsManager, accountsDynamoDb, cacheCluster, recurringJobExecutor);
|
||||
DeletedAccountsTableCrawler deletedAccountsTableCrawler = new DeletedAccountsTableCrawler(deletedAccountsManager, deletedAccountsDirectoryReconcilers, cacheCluster, recurringJobExecutor);
|
||||
|
||||
apnSender.setApnFallbackManager(apnFallbackManager);
|
||||
environment.lifecycle().manage(new ApplicationShutdownMonitor());
|
||||
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(currencyManager);
|
||||
environment.lifecycle().manage(torExitNodeManager);
|
||||
environment.lifecycle().manage(asnManager);
|
||||
environment.lifecycle().manage(directoryQueue);
|
||||
|
||||
StaticCredentialsProvider cdnCredentialsProvider = StaticCredentialsProvider
|
||||
.create(AwsBasicCredentials.create(
|
||||
@@ -510,73 +586,122 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.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());
|
||||
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(),
|
||||
config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey());
|
||||
PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().getAccessSecret(),
|
||||
config.getCdnConfiguration().getRegion());
|
||||
|
||||
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().getServerSecret());
|
||||
ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams);
|
||||
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
|
||||
boolean isZkEnabled = config.getZkConfig().isEnabled();
|
||||
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().getServerSecret());
|
||||
ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams);
|
||||
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
|
||||
ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams);
|
||||
|
||||
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());
|
||||
DonationController donationController = new DonationController(donationExecutor, config.getDonationConfiguration());
|
||||
KeysController keysController = new KeysController(rateLimiters, keysDynamoDb, accountsManager, directoryQueue, preKeyRateLimiter, dynamicConfigurationManager, rateLimitChallengeManager);
|
||||
MessageController messageController = new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager, dynamicConfigurationManager, rateLimitChallengeManager, reportMessageManager, metricsCluster, declinedMessageReceiptExecutor);
|
||||
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, dynamicConfigurationManager, 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());
|
||||
ChallengeController challengeController = new ChallengeController(rateLimitChallengeManager);
|
||||
AuthFilter<BasicCredentials, AuthenticatedAccount> accountAuthFilter = new BasicCredentialAuthFilter.Builder<AuthenticatedAccount>().setAuthenticator(
|
||||
accountAuthenticator).buildAuthFilter();
|
||||
AuthFilter<BasicCredentials, DisabledPermittedAuthenticatedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAuthenticatedAccount>().setAuthenticator(
|
||||
disabledPermittedAccountAuthenticator).buildAuthFilter();
|
||||
|
||||
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
|
||||
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
|
||||
|
||||
environment.servlets().addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
|
||||
environment.servlets()
|
||||
.addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
|
||||
environment.jersey().register(new ContentLengthFilter(TrafficSource.HTTP));
|
||||
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 PolymorphicAuthDynamicFeature<>(ImmutableMap.of(AuthenticatedAccount.class, accountAuthFilter,
|
||||
DisabledPermittedAuthenticatedAccount.class, disabledPermittedAccountAuthFilter)));
|
||||
environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)));
|
||||
environment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
environment.jersey().register(new TimestampResponseFilter());
|
||||
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator, verifyExperimentEnrollmentManager));
|
||||
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
|
||||
environment.jersey().register(new DirectoryController(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(currencyManager, paymentsCredentialsGenerator));
|
||||
environment.jersey().register(attachmentControllerV1);
|
||||
environment.jersey().register(attachmentControllerV2);
|
||||
environment.jersey().register(attachmentControllerV3);
|
||||
environment.jersey().register(donationController);
|
||||
environment.jersey().register(keysController);
|
||||
environment.jersey().register(messageController);
|
||||
environment.jersey().register(profileController);
|
||||
environment.jersey().register(stickerController);
|
||||
environment.jersey().register(remoteConfigController);
|
||||
environment.jersey().register(challengeController);
|
||||
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(),
|
||||
config.getVoiceVerificationConfiguration().getLocales()));
|
||||
|
||||
///
|
||||
WebSocketEnvironment<Account> webSocketEnvironment = new WebSocketEnvironment<>(environment, config.getWebSocketConfiguration(), 90000);
|
||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment = new WebSocketEnvironment<>(environment,
|
||||
config.getWebSocketConfiguration(), 90000);
|
||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
|
||||
webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(receiptSender, messagesManager, messageSender, apnFallbackManager, clientPresenceManager, retrySchedulingExecutor));
|
||||
webSocketEnvironment.setConnectListener(
|
||||
new AuthenticatedConnectListener(receiptSender, messagesManager, messageSender, apnFallbackManager,
|
||||
clientPresenceManager, retrySchedulingExecutor));
|
||||
webSocketEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
webSocketEnvironment.jersey().register(new ContentLengthFilter(TrafficSource.WEBSOCKET));
|
||||
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(donationController);
|
||||
webSocketEnvironment.jersey().register(remoteConfigController);
|
||||
|
||||
WebSocketEnvironment<Account> provisioningEnvironment = new WebSocketEnvironment<>(environment, webSocketEnvironment.getRequestLog(), 60000);
|
||||
// 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(),
|
||||
transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator,
|
||||
verifyExperimentEnrollmentManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
|
||||
|
||||
final List<Object> commonControllers = Lists.newArrayList(
|
||||
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),
|
||||
new ChallengeController(rateLimitChallengeManager),
|
||||
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keysDynamoDb, rateLimiters, config.getMaxDevices()),
|
||||
new DirectoryController(directoryCredentialsGenerator),
|
||||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager,
|
||||
rateLimitChallengeManager, reportMessageManager, multiRecipientMessageExecutor),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, usernamesManager, dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations),
|
||||
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())
|
||||
);
|
||||
if (config.getSubscription() != null && config.getBoost() != null) {
|
||||
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getBoost(),
|
||||
subscriptionManager, stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter,
|
||||
resourceBundleLevelTranslator));
|
||||
}
|
||||
|
||||
for (Object controller : commonControllers) {
|
||||
environment.jersey().register(controller);
|
||||
webSocketEnvironment.jersey().register(controller);
|
||||
}
|
||||
|
||||
boolean registeredAbusiveMessageFilter = false;
|
||||
|
||||
for (final AbusiveMessageFilter filter : ServiceLoader.load(AbusiveMessageFilter.class)) {
|
||||
if (filter.getClass().isAnnotationPresent(FilterAbusiveMessages.class)) {
|
||||
try {
|
||||
filter.configure(config.getAbusiveMessageFilterConfiguration().getEnvironment());
|
||||
|
||||
environment.lifecycle().manage(filter);
|
||||
environment.jersey().register(filter);
|
||||
webSocketEnvironment.jersey().register(filter);
|
||||
|
||||
log.info("Registered abusive message filter: {}", filter.getClass().getName());
|
||||
registeredAbusiveMessageFilter = true;
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to register abusive message filter: {}", filter.getClass().getName(), e);
|
||||
}
|
||||
} else {
|
||||
log.warn("Abusive message filter {} not annotated with @FilterAbusiveMessages and will not be installed",
|
||||
filter.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (!registeredAbusiveMessageFilter) {
|
||||
log.warn("No abusive message filters installed");
|
||||
}
|
||||
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment = new WebSocketEnvironment<>(environment,
|
||||
webSocketEnvironment.getRequestLog(), 60000);
|
||||
provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
|
||||
provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
|
||||
provisioningEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||
@@ -584,16 +709,23 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
registerCorsFilter(environment);
|
||||
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
||||
|
||||
RateLimitChallengeExceptionMapper rateLimitChallengeExceptionMapper = new RateLimitChallengeExceptionMapper(rateLimitChallengeManager);
|
||||
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);
|
||||
environment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
provisioningEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
|
||||
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet );
|
||||
WebSocketResourceProviderFactory<AuthenticatedAccount> webSocketServlet = new WebSocketResourceProviderFactory<>(
|
||||
webSocketEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration());
|
||||
WebSocketResourceProviderFactory<AuthenticatedAccount> provisioningServlet = new WebSocketResourceProviderFactory<>(
|
||||
provisioningEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration());
|
||||
|
||||
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet);
|
||||
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
|
||||
|
||||
websocket.addMapping("/v1/websocket/");
|
||||
@@ -605,8 +737,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.admin().addTask(new SetRequestLoggingEnabledTask());
|
||||
environment.admin().addTask(new SetCrawlerAccelerationTask(accountDatabaseCrawlerCache));
|
||||
|
||||
///
|
||||
|
||||
environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster));
|
||||
|
||||
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge(3, TimeUnit.SECONDS));
|
||||
@@ -615,31 +745,35 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge());
|
||||
environment.metrics().register(name(FileDescriptorGauge.class, "fd_count"), new FileDescriptorGauge());
|
||||
environment.metrics().register(name(MaxFileDescriptorGauge.class, "max_fd_count"), new MaxFileDescriptorGauge());
|
||||
environment.metrics().register(name(OperatingSystemMemoryGauge.class, "buffers"), new OperatingSystemMemoryGauge("Buffers"));
|
||||
environment.metrics().register(name(OperatingSystemMemoryGauge.class, "cached"), new OperatingSystemMemoryGauge("Cached"));
|
||||
environment.metrics()
|
||||
.register(name(OperatingSystemMemoryGauge.class, "buffers"), new OperatingSystemMemoryGauge("Buffers"));
|
||||
environment.metrics()
|
||||
.register(name(OperatingSystemMemoryGauge.class, "cached"), new OperatingSystemMemoryGauge("Cached"));
|
||||
|
||||
BufferPoolGauges.registerMetrics();
|
||||
GarbageCollectionGauges.registerMetrics();
|
||||
}
|
||||
|
||||
private void registerExceptionMappers(Environment environment, WebSocketEnvironment<Account> webSocketEnvironment, WebSocketEnvironment<Account> provisioningEnvironment) {
|
||||
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());
|
||||
private void registerExceptionMappers(Environment environment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||
|
||||
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 IOExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
|
||||
provisioningEnvironment.jersey().register(new RetryLaterExceptionMapper());
|
||||
List.of(
|
||||
new LoggingUnhandledExceptionMapper(),
|
||||
new CompletionExceptionMapper(),
|
||||
new IOExceptionMapper(),
|
||||
new RateLimitExceededExceptionMapper(),
|
||||
new InvalidWebsocketAddressExceptionMapper(),
|
||||
new DeviceLimitExceededExceptionMapper(),
|
||||
new RetryLaterExceptionMapper(),
|
||||
new ServerRejectedExceptionMapper(),
|
||||
new ImpossiblePhoneNumberExceptionMapper(),
|
||||
new NonNormalizedPhoneNumberExceptionMapper()
|
||||
).forEach(exceptionMapper -> {
|
||||
environment.jersey().register(exceptionMapper);
|
||||
webSocketEnvironment.jersey().register(exceptionMapper);
|
||||
provisioningEnvironment.jersey().register(exceptionMapper);
|
||||
});
|
||||
}
|
||||
|
||||
private void registerCorsFilter(Environment environment) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.abuse;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An abusive message filter is a {@link ContainerRequestFilter} that filters requests to message-sending endpoints to
|
||||
* detect and respond to patterns of abusive behavior.
|
||||
* <p/>
|
||||
* Abusive message filters are managed components that are generally loaded dynamically via a
|
||||
* {@link java.util.ServiceLoader}. Their {@link #configure(String)} method will be called prior to be adding to the
|
||||
* server's pool of {@link Managed} objects.
|
||||
* <p/>
|
||||
* Abusive message filters must be annotated with {@link FilterAbusiveMessages}, a name binding annotation that
|
||||
* restricts the endpoints to which the filter may apply.
|
||||
*/
|
||||
public interface AbusiveMessageFilter extends ContainerRequestFilter, Managed {
|
||||
|
||||
/**
|
||||
* Configures this abusive message filter. This method will be called before the filter is added to the server's pool
|
||||
* of managed objects and before the server processes any requests.
|
||||
*
|
||||
* @param environmentName the name of the environment in which this filter is running (e.g. "staging" or "production")
|
||||
* @throws IOException if the filter could not read its configuration source for any reason
|
||||
*/
|
||||
void configure(String environmentName) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.abuse;
|
||||
|
||||
import javax.ws.rs.NameBinding;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* A name-binding annotation that associates {@link AbusiveMessageFilter}s with resource methods.
|
||||
*/
|
||||
@NameBinding
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
public @interface FilterAbusiveMessages {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public interface AccountAndAuthenticatedDeviceHolder {
|
||||
|
||||
Account getAccount();
|
||||
|
||||
Device getAuthenticatedDevice();
|
||||
}
|
||||
@@ -1,39 +1,38 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import java.util.Optional;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class AccountAuthenticator extends BaseAccountAuthenticator implements Authenticator<BasicCredentials, Account> {
|
||||
public class AccountAuthenticator extends BaseAccountAuthenticator implements
|
||||
Authenticator<BasicCredentials, AuthenticatedAccount> {
|
||||
|
||||
private static final String AUTHENTICATION_COUNTER_NAME = name(AccountAuthenticator.class, "authenticate");
|
||||
private static final String GV2_CAPABLE_TAG_NAME = "gv1Migration";
|
||||
private static final String AUTHENTICATION_COUNTER_NAME = name(AccountAuthenticator.class, "authenticate");
|
||||
|
||||
public AccountAuthenticator(AccountsManager accountsManager) {
|
||||
super(accountsManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Account> authenticate(BasicCredentials basicCredentials) {
|
||||
final Optional<Account> maybeAccount = super.authenticate(basicCredentials, true);
|
||||
public Optional<AuthenticatedAccount> authenticate(BasicCredentials basicCredentials) {
|
||||
final Optional<AuthenticatedAccount> maybeAuthenticatedAccount = super.authenticate(basicCredentials, true);
|
||||
|
||||
// TODO Remove this temporary counter when we can replace it with more generic feature adoption system
|
||||
maybeAccount.ifPresent(account -> {
|
||||
Metrics.counter(AUTHENTICATION_COUNTER_NAME, GV2_CAPABLE_TAG_NAME, String.valueOf(account.isGv1MigrationSupported())).increment();
|
||||
});
|
||||
// TODO Remove after announcement groups have launched
|
||||
maybeAuthenticatedAccount.ifPresent(authenticatedAccount ->
|
||||
Metrics.counter(AUTHENTICATION_COUNTER_NAME,
|
||||
"supportsAnnouncementGroups",
|
||||
String.valueOf(authenticatedAccount.getAccount().isAnnouncementGroupSupported()))
|
||||
.increment());
|
||||
|
||||
return maybeAccount;
|
||||
return maybeAuthenticatedAccount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class AmbiguousIdentifier {
|
||||
|
||||
private final UUID uuid;
|
||||
private final String number;
|
||||
|
||||
public AmbiguousIdentifier(String target) {
|
||||
if (target.startsWith("+")) {
|
||||
this.uuid = null;
|
||||
this.number = target;
|
||||
} else {
|
||||
this.uuid = UUID.fromString(target);
|
||||
this.number = null;
|
||||
}
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public boolean hasUuid() {
|
||||
return uuid != null;
|
||||
}
|
||||
|
||||
public boolean hasNumber() {
|
||||
return number != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return hasUuid() ? uuid.toString() : number;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
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.Pair;
|
||||
|
||||
/**
|
||||
* This {@link WebsocketRefreshRequirementProvider} observes intra-request changes in {@link Account#isEnabled()} and
|
||||
* {@link Device#isEnabled()}.
|
||||
* <p>
|
||||
* If a change in {@link Account#isEnabled()} or any associated {@link Device#isEnabled()} is observed, then any active
|
||||
* WebSocket connections for the account must be closed in order for clients to get a refreshed
|
||||
* {@link io.dropwizard.auth.Auth} object with a current device list.
|
||||
*
|
||||
* @see AuthenticatedAccount
|
||||
* @see DisabledPermittedAuthenticatedAccount
|
||||
*/
|
||||
public class AuthEnablementRefreshRequirementProvider implements WebsocketRefreshRequirementProvider {
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthEnablementRefreshRequirementProvider.class);
|
||||
|
||||
private static final String ACCOUNT_UUID = AuthEnablementRefreshRequirementProvider.class.getName() + ".accountUuid";
|
||||
private static final String DEVICES_ENABLED = AuthEnablementRefreshRequirementProvider.class.getName() + ".devicesEnabled";
|
||||
|
||||
public AuthEnablementRefreshRequirementProvider(final AccountsManager accountsManager) {
|
||||
this.accountsManager = accountsManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Map<Long, Boolean> buildDevicesEnabledMap(final Account account) {
|
||||
return account.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::isEnabled));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequestFiltered(final RequestEvent requestEvent) {
|
||||
if (requestEvent.getUriInfo().getMatchedResourceMethod().getInvocable().getHandlingMethod().getAnnotation(ChangesDeviceEnabledState.class) != null) {
|
||||
// The authenticated principal, if any, will be available after filters have run.
|
||||
// Now that the account is known, capture a snapshot of `isEnabled` for the account's devices before carrying out
|
||||
// the request’s business logic.
|
||||
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest()).ifPresent(account ->
|
||||
setAccount(requestEvent.getContainerRequest(), account));
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAccount(final ContainerRequest containerRequest, final Account account) {
|
||||
containerRequest.setProperty(ACCOUNT_UUID, account.getUuid());
|
||||
containerRequest.setProperty(DEVICES_ENABLED, buildDevicesEnabledMap(account));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
// Now that the request is finished, check whether `isEnabled` changed for any of the devices. If the value did
|
||||
// change or if a devices was added or removed, all devices must disconnect and reauthenticate.
|
||||
if (requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED) != null) {
|
||||
|
||||
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
|
||||
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||
|
||||
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
final Set<Long> deviceIdsToDisplace;
|
||||
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
|
||||
|
||||
if (!initialDevicesEnabled.equals(currentDevicesEnabled)) {
|
||||
deviceIdsToDisplace = new HashSet<>(initialDevicesEnabled.keySet());
|
||||
deviceIdsToDisplace.addAll(currentDevicesEnabled.keySet());
|
||||
} else {
|
||||
deviceIdsToDisplace = Collections.emptySet();
|
||||
}
|
||||
|
||||
return deviceIdsToDisplace.stream()
|
||||
.map(deviceId -> new Pair<>(account.getUuid(), deviceId))
|
||||
.collect(Collectors.toList());
|
||||
}).orElseGet(() -> {
|
||||
logger.error("Request had account, but it is no longer present");
|
||||
return Collections.emptyList();
|
||||
});
|
||||
} else
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.function.Supplier;
|
||||
import javax.security.auth.Subject;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
public class AuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder {
|
||||
|
||||
private final Supplier<Pair<Account, Device>> accountAndDevice;
|
||||
|
||||
public AuthenticatedAccount(final Supplier<Pair<Account, Device>> accountAndDevice) {
|
||||
this.accountAndDevice = accountAndDevice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return accountAndDevice.get().first();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Device getAuthenticatedDevice() {
|
||||
return accountAndDevice.get().second();
|
||||
}
|
||||
|
||||
// Principal implementation
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implies(final Subject subject) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class AuthorizationHeader {
|
||||
|
||||
private final AmbiguousIdentifier identifier;
|
||||
private final long deviceId;
|
||||
private final String password;
|
||||
|
||||
private AuthorizationHeader(AmbiguousIdentifier identifier, long deviceId, String password) {
|
||||
this.identifier = identifier;
|
||||
this.deviceId = deviceId;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException {
|
||||
try {
|
||||
String[] numberAndId = user.split("\\.");
|
||||
return new AuthorizationHeader(new AmbiguousIdentifier(numberAndId[0]),
|
||||
numberAndId.length > 1 ? Long.parseLong(numberAndId[1]) : 1,
|
||||
password);
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new InvalidAuthorizationHeaderException(nfe);
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthorizationHeader fromFullHeader(String header) throws InvalidAuthorizationHeaderException {
|
||||
try {
|
||||
if (header == null) {
|
||||
throw new InvalidAuthorizationHeaderException("Null header");
|
||||
}
|
||||
|
||||
String[] headerParts = header.split(" ");
|
||||
|
||||
if (headerParts == null || headerParts.length < 2) {
|
||||
throw new InvalidAuthorizationHeaderException("Invalid authorization header: " + header);
|
||||
}
|
||||
|
||||
if (!"Basic".equals(headerParts[0])) {
|
||||
throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + headerParts[0]);
|
||||
}
|
||||
|
||||
String concatenatedValues = new String(Base64.getDecoder().decode(headerParts[1]));
|
||||
|
||||
if (Util.isEmpty(concatenatedValues)) {
|
||||
throw new InvalidAuthorizationHeaderException("Bad decoded value: " + concatenatedValues);
|
||||
}
|
||||
|
||||
String[] credentialParts = concatenatedValues.split(":");
|
||||
|
||||
if (credentialParts == null || credentialParts.length < 2) {
|
||||
throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues);
|
||||
}
|
||||
|
||||
return fromUserAndPassword(credentialParts[0], credentialParts[1]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new InvalidAuthorizationHeaderException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public AmbiguousIdentifier getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -9,17 +9,19 @@ import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
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 java.util.UUID;
|
||||
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.storage.RefreshingAccountAndDeviceSupplier;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class BaseAccountAuthenticator {
|
||||
@@ -28,7 +30,6 @@ public class BaseAccountAuthenticator {
|
||||
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 static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
|
||||
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
|
||||
@@ -46,23 +47,45 @@ public class BaseAccountAuthenticator {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
|
||||
static Pair<String, Long> getIdentifierAndDeviceId(final String basicUsername) {
|
||||
final String identifier;
|
||||
final long deviceId;
|
||||
|
||||
final int deviceIdSeparatorIndex = basicUsername.indexOf('.');
|
||||
|
||||
if (deviceIdSeparatorIndex == -1) {
|
||||
identifier = basicUsername;
|
||||
deviceId = Device.MASTER_ID;
|
||||
} else {
|
||||
identifier = basicUsername.substring(0, deviceIdSeparatorIndex);
|
||||
deviceId = Long.parseLong(basicUsername.substring(deviceIdSeparatorIndex + 1));
|
||||
}
|
||||
|
||||
return new Pair<>(identifier, deviceId);
|
||||
}
|
||||
|
||||
public Optional<AuthenticatedAccount> 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());
|
||||
final UUID accountUuid;
|
||||
final long deviceId;
|
||||
{
|
||||
final Pair<String, Long> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
|
||||
|
||||
credentialType = authorizationHeader.getIdentifier().hasNumber() ? "e164" : "uuid";
|
||||
accountUuid = UUID.fromString(identifierAndDeviceId.first());
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
}
|
||||
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
failureReason = "noSuchAccount";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
|
||||
Optional<Device> device = account.get().getDevice(deviceId);
|
||||
|
||||
if (device.isEmpty()) {
|
||||
failureReason = "noSuchDevice";
|
||||
@@ -83,9 +106,9 @@ public class BaseAccountAuthenticator {
|
||||
|
||||
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
|
||||
succeeded = true;
|
||||
account.get().setAuthenticatedDevice(device.get());
|
||||
updateLastSeen(account.get(), device.get());
|
||||
return account;
|
||||
final Account authenticatedAccount = updateLastSeen(account.get(), device.get());
|
||||
return Optional.of(new AuthenticatedAccount(
|
||||
new RefreshingAccountAndDeviceSupplier(authenticatedAccount, device.get().getId(), accountsManager)));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
@@ -101,16 +124,12 @@ public class BaseAccountAuthenticator {
|
||||
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());
|
||||
|
||||
@@ -118,9 +137,10 @@ public class BaseAccountAuthenticator {
|
||||
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
|
||||
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
|
||||
|
||||
device.setLastSeen(Util.todayInMillis(clock));
|
||||
accountsManager.update(account);
|
||||
return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock));
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
public class BasicAuthorizationHeader {
|
||||
|
||||
private final String username;
|
||||
private final long deviceId;
|
||||
private final String password;
|
||||
|
||||
private BasicAuthorizationHeader(final String username, final long deviceId, final String password) {
|
||||
this.username = username;
|
||||
this.deviceId = deviceId;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public static BasicAuthorizationHeader fromString(final String header) throws InvalidAuthorizationHeaderException {
|
||||
try {
|
||||
if (StringUtils.isBlank(header)) {
|
||||
throw new InvalidAuthorizationHeaderException("Blank header");
|
||||
}
|
||||
|
||||
final int spaceIndex = header.indexOf(' ');
|
||||
|
||||
if (spaceIndex == -1) {
|
||||
throw new InvalidAuthorizationHeaderException("Invalid authorization header: " + header);
|
||||
}
|
||||
|
||||
final String authorizationType = header.substring(0, spaceIndex);
|
||||
|
||||
if (!"Basic".equals(authorizationType)) {
|
||||
throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + authorizationType);
|
||||
}
|
||||
|
||||
final String credentials;
|
||||
|
||||
try {
|
||||
credentials = new String(Base64.getDecoder().decode(header.substring(spaceIndex + 1)));
|
||||
} catch (final IndexOutOfBoundsException e) {
|
||||
throw new InvalidAuthorizationHeaderException("Missing credentials");
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(credentials)) {
|
||||
throw new InvalidAuthorizationHeaderException("Bad decoded value: " + credentials);
|
||||
}
|
||||
|
||||
final int credentialSeparatorIndex = credentials.indexOf(':');
|
||||
|
||||
if (credentialSeparatorIndex == -1) {
|
||||
throw new InvalidAuthorizationHeaderException("Badly-formatted credentials: " + credentials);
|
||||
}
|
||||
|
||||
final String usernameComponent = credentials.substring(0, credentialSeparatorIndex);
|
||||
|
||||
final String username;
|
||||
final long deviceId;
|
||||
{
|
||||
final Pair<String, Long> identifierAndDeviceId =
|
||||
BaseAccountAuthenticator.getIdentifierAndDeviceId(usernameComponent);
|
||||
|
||||
username = identifierAndDeviceId.first();
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
}
|
||||
|
||||
final String password = credentials.substring(credentialSeparatorIndex + 1);
|
||||
|
||||
if (StringUtils.isAnyBlank(username, password)) {
|
||||
throw new InvalidAuthorizationHeaderException("Username or password were blank");
|
||||
}
|
||||
|
||||
return new BasicAuthorizationHeader(username, deviceId, password);
|
||||
} catch (final IllegalArgumentException | IndexOutOfBoundsException e) {
|
||||
throw new InvalidAuthorizationHeaderException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Indicates that an endpoint may change the "enabled" state of one or more devices associated with an account, and that
|
||||
* any websockets associated with the account may need to be refreshed after a call to that endpoint.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ChangesDeviceEnabledState {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import java.util.Optional;
|
||||
|
||||
class ContainerRequestUtil {
|
||||
|
||||
static Optional<Account> getAuthenticatedAccount(final ContainerRequest request) {
|
||||
return Optional.ofNullable(request.getSecurityContext())
|
||||
.map(SecurityContext::getUserPrincipal)
|
||||
.map(principal -> principal instanceof AccountAndAuthenticatedDeviceHolder
|
||||
? ((AccountAndAuthenticatedDeviceHolder) principal).getAccount() : null);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import java.security.Principal;
|
||||
|
||||
public class DisabledPermittedAccount implements Principal {
|
||||
|
||||
private final Account account;
|
||||
|
||||
public DisabledPermittedAccount(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
// Principal implementation
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implies(Subject subject) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements Authenticator<BasicCredentials, DisabledPermittedAccount> {
|
||||
public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements
|
||||
Authenticator<BasicCredentials, DisabledPermittedAuthenticatedAccount> {
|
||||
|
||||
public DisabledPermittedAccountAuthenticator(AccountsManager accountsManager) {
|
||||
super(accountsManager);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Optional<DisabledPermittedAccount> authenticate(BasicCredentials credentials) {
|
||||
Optional<Account> account = super.authenticate(credentials, false);
|
||||
return account.map(DisabledPermittedAccount::new);
|
||||
public Optional<DisabledPermittedAuthenticatedAccount> authenticate(BasicCredentials credentials) {
|
||||
Optional<AuthenticatedAccount> account = super.authenticate(credentials, false);
|
||||
return account.map(DisabledPermittedAuthenticatedAccount::new);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.security.Principal;
|
||||
import javax.security.auth.Subject;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class DisabledPermittedAuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder {
|
||||
|
||||
private final AuthenticatedAccount authenticatedAccount;
|
||||
|
||||
public DisabledPermittedAuthenticatedAccount(final AuthenticatedAccount authenticatedAccount) {
|
||||
this.authenticatedAccount = authenticatedAccount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return authenticatedAccount.getAccount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Device getAuthenticatedDevice() {
|
||||
return authenticatedAccount.getAuthenticatedDevice();
|
||||
}
|
||||
|
||||
// Principal implementation
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implies(Subject subject) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,71 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class ExternalServiceCredentialGenerator {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ExternalServiceCredentialGenerator.class);
|
||||
|
||||
private final byte[] key;
|
||||
private final byte[] userIdKey;
|
||||
private final boolean usernameDerivation;
|
||||
private final boolean prependUsername;
|
||||
private final Clock clock;
|
||||
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
|
||||
this.key = key;
|
||||
this.userIdKey = userIdKey;
|
||||
this.usernameDerivation = usernameDerivation;
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey) {
|
||||
this(key, userIdKey, true, true);
|
||||
}
|
||||
|
||||
public ExternalServiceCredentials generateFor(String number) {
|
||||
Mac mac = getMacInstance();
|
||||
String username = getUserId(number, mac, usernameDerivation);
|
||||
long currentTimeSeconds = System.currentTimeMillis() / 1000;
|
||||
String prefix = username + ":" + currentTimeSeconds;
|
||||
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
|
||||
String token = prefix + ":" + output;
|
||||
public ExternalServiceCredentialGenerator(byte[] key, boolean prependUsername) {
|
||||
this(key, new byte[0], false, prependUsername);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
|
||||
this(key, userIdKey, usernameDerivation, true);
|
||||
}
|
||||
|
||||
private ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
|
||||
boolean prependUsername) {
|
||||
this(key, userIdKey, usernameDerivation, prependUsername, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
|
||||
boolean prependUsername, Clock clock) {
|
||||
this.key = key;
|
||||
this.userIdKey = userIdKey;
|
||||
this.usernameDerivation = usernameDerivation;
|
||||
this.prependUsername = prependUsername;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public ExternalServiceCredentials generateFor(String identity) {
|
||||
Mac mac = getMacInstance();
|
||||
String username = getUserId(identity, mac, usernameDerivation);
|
||||
long currentTimeSeconds = clock.millis() / 1000;
|
||||
String prefix = username + ":" + currentTimeSeconds;
|
||||
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
|
||||
String token = (prependUsername ? prefix : currentTimeSeconds) + ":" + output;
|
||||
|
||||
return new ExternalServiceCredentials(username, token);
|
||||
}
|
||||
|
||||
|
||||
public boolean isValid(String token, String number, long currentTimeMillis) {
|
||||
String[] parts = token.split(":");
|
||||
Mac mac = getMacInstance();
|
||||
|
||||
if (parts.length != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getUserId(number, mac, usernameDerivation).equals(parts[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidTime(parts[1], currentTimeMillis)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isValidSignature(parts[0] + ":" + parts[1], parts[2], mac);
|
||||
}
|
||||
|
||||
private String getUserId(String number, Mac mac, boolean usernameDerivation) {
|
||||
if (usernameDerivation) return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10));
|
||||
else return number;
|
||||
}
|
||||
|
||||
private boolean isValidTime(String timeString, long currentTimeMillis) {
|
||||
try {
|
||||
long tokenTime = Long.parseLong(timeString);
|
||||
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
|
||||
|
||||
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Number Format", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidSignature(String prefix, String suffix, Mac mac) {
|
||||
try {
|
||||
byte[] ourSuffix = Util.truncate(getHmac(key, prefix.getBytes(), mac), 10);
|
||||
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
|
||||
|
||||
return MessageDigest.isEqual(ourSuffix, theirSuffix);
|
||||
} catch (DecoderException e) {
|
||||
logger.warn("DirectoryCredentials", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Mac getMacInstance() {
|
||||
try {
|
||||
return Mac.getInstance("HmacSHA256");
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
|
||||
public class InvalidAuthorizationHeaderException extends Exception {
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
public class InvalidAuthorizationHeaderException extends WebApplicationException {
|
||||
public InvalidAuthorizationHeaderException(String s) {
|
||||
super(s);
|
||||
super(s, Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public InvalidAuthorizationHeaderException(Exception e) {
|
||||
super(e);
|
||||
super(e, Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
public class PhoneNumberChangeRefreshRequirementProvider implements WebsocketRefreshRequirementProvider {
|
||||
|
||||
private static final String INITIAL_NUMBER_KEY =
|
||||
PhoneNumberChangeRefreshRequirementProvider.class.getName() + ".initialNumber";
|
||||
|
||||
@Override
|
||||
public void handleRequestFiltered(final RequestEvent requestEvent) {
|
||||
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest())
|
||||
.ifPresent(account -> requestEvent.getContainerRequest().setProperty(INITIAL_NUMBER_KEY, account.getNumber()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
final String initialNumber = (String) requestEvent.getContainerRequest().getProperty(INITIAL_NUMBER_KEY);
|
||||
|
||||
if (initialNumber != null) {
|
||||
final Optional<Account> maybeAuthenticatedAccount =
|
||||
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest());
|
||||
|
||||
return maybeAuthenticatedAccount
|
||||
.filter(account -> !initialNumber.equals(account.getNumber()))
|
||||
.map(account -> account.getDevices().stream()
|
||||
.map(device -> new Pair<>(account.getUuid(), device.getId()))
|
||||
.collect(Collectors.toList()))
|
||||
.orElse(Collections.emptyList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -20,42 +19,33 @@ public class StoredRegistrationLock {
|
||||
|
||||
private final Optional<String> registrationLockSalt;
|
||||
|
||||
private final Optional<String> deprecatedPin;
|
||||
|
||||
private final long lastSeen;
|
||||
|
||||
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, Optional<String> deprecatedPin, long lastSeen) {
|
||||
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, long lastSeen) {
|
||||
this.registrationLock = registrationLock;
|
||||
this.registrationLockSalt = registrationLockSalt;
|
||||
this.deprecatedPin = deprecatedPin;
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
|
||||
public boolean requiresClientRegistrationLock() {
|
||||
return ((registrationLock.isPresent() && registrationLockSalt.isPresent()) || deprecatedPin.isPresent()) && System.currentTimeMillis() - lastSeen < TimeUnit.DAYS.toMillis(7);
|
||||
return registrationLock.isPresent() && registrationLockSalt.isPresent() && System.currentTimeMillis() - lastSeen < TimeUnit.DAYS.toMillis(7);
|
||||
}
|
||||
|
||||
public boolean needsFailureCredentials() {
|
||||
return registrationLock.isPresent() && registrationLockSalt.isPresent();
|
||||
}
|
||||
|
||||
public boolean hasDeprecatedPin() {
|
||||
return deprecatedPin.isPresent();
|
||||
}
|
||||
|
||||
public long getTimeRemaining() {
|
||||
return TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - lastSeen);
|
||||
}
|
||||
|
||||
public boolean verify(@Nullable String clientRegistrationLock, @Nullable String clientDeprecatedPin) {
|
||||
if (Util.isEmpty(clientRegistrationLock) && Util.isEmpty(clientDeprecatedPin)) {
|
||||
public boolean verify(@Nullable String clientRegistrationLock) {
|
||||
if (Util.isEmpty(clientRegistrationLock)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (registrationLock.isPresent() && registrationLockSalt.isPresent() && !Util.isEmpty(clientRegistrationLock)) {
|
||||
return new AuthenticationCredentials(registrationLock.get(), registrationLockSalt.get()).verify(clientRegistrationLock);
|
||||
} else if (deprecatedPin.isPresent() && !Util.isEmpty(clientDeprecatedPin)) {
|
||||
return MessageDigest.isEqual(deprecatedPin.get().getBytes(), clientDeprecatedPin.getBytes());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -63,6 +53,6 @@ public class StoredRegistrationLock {
|
||||
|
||||
@VisibleForTesting
|
||||
public StoredRegistrationLock forTime(long timestamp) {
|
||||
return new StoredRegistrationLock(registrationLock, registrationLockSalt, deprecatedPin, timestamp);
|
||||
return new StoredRegistrationLock(registrationLock, registrationLockSalt, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
|
||||
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEventListener;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
/**
|
||||
* Delegates request events to a listener that watches for intra-request changes that require websocket refreshes
|
||||
*/
|
||||
public class WebsocketRefreshApplicationEventListener implements ApplicationEventListener {
|
||||
|
||||
private final WebsocketRefreshRequestEventListener websocketRefreshRequestEventListener;
|
||||
|
||||
public WebsocketRefreshApplicationEventListener(final AccountsManager accountsManager,
|
||||
final ClientPresenceManager clientPresenceManager) {
|
||||
|
||||
this.websocketRefreshRequestEventListener = new WebsocketRefreshRequestEventListener(clientPresenceManager,
|
||||
new AuthEnablementRefreshRequirementProvider(accountsManager),
|
||||
new PhoneNumberChangeRefreshRequirementProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(final ApplicationEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestEventListener onRequest(final RequestEvent requestEvent) {
|
||||
return websocketRefreshRequestEventListener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent.Type;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEventListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
|
||||
import javax.ws.rs.container.ResourceInfo;
|
||||
import javax.ws.rs.core.Context;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
public class WebsocketRefreshRequestEventListener implements RequestEventListener {
|
||||
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final WebsocketRefreshRequirementProvider[] providers;
|
||||
|
||||
private static final Counter DISPLACED_ACCOUNTS = Metrics.counter(
|
||||
name(WebsocketRefreshRequestEventListener.class, "displacedAccounts"));
|
||||
|
||||
private static final Counter DISPLACED_DEVICES = Metrics.counter(
|
||||
name(WebsocketRefreshRequestEventListener.class, "displacedDevices"));
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebsocketRefreshRequestEventListener.class);
|
||||
|
||||
public WebsocketRefreshRequestEventListener(
|
||||
final ClientPresenceManager clientPresenceManager,
|
||||
final WebsocketRefreshRequirementProvider... providers) {
|
||||
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
@Context
|
||||
private ResourceInfo resourceInfo;
|
||||
|
||||
@Override
|
||||
public void onEvent(final RequestEvent event) {
|
||||
if (event.getType() == Type.REQUEST_FILTERED) {
|
||||
for (final WebsocketRefreshRequirementProvider provider : providers) {
|
||||
provider.handleRequestFiltered(event);
|
||||
}
|
||||
} else if (event.getType() == Type.FINISHED) {
|
||||
final AtomicInteger displacedDevices = new AtomicInteger(0);
|
||||
|
||||
Arrays.stream(providers)
|
||||
.flatMap(provider -> provider.handleRequestFinished(event).stream())
|
||||
.distinct()
|
||||
.forEach(pair -> {
|
||||
try {
|
||||
displacedDevices.incrementAndGet();
|
||||
clientPresenceManager.displacePresence(pair.first(), pair.second());
|
||||
} catch (final Exception e) {
|
||||
logger.error("Could not displace device presence", e);
|
||||
}
|
||||
});
|
||||
|
||||
if (displacedDevices.get() > 0) {
|
||||
DISPLACED_ACCOUNTS.increment();
|
||||
DISPLACED_DEVICES.increment(displacedDevices.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
/**
|
||||
* A websocket refresh requirement provider watches for intra-request changes (e.g. to authentication status) that
|
||||
* require a websocket refresh.
|
||||
*/
|
||||
public interface WebsocketRefreshRequirementProvider {
|
||||
|
||||
/**
|
||||
* Processes a request after filters have run and the request has been mapped to a destination controller.
|
||||
*
|
||||
* @param requestEvent the request event to observe
|
||||
*/
|
||||
void handleRequestFiltered(RequestEvent requestEvent);
|
||||
|
||||
/**
|
||||
* Processes a request after all normal request handling has been completed.
|
||||
*
|
||||
* @param requestEvent the request event to observe
|
||||
* @return a list of pairs of account UUID/device ID pairs identifying websockets that need to be refreshed as a
|
||||
* result of the observed request
|
||||
*/
|
||||
List<Pair<UUID, Long>> handleRequestFinished(RequestEvent requestEvent);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.badges;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
|
||||
public interface BadgeTranslator {
|
||||
Badge translate(List<Locale> acceptableLanguages, String badgeId);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.badges;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.signal.i18n.HeaderControlledResourceBundleLookup;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
||||
import org.whispersystems.textsecuregcm.entities.SelfBadge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
|
||||
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, BadgeTranslator {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String BASE_NAME = "org.signal.badges.Badges";
|
||||
|
||||
private final Clock clock;
|
||||
private final Map<String, BadgeConfiguration> knownBadges;
|
||||
private final List<String> badgeIdsEnabledForAll;
|
||||
private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup;
|
||||
|
||||
public ConfiguredProfileBadgeConverter(
|
||||
final Clock clock,
|
||||
final BadgesConfiguration badgesConfiguration,
|
||||
final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) {
|
||||
this.clock = clock;
|
||||
this.knownBadges = badgesConfiguration.getBadges().stream()
|
||||
.collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity()));
|
||||
this.badgeIdsEnabledForAll = badgesConfiguration.getBadgeIdsEnabledForAll();
|
||||
this.headerControlledResourceBundleLookup = headerControlledResourceBundleLookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Badge translate(final List<Locale> acceptableLanguages, final String badgeId) {
|
||||
final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
|
||||
acceptableLanguages);
|
||||
final BadgeConfiguration configuration = knownBadges.get(badgeId);
|
||||
return newBadge(
|
||||
false,
|
||||
configuration.getId(),
|
||||
configuration.getCategory(),
|
||||
resourceBundle.getString(configuration.getId() + "_name"),
|
||||
resourceBundle.getString(configuration.getId() + "_description"),
|
||||
configuration.getSprites(),
|
||||
configuration.getSvg(),
|
||||
configuration.getSvgs(),
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Badge> convert(
|
||||
final List<Locale> acceptableLanguages,
|
||||
final List<AccountBadge> accountBadges,
|
||||
final boolean isSelf) {
|
||||
if (accountBadges.isEmpty() && badgeIdsEnabledForAll.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final Instant now = clock.instant();
|
||||
final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
|
||||
acceptableLanguages);
|
||||
List<Badge> badges = accountBadges.stream()
|
||||
.filter(accountBadge -> (isSelf || accountBadge.isVisible())
|
||||
&& now.isBefore(accountBadge.getExpiration())
|
||||
&& knownBadges.containsKey(accountBadge.getId()))
|
||||
.map(accountBadge -> {
|
||||
BadgeConfiguration configuration = knownBadges.get(accountBadge.getId());
|
||||
return newBadge(
|
||||
isSelf,
|
||||
accountBadge.getId(),
|
||||
configuration.getCategory(),
|
||||
resourceBundle.getString(accountBadge.getId() + "_name"),
|
||||
resourceBundle.getString(accountBadge.getId() + "_description"),
|
||||
configuration.getSprites(),
|
||||
configuration.getSvg(),
|
||||
configuration.getSvgs(),
|
||||
accountBadge.getExpiration(),
|
||||
accountBadge.isVisible());
|
||||
})
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
badges.addAll(badgeIdsEnabledForAll.stream().filter(knownBadges::containsKey).map(id -> {
|
||||
BadgeConfiguration configuration = knownBadges.get(id);
|
||||
return newBadge(
|
||||
isSelf,
|
||||
id,
|
||||
configuration.getCategory(),
|
||||
resourceBundle.getString(id + "_name"),
|
||||
resourceBundle.getString(id + "_description"),
|
||||
configuration.getSprites(),
|
||||
configuration.getSvg(),
|
||||
configuration.getSvgs(),
|
||||
now.plus(Duration.ofDays(1)),
|
||||
true);
|
||||
}).collect(Collectors.toList()));
|
||||
return badges;
|
||||
}
|
||||
|
||||
private Badge newBadge(
|
||||
final boolean isSelf,
|
||||
final String id,
|
||||
final String category,
|
||||
final String name,
|
||||
final String description,
|
||||
final List<String> sprites,
|
||||
final String svg,
|
||||
final List<BadgeSvg> svgs,
|
||||
final Instant expiration,
|
||||
final boolean visible) {
|
||||
if (isSelf) {
|
||||
return new SelfBadge(id, category, name, description, sprites, svg, svgs, expiration, visible);
|
||||
} else {
|
||||
return new Badge(id, category, name, description, sprites, svg, svgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.badges;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public interface LevelTranslator {
|
||||
String translate(List<Locale> acceptableLanguages, String badgeId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.badges;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
|
||||
public interface ProfileBadgeConverter {
|
||||
|
||||
/**
|
||||
* Converts the {@link AccountBadge}s for an account into the objects
|
||||
* that can be returned on a profile fetch.
|
||||
*/
|
||||
List<Badge> convert(List<Locale> acceptableLanguages, List<AccountBadge> accountBadges, boolean isSelf);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.badges;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import javax.annotation.Nonnull;
|
||||
import org.signal.i18n.HeaderControlledResourceBundleLookup;
|
||||
|
||||
public class ResourceBundleLevelTranslator implements LevelTranslator {
|
||||
|
||||
private static final String BASE_NAME = "org.signal.subscriptions.Subscriptions";
|
||||
|
||||
private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup;
|
||||
|
||||
public ResourceBundleLevelTranslator(
|
||||
@Nonnull final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) {
|
||||
this.headerControlledResourceBundleLookup = Objects.requireNonNull(headerControlledResourceBundleLookup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String translate(final List<Locale> acceptableLanguages, final String badgeId) {
|
||||
final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
|
||||
acceptableLanguages);
|
||||
return resourceBundle.getString(badgeId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
public class AbusiveMessageFilterConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotBlank
|
||||
private final String environment;
|
||||
|
||||
@JsonCreator
|
||||
public AbusiveMessageFilterConfiguration(@JsonProperty("environment") final String environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
public String getEnvironment() {
|
||||
return environment;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
@@ -7,7 +8,24 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
@NotNull
|
||||
private String phoneNumberTableName;
|
||||
|
||||
@NotNull
|
||||
private String phoneNumberIdentifierTableName;
|
||||
|
||||
private int scanPageSize = 100;
|
||||
|
||||
@JsonProperty
|
||||
public String getPhoneNumberTableName() {
|
||||
return phoneNumberTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public String getPhoneNumberIdentifierTableName() {
|
||||
return phoneNumberIdentifierTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public int getScanPageSize() {
|
||||
return scanPageSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public class BadgeConfiguration {
|
||||
public static final String CATEGORY_TESTING = "testing";
|
||||
|
||||
private final String id;
|
||||
private final String category;
|
||||
private final List<String> sprites;
|
||||
private final String svg;
|
||||
private final List<BadgeSvg> svgs;
|
||||
|
||||
@JsonCreator
|
||||
public BadgeConfiguration(
|
||||
@JsonProperty("id") final String id,
|
||||
@JsonProperty("category") final String category,
|
||||
@JsonProperty("sprites") final List<String> sprites,
|
||||
@JsonProperty("svg") final String svg,
|
||||
@JsonProperty("svgs") final List<BadgeSvg> svgs) {
|
||||
this.id = id;
|
||||
this.category = category;
|
||||
this.sprites = sprites;
|
||||
this.svg = svg;
|
||||
this.svgs = svgs;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ExactlySize(6)
|
||||
public List<String> getSprites() {
|
||||
return sprites;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getSvg() {
|
||||
return svg;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<BadgeSvg> getSvgs() {
|
||||
return svgs;
|
||||
}
|
||||
|
||||
public boolean isTestBadge() {
|
||||
return CATEGORY_TESTING.equals(category);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
import com.fasterxml.jackson.annotation.Nulls;
|
||||
import io.dropwizard.validation.ValidationMethod;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class BadgesConfiguration {
|
||||
private final List<BadgeConfiguration> badges;
|
||||
private final List<String> badgeIdsEnabledForAll;
|
||||
private final Map<Long, String> receiptLevels;
|
||||
|
||||
@JsonCreator
|
||||
public BadgesConfiguration(
|
||||
@JsonProperty("badges") @JsonSetter(nulls = Nulls.AS_EMPTY) final List<BadgeConfiguration> badges,
|
||||
@JsonProperty("badgeIdsEnabledForAll") @JsonSetter(nulls = Nulls.AS_EMPTY) final List<String> badgeIdsEnabledForAll,
|
||||
@JsonProperty("receiptLevels") @JsonSetter(nulls = Nulls.AS_EMPTY) final Map<Long, String> receiptLevels) {
|
||||
this.badges = Objects.requireNonNull(badges);
|
||||
this.badgeIdsEnabledForAll = Objects.requireNonNull(badgeIdsEnabledForAll);
|
||||
this.receiptLevels = Objects.requireNonNull(receiptLevels);
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public List<BadgeConfiguration> getBadges() {
|
||||
return badges;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public List<String> getBadgeIdsEnabledForAll() {
|
||||
return badgeIdsEnabledForAll;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public Map<Long, String> getReceiptLevels() {
|
||||
return receiptLevels;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@ValidationMethod(message = "contains receipt level mappings that are not configured badges")
|
||||
public boolean isAllReceiptLevelsConfigured() {
|
||||
final Set<String> badgeNames = badges.stream().map(BadgeConfiguration::getId).collect(Collectors.toSet());
|
||||
return badgeNames.containsAll(receiptLevels.values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public class BoostConfiguration {
|
||||
|
||||
private final long level;
|
||||
private final Duration expiration;
|
||||
private final Map<String, List<BigDecimal>> currencies;
|
||||
private final String badge;
|
||||
|
||||
@JsonCreator
|
||||
public BoostConfiguration(
|
||||
@JsonProperty("level") long level,
|
||||
@JsonProperty("expiration") Duration expiration,
|
||||
@JsonProperty("currencies") Map<String, List<BigDecimal>> currencies,
|
||||
@JsonProperty("badge") String badge) {
|
||||
this.level = level;
|
||||
this.expiration = expiration;
|
||||
this.currencies = currencies;
|
||||
this.badge = badge;
|
||||
}
|
||||
|
||||
public long getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Duration getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() {
|
||||
return currencies;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getBadge() {
|
||||
return badge;
|
||||
}
|
||||
}
|
||||
@@ -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,26 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public class DirectoryV2ClientConfiguration {
|
||||
|
||||
private final byte[] userAuthenticationTokenSharedSecret;
|
||||
|
||||
@JsonCreator
|
||||
public DirectoryV2ClientConfiguration(
|
||||
@JsonProperty("userAuthenticationTokenSharedSecret") final byte[] userAuthenticationTokenSharedSecret) {
|
||||
this.userAuthenticationTokenSharedSecret = userAuthenticationTokenSharedSecret;
|
||||
}
|
||||
|
||||
@ExactlySize({32})
|
||||
public byte[] getUserAuthenticationTokenSharedSecret() {
|
||||
return userAuthenticationTokenSharedSecret;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.Valid;
|
||||
|
||||
public class DirectoryV2Configuration {
|
||||
|
||||
private final DirectoryV2ClientConfiguration clientConfiguration;
|
||||
|
||||
@JsonCreator
|
||||
public DirectoryV2Configuration(@JsonProperty("client") DirectoryV2ClientConfiguration clientConfiguration) {
|
||||
this.clientConfiguration = clientConfiguration;
|
||||
}
|
||||
|
||||
@Valid
|
||||
public DirectoryV2ClientConfiguration getDirectoryV2ClientConfiguration() {
|
||||
return clientConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ 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();
|
||||
@@ -32,17 +31,6 @@ public class DonationConfiguration {
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Duration;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class DynamoDbClientConfiguration {
|
||||
|
||||
private final String region;
|
||||
private final Duration clientExecutionTimeout;
|
||||
private final Duration clientRequestTimeout;
|
||||
|
||||
@JsonCreator
|
||||
public DynamoDbClientConfiguration(
|
||||
@JsonProperty("region") final String region,
|
||||
@JsonProperty("clientExcecutionTimeout") final Duration clientExecutionTimeout,
|
||||
@JsonProperty("clientRequestTimeout") final Duration clientRequestTimeout) {
|
||||
this.region = region;
|
||||
this.clientExecutionTimeout = clientExecutionTimeout != null ? clientExecutionTimeout : Duration.ofSeconds(30);
|
||||
this.clientRequestTimeout = clientRequestTimeout != null ? clientRequestTimeout : Duration.ofSeconds(10);
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public Duration getClientExecutionTimeout() {
|
||||
return clientExecutionTimeout;
|
||||
}
|
||||
|
||||
public Duration getClientRequestTimeout() {
|
||||
return clientRequestTimeout;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Duration;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DynamoDbTables {
|
||||
|
||||
public static class Table {
|
||||
private final String tableName;
|
||||
|
||||
@JsonCreator
|
||||
public Table(
|
||||
@JsonProperty("tableName") final String tableName) {
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getTableName() {
|
||||
return tableName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TableWithExpiration extends Table {
|
||||
private final Duration expiration;
|
||||
|
||||
@JsonCreator
|
||||
public TableWithExpiration(
|
||||
@JsonProperty("tableName") final String tableName,
|
||||
@JsonProperty("expiration") final Duration expiration) {
|
||||
super(tableName);
|
||||
this.expiration = expiration;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Duration getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
}
|
||||
|
||||
private final IssuedReceiptsTableConfiguration issuedReceipts;
|
||||
private final TableWithExpiration redeemedReceipts;
|
||||
private final Table subscriptions;
|
||||
|
||||
@JsonCreator
|
||||
public DynamoDbTables(
|
||||
@JsonProperty("issuedReceipts") final IssuedReceiptsTableConfiguration issuedReceipts,
|
||||
@JsonProperty("redeemedReceipts") final TableWithExpiration redeemedReceipts,
|
||||
@JsonProperty("subscriptions") final Table subscriptions) {
|
||||
this.issuedReceipts = issuedReceipts;
|
||||
this.redeemedReceipts = redeemedReceipts;
|
||||
this.subscriptions = subscriptions;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public IssuedReceiptsTableConfiguration getIssuedReceipts() {
|
||||
return issuedReceipts;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public TableWithExpiration getRedeemedReceipts() {
|
||||
return redeemedReceipts;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
public Table getSubscriptions() {
|
||||
return subscriptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Duration;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class IssuedReceiptsTableConfiguration extends DynamoDbTables.TableWithExpiration {
|
||||
|
||||
private final byte[] generator;
|
||||
|
||||
public IssuedReceiptsTableConfiguration(
|
||||
@JsonProperty("tableName") final String tableName,
|
||||
@JsonProperty("expiration") final Duration expiration,
|
||||
@JsonProperty("generator") final byte[] generator) {
|
||||
super(tableName, expiration);
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public byte[] getGenerator() {
|
||||
return generator;
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,12 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.time.Duration;
|
||||
import javax.validation.Valid;
|
||||
|
||||
public class MessageDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
|
||||
private Duration timeToLive = Duration.ofDays(7);
|
||||
private Duration timeToLive = Duration.ofDays(14);
|
||||
|
||||
@Valid
|
||||
public Duration getTimeToLive() {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class RecaptchaV2Configuration {
|
||||
|
||||
private BigDecimal scoreFloor;
|
||||
private String projectPath;
|
||||
private String siteKey;
|
||||
private String credentialConfigurationJson;
|
||||
|
||||
@DecimalMin("0")
|
||||
@DecimalMax("1")
|
||||
@NotNull
|
||||
public BigDecimal getScoreFloor() {
|
||||
return scoreFloor;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getProjectPath() {
|
||||
return projectPath;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getSiteKey() {
|
||||
return siteKey;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getCredentialConfigurationJson() {
|
||||
return credentialConfigurationJson;
|
||||
}
|
||||
}
|
||||
@@ -11,41 +11,40 @@ import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
public class RedisClusterConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private List<String> urls;
|
||||
@JsonProperty
|
||||
@NotEmpty
|
||||
private String configurationUri;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Duration timeout = Duration.ofMillis(3_000);
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Duration timeout = Duration.ofMillis(3_000);
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private RetryConfiguration retry = new RetryConfiguration();
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private RetryConfiguration retry = new RetryConfiguration();
|
||||
|
||||
public List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
public String getConfigurationUri() {
|
||||
return configurationUri;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
public Duration getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
public CircuitBreakerConfiguration getCircuitBreakerConfiguration() {
|
||||
return circuitBreaker;
|
||||
}
|
||||
|
||||
public RetryConfiguration getRetryConfiguration() {
|
||||
return retry;
|
||||
}
|
||||
public RetryConfiguration getRetryConfiguration() {
|
||||
return retry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
|
||||
public class ReportMessageConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private final Duration reportTtl = Duration.ofDays(7);
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private final Duration counterTtl = Duration.ofDays(1);
|
||||
|
||||
public Duration getReportTtl() {
|
||||
return reportTtl;
|
||||
}
|
||||
|
||||
public Duration getCounterTtl() {
|
||||
return counterTtl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
public class StripeConfiguration {
|
||||
|
||||
private final String apiKey;
|
||||
private final byte[] idempotencyKeyGenerator;
|
||||
|
||||
@JsonCreator
|
||||
public StripeConfiguration(
|
||||
@JsonProperty("apiKey") final String apiKey,
|
||||
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator) {
|
||||
this.apiKey = apiKey;
|
||||
this.idempotencyKeyGenerator = idempotencyKeyGenerator;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public byte[] getIdempotencyKeyGenerator() {
|
||||
return idempotencyKeyGenerator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.dropwizard.validation.ValidationMethod;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class SubscriptionConfiguration {
|
||||
|
||||
private final Duration badgeGracePeriod;
|
||||
private final Map<Long, SubscriptionLevelConfiguration> levels;
|
||||
|
||||
@JsonCreator
|
||||
public SubscriptionConfiguration(
|
||||
@JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
|
||||
@JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) {
|
||||
this.badgeGracePeriod = badgeGracePeriod;
|
||||
this.levels = levels;
|
||||
}
|
||||
|
||||
public Duration getBadgeGracePeriod() {
|
||||
return badgeGracePeriod;
|
||||
}
|
||||
|
||||
public Map<Long, SubscriptionLevelConfiguration> getLevels() {
|
||||
return levels;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@ValidationMethod(message = "has a mismatch between the levels supported currencies")
|
||||
public boolean isCurrencyListSameAcrossAllLevels() {
|
||||
Optional<SubscriptionLevelConfiguration> any = levels.values().stream().findAny();
|
||||
if (any.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Set<String> currencies = any.get().getPrices().keySet();
|
||||
return levels.values().stream().allMatch(level -> currencies.equals(level.getPrices().keySet()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class SubscriptionLevelConfiguration {
|
||||
|
||||
private final String badge;
|
||||
private final String product;
|
||||
private final Map<String, SubscriptionPriceConfiguration> prices;
|
||||
|
||||
@JsonCreator
|
||||
public SubscriptionLevelConfiguration(
|
||||
@JsonProperty("badge") @NotEmpty String badge,
|
||||
@JsonProperty("product") @NotEmpty String product,
|
||||
@JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices) {
|
||||
this.badge = badge;
|
||||
this.product = product;
|
||||
this.prices = prices;
|
||||
}
|
||||
|
||||
public String getBadge() {
|
||||
return badge;
|
||||
}
|
||||
|
||||
public String getProduct() {
|
||||
return product;
|
||||
}
|
||||
|
||||
public Map<String, SubscriptionPriceConfiguration> getPrices() {
|
||||
return prices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.math.BigDecimal;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class SubscriptionPriceConfiguration {
|
||||
|
||||
private final String id;
|
||||
private final BigDecimal amount;
|
||||
|
||||
@JsonCreator
|
||||
public SubscriptionPriceConfiguration(
|
||||
@JsonProperty("id") @NotEmpty String id,
|
||||
@JsonProperty("amount") @NotNull @DecimalMin("0.01") BigDecimal amount) {
|
||||
this.id = id;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.databind.util.StdConverter;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
final class URLDeserializationConverter extends StdConverter<String, URL> {
|
||||
|
||||
@Override
|
||||
public URL convert(final String value) {
|
||||
try {
|
||||
return new URL(value);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.databind.util.StdConverter;
|
||||
import java.net.URL;
|
||||
|
||||
final class URLSerializationConverter extends StdConverter<URL, String> {
|
||||
|
||||
@Override
|
||||
public String convert(final URL value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,28 +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.Positive;
|
||||
|
||||
public class WavefrontConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private String uri;
|
||||
|
||||
@JsonProperty
|
||||
@Positive
|
||||
private int batchSize = 10_000;
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public int getBatchSize() {
|
||||
return batchSize;
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,6 @@ public class ZkConfig {
|
||||
@NotNull
|
||||
private byte[] serverPublic;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Boolean enabled;
|
||||
|
||||
public byte[] getServerSecret() {
|
||||
return serverSecret;
|
||||
}
|
||||
@@ -37,8 +33,4 @@ public class ZkConfig {
|
||||
public byte[] getServerPublic() {
|
||||
return serverPublic;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,15 @@ public class DynamicConfiguration {
|
||||
private DynamicSignupCaptchaConfiguration signupCaptcha = new DynamicSignupCaptchaConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
private DynamicAccountsDynamoDbMigrationConfiguration accountsDynamoDbMigration = new DynamicAccountsDynamoDbMigrationConfiguration();
|
||||
@Valid
|
||||
private DynamicRateLimitChallengeConfiguration rateLimitChallenge = new DynamicRateLimitChallengeConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
private DynamicDirectoryReconcilerConfiguration directoryReconciler = new DynamicDirectoryReconcilerConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicRateLimitChallengeConfiguration rateLimitChallenge = new DynamicRateLimitChallengeConfiguration();
|
||||
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
|
||||
|
||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||
final String experimentName) {
|
||||
@@ -94,11 +98,15 @@ public class DynamicConfiguration {
|
||||
return signupCaptcha;
|
||||
}
|
||||
|
||||
public DynamicAccountsDynamoDbMigrationConfiguration getAccountsDynamoDbMigrationConfiguration() {
|
||||
return accountsDynamoDbMigration;
|
||||
}
|
||||
|
||||
public DynamicRateLimitChallengeConfiguration getRateLimitChallengeConfiguration() {
|
||||
return rateLimitChallenge;
|
||||
}
|
||||
|
||||
public DynamicDirectoryReconcilerConfiguration getDirectoryReconcilerConfiguration() {
|
||||
return directoryReconciler;
|
||||
}
|
||||
|
||||
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
|
||||
return pushLatency;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class DynamicDirectoryReconcilerConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private boolean enabled = true;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -6,66 +6,13 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import java.util.Set;
|
||||
public class DynamicPaymentsConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private Set<String> allowedCountryCodes = Collections.emptySet();
|
||||
private Set<String> disallowedCountryCodes = Collections.emptySet();
|
||||
|
||||
public Set<String> getAllowedCountryCodes() {
|
||||
return allowedCountryCodes;
|
||||
public Set<String> getDisallowedCountryCodes() {
|
||||
return disallowedCountryCodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,16 @@ public class DynamicPreRegistrationExperimentEnrollmentConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Set<String> excludedCountryCodes = Collections.emptySet();
|
||||
private Set<String> excludedE164s = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Set<String> includedCountryCodes = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private Set<String> excludedCountryCodes = Collections.emptySet();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
@Min(0)
|
||||
@@ -36,14 +40,18 @@ public class DynamicPreRegistrationExperimentEnrollmentConfiguration {
|
||||
return enrolledE164s;
|
||||
}
|
||||
|
||||
public Set<String> getExcludedCountryCodes() {
|
||||
return excludedCountryCodes;
|
||||
public Set<String> getExcludedE164s() {
|
||||
return excludedE164s;
|
||||
}
|
||||
|
||||
public Set<String> getIncludedCountryCodes() {
|
||||
return includedCountryCodes;
|
||||
}
|
||||
|
||||
public Set<String> getExcludedCountryCodes() {
|
||||
return excludedCountryCodes;
|
||||
}
|
||||
|
||||
public int getEnrollmentPercentage() {
|
||||
return enrollmentPercentage;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class DynamicPushLatencyConfiguration {
|
||||
|
||||
private final Map<ClientPlatform, Set<Semver>> instrumentedVersions;
|
||||
|
||||
@JsonCreator
|
||||
public DynamicPushLatencyConfiguration(@JsonProperty("instrumentedVersions") final Map<ClientPlatform, Set<Semver>> instrumentedVersions) {
|
||||
this.instrumentedVersions = instrumentedVersions;
|
||||
}
|
||||
|
||||
public Map<ClientPlatform, Set<Semver>> getInstrumentedVersions() {
|
||||
return instrumentedVersions;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
@@ -22,9 +22,9 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
@@ -40,21 +40,23 @@ import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
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.DynamicConfiguration;
|
||||
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.ChangePhoneNumberRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceName;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||
@@ -68,19 +70,19 @@ 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.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.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
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.ImpossiblePhoneNumberException;
|
||||
import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
@@ -90,7 +92,6 @@ 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" ));
|
||||
@@ -118,12 +119,10 @@ public class AccountController {
|
||||
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 DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final TurnTokenGenerator turnTokenGenerator;
|
||||
private final Map<String, Integer> testDevices;
|
||||
private final RecaptchaClient recaptchaClient;
|
||||
private final RecaptchaClient recaptchaClient;
|
||||
private final GCMSender gcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
|
||||
@@ -136,9 +135,7 @@ public class AccountController {
|
||||
AbusiveHostRules abusiveHostRules,
|
||||
RateLimiters rateLimiters,
|
||||
SmsSender smsSenderFactory,
|
||||
DirectoryQueue directoryQueue,
|
||||
MessagesManager messagesManager,
|
||||
DynamicConfigurationManager dynamicConfigurationManager,
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
TurnTokenGenerator turnTokenGenerator,
|
||||
Map<String, Integer> testDevices,
|
||||
RecaptchaClient recaptchaClient,
|
||||
@@ -153,12 +150,10 @@ 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.recaptchaClient = recaptchaClient;
|
||||
this.gcmSender = gcmSender;
|
||||
this.apnSender = apnSender;
|
||||
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
|
||||
@@ -168,18 +163,18 @@ public class AccountController {
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/{type}/preauth/{token}/{number}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getPreAuth(@PathParam("type") String pushType,
|
||||
@PathParam("token") String pushToken,
|
||||
@PathParam("number") String number,
|
||||
@QueryParam("voip") Optional<Boolean> useVoip)
|
||||
{
|
||||
throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
||||
|
||||
if (!"apn".equals(pushType) && !"fcm".equals(pushType)) {
|
||||
return Response.status(400).build();
|
||||
}
|
||||
|
||||
if (!Util.isValidNumber(number)) {
|
||||
return Response.status(400).build();
|
||||
}
|
||||
Util.requireNormalizedNumber(number);
|
||||
|
||||
String pushChallenge = generatePushChallenge();
|
||||
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null,
|
||||
@@ -190,9 +185,9 @@ public class AccountController {
|
||||
pendingAccounts.store(number, storedVerificationCode);
|
||||
|
||||
if ("fcm".equals(pushType)) {
|
||||
gcmSender.sendMessage(new GcmMessage(pushToken, number, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
||||
gcmSender.sendMessage(new GcmMessage(pushToken, null, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
||||
} else if ("apn".equals(pushType)) {
|
||||
apnSender.sendMessage(new ApnMessage(pushToken, number, 0, useVoip.orElse(true), ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
||||
apnSender.sendMessage(new ApnMessage(pushToken, null, 0, useVoip.orElse(true), ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -203,6 +198,7 @@ public class AccountController {
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/{transport}/code/{number}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response createAccount(@PathParam("transport") String transport,
|
||||
@PathParam("number") String number,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor,
|
||||
@@ -211,28 +207,21 @@ public class AccountController {
|
||||
@QueryParam("client") Optional<String> client,
|
||||
@QueryParam("captcha") Optional<String> captcha,
|
||||
@QueryParam("challenge") Optional<String> pushChallenge)
|
||||
throws RateLimitExceededException, RetryLaterException
|
||||
{
|
||||
if (!Util.isValidNumber(number)) {
|
||||
logger.info("Invalid number: " + number);
|
||||
throw new WebApplicationException(Response.status(400).build());
|
||||
}
|
||||
throws RateLimitExceededException, RetryLaterException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
||||
|
||||
if (number.startsWith("+98")) {
|
||||
transport = "voice";
|
||||
}
|
||||
Util.requireNormalizedNumber(number);
|
||||
|
||||
String requester = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
|
||||
String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
|
||||
|
||||
Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number);
|
||||
CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, requester, captcha, storedChallenge, pushChallenge);
|
||||
CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, sourceHost, captcha, storedChallenge, pushChallenge);
|
||||
|
||||
if (requirement.isCaptchaRequired()) {
|
||||
captchaRequiredMeter.mark();
|
||||
|
||||
if (requirement.isAutoBlock() && shouldAutoBlock(requester)) {
|
||||
logger.info("Auto-block: " + requester);
|
||||
abusiveHostRules.setBlockedHost(requester, "Auto-Block");
|
||||
if (requirement.isAutoBlock() && shouldAutoBlock(sourceHost)) {
|
||||
logger.info("Auto-block: {}", sourceHost);
|
||||
abusiveHostRules.setBlockedHost(sourceHost, "Auto-Block");
|
||||
}
|
||||
|
||||
return Response.status(402).build();
|
||||
@@ -318,6 +307,7 @@ public class AccountController {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO Remove this meter when external dependencies have been resolved
|
||||
metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark();
|
||||
|
||||
{
|
||||
@@ -339,88 +329,106 @@ public class AccountController {
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/code/{verification_code}")
|
||||
public AccountCreationResult verifyAccount(@PathParam("verification_code") String verificationCode,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@HeaderParam("X-Signal-Agent") String signalAgent,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@QueryParam("transfer") Optional<Boolean> availableForTransfer,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
try {
|
||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||
String number = header.getIdentifier().getNumber();
|
||||
String password = header.getPassword();
|
||||
@HeaderParam("Authorization") BasicAuthorizationHeader authorizationHeader,
|
||||
@HeaderParam("X-Signal-Agent") String signalAgent,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@QueryParam("transfer") Optional<Boolean> availableForTransfer,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException, InterruptedException {
|
||||
|
||||
if (number == null) {
|
||||
throw new WebApplicationException(400);
|
||||
}
|
||||
String number = authorizationHeader.getUsername();
|
||||
String password = authorizationHeader.getPassword();
|
||||
|
||||
rateLimiters.getVerifyLimiter().validate(number);
|
||||
rateLimiters.getVerifyLimiter().validate(number);
|
||||
|
||||
Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
|
||||
// Note that successful verification depends on being able to find a stored verification code for the given number.
|
||||
// We check that numbers are normalized before we store verification codes, and so don't need to re-assert
|
||||
// normalization here.
|
||||
Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
|
||||
|
||||
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(verificationCode)) {
|
||||
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)
|
||||
.map(uuid -> backupServiceCredentialGenerator.generateFor(uuid.toString()));
|
||||
|
||||
if (existingRegistrationLock.isPresent() && existingRegistrationLock.get().requiresClientRegistrationLock()) {
|
||||
rateLimiters.getVerifyLimiter().clear(number);
|
||||
|
||||
if (!Util.isEmpty(accountAttributes.getRegistrationLock()) || !Util.isEmpty(accountAttributes.getPin())) {
|
||||
rateLimiters.getPinLimiter().validate(number);
|
||||
}
|
||||
|
||||
if (!existingRegistrationLock.get().verify(accountAttributes.getRegistrationLock(), accountAttributes.getPin())) {
|
||||
throw new WebApplicationException(Response.status(423)
|
||||
.entity(new RegistrationLockFailure(existingRegistrationLock.get().getTimeRemaining(),
|
||||
existingRegistrationLock.get().needsFailureCredentials() ? existingBackupCredentials.orElseThrow() : null))
|
||||
.build());
|
||||
}
|
||||
|
||||
rateLimiters.getPinLimiter().clear(number);
|
||||
}
|
||||
|
||||
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
|
||||
throw new WebApplicationException(Response.status(409).build());
|
||||
}
|
||||
|
||||
Account account = createAccount(number, password, signalAgent, accountAttributes);
|
||||
|
||||
{
|
||||
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) {
|
||||
logger.info("Bad Authorization Header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(verificationCode)) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
Optional<Account> existingAccount = accounts.getByE164(number);
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
|
||||
}
|
||||
|
||||
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
|
||||
throw new WebApplicationException(Response.status(409).build());
|
||||
}
|
||||
|
||||
rateLimiters.getVerifyLimiter().clear(number);
|
||||
|
||||
Account account = accounts.create(number, password, signalAgent, accountAttributes,
|
||||
existingAccount.map(Account::getBadges).orElseGet(ArrayList::new));
|
||||
|
||||
{
|
||||
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(), account.getNumber(), existingAccount.map(Account::isStorageSupported).orElse(false));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/number")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void changeNumber(@Auth final AuthenticatedAccount authenticatedAccount, @Valid final ChangePhoneNumberRequest request)
|
||||
throws RateLimitExceededException, InterruptedException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
||||
|
||||
if (request.getNumber().equals(authenticatedAccount.getAccount().getNumber())) {
|
||||
// This may be a request that got repeated due to poor network conditions or other client error; take no action,
|
||||
// but report success since the account is in the desired state
|
||||
return;
|
||||
}
|
||||
|
||||
Util.requireNormalizedNumber(request.getNumber());
|
||||
|
||||
rateLimiters.getVerifyLimiter().validate(request.getNumber());
|
||||
|
||||
final Optional<StoredVerificationCode> storedVerificationCode =
|
||||
pendingAccounts.getCodeForNumber(request.getNumber());
|
||||
|
||||
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(request.getCode())) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
final Optional<Account> existingAccount = accounts.getByE164(request.getNumber());
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock());
|
||||
}
|
||||
|
||||
rateLimiters.getVerifyLimiter().clear(request.getNumber());
|
||||
|
||||
accounts.changeNumber(authenticatedAccount.getAccount(), request.getNumber());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/turn/")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public TurnToken getTurnToken(@Auth Account account) throws RateLimitExceededException {
|
||||
rateLimiters.getTurnLimiter().validate(account.getNumber());
|
||||
public TurnToken getTurnToken(@Auth AuthenticatedAccount auth) throws RateLimitExceededException {
|
||||
rateLimiters.getTurnLimiter().validate(auth.getAccount().getUuid());
|
||||
return turnTokenGenerator.generate();
|
||||
}
|
||||
|
||||
@@ -428,200 +436,178 @@ public class AccountController {
|
||||
@PUT
|
||||
@Path("/gcm/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid GcmRegistrationId registrationId) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
boolean wasAccountEnabled = account.isEnabled();
|
||||
@ChangesDeviceEnabledState
|
||||
public void setGcmRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||
@Valid GcmRegistrationId registrationId) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
|
||||
if (device.getGcmId() != null &&
|
||||
device.getGcmId().equals(registrationId.getGcmRegistrationId()))
|
||||
{
|
||||
device.getGcmId().equals(registrationId.getGcmRegistrationId())) {
|
||||
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
|
||||
@DELETE
|
||||
@Path("/gcm/")
|
||||
public void deleteGcmRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setGcmId(null);
|
||||
device.setFetchesMessages(false);
|
||||
device.setUserAgent("OWA");
|
||||
@ChangesDeviceEnabledState
|
||||
public void deleteGcmRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
|
||||
accounts.update(account);
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
accounts.updateDevice(account, device.getId(), d -> {
|
||||
d.setGcmId(null);
|
||||
d.setFetchesMessages(false);
|
||||
d.setUserAgent("OWA");
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/apn/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid ApnRegistrationId registrationId) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
boolean wasAccountEnabled = account.isEnabled();
|
||||
@ChangesDeviceEnabledState
|
||||
public void setApnRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||
@Valid ApnRegistrationId registrationId) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
|
||||
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
|
||||
@DELETE
|
||||
@Path("/apn/")
|
||||
public void deleteApnRegistrationId(@Auth DisabledPermittedAccount disabledPermittedAccount) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
device.setApnId(null);
|
||||
device.setFetchesMessages(false);
|
||||
if (device.getId() == 1) {
|
||||
device.setUserAgent("OWI");
|
||||
} else {
|
||||
device.setUserAgent("OWP");
|
||||
}
|
||||
@ChangesDeviceEnabledState
|
||||
public void deleteApnRegistrationId(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
|
||||
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
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/registration_lock")
|
||||
public void setRegistrationLock(@Auth Account account, @Valid RegistrationLock accountLock) {
|
||||
public void setRegistrationLock(@Auth AuthenticatedAccount auth, @Valid RegistrationLock accountLock) {
|
||||
AuthenticationCredentials credentials = new AuthenticationCredentials(accountLock.getRegistrationLock());
|
||||
account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt());
|
||||
account.setPin(null);
|
||||
|
||||
accounts.update(account);
|
||||
accounts.update(auth.getAccount(),
|
||||
a -> a.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt()));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/registration_lock")
|
||||
public void removeRegistrationLock(@Auth Account account) {
|
||||
account.setRegistrationLock(null, null);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/pin/")
|
||||
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);
|
||||
|
||||
account.setPin(accountLock.getPin());
|
||||
account.setRegistrationLock(null, null);
|
||||
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/pin/")
|
||||
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);
|
||||
|
||||
account.setPin(null);
|
||||
accounts.update(account);
|
||||
public void removeRegistrationLock(@Auth AuthenticatedAccount auth) {
|
||||
accounts.update(auth.getAccount(), a -> a.setRegistrationLock(null, null));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/name/")
|
||||
public void setName(@Auth DisabledPermittedAccount disabledPermittedAccount, @Valid DeviceName deviceName) {
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
account.getAuthenticatedDevice().get().setName(deviceName.getDeviceName());
|
||||
accounts.update(account);
|
||||
public void setName(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @Valid DeviceName deviceName) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
accounts.updateDevice(account, device.getId(), d -> d.setName(deviceName.getDeviceName()));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/signaling_key")
|
||||
public void removeSignalingKey(@Auth DisabledPermittedAccount disabledPermittedAccount) {
|
||||
public void removeSignalingKey(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth) {
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/attributes/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setAccountAttributes(@Auth DisabledPermittedAccount disabledPermittedAccount,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@Valid AccountAttributes attributes)
|
||||
{
|
||||
Account account = disabledPermittedAccount.getAccount();
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
@ChangesDeviceEnabledState
|
||||
public void setAccountAttributes(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@Valid AccountAttributes attributes) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
long deviceId = disabledPermittedAuth.getAuthenticatedDevice().getId();
|
||||
|
||||
device.setFetchesMessages(attributes.getFetchesMessages());
|
||||
device.setName(attributes.getName());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setCapabilities(attributes.getCapabilities());
|
||||
device.setRegistrationId(attributes.getRegistrationId());
|
||||
device.setUserAgent(userAgent);
|
||||
// temporary: For deterministic updates during the DynamoDB migration, use a fully parameterized registration lock
|
||||
@Nullable final AuthenticationCredentials registrationLockCredentials =
|
||||
Util.isEmpty(attributes.getRegistrationLock()) ? null
|
||||
: new AuthenticationCredentials(attributes.getRegistrationLock());
|
||||
|
||||
setAccountRegistrationLockFromAttributes(account, attributes);
|
||||
accounts.update(account, a -> {
|
||||
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());
|
||||
// temporary: for deterministic updates during the DynamoDB migration, use a fully parameterized registration lock
|
||||
// a.setRegistrationLockFromAttributes(attributes);
|
||||
if (registrationLockCredentials != null) {
|
||||
a.setRegistrationLock(registrationLockCredentials.getHashedAuthenticationToken(),
|
||||
registrationLockCredentials.getSalt());
|
||||
} else {
|
||||
a.setRegistrationLock(null, null);
|
||||
}
|
||||
|
||||
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
|
||||
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
|
||||
@Path("/me")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCreationResult getMe(@Auth Account account) {
|
||||
return whoAmI(account);
|
||||
public AccountCreationResult getMe(@Auth AuthenticatedAccount auth) {
|
||||
return whoAmI(auth);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/whoami")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCreationResult whoAmI(@Auth Account account) {
|
||||
return new AccountCreationResult(account.getUuid(), account.isStorageSupported());
|
||||
public AccountCreationResult whoAmI(@Auth AuthenticatedAccount auth) {
|
||||
return new AccountCreationResult(auth.getAccount().getUuid(), auth.getAccount().getNumber(), auth.getAccount().isStorageSupported());
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/username")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void deleteUsername(@Auth Account account) {
|
||||
usernames.delete(account.getUuid());
|
||||
public void deleteUsername(@Auth AuthenticatedAccount auth) {
|
||||
usernames.delete(auth.getAccount().getUuid());
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/username/{username}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response setUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameSetLimiter().validate(account.getUuid().toString());
|
||||
public Response setUsername(@Auth AuthenticatedAccount auth, @PathParam("username") String username)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameSetLimiter().validate(auth.getAccount().getUuid());
|
||||
|
||||
if (username == null || username.isEmpty()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
@@ -633,22 +619,45 @@ public class AccountController {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
if (!usernames.put(account.getUuid(), username)) {
|
||||
if (!usernames.put(auth.getAccount().getUuid(), username)) {
|
||||
return Response.status(Response.Status.CONFLICT).build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
private void verifyRegistrationLock(final Account existingAccount, @Nullable final String clientRegistrationLock)
|
||||
throws RateLimitExceededException, WebApplicationException {
|
||||
|
||||
final StoredRegistrationLock existingRegistrationLock = existingAccount.getRegistrationLock();
|
||||
final ExternalServiceCredentials existingBackupCredentials =
|
||||
backupServiceCredentialGenerator.generateFor(existingAccount.getUuid().toString());
|
||||
|
||||
if (existingRegistrationLock.requiresClientRegistrationLock()) {
|
||||
if (!Util.isEmpty(clientRegistrationLock)) {
|
||||
rateLimiters.getPinLimiter().validate(existingAccount.getNumber());
|
||||
}
|
||||
|
||||
if (!existingRegistrationLock.verify(clientRegistrationLock)) {
|
||||
throw new WebApplicationException(Response.status(423)
|
||||
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining(),
|
||||
existingRegistrationLock.needsFailureCredentials() ? existingBackupCredentials : null))
|
||||
.build());
|
||||
}
|
||||
|
||||
rateLimiters.getPinLimiter().clear(existingAccount.getNumber());
|
||||
}
|
||||
}
|
||||
|
||||
private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor,
|
||||
String requester,
|
||||
String sourceHost,
|
||||
Optional<String> captchaToken,
|
||||
Optional<StoredVerificationCode> storedVerificationCode,
|
||||
Optional<String> pushChallenge)
|
||||
{
|
||||
|
||||
if (captchaToken.isPresent()) {
|
||||
boolean validToken = recaptchaClient.verify(captchaToken.get(), requester);
|
||||
boolean validToken = recaptchaClient.verify(captchaToken.get(), sourceHost);
|
||||
|
||||
if (validToken) {
|
||||
captchaSuccessMeter.mark();
|
||||
@@ -686,18 +695,18 @@ public class AccountController {
|
||||
}
|
||||
}
|
||||
|
||||
List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(requester);
|
||||
List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(sourceHost);
|
||||
|
||||
for (AbusiveHostRule abuseRule : abuseRules) {
|
||||
if (abuseRule.isBlocked()) {
|
||||
logger.info("Blocked host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
logger.info("Blocked host: {}, {}, {} ({})", transport, number, sourceHost, 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 + ")");
|
||||
logger.info("Restricted host: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
|
||||
filteredHostMeter.mark();
|
||||
return new CaptchaRequirement(true, false);
|
||||
}
|
||||
@@ -705,9 +714,9 @@ public class AccountController {
|
||||
}
|
||||
|
||||
try {
|
||||
rateLimiters.getSmsVoiceIpLimiter().validate(requester);
|
||||
rateLimiters.getSmsVoiceIpLimiter().validate(sourceHost);
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.info("Rate limited exceeded: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")");
|
||||
logger.info("Rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
|
||||
rateLimitedHostMeter.mark();
|
||||
return new CaptchaRequirement(true, true);
|
||||
}
|
||||
@@ -715,7 +724,7 @@ public class AccountController {
|
||||
try {
|
||||
rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number));
|
||||
} catch (RateLimitExceededException e) {
|
||||
logger.info("Prefix rate limit exceeded: " + transport + ", " + number + ", (" + forwardedFor + ")");
|
||||
logger.info("Prefix rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
|
||||
rateLimitedPrefixMeter.mark();
|
||||
return new CaptchaRequirement(true, true);
|
||||
}
|
||||
@@ -731,13 +740,13 @@ public class AccountController {
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/me")
|
||||
public void deleteAccount(@Auth Account account) {
|
||||
accounts.delete(account, AccountsManager.DeletionReason.USER_REQUEST);
|
||||
public void deleteAccount(@Auth AuthenticatedAccount auth) throws InterruptedException {
|
||||
accounts.delete(auth.getAccount(), AccountsManager.DeletionReason.USER_REQUEST);
|
||||
}
|
||||
|
||||
private boolean shouldAutoBlock(String requester) {
|
||||
private boolean shouldAutoBlock(String sourceHost) {
|
||||
try {
|
||||
rateLimiters.getAutoBlockLimiter().validate(requester);
|
||||
rateLimiters.getAutoBlockLimiter().validate(sourceHost);
|
||||
} catch (RateLimitExceededException e) {
|
||||
return true;
|
||||
}
|
||||
@@ -745,52 +754,6 @@ public class AccountController {
|
||||
return false;
|
||||
}
|
||||
|
||||
private Account createAccount(String number, String password, String signalAgent, AccountAttributes accountAttributes) {
|
||||
Optional<Account> maybeExistingAccount = accounts.get(number);
|
||||
|
||||
Device device = new Device();
|
||||
device.setId(Device.MASTER_ID);
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
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(signalAgent);
|
||||
|
||||
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.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
|
||||
|
||||
if (accounts.create(account)) {
|
||||
newUserMeter.mark();
|
||||
}
|
||||
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
maybeExistingAccount.ifPresent(definitelyExistingAccount -> messagesManager.clear(definitelyExistingAccount.getUuid()));
|
||||
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)) {
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.amazonaws.HttpMethod;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV1;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.stream.Stream;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV1;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.UrlSigner;
|
||||
|
||||
|
||||
@Path("/v1/attachments")
|
||||
@@ -35,25 +33,25 @@ public class AttachmentControllerV1 extends AttachmentControllerBase {
|
||||
private static final String[] UNACCELERATED_REGIONS = {"+20", "+971", "+968", "+974"};
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final UrlSigner urlSigner;
|
||||
private final UrlSigner urlSigner;
|
||||
|
||||
public AttachmentControllerV1(RateLimiters rateLimiters, String accessKey, String accessSecret, String bucket) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.urlSigner = new UrlSigner(accessKey, accessSecret, bucket);
|
||||
this.urlSigner = new UrlSigner(accessKey, accessSecret, bucket);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AttachmentDescriptorV1 allocateAttachment(@Auth Account account)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (account.isRateLimited()) {
|
||||
rateLimiters.getAttachmentLimiter().validate(account.getNumber());
|
||||
public AttachmentDescriptorV1 allocateAttachment(@Auth AuthenticatedAccount auth)
|
||||
throws RateLimitExceededException {
|
||||
if (auth.getAccount().isRateLimited()) {
|
||||
rateLimiters.getAttachmentLimiter().validate(auth.getAccount().getUuid());
|
||||
}
|
||||
|
||||
long attachmentId = generateAttachmentId();
|
||||
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region)));
|
||||
URL url = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT,
|
||||
Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> auth.getAccount().getNumber().startsWith(region)));
|
||||
|
||||
return new AttachmentDescriptorV1(attachmentId, url.toExternalForm());
|
||||
|
||||
@@ -63,11 +61,11 @@ public class AttachmentControllerV1 extends AttachmentControllerBase {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{attachmentId}")
|
||||
public AttachmentUri redirectToAttachment(@Auth Account account,
|
||||
@PathParam("attachmentId") long attachmentId)
|
||||
throws IOException
|
||||
{
|
||||
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region))));
|
||||
public AttachmentUri redirectToAttachment(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("attachmentId") long attachmentId)
|
||||
throws IOException {
|
||||
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET,
|
||||
Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> auth.getAccount().getNumber().startsWith(region))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-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.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v2/attachments")
|
||||
public class AttachmentControllerV2 extends AttachmentControllerBase {
|
||||
|
||||
@@ -40,19 +38,20 @@ public class AttachmentControllerV2 extends AttachmentControllerBase {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/upload")
|
||||
public AttachmentDescriptorV2 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException {
|
||||
rateLimiter.validate(account.getNumber());
|
||||
public AttachmentDescriptorV2 getAttachmentUploadForm(@Auth AuthenticatedAccount auth)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiter.validate(auth.getAccount().getUuid());
|
||||
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
long attachmentId = generateAttachmentId();
|
||||
String objectName = String.valueOf(attachmentId);
|
||||
Pair<String, String> policy = policyGenerator.createFor(now, String.valueOf(objectName), 100 * 1024 * 1024);
|
||||
String signature = policySigner.getSignature(now, policy.second());
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
long attachmentId = generateAttachmentId();
|
||||
String objectName = String.valueOf(attachmentId);
|
||||
Pair<String, String> policy = policyGenerator.createFor(now, String.valueOf(objectName), 100 * 1024 * 1024);
|
||||
String signature = policySigner.getSignature(now, policy.second());
|
||||
|
||||
return new AttachmentDescriptorV2(attachmentId, objectName, policy.first(),
|
||||
"private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME),
|
||||
policy.second(), signature);
|
||||
"private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME),
|
||||
policy.second(), signature);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -7,19 +7,6 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.SecureRandom;
|
||||
@@ -29,6 +16,18 @@ import java.time.ZonedDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV3;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
|
||||
@Path("/v3/attachments")
|
||||
public class AttachmentControllerV3 extends AttachmentControllerBase {
|
||||
@@ -45,26 +44,29 @@ public class AttachmentControllerV3 extends AttachmentControllerBase {
|
||||
@Nonnull
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull String domain, @Nonnull String email, int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
|
||||
public AttachmentControllerV3(@Nonnull RateLimiters rateLimiters, @Nonnull String domain, @Nonnull String email,
|
||||
int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
|
||||
throws IOException, InvalidKeyException, InvalidKeySpecException {
|
||||
this.rateLimiter = rateLimiters.getAttachmentLimiter();
|
||||
this.rateLimiter = rateLimiters.getAttachmentLimiter();
|
||||
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
|
||||
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
|
||||
this.secureRandom = new SecureRandom();
|
||||
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
|
||||
this.secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/upload")
|
||||
public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException {
|
||||
rateLimiter.validate(account.getNumber());
|
||||
public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth AuthenticatedAccount auth)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiter.validate(auth.getAccount().getUuid());
|
||||
|
||||
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
final String key = generateAttachmentKey();
|
||||
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
final String key = generateAttachmentKey();
|
||||
final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now);
|
||||
|
||||
return new AttachmentDescriptorV3(2, key, getHeaderMap(canonicalRequest), getSignedUploadLocation(canonicalRequest));
|
||||
return new AttachmentDescriptorV3(2, key, getHeaderMap(canonicalRequest),
|
||||
getSignedUploadLocation(canonicalRequest));
|
||||
}
|
||||
|
||||
private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
@@ -74,11 +76,10 @@ public class AttachmentControllerV3 extends AttachmentControllerBase {
|
||||
}
|
||||
|
||||
private static Map<String, String> getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
Map<String, String> result = new HashMap<>(3);
|
||||
result.put("host", canonicalRequest.getDomain());
|
||||
result.put("x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes());
|
||||
result.put("x-goog-resumable", "start");
|
||||
return result;
|
||||
return Map.of(
|
||||
"host", canonicalRequest.getDomain(),
|
||||
"x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(),
|
||||
"x-goog-resumable", "start");
|
||||
}
|
||||
|
||||
private String generateAttachmentKey() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,6 @@ import static com.codahale.metrics.MetricRegistry.name;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -24,10 +23,10 @@ import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.signal.zkgroup.auth.ServerZkAuthOperations;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
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")
|
||||
@@ -36,58 +35,59 @@ public class CertificateController {
|
||||
|
||||
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) {
|
||||
public CertificateController(CertificateGenerator certificateGenerator, ServerZkAuthOperations serverZkAuthOperations) {
|
||||
this.certificateGenerator = certificateGenerator;
|
||||
this.serverZkAuthOperations = serverZkAuthOperations;
|
||||
this.isZkEnabled = isZkEnabled;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/delivery")
|
||||
public DeliveryCertificate getDeliveryCertificate(@Auth Account account,
|
||||
@QueryParam("includeE164") Optional<Boolean> maybeIncludeE164)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (account.getAuthenticatedDevice().isEmpty()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
if (Util.isEmpty(account.getIdentityKey())) {
|
||||
public DeliveryCertificate getDeliveryCertificate(@Auth AuthenticatedAccount auth,
|
||||
@QueryParam("includeE164") Optional<Boolean> maybeIncludeE164)
|
||||
throws InvalidKeyException {
|
||||
if (Util.isEmpty(auth.getAccount().getIdentityKey())) {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
final boolean includeE164 = maybeIncludeE164.orElse(true);
|
||||
|
||||
Metrics.counter(GENERATE_DELIVERY_CERTIFICATE_COUNTER_NAME, INCLUDE_E164_TAG_NAME, String.valueOf(includeE164)).increment();
|
||||
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));
|
||||
return new DeliveryCertificate(
|
||||
certificateGenerator.createFor(auth.getAccount(), auth.getAuthenticatedDevice(), includeE164));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/group/{startRedemptionTime}/{endRedemptionTime}")
|
||||
public GroupCredentials getAuthenticationCredentials(@Auth Account account,
|
||||
@PathParam("startRedemptionTime") int startRedemptionTime,
|
||||
@PathParam("endRedemptionTime") int endRedemptionTime)
|
||||
{
|
||||
if (!isZkEnabled) throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
if (startRedemptionTime > endRedemptionTime) throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
if (endRedemptionTime > Util.currentDaysSinceEpoch() + 7) throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
if (startRedemptionTime < Util.currentDaysSinceEpoch()) throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
public GroupCredentials getAuthenticationCredentials(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("startRedemptionTime") int startRedemptionTime,
|
||||
@PathParam("endRedemptionTime") int endRedemptionTime) {
|
||||
if (startRedemptionTime > endRedemptionTime) {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
if (endRedemptionTime > Util.currentDaysSinceEpoch() + 7) {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
if (startRedemptionTime < Util.currentDaysSinceEpoch()) {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
List<GroupCredentials.GroupCredential> credentials = new LinkedList<>();
|
||||
|
||||
for (int i=startRedemptionTime;i<=endRedemptionTime;i++) {
|
||||
credentials.add(new GroupCredentials.GroupCredential(serverZkAuthOperations.issueAuthCredential(account.getUuid(), i)
|
||||
.serialize(),
|
||||
i));
|
||||
for (int i = startRedemptionTime; i <= endRedemptionTime; i++) {
|
||||
credentials.add(new GroupCredentials.GroupCredential(
|
||||
serverZkAuthOperations.issueAuthCredential(auth.getAccount().getUuid(), i)
|
||||
.serialize(),
|
||||
i));
|
||||
}
|
||||
|
||||
return new GroupCredentials(credentials);
|
||||
|
||||
@@ -17,12 +17,12 @@ 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.auth.AuthenticatedAccount;
|
||||
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")
|
||||
@@ -38,7 +38,7 @@ public class ChallengeController {
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response handleChallengeResponse(@Auth final Account account,
|
||||
public Response handleChallengeResponse(@Auth final AuthenticatedAccount auth,
|
||||
@Valid final AnswerChallengeRequest answerRequest,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor) throws RetryLaterException {
|
||||
|
||||
@@ -46,14 +46,15 @@ public class ChallengeController {
|
||||
if (answerRequest instanceof AnswerPushChallengeRequest) {
|
||||
final AnswerPushChallengeRequest pushChallengeRequest = (AnswerPushChallengeRequest) answerRequest;
|
||||
|
||||
rateLimitChallengeManager.answerPushChallenge(account, pushChallengeRequest.getChallenge());
|
||||
rateLimitChallengeManager.answerPushChallenge(auth.getAccount(), 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);
|
||||
rateLimitChallengeManager.answerRecaptchaChallenge(auth.getAccount(), recaptchaChallengeRequest.getCaptcha(),
|
||||
mostRecentProxy);
|
||||
|
||||
} catch (final NoSuchElementException e) {
|
||||
return Response.status(400).build();
|
||||
@@ -69,9 +70,9 @@ public class ChallengeController {
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/push")
|
||||
public Response requestPushChallenge(@Auth final Account account) {
|
||||
public Response requestPushChallenge(@Auth final AuthenticatedAccount auth) {
|
||||
try {
|
||||
rateLimitChallengeManager.sendPushChallenge(account);
|
||||
rateLimitChallengeManager.sendPushChallenge(auth.getAccount());
|
||||
return Response.status(200).build();
|
||||
} catch (final NotPushRegisteredException e) {
|
||||
return Response.status(404).build();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
@@ -7,29 +7,11 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
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.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.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.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
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;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
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.DELETE;
|
||||
@@ -40,39 +22,56 @@ 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.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthEnablementRefreshRequirementProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
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;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
@Path("/v1/devices")
|
||||
public class DeviceController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
|
||||
|
||||
private static final int MAX_DEVICES = 6;
|
||||
|
||||
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(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;
|
||||
}
|
||||
@@ -80,12 +79,12 @@ public class DeviceController {
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public DeviceInfoList getDevices(@Auth Account account) {
|
||||
public DeviceInfoList getDevices(@Auth AuthenticatedAccount auth) {
|
||||
List<DeviceInfo> devices = new LinkedList<>();
|
||||
|
||||
for (Device device : account.getDevices()) {
|
||||
for (Device device : auth.getAccount().getDevices()) {
|
||||
devices.add(new DeviceInfo(device.getId(), device.getName(),
|
||||
device.getLastSeen(), device.getCreated()));
|
||||
device.getLastSeen(), device.getCreated()));
|
||||
}
|
||||
|
||||
return new DeviceInfoList(devices);
|
||||
@@ -94,15 +93,16 @@ public class DeviceController {
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/{device_id}")
|
||||
public void removeDevice(@Auth Account account, @PathParam("device_id") long deviceId) {
|
||||
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
|
||||
@ChangesDeviceEnabledState
|
||||
public void removeDevice(@Auth AuthenticatedAccount auth, @PathParam("device_id") long deviceId) {
|
||||
Account account = auth.getAccount();
|
||||
if (auth.getAuthenticatedDevice().getId() != Device.MASTER_ID) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
messages.clear(account.getUuid(), deviceId);
|
||||
account.removeDevice(deviceId);
|
||||
accounts.update(account);
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
account = accounts.update(account, a -> a.removeDevice(deviceId));
|
||||
keys.delete(account.getUuid(), deviceId);
|
||||
// ensure any messages that came in after the first clear() are also removed
|
||||
messages.clear(account.getUuid(), deviceId);
|
||||
}
|
||||
@@ -111,10 +111,12 @@ public class DeviceController {
|
||||
@GET
|
||||
@Path("/provisioning/code")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public VerificationCode createDeviceToken(@Auth Account account)
|
||||
throws RateLimitExceededException, DeviceLimitExceededException
|
||||
{
|
||||
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
|
||||
public VerificationCode createDeviceToken(@Auth AuthenticatedAccount auth)
|
||||
throws RateLimitExceededException, DeviceLimitExceededException {
|
||||
|
||||
final Account account = auth.getAccount();
|
||||
|
||||
rateLimiters.getAllocateDeviceLimiter().validate(account.getUuid());
|
||||
|
||||
int maxDeviceLimit = MAX_DEVICES;
|
||||
|
||||
@@ -126,7 +128,7 @@ public class DeviceController {
|
||||
throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES);
|
||||
}
|
||||
|
||||
if (account.getAuthenticatedDevice().get().getId() != Device.MASTER_ID) {
|
||||
if (auth.getAuthenticatedDevice().getId() != Device.MASTER_ID) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -146,86 +148,87 @@ public class DeviceController {
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Path("/{verification_code}")
|
||||
@ChangesDeviceEnabledState
|
||||
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
@HeaderParam("Authorization") BasicAuthorizationHeader authorizationHeader,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@Valid AccountAttributes accountAttributes,
|
||||
@Context ContainerRequest containerRequest)
|
||||
throws RateLimitExceededException, DeviceLimitExceededException
|
||||
{
|
||||
try {
|
||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||
String number = header.getIdentifier().getNumber();
|
||||
String password = header.getPassword();
|
||||
|
||||
if (number == null) throw new WebApplicationException(400);
|
||||
String number = authorizationHeader.getUsername();
|
||||
String password = authorizationHeader.getPassword();
|
||||
|
||||
rateLimiters.getVerifyDeviceLimiter().validate(number);
|
||||
rateLimiters.getVerifyDeviceLimiter().validate(number);
|
||||
|
||||
Optional<StoredVerificationCode> storedVerificationCode = pendingDevices.getCodeForNumber(number);
|
||||
Optional<StoredVerificationCode> storedVerificationCode = pendingDevices.getCodeForNumber(number);
|
||||
|
||||
if (!storedVerificationCode.isPresent() || !storedVerificationCode.get().isValid(verificationCode)) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Optional<Account> account = accounts.get(number);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
int maxDeviceLimit = MAX_DEVICES;
|
||||
|
||||
if (maxDeviceConfiguration.containsKey(account.get().getNumber())) {
|
||||
maxDeviceLimit = maxDeviceConfiguration.get(account.get().getNumber());
|
||||
}
|
||||
|
||||
if (account.get().getEnabledDeviceCount() >= maxDeviceLimit) {
|
||||
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
|
||||
}
|
||||
|
||||
final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
|
||||
if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities, userAgent)) {
|
||||
throw new WebApplicationException(Response.status(409).build());
|
||||
}
|
||||
|
||||
Device device = new Device();
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
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().getUuid(), device.getId());
|
||||
accounts.update(account.get());
|
||||
|
||||
pendingDevices.remove(number);
|
||||
|
||||
return new DeviceResponse(device.getId());
|
||||
} catch (InvalidAuthorizationHeaderException e) {
|
||||
logger.info("Bad Authorization Header", e);
|
||||
throw new WebApplicationException(Response.status(401).build());
|
||||
if (!storedVerificationCode.isPresent() || !storedVerificationCode.get().isValid(verificationCode)) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Optional<Account> account = accounts.getByE164(number);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
// Normally, the the "do we need to refresh somebody's websockets" listener can do this on its own. In this case,
|
||||
// we're not using the conventional authentication system, and so we need to give it a hint so it knows who the
|
||||
// active user is and what their device states look like.
|
||||
AuthEnablementRefreshRequirementProvider.setAccount(containerRequest, account.get());
|
||||
|
||||
int maxDeviceLimit = MAX_DEVICES;
|
||||
|
||||
if (maxDeviceConfiguration.containsKey(account.get().getNumber())) {
|
||||
maxDeviceLimit = maxDeviceConfiguration.get(account.get().getNumber());
|
||||
}
|
||||
|
||||
if (account.get().getEnabledDeviceCount() >= maxDeviceLimit) {
|
||||
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
|
||||
}
|
||||
|
||||
final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
|
||||
if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities, userAgent)) {
|
||||
throw new WebApplicationException(Response.status(409).build());
|
||||
}
|
||||
|
||||
Device device = new Device();
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
device.setCapabilities(accountAttributes.getCapabilities());
|
||||
|
||||
accounts.update(account.get(), a -> {
|
||||
device.setId(a.getNextDeviceId());
|
||||
messages.clear(a.getUuid(), device.getId());
|
||||
a.addDevice(device);
|
||||
});
|
||||
|
||||
pendingDevices.remove(number);
|
||||
|
||||
return new DeviceResponse(device.getId());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/unauthenticated_delivery")
|
||||
public void setUnauthenticatedDelivery(@Auth Account account) {
|
||||
assert(account.getAuthenticatedDevice().isPresent());
|
||||
public void setUnauthenticatedDelivery(@Auth AuthenticatedAccount auth) {
|
||||
assert (auth.getAuthenticatedDevice() != null);
|
||||
// Deprecated
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/capabilities")
|
||||
public void setCapabiltities(@Auth Account account, @Valid DeviceCapabilities capabilities) {
|
||||
assert(account.getAuthenticatedDevice().isPresent());
|
||||
account.getAuthenticatedDevice().get().setCapabilities(capabilities);
|
||||
accounts.update(account);
|
||||
public void setCapabiltities(@Auth AuthenticatedAccount auth, @Valid DeviceCapabilities capabilities) {
|
||||
assert (auth.getAuthenticatedDevice() != null);
|
||||
final long deviceId = auth.getAuthenticatedDevice().getId();
|
||||
accounts.updateDevice(auth.getAccount(), deviceId, d -> d.setCapabilities(capabilities));
|
||||
}
|
||||
|
||||
@VisibleForTesting protected VerificationCode generateVerificationCode() {
|
||||
@@ -237,6 +240,7 @@ public class DeviceController {
|
||||
private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) {
|
||||
boolean isDowngrade = false;
|
||||
|
||||
isDowngrade |= account.isChangeNumberSupported() && !capabilities.isChangeNumber();
|
||||
isDowngrade |= account.isAnnouncementGroupSupported() && !capabilities.isAnnouncementGroup();
|
||||
isDowngrade |= account.isSenderKeySupported() && !capabilities.isSenderKey();
|
||||
isDowngrade |= account.isGv1MigrationSupported() && !capabilities.isGv1Migration();
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-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 org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
@@ -16,6 +13,8 @@ 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.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
|
||||
@Path("/v1/directory")
|
||||
public class DirectoryController {
|
||||
@@ -30,15 +29,15 @@ public class DirectoryController {
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getAuthToken(@Auth Account account) {
|
||||
return Response.ok().entity(directoryServiceTokenGenerator.generateFor(account.getNumber())).build();
|
||||
public Response getAuthToken(@Auth AuthenticatedAccount auth) {
|
||||
return Response.ok().entity(directoryServiceTokenGenerator.generateFor(auth.getAccount().getNumber())).build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/feedback-v3/{status}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response setFeedback(@Auth Account account) {
|
||||
public Response setFeedback(@Auth AuthenticatedAccount auth) {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ public class DirectoryController {
|
||||
@GET
|
||||
@Path("/{token}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getTokenPresence(@Auth Account account) {
|
||||
public Response getTokenPresence(@Auth AuthenticatedAccount auth) {
|
||||
return Response.status(429).build();
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ public class DirectoryController {
|
||||
@Path("/tokens")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response getContactIntersection(@Auth Account account) {
|
||||
public Response getContactIntersection(@Auth AuthenticatedAccount auth) {
|
||||
return Response.status(429).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013-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.Base64;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.GET;
|
||||
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.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.util.ByteUtil;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@Path("/v2/directory")
|
||||
public class DirectoryV2Controller {
|
||||
|
||||
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
|
||||
|
||||
public DirectoryV2Controller(ExternalServiceCredentialGenerator userTokenGenerator) {
|
||||
this.directoryServiceTokenGenerator = userTokenGenerator;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getAuthToken(@Auth AuthenticatedAccount auth) {
|
||||
|
||||
final UUID uuid = auth.getAccount().getUuid();
|
||||
final String e164 = auth.getAccount().getNumber();
|
||||
final long e164AsLong = Long.parseLong(e164, e164.indexOf('+'), e164.length() - 1, 10);
|
||||
|
||||
final byte[] uuidAndNumber = ByteUtil.combine(UUIDUtil.toBytes(uuid), Util.longToByteArray(e164AsLong));
|
||||
final String username = Base64.getEncoder().encodeToString(uuidAndNumber);
|
||||
|
||||
final ExternalServiceCredentials credentials = directoryServiceTokenGenerator.generateFor(username);
|
||||
|
||||
return Response.ok().entity(credentials).build();
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,22 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinPool.ManagedBlocker;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
@@ -32,30 +41,68 @@ import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.zkgroup.receipts.ReceiptSerial;
|
||||
import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ApplePayAuthorizationResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.RedeemReceiptRequest;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@Path("/v1/donation")
|
||||
public class DonationController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DonationController.class);
|
||||
public interface ReceiptCredentialPresentationFactory {
|
||||
ReceiptCredentialPresentation build(byte[] bytes) throws InvalidInputException;
|
||||
}
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DonationController.class);
|
||||
|
||||
private final Clock clock;
|
||||
private final ServerZkReceiptOperations serverZkReceiptOperations;
|
||||
private final RedeemedReceiptsManager redeemedReceiptsManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final BadgesConfiguration badgesConfiguration;
|
||||
private final ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory;
|
||||
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) {
|
||||
public DonationController(
|
||||
@Nonnull final Clock clock,
|
||||
@Nonnull final ServerZkReceiptOperations serverZkReceiptOperations,
|
||||
@Nonnull final RedeemedReceiptsManager redeemedReceiptsManager,
|
||||
@Nonnull final AccountsManager accountsManager,
|
||||
@Nonnull final BadgesConfiguration badgesConfiguration,
|
||||
@Nonnull final ReceiptCredentialPresentationFactory receiptCredentialPresentationFactory,
|
||||
@Nonnull final Executor httpClientExecutor,
|
||||
@Nonnull final DonationConfiguration configuration,
|
||||
@Nonnull final StripeConfiguration stripeConfiguration) {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
this.serverZkReceiptOperations = Objects.requireNonNull(serverZkReceiptOperations);
|
||||
this.redeemedReceiptsManager = Objects.requireNonNull(redeemedReceiptsManager);
|
||||
this.accountsManager = Objects.requireNonNull(accountsManager);
|
||||
this.badgesConfiguration = Objects.requireNonNull(badgesConfiguration);
|
||||
this.receiptCredentialPresentationFactory = Objects.requireNonNull(receiptCredentialPresentationFactory);
|
||||
this.uri = URI.create(configuration.getUri());
|
||||
this.apiKey = configuration.getApiKey();
|
||||
this.apiKey = stripeConfiguration.getApiKey();
|
||||
this.description = configuration.getDescription();
|
||||
this.supportedCurrencies = configuration.getSupportedCurrencies();
|
||||
this.httpClient = FaultTolerantHttpClient.newBuilder()
|
||||
@@ -64,18 +111,88 @@ public class DonationController {
|
||||
.withVersion(Version.HTTP_2)
|
||||
.withConnectTimeout(Duration.ofSeconds(10))
|
||||
.withRedirect(Redirect.NEVER)
|
||||
.withExecutor(executor)
|
||||
.withExecutor(Objects.requireNonNull(httpClientExecutor))
|
||||
.withName("donation")
|
||||
.withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_3)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/redeem-receipt")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
|
||||
public CompletionStage<Response> redeemReceipt(
|
||||
@Auth final AuthenticatedAccount auth,
|
||||
@Valid final RedeemReceiptRequest request) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
ReceiptCredentialPresentation receiptCredentialPresentation;
|
||||
try {
|
||||
receiptCredentialPresentation = receiptCredentialPresentationFactory.build(
|
||||
request.getReceiptCredentialPresentation());
|
||||
} catch (InvalidInputException e) {
|
||||
return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("invalid receipt credential presentation").type(MediaType.TEXT_PLAIN_TYPE).build());
|
||||
}
|
||||
try {
|
||||
serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
|
||||
} catch (VerificationFailedException e) {
|
||||
return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("receipt credential presentation verification failed").type(MediaType.TEXT_PLAIN_TYPE).build());
|
||||
}
|
||||
|
||||
final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial();
|
||||
final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
|
||||
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
|
||||
final String badgeId = badgesConfiguration.getReceiptLevels().get(receiptLevel);
|
||||
if (badgeId == null) {
|
||||
return CompletableFuture.completedFuture(Response.serverError().entity("server does not recognize the requested receipt level").type(MediaType.TEXT_PLAIN_TYPE).build());
|
||||
}
|
||||
final CompletionStage<Boolean> putStage = redeemedReceiptsManager.put(
|
||||
receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid());
|
||||
return putStage.thenApplyAsync(receiptMatched -> {
|
||||
if (!receiptMatched) {
|
||||
return Response.status(Status.BAD_REQUEST).entity("receipt serial is already redeemed").type(MediaType.TEXT_PLAIN_TYPE).build();
|
||||
}
|
||||
|
||||
try {
|
||||
ForkJoinPool.managedBlock(new ManagedBlocker() {
|
||||
boolean done = false;
|
||||
|
||||
@Override
|
||||
public boolean block() {
|
||||
final Optional<Account> optionalAccount = accountsManager.getByAccountIdentifier(auth.getAccount().getUuid());
|
||||
optionalAccount.ifPresent(account -> {
|
||||
accountsManager.update(account, a -> {
|
||||
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));
|
||||
if (request.isPrimary()) {
|
||||
a.makeBadgePrimaryIfExists(clock, badgeId);
|
||||
}
|
||||
});
|
||||
});
|
||||
done = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
return done;
|
||||
}
|
||||
});
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return Response.serverError().build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
});
|
||||
}).thenCompose(Function.identity());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/authorize-apple-pay")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getApplePayAuthorization(@Auth Account account, @Valid ApplePayAuthorizationRequest request) {
|
||||
public CompletableFuture<Response> getApplePayAuthorization(@Auth AuthenticatedAccount auth, @Valid ApplePayAuthorizationRequest request) {
|
||||
if (!supportedCurrencies.contains(request.getCurrency())) {
|
||||
return CompletableFuture.completedFuture(Response.status(422).build());
|
||||
}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
import org.whispersystems.websocket.session.WebSocketSession;
|
||||
import org.whispersystems.websocket.session.WebSocketSessionContext;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
|
||||
@Path("/v1/keepalive")
|
||||
public class KeepAliveController {
|
||||
@@ -40,15 +39,14 @@ public class KeepAliveController {
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
public Response getKeepAlive(@Auth Account account,
|
||||
@WebSocketSession WebSocketSessionContext context)
|
||||
{
|
||||
if (account != null) {
|
||||
if (!clientPresenceManager.isLocallyPresent(account.getUuid(), account.getAuthenticatedDevice().get().getId())) {
|
||||
public Response getKeepAlive(@Auth AuthenticatedAccount auth,
|
||||
@WebSocketSession WebSocketSessionContext context) {
|
||||
if (auth != null) {
|
||||
if (!clientPresenceManager.isLocallyPresent(auth.getAccount().getUuid(), auth.getAuthenticatedDevice().getId())) {
|
||||
logger.warn("***** No local subscription found for {}::{}; age = {}ms, User-Agent = {}",
|
||||
account.getUuid(), account.getAuthenticatedDevice().get().getId(),
|
||||
System.currentTimeMillis() - context.getClient().getCreatedTimestamp(),
|
||||
context.getClient().getUserAgent());
|
||||
auth.getAccount().getUuid(), auth.getAuthenticatedDevice().getId(),
|
||||
System.currentTimeMillis() - context.getClient().getCreatedTimestamp(),
|
||||
context.getClient().getUserAgent());
|
||||
|
||||
context.getClient().close(1000, "OK");
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
@@ -15,6 +15,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -26,9 +27,9 @@ import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
@@ -40,11 +41,9 @@ 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;
|
||||
|
||||
@@ -55,10 +54,8 @@ public class KeysController {
|
||||
private final RateLimiters rateLimiters;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final AccountsManager accounts;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final PreKeyRateLimiter preKeyRateLimiter;
|
||||
|
||||
private final DynamicConfigurationManager dynamicConfigurationManager;
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
|
||||
private static final String PREKEY_REQUEST_COUNTER_NAME = name(KeysController.class, "preKeyGet");
|
||||
@@ -66,26 +63,21 @@ public class KeysController {
|
||||
|
||||
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,
|
||||
DirectoryQueue directoryQueue, PreKeyRateLimiter preKeyRateLimiter,
|
||||
DynamicConfigurationManager dynamicConfigurationManager,
|
||||
PreKeyRateLimiter preKeyRateLimiter,
|
||||
RateLimitChallengeManager rateLimitChallengeManager) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keysDynamoDb = keysDynamoDb;
|
||||
this.accounts = accounts;
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.preKeyRateLimiter = preKeyRateLimiter;
|
||||
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKeyCount getStatus(@Auth Account account) {
|
||||
int count = keysDynamoDb.getCount(account, account.getAuthenticatedDevice().get().getId());
|
||||
public PreKeyCount getStatus(@Auth AuthenticatedAccount auth) {
|
||||
int count = keysDynamoDb.getCount(auth.getAccount(), auth.getAuthenticatedDevice().getId());
|
||||
|
||||
if (count > 0) {
|
||||
count = count - 1;
|
||||
@@ -97,28 +89,24 @@ public class KeysController {
|
||||
@Timed
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
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;
|
||||
public void setKeys(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @Valid PreKeyState preKeys) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
keysDynamoDb.store(account, device.getId(), preKeys.getPreKeys());
|
||||
@@ -128,20 +116,23 @@ public class KeysController {
|
||||
@GET
|
||||
@Path("/{identifier}/{device_id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
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()) {
|
||||
public Response getDeviceKeys(@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("identifier") UUID targetUuid,
|
||||
@PathParam("device_id") String deviceId,
|
||||
@HeaderParam("User-Agent") String userAgent)
|
||||
throws RateLimitExceededException, RateLimitChallengeException, ServerRejectedException {
|
||||
|
||||
if (!auth.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
Optional<Account> target = accounts.get(targetName);
|
||||
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
|
||||
|
||||
Optional<Account> target = accounts.getByAccountIdentifier(targetUuid);
|
||||
OptionalAccess.verify(account, accessKey, target, deviceId);
|
||||
|
||||
assert(target.isPresent());
|
||||
assert (target.isPresent());
|
||||
|
||||
{
|
||||
final String sourceCountryCode = account.map(a -> Util.getCountryCode(a.getNumber())).orElse("0");
|
||||
@@ -149,28 +140,30 @@ public class KeysController {
|
||||
|
||||
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"
|
||||
INTERNATIONAL_TAG_NAME, String.valueOf(!sourceCountryCode.equals(targetCountryCode))
|
||||
)).increment();
|
||||
}
|
||||
|
||||
if (account.isPresent()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(account.get().getNumber() + "." + account.get().getAuthenticatedDevice().get().getId() + "__" + target.get().getNumber() + "." + deviceId);
|
||||
rateLimiters.getPreKeysLimiter().validate(
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + target.get().getUuid()
|
||||
+ "." + deviceId);
|
||||
|
||||
try {
|
||||
preKeyRateLimiter.validate(account.get());
|
||||
} catch (RateLimitExceededException e) {
|
||||
|
||||
final boolean enforceLimit = rateLimitChallengeManager.shouldIssueRateLimitChallenge(userAgent);
|
||||
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
|
||||
|
||||
Metrics.counter(RATE_LIMITED_GET_PREKEYS_COUNTER_NAME,
|
||||
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.get().getNumber()),
|
||||
"enforced", String.valueOf(enforceLimit))
|
||||
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.get().getNumber()),
|
||||
"legacyClient", String.valueOf(legacyClient))
|
||||
.increment();
|
||||
|
||||
if (enforceLimit) {
|
||||
throw new RateLimitChallengeException(account.get(), e.getRetryDuration());
|
||||
if (legacyClient) {
|
||||
throw new ServerRejectedException();
|
||||
}
|
||||
throw new RateLimitChallengeException(account.get(), e.getRetryDuration());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,28 +189,25 @@ public class KeysController {
|
||||
@PUT
|
||||
@Path("/signed")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setSignedKey(@Auth Account account, @Valid SignedPreKey signedPreKey) {
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
boolean wasAccountEnabled = account.isEnabled();
|
||||
public void setSignedKey(@Auth AuthenticatedAccount auth, @Valid SignedPreKey signedPreKey) {
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
|
||||
device.setSignedPreKey(signedPreKey);
|
||||
accounts.update(account);
|
||||
|
||||
if (!wasAccountEnabled && account.isEnabled()) {
|
||||
directoryQueue.refreshRegisteredUser(account);
|
||||
}
|
||||
accounts.updateDevice(auth.getAccount(), device.getId(), d -> d.setSignedPreKey(signedPreKey));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/signed")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Optional<SignedPreKey> getSignedKey(@Auth Account account) {
|
||||
Device device = account.getAuthenticatedDevice().get();
|
||||
public Optional<SignedPreKey> getSignedKey(@Auth AuthenticatedAccount auth) {
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
|
||||
if (signedPreKey != null) return Optional.of(signedPreKey);
|
||||
else return Optional.empty();
|
||||
if (signedPreKey != null) {
|
||||
return Optional.of(signedPreKey);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
@@ -16,12 +16,11 @@ 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.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.io.IOException;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
@@ -31,16 +30,17 @@ import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
@@ -56,16 +56,13 @@ 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 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.abuse.FilterAbusiveMessages;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
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;
|
||||
@@ -90,17 +87,13 @@ 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;
|
||||
@@ -127,61 +120,44 @@ public class MessageController {
|
||||
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 final ExecutorService multiRecipientMessageExecutor;
|
||||
|
||||
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 SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages");
|
||||
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 String RATE_LIMITED_MESSAGE_COUNTER_NAME = name(MessageController.class, "rateLimitedMessage");
|
||||
|
||||
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
||||
private static final String SENDER_TYPE_TAG_NAME = "senderType";
|
||||
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 RATE_LIMIT_REASON_TAG_NAME = "rateLimitReason";
|
||||
|
||||
private static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes();
|
||||
|
||||
public MessageController(RateLimiters rateLimiters,
|
||||
public MessageController(
|
||||
RateLimiters rateLimiters,
|
||||
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.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);
|
||||
}
|
||||
@Nonnull ExecutorService multiRecipientMessageExecutor) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.messageSender = messageSender;
|
||||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.unsealedSenderRateLimiter = unsealedSenderRateLimiter;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
this.reportMessageManager = reportMessageManager;
|
||||
this.multiRecipientMessageExecutor = Objects.requireNonNull(multiRecipientMessageExecutor);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -189,31 +165,22 @@ public class MessageController {
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
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)
|
||||
@FilterAbusiveMessages
|
||||
public Response sendMessage(@Auth Optional<AuthenticatedAccount> source,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@HeaderParam("X-Forwarded-For") String forwardedFor,
|
||||
@PathParam("destination") UUID destinationUuid,
|
||||
@Valid IncomingMessageList messages)
|
||||
throws RateLimitExceededException, RateLimitChallengeException {
|
||||
|
||||
if (source.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (source.isPresent() && !source.get().isFor(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)) {
|
||||
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
|
||||
identifiedMeter.mark();
|
||||
senderType = "identified";
|
||||
} else if (source.isEmpty()) {
|
||||
@@ -243,79 +210,71 @@ public class MessageController {
|
||||
}
|
||||
|
||||
try {
|
||||
boolean isSyncMessage = source.isPresent() && source.get().isFor(destinationName);
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getAccount().getUuid().equals(destinationUuid);
|
||||
|
||||
Optional<Account> destination;
|
||||
|
||||
if (!isSyncMessage) destination = accountsManager.get(destinationName);
|
||||
else destination = source;
|
||||
if (!isSyncMessage) {
|
||||
destination = accountsManager.getByAccountIdentifier(destinationUuid);
|
||||
} else {
|
||||
destination = source.map(AuthenticatedAccount::getAccount);
|
||||
}
|
||||
|
||||
OptionalAccess.verify(source, accessKey, destination);
|
||||
assert(destination.isPresent());
|
||||
OptionalAccess.verify(source.map(AuthenticatedAccount::getAccount), 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());
|
||||
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
|
||||
final String senderCountryCode = Util.getCountryCode(source.get().getAccount().getNumber());
|
||||
|
||||
try {
|
||||
unsealedSenderRateLimiter.validate(source.get(), destination.get());
|
||||
rateLimiters.getMessagesLimiter().validate(source.get().getAccount().getUuid(), destination.get().getUuid());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final boolean enforceLimit = rateLimitChallengeManager.shouldIssueRateLimitChallenge(userAgent);
|
||||
|
||||
Metrics.counter(REJECT_UNSEALED_SENDER_COUNTER_NAME,
|
||||
Metrics.counter(RATE_LIMITED_MESSAGE_COUNTER_NAME,
|
||||
SENDER_COUNTRY_TAG_NAME, senderCountryCode,
|
||||
"enforced", String.valueOf(enforceLimit))
|
||||
.increment();
|
||||
RATE_LIMIT_REASON_TAG_NAME, "singleDestinationRate").increment();
|
||||
|
||||
if (enforceLimit) {
|
||||
logger.debug("Rejected unsealed sender limit from: {}", source.get().getNumber());
|
||||
|
||||
throw new RateLimitChallengeException(source.get(), e.getRetryDuration());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
final String destinationCountryCode = Util.getCountryCode(destination.get().getNumber());
|
||||
final Device masterDevice = source.get().getMasterDevice().get();
|
||||
try {
|
||||
unsealedSenderRateLimiter.validate(source.get().getAccount(), destination.get());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
if (!senderCountryCode.equals(destinationCountryCode)) {
|
||||
recordInternationalUnsealedSenderMetrics(forwardedFor, senderCountryCode, destination.get().getNumber());
|
||||
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
|
||||
final String rateLimitReason = legacyClient ? "unsealedSenderCardinality" : "challengeIssued";
|
||||
|
||||
if (StringUtils.isAllBlank(masterDevice.getApnId(), masterDevice.getVoipApnId(), masterDevice.getGcmId()) || masterDevice.getUninstalledFeedbackTimestamp() > 0) {
|
||||
if (dynamicConfigurationManager.getConfiguration().getMessageRateConfiguration().getRateLimitedCountryCodes().contains(senderCountryCode)) {
|
||||
Metrics.counter(RATE_LIMITED_MESSAGE_COUNTER_NAME,
|
||||
SENDER_COUNTRY_TAG_NAME, senderCountryCode,
|
||||
RATE_LIMIT_REASON_TAG_NAME, rateLimitReason).increment();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
if (legacyClient) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw new RateLimitChallengeException(source.get().getAccount(), e.getRetryDuration());
|
||||
}
|
||||
}
|
||||
|
||||
validateCompleteDeviceList(destination.get(), messages.getMessages(), isSyncMessage);
|
||||
validateCompleteDeviceList(destination.get(), messages.getMessages(), isSyncMessage,
|
||||
source.map(AuthenticatedAccount::getAuthenticatedDevice).map(Device::getId));
|
||||
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(EPHEMERAL_TAG_NAME, String.valueOf(messages.isOnline())),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, senderType));
|
||||
|
||||
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);
|
||||
sendMessage(source, destination.get(), destinationDevice.get(), messages.getTimestamp(), messages.isOnline(),
|
||||
incomingMessage);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.ok(new SendMessageResponse(!isSyncMessage && source.isPresent() && source.get().getEnabledDeviceCount() > 1)).build();
|
||||
return Response.ok(new SendMessageResponse(
|
||||
!isSyncMessage && source.isPresent() && source.get().getAccount().getEnabledDeviceCount() > 1)).build();
|
||||
} catch (NoSuchUserException e) {
|
||||
throw new WebApplicationException(Response.status(404).build());
|
||||
} catch (MismatchedDevicesException e) {
|
||||
@@ -337,6 +296,7 @@ public class MessageController {
|
||||
@PUT
|
||||
@Consumes(MultiRecipientMessageProvider.MEDIA_TYPE)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@FilterAbusiveMessages
|
||||
public Response sendMultiRecipientMessage(
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) CombinedUnidentifiedSenderAccessKeys accessKeys,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@@ -350,8 +310,8 @@ public class MessageController {
|
||||
Map<UUID, Account> uuidToAccountMap = Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(Recipient::getUuid)
|
||||
.distinct()
|
||||
.collect(Collectors.toMap(Function.identity(), uuid -> {
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
.collect(Collectors.toUnmodifiableMap(Function.identity(), uuid -> {
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
if (account.isEmpty()) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
@@ -378,7 +338,7 @@ public class MessageController {
|
||||
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);
|
||||
validateCompleteDeviceList(account, deviceIds, false, Optional.empty());
|
||||
validateRegistrationIds(account, deviceIdAndRegistrationIdSet.stream());
|
||||
} catch (MismatchedDevicesException e) {
|
||||
accountMismatchedDevices.add(new AccountMismatchedDevices(account.getUuid(),
|
||||
@@ -406,20 +366,28 @@ public class MessageController {
|
||||
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()) {
|
||||
List<UUID> uuids404 = Collections.synchronizedList(new ArrayList<>());
|
||||
final Counter counter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags);
|
||||
try {
|
||||
multiRecipientMessageExecutor.invokeAll(Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(recipient -> (Callable<Void>) () -> {
|
||||
Account destinationAccount = uuidToAccountMap.get(recipient.getUuid());
|
||||
|
||||
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());
|
||||
}
|
||||
// we asserted this must exist in validateCompleteDeviceList
|
||||
Device destinationDevice = destinationAccount.getDevice(recipient.getDeviceId()).orElseThrow();
|
||||
counter.increment();
|
||||
try {
|
||||
sendMessage(destinationAccount, destinationDevice, timestamp, online, recipient,
|
||||
multiRecipientMessage.getCommonPayload());
|
||||
} catch (NoSuchUserException e) {
|
||||
uuids404.add(destinationAccount.getUuid());
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("interrupted while delivering multi-recipient messages", e);
|
||||
return Response.serverError().entity("interrupted during delivery").build();
|
||||
}
|
||||
return Response.ok(new SendMultiRecipientMessageResponse(uuids404)).build();
|
||||
}
|
||||
@@ -427,8 +395,15 @@ public class MessageController {
|
||||
private void checkAccessKeys(CombinedUnidentifiedSenderAccessKeys accessKeys, Map<UUID, Account> uuidToAccountMap) {
|
||||
AtomicBoolean throwUnauthorized = new AtomicBoolean(false);
|
||||
byte[] empty = new byte[16];
|
||||
final Optional<byte[]> UNRESTRICTED_UNIDENTIFIED_ACCESS_KEY = Optional.of(new byte[16]);
|
||||
byte[] combinedUnknownAccessKeys = uuidToAccountMap.values().stream()
|
||||
.map(Account::getUnidentifiedAccessKey)
|
||||
.map(account -> {
|
||||
if (account.isUnrestrictedUnidentifiedAccess()) {
|
||||
return UNRESTRICTED_UNIDENTIFIED_ACCESS_KEY;
|
||||
} else {
|
||||
return account.getUnidentifiedAccessKey();
|
||||
}
|
||||
})
|
||||
.map(accessKey -> {
|
||||
if (accessKey.isEmpty()) {
|
||||
throwUnauthorized.set(true);
|
||||
@@ -452,63 +427,20 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
public OutgoingMessageEntityList getPendingMessages(@Auth AuthenticatedAccount auth,
|
||||
@HeaderParam("User-Agent") String userAgent) {
|
||||
assert auth.getAuthenticatedDevice() != null;
|
||||
|
||||
// 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()));
|
||||
if (!Util.isEmpty(auth.getAuthenticatedDevice().getApnId())) {
|
||||
RedisOperation.unchecked(() -> apnFallbackManager.cancel(auth.getAccount(), auth.getAuthenticatedDevice()));
|
||||
}
|
||||
|
||||
final OutgoingMessageEntityList outgoingMessages = messagesManager.getMessagesForDevice(
|
||||
account.getUuid(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
auth.getAccount().getUuid(),
|
||||
auth.getAuthenticatedDevice().getId(),
|
||||
userAgent,
|
||||
false);
|
||||
|
||||
@@ -542,44 +474,21 @@ public class MessageController {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/{source}/{timestamp}")
|
||||
public void removePendingMessage(@Auth Account account,
|
||||
@PathParam("source") String source,
|
||||
@PathParam("timestamp") long timestamp)
|
||||
{
|
||||
try {
|
||||
WebSocketConnection.recordMessageDeliveryDuration(timestamp, account.getAuthenticatedDevice().get());
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(
|
||||
account.getUuid(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
source, timestamp);
|
||||
|
||||
if (message.isPresent() && message.get().getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(account,
|
||||
message.get().getSource(),
|
||||
message.get().getTimestamp());
|
||||
}
|
||||
} catch (NoSuchUserException e) {
|
||||
logger.warn("Sending delivery receipt", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/uuid/{uuid}")
|
||||
public void removePendingMessage(@Auth Account account, @PathParam("uuid") UUID uuid) {
|
||||
public void removePendingMessage(@Auth AuthenticatedAccount auth, @PathParam("uuid") UUID uuid) {
|
||||
try {
|
||||
Optional<OutgoingMessageEntity> message = messagesManager.delete(
|
||||
account.getUuid(),
|
||||
account.getAuthenticatedDevice().get().getId(),
|
||||
uuid);
|
||||
auth.getAccount().getUuid(),
|
||||
auth.getAuthenticatedDevice().getId(),
|
||||
uuid);
|
||||
|
||||
if (message.isPresent()) {
|
||||
WebSocketConnection.recordMessageDeliveryDuration(message.get().getTimestamp(), account.getAuthenticatedDevice().get());
|
||||
if (!Util.isEmpty(message.get().getSource()) && message.get().getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(account, message.get().getSource(), message.get().getTimestamp());
|
||||
WebSocketConnection.recordMessageDeliveryDuration(message.get().getTimestamp(), auth.getAuthenticatedDevice());
|
||||
if (!Util.isEmpty(message.get().getSource())
|
||||
&& message.get().getType() != Envelope.Type.SERVER_DELIVERY_RECEIPT_VALUE) {
|
||||
receiptSender.sendReceipt(auth, message.get().getSourceUuid(), message.get().getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,7 +500,8 @@ public class MessageController {
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/report/{sourceNumber}/{messageGuid}")
|
||||
public Response reportMessage(@Auth Account account, @PathParam("sourceNumber") String sourceNumber, @PathParam("messageGuid") UUID messageGuid) {
|
||||
public Response reportMessage(@Auth AuthenticatedAccount auth, @PathParam("sourceNumber") String sourceNumber,
|
||||
@PathParam("messageGuid") UUID messageGuid) {
|
||||
|
||||
reportMessageManager.report(sourceNumber, messageGuid);
|
||||
|
||||
@@ -599,27 +509,26 @@ public class MessageController {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void sendMessage(Optional<Account> source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
boolean online,
|
||||
IncomingMessage incomingMessage)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
private void sendMessage(Optional<AuthenticatedAccount> source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
boolean online,
|
||||
IncomingMessage incomingMessage)
|
||||
throws NoSuchUserException {
|
||||
try (final Timer.Context ignored = sendMessageInternalTimer.time()) {
|
||||
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
|
||||
Optional<byte[]> messageBody = getMessageBody(incomingMessage);
|
||||
Optional<byte[]> messageContent = getMessageContent(incomingMessage);
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
|
||||
messageBuilder.setType(Envelope.Type.forNumber(incomingMessage.getType()))
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setServerTimestamp(System.currentTimeMillis());
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setServerTimestamp(System.currentTimeMillis());
|
||||
|
||||
if (source.isPresent()) {
|
||||
messageBuilder.setSource(source.get().getNumber())
|
||||
.setSourceUuid(source.get().getUuid().toString())
|
||||
.setSourceDevice((int)source.get().getAuthenticatedDevice().get().getId());
|
||||
messageBuilder.setSource(source.get().getAccount().getNumber())
|
||||
.setSourceUuid(source.get().getAccount().getUuid().toString())
|
||||
.setSourceDevice((int) source.get().getAuthenticatedDevice().getId());
|
||||
}
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
@@ -693,24 +602,26 @@ public class MessageController {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void validateCompleteDeviceList(Account account, List<IncomingMessage> messages, boolean isSyncMessage)
|
||||
public static void validateCompleteDeviceList(Account account, List<IncomingMessage> messages, boolean isSyncMessage,
|
||||
Optional<Long> authenticatedDeviceId)
|
||||
throws MismatchedDevicesException {
|
||||
Set<Long> messageDeviceIds = messages.stream().map(IncomingMessage::getDestinationDeviceId).collect(Collectors.toSet());
|
||||
validateCompleteDeviceList(account, messageDeviceIds, isSyncMessage);
|
||||
Set<Long> messageDeviceIds = messages.stream().map(IncomingMessage::getDestinationDeviceId)
|
||||
.collect(Collectors.toSet());
|
||||
validateCompleteDeviceList(account, messageDeviceIds, isSyncMessage, authenticatedDeviceId);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void validateCompleteDeviceList(Account account, Set<Long> messageDeviceIds, boolean isSyncMessage)
|
||||
public static void validateCompleteDeviceList(Account account, Set<Long> messageDeviceIds, boolean isSyncMessage,
|
||||
Optional<Long> authenticatedDeviceId)
|
||||
throws MismatchedDevicesException {
|
||||
Set<Long> accountDeviceIds = new HashSet<>();
|
||||
|
||||
List<Long> missingDeviceIds = new LinkedList<>();
|
||||
List<Long> extraDeviceIds = new LinkedList<>();
|
||||
List<Long> extraDeviceIds = new LinkedList<>();
|
||||
|
||||
for (Device device : account.getDevices()) {
|
||||
for (Device device : account.getDevices()) {
|
||||
if (device.isEnabled() &&
|
||||
!(isSyncMessage && device.getId() == account.getAuthenticatedDevice().get().getId()))
|
||||
{
|
||||
!(isSyncMessage && device.getId() == authenticatedDeviceId.get())) {
|
||||
accountDeviceIds.add(device.getId());
|
||||
|
||||
if (!messageDeviceIds.contains(device.getId())) {
|
||||
@@ -751,28 +662,4 @@ public class MessageController {
|
||||
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 + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,15 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class NoSuchUserException extends Exception {
|
||||
|
||||
private List<String> missing;
|
||||
|
||||
public NoSuchUserException(String user) {
|
||||
super(user);
|
||||
missing = new LinkedList<>();
|
||||
missing.add(user);
|
||||
}
|
||||
|
||||
public NoSuchUserException(List<String> missing) {
|
||||
this.missing = missing;
|
||||
public NoSuchUserException(final UUID uuid) {
|
||||
super(uuid.toString());
|
||||
}
|
||||
|
||||
public NoSuchUserException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public List<String> getMissing() {
|
||||
return missing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
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 io.dropwizard.auth.Auth;
|
||||
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;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||
import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList;
|
||||
|
||||
@Path("/v1/payments")
|
||||
public class PaymentsController {
|
||||
@@ -34,15 +32,15 @@ public class PaymentsController {
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ExternalServiceCredentials getAuth(@Auth Account account) {
|
||||
return paymentsServiceCredentialGenerator.generateFor(account.getUuid().toString());
|
||||
public ExternalServiceCredentials getAuth(@Auth AuthenticatedAccount auth) {
|
||||
return paymentsServiceCredentialGenerator.generateFor(auth.getAccount().getUuid().toString());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/conversions")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CurrencyConversionEntityList getConversions(@Auth Account account) {
|
||||
public CurrencyConversionEntityList getConversions(@Auth AuthenticatedAccount auth) {
|
||||
return currencyManager.getCurrencyConversions().orElseThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -8,11 +8,21 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.valueextraction.Unwrapping;
|
||||
import javax.ws.rs.Consumes;
|
||||
@@ -21,9 +31,12 @@ import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
@@ -39,10 +52,14 @@ import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
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.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
|
||||
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.Profile;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
@@ -51,6 +68,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
@@ -68,66 +86,71 @@ public class ProfileController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ProfileController.class);
|
||||
|
||||
private final Clock clock;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final DynamicConfigurationManager dynamicConfigurationManager;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final ProfileBadgeConverter profileBadgeConverter;
|
||||
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
|
||||
|
||||
private final PolicySigner policySigner;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
private final ServerZkProfileOperations zkProfileOperations;
|
||||
private final boolean isZkEnabled;
|
||||
|
||||
private final S3Client s3client;
|
||||
private final String bucket;
|
||||
|
||||
public ProfileController(RateLimiters rateLimiters,
|
||||
public ProfileController(
|
||||
Clock clock,
|
||||
RateLimiters rateLimiters,
|
||||
AccountsManager accountsManager,
|
||||
ProfilesManager profilesManager,
|
||||
UsernamesManager usernamesManager,
|
||||
DynamicConfigurationManager dynamicConfigurationManager,
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
ProfileBadgeConverter profileBadgeConverter,
|
||||
BadgesConfiguration badgesConfiguration,
|
||||
S3Client s3client,
|
||||
PostPolicyGenerator policyGenerator,
|
||||
PolicySigner policySigner,
|
||||
String bucket,
|
||||
ServerZkProfileOperations zkProfileOperations,
|
||||
boolean isZkEnabled)
|
||||
{
|
||||
ServerZkProfileOperations zkProfileOperations) {
|
||||
this.clock = clock;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.profileBadgeConverter = profileBadgeConverter;
|
||||
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap(
|
||||
BadgeConfiguration::getId, Function.identity()));
|
||||
this.zkProfileOperations = zkProfileOperations;
|
||||
this.bucket = bucket;
|
||||
this.s3client = s3client;
|
||||
this.policyGenerator = policyGenerator;
|
||||
this.policySigner = policySigner;
|
||||
this.isZkEnabled = isZkEnabled;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
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();
|
||||
public Response setProfile(@Auth AuthenticatedAccount auth, @Valid CreateProfileRequest request) {
|
||||
final Set<String> disallowedPaymentsCountryCodes =
|
||||
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedCountryCodes();
|
||||
|
||||
if (StringUtils.isNotBlank(request.getPaymentAddress()) &&
|
||||
!allowedPaymentsCountryCodes.contains(Util.getCountryCode(account.getNumber()))) {
|
||||
disallowedPaymentsCountryCodes.contains(Util.getCountryCode(auth.getAccount().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();
|
||||
Optional<VersionedProfile> currentProfile = profilesManager.get(auth.getAccount().getUuid(), request.getVersion());
|
||||
String avatar = request.isAvatar() ? generateAvatarObjectName() : null;
|
||||
Optional<ProfileAvatarUploadAttributes> response = Optional.empty();
|
||||
|
||||
profilesManager.set(account.getUuid(),
|
||||
profilesManager.set(auth.getAccount().getUuid(),
|
||||
new VersionedProfile(
|
||||
request.getVersion(),
|
||||
request.getName(),
|
||||
@@ -140,79 +163,95 @@ public class ProfileController {
|
||||
if (request.isAvatar()) {
|
||||
Optional<String> currentAvatar = Optional.empty();
|
||||
|
||||
if (currentProfile.isPresent() && currentProfile.get().getAvatar() != null && currentProfile.get().getAvatar().startsWith("profiles/")) {
|
||||
if (currentProfile.isPresent() && currentProfile.get().getAvatar() != null && currentProfile.get().getAvatar()
|
||||
.startsWith("profiles/")) {
|
||||
currentAvatar = Optional.of(currentProfile.get().getAvatar());
|
||||
}
|
||||
|
||||
if (currentAvatar.isEmpty() && account.getAvatar() != null && account.getAvatar().startsWith("profiles/")) {
|
||||
currentAvatar = Optional.of(account.getAvatar());
|
||||
if (currentAvatar.isEmpty() && auth.getAccount().getAvatar() != null && auth.getAccount().getAvatar()
|
||||
.startsWith("profiles/")) {
|
||||
currentAvatar = Optional.of(auth.getAccount().getAvatar());
|
||||
}
|
||||
|
||||
currentAvatar.ifPresent(s -> s3client.deleteObject(DeleteObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(s)
|
||||
.build()));
|
||||
.bucket(bucket)
|
||||
.key(s)
|
||||
.build()));
|
||||
|
||||
response = Optional.of(generateAvatarUploadForm(avatar));
|
||||
}
|
||||
|
||||
account.setProfileName(request.getName());
|
||||
account.setAvatar(avatar);
|
||||
account.setCurrentProfileVersion(request.getVersion());
|
||||
accountsManager.update(account);
|
||||
List<AccountBadge> updatedBadges = mergeBadgeIdsWithExistingAccountBadges(
|
||||
request.getBadges(), auth.getAccount().getBadges());
|
||||
|
||||
if (response.isPresent()) return Response.ok(response).build();
|
||||
else return Response.ok().build();
|
||||
accountsManager.update(auth.getAccount(), a -> {
|
||||
a.setProfileName(request.getName());
|
||||
a.setAvatar(avatar);
|
||||
a.setBadges(clock, updatedBadges);
|
||||
a.setCurrentProfileVersion(request.getVersion());
|
||||
});
|
||||
|
||||
if (response.isPresent()) {
|
||||
return Response.ok(response).build();
|
||||
} else {
|
||||
return Response.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}")
|
||||
public Optional<Profile> getProfile(@Auth Optional<Account> requestAccount,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("version") String version)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (!isZkEnabled) throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
return getVersionedProfile(requestAccount, accessKey, uuid, version, Optional.empty());
|
||||
public Optional<Profile> getProfile(
|
||||
@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("version") String version)
|
||||
throws RateLimitExceededException {
|
||||
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
|
||||
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
|
||||
version, Optional.empty());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}/{credentialRequest}")
|
||||
public Optional<Profile> getProfile(@Auth Optional<Account> requestAccount,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("version") String version,
|
||||
@PathParam("credentialRequest") String credentialRequest)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (!isZkEnabled) throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
return getVersionedProfile(requestAccount, accessKey, uuid, version, Optional.of(credentialRequest));
|
||||
public Optional<Profile> getProfile(
|
||||
@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("version") String version,
|
||||
@PathParam("credentialRequest") String credentialRequest)
|
||||
throws RateLimitExceededException {
|
||||
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
|
||||
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
|
||||
version, Optional.of(credentialRequest));
|
||||
}
|
||||
|
||||
private Optional<Profile> getVersionedProfile(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
UUID uuid,
|
||||
String version,
|
||||
Optional<String> credentialRequest)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (!isZkEnabled) throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
|
||||
private Optional<Profile> getVersionedProfile(
|
||||
Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
List<Locale> acceptableLanguages,
|
||||
UUID uuid,
|
||||
String version,
|
||||
Optional<String> credentialRequest)
|
||||
throws RateLimitExceededException {
|
||||
try {
|
||||
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
boolean isSelf = false;
|
||||
if (requestAccount.isPresent()) {
|
||||
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber());
|
||||
UUID authedUuid = requestAccount.get().getUuid();
|
||||
rateLimiters.getProfileLimiter().validate(authedUuid);
|
||||
isSelf = uuid.equals(authedUuid);
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid);
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
|
||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||
|
||||
assert(accountProfile.isPresent());
|
||||
@@ -235,18 +274,20 @@ public class ProfileController {
|
||||
|
||||
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(),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username.orElse(null),
|
||||
null,
|
||||
credential.orElse(null)));
|
||||
return Optional.of(new Profile(
|
||||
name,
|
||||
about,
|
||||
aboutEmoji,
|
||||
avatar,
|
||||
paymentAddress,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username.orElse(null),
|
||||
null,
|
||||
profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges(), isSelf),
|
||||
credential.orElse(null)));
|
||||
} catch (InvalidInputException e) {
|
||||
logger.info("Bad profile request", e);
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
@@ -258,8 +299,12 @@ public class ProfileController {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/username/{username}")
|
||||
public Profile getProfileByUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameLookupLimiter().validate(account.getUuid().toString());
|
||||
public Profile getProfileByUsername(
|
||||
@Auth AuthenticatedAccount auth,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@PathParam("username") String username)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameLookupLimiter().validate(auth.getAccount().getUuid());
|
||||
|
||||
username = username.toLowerCase();
|
||||
|
||||
@@ -269,24 +314,31 @@ public class ProfileController {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid.get());
|
||||
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid.get());
|
||||
|
||||
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(),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username,
|
||||
accountProfile.get().getUuid(),
|
||||
null);
|
||||
return new Profile(
|
||||
accountProfile.get().getProfileName(),
|
||||
null,
|
||||
null,
|
||||
accountProfile.get().getAvatar(),
|
||||
null,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username,
|
||||
accountProfile.get().getUuid(),
|
||||
profileBadgeConverter.convert(
|
||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||
accountProfile.get().getBadges(),
|
||||
isSelf),
|
||||
null);
|
||||
}
|
||||
|
||||
private Optional<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> encodedProfileCredentialRequest,
|
||||
@@ -311,79 +363,83 @@ public class ProfileController {
|
||||
|
||||
// Old profile endpoints. Replaced by versioned profile endpoints (above)
|
||||
|
||||
@Deprecated
|
||||
@Timed
|
||||
@PUT
|
||||
@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);
|
||||
}
|
||||
@Deprecated
|
||||
@Timed
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/name/{name}")
|
||||
public void setProfile(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("name") @ExactlySize(value = {72, 108}, payload = {Unwrapping.Unwrap.class}) Optional<String> name) {
|
||||
accountsManager.update(auth.getAccount(), a -> a.setProfileName(name.orElse(null)));
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{identifier}")
|
||||
public Profile getProfile(@Auth Optional<Account> requestAccount,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@PathParam("identifier") AmbiguousIdentifier identifier,
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
|
||||
public Profile getProfile(
|
||||
@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@HeaderParam("User-Agent") String userAgent,
|
||||
@PathParam("identifier") UUID identifier,
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
throws RateLimitExceededException {
|
||||
|
||||
if (auth.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (requestAccount.isPresent()) {
|
||||
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber());
|
||||
boolean isSelf = false;
|
||||
if (auth.isPresent()) {
|
||||
UUID authedUuid = auth.get().getAccount().getUuid();
|
||||
rateLimiters.getProfileLimiter().validate(authedUuid);
|
||||
isSelf = authedUuid.equals(identifier);
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(identifier);
|
||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||
Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(identifier);
|
||||
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
|
||||
|
||||
Optional<String> username = Optional.empty();
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
|
||||
if (!identifier.hasNumber()) {
|
||||
//noinspection OptionalGetWithoutIsPresent
|
||||
username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
}
|
||||
|
||||
return new Profile(accountProfile.get().getProfileName(),
|
||||
null,
|
||||
null,
|
||||
accountProfile.get().getAvatar(),
|
||||
null,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username.orElse(null),
|
||||
null,
|
||||
null);
|
||||
return new Profile(
|
||||
accountProfile.get().getProfileName(),
|
||||
null,
|
||||
null,
|
||||
accountProfile.get().getAvatar(),
|
||||
null,
|
||||
accountProfile.get().getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()),
|
||||
accountProfile.get().isUnrestrictedUnidentifiedAccess(),
|
||||
UserCapabilities.createForAccount(accountProfile.get()),
|
||||
username.orElse(null),
|
||||
null,
|
||||
profileBadgeConverter.convert(
|
||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||
accountProfile.get().getBadges(),
|
||||
isSelf),
|
||||
null);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/avatar")
|
||||
public ProfileAvatarUploadAttributes getAvatarUploadForm(@Auth AuthenticatedAccount auth) {
|
||||
String previousAvatar = auth.getAccount().getAvatar();
|
||||
String objectName = generateAvatarObjectName();
|
||||
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
|
||||
|
||||
@Deprecated
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/form/avatar")
|
||||
public ProfileAvatarUploadAttributes getAvatarUploadForm(@Auth Account account) {
|
||||
String previousAvatar = account.getAvatar();
|
||||
String objectName = generateAvatarObjectName();
|
||||
ProfileAvatarUploadAttributes profileAvatarUploadAttributes = generateAvatarUploadForm(objectName);
|
||||
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
|
||||
s3client.deleteObject(DeleteObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(previousAvatar)
|
||||
.build());
|
||||
}
|
||||
|
||||
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
|
||||
s3client.deleteObject(DeleteObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(previousAvatar)
|
||||
.build());
|
||||
}
|
||||
|
||||
account.setAvatar(objectName);
|
||||
accountsManager.update(account);
|
||||
accountsManager.update(auth.getAccount(), a -> a.setAvatar(objectName));
|
||||
|
||||
return profileAvatarUploadAttributes;
|
||||
}
|
||||
@@ -406,4 +462,55 @@ public class ProfileController {
|
||||
|
||||
return "profiles/" + Base64.encodeBase64URLSafeString(object);
|
||||
}
|
||||
|
||||
private List<Locale> getAcceptableLanguagesForRequest(ContainerRequestContext containerRequestContext) {
|
||||
try {
|
||||
return containerRequestContext.getAcceptableLanguages();
|
||||
} catch (final ProcessingException e) {
|
||||
logger.warn("Could not get acceptable languages", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private List<AccountBadge> mergeBadgeIdsWithExistingAccountBadges(
|
||||
final List<String> badgeIds,
|
||||
final List<AccountBadge> accountBadges) {
|
||||
LinkedHashMap<String, AccountBadge> existingBadges = new LinkedHashMap<>(accountBadges.size());
|
||||
for (final AccountBadge accountBadge : accountBadges) {
|
||||
existingBadges.putIfAbsent(accountBadge.getId(), accountBadge);
|
||||
}
|
||||
|
||||
LinkedHashMap<String, AccountBadge> result = new LinkedHashMap<>(accountBadges.size());
|
||||
for (final String badgeId : badgeIds) {
|
||||
|
||||
// duplicate in the list, ignore it
|
||||
if (result.containsKey(badgeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is for testing badges and allows them to be added to an account at any time with an expiration of 1 day
|
||||
// in the future.
|
||||
BadgeConfiguration badgeConfiguration = badgeConfigurationMap.get(badgeId);
|
||||
if (badgeConfiguration != null && badgeConfiguration.isTestBadge()) {
|
||||
result.put(badgeId, new AccountBadge(badgeId, clock.instant().plus(Duration.ofDays(1)), true));
|
||||
continue;
|
||||
}
|
||||
|
||||
// reordering or making visible existing badges
|
||||
if (existingBadges.containsKey(badgeId)) {
|
||||
AccountBadge accountBadge = existingBadges.get(badgeId).withVisibility(true);
|
||||
result.put(badgeId, accountBadge);
|
||||
}
|
||||
}
|
||||
|
||||
// take any remaining account badges and make them invisible
|
||||
for (final Entry<String, AccountBadge> entry : existingBadges.entrySet()) {
|
||||
if (!result.containsKey(entry.getKey())) {
|
||||
AccountBadge accountBadge = entry.getValue().withVisibility(false);
|
||||
result.put(accountBadge.getId(), accountBadge);
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(result.values());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import java.util.Base64;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.PUT;
|
||||
@@ -22,10 +17,11 @@ import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
|
||||
|
||||
@Path("/v1/provisioning")
|
||||
public class ProvisioningController {
|
||||
@@ -43,16 +39,15 @@ public class ProvisioningController {
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void sendProvisioningMessage(@Auth Account source,
|
||||
@PathParam("destination") String destinationName,
|
||||
@Valid ProvisioningMessage message)
|
||||
throws RateLimitExceededException, InvalidWebsocketAddressException, IOException
|
||||
{
|
||||
rateLimiters.getMessagesLimiter().validate(source.getNumber());
|
||||
public void sendProvisioningMessage(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("destination") String destinationName,
|
||||
@Valid ProvisioningMessage message)
|
||||
throws RateLimitExceededException {
|
||||
|
||||
rateLimiters.getMessagesLimiter().validate(auth.getAccount().getUuid());
|
||||
|
||||
if (!provisioningManager.sendProvisioningMessage(new ProvisioningAddress(destinationName, 0),
|
||||
Base64.getDecoder().decode(message.getBody())))
|
||||
{
|
||||
Base64.getMimeDecoder().decode(message.getBody()))) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -8,13 +8,16 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
@@ -27,16 +30,12 @@ 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.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
|
||||
@Path("/v1/config")
|
||||
public class RemoteConfigController {
|
||||
@@ -57,15 +56,19 @@ public class RemoteConfigController {
|
||||
@GET
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public UserRemoteConfigList getAll(@Auth Account account) {
|
||||
public UserRemoteConfigList getAll(@Auth AuthenticatedAccount auth) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||
|
||||
final Stream<UserRemoteConfig> globalConfigStream = globalConfig.entrySet().stream().map(entry -> new UserRemoteConfig(GLOBAL_CONFIG_PREFIX + entry.getKey(), true, entry.getValue()));
|
||||
final Stream<UserRemoteConfig> globalConfigStream = globalConfig.entrySet().stream()
|
||||
.map(entry -> new UserRemoteConfig(GLOBAL_CONFIG_PREFIX + entry.getKey(), true, entry.getValue()));
|
||||
return new UserRemoteConfigList(Stream.concat(remoteConfigsManager.getAll().stream().map(config -> {
|
||||
final byte[] hashKey = config.getHashKey() != null ? config.getHashKey().getBytes(StandardCharsets.UTF_8) : config.getName().getBytes(StandardCharsets.UTF_8);
|
||||
boolean inBucket = isInBucket(digest, account.getUuid(), hashKey, config.getPercentage(), config.getUuids());
|
||||
return new UserRemoteConfig(config.getName(), inBucket, inBucket ? config.getValue() : config.getDefaultValue());
|
||||
final byte[] hashKey = config.getHashKey() != null ? config.getHashKey().getBytes(StandardCharsets.UTF_8)
|
||||
: config.getName().getBytes(StandardCharsets.UTF_8);
|
||||
boolean inBucket = isInBucket(digest, auth.getAccount().getUuid(), hashKey, config.getPercentage(),
|
||||
config.getUuids());
|
||||
return new UserRemoteConfig(config.getName(), inBucket,
|
||||
inBucket ? config.getValue() : config.getDefaultValue());
|
||||
}), globalConfigStream).collect(Collectors.toList()));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
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;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
@Path("/v1/backup")
|
||||
public class SecureBackupController {
|
||||
@@ -30,7 +28,7 @@ public class SecureBackupController {
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ExternalServiceCredentials getAuth(@Auth Account account) {
|
||||
return backupServiceCredentialGenerator.generateFor(account.getUuid().toString());
|
||||
public ExternalServiceCredentials getAuth(@Auth AuthenticatedAccount auth) {
|
||||
return backupServiceCredentialGenerator.generateFor(auth.getAccount().getUuid().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
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;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
@Path("/v1/storage")
|
||||
public class SecureStorageController {
|
||||
@@ -30,7 +28,7 @@ public class SecureStorageController {
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ExternalServiceCredentials getAuth(@Auth Account account) {
|
||||
return storageServiceCredentialGenerator.generateFor(account.getUuid().toString());
|
||||
public ExternalServiceCredentials getAuth(@Auth AuthenticatedAccount auth) {
|
||||
return storageServiceCredentialGenerator.generateFor(auth.getAccount().getUuid().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
public class ServerRejectedException extends Exception {
|
||||
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes.StickerPackFormUploadItem;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -23,11 +18,15 @@ import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes.StickerPackFormUploadItem;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Hex;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
@Path("/v1/sticker")
|
||||
public class StickerController {
|
||||
@@ -45,30 +44,31 @@ public class StickerController {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/pack/form/{count}")
|
||||
public StickerPackFormUploadAttributes getStickersForm(@Auth Account account,
|
||||
@PathParam("count") @Min(1) @Max(201) int stickerCount)
|
||||
throws RateLimitExceededException
|
||||
{
|
||||
rateLimiters.getStickerPackLimiter().validate(account.getNumber());
|
||||
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
String packId = generatePackId();
|
||||
String packLocation = "stickers/" + packId;
|
||||
String manifestKey = packLocation + "/manifest.proto";
|
||||
Pair<String, String> manifestPolicy = policyGenerator.createFor(now, manifestKey, Constants.MAXIMUM_STICKER_MANIFEST_SIZE_BYTES);
|
||||
String manifestSignature = policySigner.getSignature(now, manifestPolicy.second());
|
||||
StickerPackFormUploadItem manifest = new StickerPackFormUploadItem(-1, manifestKey, manifestPolicy.first(), "private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), manifestPolicy.second(), manifestSignature);
|
||||
public StickerPackFormUploadAttributes getStickersForm(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("count") @Min(1) @Max(201) int stickerCount)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiters.getStickerPackLimiter().validate(auth.getAccount().getUuid());
|
||||
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
String packId = generatePackId();
|
||||
String packLocation = "stickers/" + packId;
|
||||
String manifestKey = packLocation + "/manifest.proto";
|
||||
Pair<String, String> manifestPolicy = policyGenerator.createFor(now, manifestKey,
|
||||
Constants.MAXIMUM_STICKER_MANIFEST_SIZE_BYTES);
|
||||
String manifestSignature = policySigner.getSignature(now, manifestPolicy.second());
|
||||
StickerPackFormUploadItem manifest = new StickerPackFormUploadItem(-1, manifestKey, manifestPolicy.first(),
|
||||
"private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), manifestPolicy.second(), manifestSignature);
|
||||
|
||||
List<StickerPackFormUploadItem> stickers = new LinkedList<>();
|
||||
|
||||
for (int i=0;i<stickerCount;i++) {
|
||||
String stickerKey = packLocation + "/full/" + i;
|
||||
Pair<String, String> stickerPolicy = policyGenerator.createFor(now, stickerKey, Constants.MAXIMUM_STICKER_SIZE_BYTES);
|
||||
String stickerSignature = policySigner.getSignature(now, stickerPolicy.second());
|
||||
for (int i = 0; i < stickerCount; i++) {
|
||||
String stickerKey = packLocation + "/full/" + i;
|
||||
Pair<String, String> stickerPolicy = policyGenerator.createFor(now, stickerKey,
|
||||
Constants.MAXIMUM_STICKER_SIZE_BYTES);
|
||||
String stickerSignature = policySigner.getSignature(now, stickerPolicy.second());
|
||||
stickers.add(new StickerPackFormUploadItem(i, stickerKey, stickerPolicy.first(), "private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), stickerPolicy.second(), stickerSignature));
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), stickerPolicy.second(), stickerSignature));
|
||||
}
|
||||
|
||||
return new StickerPackFormUploadAttributes(packId, manifest, stickers);
|
||||
|
||||
@@ -0,0 +1,920 @@
|
||||
/*
|
||||
* 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.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.Strings;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.InvoiceLineItem;
|
||||
import com.stripe.model.Subscription;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import java.math.BigDecimal;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.InternalServerErrorException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialRequest;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialResponse;
|
||||
import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||
import org.whispersystems.textsecuregcm.badges.LevelTranslator;
|
||||
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult;
|
||||
import org.whispersystems.textsecuregcm.stripe.StripeManager;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
@Path("/v1/subscription")
|
||||
public class SubscriptionController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class);
|
||||
|
||||
private final Clock clock;
|
||||
private final SubscriptionConfiguration subscriptionConfiguration;
|
||||
private final BoostConfiguration boostConfiguration;
|
||||
private final SubscriptionManager subscriptionManager;
|
||||
private final StripeManager stripeManager;
|
||||
private final ServerZkReceiptOperations zkReceiptOperations;
|
||||
private final IssuedReceiptsManager issuedReceiptsManager;
|
||||
private final BadgeTranslator badgeTranslator;
|
||||
private final LevelTranslator levelTranslator;
|
||||
|
||||
public SubscriptionController(
|
||||
@Nonnull Clock clock,
|
||||
@Nonnull SubscriptionConfiguration subscriptionConfiguration,
|
||||
@Nonnull BoostConfiguration boostConfiguration,
|
||||
@Nonnull SubscriptionManager subscriptionManager,
|
||||
@Nonnull StripeManager stripeManager,
|
||||
@Nonnull ServerZkReceiptOperations zkReceiptOperations,
|
||||
@Nonnull IssuedReceiptsManager issuedReceiptsManager,
|
||||
@Nonnull BadgeTranslator badgeTranslator,
|
||||
@Nonnull LevelTranslator levelTranslator) {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
this.subscriptionConfiguration = Objects.requireNonNull(subscriptionConfiguration);
|
||||
this.boostConfiguration = Objects.requireNonNull(boostConfiguration);
|
||||
this.subscriptionManager = Objects.requireNonNull(subscriptionManager);
|
||||
this.stripeManager = Objects.requireNonNull(stripeManager);
|
||||
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
|
||||
this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager);
|
||||
this.badgeTranslator = Objects.requireNonNull(badgeTranslator);
|
||||
this.levelTranslator = Objects.requireNonNull(levelTranslator);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/{subscriberId}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> deleteSubscriber(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId) {
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||
.thenCompose(getResult -> {
|
||||
if (getResult == GetResult.NOT_STORED || getResult == GetResult.PASSWORD_MISMATCH) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
String customerId = getResult.record.customerId;
|
||||
if (Strings.isNullOrEmpty(customerId)) {
|
||||
throw new InternalServerErrorException("no customer id found");
|
||||
}
|
||||
return stripeManager.getCustomer(customerId).thenCompose(customer -> {
|
||||
if (customer == null) {
|
||||
throw new InternalServerErrorException("no customer record found for id " + customerId);
|
||||
}
|
||||
return stripeManager.listNonCanceledSubscriptions(customer);
|
||||
}).thenCompose(subscriptions -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
CompletableFuture<Subscription>[] futures = (CompletableFuture<Subscription>[]) subscriptions.stream()
|
||||
.map(stripeManager::cancelSubscriptionAtEndOfCurrentPeriod).toArray(CompletableFuture[]::new);
|
||||
return CompletableFuture.allOf(futures);
|
||||
});
|
||||
})
|
||||
.thenCompose(unused -> subscriptionManager.canceledAt(requestData.subscriberUser, requestData.now))
|
||||
.thenApply(unused -> Response.ok().build());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/{subscriberId}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> updateSubscriber(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId) {
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||
.thenCompose(getResult -> {
|
||||
if (getResult == GetResult.PASSWORD_MISMATCH) {
|
||||
throw new ForbiddenException("subscriberId mismatch");
|
||||
} else if (getResult == GetResult.NOT_STORED) {
|
||||
// create a customer and write it to ddb
|
||||
return stripeManager.createCustomer(requestData.subscriberUser).thenCompose(
|
||||
customer -> subscriptionManager.create(
|
||||
requestData.subscriberUser, requestData.hmac, customer.getId(), requestData.now)
|
||||
.thenApply(updatedRecord -> {
|
||||
if (updatedRecord == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return updatedRecord;
|
||||
}));
|
||||
} else {
|
||||
// already exists so just touch access time and return
|
||||
return subscriptionManager.accessedAt(requestData.subscriberUser, requestData.now)
|
||||
.thenApply(unused -> getResult.record);
|
||||
}
|
||||
})
|
||||
.thenApply(record -> Response.ok().build());
|
||||
}
|
||||
|
||||
public static class CreatePaymentMethodResponse {
|
||||
|
||||
private final String clientSecret;
|
||||
|
||||
@JsonCreator
|
||||
public CreatePaymentMethodResponse(
|
||||
@JsonProperty("clientSecret") String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public String getClientSecret() {
|
||||
return clientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/{subscriberId}/create_payment_method")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> createPaymentMethod(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId) {
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||
.thenApply(this::requireRecordFromGetResult)
|
||||
.thenCompose(record -> stripeManager.createSetupIntent(record.customerId))
|
||||
.thenApply(setupIntent -> Response.ok(new CreatePaymentMethodResponse(setupIntent.getClientSecret())).build());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/{subscriberId}/default_payment_method/{paymentMethodId}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> setDefaultPaymentMethod(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@PathParam("paymentMethodId") @NotEmpty String paymentMethodId) {
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||
.thenApply(this::requireRecordFromGetResult)
|
||||
.thenCompose(record -> stripeManager.setDefaultPaymentMethodForCustomer(record.customerId, paymentMethodId))
|
||||
.thenApply(customer -> Response.ok().build());
|
||||
}
|
||||
|
||||
public static class SetSubscriptionLevelSuccessResponse {
|
||||
|
||||
private final long level;
|
||||
|
||||
@JsonCreator
|
||||
public SetSubscriptionLevelSuccessResponse(
|
||||
@JsonProperty("level") long level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public long getLevel() {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SetSubscriptionLevelErrorResponse {
|
||||
|
||||
public static class Error {
|
||||
|
||||
public enum Type {
|
||||
UNSUPPORTED_LEVEL,
|
||||
UNSUPPORTED_CURRENCY,
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final String message;
|
||||
|
||||
@JsonCreator
|
||||
public Error(
|
||||
@JsonProperty("type") Type type,
|
||||
@JsonProperty("message") String message) {
|
||||
this.type = type;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Error> errors;
|
||||
|
||||
@JsonCreator
|
||||
public SetSubscriptionLevelErrorResponse(
|
||||
@JsonProperty("errors") List<Error> errors) {
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public List<Error> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/{subscriberId}/level/{level}/{currency}/{idempotencyKey}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> setSubscriptionLevel(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@PathParam("level") long level,
|
||||
@PathParam("currency") String currency,
|
||||
@PathParam("idempotencyKey") String idempotencyKey) {
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||
.thenApply(this::requireRecordFromGetResult)
|
||||
.thenCompose(record -> {
|
||||
SubscriptionLevelConfiguration levelConfiguration = subscriptionConfiguration.getLevels().get(level);
|
||||
if (levelConfiguration == null) {
|
||||
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
||||
.entity(new SetSubscriptionLevelErrorResponse(List.of(
|
||||
new SetSubscriptionLevelErrorResponse.Error(
|
||||
SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL, null))))
|
||||
.build());
|
||||
}
|
||||
SubscriptionPriceConfiguration priceConfiguration = levelConfiguration.getPrices()
|
||||
.get(currency.toLowerCase(Locale.ROOT));
|
||||
if (priceConfiguration == null) {
|
||||
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
||||
.entity(new SetSubscriptionLevelErrorResponse(List.of(
|
||||
new SetSubscriptionLevelErrorResponse.Error(
|
||||
SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_CURRENCY, null))))
|
||||
.build());
|
||||
}
|
||||
|
||||
if (record.subscriptionId == null) {
|
||||
long lastSubscriptionCreatedAt =
|
||||
record.subscriptionCreatedAt != null ? record.subscriptionCreatedAt.getEpochSecond() : 0;
|
||||
// we don't have one yet so create it and then record the subscription id
|
||||
//
|
||||
// this relies on stripe's idempotency key to avoid creating more than one subscription if the client
|
||||
// retries this request
|
||||
return stripeManager.createSubscription(record.customerId, priceConfiguration.getId(), level,
|
||||
lastSubscriptionCreatedAt)
|
||||
.thenCompose(subscription -> subscriptionManager.subscriptionCreated(
|
||||
requestData.subscriberUser, subscription.getId(), requestData.now, level)
|
||||
.thenApply(unused -> subscription));
|
||||
} else {
|
||||
// we already have a subscription in our records so let's check the level and change it if needed
|
||||
return stripeManager.getSubscription(record.subscriptionId).thenCompose(
|
||||
subscription -> stripeManager.getLevelForSubscription(subscription).thenCompose(existingLevel -> {
|
||||
if (level == existingLevel) {
|
||||
return CompletableFuture.completedFuture(subscription);
|
||||
}
|
||||
return stripeManager.updateSubscription(
|
||||
subscription, priceConfiguration.getId(), level, idempotencyKey)
|
||||
.thenCompose(updatedSubscription ->
|
||||
subscriptionManager.subscriptionLevelChanged(requestData.subscriberUser, requestData.now,
|
||||
level)
|
||||
.thenApply(unused -> updatedSubscription));
|
||||
}));
|
||||
}
|
||||
})
|
||||
.thenApply(subscription -> Response.ok(new SetSubscriptionLevelSuccessResponse(level)).build());
|
||||
}
|
||||
|
||||
public static class GetLevelsResponse {
|
||||
|
||||
public static class Level {
|
||||
|
||||
private final String name;
|
||||
private final Badge badge;
|
||||
private final Map<String, BigDecimal> currencies;
|
||||
|
||||
@JsonCreator
|
||||
public Level(
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("badge") Badge badge,
|
||||
@JsonProperty("currencies") Map<String, BigDecimal> currencies) {
|
||||
this.name = name;
|
||||
this.badge = badge;
|
||||
this.currencies = currencies;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Badge getBadge() {
|
||||
return badge;
|
||||
}
|
||||
|
||||
public Map<String, BigDecimal> getCurrencies() {
|
||||
return currencies;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Long, Level> levels;
|
||||
|
||||
@JsonCreator
|
||||
public GetLevelsResponse(
|
||||
@JsonProperty("levels") Map<Long, Level> levels) {
|
||||
this.levels = levels;
|
||||
}
|
||||
|
||||
public Map<Long, Level> getLevels() {
|
||||
return levels;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/levels")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getLevels(@Context ContainerRequestContext containerRequestContext) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
|
||||
GetLevelsResponse getLevelsResponse = new GetLevelsResponse(
|
||||
subscriptionConfiguration.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey,
|
||||
entry -> new GetLevelsResponse.Level(
|
||||
levelTranslator.translate(acceptableLanguages, entry.getValue().getBadge()),
|
||||
badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()),
|
||||
entry.getValue().getPrices().entrySet().stream().collect(
|
||||
Collectors.toMap(levelEntry -> levelEntry.getKey().toUpperCase(Locale.ROOT),
|
||||
levelEntry -> levelEntry.getValue().getAmount()))))));
|
||||
return Response.ok(getLevelsResponse).build();
|
||||
});
|
||||
}
|
||||
|
||||
public static class GetBoostBadgesResponse {
|
||||
public static class Level {
|
||||
private final Badge badge;
|
||||
|
||||
@JsonCreator
|
||||
public Level(
|
||||
@JsonProperty("badge") Badge badge) {
|
||||
this.badge = badge;
|
||||
}
|
||||
|
||||
public Badge getBadge() {
|
||||
return badge;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Long, Level> levels;
|
||||
|
||||
@JsonCreator
|
||||
public GetBoostBadgesResponse(
|
||||
@JsonProperty("levels") Map<Long, Level> levels) {
|
||||
this.levels = Objects.requireNonNull(levels);
|
||||
}
|
||||
|
||||
public Map<Long, Level> getLevels() {
|
||||
return levels;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/boost/badges")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getBoostBadges(@Context ContainerRequestContext containerRequestContext) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
long boostLevel = boostConfiguration.getLevel();
|
||||
String boostBadge = boostConfiguration.getBadge();
|
||||
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
|
||||
GetBoostBadgesResponse getBoostBadgesResponse = new GetBoostBadgesResponse(Map.of(boostLevel,
|
||||
new GetBoostBadgesResponse.Level(badgeTranslator.translate(acceptableLanguages, boostBadge))));
|
||||
return Response.ok(getBoostBadgesResponse).build();
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/boost/amounts")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getBoostAmounts() {
|
||||
return CompletableFuture.supplyAsync(() -> Response.ok(
|
||||
boostConfiguration.getCurrencies().entrySet().stream().collect(
|
||||
Collectors.toMap(entry -> entry.getKey().toUpperCase(Locale.ROOT), Entry::getValue))).build());
|
||||
}
|
||||
|
||||
public static class CreateBoostRequest {
|
||||
|
||||
private final String currency;
|
||||
private final long amount;
|
||||
|
||||
@JsonCreator
|
||||
public CreateBoostRequest(
|
||||
@JsonProperty("currency") String currency,
|
||||
@JsonProperty("amount") long amount) {
|
||||
this.currency = currency;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
@ExactlySize(3)
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
@Min(1)
|
||||
public long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CreateBoostResponse {
|
||||
|
||||
private final String clientSecret;
|
||||
|
||||
@JsonCreator
|
||||
public CreateBoostResponse(
|
||||
@JsonProperty("clientSecret") String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public String getClientSecret() {
|
||||
return clientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/boost/create")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> createBoostPaymentIntent(CreateBoostRequest request) {
|
||||
return stripeManager.createPaymentIntent(request.getCurrency(), request.getAmount())
|
||||
.thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build());
|
||||
}
|
||||
|
||||
public static class CreateBoostReceiptCredentialsRequest {
|
||||
|
||||
private final String paymentIntentId;
|
||||
private final byte[] receiptCredentialRequest;
|
||||
|
||||
@JsonCreator
|
||||
public CreateBoostReceiptCredentialsRequest(
|
||||
@JsonProperty("paymentIntentId") String paymentIntentId,
|
||||
@JsonProperty("receiptCredentialRequest") byte[] receiptCredentialRequest) {
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.receiptCredentialRequest = receiptCredentialRequest;
|
||||
}
|
||||
|
||||
public String getPaymentIntentId() {
|
||||
return paymentIntentId;
|
||||
}
|
||||
|
||||
public byte[] getReceiptCredentialRequest() {
|
||||
return receiptCredentialRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CreateBoostReceiptCredentialsResponse {
|
||||
|
||||
private final byte[] receiptCredentialResponse;
|
||||
|
||||
@JsonCreator
|
||||
public CreateBoostReceiptCredentialsResponse(
|
||||
@JsonProperty("receiptCredentialResponse") byte[] receiptCredentialResponse) {
|
||||
this.receiptCredentialResponse = receiptCredentialResponse;
|
||||
}
|
||||
|
||||
public byte[] getReceiptCredentialResponse() {
|
||||
return receiptCredentialResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/boost/receipt_credentials")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> createBoostReceiptCredentials(CreateBoostReceiptCredentialsRequest request) {
|
||||
return stripeManager.getPaymentIntent(request.getPaymentIntentId())
|
||||
.thenCompose(paymentIntent -> {
|
||||
if (paymentIntent == null) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
if (!"succeeded".equalsIgnoreCase(paymentIntent.getStatus())) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
ReceiptCredentialRequest receiptCredentialRequest;
|
||||
try {
|
||||
receiptCredentialRequest = new ReceiptCredentialRequest(request.getReceiptCredentialRequest());
|
||||
} catch (InvalidInputException e) {
|
||||
throw new BadRequestException("invalid receipt credential request", e);
|
||||
}
|
||||
return issuedReceiptsManager.recordIssuance(paymentIntent.getId(), receiptCredentialRequest, clock.instant())
|
||||
.thenApply(unused -> {
|
||||
Instant expiration = Instant.ofEpochSecond(paymentIntent.getCreated())
|
||||
.plus(boostConfiguration.getExpiration())
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.plus(1, ChronoUnit.DAYS);
|
||||
ReceiptCredentialResponse receiptCredentialResponse;
|
||||
try {
|
||||
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
|
||||
receiptCredentialRequest, expiration.getEpochSecond(), boostConfiguration.getLevel());
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new BadRequestException("receipt credential request failed verification", e);
|
||||
}
|
||||
return Response.ok(new CreateBoostReceiptCredentialsResponse(receiptCredentialResponse.serialize()))
|
||||
.build();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static class GetSubscriptionInformationResponse {
|
||||
|
||||
public static class Subscription {
|
||||
|
||||
private final long level;
|
||||
private final Instant billingCycleAnchor;
|
||||
private final Instant endOfCurrentPeriod;
|
||||
private final boolean active;
|
||||
private final boolean cancelAtPeriodEnd;
|
||||
private final String currency;
|
||||
private final BigDecimal amount;
|
||||
|
||||
public Subscription(
|
||||
@JsonProperty("level") long level,
|
||||
@JsonProperty("billingCycleAnchor") Instant billingCycleAnchor,
|
||||
@JsonProperty("endOfCurrentPeriod") Instant endOfCurrentPeriod,
|
||||
@JsonProperty("active") boolean active,
|
||||
@JsonProperty("cancelAtPeriodEnd") boolean cancelAtPeriodEnd,
|
||||
@JsonProperty("currency") String currency,
|
||||
@JsonProperty("amount") BigDecimal amount) {
|
||||
this.level = level;
|
||||
this.billingCycleAnchor = billingCycleAnchor;
|
||||
this.endOfCurrentPeriod = endOfCurrentPeriod;
|
||||
this.active = active;
|
||||
this.cancelAtPeriodEnd = cancelAtPeriodEnd;
|
||||
this.currency = currency;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public long getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public Instant getBillingCycleAnchor() {
|
||||
return billingCycleAnchor;
|
||||
}
|
||||
|
||||
public Instant getEndOfCurrentPeriod() {
|
||||
return endOfCurrentPeriod;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public boolean isCancelAtPeriodEnd() {
|
||||
return cancelAtPeriodEnd;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
|
||||
private final Subscription subscription;
|
||||
|
||||
@JsonCreator
|
||||
public GetSubscriptionInformationResponse(
|
||||
@JsonProperty("subscription") Subscription subscription) {
|
||||
this.subscription = subscription;
|
||||
}
|
||||
|
||||
public Subscription getSubscription() {
|
||||
return subscription;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/{subscriberId}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getSubscriptionInformation(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId) {
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||
.thenApply(this::requireRecordFromGetResult)
|
||||
.thenCompose(record -> {
|
||||
if (record.subscriptionId == null) {
|
||||
return CompletableFuture.completedFuture(Response.ok(new GetSubscriptionInformationResponse(null)).build());
|
||||
}
|
||||
return stripeManager.getSubscription(record.subscriptionId).thenCompose(subscription ->
|
||||
stripeManager.getPriceForSubscription(subscription).thenCompose(price ->
|
||||
stripeManager.getLevelForPrice(price).thenApply(level -> Response.ok(
|
||||
new GetSubscriptionInformationResponse(new GetSubscriptionInformationResponse.Subscription(
|
||||
level,
|
||||
Instant.ofEpochSecond(subscription.getBillingCycleAnchor()),
|
||||
Instant.ofEpochSecond(subscription.getCurrentPeriodEnd()),
|
||||
Objects.equals(subscription.getStatus(), "active"),
|
||||
subscription.getCancelAtPeriodEnd(),
|
||||
price.getCurrency().toUpperCase(Locale.ROOT),
|
||||
price.getUnitAmountDecimal()
|
||||
))).build())));
|
||||
});
|
||||
}
|
||||
|
||||
public static class GetReceiptCredentialsRequest {
|
||||
|
||||
private final byte[] receiptCredentialRequest;
|
||||
|
||||
@JsonCreator
|
||||
public GetReceiptCredentialsRequest(
|
||||
@JsonProperty("receiptCredentialRequest") byte[] receiptCredentialRequest) {
|
||||
this.receiptCredentialRequest = receiptCredentialRequest;
|
||||
}
|
||||
|
||||
@ExactlySize(ReceiptCredentialRequest.SIZE)
|
||||
public byte[] getReceiptCredentialRequest() {
|
||||
return receiptCredentialRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GetReceiptCredentialsResponse {
|
||||
|
||||
private final byte[] receiptCredentialResponse;
|
||||
|
||||
@JsonCreator
|
||||
public GetReceiptCredentialsResponse(
|
||||
@JsonProperty("receiptCredentialResponse") byte[] receiptCredentialResponse) {
|
||||
this.receiptCredentialResponse = receiptCredentialResponse;
|
||||
}
|
||||
|
||||
@ExactlySize(ReceiptCredentialResponse.SIZE)
|
||||
public byte[] getReceiptCredentialResponse() {
|
||||
return receiptCredentialResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/{subscriberId}/receipt_credentials")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getReceiptCredentials(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@Valid GetReceiptCredentialsRequest request) {
|
||||
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
|
||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||
.thenApply(this::requireRecordFromGetResult)
|
||||
.thenCompose(record -> {
|
||||
if (record.subscriptionId == null) {
|
||||
return CompletableFuture.completedFuture(Response.noContent().build());
|
||||
}
|
||||
ReceiptCredentialRequest receiptCredentialRequest;
|
||||
try {
|
||||
receiptCredentialRequest = new ReceiptCredentialRequest(request.getReceiptCredentialRequest());
|
||||
} catch (InvalidInputException e) {
|
||||
throw new BadRequestException("invalid receipt credential request", e);
|
||||
}
|
||||
return stripeManager.getPaidInvoicesForSubscription(record.subscriptionId, requestData.now)
|
||||
.thenCompose(invoices -> checkNextInvoice(invoices.iterator(), record.subscriptionId))
|
||||
.thenCompose(receipt -> {
|
||||
if (receipt == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
return issuedReceiptsManager.recordIssuance(
|
||||
receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now)
|
||||
.thenApply(unused -> receipt);
|
||||
})
|
||||
.thenApply(receipt -> {
|
||||
if (receipt == null) {
|
||||
return Response.noContent().build();
|
||||
} else {
|
||||
ReceiptCredentialResponse receiptCredentialResponse;
|
||||
try {
|
||||
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
|
||||
receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel());
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new BadRequestException("receipt credential request failed verification", e);
|
||||
}
|
||||
return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static class Receipt {
|
||||
|
||||
private final Instant expiration;
|
||||
private final long level;
|
||||
private final String invoiceLineItemId;
|
||||
|
||||
public Receipt(Instant expiration, long level, String invoiceLineItemId) {
|
||||
this.expiration = expiration;
|
||||
this.level = level;
|
||||
this.invoiceLineItemId = invoiceLineItemId;
|
||||
}
|
||||
|
||||
public Instant getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public long getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public String getInvoiceLineItemId() {
|
||||
return invoiceLineItemId;
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Receipt> checkNextInvoice(Iterator<Invoice> invoiceIterator, String subscriptionId) {
|
||||
if (!invoiceIterator.hasNext()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Invoice invoice = invoiceIterator.next();
|
||||
return stripeManager.getInvoiceLineItemsForInvoice(invoice).thenCompose(invoiceLineItems -> {
|
||||
Collection<InvoiceLineItem> subscriptionLineItems = invoiceLineItems.stream()
|
||||
.filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType()))
|
||||
.collect(Collectors.toList());
|
||||
if (subscriptionLineItems.isEmpty()) {
|
||||
return checkNextInvoice(invoiceIterator, subscriptionId);
|
||||
}
|
||||
if (subscriptionLineItems.size() > 1) {
|
||||
throw new IllegalStateException("invoice has more than one subscription; subscriptionId=" + subscriptionId
|
||||
+ "; count=" + subscriptionLineItems.size());
|
||||
}
|
||||
|
||||
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
|
||||
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
|
||||
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
|
||||
.plus(subscriptionConfiguration.getBadgeGracePeriod())
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.plus(1, ChronoUnit.DAYS),
|
||||
stripeManager.getLevelForProduct(product),
|
||||
subscriptionLineItem.getId()));
|
||||
});
|
||||
}
|
||||
|
||||
private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) {
|
||||
if (getResult == GetResult.PASSWORD_MISMATCH) {
|
||||
throw new ForbiddenException("subscriberId mismatch");
|
||||
} else if (getResult == GetResult.NOT_STORED) {
|
||||
throw new NotFoundException();
|
||||
} else {
|
||||
return getResult.record;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Locale> getAcceptableLanguagesForRequest(ContainerRequestContext containerRequestContext) {
|
||||
try {
|
||||
return containerRequestContext.getAcceptableLanguages();
|
||||
} catch (final ProcessingException e) {
|
||||
logger.warn("Could not get acceptable languages", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestData {
|
||||
|
||||
public final byte[] subscriberBytes;
|
||||
public final byte[] subscriberUser;
|
||||
public final byte[] subscriberKey;
|
||||
public final byte[] hmac;
|
||||
public final Instant now;
|
||||
|
||||
private RequestData(
|
||||
@Nonnull byte[] subscriberBytes,
|
||||
@Nonnull byte[] subscriberUser,
|
||||
@Nonnull byte[] subscriberKey,
|
||||
@Nonnull byte[] hmac,
|
||||
@Nonnull Instant now) {
|
||||
this.subscriberBytes = Objects.requireNonNull(subscriberBytes);
|
||||
this.subscriberUser = Objects.requireNonNull(subscriberUser);
|
||||
this.subscriberKey = Objects.requireNonNull(subscriberKey);
|
||||
this.hmac = Objects.requireNonNull(hmac);
|
||||
this.now = Objects.requireNonNull(now);
|
||||
}
|
||||
|
||||
public static RequestData process(
|
||||
Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
String subscriberId,
|
||||
Clock clock) {
|
||||
Instant now = clock.instant();
|
||||
if (authenticatedAccount.isPresent()) {
|
||||
throw new ForbiddenException("must not use authenticated connection for subscriber operations");
|
||||
}
|
||||
byte[] subscriberBytes = convertSubscriberIdStringToBytes(subscriberId);
|
||||
byte[] subscriberUser = getUser(subscriberBytes);
|
||||
byte[] subscriberKey = getKey(subscriberBytes);
|
||||
byte[] hmac = computeHmac(subscriberUser, subscriberKey);
|
||||
return new RequestData(subscriberBytes, subscriberUser, subscriberKey, hmac, now);
|
||||
}
|
||||
|
||||
private static byte[] convertSubscriberIdStringToBytes(String subscriberId) {
|
||||
try {
|
||||
byte[] bytes = Base64.getUrlDecoder().decode(subscriberId);
|
||||
if (bytes.length != 32) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return bytes;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new NotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getUser(byte[] subscriberBytes) {
|
||||
byte[] user = new byte[16];
|
||||
System.arraycopy(subscriberBytes, 0, user, 0, user.length);
|
||||
return user;
|
||||
}
|
||||
|
||||
private static byte[] getKey(byte[] subscriberBytes) {
|
||||
byte[] key = new byte[16];
|
||||
System.arraycopy(subscriberBytes, 16, key, 0, key.length);
|
||||
return key;
|
||||
}
|
||||
|
||||
private static byte[] computeHmac(byte[] subscriberUser, byte[] subscriberKey) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(subscriberKey, "HmacSHA256"));
|
||||
return mac.doFinal(subscriberUser);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new InternalServerErrorException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.whispersystems.textsecuregcm.entities.CurrencyConversionEntityList;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -34,8 +35,8 @@ public class CurrencyConversionManager implements Managed {
|
||||
private long fixerUpdatedTimestamp;
|
||||
private long ftxUpdatedTimestamp;
|
||||
|
||||
private Map<String, Double> cachedFixerValues;
|
||||
private Map<String, Double> cachedFtxValues;
|
||||
private Map<String, BigDecimal> cachedFixerValues;
|
||||
private Map<String, BigDecimal> cachedFtxValues;
|
||||
|
||||
public CurrencyConversionManager(FixerClient fixerClient, FtxClient ftxClient, List<String> currencies) {
|
||||
this.fixerClient = fixerClient;
|
||||
@@ -75,7 +76,7 @@ public class CurrencyConversionManager implements Managed {
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - ftxUpdatedTimestamp > FTX_INTERVAL || cachedFtxValues == null) {
|
||||
Map<String, Double> cachedFtxValues = new HashMap<>();
|
||||
Map<String, BigDecimal> cachedFtxValues = new HashMap<>();
|
||||
|
||||
for (String currency : currencies) {
|
||||
cachedFtxValues.put(currency, ftxClient.getSpotPrice(currency, "USD"));
|
||||
@@ -87,14 +88,14 @@ public class CurrencyConversionManager implements Managed {
|
||||
|
||||
List<CurrencyConversionEntity> entities = new LinkedList<>();
|
||||
|
||||
for (Map.Entry<String, Double> currency : cachedFtxValues.entrySet()) {
|
||||
double usdValue = currency.getValue();
|
||||
for (Map.Entry<String, BigDecimal> currency : cachedFtxValues.entrySet()) {
|
||||
BigDecimal usdValue = stripTrailingZerosAfterDecimal(currency.getValue());
|
||||
|
||||
Map<String, Double> values = new HashMap<>();
|
||||
Map<String, BigDecimal> values = new HashMap<>();
|
||||
values.put("USD", usdValue);
|
||||
|
||||
for (Map.Entry<String, Double> conversion : cachedFixerValues.entrySet()) {
|
||||
values.put(conversion.getKey(), conversion.getValue() * usdValue);
|
||||
for (Map.Entry<String, BigDecimal> conversion : cachedFixerValues.entrySet()) {
|
||||
values.put(conversion.getKey(), stripTrailingZerosAfterDecimal(conversion.getValue().multiply(usdValue)));
|
||||
}
|
||||
|
||||
entities.add(new CurrencyConversionEntity(currency.getKey(), values));
|
||||
@@ -104,6 +105,15 @@ public class CurrencyConversionManager implements Managed {
|
||||
this.cached.set(new CurrencyConversionEntityList(entities, ftxUpdatedTimestamp));
|
||||
}
|
||||
|
||||
private BigDecimal stripTrailingZerosAfterDecimal(BigDecimal bigDecimal) {
|
||||
BigDecimal n = bigDecimal.stripTrailingZeros();
|
||||
if (n.scale() < 0) {
|
||||
return n.setScale(0);
|
||||
} else {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setFixerUpdatedTimestamp(long timestamp) {
|
||||
this.fixerUpdatedTimestamp = timestamp;
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
@@ -20,7 +21,7 @@ public class FixerClient {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Map<String, Double> getConversionsForBase(String base) throws FixerException {
|
||||
public Map<String, BigDecimal> getConversionsForBase(String base) throws FixerException {
|
||||
try {
|
||||
URI uri = URI.create("https://data.fixer.io/api/latest?access_key=" + apiKey + "&base=" + base);
|
||||
|
||||
@@ -58,7 +59,7 @@ public class FixerClient {
|
||||
private String date;
|
||||
|
||||
@JsonProperty
|
||||
private Map<String, Double> rates;
|
||||
private Map<String, BigDecimal> rates;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
@@ -18,7 +19,7 @@ public class FtxClient {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public double getSpotPrice(String currency, String base) throws FtxException{
|
||||
public BigDecimal getSpotPrice(String currency, String base) throws FtxException{
|
||||
try {
|
||||
URI uri = URI.create("https://ftx.com/api/markets/" + currency + "/" + base);
|
||||
|
||||
@@ -51,7 +52,7 @@ public class FtxClient {
|
||||
private static class FtxResult {
|
||||
|
||||
@JsonProperty
|
||||
private double price;
|
||||
private BigDecimal price;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@ public class AccountAttributes {
|
||||
@Size(max = 204, message = "This field must be less than 50 characters")
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String pin;
|
||||
|
||||
@JsonProperty
|
||||
private String registrationLock;
|
||||
|
||||
@@ -42,11 +39,11 @@ public class AccountAttributes {
|
||||
public AccountAttributes() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public AccountAttributes(boolean fetchesMessages, int registrationId, String name, String pin, String registrationLock, boolean discoverableByPhoneNumber, final DeviceCapabilities capabilities) {
|
||||
public AccountAttributes(boolean fetchesMessages, int registrationId, String name, String registrationLock,
|
||||
boolean discoverableByPhoneNumber, final DeviceCapabilities capabilities) {
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.name = name;
|
||||
this.pin = pin;
|
||||
this.registrationLock = registrationLock;
|
||||
this.discoverableByPhoneNumber = discoverableByPhoneNumber;
|
||||
this.capabilities = capabilities;
|
||||
@@ -64,10 +61,6 @@ public class AccountAttributes {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPin() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public String getRegistrationLock() {
|
||||
return registrationLock;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
@@ -14,15 +15,22 @@ import java.util.UUID;
|
||||
public class AccountCreationResult {
|
||||
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
private final UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private boolean storageCapable;
|
||||
private final String number;
|
||||
|
||||
public AccountCreationResult() {}
|
||||
@JsonProperty
|
||||
private final boolean storageCapable;
|
||||
|
||||
public AccountCreationResult(UUID uuid, boolean storageCapable) {
|
||||
this.uuid = uuid;
|
||||
@JsonCreator
|
||||
public AccountCreationResult(
|
||||
@JsonProperty("uuid") final UUID uuid,
|
||||
@JsonProperty("number") final String number,
|
||||
@JsonProperty("storageCapable") final boolean storageCapable) {
|
||||
|
||||
this.uuid = uuid;
|
||||
this.number = number;
|
||||
this.storageCapable = storageCapable;
|
||||
}
|
||||
|
||||
@@ -30,6 +38,10 @@ public class AccountCreationResult {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public boolean isStorageCapable() {
|
||||
return storageCapable;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user