Compare commits
155
Commits
dockerfile
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bc00e00e3 | ||
|
|
3e777df86c | ||
|
|
278b4e810d | ||
|
|
346c7cd743 | ||
|
|
867bf97d8f | ||
|
|
8a67949168 | ||
|
|
5baa51d547 | ||
|
|
616db337e1 | ||
|
|
3895871462 | ||
|
|
a87b84fbe2 | ||
|
|
b2f0ace9db | ||
|
|
20c95e2606 | ||
|
|
22dccaeddb | ||
|
|
e611a70ba4 | ||
|
|
66845d7080 | ||
|
|
4ea7278c6f | ||
|
|
2b2e26f14b | ||
|
|
b496ef8d6f | ||
|
|
7f5e83141d | ||
|
|
2d1ca98605 | ||
|
|
eaa4c318e3 | ||
|
|
31373fd1ba | ||
|
|
9086246947 | ||
|
|
7855b70682 | ||
|
|
0ce87153e5 | ||
|
|
dba1711e8d | ||
|
|
a70b057e1c | ||
|
|
9a5ffea0ad | ||
|
|
96f4b771ea | ||
|
|
3df143dd3d | ||
|
|
d78d7c726e | ||
|
|
d0ad580c7d | ||
|
|
4a8a2a70b5 | ||
|
|
20a71b7df2 | ||
|
|
68412b3901 | ||
|
|
93a7c60a15 | ||
|
|
31e5058b15 | ||
|
|
14cff958e9 | ||
|
|
9628f147f1 | ||
|
|
13e346d4eb | ||
|
|
e507ce2f26 | ||
|
|
9c62622733 | ||
|
|
62aa0cef39 | ||
|
|
401953313a | ||
|
|
4d2403d619 | ||
|
|
c5f261305d | ||
|
|
394f58f6cc | ||
|
|
674bf1b0e0 | ||
|
|
606ddd8a9b | ||
|
|
e23a1fac50 | ||
|
|
342323a7e6 | ||
|
|
efb410444b | ||
|
|
17c9b4c5d3 | ||
|
|
706de8e2f1 | ||
|
|
23bc11f3b6 | ||
|
|
4eb7dde1c8 | ||
|
|
064861b930 | ||
|
|
afa910bbd7 | ||
|
|
6aceb24fd2 | ||
|
|
d94e86781f | ||
|
|
0d4a3b1ad4 | ||
|
|
acfcb18f29 | ||
|
|
f7ff8e3837 | ||
|
|
048e17c62b | ||
|
|
d89b4f7e95 | ||
|
|
795b226b90 | ||
|
|
e485c380e0 | ||
|
|
bb4f4bc441 | ||
|
|
65b49b2d9c | ||
|
|
9e7010f185 | ||
|
|
3bb8e5bb00 | ||
|
|
2a4d1da2ca | ||
|
|
6b71b66bd2 | ||
|
|
ebf24fb125 | ||
|
|
46d64b949e | ||
|
|
6919354520 | ||
|
|
a42fe9bfb0 | ||
|
|
ee1f8b34ea | ||
|
|
c910fa406d | ||
|
|
559205e33f | ||
|
|
c0756e9c60 | ||
|
|
bf1190696e | ||
|
|
71dd0890de | ||
|
|
e5acdf1402 | ||
|
|
0f08b6bb59 | ||
|
|
6198a7b69a | ||
|
|
067aee6664 | ||
|
|
138a2ebbd0 | ||
|
|
296f6a7a88 | ||
|
|
069ffa9921 | ||
|
|
f42fd8a840 | ||
|
|
10f27af6f2 | ||
|
|
0bbd34d060 | ||
|
|
282daeb0dc | ||
|
|
d33b313c11 | ||
|
|
fb7316c9ae | ||
|
|
279b0a51d9 | ||
|
|
6547d5ebf3 | ||
|
|
4f1ef9a039 | ||
|
|
4c80714d19 | ||
|
|
077ead71a5 | ||
|
|
caba110266 | ||
|
|
0fdb23c1e9 | ||
|
|
13a84f0c72 | ||
|
|
669bd58e33 | ||
|
|
6e82740a9b | ||
|
|
7ea43a728d | ||
|
|
71b38356b1 | ||
|
|
5a99708f56 | ||
|
|
24191d9599 | ||
|
|
482ea8eb40 | ||
|
|
1dae05651f | ||
|
|
5164e92538 | ||
|
|
f89a20dbc7 | ||
|
|
3a4c5a2bfb | ||
|
|
5e1334e8de | ||
|
|
fa6e3d3690 | ||
|
|
9383e7716b | ||
|
|
cfe34fbf0f | ||
|
|
9fe110625c | ||
|
|
975f753c2b | ||
|
|
e6237480f8 | ||
|
|
966d4e29d4 | ||
|
|
26f876a2cb | ||
|
|
ab9e6ac48a | ||
|
|
c1d6c04ab2 | ||
|
|
888cec3d56 | ||
|
|
1461bcc2c2 | ||
|
|
11f1cf80bd | ||
|
|
c675cc8b26 | ||
|
|
0011b8925b | ||
|
|
73ea6e4251 | ||
|
|
e4441dddbb | ||
|
|
8d1d56f694 | ||
|
|
2015ba77ca | ||
|
|
7033a0f68f | ||
|
|
6ada76da7f | ||
|
|
cbdec0cb22 | ||
|
|
de6e9d31c9 | ||
|
|
f0a6be32fc | ||
|
|
5c4855cca6 | ||
|
|
2e1e380418 | ||
|
|
d07f0b4f71 | ||
|
|
aaa2a6eef1 | ||
|
|
b1f56c3324 | ||
|
|
da5c0ae4b6 | ||
|
|
1e1394560d | ||
|
|
bae0196bcf | ||
|
|
3398955c1a | ||
|
|
a1b925d1e0 | ||
|
|
31c0c3275f | ||
|
|
0a4392f700 | ||
|
|
eb86986cf4 | ||
|
|
1053a47e42 | ||
|
|
99b1f48e0e |
@@ -8,11 +8,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@3bc31aaf88e8fc94dc1e632d48af61be5ca8721c
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: 11
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
- name: Build with Maven
|
||||
run: mvn -e -B verify
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Note that the implmentation of the abusive message filter is private; internal
|
||||
# Note that the implementation of the abusive message filter is private; internal
|
||||
# developers will need to override this URL with:
|
||||
#
|
||||
# ```
|
||||
|
||||
+1
-1
Submodule abusive-message-filter updated: 56398ec89d...a4d0ece167
@@ -21,6 +21,19 @@
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<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>
|
||||
|
||||
<modules>
|
||||
<module>redis-dispatch</module>
|
||||
<module>websocket-resources</module>
|
||||
@@ -34,7 +47,7 @@
|
||||
<commons-codec.version>1.15</commons-codec.version>
|
||||
<commons-csv.version>1.8</commons-csv.version>
|
||||
<commons-io.version>2.9.0</commons-io.version>
|
||||
<dropwizard.version>2.0.22</dropwizard.version>
|
||||
<dropwizard.version>2.0.26</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>
|
||||
@@ -44,7 +57,7 @@
|
||||
<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>
|
||||
<mockito.version>4.0.0</mockito.version>
|
||||
<netty.version>4.1.65.Final</netty.version>
|
||||
<netty.tcnative-boringssl-static.version>2.0.39.Final</netty.tcnative-boringssl-static.version>
|
||||
<opentest4j.version>1.2.0</opentest4j.version>
|
||||
@@ -253,6 +266,26 @@
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>embedded-redis</artifactId>
|
||||
<version>0.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.zonky.test.postgres</groupId>
|
||||
<artifactId>embedded-postgres-binaries-bom</artifactId>
|
||||
<version>11.13.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-bom</artifactId>
|
||||
<version>2.17.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -266,7 +299,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>2.31.0</version>
|
||||
<version>2.32.0</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@@ -337,6 +370,7 @@
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:3.18.0:exe:${os.detected.classifier}</protocArtifact>
|
||||
<checkStaleness>true</checkStaleness>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -353,8 +387,7 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
<release>17</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -418,7 +451,7 @@
|
||||
<rules>
|
||||
<dependencyConvergence/>
|
||||
<requireMavenVersion>
|
||||
<version>3.0.0</version>
|
||||
<version>3.8.3</version>
|
||||
</requireMavenVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
|
||||
+275
-185
@@ -1,59 +1,99 @@
|
||||
# Example, relatively minimal, configuration that passes validation (see `io.dropwizard.cli.CheckCommand`)
|
||||
#
|
||||
# `unset` values will need to be set to work properly.
|
||||
# Most other values are technically valid for a local/demonstration environment, but are probably not production-ready.
|
||||
|
||||
stripe:
|
||||
apiKey:
|
||||
idempotencyKeyGenerator:
|
||||
apiKey: unset
|
||||
idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
|
||||
boostDescription: >
|
||||
Example
|
||||
|
||||
dynamoDbClientConfiguration:
|
||||
region: # AWS Region
|
||||
region: us-west-2 # AWS Region
|
||||
|
||||
dynamoDbTables:
|
||||
accounts:
|
||||
tableName: Example_Accounts
|
||||
phoneNumberTableName: Example_Accounts_PhoneNumbers
|
||||
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
|
||||
usernamesTableName: Example_Accounts_Usernames
|
||||
scanPageSize: 100
|
||||
deletedAccounts:
|
||||
tableName: Example_DeletedAccounts
|
||||
needsReconciliationIndexName: NeedsReconciliation
|
||||
deletedAccountsLock:
|
||||
tableName: Example_DeletedAccountsLock
|
||||
issuedReceipts:
|
||||
tableName: # DDB Table Name
|
||||
expiration: # Duration of time until rows expire
|
||||
generator: # random binary sequence
|
||||
tableName: Example_IssuedReceipts
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
generator: abcdefg12345678= # random base64-encoded binary sequence
|
||||
keys:
|
||||
tableName: Example_Keys
|
||||
messages:
|
||||
tableName: Example_Messages
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
pendingAccounts:
|
||||
tableName: Example_PendingAccounts
|
||||
pendingDevices:
|
||||
tableName: Example_PendingDevices
|
||||
phoneNumberIdentifiers:
|
||||
tableName: Example_PhoneNumberIdentifiers
|
||||
profiles:
|
||||
tableName: Example_Profiles
|
||||
pushChallenge:
|
||||
tableName: Example_PushChallenge
|
||||
redeemedReceipts:
|
||||
tableName: # DDB Table Name
|
||||
expiration: # Duration of time until rows expire
|
||||
tableName: Example_RedeemedReceipts
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
remoteConfig:
|
||||
tableName: Example_RemoteConfig
|
||||
reportMessage:
|
||||
tableName: Example_ReportMessage
|
||||
reservedUsernames:
|
||||
tableName: Example_ReservedUsernames
|
||||
subscriptions:
|
||||
tableName: # DDB Table Name
|
||||
tableName: Example_Subscriptions
|
||||
|
||||
twilio: # Twilio gateway configuration
|
||||
accountId:
|
||||
accountToken:
|
||||
nanpaMessagingServiceSid: # Twilio SID for the messaging service to use for NANPA.
|
||||
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
|
||||
verifyServiceSid: # Twilio SID for a Verify service
|
||||
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
|
||||
accountId: unset
|
||||
accountToken: unset
|
||||
nanpaMessagingServiceSid: unset # Twilio SID for the messaging service to use for NANPA.
|
||||
messagingServiceSid: unset # Twilio SID for the message service to use for non-NANPA.
|
||||
verifyServiceSid: unset # Twilio SID for a Verify service
|
||||
localDomain: example.com # Domain Twilio can connect back to for calls. Should be domain of your service.
|
||||
defaultClientVerificationTexts:
|
||||
ios: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||
androidNg: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202001: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202103: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
generic: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||
ios: example %1$s # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||
androidNg: example %1$s # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202001: example %1$s # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
android202103: example %1$s # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
||||
generic: example %1$s # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||
regionalClientVerificationTexts: # Map of country codes to custom texts
|
||||
999: # example country code
|
||||
ios:
|
||||
# … all keys from defaultClientVerificationTexts are required
|
||||
androidAppHash: # Hash appended to Android
|
||||
verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable
|
||||
ios: example %1$s # all keys from defaultClientVerificationTexts are required
|
||||
androidNg: example %1$s
|
||||
android202001: example %1$s
|
||||
android202103: example %1$s
|
||||
generic: example %1$s
|
||||
androidAppHash: example # Hash appended to Android
|
||||
verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable
|
||||
|
||||
push:
|
||||
queueSize: # Size of push pending queue
|
||||
queueSize: 1000 # Size of push pending queue
|
||||
|
||||
turn: # TURN server configuration
|
||||
secret: # TURN server secret
|
||||
secret: example # TURN server secret
|
||||
uris:
|
||||
- stun:yourdomain:80
|
||||
- stun:yourdomain.com:443
|
||||
- turn:yourdomain:443?transport=udp
|
||||
- turn:etc.com:80?transport=udp
|
||||
- stun:example.com:80
|
||||
- stun:another.example.com:443
|
||||
- turn:example.com:443?transport=udp
|
||||
- turn:ya.example.com:80?transport=udp
|
||||
|
||||
cacheCluster: # Redis server configuration for cache cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
clientPresenceCluster: # Redis server configuration for client presence cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
pubsub: # Redis server configuration for pubsub cluster
|
||||
url: redis://redis.example.com:6379/
|
||||
@@ -61,148 +101,141 @@ pubsub: # Redis server configuration for pubsub cluster
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
rateLimitersCluster: # Redis server configuration for rate limiters cluster
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
directory:
|
||||
client: # Configuration for interfacing with Contact Discovery Service cluster
|
||||
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
|
||||
userAuthenticationTokenUserIdSecret: # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
|
||||
userAuthenticationTokenSharedSecret: 00000f # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
|
||||
userAuthenticationTokenUserIdSecret: 00000f # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
|
||||
sqs:
|
||||
accessKey: # AWS SQS accessKey
|
||||
accessSecret: # AWS SQS accessSecret
|
||||
queueUrls: # AWS SQS queue urls
|
||||
accessKey: test # AWS SQS accessKey
|
||||
accessSecret: test # AWS SQS accessSecret
|
||||
queueUrls: # AWS SQS queue urls
|
||||
- https://sqs.example.com/directory.fifo
|
||||
server: # One or more CDS servers
|
||||
- replicationName: # CDS replication name
|
||||
replicationUrl: # CDS replication endpoint base url
|
||||
replicationPassword: # CDS replication endpoint password
|
||||
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
|
||||
- replicationName: example # CDS replication name
|
||||
replicationUrl: cds.example.com # CDS replication endpoint base url
|
||||
replicationPassword: example # CDS replication endpoint password
|
||||
replicationCaCertificate: | # CDS replication endpoint TLS certificate trust root
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
directoryV2:
|
||||
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
|
||||
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes:
|
||||
|
||||
persistDelayMinutes: 1
|
||||
cluster:
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
metricsCluster:
|
||||
urls:
|
||||
- redis://redis.example.com:6379/
|
||||
|
||||
messageDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
keysDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
accountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
phoneNumberTableName:
|
||||
|
||||
deletedAccountsDynamoDb: # DynamoDb table configuration
|
||||
region:
|
||||
tableName:
|
||||
needsReconciliationIndexName:
|
||||
|
||||
deletedAccountsLockDynamoDb: # DynamoDb table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
redeemedReceiptsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
expirationTime: # ISO8601 Duration
|
||||
|
||||
migrationDeletedAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
migrationRetryAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
pendingAccountsDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
pendingDevicesDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
pushChallengeDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
|
||||
reportMessageDynamoDb: # DynamoDB table configuration
|
||||
region:
|
||||
tableName:
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
awsAttachments: # AWS S3 configuration
|
||||
accessKey:
|
||||
accessSecret:
|
||||
bucket:
|
||||
region:
|
||||
accessKey: test
|
||||
accessSecret: test
|
||||
bucket: aws-attachments
|
||||
region: us-west-2
|
||||
|
||||
gcpAttachments: # GCP Storage configuration
|
||||
domain:
|
||||
email:
|
||||
maxSizeInBytes:
|
||||
domain: example.com
|
||||
email: user@example.cocm
|
||||
maxSizeInBytes: 1024
|
||||
pathPrefix:
|
||||
rsaSigningKey:
|
||||
rsaSigningKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
abuseDatabase: # Postgresql database configuration
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
|
||||
accountsDatabase: # Postgresql database configuration
|
||||
driverClass: org.postgresql.Driver
|
||||
user:
|
||||
password:
|
||||
url:
|
||||
user: example
|
||||
password: password
|
||||
url: jdbc:postgresql://example.com:5432/abusedb
|
||||
|
||||
accountDatabaseCrawler:
|
||||
chunkSize: # accounts per run
|
||||
chunkIntervalMs: # time per run
|
||||
|
||||
dynamoDbMigrationCrawler:
|
||||
chunkSize: # accounts per run
|
||||
chunkIntervalMs: # time per run
|
||||
chunkSize: 10 # accounts per run
|
||||
chunkIntervalMs: 60000 # time per run
|
||||
|
||||
apn: # Apple Push Notifications configuration
|
||||
sandbox: true
|
||||
bundleId:
|
||||
keyId:
|
||||
teamId:
|
||||
signingKey:
|
||||
bundleId: com.example.textsecuregcm
|
||||
keyId: unset
|
||||
teamId: unset
|
||||
signingKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
gcm: # GCM Configuration
|
||||
senderId:
|
||||
apiKey:
|
||||
senderId: 123456789
|
||||
apiKey: unset
|
||||
|
||||
cdn:
|
||||
accessKey: # AWS Access Key ID
|
||||
accessSecret: # AWS Access Secret
|
||||
bucket: # S3 Bucket name
|
||||
region: # AWS region
|
||||
accessKey: test # AWS Access Key ID
|
||||
accessSecret: test # AWS Access Secret
|
||||
bucket: cdn # S3 Bucket name
|
||||
region: us-west-2 # AWS region
|
||||
|
||||
datadog:
|
||||
apiKey:
|
||||
environment:
|
||||
apiKey: unset
|
||||
environment: dev
|
||||
|
||||
unidentifiedDelivery:
|
||||
certificate:
|
||||
privateKey:
|
||||
expiresDays:
|
||||
certificate: ABCD1234
|
||||
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
||||
expiresDays: 7
|
||||
|
||||
voiceVerification:
|
||||
url: https://cdn-ca.signal.org/verification/
|
||||
@@ -210,33 +243,74 @@ voiceVerification:
|
||||
- en
|
||||
|
||||
recaptcha:
|
||||
secret:
|
||||
secret: unset
|
||||
|
||||
recaptchaV2:
|
||||
siteKey:
|
||||
scoreFloor:
|
||||
projectPath:
|
||||
credentialConfigurationJson:
|
||||
siteKey: unset
|
||||
scoreFloor: 1.0
|
||||
projectPath: projects/example
|
||||
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||
|
||||
storageService:
|
||||
uri:
|
||||
userAuthenticationTokenSharedSecret:
|
||||
storageCaCertificate:
|
||||
uri: storage.example.com
|
||||
userAuthenticationTokenSharedSecret: 00000f
|
||||
storageCaCertificate: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
backupService:
|
||||
uri:
|
||||
userAuthenticationTokenSharedSecret:
|
||||
backupCaCertificate:
|
||||
uri: backup.example.com
|
||||
userAuthenticationTokenSharedSecret: 00000f
|
||||
backupCaCertificate: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
zkConfig:
|
||||
serverPublic:
|
||||
serverSecret:
|
||||
enabled:
|
||||
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
|
||||
appConfig:
|
||||
application:
|
||||
environment:
|
||||
configuration:
|
||||
application: example
|
||||
environment: example
|
||||
configuration: example
|
||||
|
||||
remoteConfig:
|
||||
authorizedTokens:
|
||||
@@ -245,48 +319,64 @@ remoteConfig:
|
||||
- # ...
|
||||
- # Nth authorized token
|
||||
globalConfig: # keys and values that are given to clients on GET /v1/config
|
||||
|
||||
EXAMPLE_KEY: VALUE
|
||||
|
||||
paymentsService:
|
||||
userAuthenticationTokenSharedSecret: # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
fixerApiKey:
|
||||
userAuthenticationTokenSharedSecret: 0000000f0000000f0000000f0000000f0000000f0000000f0000000f0000000f # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
fixerApiKey: unset
|
||||
paymentCurrencies:
|
||||
-
|
||||
|
||||
torExitNodeList:
|
||||
s3Region:
|
||||
s3Bucket:
|
||||
objectKey:
|
||||
maxSize:
|
||||
|
||||
asnTable:
|
||||
s3Region:
|
||||
s3Bucket:
|
||||
objectKey:
|
||||
maxSize:
|
||||
# list of symbols for supported currencies
|
||||
- MOB
|
||||
|
||||
donation:
|
||||
uri: # value
|
||||
uri: donation.example.com # value
|
||||
supportedCurrencies:
|
||||
- # 1st supported currency
|
||||
- # 2nd supported currency
|
||||
- # ...
|
||||
- # Nth supported currency
|
||||
circuitBreaker:
|
||||
failureRateThreshold: # value
|
||||
ringBufferSizeInHalfOpenState: # value
|
||||
ringBufferSizeInClosedState: # value
|
||||
waitDurationInOpenStateInSeconds: # value
|
||||
retry:
|
||||
maxAttempts: # value
|
||||
waitDuration: # value
|
||||
|
||||
badges:
|
||||
badges:
|
||||
- id: TEST
|
||||
imageUrl: https://example.com/test-badge
|
||||
category: other
|
||||
sprites: # exactly 6
|
||||
- sprite-1.png
|
||||
- sprite-2.png
|
||||
- sprite-3.png
|
||||
- sprite-4.png
|
||||
- sprite-5.png
|
||||
- sprite-6.png
|
||||
svg: example.svg
|
||||
svgs:
|
||||
- light: example-light.svg
|
||||
dark: example-dark.svg
|
||||
badgeIdsEnabledForAll:
|
||||
- TEST
|
||||
receiptLevels:
|
||||
'1': TEST
|
||||
|
||||
subscription: # configuration for Stripe subscriptions
|
||||
badgeGracePeriod: P15D
|
||||
levels:
|
||||
500:
|
||||
badge: EXAMPLE
|
||||
prices:
|
||||
# list of ISO 4217 currency codes and amounts for the given badge level
|
||||
xts:
|
||||
amount: '10'
|
||||
id: price_example # stripe ID
|
||||
|
||||
boost:
|
||||
level: 1
|
||||
expiration: P90D
|
||||
badge: EXAMPLE
|
||||
currencies:
|
||||
# ISO 4217 currency codes and amounts in those currencies
|
||||
xts:
|
||||
- '1'
|
||||
- '2'
|
||||
- '4'
|
||||
- '8'
|
||||
- '20'
|
||||
- '40'
|
||||
|
||||
+26
-18
@@ -10,19 +10,6 @@
|
||||
<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>
|
||||
@@ -55,7 +42,7 @@
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>zkgroup-java</artifactId>
|
||||
<version>0.8.2</version>
|
||||
<version>0.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems</groupId>
|
||||
@@ -382,9 +369,9 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.opentable.components</groupId>
|
||||
<artifactId>otj-pg-embedded</artifactId>
|
||||
<version>0.13.3</version>
|
||||
<groupId>io.zonky.test</groupId>
|
||||
<artifactId>embedded-postgres</artifactId>
|
||||
<version>1.3.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -420,7 +407,6 @@
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>embedded-redis</artifactId>
|
||||
<version>0.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -593,6 +579,28 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>check-all-service-config</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<mainClass>org.whispersystems.textsecuregcm.CheckServiceConfigurations</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
<arguments>
|
||||
<argument>${project.basedir}/config</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
+10
-95
@@ -15,8 +15,6 @@ 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;
|
||||
@@ -25,17 +23,15 @@ 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.PaymentsServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
@@ -125,6 +121,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private DirectoryConfiguration directory;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DirectoryV2Configuration directoryV2;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -150,51 +151,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration clientPresenceCluster;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private MessageDynamoDbConfiguration messageDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration keysDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private AccountsDynamoDbConfiguration accountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DeletedAccountsDynamoDbConfiguration deletedAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration deletedAccountsLockDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration pushChallengeDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration reportMessageDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration pendingAccountsDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private DynamoDbConfiguration pendingDevicesDynamoDb;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -210,11 +166,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@JsonProperty
|
||||
private List<MaxDeviceConfiguration> maxDevices = new LinkedList<>();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private AccountsDatabaseConfiguration accountsDatabase;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -390,6 +341,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public DirectoryV2Configuration getDirectoryV2Configuration() {
|
||||
return directoryV2;
|
||||
}
|
||||
|
||||
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
|
||||
return storageService;
|
||||
}
|
||||
@@ -414,34 +369,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return rateLimitersCluster;
|
||||
}
|
||||
|
||||
public MessageDynamoDbConfiguration getMessageDynamoDbConfiguration() {
|
||||
return messageDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getKeysDynamoDbConfiguration() {
|
||||
return keysDynamoDb;
|
||||
}
|
||||
|
||||
public AccountsDynamoDbConfiguration getAccountsDynamoDbConfiguration() {
|
||||
return accountsDynamoDb;
|
||||
}
|
||||
|
||||
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
|
||||
return deletedAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getDeletedAccountsLockDynamoDbConfiguration() {
|
||||
return deletedAccountsLockDynamoDb;
|
||||
}
|
||||
|
||||
public DatabaseConfiguration getAbuseDatabaseConfiguration() {
|
||||
return abuseDatabase;
|
||||
}
|
||||
|
||||
public AccountsDatabaseConfiguration getAccountsDatabaseConfiguration() {
|
||||
return accountsDatabase;
|
||||
}
|
||||
|
||||
public RateLimitsConfiguration getLimitsConfiguration() {
|
||||
return limits;
|
||||
}
|
||||
@@ -516,22 +447,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPushChallengeDynamoDbConfiguration() {
|
||||
return pushChallengeDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getReportMessageDynamoDbConfiguration() {
|
||||
return reportMessageDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPendingAccountsDynamoDbConfiguration() {
|
||||
return pendingAccountsDynamoDb;
|
||||
}
|
||||
|
||||
public DynamoDbConfiguration getPendingDevicesDynamoDbConfiguration() {
|
||||
return pendingDevicesDynamoDb;
|
||||
}
|
||||
|
||||
public DonationConfiguration getDonationConfiguration() {
|
||||
return donation;
|
||||
}
|
||||
|
||||
+142
-138
@@ -24,7 +24,6 @@ import io.dropwizard.auth.PolymorphicAuthDynamicFeature;
|
||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.db.PooledDataSourceFactory;
|
||||
import io.dropwizard.jdbi3.JdbiFactory;
|
||||
import io.dropwizard.setup.Bootstrap;
|
||||
@@ -53,7 +52,6 @@ import java.util.concurrent.TimeUnit;
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.FilterRegistration;
|
||||
import javax.servlet.ServletRegistration;
|
||||
import javax.ws.rs.container.DynamicFeature;
|
||||
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
import org.jdbi.v3.core.Jdbi;
|
||||
@@ -68,6 +66,7 @@ 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.abuse.RateLimitChallengeListener;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||
@@ -80,6 +79,7 @@ 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.AcceptNumericOnlineFlagRequestFilter;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||
@@ -88,6 +88,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;
|
||||
@@ -108,13 +109,13 @@ 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;
|
||||
import org.whispersystems.textsecuregcm.limits.DynamicRateLimiters;
|
||||
import org.whispersystems.textsecuregcm.limits.PushChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitResetMetricsManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeOptionManager;
|
||||
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;
|
||||
@@ -179,12 +180,13 @@ 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.Keys;
|
||||
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.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;
|
||||
@@ -198,8 +200,6 @@ 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.stripe.StripeManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
@@ -211,9 +211,11 @@ import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
|
||||
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.workers.AssignUsernameCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ReserveUsernameCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.SetCrawlerAccelerationTask;
|
||||
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
|
||||
@@ -240,13 +242,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
bootstrap.addCommand(new ServerVersionCommand());
|
||||
bootstrap.addCommand(new CheckDynamicConfigurationCommand());
|
||||
bootstrap.addCommand(new SetUserDiscoverabilityCommand());
|
||||
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
|
||||
return configuration.getAccountsDatabaseConfiguration();
|
||||
}
|
||||
});
|
||||
bootstrap.addCommand(new ReserveUsernameCommand());
|
||||
bootstrap.addCommand(new AssignUsernameCommand());
|
||||
|
||||
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("abusedb", "abusedb.xml") {
|
||||
@Override
|
||||
@@ -310,96 +307,79 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
headerControlledResourceBundleLookup);
|
||||
|
||||
JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY);
|
||||
Jdbi accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb");
|
||||
Jdbi abuseJdbi = jdbiFactory.build(environment, config.getAbuseDatabaseConfiguration(), "abusedb" );
|
||||
Jdbi abuseJdbi = jdbiFactory.build(environment, config.getAbuseDatabaseConfiguration(), "abusedb");
|
||||
|
||||
FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("accounts_database", accountJdbi, config.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration());
|
||||
FaultTolerantDatabase abuseDatabase = new FaultTolerantDatabase("abuse_database", abuseJdbi, config.getAbuseDatabaseConfiguration().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());
|
||||
|
||||
DynamoDbClient preKeyDynamoDb = DynamoDbFromConfig.client(config.getKeysDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pushChallengeDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getPushChallengeDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient reportMessageDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getReportMessageDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pendingAccountsDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getPendingAccountsDynamoDbConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
DynamoDbClient pendingDevicesDynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getPendingDevicesDynamoDbConfiguration(),
|
||||
DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(
|
||||
config.getDynamoDbClientConfiguration(),
|
||||
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
|
||||
|
||||
AmazonDynamoDB deletedAccountsLockDynamoDbClient = AmazonDynamoDBClientBuilder.standard()
|
||||
.withRegion(config.getDeletedAccountsLockDynamoDbConfiguration().getRegion())
|
||||
.withRegion(config.getDynamoDbClientConfiguration().getRegion())
|
||||
.withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(
|
||||
((int) config.getDeletedAccountsLockDynamoDbConfiguration().getClientExecutionTimeout().toMillis()))
|
||||
((int) config.getDynamoDbClientConfiguration().getClientExecutionTimeout().toMillis()))
|
||||
.withRequestTimeout(
|
||||
(int) config.getDeletedAccountsLockDynamoDbConfiguration().getClientRequestTimeout().toMillis()))
|
||||
(int) config.getDynamoDbClientConfiguration().getClientRequestTimeout().toMillis()))
|
||||
.withCredentials(InstanceProfileCredentialsProvider.getInstance())
|
||||
.build();
|
||||
|
||||
DeletedAccounts deletedAccounts = new DeletedAccounts(deletedAccountsDynamoDbClient,
|
||||
config.getDeletedAccountsDynamoDbConfiguration().getTableName(),
|
||||
config.getDeletedAccountsDynamoDbConfiguration().getNeedsReconciliationIndexName());
|
||||
DeletedAccounts deletedAccounts = new DeletedAccounts(dynamoDbClient,
|
||||
config.getDynamoDbTables().getDeletedAccounts().getTableName(),
|
||||
config.getDynamoDbTables().getDeletedAccounts().getNeedsReconciliationIndexName());
|
||||
|
||||
Accounts accounts = new Accounts(accountsDynamoDbClient,
|
||||
config.getAccountsDynamoDbConfiguration().getTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
|
||||
config.getAccountsDynamoDbConfiguration().getScanPageSize());
|
||||
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());
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
|
||||
config.getAppConfig().getEnvironment(),
|
||||
config.getAppConfig().getConfigurationName(),
|
||||
DynamicConfiguration.class);
|
||||
|
||||
Accounts accounts = new Accounts(dynamoDbClient,
|
||||
config.getDynamoDbTables().getAccounts().getTableName(),
|
||||
config.getDynamoDbTables().getAccounts().getPhoneNumberTableName(),
|
||||
config.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(),
|
||||
config.getDynamoDbTables().getAccounts().getUsernamesTableName(),
|
||||
config.getDynamoDbTables().getAccounts().getScanPageSize());
|
||||
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient,
|
||||
config.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName());
|
||||
ReservedUsernames reservedUsernames = new ReservedUsernames(dynamoDbClient,
|
||||
config.getDynamoDbTables().getReservedUsernames().getTableName());
|
||||
Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient,
|
||||
config.getDynamoDbTables().getProfiles().getTableName());
|
||||
Keys keys = new Keys(dynamoDbClient, config.getDynamoDbTables().getKeys().getTableName());
|
||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient,
|
||||
config.getDynamoDbTables().getMessages().getTableName(),
|
||||
config.getDynamoDbTables().getMessages().getExpiration());
|
||||
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(), config.getReportMessageConfiguration().getReportTtl());
|
||||
VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient, config.getPendingAccountsDynamoDbConfiguration().getTableName());
|
||||
VerificationCodeStore pendingDevices = new VerificationCodeStore(pendingDevicesDynamoDbClient, config.getPendingDevicesDynamoDbConfiguration().getTableName());
|
||||
RemoteConfigs remoteConfigs = new RemoteConfigs(dynamoDbClient,
|
||||
config.getDynamoDbTables().getRemoteConfig().getTableName());
|
||||
PushChallengeDynamoDb pushChallengeDynamoDb = new PushChallengeDynamoDb(dynamoDbClient,
|
||||
config.getDynamoDbTables().getPushChallenge().getTableName());
|
||||
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(dynamoDbClient,
|
||||
config.getDynamoDbTables().getReportMessage().getTableName(),
|
||||
config.getReportMessageConfiguration().getReportTtl());
|
||||
VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
|
||||
config.getDynamoDbTables().getPendingAccounts().getTableName());
|
||||
VerificationCodeStore pendingDevices = new VerificationCodeStore(dynamoDbClient,
|
||||
config.getDynamoDbTables().getPendingDevices().getTableName());
|
||||
|
||||
RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
|
||||
ReplicatedJedisPool pubsubClient = pubSubClientFactory.getRedisClientPool();
|
||||
|
||||
ClientResources generalCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources messageCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources presenceClientResources = ClientResources.builder().build();
|
||||
ClientResources metricsCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources pushSchedulerCacheClientResources = ClientResources.builder().ioThreadPoolSize(4).build();
|
||||
ClientResources rateLimitersCacheClientResources = ClientResources.builder().build();
|
||||
ClientResources redisClientResources = ClientResources.builder().build();
|
||||
ConnectionEventLogger.logConnectionEvents(redisClientResources);
|
||||
|
||||
ConnectionEventLogger.logConnectionEvents(generalCacheClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(messageCacheClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(presenceClientResources);
|
||||
ConnectionEventLogger.logConnectionEvents(metricsCacheClientResources);
|
||||
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), generalCacheClientResources);
|
||||
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), messageCacheClientResources);
|
||||
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), presenceClientResources);
|
||||
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), metricsCacheClientResources);
|
||||
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), pushSchedulerCacheClientResources);
|
||||
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), rateLimitersCacheClientResources);
|
||||
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClientResources);
|
||||
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), redisClientResources);
|
||||
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), redisClientResources);
|
||||
|
||||
BlockingQueue<Runnable> keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(10_000);
|
||||
Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(), keyspaceNotificationDispatchQueue);
|
||||
@@ -421,17 +401,14 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
build();
|
||||
|
||||
StripeManager stripeManager = new StripeManager(config.getStripe().getApiKey(), stripeExecutor,
|
||||
config.getStripe().getIdempotencyKeyGenerator());
|
||||
config.getStripe().getIdempotencyKeyGenerator(), config.getStripe().getBoostDescription());
|
||||
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
|
||||
true);
|
||||
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
|
||||
config.getAppConfig().getEnvironment(),
|
||||
config.getAppConfig().getConfigurationName(),
|
||||
DynamicConfiguration.class);
|
||||
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
|
||||
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret());
|
||||
ExternalServiceCredentialGenerator directoryV2CredentialsGenerator = new ExternalServiceCredentialGenerator(
|
||||
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration()
|
||||
.getUserAuthenticationTokenSharedSecret(), false);
|
||||
|
||||
dynamicConfigurationManager.start();
|
||||
|
||||
@@ -440,9 +417,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());
|
||||
@@ -450,24 +430,24 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration());
|
||||
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
|
||||
StoredVerificationCodeManager pendingDevicesManager = new StoredVerificationCodeManager(pendingDevices);
|
||||
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
|
||||
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
|
||||
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor);
|
||||
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
|
||||
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);
|
||||
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
|
||||
deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
|
||||
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager);
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
deletedAccountsLockDynamoDbClient, config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
deletedAccountsManager, directoryQueue, keys, messagesManager, reservedUsernames, 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);
|
||||
APNSender apnSender = new APNSender(apnSenderExecutor, accountsManager, config.getApnConfiguration());
|
||||
GCMSender gcmSender = new GCMSender(gcmSenderExecutor, accountsManager, config.getGcmConfiguration().getApiKey());
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), dynamicConfigurationManager, rateLimitersCluster);
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), rateLimitersCluster);
|
||||
DynamicRateLimiters dynamicRateLimiters = new DynamicRateLimiters(rateLimitersCluster, dynamicConfigurationManager);
|
||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||
IssuedReceiptsManager issuedReceiptsManager = new IssuedReceiptsManager(
|
||||
config.getDynamoDbTables().getIssuedReceipts().getTableName(),
|
||||
@@ -485,11 +465,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
|
||||
RateLimitResetMetricsManager rateLimitResetMetricsManager = new RateLimitResetMetricsManager(metricsCluster, Metrics.globalRegistry);
|
||||
|
||||
UnsealedSenderRateLimiter unsealedSenderRateLimiter = new UnsealedSenderRateLimiter(rateLimiters, rateLimitersCluster, dynamicConfigurationManager, rateLimitResetMetricsManager);
|
||||
PreKeyRateLimiter preKeyRateLimiter = new PreKeyRateLimiter(rateLimiters, dynamicConfigurationManager, rateLimitResetMetricsManager);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerCluster, apnSender, accountsManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration(), dynamicConfigurationManager);
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||
@@ -505,14 +480,13 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
TransitionalRecaptchaClient transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient, enterpriseRecaptchaClient);
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, gcmSender, pushChallengeDynamoDb);
|
||||
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||
transitionalRecaptchaClient, preKeyRateLimiter, unsealedSenderRateLimiter, rateLimiters,
|
||||
dynamicConfigurationManager);
|
||||
transitionalRecaptchaClient, dynamicRateLimiters);
|
||||
RateLimitChallengeOptionManager rateLimitChallengeOptionManager =
|
||||
new RateLimitChallengeOptionManager(dynamicRateLimiters, dynamicConfigurationManager);
|
||||
|
||||
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
|
||||
|
||||
// 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<>();
|
||||
|
||||
final List<AccountDatabaseCrawlerListener> directoryReconciliationAccountDatabaseCrawlerListeners = new ArrayList<>();
|
||||
final List<DeletedAccountsDirectoryReconciler> deletedAccountsDirectoryReconcilers = new ArrayList<>();
|
||||
for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration()
|
||||
.getDirectoryServerConfiguration()) {
|
||||
@@ -522,26 +496,37 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
directoryServerConfiguration.getReplicationName(), directoryReconciliationClient,
|
||||
dynamicConfigurationManager);
|
||||
// reconcilers are read-only
|
||||
accountDatabaseCrawlerListeners.add(directoryReconciler);
|
||||
directoryReconciliationAccountDatabaseCrawlerListeners.add(directoryReconciler);
|
||||
|
||||
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));
|
||||
|
||||
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
||||
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().getFixerApiKey());
|
||||
FtxClient ftxClient = new FtxClient(currencyClient);
|
||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
||||
AccountDatabaseCrawlerCache directoryReconciliationAccountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(
|
||||
cacheCluster, AccountDatabaseCrawlerCache.DIRECTORY_RECONCILER_PREFIX);
|
||||
AccountDatabaseCrawler directoryReconciliationAccountDatabaseCrawler = new AccountDatabaseCrawler(
|
||||
"Reconciliation crawler",
|
||||
accountsManager,
|
||||
directoryReconciliationAccountDatabaseCrawlerCache, directoryReconciliationAccountDatabaseCrawlerListeners,
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkSize(),
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()
|
||||
);
|
||||
|
||||
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster);
|
||||
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager,
|
||||
// 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 = List.of(
|
||||
new NonNormalizedAccountCrawlerListener(accountsManager, metricsCluster),
|
||||
new ContactDiscoveryWriter(accountsManager),
|
||||
// PushFeedbackProcessor may update device properties
|
||||
new PushFeedbackProcessor(accountsManager),
|
||||
// delete accounts last
|
||||
new AccountCleaner(accountsManager)
|
||||
);
|
||||
|
||||
AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheCluster,
|
||||
AccountDatabaseCrawlerCache.GENERAL_PURPOSE_PREFIX);
|
||||
AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler("General-purpose account crawler",
|
||||
accountsManager,
|
||||
accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners,
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkSize(),
|
||||
config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()
|
||||
@@ -549,14 +534,18 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
DeletedAccountsTableCrawler deletedAccountsTableCrawler = new DeletedAccountsTableCrawler(deletedAccountsManager, deletedAccountsDirectoryReconcilers, cacheCluster, recurringJobExecutor);
|
||||
|
||||
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
||||
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().getFixerApiKey());
|
||||
FtxClient ftxClient = new FtxClient(currencyClient);
|
||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
||||
|
||||
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(directoryReconciliationAccountDatabaseCrawler);
|
||||
environment.lifecycle().manage(deletedAccountsTableCrawler);
|
||||
environment.lifecycle().manage(remoteConfigsManager);
|
||||
environment.lifecycle().manage(messagesCache);
|
||||
environment.lifecycle().manage(messagePersister);
|
||||
environment.lifecycle().manage(clientPresenceManager);
|
||||
@@ -590,7 +579,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
.addFilter("RemoteDeprecationFilter", new RemoteDeprecationFilter(dynamicConfigurationManager))
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
|
||||
// TODO Remove on or after 2022-03-01
|
||||
final AcceptNumericOnlineFlagRequestFilter acceptNumericOnlineFlagRequestFilter =
|
||||
new AcceptNumericOnlineFlagRequestFilter("v1/messages/multi_recipient");
|
||||
|
||||
environment.jersey().register(new ContentLengthFilter(TrafficSource.HTTP));
|
||||
environment.jersey().register(acceptNumericOnlineFlagRequestFilter);
|
||||
environment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP));
|
||||
environment.jersey()
|
||||
@@ -612,17 +606,18 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
clientPresenceManager, retrySchedulingExecutor));
|
||||
webSocketEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
webSocketEnvironment.jersey().register(new ContentLengthFilter(TrafficSource.WEBSOCKET));
|
||||
webSocketEnvironment.jersey().register(acceptNumericOnlineFlagRequestFilter);
|
||||
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
|
||||
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET));
|
||||
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||
|
||||
// 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,
|
||||
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
|
||||
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||
transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator,
|
||||
verifyExperimentEnrollmentManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
|
||||
|
||||
final List<Object> commonControllers = Lists.newArrayList(
|
||||
new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()),
|
||||
@@ -630,14 +625,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
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 DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, 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 MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, apnFallbackManager,
|
||||
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 ProfileController(clock, rateLimiters, accountsManager, profilesManager, 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),
|
||||
@@ -677,6 +673,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
log.warn("Abusive message filter {} not annotated with @FilterAbusiveMessages and will not be installed",
|
||||
filter.getClass().getName());
|
||||
}
|
||||
|
||||
if (filter instanceof RateLimitChallengeListener) {
|
||||
log.info("Registered rate limit challenge listener: {}", filter.getClass().getName());
|
||||
rateLimitChallengeManager.addListener((RateLimitChallengeListener) filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!registeredAbusiveMessageFilter) {
|
||||
@@ -693,8 +694,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
registerCorsFilter(environment);
|
||||
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
||||
|
||||
RateLimitChallengeExceptionMapper rateLimitChallengeExceptionMapper = new RateLimitChallengeExceptionMapper(
|
||||
rateLimitChallengeManager);
|
||||
RateLimitChallengeExceptionMapper rateLimitChallengeExceptionMapper =
|
||||
new RateLimitChallengeExceptionMapper(rateLimitChallengeOptionManager);
|
||||
|
||||
environment.jersey().register(rateLimitChallengeExceptionMapper);
|
||||
webSocketEnvironment.jersey().register(rateLimitChallengeExceptionMapper);
|
||||
@@ -723,6 +724,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster));
|
||||
|
||||
environment.lifecycle().manage(new ApplicationShutdownMonitor());
|
||||
|
||||
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge(3, TimeUnit.SECONDS));
|
||||
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
|
||||
environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge());
|
||||
@@ -744,6 +747,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
List.of(
|
||||
new LoggingUnhandledExceptionMapper(),
|
||||
new CompletionExceptionMapper(),
|
||||
new IOExceptionMapper(),
|
||||
new RateLimitExceededExceptionMapper(),
|
||||
new InvalidWebsocketAddressExceptionMapper(),
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.abuse;
|
||||
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface RateLimitChallengeListener {
|
||||
|
||||
void handleRateLimitChallengeAnswered(Account account);
|
||||
|
||||
/**
|
||||
* Configures this rate limit challenge listener. This method will be called before the service begins processing any
|
||||
* challenges.
|
||||
*
|
||||
* @param environmentName the name of the environment in which this listener is running (e.g. "staging" or "production")
|
||||
* @throws IOException if the listener could not read its configuration source for any reason
|
||||
*/
|
||||
void configure(String environmentName) throws IOException;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.abuse;
|
||||
|
||||
public enum RateLimitChallengeType {
|
||||
|
||||
PUSH_CHALLENGE,
|
||||
RECAPTCHA
|
||||
}
|
||||
+1
-1
@@ -76,7 +76,7 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
|
||||
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
|
||||
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||
|
||||
return accountsManager.get((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
final Set<Long> deviceIdsToDisplace;
|
||||
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
|
||||
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ public class BaseAccountAuthenticator {
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
}
|
||||
|
||||
Optional<Account> account = accountsManager.get(accountUuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
failureReason = "noSuchAccount";
|
||||
|
||||
+42
-68
@@ -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");
|
||||
|
||||
@@ -8,6 +8,8 @@ package org.whispersystems.textsecuregcm.auth;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
@@ -36,9 +38,9 @@ public class OptionalAccess {
|
||||
}
|
||||
|
||||
if (requestAccount.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
throw new NotFoundException();
|
||||
} else {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -56,7 +58,7 @@ public class OptionalAccess {
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (requestAccount.isPresent() && (targetAccount.isEmpty() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
|
||||
@@ -72,7 +74,7 @@ public class OptionalAccess {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-8
@@ -5,10 +5,14 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import javax.ws.rs.container.ResourceInfo;
|
||||
import javax.ws.rs.core.Context;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent.Type;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEventListener;
|
||||
@@ -16,11 +20,6 @@ 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;
|
||||
@@ -60,7 +59,7 @@ public class WebsocketRefreshRequestEventListener implements RequestEventListene
|
||||
.forEach(pair -> {
|
||||
try {
|
||||
displacedDevices.incrementAndGet();
|
||||
clientPresenceManager.displacePresence(pair.first(), pair.second());
|
||||
clientPresenceManager.disconnectPresence(pair.first(), pair.second());
|
||||
} catch (final Exception e) {
|
||||
logger.error("Could not displace device presence", e);
|
||||
}
|
||||
|
||||
-23
@@ -1,23 +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 javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class AccountsDatabaseConfiguration extends DatabaseConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private RetryConfiguration keyOperationRetry = new RetryConfiguration();
|
||||
|
||||
public RetryConfiguration getKeyOperationRetryConfiguration() {
|
||||
return keyOperationRetry;
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
|
||||
@NotNull
|
||||
private String phoneNumberTableName;
|
||||
|
||||
private int scanPageSize = 100;
|
||||
|
||||
@JsonProperty
|
||||
public String getPhoneNumberTableName() {
|
||||
return phoneNumberTableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public int getScanPageSize() {
|
||||
return scanPageSize;
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables.Table;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
public class AccountsTableConfiguration extends Table {
|
||||
|
||||
private final String phoneNumberTableName;
|
||||
private final String phoneNumberIdentifierTableName;
|
||||
private final String usernamesTableName;
|
||||
private final int scanPageSize;
|
||||
|
||||
@JsonCreator
|
||||
public AccountsTableConfiguration(
|
||||
@JsonProperty("tableName") final String tableName,
|
||||
@JsonProperty("phoneNumberTableName") final String phoneNumberTableName,
|
||||
@JsonProperty("phoneNumberIdentifierTableName") final String phoneNumberIdentifierTableName,
|
||||
@JsonProperty("usernamesTableName") final String usernamesTableName,
|
||||
@JsonProperty("scanPageSize") final int scanPageSize) {
|
||||
|
||||
super(tableName);
|
||||
|
||||
this.phoneNumberTableName = phoneNumberTableName;
|
||||
this.phoneNumberIdentifierTableName = phoneNumberIdentifierTableName;
|
||||
this.usernamesTableName = usernamesTableName;
|
||||
this.scanPageSize = scanPageSize;
|
||||
}
|
||||
|
||||
@NotBlank
|
||||
public String getPhoneNumberTableName() {
|
||||
return phoneNumberTableName;
|
||||
}
|
||||
|
||||
@NotBlank
|
||||
public String getPhoneNumberIdentifierTableName() {
|
||||
return phoneNumberIdentifierTableName;
|
||||
}
|
||||
|
||||
@NotBlank
|
||||
public String getUsernamesTableName() {
|
||||
return usernamesTableName;
|
||||
}
|
||||
|
||||
public int getScanPageSize() {
|
||||
return scanPageSize;
|
||||
}
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DeletedAccountsDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
|
||||
@NotNull
|
||||
private String needsReconciliationIndexName;
|
||||
|
||||
public String getNeedsReconciliationIndexName() {
|
||||
return needsReconciliationIndexName;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables.Table;
|
||||
|
||||
public class DeletedAccountsTableConfiguration extends Table {
|
||||
|
||||
private final String needsReconciliationIndexName;
|
||||
|
||||
@JsonCreator
|
||||
public DeletedAccountsTableConfiguration(
|
||||
@JsonProperty("tableName") final String tableName,
|
||||
@JsonProperty("needsReconciliationIndexName") final String needsReconciliationIndexName) {
|
||||
|
||||
super(tableName);
|
||||
this.needsReconciliationIndexName = needsReconciliationIndexName;
|
||||
}
|
||||
|
||||
@NotBlank
|
||||
public String getNeedsReconciliationIndexName() {
|
||||
return needsReconciliationIndexName;
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
-44
@@ -1,44 +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 javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.time.Duration;
|
||||
|
||||
public class DynamoDbConfiguration {
|
||||
|
||||
private String region;
|
||||
private String tableName;
|
||||
private Duration clientExecutionTimeout = Duration.ofSeconds(30);
|
||||
private Duration clientRequestTimeout = Duration.ofSeconds(10);
|
||||
|
||||
@Valid
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotEmpty
|
||||
@JsonProperty
|
||||
public String getTableName() {
|
||||
return tableName;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public Duration getClientExecutionTimeout() {
|
||||
return clientExecutionTimeout;
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
public Duration getClientRequestTimeout() {
|
||||
return clientRequestTimeout;
|
||||
}
|
||||
}
|
||||
+121
-4
@@ -46,34 +46,151 @@ public class DynamoDbTables {
|
||||
}
|
||||
}
|
||||
|
||||
private final AccountsTableConfiguration accounts;
|
||||
private final DeletedAccountsTableConfiguration deletedAccounts;
|
||||
private final Table deletedAccountsLock;
|
||||
private final IssuedReceiptsTableConfiguration issuedReceipts;
|
||||
private final Table keys;
|
||||
private final TableWithExpiration messages;
|
||||
private final Table pendingAccounts;
|
||||
private final Table pendingDevices;
|
||||
private final Table phoneNumberIdentifiers;
|
||||
private final Table profiles;
|
||||
private final Table pushChallenge;
|
||||
private final TableWithExpiration redeemedReceipts;
|
||||
private final Table remoteConfig;
|
||||
private final Table reportMessage;
|
||||
private final Table reservedUsernames;
|
||||
private final Table subscriptions;
|
||||
|
||||
@JsonCreator
|
||||
public DynamoDbTables(
|
||||
@JsonProperty("accounts") final AccountsTableConfiguration accounts,
|
||||
@JsonProperty("deletedAccounts") final DeletedAccountsTableConfiguration deletedAccounts,
|
||||
@JsonProperty("deletedAccountsLock") final Table deletedAccountsLock,
|
||||
@JsonProperty("issuedReceipts") final IssuedReceiptsTableConfiguration issuedReceipts,
|
||||
@JsonProperty("keys") final Table keys,
|
||||
@JsonProperty("messages") final TableWithExpiration messages,
|
||||
@JsonProperty("pendingAccounts") final Table pendingAccounts,
|
||||
@JsonProperty("pendingDevices") final Table pendingDevices,
|
||||
@JsonProperty("phoneNumberIdentifiers") final Table phoneNumberIdentifiers,
|
||||
@JsonProperty("profiles") final Table profiles,
|
||||
@JsonProperty("pushChallenge") final Table pushChallenge,
|
||||
@JsonProperty("redeemedReceipts") final TableWithExpiration redeemedReceipts,
|
||||
@JsonProperty("remoteConfig") final Table remoteConfig,
|
||||
@JsonProperty("reportMessage") final Table reportMessage,
|
||||
@JsonProperty("reservedUsernames") final Table reservedUsernames,
|
||||
@JsonProperty("subscriptions") final Table subscriptions) {
|
||||
|
||||
this.accounts = accounts;
|
||||
this.deletedAccounts = deletedAccounts;
|
||||
this.deletedAccountsLock = deletedAccountsLock;
|
||||
this.issuedReceipts = issuedReceipts;
|
||||
this.keys = keys;
|
||||
this.messages = messages;
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.pendingDevices = pendingDevices;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
this.profiles = profiles;
|
||||
this.pushChallenge = pushChallenge;
|
||||
this.redeemedReceipts = redeemedReceipts;
|
||||
this.remoteConfig = remoteConfig;
|
||||
this.reportMessage = reportMessage;
|
||||
this.reservedUsernames = reservedUsernames;
|
||||
this.subscriptions = subscriptions;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@Valid
|
||||
public AccountsTableConfiguration getAccounts() {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public DeletedAccountsTableConfiguration getDeletedAccounts() {
|
||||
return deletedAccounts;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getDeletedAccountsLock() {
|
||||
return deletedAccountsLock;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public IssuedReceiptsTableConfiguration getIssuedReceipts() {
|
||||
return issuedReceipts;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public TableWithExpiration getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getPendingAccounts() {
|
||||
return pendingAccounts;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getPendingDevices() {
|
||||
return pendingDevices;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getPhoneNumberIdentifiers() {
|
||||
return phoneNumberIdentifiers;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getPushChallenge() {
|
||||
return pushChallenge;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public TableWithExpiration getRedeemedReceipts() {
|
||||
return redeemedReceipts;
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getRemoteConfig() {
|
||||
return remoteConfig;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getReportMessage() {
|
||||
return reportMessage;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getReservedUsernames() {
|
||||
return reservedUsernames;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getSubscriptions() {
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.time.Duration;
|
||||
import javax.validation.Valid;
|
||||
|
||||
public class MessageDynamoDbConfiguration extends DynamoDbConfiguration {
|
||||
|
||||
private Duration timeToLive = Duration.ofDays(14);
|
||||
|
||||
@Valid
|
||||
public Duration getTimeToLive() {
|
||||
return timeToLive;
|
||||
}
|
||||
}
|
||||
+7
@@ -63,6 +63,9 @@ public class RateLimitsConfiguration {
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration usernameSet = new RateLimitConfiguration(100, 100 / (24.0 * 60.0));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration checkAccountExistence = new RateLimitConfiguration(1_000, 1_000 / 60.0);
|
||||
|
||||
public RateLimitConfiguration getAutoBlock() {
|
||||
return autoBlock;
|
||||
}
|
||||
@@ -135,6 +138,10 @@ public class RateLimitsConfiguration {
|
||||
return usernameSet;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getCheckAccountExistence() {
|
||||
return checkAccountExistence;
|
||||
}
|
||||
|
||||
public static class RateLimitConfiguration {
|
||||
@JsonProperty
|
||||
private int bucketSize;
|
||||
|
||||
+9
-1
@@ -13,13 +13,16 @@ public class StripeConfiguration {
|
||||
|
||||
private final String apiKey;
|
||||
private final byte[] idempotencyKeyGenerator;
|
||||
private final String boostDescription;
|
||||
|
||||
@JsonCreator
|
||||
public StripeConfiguration(
|
||||
@JsonProperty("apiKey") final String apiKey,
|
||||
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator) {
|
||||
@JsonProperty("idempotencyKeyGenerator") final byte[] idempotencyKeyGenerator,
|
||||
@JsonProperty("boostDescription") final String boostDescription) {
|
||||
this.apiKey = apiKey;
|
||||
this.idempotencyKeyGenerator = idempotencyKeyGenerator;
|
||||
this.boostDescription = boostDescription;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
@@ -31,4 +34,9 @@ public class StripeConfiguration {
|
||||
public byte[] getIdempotencyKeyGenerator() {
|
||||
return idempotencyKeyGenerator;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getBoostDescription() {
|
||||
return boostDescription;
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -51,6 +51,10 @@ public class DynamicConfiguration {
|
||||
@JsonProperty
|
||||
private DynamicDirectoryReconcilerConfiguration directoryReconciler = new DynamicDirectoryReconcilerConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicPushLatencyConfiguration pushLatency = new DynamicPushLatencyConfiguration(Collections.emptyMap());
|
||||
|
||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||
final String experimentName) {
|
||||
return Optional.ofNullable(experiments.get(experimentName));
|
||||
@@ -101,4 +105,9 @@ public class DynamicConfiguration {
|
||||
public DynamicDirectoryReconcilerConfiguration getDirectoryReconcilerConfiguration() {
|
||||
return directoryReconciler;
|
||||
}
|
||||
|
||||
public DynamicPushLatencyConfiguration getPushLatencyConfiguration() {
|
||||
return pushLatency;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-4
@@ -7,14 +7,14 @@ package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
|
||||
public class DynamicPaymentsConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private Set<String> allowedCountryCodes = Collections.emptySet();
|
||||
private List<String> disallowedPrefixes = Collections.emptyList();
|
||||
|
||||
public Set<String> getAllowedCountryCodes() {
|
||||
return allowedCountryCodes;
|
||||
public List<String> getDisallowedPrefixes() {
|
||||
return disallowedPrefixes;
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
-14
@@ -11,12 +11,6 @@ import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DynamicRateLimitChallengeConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private boolean preKeyLimitEnforced = false;
|
||||
|
||||
@JsonProperty
|
||||
boolean unsealedSenderLimitEnforced = false;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Map<ClientPlatform, Semver> clientSupportedVersions = Collections.emptyMap();
|
||||
@@ -29,12 +23,4 @@ public class DynamicRateLimitChallengeConfiguration {
|
||||
public Optional<Semver> getMinimumSupportedVersion(final ClientPlatform platform) {
|
||||
return Optional.ofNullable(clientSupportedVersions.get(platform));
|
||||
}
|
||||
|
||||
public boolean isPreKeyLimitEnforced() {
|
||||
return preKeyLimitEnforced;
|
||||
}
|
||||
|
||||
public boolean isUnsealedSenderLimitEnforced() {
|
||||
return unsealedSenderLimitEnforced;
|
||||
}
|
||||
}
|
||||
|
||||
-35
@@ -7,18 +7,6 @@ import java.time.Duration;
|
||||
|
||||
public class DynamicRateLimitsConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private CardinalityRateLimitConfiguration unsealedSenderNumber = new CardinalityRateLimitConfiguration(100, Duration.ofDays(1));
|
||||
|
||||
@JsonProperty
|
||||
private int unsealedSenderDefaultCardinalityLimit = 100;
|
||||
|
||||
@JsonProperty
|
||||
private int unsealedSenderPermitIncrement = 50;
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration unsealedSenderIp = new RateLimitConfiguration(120, 2.0 / 60);
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration rateLimitReset = new RateLimitConfiguration(2, 2.0 / (60 * 24));
|
||||
|
||||
@@ -34,17 +22,6 @@ public class DynamicRateLimitsConfiguration {
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration pushChallengeSuccess = new RateLimitConfiguration(2, 2.0 / (60 * 24));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration dailyPreKeys = new RateLimitConfiguration(50, 50.0 / (24.0 * 60));
|
||||
|
||||
public RateLimitConfiguration getUnsealedSenderIp() {
|
||||
return unsealedSenderIp;
|
||||
}
|
||||
|
||||
public CardinalityRateLimitConfiguration getUnsealedSenderNumber() {
|
||||
return unsealedSenderNumber;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getRateLimitReset() {
|
||||
return rateLimitReset;
|
||||
}
|
||||
@@ -64,16 +41,4 @@ public class DynamicRateLimitsConfiguration {
|
||||
public RateLimitConfiguration getPushChallengeSuccess() {
|
||||
return pushChallengeSuccess;
|
||||
}
|
||||
|
||||
public int getUnsealedSenderDefaultCardinalityLimit() {
|
||||
return unsealedSenderDefaultCardinalityLimit;
|
||||
}
|
||||
|
||||
public int getUnsealedSenderPermitIncrement() {
|
||||
return unsealedSenderPermitIncrement;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getDailyPreKeys() {
|
||||
return dailyPreKeys;
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import javax.ws.rs.container.PreMatching;
|
||||
|
||||
@PreMatching
|
||||
public class AcceptNumericOnlineFlagRequestFilter implements ContainerRequestFilter {
|
||||
|
||||
private final String pathPrefix;
|
||||
|
||||
public AcceptNumericOnlineFlagRequestFilter(final String pathPrefix) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(final ContainerRequestContext requestContext) throws IOException {
|
||||
if (requestContext.getUriInfo().getPath().startsWith(pathPrefix)) {
|
||||
if ("1".equals(requestContext.getUriInfo().getQueryParameters().getFirst("online"))) {
|
||||
requestContext.setRequestUri(requestContext.getUriInfo().getRequestUriBuilder()
|
||||
.replaceQueryParam("online", "true")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
-28
@@ -15,6 +15,7 @@ import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -22,13 +23,17 @@ 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.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HEAD;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
@@ -36,8 +41,11 @@ import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
@@ -77,12 +85,13 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
|
||||
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.Username;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
@@ -115,7 +124,6 @@ public class AccountController {
|
||||
|
||||
private final StoredVerificationCodeManager pendingAccounts;
|
||||
private final AccountsManager accounts;
|
||||
private final UsernamesManager usernames;
|
||||
private final AbusiveHostRules abusiveHostRules;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final SmsSender smsSender;
|
||||
@@ -131,7 +139,6 @@ public class AccountController {
|
||||
|
||||
public AccountController(StoredVerificationCodeManager pendingAccounts,
|
||||
AccountsManager accounts,
|
||||
UsernamesManager usernames,
|
||||
AbusiveHostRules abusiveHostRules,
|
||||
RateLimiters rateLimiters,
|
||||
SmsSender smsSenderFactory,
|
||||
@@ -146,7 +153,6 @@ public class AccountController {
|
||||
{
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.accounts = accounts;
|
||||
this.usernames = usernames;
|
||||
this.abusiveHostRules = abusiveHostRules;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.smsSender = smsSenderFactory;
|
||||
@@ -353,11 +359,11 @@ public class AccountController {
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
Optional<Account> existingAccount = accounts.get(number);
|
||||
Optional<Account> existingAccount = accounts.getByE164(number);
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
|
||||
}
|
||||
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());
|
||||
@@ -365,7 +371,8 @@ public class AccountController {
|
||||
|
||||
rateLimiters.getVerifyLimiter().clear(number);
|
||||
|
||||
Account account = accounts.create(number, password, signalAgent, accountAttributes);
|
||||
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();
|
||||
@@ -381,7 +388,11 @@ public class AccountController {
|
||||
.record(Instant.now().toEpochMilli() - storedVerificationCode.get().getTimestamp(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
return new AccountCreationResult(account.getUuid(), account.getNumber(), existingAccount.map(Account::isStorageSupported).orElse(false));
|
||||
return new AccountCreationResult(account.getUuid(),
|
||||
account.getNumber(),
|
||||
account.getPhoneNumberIdentifier(),
|
||||
account.getUsername().orElse(null),
|
||||
existingAccount.map(Account::isStorageSupported).orElse(false));
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -411,7 +422,7 @@ public class AccountController {
|
||||
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
|
||||
.ifPresent(smsSender::reportVerificationSucceeded);
|
||||
|
||||
final Optional<Account> existingAccount = accounts.get(request.getNumber());
|
||||
final Optional<Account> existingAccount = accounts.getByE164(request.getNumber());
|
||||
|
||||
if (existingAccount.isPresent()) {
|
||||
verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock());
|
||||
@@ -543,6 +554,7 @@ public class AccountController {
|
||||
@PUT
|
||||
@Path("/attributes/")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ChangesDeviceEnabledState
|
||||
public void setAccountAttributes(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||
@HeaderParam("X-Signal-Agent") String userAgent,
|
||||
@@ -591,40 +603,60 @@ public class AccountController {
|
||||
@Path("/whoami")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCreationResult whoAmI(@Auth AuthenticatedAccount auth) {
|
||||
return new AccountCreationResult(auth.getAccount().getUuid(), auth.getAccount().getNumber(), auth.getAccount().isStorageSupported());
|
||||
return new AccountCreationResult(auth.getAccount().getUuid(),
|
||||
auth.getAccount().getNumber(),
|
||||
auth.getAccount().getPhoneNumberIdentifier(),
|
||||
auth.getAccount().getUsername().orElse(null),
|
||||
auth.getAccount().isStorageSupported());
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/username")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public void deleteUsername(@Auth AuthenticatedAccount auth) {
|
||||
usernames.delete(auth.getAccount().getUuid());
|
||||
accounts.clearUsername(auth.getAccount());
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/username/{username}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response setUsername(@Auth AuthenticatedAccount auth, @PathParam("username") String username)
|
||||
public Response setUsername(@Auth AuthenticatedAccount auth, @PathParam("username") @Username String username)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameSetLimiter().validate(auth.getAccount().getUuid());
|
||||
|
||||
if (username == null || username.isEmpty()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
username = username.toLowerCase();
|
||||
|
||||
if (!username.matches("^[a-z_][a-z0-9_]+$")) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
if (!usernames.put(auth.getAccount().getUuid(), username)) {
|
||||
try {
|
||||
accounts.setUsername(auth.getAccount(), username);
|
||||
} catch (final UsernameNotAvailableException e) {
|
||||
return Response.status(Response.Status.CONFLICT).build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@HEAD
|
||||
@Path("/account/{uuid}")
|
||||
public Response accountExists(
|
||||
@HeaderParam("X-Forwarded-For") final String forwardedFor,
|
||||
@PathParam("uuid") final UUID uuid,
|
||||
@Context HttpServletRequest request) throws RateLimitExceededException {
|
||||
|
||||
// Disallow clients from making authenticated requests to this endpoint
|
||||
if (StringUtils.isNotBlank(request.getHeader("Authorization"))) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor)
|
||||
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(1)));
|
||||
|
||||
rateLimiters.getCheckAccountExistenceLimiter().validate(mostRecentProxy);
|
||||
|
||||
final Status status = accounts.getByAccountIdentifier(uuid)
|
||||
.or(() -> accounts.getByPhoneNumberIdentifier(uuid))
|
||||
.isPresent() ? Status.OK : Status.NOT_FOUND;
|
||||
|
||||
return Response.status(status).build();
|
||||
}
|
||||
|
||||
private void verifyRegistrationLock(final Account existingAccount, @Nullable final String clientRegistrationLock)
|
||||
throws RateLimitExceededException, WebApplicationException {
|
||||
|
||||
@@ -697,14 +729,14 @@ public class AccountController {
|
||||
List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(sourceHost);
|
||||
|
||||
for (AbusiveHostRule abuseRule : abuseRules) {
|
||||
if (abuseRule.isBlocked()) {
|
||||
if (abuseRule.blocked()) {
|
||||
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)) {
|
||||
if (!abuseRule.regions().isEmpty()) {
|
||||
if (abuseRule.regions().stream().noneMatch(number::startsWith)) {
|
||||
logger.info("Restricted host: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
|
||||
filteredHostMeter.mark();
|
||||
return new CaptchaRequirement(true, false);
|
||||
|
||||
+8
-3
@@ -14,6 +14,7 @@ import java.security.InvalidKeyException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@@ -70,7 +71,8 @@ public class CertificateController {
|
||||
@Path("/group/{startRedemptionTime}/{endRedemptionTime}")
|
||||
public GroupCredentials getAuthenticationCredentials(@Auth AuthenticatedAccount auth,
|
||||
@PathParam("startRedemptionTime") int startRedemptionTime,
|
||||
@PathParam("endRedemptionTime") int endRedemptionTime) {
|
||||
@PathParam("endRedemptionTime") int endRedemptionTime,
|
||||
@QueryParam("identity") Optional<String> identityType) {
|
||||
if (startRedemptionTime > endRedemptionTime) {
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
@@ -83,10 +85,13 @@ public class CertificateController {
|
||||
|
||||
List<GroupCredentials.GroupCredential> credentials = new LinkedList<>();
|
||||
|
||||
final UUID identifier = identityType.map(String::toLowerCase).orElse("aci").equals("pni") ?
|
||||
auth.getAccount().getPhoneNumberIdentifier() :
|
||||
auth.getAccount().getUuid();
|
||||
|
||||
for (int i = startRedemptionTime; i <= endRedemptionTime; i++) {
|
||||
credentials.add(new GroupCredentials.GroupCredential(
|
||||
serverZkAuthOperations.issueAuthCredential(auth.getAccount().getUuid(), i)
|
||||
.serialize(),
|
||||
serverZkAuthOperations.issueAuthCredential(identifier, i).serialize(),
|
||||
i));
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -41,7 +41,7 @@ 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.Keys;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
@@ -57,14 +57,14 @@ public class DeviceController {
|
||||
private final StoredVerificationCodeManager pendingDevices;
|
||||
private final AccountsManager accounts;
|
||||
private final MessagesManager messages;
|
||||
private final KeysDynamoDb keys;
|
||||
private final Keys keys;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Map<String, Integer> maxDeviceConfiguration;
|
||||
|
||||
public DeviceController(StoredVerificationCodeManager pendingDevices,
|
||||
AccountsManager accounts,
|
||||
MessagesManager messages,
|
||||
KeysDynamoDb keys,
|
||||
Keys keys,
|
||||
RateLimiters rateLimiters,
|
||||
Map<String, Integer> maxDeviceConfiguration)
|
||||
{
|
||||
@@ -168,7 +168,7 @@ public class DeviceController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
Optional<Account> account = accounts.get(number);
|
||||
Optional<Account> account = accounts.getByE164(number);
|
||||
|
||||
if (!account.isPresent()) {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
@@ -203,7 +203,7 @@ public class DeviceController {
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
device.setCapabilities(accountAttributes.getCapabilities());
|
||||
|
||||
accounts.update(account.get(), a -> {
|
||||
final Account updatedAccount = accounts.update(account.get(), a -> {
|
||||
device.setId(a.getNextDeviceId());
|
||||
messages.clear(a.getUuid(), device.getId());
|
||||
a.addDevice(device);
|
||||
@@ -211,7 +211,7 @@ public class DeviceController {
|
||||
|
||||
pendingDevices.remove(number);
|
||||
|
||||
return new DeviceResponse(device.getId());
|
||||
return new DeviceResponse(updatedAccount.getUuid(), updatedAccount.getPhoneNumberIdentifier(), device.getId());
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
||||
+49
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -159,7 +159,7 @@ public class DonationController {
|
||||
|
||||
@Override
|
||||
public boolean block() {
|
||||
final Optional<Account> optionalAccount = accountsManager.get(auth.getAccount().getUuid());
|
||||
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()));
|
||||
|
||||
+104
-71
@@ -11,6 +11,7 @@ import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -24,11 +25,13 @@ import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
@@ -37,14 +40,11 @@ import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
|
||||
import org.whispersystems.textsecuregcm.storage.Keys;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@@ -52,32 +52,26 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||
public class KeysController {
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final PreKeyRateLimiter preKeyRateLimiter;
|
||||
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
|
||||
private static final String PREKEY_REQUEST_COUNTER_NAME = name(KeysController.class, "preKeyGet");
|
||||
private static final String RATE_LIMITED_GET_PREKEYS_COUNTER_NAME = name(KeysController.class, "rateLimitedGetPreKeys");
|
||||
|
||||
private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
|
||||
private static final String INTERNATIONAL_TAG_NAME = "international";
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts,
|
||||
PreKeyRateLimiter preKeyRateLimiter,
|
||||
RateLimitChallengeManager rateLimitChallengeManager) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keysDynamoDb = keysDynamoDb;
|
||||
this.accounts = accounts;
|
||||
this.preKeyRateLimiter = preKeyRateLimiter;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKeyCount getStatus(@Auth AuthenticatedAccount auth) {
|
||||
int count = keysDynamoDb.getCount(auth.getAccount(), auth.getAuthenticatedDevice().getId());
|
||||
public PreKeyCount getStatus(@Auth final AuthenticatedAccount auth,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
int count = keys.getCount(getIdentifier(auth.getAccount(), identityType), auth.getAuthenticatedDevice().getId());
|
||||
|
||||
if (count > 0) {
|
||||
count = count - 1;
|
||||
@@ -89,7 +83,10 @@ public class KeysController {
|
||||
@Timed
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setKeys(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth, @Valid PreKeyState preKeys) {
|
||||
@ChangesDeviceEnabledState
|
||||
public void setKeys(@Auth final DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
|
||||
@Valid final PreKeyState preKeys,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
Account account = disabledPermittedAuth.getAccount();
|
||||
Device device = disabledPermittedAuth.getAuthenticatedDevice();
|
||||
boolean updateAccount = false;
|
||||
@@ -102,14 +99,27 @@ public class KeysController {
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
final boolean usePhoneNumberIdentity = usePhoneNumberIdentity(identityType);
|
||||
|
||||
if (updateAccount) {
|
||||
account = accounts.update(account, a -> {
|
||||
a.getDevice(device.getId()).ifPresent(d -> d.setSignedPreKey(preKeys.getSignedPreKey()));
|
||||
a.setIdentityKey(preKeys.getIdentityKey());
|
||||
a.getDevice(device.getId()).ifPresent(d -> {
|
||||
if (usePhoneNumberIdentity) {
|
||||
d.setPhoneNumberIdentitySignedPreKey(preKeys.getSignedPreKey());
|
||||
} else {
|
||||
d.setSignedPreKey(preKeys.getSignedPreKey());
|
||||
}
|
||||
});
|
||||
|
||||
if (usePhoneNumberIdentity) {
|
||||
a.setPhoneNumberIdentityKey(preKeys.getIdentityKey());
|
||||
} else {
|
||||
a.setIdentityKey(preKeys.getIdentityKey());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
keysDynamoDb.store(account, device.getId(), preKeys.getPreKeys());
|
||||
keys.store(getIdentifier(account, identityType), device.getId(), preKeys.getPreKeys());
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -121,7 +131,7 @@ public class KeysController {
|
||||
@PathParam("identifier") UUID targetUuid,
|
||||
@PathParam("device_id") String deviceId,
|
||||
@HeaderParam("User-Agent") String userAgent)
|
||||
throws RateLimitExceededException, RateLimitChallengeException, ServerRejectedException {
|
||||
throws RateLimitExceededException {
|
||||
|
||||
if (!auth.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
@@ -129,14 +139,19 @@ public class KeysController {
|
||||
|
||||
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
|
||||
|
||||
Optional<Account> target = accounts.get(targetUuid);
|
||||
OptionalAccess.verify(account, accessKey, target, deviceId);
|
||||
final Account target;
|
||||
{
|
||||
final Optional<Account> maybeTarget = accounts.getByAccountIdentifier(targetUuid)
|
||||
.or(() -> accounts.getByPhoneNumberIdentifier(targetUuid));
|
||||
|
||||
assert (target.isPresent());
|
||||
OptionalAccess.verify(account, accessKey, maybeTarget, deviceId);
|
||||
|
||||
target = maybeTarget.orElseThrow();
|
||||
}
|
||||
|
||||
{
|
||||
final String sourceCountryCode = account.map(a -> Util.getCountryCode(a.getNumber())).orElse("0");
|
||||
final String targetCountryCode = target.map(a -> Util.getCountryCode(a.getNumber())).orElseThrow();
|
||||
final String targetCountryCode = Util.getCountryCode(target.getNumber());
|
||||
|
||||
Metrics.counter(PREKEY_REQUEST_COUNTER_NAME, Tags.of(
|
||||
SOURCE_COUNTRY_TAG_NAME, sourceCountryCode,
|
||||
@@ -146,33 +161,18 @@ public class KeysController {
|
||||
|
||||
if (account.isPresent()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + target.get().getUuid()
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + targetUuid
|
||||
+ "." + deviceId);
|
||||
|
||||
try {
|
||||
preKeyRateLimiter.validate(account.get());
|
||||
} catch (RateLimitExceededException e) {
|
||||
|
||||
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
|
||||
|
||||
Metrics.counter(RATE_LIMITED_GET_PREKEYS_COUNTER_NAME,
|
||||
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.get().getNumber()),
|
||||
"legacyClient", String.valueOf(legacyClient))
|
||||
.increment();
|
||||
|
||||
if (legacyClient) {
|
||||
throw new ServerRejectedException();
|
||||
}
|
||||
throw new RateLimitChallengeException(account.get(), e.getRetryDuration());
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, PreKey> preKeysByDeviceId = getLocalKeys(target.get(), deviceId);
|
||||
final boolean usePhoneNumberIdentity = target.getPhoneNumberIdentifier().equals(targetUuid);
|
||||
|
||||
Map<Long, PreKey> preKeysByDeviceId = getLocalKeys(target, deviceId, usePhoneNumberIdentity);
|
||||
List<PreKeyResponseItem> responseItems = new LinkedList<>();
|
||||
|
||||
for (Device device : target.get().getDevices()) {
|
||||
for (Device device : target.getDevices()) {
|
||||
if (device.isEnabled() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
SignedPreKey signedPreKey = usePhoneNumberIdentity ? device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
PreKey preKey = preKeysByDeviceId.get(device.getId());
|
||||
|
||||
if (signedPreKey != null || preKey != null) {
|
||||
@@ -181,48 +181,81 @@ public class KeysController {
|
||||
}
|
||||
}
|
||||
|
||||
final String identityKey = usePhoneNumberIdentity ? target.getPhoneNumberIdentityKey() : target.getIdentityKey();
|
||||
|
||||
if (responseItems.isEmpty()) return Response.status(404).build();
|
||||
else return Response.ok().entity(new PreKeyResponse(target.get().getIdentityKey(), responseItems)).build();
|
||||
else return Response.ok().entity(new PreKeyResponse(identityKey, responseItems)).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/signed")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public void setSignedKey(@Auth AuthenticatedAccount auth, @Valid SignedPreKey signedPreKey) {
|
||||
@ChangesDeviceEnabledState
|
||||
public void setSignedKey(@Auth final AuthenticatedAccount auth,
|
||||
@Valid final SignedPreKey signedPreKey,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
|
||||
accounts.updateDevice(auth.getAccount(), device.getId(), d -> d.setSignedPreKey(signedPreKey));
|
||||
accounts.updateDevice(auth.getAccount(), device.getId(), d -> {
|
||||
if (usePhoneNumberIdentity(identityType)) {
|
||||
d.setPhoneNumberIdentitySignedPreKey(signedPreKey);
|
||||
} else {
|
||||
d.setSignedPreKey(signedPreKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/signed")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Optional<SignedPreKey> getSignedKey(@Auth AuthenticatedAccount auth) {
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
SignedPreKey signedPreKey = device.getSignedPreKey();
|
||||
public Optional<SignedPreKey> getSignedKey(@Auth final AuthenticatedAccount auth,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
if (signedPreKey != null) {
|
||||
return Optional.of(signedPreKey);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
SignedPreKey signedPreKey = usePhoneNumberIdentity(identityType) ?
|
||||
device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
|
||||
return Optional.ofNullable(signedPreKey);
|
||||
}
|
||||
|
||||
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector) {
|
||||
try {
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
return keysDynamoDb.take(destination);
|
||||
private static boolean usePhoneNumberIdentity(final Optional<String> identityType) {
|
||||
return "pni".equals(identityType.map(String::toLowerCase).orElse("aci"));
|
||||
}
|
||||
|
||||
private static UUID getIdentifier(final Account account, final Optional<String> identityType) {
|
||||
return usePhoneNumberIdentity(identityType) ?
|
||||
account.getPhoneNumberIdentifier() :
|
||||
account.getUuid();
|
||||
}
|
||||
|
||||
private Map<Long, PreKey> getLocalKeys(Account destination, String deviceIdSelector, final boolean usePhoneNumberIdentity) {
|
||||
final Map<Long, PreKey> preKeys;
|
||||
|
||||
final UUID identifier = usePhoneNumberIdentity ?
|
||||
destination.getPhoneNumberIdentifier() :
|
||||
destination.getUuid();
|
||||
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
preKeys = new HashMap<>();
|
||||
|
||||
for (final Device device : destination.getDevices()) {
|
||||
keys.take(identifier, device.getId()).ifPresent(preKey -> preKeys.put(device.getId(), preKey));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
|
||||
return keysDynamoDb.take(destination, deviceId)
|
||||
.map(preKey -> Map.of(deviceId, preKey))
|
||||
.orElse(Collections.emptyMap());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
preKeys = keys.take(identifier, deviceId)
|
||||
.map(preKey -> Map.of(deviceId, preKey))
|
||||
.orElse(Collections.emptyMap());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
}
|
||||
|
||||
return preKeys;
|
||||
}
|
||||
}
|
||||
|
||||
+85
-83
@@ -6,14 +6,10 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.Histogram;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
@@ -42,6 +38,7 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -78,9 +75,7 @@ import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMultiRecipientMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
@@ -93,10 +88,11 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||
|
||||
@@ -104,26 +100,24 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
|
||||
@Path("/v1/messages")
|
||||
public class MessageController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Meter unidentifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "unidentified"));
|
||||
private final Meter identifiedMeter = metricRegistry.meter(name(getClass(), "delivery", "identified" ));
|
||||
private final Meter rejectOver256kibMessageMeter = metricRegistry.meter(name(getClass(), "rejectOver256kibMessage"));
|
||||
private final Timer sendMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendMessageInternal"));
|
||||
private final Timer sendCommonMessageInternalTimer = metricRegistry.timer(name(getClass(), "sendCommonMessageInternal"));
|
||||
private final Histogram outgoingMessageListSizeHistogram = metricRegistry.histogram(name(getClass(), "outgoingMessageListSize"));
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final MessageSender messageSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final UnsealedSenderRateLimiter unsealedSenderRateLimiter;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
private final ReportMessageManager reportMessageManager;
|
||||
private final ExecutorService multiRecipientMessageExecutor;
|
||||
|
||||
@VisibleForTesting
|
||||
static final Semver FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE = new Semver("5.22.0");
|
||||
|
||||
@VisibleForTesting
|
||||
static final Semver IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE = new Semver("5.25.0");
|
||||
|
||||
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER = name(MessageController.class, "rejectOversizeMessage");
|
||||
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 CONTENT_SIZE_DISTRIBUTION_NAME = name(MessageController.class, "messageContentSize");
|
||||
@@ -135,6 +129,10 @@ public class MessageController {
|
||||
private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry";
|
||||
private static final String RATE_LIMIT_REASON_TAG_NAME = "rateLimitReason";
|
||||
|
||||
private static final String SENDER_TYPE_IDENTIFIED = "identified";
|
||||
private static final String SENDER_TYPE_UNIDENTIFIED = "unidentified";
|
||||
private static final String SENDER_TYPE_SELF = "self";
|
||||
|
||||
private static final long MAX_MESSAGE_SIZE = DataSize.kibibytes(256).toBytes();
|
||||
|
||||
public MessageController(
|
||||
@@ -143,9 +141,7 @@ public class MessageController {
|
||||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
MessagesManager messagesManager,
|
||||
UnsealedSenderRateLimiter unsealedSenderRateLimiter,
|
||||
ApnFallbackManager apnFallbackManager,
|
||||
RateLimitChallengeManager rateLimitChallengeManager,
|
||||
ReportMessageManager reportMessageManager,
|
||||
@Nonnull ExecutorService multiRecipientMessageExecutor) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
@@ -153,9 +149,7 @@ public class MessageController {
|
||||
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);
|
||||
}
|
||||
@@ -180,14 +174,14 @@ public class MessageController {
|
||||
|
||||
final String senderType;
|
||||
|
||||
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
|
||||
identifiedMeter.mark();
|
||||
senderType = "identified";
|
||||
} else if (source.isEmpty()) {
|
||||
unidentifiedMeter.mark();
|
||||
senderType = "unidentified";
|
||||
if (source.isPresent()) {
|
||||
if (source.get().getAccount().isIdentifiedBy(destinationUuid)) {
|
||||
senderType = SENDER_TYPE_SELF;
|
||||
} else {
|
||||
senderType = SENDER_TYPE_IDENTIFIED;
|
||||
}
|
||||
} else {
|
||||
senderType = "self";
|
||||
senderType = SENDER_TYPE_UNIDENTIFIED;
|
||||
}
|
||||
|
||||
for (final IncomingMessage message : messages.getMessages()) {
|
||||
@@ -204,18 +198,19 @@ public class MessageController {
|
||||
Metrics.summary(CONTENT_SIZE_DISTRIBUTION_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(contentLength);
|
||||
|
||||
if (contentLength > MAX_MESSAGE_SIZE) {
|
||||
rejectOver256kibMessageMeter.mark();
|
||||
Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
|
||||
return Response.status(Response.Status.REQUEST_ENTITY_TOO_LARGE).build();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getAccount().getUuid().equals(destinationUuid);
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getAccount().isIdentifiedBy(destinationUuid);
|
||||
|
||||
Optional<Account> destination;
|
||||
|
||||
if (!isSyncMessage) {
|
||||
destination = accountsManager.get(destinationUuid);
|
||||
destination = accountsManager.getByAccountIdentifier(destinationUuid)
|
||||
.or(() -> accountsManager.getByPhoneNumberIdentifier(destinationUuid));
|
||||
} else {
|
||||
destination = source.map(AuthenticatedAccount::getAccount);
|
||||
}
|
||||
@@ -223,7 +218,7 @@ public class MessageController {
|
||||
OptionalAccess.verify(source.map(AuthenticatedAccount::getAccount), accessKey, destination);
|
||||
assert (destination.isPresent());
|
||||
|
||||
if (source.isPresent() && !source.get().getAccount().getUuid().equals(destinationUuid)) {
|
||||
if (source.isPresent() && !source.get().getAccount().isIdentifiedBy(destinationUuid)) {
|
||||
final String senderCountryCode = Util.getCountryCode(source.get().getAccount().getNumber());
|
||||
|
||||
try {
|
||||
@@ -235,24 +230,6 @@ public class MessageController {
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
unsealedSenderRateLimiter.validate(source.get().getAccount(), destination.get());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
|
||||
final String rateLimitReason = legacyClient ? "unsealedSenderCardinality" : "challengeIssued";
|
||||
|
||||
Metrics.counter(RATE_LIMITED_MESSAGE_COUNTER_NAME,
|
||||
SENDER_COUNTRY_TAG_NAME, senderCountryCode,
|
||||
RATE_LIMIT_REASON_TAG_NAME, rateLimitReason).increment();
|
||||
|
||||
if (legacyClient) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw new RateLimitChallengeException(source.get().getAccount(), e.getRetryDuration());
|
||||
}
|
||||
}
|
||||
|
||||
validateCompleteDeviceList(destination.get(), messages.getMessages(), isSyncMessage,
|
||||
@@ -268,8 +245,8 @@ public class MessageController {
|
||||
|
||||
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(), destinationUuid, messages.getTimestamp(),
|
||||
messages.isOnline(), incomingMessage, userAgent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,13 +282,11 @@ public class MessageController {
|
||||
@QueryParam("ts") long timestamp,
|
||||
@Valid MultiRecipientMessage multiRecipientMessage) {
|
||||
|
||||
unidentifiedMeter.mark(multiRecipientMessage.getRecipients().length);
|
||||
|
||||
Map<UUID, Account> uuidToAccountMap = Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(Recipient::getUuid)
|
||||
.distinct()
|
||||
.collect(Collectors.toUnmodifiableMap(Function.identity(), uuid -> {
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
if (account.isEmpty()) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
@@ -362,20 +337,21 @@ public class MessageController {
|
||||
.build();
|
||||
}
|
||||
|
||||
List<Tag> tags = List.of(
|
||||
UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, "unidentified"));
|
||||
List<UUID> uuids404 = Collections.synchronizedList(new ArrayList<>());
|
||||
final Counter counter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags);
|
||||
|
||||
try {
|
||||
final Counter sentMessageCounter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, Tags.of(
|
||||
UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, SENDER_TYPE_UNIDENTIFIED)));
|
||||
|
||||
multiRecipientMessageExecutor.invokeAll(Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(recipient -> (Callable<Void>) () -> {
|
||||
Account destinationAccount = uuidToAccountMap.get(recipient.getUuid());
|
||||
|
||||
// we asserted this must exist in validateCompleteDeviceList
|
||||
Device destinationDevice = destinationAccount.getDevice(recipient.getDeviceId()).orElseThrow();
|
||||
counter.increment();
|
||||
sentMessageCounter.increment();
|
||||
try {
|
||||
sendMessage(destinationAccount, destinationDevice, timestamp, online, recipient,
|
||||
multiRecipientMessage.getCommonPayload());
|
||||
@@ -444,8 +420,6 @@ public class MessageController {
|
||||
userAgent,
|
||||
false);
|
||||
|
||||
outgoingMessageListSizeHistogram.update(outgoingMessages.getMessages().size());
|
||||
|
||||
{
|
||||
String platform;
|
||||
|
||||
@@ -503,7 +477,7 @@ public class MessageController {
|
||||
public Response reportMessage(@Auth AuthenticatedAccount auth, @PathParam("sourceNumber") String sourceNumber,
|
||||
@PathParam("messageGuid") UUID messageGuid) {
|
||||
|
||||
reportMessageManager.report(sourceNumber, messageGuid);
|
||||
reportMessageManager.report(sourceNumber, messageGuid, auth.getAccount().getUuid());
|
||||
|
||||
return Response.status(Status.ACCEPTED)
|
||||
.build();
|
||||
@@ -512,33 +486,56 @@ public class MessageController {
|
||||
private void sendMessage(Optional<AuthenticatedAccount> source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
UUID destinationUuid,
|
||||
long timestamp,
|
||||
boolean online,
|
||||
IncomingMessage incomingMessage)
|
||||
IncomingMessage incomingMessage,
|
||||
String userAgentString)
|
||||
throws NoSuchUserException {
|
||||
try (final Timer.Context ignored = sendMessageInternalTimer.time()) {
|
||||
try {
|
||||
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());
|
||||
int envelopeTypeNumber = incomingMessage.getType();
|
||||
|
||||
if (source.isPresent()) {
|
||||
messageBuilder.setSource(source.get().getAccount().getNumber())
|
||||
.setSourceUuid(source.get().getAccount().getUuid().toString())
|
||||
.setSourceDevice((int) source.get().getAuthenticatedDevice().getId());
|
||||
// Some versions of the iOS app incorrectly use the reserved envelope type 7 for PLAINTEXT_CONTENT instead of type
|
||||
// 8. This check can be removed safely after 2022-03-01.
|
||||
if (envelopeTypeNumber == 7) {
|
||||
try {
|
||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||
if (userAgent.getPlatform() == ClientPlatform.IOS &&
|
||||
FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE.isLowerThanOrEqualTo(userAgent.getVersion()) &&
|
||||
userAgent.getVersion().isLowerThan(IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE)) {
|
||||
envelopeTypeNumber = Type.PLAINTEXT_CONTENT.getNumber();
|
||||
}
|
||||
} catch (final UnrecognizedUserAgentException ignored2) {
|
||||
}
|
||||
}
|
||||
|
||||
if (messageBody.isPresent()) {
|
||||
final Envelope.Type envelopeType = Envelope.Type.forNumber(envelopeTypeNumber);
|
||||
|
||||
if (envelopeType == null) {
|
||||
logger.warn("Received bad envelope type {} from {}", incomingMessage.getType(), userAgentString);
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
messageBuilder.setType(envelopeType)
|
||||
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
|
||||
.setServerTimestamp(System.currentTimeMillis())
|
||||
.setDestinationUuid(destinationUuid.toString());
|
||||
|
||||
source.ifPresent(authenticatedAccount ->
|
||||
messageBuilder.setSource(authenticatedAccount.getAccount().getNumber())
|
||||
.setSourceUuid(authenticatedAccount.getAccount().getUuid().toString())
|
||||
.setSourceDevice((int) authenticatedAccount.getAuthenticatedDevice().getId()));
|
||||
|
||||
messageBody.ifPresent(bytes -> {
|
||||
Metrics.counter(LEGACY_MESSAGE_SENT_COUNTER).increment();
|
||||
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
|
||||
}
|
||||
});
|
||||
|
||||
if (messageContent.isPresent()) {
|
||||
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
|
||||
}
|
||||
messageContent.ifPresent(bytes -> messageBuilder.setContent(ByteString.copyFrom(bytes)));
|
||||
|
||||
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
@@ -547,9 +544,13 @@ public class MessageController {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(Account destinationAccount, Device destinationDevice, long timestamp, boolean online,
|
||||
Recipient recipient, byte[] commonPayload) throws NoSuchUserException {
|
||||
try (final Timer.Context ignored = sendCommonMessageInternalTimer.time()) {
|
||||
private void sendMessage(Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
long timestamp,
|
||||
boolean online,
|
||||
Recipient recipient,
|
||||
byte[] commonPayload) throws NoSuchUserException {
|
||||
try {
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
long serverTimestamp = System.currentTimeMillis();
|
||||
byte[] recipientKeyMaterial = recipient.getPerRecipientKeyMaterial();
|
||||
@@ -563,7 +564,8 @@ public class MessageController {
|
||||
.setType(Type.UNIDENTIFIED_SENDER)
|
||||
.setTimestamp(timestamp == 0 ? serverTimestamp : timestamp)
|
||||
.setServerTimestamp(serverTimestamp)
|
||||
.setContent(ByteString.copyFrom(payload));
|
||||
.setContent(ByteString.copyFrom(payload))
|
||||
.setDestinationUuid(destinationAccount.getUuid().toString());
|
||||
|
||||
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
|
||||
+214
-214
@@ -19,15 +19,18 @@ 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.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@@ -40,12 +43,15 @@ 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 io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.PniCredentialResponse;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
@@ -61,9 +67,13 @@ 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.CredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PniCredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileKeyCredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.BaseProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
|
||||
import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
@@ -72,14 +82,13 @@ import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
@Path("/v1/profile")
|
||||
public class ProfileController {
|
||||
@@ -90,7 +99,6 @@ public class ProfileController {
|
||||
private final RateLimiters rateLimiters;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final ProfileBadgeConverter profileBadgeConverter;
|
||||
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
|
||||
@@ -102,12 +110,16 @@ public class ProfileController {
|
||||
private final S3Client s3client;
|
||||
private final String bucket;
|
||||
|
||||
private static final String PROFILE_KEY_CREDENTIAL_TYPE = "profileKey";
|
||||
private static final String PNI_CREDENTIAL_TYPE = "pni";
|
||||
|
||||
private static final Counter VERSION_NOT_FOUND_COUNTER = Metrics.counter(name(ProfileController.class, "versionNotFound"));
|
||||
|
||||
public ProfileController(
|
||||
Clock clock,
|
||||
RateLimiters rateLimiters,
|
||||
AccountsManager accountsManager,
|
||||
ProfilesManager profilesManager,
|
||||
UsernamesManager usernamesManager,
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
ProfileBadgeConverter profileBadgeConverter,
|
||||
BadgesConfiguration badgesConfiguration,
|
||||
@@ -120,7 +132,6 @@ public class ProfileController {
|
||||
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(
|
||||
@@ -137,13 +148,14 @@ public class ProfileController {
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response setProfile(@Auth AuthenticatedAccount auth, @Valid CreateProfileRequest request) {
|
||||
final Set<String> allowedPaymentsCountryCodes =
|
||||
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getAllowedCountryCodes();
|
||||
if (StringUtils.isNotBlank(request.getPaymentAddress())) {
|
||||
final boolean hasDisallowedPrefix =
|
||||
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream()
|
||||
.anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix));
|
||||
|
||||
if (StringUtils.isNotBlank(request.getPaymentAddress()) &&
|
||||
!allowedPaymentsCountryCodes.contains(Util.getCountryCode(auth.getAccount().getNumber()))) {
|
||||
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
if (hasDisallowedPrefix) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
}
|
||||
|
||||
Optional<VersionedProfile> currentProfile = profilesManager.get(auth.getAccount().getUuid(), request.getVersion());
|
||||
@@ -168,11 +180,6 @@ public class ProfileController {
|
||||
currentAvatar = Optional.of(currentProfile.get().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)
|
||||
@@ -181,12 +188,11 @@ public class ProfileController {
|
||||
response = Optional.of(generateAvatarUploadForm(avatar));
|
||||
}
|
||||
|
||||
List<AccountBadge> updatedBadges = mergeBadgeIdsWithExistingAccountBadges(
|
||||
request.getBadges(), auth.getAccount().getBadges());
|
||||
List<AccountBadge> updatedBadges = request.getBadges()
|
||||
.map(badges -> mergeBadgeIdsWithExistingAccountBadges(badges, auth.getAccount().getBadges()))
|
||||
.orElseGet(() -> auth.getAccount().getBadges());
|
||||
|
||||
accountsManager.update(auth.getAccount(), a -> {
|
||||
a.setProfileName(request.getName());
|
||||
a.setAvatar(avatar);
|
||||
a.setBadges(clock, updatedBadges);
|
||||
a.setCurrentProfileVersion(request.getVersion());
|
||||
});
|
||||
@@ -202,183 +208,72 @@ public class ProfileController {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}")
|
||||
public Optional<Profile> getProfile(
|
||||
public VersionedProfileResponse 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());
|
||||
|
||||
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
|
||||
final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, uuid);
|
||||
|
||||
return buildVersionedProfileResponse(targetAccount,
|
||||
version,
|
||||
isSelfProfileRequest(maybeRequester, uuid),
|
||||
containerRequestContext);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}/{credentialRequest}")
|
||||
public Optional<Profile> getProfile(
|
||||
public CredentialProfileResponse 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)
|
||||
@PathParam("credentialRequest") String credentialRequest,
|
||||
@QueryParam("credentialType") @DefaultValue(PROFILE_KEY_CREDENTIAL_TYPE) String credentialType)
|
||||
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,
|
||||
List<Locale> acceptableLanguages,
|
||||
UUID uuid,
|
||||
String version,
|
||||
Optional<String> credentialRequest)
|
||||
throws RateLimitExceededException {
|
||||
try {
|
||||
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
|
||||
final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, uuid);
|
||||
final boolean isSelf = isSelfProfileRequest(maybeRequester, uuid);
|
||||
|
||||
switch (credentialType) {
|
||||
case PROFILE_KEY_CREDENTIAL_TYPE -> {
|
||||
return buildProfileKeyCredentialProfileResponse(targetAccount,
|
||||
version,
|
||||
credentialRequest,
|
||||
isSelf,
|
||||
containerRequestContext);
|
||||
}
|
||||
|
||||
boolean isSelf = false;
|
||||
if (requestAccount.isPresent()) {
|
||||
UUID authedUuid = requestAccount.get().getUuid();
|
||||
rateLimiters.getProfileLimiter().validate(authedUuid);
|
||||
isSelf = uuid.equals(authedUuid);
|
||||
case PNI_CREDENTIAL_TYPE -> {
|
||||
if (!isSelf) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return buildPniCredentialProfileResponse(targetAccount,
|
||||
version,
|
||||
credentialRequest,
|
||||
containerRequestContext);
|
||||
}
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(uuid);
|
||||
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
|
||||
|
||||
assert(accountProfile.isPresent());
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
Optional<VersionedProfile> profile = profilesManager.get(uuid, version);
|
||||
|
||||
String name = profile.map(VersionedProfile::getName).orElse(accountProfile.get().getProfileName());
|
||||
String about = profile.map(VersionedProfile::getAbout).orElse(null);
|
||||
String aboutEmoji = profile.map(VersionedProfile::getAboutEmoji).orElse(null);
|
||||
String avatar = profile.map(VersionedProfile::getAvatar).orElse(accountProfile.get().getAvatar());
|
||||
Optional<String> currentProfileVersion = accountProfile.get().getCurrentProfileVersion();
|
||||
|
||||
// Allow requests where either the version matches the latest version on Account or the latest version on Account
|
||||
// is empty to read the payment address.
|
||||
final String paymentAddress = profile
|
||||
.filter(p -> currentProfileVersion.map(v -> v.equals(version)).orElse(true))
|
||||
.map(VersionedProfile::getPaymentAddress)
|
||||
.orElse(null);
|
||||
|
||||
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,
|
||||
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);
|
||||
default -> throw new BadRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/username/{username}")
|
||||
public Profile getProfileByUsername(
|
||||
@Auth AuthenticatedAccount auth,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@PathParam("username") String username)
|
||||
throws RateLimitExceededException {
|
||||
rateLimiters.getUsernameLookupLimiter().validate(auth.getAccount().getUuid());
|
||||
|
||||
username = username.toLowerCase();
|
||||
|
||||
Optional<UUID> uuid = usernamesManager.get(username);
|
||||
|
||||
if (uuid.isEmpty()) {
|
||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||
}
|
||||
|
||||
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(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(),
|
||||
profileBadgeConverter.convert(
|
||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||
accountProfile.get().getBadges(),
|
||||
isSelf),
|
||||
null);
|
||||
}
|
||||
|
||||
private Optional<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> encodedProfileCredentialRequest,
|
||||
Optional<VersionedProfile> profile,
|
||||
UUID uuid)
|
||||
throws InvalidInputException
|
||||
{
|
||||
if (encodedProfileCredentialRequest.isEmpty()) return Optional.empty();
|
||||
if (profile.isEmpty()) return Optional.empty();
|
||||
|
||||
try {
|
||||
ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.get().getCommitment());
|
||||
ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest.get()));
|
||||
ProfileKeyCredentialResponse response = zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
|
||||
|
||||
return Optional.of(response);
|
||||
} catch (DecoderException | VerificationFailedException e) {
|
||||
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Old profile endpoints. Replaced by versioned profile endpoints (above)
|
||||
|
||||
@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
|
||||
// Although clients should generally be using versioned profiles wherever possible, there are still a few lingering
|
||||
// use cases for getting profiles without a version (e.g. getting a contact's unidentified access key checksum).
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{identifier}")
|
||||
public Profile getProfile(
|
||||
public BaseProfileResponse getUnversionedProfile(
|
||||
@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@@ -387,64 +282,131 @@ public class ProfileController {
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
throws RateLimitExceededException {
|
||||
|
||||
if (auth.isEmpty() && accessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
|
||||
final Account targetAccount = verifyPermissionToReceiveProfile(maybeRequester, accessKey, identifier);
|
||||
|
||||
return buildBaseProfileResponse(targetAccount,
|
||||
isSelfProfileRequest(maybeRequester, identifier),
|
||||
containerRequestContext);
|
||||
}
|
||||
|
||||
private ProfileKeyCredentialProfileResponse buildProfileKeyCredentialProfileResponse(final Account account,
|
||||
final String version,
|
||||
final String encodedCredentialRequest,
|
||||
final boolean isSelf,
|
||||
final ContainerRequestContext containerRequestContext) {
|
||||
|
||||
final ProfileKeyCredentialResponse profileKeyCredentialResponse = profilesManager.get(account.getUuid(), version)
|
||||
.map(profile -> getProfileCredential(encodedCredentialRequest, profile, account.getUuid()))
|
||||
.orElse(null);
|
||||
|
||||
return new ProfileKeyCredentialProfileResponse(
|
||||
buildVersionedProfileResponse(account, version, isSelf, containerRequestContext),
|
||||
profileKeyCredentialResponse);
|
||||
}
|
||||
|
||||
private PniCredentialProfileResponse buildPniCredentialProfileResponse(final Account account,
|
||||
final String version,
|
||||
final String encodedCredentialRequest,
|
||||
final ContainerRequestContext containerRequestContext) {
|
||||
|
||||
final PniCredentialResponse pniCredentialResponse = profilesManager.get(account.getUuid(), version)
|
||||
.map(profile -> getPniCredential(encodedCredentialRequest, profile, account.getUuid(), account.getPhoneNumberIdentifier()))
|
||||
.orElse(null);
|
||||
|
||||
return new PniCredentialProfileResponse(
|
||||
buildVersionedProfileResponse(account, version, true, containerRequestContext),
|
||||
pniCredentialResponse);
|
||||
}
|
||||
|
||||
private VersionedProfileResponse buildVersionedProfileResponse(final Account account,
|
||||
final String version,
|
||||
final boolean isSelf,
|
||||
final ContainerRequestContext containerRequestContext) {
|
||||
|
||||
final Optional<VersionedProfile> maybeProfile = profilesManager.get(account.getUuid(), version);
|
||||
|
||||
if (maybeProfile.isEmpty()) {
|
||||
// Hypothesis: this should basically never happen since clients can't delete versions
|
||||
VERSION_NOT_FOUND_COUNTER.increment();
|
||||
}
|
||||
|
||||
boolean isSelf = false;
|
||||
if (auth.isPresent()) {
|
||||
UUID authedUuid = auth.get().getAccount().getUuid();
|
||||
rateLimiters.getProfileLimiter().validate(authedUuid);
|
||||
isSelf = authedUuid.equals(identifier);
|
||||
}
|
||||
final String name = maybeProfile.map(VersionedProfile::getName).orElse(null);
|
||||
final String about = maybeProfile.map(VersionedProfile::getAbout).orElse(null);
|
||||
final String aboutEmoji = maybeProfile.map(VersionedProfile::getAboutEmoji).orElse(null);
|
||||
final String avatar = maybeProfile.map(VersionedProfile::getAvatar).orElse(null);
|
||||
|
||||
Optional<Account> accountProfile = accountsManager.get(identifier);
|
||||
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
|
||||
// Allow requests where either the version matches the latest version on Account or the latest version on Account
|
||||
// is empty to read the payment address.
|
||||
final String paymentAddress = maybeProfile
|
||||
.filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(version)).orElse(true))
|
||||
.map(VersionedProfile::getPaymentAddress)
|
||||
.orElse(null);
|
||||
|
||||
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());
|
||||
return new VersionedProfileResponse(buildBaseProfileResponse(account, isSelf, containerRequestContext),
|
||||
name, about, aboutEmoji, avatar, paymentAddress);
|
||||
}
|
||||
|
||||
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,
|
||||
private BaseProfileResponse buildBaseProfileResponse(final Account account,
|
||||
final boolean isSelf,
|
||||
final ContainerRequestContext containerRequestContext) {
|
||||
|
||||
return new BaseProfileResponse(account.getIdentityKey(),
|
||||
UnidentifiedAccessChecksum.generateFor(account.getUnidentifiedAccessKey()),
|
||||
account.isUnrestrictedUnidentifiedAccess(),
|
||||
UserCapabilities.createForAccount(account),
|
||||
profileBadgeConverter.convert(
|
||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||
accountProfile.get().getBadges(),
|
||||
account.getBadges(),
|
||||
isSelf),
|
||||
null);
|
||||
account.getUuid());
|
||||
}
|
||||
|
||||
@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);
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/username/{username}")
|
||||
public BaseProfileResponse getProfileByUsername(
|
||||
@Auth AuthenticatedAccount auth,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@PathParam("username") String username)
|
||||
throws RateLimitExceededException {
|
||||
|
||||
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {
|
||||
s3client.deleteObject(DeleteObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(previousAvatar)
|
||||
.build());
|
||||
}
|
||||
rateLimiters.getUsernameLookupLimiter().validate(auth.getAccount().getUuid());
|
||||
|
||||
accountsManager.update(auth.getAccount(), a -> a.setAvatar(objectName));
|
||||
final Account targetAccount = accountsManager.getByUsername(username).orElseThrow(NotFoundException::new);
|
||||
final boolean isSelf = auth.getAccount().getUuid().equals(targetAccount.getUuid());
|
||||
|
||||
return profileAvatarUploadAttributes;
|
||||
return buildBaseProfileResponse(targetAccount, isSelf, containerRequestContext);
|
||||
}
|
||||
|
||||
////
|
||||
private ProfileKeyCredentialResponse getProfileCredential(final String encodedProfileCredentialRequest,
|
||||
final VersionedProfile profile,
|
||||
final UUID uuid) {
|
||||
try {
|
||||
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
|
||||
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest));
|
||||
|
||||
return zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
|
||||
} catch (DecoderException | VerificationFailedException | InvalidInputException e) {
|
||||
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
||||
}
|
||||
}
|
||||
|
||||
private PniCredentialResponse getPniCredential(final String encodedCredentialRequest,
|
||||
final VersionedProfile profile,
|
||||
final UUID accountIdentifier,
|
||||
final UUID phoneNumberIdentifier) {
|
||||
|
||||
try {
|
||||
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
|
||||
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedCredentialRequest));
|
||||
|
||||
return zkProfileOperations.issuePniCredential(request, accountIdentifier, phoneNumberIdentifier, commitment);
|
||||
} catch (DecoderException | VerificationFailedException | InvalidInputException e) {
|
||||
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
||||
}
|
||||
}
|
||||
|
||||
private ProfileAvatarUploadAttributes generateAvatarUploadForm(String objectName) {
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
@@ -513,4 +475,42 @@ public class ProfileController {
|
||||
|
||||
return new ArrayList<>(result.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the requester has permission to view the profile of the account identified by the given ACI.
|
||||
*
|
||||
* @param maybeRequester the authenticated account requesting the profile, if any
|
||||
* @param maybeAccessKey an anonymous access key for the target account
|
||||
* @param targetUuid the ACI of the target account
|
||||
*
|
||||
* @return the target account
|
||||
*
|
||||
* @throws RateLimitExceededException if the requester must wait before requesting the target account's profile
|
||||
* @throws NotFoundException if no account was found for the target ACI
|
||||
* @throws NotAuthorizedException if the requester is not authorized to receive the target account's profile or if the
|
||||
* requester was not authenticated and did not present an anonymous access key
|
||||
*/
|
||||
private Account verifyPermissionToReceiveProfile(final Optional<Account> maybeRequester,
|
||||
final Optional<Anonymous> maybeAccessKey,
|
||||
final UUID targetUuid) throws RateLimitExceededException {
|
||||
|
||||
if (maybeRequester.isEmpty() && maybeAccessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (maybeRequester.isPresent()) {
|
||||
rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid());
|
||||
}
|
||||
|
||||
final Optional<Account> maybeTargetAccount = accountsManager.getByAccountIdentifier(targetUuid);
|
||||
|
||||
OptionalAccess.verify(maybeRequester, maybeAccessKey, maybeTargetAccount);
|
||||
assert maybeTargetAccount.isPresent();
|
||||
|
||||
return maybeTargetAccount.get();
|
||||
}
|
||||
|
||||
private boolean isSelfProfileRequest(final Optional<Account> maybeRequester, final UUID targetUuid) {
|
||||
return maybeRequester.map(requester -> requester.getUuid().equals(targetUuid)).orElse(false);
|
||||
}
|
||||
}
|
||||
|
||||
+56
-43
@@ -21,7 +21,6 @@ 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;
|
||||
@@ -55,6 +54,7 @@ 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.apache.commons.lang3.StringUtils;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.receipts.ReceiptCredentialRequest;
|
||||
@@ -562,10 +562,13 @@ public class SubscriptionController {
|
||||
return stripeManager.getPaymentIntent(request.getPaymentIntentId())
|
||||
.thenCompose(paymentIntent -> {
|
||||
if (paymentIntent == null) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
if (StringUtils.equalsIgnoreCase("processing", paymentIntent.getStatus())) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
if (!"succeeded".equalsIgnoreCase(paymentIntent.getStatus())) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
if (!StringUtils.equalsIgnoreCase("succeeded", paymentIntent.getStatus())) {
|
||||
throw new WebApplicationException(Status.PAYMENT_REQUIRED);
|
||||
}
|
||||
ReceiptCredentialRequest receiptCredentialRequest;
|
||||
try {
|
||||
@@ -603,6 +606,7 @@ public class SubscriptionController {
|
||||
private final boolean cancelAtPeriodEnd;
|
||||
private final String currency;
|
||||
private final BigDecimal amount;
|
||||
private final String status;
|
||||
|
||||
public Subscription(
|
||||
@JsonProperty("level") long level,
|
||||
@@ -611,7 +615,8 @@ public class SubscriptionController {
|
||||
@JsonProperty("active") boolean active,
|
||||
@JsonProperty("cancelAtPeriodEnd") boolean cancelAtPeriodEnd,
|
||||
@JsonProperty("currency") String currency,
|
||||
@JsonProperty("amount") BigDecimal amount) {
|
||||
@JsonProperty("amount") BigDecimal amount,
|
||||
@JsonProperty("status") String status) {
|
||||
this.level = level;
|
||||
this.billingCycleAnchor = billingCycleAnchor;
|
||||
this.endOfCurrentPeriod = endOfCurrentPeriod;
|
||||
@@ -619,6 +624,7 @@ public class SubscriptionController {
|
||||
this.cancelAtPeriodEnd = cancelAtPeriodEnd;
|
||||
this.currency = currency;
|
||||
this.amount = amount;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public long getLevel() {
|
||||
@@ -648,6 +654,10 @@ public class SubscriptionController {
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
private final Subscription subscription;
|
||||
@@ -688,7 +698,8 @@ public class SubscriptionController {
|
||||
Objects.equals(subscription.getStatus(), "active"),
|
||||
subscription.getCancelAtPeriodEnd(),
|
||||
price.getCurrency().toUpperCase(Locale.ROOT),
|
||||
price.getUnitAmountDecimal()
|
||||
price.getUnitAmountDecimal(),
|
||||
subscription.getStatus()
|
||||
))).build())));
|
||||
});
|
||||
}
|
||||
@@ -730,7 +741,7 @@ public class SubscriptionController {
|
||||
@Path("/{subscriberId}/receipt_credentials")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getReceiptCredentials(
|
||||
public CompletableFuture<Response> createSubscriptionReceiptCredentials(
|
||||
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("subscriberId") String subscriberId,
|
||||
@Valid GetReceiptCredentialsRequest request) {
|
||||
@@ -739,7 +750,7 @@ public class SubscriptionController {
|
||||
.thenApply(this::requireRecordFromGetResult)
|
||||
.thenCompose(record -> {
|
||||
if (record.subscriptionId == null) {
|
||||
return CompletableFuture.completedFuture(Response.noContent().build());
|
||||
return CompletableFuture.completedFuture(Response.status(Status.NOT_FOUND).build());
|
||||
}
|
||||
ReceiptCredentialRequest receiptCredentialRequest;
|
||||
try {
|
||||
@@ -747,29 +758,20 @@ public class SubscriptionController {
|
||||
} 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);
|
||||
})
|
||||
return stripeManager.getLatestInvoiceForSubscription(record.subscriptionId)
|
||||
.thenCompose(invoice -> convertInvoiceToReceipt(invoice, record.subscriptionId))
|
||||
.thenCompose(receipt -> 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();
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -799,35 +801,46 @@ public class SubscriptionController {
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Receipt> checkNextInvoice(Iterator<Invoice> invoiceIterator, String subscriptionId) {
|
||||
if (!invoiceIterator.hasNext()) {
|
||||
return null;
|
||||
private CompletableFuture<Receipt> convertInvoiceToReceipt(Invoice latestSubscriptionInvoice, String subscriptionId) {
|
||||
if (latestSubscriptionInvoice == null) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
if (StringUtils.equalsIgnoreCase("open", latestSubscriptionInvoice.getStatus())) {
|
||||
throw new WebApplicationException(Status.NO_CONTENT);
|
||||
}
|
||||
if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) {
|
||||
throw new WebApplicationException(Status.PAYMENT_REQUIRED);
|
||||
}
|
||||
|
||||
Invoice invoice = invoiceIterator.next();
|
||||
return stripeManager.getInvoiceLineItemsForInvoice(invoice).thenCompose(invoiceLineItems -> {
|
||||
return stripeManager.getInvoiceLineItemsForInvoice(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> {
|
||||
Collection<InvoiceLineItem> subscriptionLineItems = invoiceLineItems.stream()
|
||||
.filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType()))
|
||||
.collect(Collectors.toList());
|
||||
if (subscriptionLineItems.isEmpty()) {
|
||||
return checkNextInvoice(invoiceIterator, subscriptionId);
|
||||
throw new IllegalStateException("latest subscription invoice has no subscription line items; subscriptionId="
|
||||
+ subscriptionId + "; invoiceId=" + latestSubscriptionInvoice.getId());
|
||||
}
|
||||
if (subscriptionLineItems.size() > 1) {
|
||||
throw new IllegalStateException("invoice has more than one subscription; subscriptionId=" + subscriptionId
|
||||
+ "; count=" + subscriptionLineItems.size());
|
||||
throw new IllegalStateException(
|
||||
"latest subscription invoice has too many subscription line items; subscriptionId=" + subscriptionId
|
||||
+ "; invoiceId=" + latestSubscriptionInvoice.getId() + "; 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()));
|
||||
return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem);
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Receipt> getReceiptForSubscriptionInvoiceLineItem(InvoiceLineItem subscriptionLineItem) {
|
||||
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");
|
||||
|
||||
+28
-26
@@ -23,32 +23,34 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||
@Path("/v1/voice/")
|
||||
public class VoiceVerificationController {
|
||||
|
||||
private static final String PLAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<Response>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Pause length=\"1\"/>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Pause length=\"1\"/>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
" <Play>%s</Play>\n" +
|
||||
"</Response>";
|
||||
private static final String PLAY_TWIML = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Response>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Pause length="1"/>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Pause length="1"/>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
<Play>%s</Play>
|
||||
</Response>
|
||||
""";
|
||||
|
||||
private static final String DEFAULT_LOCALE = "en-US";
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import javax.validation.constraints.Size;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public class AccountAttributes {
|
||||
|
||||
@@ -25,6 +26,7 @@ public class AccountAttributes {
|
||||
private String registrationLock;
|
||||
|
||||
@JsonProperty
|
||||
@ExactlySize({0, 16})
|
||||
private byte[] unidentifiedAccessKey;
|
||||
|
||||
@JsonProperty
|
||||
@@ -80,4 +82,9 @@ public class AccountAttributes {
|
||||
public boolean isDiscoverableByPhoneNumber() {
|
||||
return discoverableByPhoneNumber;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setUnidentifiedAccessKey(final byte[] unidentifiedAccessKey) {
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-6
@@ -8,29 +8,32 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AccountCreationResult {
|
||||
|
||||
@JsonProperty
|
||||
private final UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private final String number;
|
||||
private final UUID pni;
|
||||
|
||||
@Nullable
|
||||
private final String username;
|
||||
|
||||
@JsonProperty
|
||||
private final boolean storageCapable;
|
||||
|
||||
@JsonCreator
|
||||
public AccountCreationResult(
|
||||
@JsonProperty("uuid") final UUID uuid,
|
||||
@JsonProperty("number") final String number,
|
||||
@JsonProperty("pni") final UUID pni,
|
||||
@JsonProperty("username") @Nullable final String username,
|
||||
@JsonProperty("storageCapable") final boolean storageCapable) {
|
||||
|
||||
this.uuid = uuid;
|
||||
this.number = number;
|
||||
this.pni = pni;
|
||||
this.username = username;
|
||||
this.storageCapable = storageCapable;
|
||||
}
|
||||
|
||||
@@ -42,6 +45,15 @@ public class AccountCreationResult {
|
||||
return number;
|
||||
}
|
||||
|
||||
public UUID getPni() {
|
||||
return pni;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public boolean isStorageCapable() {
|
||||
return storageCapable;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,15 @@ public class Badge {
|
||||
return svgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for old Android builds that expect this field to exist but don't care it's an empty string.
|
||||
*/
|
||||
@Deprecated
|
||||
@JsonProperty
|
||||
public String getImageUrl() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
|
||||
@@ -14,13 +14,11 @@ import javax.validation.constraints.NotEmpty;
|
||||
public class BadgeSvg {
|
||||
private final String light;
|
||||
private final String dark;
|
||||
private final String transparent;
|
||||
|
||||
@JsonCreator
|
||||
public BadgeSvg(
|
||||
@JsonProperty("light") final String light,
|
||||
@JsonProperty("dark") final String dark,
|
||||
@JsonProperty("transparent") final String transparent) {
|
||||
@JsonProperty("dark") final String dark) {
|
||||
if (Strings.isNullOrEmpty(light)) {
|
||||
throw new IllegalArgumentException("light cannot be empty");
|
||||
}
|
||||
@@ -29,10 +27,6 @@ public class BadgeSvg {
|
||||
throw new IllegalArgumentException("dark cannot be empty");
|
||||
}
|
||||
this.dark = dark;
|
||||
if (Strings.isNullOrEmpty(transparent)) {
|
||||
throw new IllegalArgumentException("transparent cannot be empty");
|
||||
}
|
||||
this.transparent = transparent;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
@@ -45,11 +39,6 @@ public class BadgeSvg {
|
||||
return dark;
|
||||
}
|
||||
|
||||
@NotEmpty
|
||||
public String getTransparent() {
|
||||
return transparent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
@@ -60,13 +49,12 @@ public class BadgeSvg {
|
||||
}
|
||||
BadgeSvg badgeSvg = (BadgeSvg) o;
|
||||
return Objects.equals(light, badgeSvg.light)
|
||||
&& Objects.equals(dark, badgeSvg.dark)
|
||||
&& Objects.equals(transparent, badgeSvg.transparent);
|
||||
&& Objects.equals(dark, badgeSvg.dark);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(light, dark, transparent);
|
||||
return Objects.hash(light, dark);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,7 +62,6 @@ public class BadgeSvg {
|
||||
return "BadgeSvg{" +
|
||||
"light='" + light + '\'' +
|
||||
", dark='" + dark + '\'' +
|
||||
", transparent='" + transparent + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class BaseProfileResponse {
|
||||
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private String unidentifiedAccess;
|
||||
|
||||
@JsonProperty
|
||||
private boolean unrestrictedUnidentifiedAccess;
|
||||
|
||||
@JsonProperty
|
||||
private UserCapabilities capabilities;
|
||||
|
||||
@JsonProperty
|
||||
private List<Badge> badges;
|
||||
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
|
||||
public BaseProfileResponse() {
|
||||
}
|
||||
|
||||
public BaseProfileResponse(final String identityKey,
|
||||
final String unidentifiedAccess,
|
||||
final boolean unrestrictedUnidentifiedAccess,
|
||||
final UserCapabilities capabilities,
|
||||
final List<Badge> badges,
|
||||
final UUID uuid) {
|
||||
|
||||
this.identityKey = identityKey;
|
||||
this.unidentifiedAccess = unidentifiedAccess;
|
||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
||||
this.capabilities = capabilities;
|
||||
this.badges = badges;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public String getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public String getUnidentifiedAccess() {
|
||||
return unidentifiedAccess;
|
||||
}
|
||||
|
||||
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||
return unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
||||
public UserCapabilities getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
public List<Badge> getBadges() {
|
||||
return badges;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -8,8 +8,9 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -42,8 +43,8 @@ public class CreateProfileRequest {
|
||||
private String paymentAddress;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private List<String> badgeIds = new ArrayList<>();
|
||||
@Nullable
|
||||
private List<String> badgeIds;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@@ -95,7 +96,7 @@ public class CreateProfileRequest {
|
||||
return StringUtils.stripToNull(paymentAddress);
|
||||
}
|
||||
|
||||
public List<String> getBadges() {
|
||||
return badgeIds;
|
||||
public Optional<List<String>> getBadges() {
|
||||
return Optional.ofNullable(badgeIds);
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
|
||||
public abstract class CredentialProfileResponse {
|
||||
|
||||
@JsonUnwrapped
|
||||
private VersionedProfileResponse versionedProfileResponse;
|
||||
|
||||
protected CredentialProfileResponse() {
|
||||
}
|
||||
|
||||
protected CredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse) {
|
||||
this.versionedProfileResponse = versionedProfileResponse;
|
||||
}
|
||||
|
||||
public VersionedProfileResponse getVersionedProfileResponse() {
|
||||
return versionedProfileResponse;
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -8,7 +8,14 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class DeviceResponse {
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private UUID pni;
|
||||
|
||||
@JsonProperty
|
||||
private long deviceId;
|
||||
@@ -16,10 +23,20 @@ public class DeviceResponse {
|
||||
@VisibleForTesting
|
||||
public DeviceResponse() {}
|
||||
|
||||
public DeviceResponse(long deviceId) {
|
||||
public DeviceResponse(UUID uuid, UUID pni, long deviceId) {
|
||||
this.uuid = uuid;
|
||||
this.pni = pni;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public UUID getPni() {
|
||||
return pni;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
+33
-60
@@ -5,60 +5,39 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class OutgoingMessageEntity {
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
private final UUID guid;
|
||||
private final int type;
|
||||
private final String relay;
|
||||
private final long timestamp;
|
||||
private final String source;
|
||||
private final UUID sourceUuid;
|
||||
private final int sourceDevice;
|
||||
private final UUID destinationUuid;
|
||||
private final byte[] message;
|
||||
private final byte[] content;
|
||||
private final long serverTimestamp;
|
||||
|
||||
@JsonIgnore
|
||||
private boolean cached;
|
||||
|
||||
@JsonProperty
|
||||
private UUID guid;
|
||||
|
||||
@JsonProperty
|
||||
private int type;
|
||||
|
||||
@JsonProperty
|
||||
private String relay;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
@JsonProperty
|
||||
private String source;
|
||||
|
||||
@JsonProperty
|
||||
private UUID sourceUuid;
|
||||
|
||||
@JsonProperty
|
||||
private int sourceDevice;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] message;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] content;
|
||||
|
||||
@JsonProperty
|
||||
private long serverTimestamp;
|
||||
|
||||
public OutgoingMessageEntity() {}
|
||||
|
||||
public OutgoingMessageEntity(long id, boolean cached,
|
||||
UUID guid, int type, String relay, long timestamp,
|
||||
String source, UUID sourceUuid, int sourceDevice,
|
||||
byte[] message, byte[] content, long serverTimestamp)
|
||||
@JsonCreator
|
||||
public OutgoingMessageEntity(@JsonProperty("guid") final UUID guid,
|
||||
@JsonProperty("type") final int type,
|
||||
@JsonProperty("relay") final String relay,
|
||||
@JsonProperty("timestamp") final long timestamp,
|
||||
@JsonProperty("source") final String source,
|
||||
@JsonProperty("sourceUuid") final UUID sourceUuid,
|
||||
@JsonProperty("sourceDevice") final int sourceDevice,
|
||||
@JsonProperty("destinationUuid") final UUID destinationUuid,
|
||||
@JsonProperty("message") final byte[] message,
|
||||
@JsonProperty("content") final byte[] content,
|
||||
@JsonProperty("serverTimestamp") final long serverTimestamp)
|
||||
{
|
||||
this.id = id;
|
||||
this.cached = cached;
|
||||
this.guid = guid;
|
||||
this.type = type;
|
||||
this.relay = relay;
|
||||
@@ -66,6 +45,7 @@ public class OutgoingMessageEntity {
|
||||
this.source = source;
|
||||
this.sourceUuid = sourceUuid;
|
||||
this.sourceDevice = sourceDevice;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.message = message;
|
||||
this.content = content;
|
||||
this.serverTimestamp = serverTimestamp;
|
||||
@@ -99,6 +79,10 @@ public class OutgoingMessageEntity {
|
||||
return sourceDevice;
|
||||
}
|
||||
|
||||
public UUID getDestinationUuid() {
|
||||
return destinationUuid;
|
||||
}
|
||||
|
||||
public byte[] getMessage() {
|
||||
return message;
|
||||
}
|
||||
@@ -107,16 +91,6 @@ public class OutgoingMessageEntity {
|
||||
return content;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isCached() {
|
||||
return cached;
|
||||
}
|
||||
|
||||
public long getServerTimestamp() {
|
||||
return serverTimestamp;
|
||||
}
|
||||
@@ -126,23 +100,22 @@ public class OutgoingMessageEntity {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final OutgoingMessageEntity that = (OutgoingMessageEntity)o;
|
||||
return id == that.id &&
|
||||
cached == that.cached &&
|
||||
type == that.type &&
|
||||
return type == that.type &&
|
||||
timestamp == that.timestamp &&
|
||||
sourceDevice == that.sourceDevice &&
|
||||
serverTimestamp == that.serverTimestamp &&
|
||||
Objects.equals(guid, that.guid) &&
|
||||
guid.equals(that.guid) &&
|
||||
Objects.equals(relay, that.relay) &&
|
||||
Objects.equals(source, that.source) &&
|
||||
Objects.equals(sourceUuid, that.sourceUuid) &&
|
||||
destinationUuid.equals(that.destinationUuid) &&
|
||||
Arrays.equals(message, that.message) &&
|
||||
Arrays.equals(content, that.content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(id, cached, guid, type, relay, timestamp, source, sourceUuid, sourceDevice, serverTimestamp);
|
||||
int result = Objects.hash(guid, type, relay, timestamp, source, sourceUuid, sourceDevice, destinationUuid, serverTimestamp);
|
||||
result = 31 * result + Arrays.hashCode(message);
|
||||
result = 31 * result + Arrays.hashCode(content);
|
||||
return result;
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import javax.annotation.Nullable;
|
||||
import org.signal.zkgroup.profiles.PniCredentialResponse;
|
||||
|
||||
public class PniCredentialProfileResponse extends CredentialProfileResponse {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = PniCredentialResponseAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = PniCredentialResponseAdapter.Deserializing.class)
|
||||
@Nullable
|
||||
private PniCredentialResponse pniCredential;
|
||||
|
||||
public PniCredentialProfileResponse() {
|
||||
}
|
||||
|
||||
public PniCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse,
|
||||
@Nullable final PniCredentialResponse pniCredential) {
|
||||
|
||||
super(versionedProfileResponse);
|
||||
this.pniCredential = pniCredential;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PniCredentialResponse getPniCredential() {
|
||||
return pniCredential;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.PniCredentialResponse;
|
||||
|
||||
public class PniCredentialResponseAdapter {
|
||||
|
||||
public static class Serializing extends JsonSerializer<PniCredentialResponse> {
|
||||
@Override
|
||||
public void serialize(PniCredentialResponse response, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
if (response == null) jsonGenerator.writeNull();
|
||||
else jsonGenerator.writeString(Base64.getEncoder().encodeToString(response.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializing extends JsonDeserializer<PniCredentialResponse> {
|
||||
@Override
|
||||
public PniCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException {
|
||||
try {
|
||||
return new PniCredentialResponse(Base64.getDecoder().decode(jsonParser.getValueAsString()));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
|
||||
public class Profile {
|
||||
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String about;
|
||||
|
||||
@JsonProperty
|
||||
private String aboutEmoji;
|
||||
|
||||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
@JsonProperty
|
||||
private String paymentAddress;
|
||||
|
||||
@JsonProperty
|
||||
private String unidentifiedAccess;
|
||||
|
||||
@JsonProperty
|
||||
private boolean unrestrictedUnidentifiedAccess;
|
||||
|
||||
@JsonProperty
|
||||
private UserCapabilities capabilities;
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private List<Badge> badges;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ProfileKeyCredentialResponseAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
|
||||
private ProfileKeyCredentialResponse credential;
|
||||
|
||||
public Profile() {}
|
||||
|
||||
public Profile(
|
||||
String name, String about, String aboutEmoji, String avatar, String paymentAddress, String identityKey,
|
||||
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess, UserCapabilities capabilities, String username,
|
||||
UUID uuid, List<Badge> badges, ProfileKeyCredentialResponse credential)
|
||||
{
|
||||
this.name = name;
|
||||
this.about = about;
|
||||
this.aboutEmoji = aboutEmoji;
|
||||
this.avatar = avatar;
|
||||
this.paymentAddress = paymentAddress;
|
||||
this.identityKey = identityKey;
|
||||
this.unidentifiedAccess = unidentifiedAccess;
|
||||
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
|
||||
this.capabilities = capabilities;
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
this.badges = badges;
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getAbout() {
|
||||
return about;
|
||||
}
|
||||
|
||||
public String getAboutEmoji() {
|
||||
return aboutEmoji;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public String getPaymentAddress() {
|
||||
return paymentAddress;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getUnidentifiedAccess() {
|
||||
return unidentifiedAccess;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean isUnrestrictedUnidentifiedAccess() {
|
||||
return unrestrictedUnidentifiedAccess;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public UserCapabilities getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public List<Badge> getBadges() {
|
||||
return badges;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ProfileKeyCredentialProfileResponse extends CredentialProfileResponse {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ProfileKeyCredentialResponseAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
|
||||
@Nullable
|
||||
private ProfileKeyCredentialResponse credential;
|
||||
|
||||
public ProfileKeyCredentialProfileResponse() {
|
||||
}
|
||||
|
||||
public ProfileKeyCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse,
|
||||
@Nullable final ProfileKeyCredentialResponse credential) {
|
||||
|
||||
super(versionedProfileResponse);
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ProfileKeyCredentialResponse getCredential() {
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class VersionedProfileResponse {
|
||||
|
||||
@JsonUnwrapped
|
||||
private BaseProfileResponse baseProfileResponse;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String about;
|
||||
|
||||
@JsonProperty
|
||||
private String aboutEmoji;
|
||||
|
||||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
@JsonProperty
|
||||
private String paymentAddress;
|
||||
|
||||
public VersionedProfileResponse() {
|
||||
}
|
||||
|
||||
public VersionedProfileResponse(final BaseProfileResponse baseProfileResponse,
|
||||
final String name,
|
||||
final String about,
|
||||
final String aboutEmoji,
|
||||
final String avatar,
|
||||
final String paymentAddress) {
|
||||
|
||||
this.baseProfileResponse = baseProfileResponse;
|
||||
this.name = name;
|
||||
this.about = about;
|
||||
this.aboutEmoji = aboutEmoji;
|
||||
this.avatar = avatar;
|
||||
this.paymentAddress = paymentAddress;
|
||||
}
|
||||
|
||||
public BaseProfileResponse getBaseProfileResponse() {
|
||||
return baseProfileResponse;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getAbout() {
|
||||
return about;
|
||||
}
|
||||
|
||||
public String getAboutEmoji() {
|
||||
return aboutEmoji;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public String getPaymentAddress() {
|
||||
return paymentAddress;
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.experiment;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -18,115 +17,120 @@ import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* An experiment compares the results of two operations and records metrics to assess how frequently they match.
|
||||
*/
|
||||
public class Experiment {
|
||||
|
||||
private final String name;
|
||||
private final String name;
|
||||
|
||||
private final Timer matchTimer;
|
||||
private final Timer errorTimer;
|
||||
private final Timer matchTimer;
|
||||
private final Timer errorTimer;
|
||||
|
||||
private final Timer bothPresentMismatchTimer;
|
||||
private final Timer controlNullMismatchTimer;
|
||||
private final Timer experimentNullMismatchTimer;
|
||||
private final Timer bothPresentMismatchTimer;
|
||||
private final Timer controlNullMismatchTimer;
|
||||
private final Timer experimentNullMismatchTimer;
|
||||
|
||||
private static final String OUTCOME_TAG = "outcome";
|
||||
private static final String MATCH_OUTCOME = "match";
|
||||
private static final String MISMATCH_OUTCOME = "mismatch";
|
||||
private static final String ERROR_OUTCOME = "error";
|
||||
private static final String OUTCOME_TAG = "outcome";
|
||||
private static final String MATCH_OUTCOME = "match";
|
||||
private static final String MISMATCH_OUTCOME = "mismatch";
|
||||
private static final String ERROR_OUTCOME = "error";
|
||||
|
||||
private static final String MISMATCH_TYPE_TAG = "mismatchType";
|
||||
private static final String BOTH_PRESENT_MISMATCH = "bothPresent";
|
||||
private static final String CONTROL_NULL_MISMATCH = "controlResultNull";
|
||||
private static final String EXPERIMENT_NULL_MISMATCH = "experimentResultNull";
|
||||
private static final String MISMATCH_TYPE_TAG = "mismatchType";
|
||||
private static final String BOTH_PRESENT_MISMATCH = "bothPresent";
|
||||
private static final String CONTROL_NULL_MISMATCH = "controlResultNull";
|
||||
private static final String EXPERIMENT_NULL_MISMATCH = "experimentResultNull";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Experiment.class);
|
||||
private static final Logger log = LoggerFactory.getLogger(Experiment.class);
|
||||
|
||||
public Experiment(final String... names) {
|
||||
this(name(Experiment.class, names),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MATCH_OUTCOME),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, ERROR_OUTCOME),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG, BOTH_PRESENT_MISMATCH),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG, CONTROL_NULL_MISMATCH),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG, EXPERIMENT_NULL_MISMATCH));
|
||||
public Experiment(final String... names) {
|
||||
this(name(Experiment.class, names),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MATCH_OUTCOME),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, ERROR_OUTCOME),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG,
|
||||
BOTH_PRESENT_MISMATCH),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG,
|
||||
CONTROL_NULL_MISMATCH),
|
||||
Metrics.timer(name(Experiment.class, names), OUTCOME_TAG, MISMATCH_OUTCOME, MISMATCH_TYPE_TAG,
|
||||
EXPERIMENT_NULL_MISMATCH));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Experiment(final String name, final Timer matchTimer, final Timer errorTimer, final Timer bothPresentMismatchTimer,
|
||||
final Timer controlNullMismatchTimer, final Timer experimentNullMismatchTimer) {
|
||||
this.name = name;
|
||||
|
||||
this.matchTimer = matchTimer;
|
||||
this.errorTimer = errorTimer;
|
||||
|
||||
this.bothPresentMismatchTimer = bothPresentMismatchTimer;
|
||||
this.controlNullMismatchTimer = controlNullMismatchTimer;
|
||||
this.experimentNullMismatchTimer = experimentNullMismatchTimer;
|
||||
}
|
||||
|
||||
public <T> void compareFutureResult(final T expected, final CompletionStage<T> experimentStage) {
|
||||
final long startNanos = System.nanoTime();
|
||||
|
||||
experimentStage.whenComplete((actual, cause) -> {
|
||||
final long durationNanos = System.nanoTime() - startNanos;
|
||||
|
||||
if (cause != null) {
|
||||
recordError(cause, durationNanos);
|
||||
} else {
|
||||
recordResult(expected, actual, durationNanos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public <T> void compareSupplierResult(final T expected, final Supplier<T> experimentSupplier) {
|
||||
final long startNanos = System.nanoTime();
|
||||
|
||||
try {
|
||||
final T result = experimentSupplier.get();
|
||||
|
||||
recordResult(expected, result, System.nanoTime() - startNanos);
|
||||
} catch (final Exception e) {
|
||||
recordError(e, System.nanoTime() - startNanos);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Experiment(final String name, final Timer matchTimer, final Timer errorTimer, final Timer bothPresentMismatchTimer, final Timer controlNullMismatchTimer, final Timer experimentNullMismatchTimer) {
|
||||
this.name = name;
|
||||
public <T> void compareSupplierResultAsync(final T expected, final Supplier<T> experimentSupplier,
|
||||
final Executor executor) {
|
||||
final long startNanos = System.nanoTime();
|
||||
|
||||
this.matchTimer = matchTimer;
|
||||
this.errorTimer = errorTimer;
|
||||
|
||||
this.bothPresentMismatchTimer = bothPresentMismatchTimer;
|
||||
this.controlNullMismatchTimer = controlNullMismatchTimer;
|
||||
this.experimentNullMismatchTimer = experimentNullMismatchTimer;
|
||||
try {
|
||||
compareFutureResult(expected, CompletableFuture.supplyAsync(experimentSupplier, executor));
|
||||
} catch (final Exception e) {
|
||||
recordError(e, System.nanoTime() - startNanos);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void compareFutureResult(final T expected, final CompletionStage<T> experimentStage) {
|
||||
final long startNanos = System.nanoTime();
|
||||
private void recordError(final Throwable cause, final long durationNanos) {
|
||||
log.warn("Experiment {} threw an exception.", name, cause);
|
||||
errorTimer.record(durationNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
experimentStage.whenComplete((actual, cause) -> {
|
||||
final long durationNanos = System.nanoTime() - startNanos;
|
||||
@VisibleForTesting
|
||||
<T> void recordResult(final T expected, final T actual, final long durationNanos) {
|
||||
if (expected instanceof Optional && actual instanceof Optional) {
|
||||
recordResult(((Optional) expected).orElse(null), ((Optional) actual).orElse(null), durationNanos);
|
||||
} else {
|
||||
final Timer Timer;
|
||||
|
||||
if (cause != null) {
|
||||
recordError(cause, durationNanos);
|
||||
} else {
|
||||
recordResult(expected, actual, durationNanos);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public <T> void compareSupplierResult(final T expected, final Supplier<T> experimentSupplier) {
|
||||
final long startNanos = System.nanoTime();
|
||||
|
||||
try {
|
||||
final T result = experimentSupplier.get();
|
||||
|
||||
recordResult(expected, result, System.nanoTime() - startNanos);
|
||||
} catch (final Exception e) {
|
||||
recordError(e, System.nanoTime() - startNanos);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void compareSupplierResultAsync(final T expected, final Supplier<T> experimentSupplier, final Executor executor) {
|
||||
final long startNanos = System.nanoTime();
|
||||
|
||||
try {
|
||||
compareFutureResult(expected, CompletableFuture.supplyAsync(experimentSupplier, executor));
|
||||
} catch (final Exception e) {
|
||||
recordError(e, System.nanoTime() - startNanos);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordError(final Throwable cause, final long durationNanos) {
|
||||
log.warn("Experiment {} threw an exception.", name, cause);
|
||||
errorTimer.record(durationNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
<T> void recordResult(final T expected, final T actual, final long durationNanos) {
|
||||
if (expected instanceof Optional && actual instanceof Optional) {
|
||||
recordResult(((Optional)expected).orElse(null), ((Optional)actual).orElse(null), durationNanos);
|
||||
} else {
|
||||
final Timer Timer;
|
||||
|
||||
if (Objects.equals(expected, actual)) {
|
||||
Timer = matchTimer;
|
||||
} else if (expected == null) {
|
||||
Timer = controlNullMismatchTimer;
|
||||
} else if (actual == null) {
|
||||
Timer = experimentNullMismatchTimer;
|
||||
} else {
|
||||
Timer = bothPresentMismatchTimer;
|
||||
}
|
||||
|
||||
Timer.record(durationNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
if (Objects.equals(expected, actual)) {
|
||||
Timer = matchTimer;
|
||||
} else if (expected == null) {
|
||||
Timer = controlNullMismatchTimer;
|
||||
} else if (actual == null) {
|
||||
Timer = experimentNullMismatchTimer;
|
||||
} else {
|
||||
Timer = bothPresentMismatchTimer;
|
||||
}
|
||||
|
||||
Timer.record(durationNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.RateLimitConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
|
||||
public class DynamicRateLimiters {
|
||||
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
private final AtomicReference<RateLimiter> rateLimitResetLimiter;
|
||||
private final AtomicReference<RateLimiter> recaptchaChallengeAttemptLimiter;
|
||||
private final AtomicReference<RateLimiter> recaptchaChallengeSuccessLimiter;
|
||||
private final AtomicReference<RateLimiter> pushChallengeAttemptLimiter;
|
||||
private final AtomicReference<RateLimiter> pushChallengeSuccessLimiter;
|
||||
|
||||
public DynamicRateLimiters(final FaultTolerantRedisCluster rateLimitCluster,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
|
||||
this.cacheCluster = rateLimitCluster;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
|
||||
this.rateLimitResetLimiter = new AtomicReference<>(
|
||||
createRateLimitResetLimiter(this.cacheCluster,
|
||||
this.dynamicConfigurationManager.getConfiguration().getLimits().getRateLimitReset()));
|
||||
|
||||
this.recaptchaChallengeAttemptLimiter = new AtomicReference<>(createRecaptchaChallengeAttemptLimiter(
|
||||
this.cacheCluster,
|
||||
this.dynamicConfigurationManager.getConfiguration().getLimits().getRecaptchaChallengeAttempt()));
|
||||
|
||||
this.recaptchaChallengeSuccessLimiter = new AtomicReference<>(createRecaptchaChallengeSuccessLimiter(
|
||||
this.cacheCluster,
|
||||
this.dynamicConfigurationManager.getConfiguration().getLimits().getRecaptchaChallengeSuccess()));
|
||||
|
||||
this.pushChallengeAttemptLimiter = new AtomicReference<>(createPushChallengeAttemptLimiter(this.cacheCluster,
|
||||
this.dynamicConfigurationManager.getConfiguration().getLimits().getPushChallengeAttempt()));
|
||||
|
||||
this.pushChallengeSuccessLimiter = new AtomicReference<>(createPushChallengeSuccessLimiter(this.cacheCluster,
|
||||
this.dynamicConfigurationManager.getConfiguration().getLimits().getPushChallengeSuccess()));
|
||||
}
|
||||
|
||||
public RateLimiter getRateLimitResetLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
rateLimitResetLimiter,
|
||||
dynamicConfigurationManager.getConfiguration().getLimits().getRateLimitReset(),
|
||||
this::createRateLimitResetLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getRecaptchaChallengeAttemptLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
recaptchaChallengeAttemptLimiter,
|
||||
dynamicConfigurationManager.getConfiguration().getLimits().getRecaptchaChallengeAttempt(),
|
||||
this::createRecaptchaChallengeAttemptLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getRecaptchaChallengeSuccessLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
recaptchaChallengeSuccessLimiter,
|
||||
dynamicConfigurationManager.getConfiguration().getLimits().getRecaptchaChallengeSuccess(),
|
||||
this::createRecaptchaChallengeSuccessLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getPushChallengeAttemptLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
pushChallengeAttemptLimiter,
|
||||
dynamicConfigurationManager.getConfiguration().getLimits().getPushChallengeAttempt(),
|
||||
this::createPushChallengeAttemptLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getPushChallengeSuccessLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
pushChallengeSuccessLimiter,
|
||||
dynamicConfigurationManager.getConfiguration().getLimits().getPushChallengeSuccess(),
|
||||
this::createPushChallengeSuccessLimiter);
|
||||
}
|
||||
|
||||
private RateLimiter updateAndGetRateLimiter(final AtomicReference<RateLimiter> rateLimiter,
|
||||
RateLimitConfiguration currentConfiguration,
|
||||
BiFunction<FaultTolerantRedisCluster, RateLimitConfiguration, RateLimiter> rateLimitFactory) {
|
||||
|
||||
return rateLimiter.updateAndGet(limiter -> {
|
||||
if (limiter.hasConfiguration(currentConfiguration)) {
|
||||
return limiter;
|
||||
} else {
|
||||
return rateLimitFactory.apply(cacheCluster, currentConfiguration);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public RateLimiter createRateLimitResetLimiter(FaultTolerantRedisCluster cacheCluster,
|
||||
RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "rateLimitReset");
|
||||
}
|
||||
|
||||
public RateLimiter createRecaptchaChallengeAttemptLimiter(FaultTolerantRedisCluster cacheCluster,
|
||||
RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "recaptchaChallengeAttempt");
|
||||
}
|
||||
|
||||
public RateLimiter createRecaptchaChallengeSuccessLimiter(FaultTolerantRedisCluster cacheCluster,
|
||||
RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "recaptchaChallengeSuccess");
|
||||
}
|
||||
|
||||
public RateLimiter createPushChallengeAttemptLimiter(FaultTolerantRedisCluster cacheCluster,
|
||||
RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "pushChallengeAttempt");
|
||||
}
|
||||
|
||||
public RateLimiter createPushChallengeSuccessLimiter(FaultTolerantRedisCluster cacheCluster,
|
||||
RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "pushChallengeSuccess");
|
||||
}
|
||||
|
||||
private RateLimiter createLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration,
|
||||
String name) {
|
||||
return new RateLimiter(cacheCluster, name,
|
||||
configuration.getBucketSize(),
|
||||
configuration.getLeakRatePerMinute());
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import io.dropwizard.util.Duration;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class PreKeyRateLimiter {
|
||||
|
||||
private static final String RATE_LIMIT_RESET_COUNTER_NAME = name(PreKeyRateLimiter.class, "reset");
|
||||
private static final String RATE_LIMITED_PREKEYS_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimited");
|
||||
private static final String RATE_LIMITED_PREKEYS_TOTAL_ACCOUNTS_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimitedTotal");
|
||||
private static final String RATE_LIMITED_PREKEYS_ACCOUNTS_ENFORCED_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimitedAccountsEnforced");
|
||||
private static final String RATE_LIMITED_PREKEYS_ACCOUNTS_UNENFORCED_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimitedAccountsUnenforced");
|
||||
|
||||
private static final String RATE_LIMITED_ACCOUNTS_HLL_KEY = "PreKeyRateLimiter::rateLimitedAccounts";
|
||||
private static final String RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY = "PreKeyRateLimiter::rateLimitedAccounts::enforced";
|
||||
private static final String RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY = "PreKeyRateLimiter::rateLimitedAccounts::unenforced";
|
||||
private static final long RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS = Duration.days(1).toSeconds();
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final RateLimitResetMetricsManager metricsManager;
|
||||
|
||||
public PreKeyRateLimiter(final RateLimiters rateLimiters,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final RateLimitResetMetricsManager metricsManager) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.metricsManager = metricsManager;
|
||||
|
||||
metricsManager.initializeFunctionCounters(RATE_LIMITED_PREKEYS_TOTAL_ACCOUNTS_COUNTER_NAME,
|
||||
RATE_LIMITED_ACCOUNTS_HLL_KEY);
|
||||
metricsManager.initializeFunctionCounters(RATE_LIMITED_PREKEYS_ACCOUNTS_ENFORCED_COUNTER_NAME,
|
||||
RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY);
|
||||
metricsManager.initializeFunctionCounters(RATE_LIMITED_PREKEYS_ACCOUNTS_UNENFORCED_COUNTER_NAME,
|
||||
RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY);
|
||||
}
|
||||
|
||||
public void validate(final Account account) throws RateLimitExceededException {
|
||||
|
||||
try {
|
||||
rateLimiters.getDailyPreKeysLimiter().validate(account.getUuid());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final boolean enforceLimit = dynamicConfigurationManager.getConfiguration()
|
||||
.getRateLimitChallengeConfiguration().isPreKeyLimitEnforced();
|
||||
|
||||
metricsManager.recordMetrics(account, enforceLimit,
|
||||
RATE_LIMITED_PREKEYS_COUNTER_NAME,
|
||||
enforceLimit ? RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY : RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY,
|
||||
RATE_LIMITED_ACCOUNTS_HLL_KEY,
|
||||
RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS
|
||||
);
|
||||
|
||||
if (enforceLimit) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRateLimitReset(final Account account) {
|
||||
|
||||
rateLimiters.getDailyPreKeysLimiter().clear(account.getUuid());
|
||||
|
||||
Metrics.counter(RATE_LIMIT_RESET_COUNTER_NAME, "countryCode", Util.getCountryCode(account.getNumber()))
|
||||
.increment();
|
||||
}
|
||||
}
|
||||
+11
-55
@@ -2,35 +2,26 @@ package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.abuse.RateLimitChallengeListener;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
public class RateLimitChallengeManager {
|
||||
|
||||
private final PushChallengeManager pushChallengeManager;
|
||||
private final RecaptchaClient recaptchaClient;
|
||||
|
||||
private final PreKeyRateLimiter preKeyRateLimiter;
|
||||
private final UnsealedSenderRateLimiter unsealedSenderRateLimiter;
|
||||
private final DynamicRateLimiters rateLimiters;
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
public static final String OPTION_RECAPTCHA = "recaptcha";
|
||||
public static final String OPTION_PUSH_CHALLENGE = "pushChallenge";
|
||||
private final List<RateLimitChallengeListener> rateLimitChallengeListeners =
|
||||
Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private static final String RECAPTCHA_ATTEMPT_COUNTER_NAME = name(RateLimitChallengeManager.class, "recaptcha", "attempt");
|
||||
private static final String RESET_RATE_LIMIT_EXCEEDED_COUNTER_NAME = name(RateLimitChallengeManager.class, "resetRateLimitExceeded");
|
||||
@@ -41,17 +32,15 @@ public class RateLimitChallengeManager {
|
||||
public RateLimitChallengeManager(
|
||||
final PushChallengeManager pushChallengeManager,
|
||||
final RecaptchaClient recaptchaClient,
|
||||
final PreKeyRateLimiter preKeyRateLimiter,
|
||||
final UnsealedSenderRateLimiter unsealedSenderRateLimiter,
|
||||
final RateLimiters rateLimiters,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
final DynamicRateLimiters rateLimiters) {
|
||||
|
||||
this.pushChallengeManager = pushChallengeManager;
|
||||
this.recaptchaClient = recaptchaClient;
|
||||
this.preKeyRateLimiter = preKeyRateLimiter;
|
||||
this.unsealedSenderRateLimiter = unsealedSenderRateLimiter;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
}
|
||||
|
||||
public void addListener(final RateLimitChallengeListener rateLimitChallengeListener) {
|
||||
rateLimitChallengeListeners.add(rateLimitChallengeListener);
|
||||
}
|
||||
|
||||
public void answerPushChallenge(final Account account, final String challenge) throws RateLimitExceededException {
|
||||
@@ -92,40 +81,7 @@ public class RateLimitChallengeManager {
|
||||
throw e;
|
||||
}
|
||||
|
||||
preKeyRateLimiter.handleRateLimitReset(account);
|
||||
unsealedSenderRateLimiter.handleRateLimitReset(account);
|
||||
}
|
||||
|
||||
public boolean isClientBelowMinimumVersion(final String userAgent) {
|
||||
try {
|
||||
final UserAgent client = UserAgentUtil.parseUserAgentString(userAgent);
|
||||
final Optional<Semver> minimumClientVersion = dynamicConfigurationManager.getConfiguration()
|
||||
.getRateLimitChallengeConfiguration()
|
||||
.getMinimumSupportedVersion(client.getPlatform());
|
||||
|
||||
return minimumClientVersion.map(version -> version.isGreaterThan(client.getVersion()))
|
||||
.orElse(true);
|
||||
} catch (final UnrecognizedUserAgentException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getChallengeOptions(final Account account) {
|
||||
final List<String> options = new ArrayList<>(2);
|
||||
|
||||
if (rateLimiters.getRecaptchaChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) &&
|
||||
rateLimiters.getRecaptchaChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) {
|
||||
|
||||
options.add(OPTION_RECAPTCHA);
|
||||
}
|
||||
|
||||
if (rateLimiters.getPushChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) &&
|
||||
rateLimiters.getPushChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) {
|
||||
|
||||
options.add(OPTION_PUSH_CHALLENGE);
|
||||
}
|
||||
|
||||
return options;
|
||||
rateLimitChallengeListeners.forEach(listener -> listener.handleRateLimitChallengeAnswered(account));
|
||||
}
|
||||
|
||||
public void sendPushChallenge(final Account account) throws NotPushRegisteredException {
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class RateLimitChallengeOptionManager {
|
||||
|
||||
private final DynamicRateLimiters rateLimiters;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
public static final String OPTION_RECAPTCHA = "recaptcha";
|
||||
public static final String OPTION_PUSH_CHALLENGE = "pushChallenge";
|
||||
|
||||
public RateLimitChallengeOptionManager(final DynamicRateLimiters rateLimiters,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
}
|
||||
|
||||
public boolean isClientBelowMinimumVersion(final String userAgent) {
|
||||
try {
|
||||
final UserAgent client = UserAgentUtil.parseUserAgentString(userAgent);
|
||||
final Optional<Semver> minimumClientVersion = dynamicConfigurationManager.getConfiguration()
|
||||
.getRateLimitChallengeConfiguration()
|
||||
.getMinimumSupportedVersion(client.getPlatform());
|
||||
|
||||
return minimumClientVersion.map(version -> version.isGreaterThan(client.getVersion()))
|
||||
.orElse(true);
|
||||
} catch (final UnrecognizedUserAgentException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getChallengeOptions(final Account account) {
|
||||
final List<String> options = new ArrayList<>(2);
|
||||
|
||||
if (rateLimiters.getRecaptchaChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) &&
|
||||
rateLimiters.getRecaptchaChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) {
|
||||
|
||||
options.add(OPTION_RECAPTCHA);
|
||||
}
|
||||
|
||||
if (rateLimiters.getPushChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) &&
|
||||
rateLimiters.getPushChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) {
|
||||
|
||||
options.add(OPTION_PUSH_CHALLENGE);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.FunctionCounter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
public class RateLimitResetMetricsManager {
|
||||
|
||||
private final FaultTolerantRedisCluster metricsCluster;
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public RateLimitResetMetricsManager(
|
||||
final FaultTolerantRedisCluster metricsCluster, final MeterRegistry meterRegistry) {
|
||||
this.metricsCluster = metricsCluster;
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
void initializeFunctionCounters(String counterKey, String hllKey) {
|
||||
FunctionCounter
|
||||
.builder(counterKey, this, manager -> manager.getCount(hllKey))
|
||||
.register(meterRegistry);
|
||||
}
|
||||
|
||||
Long getCount(final String hllKey) {
|
||||
return metricsCluster.<Long>withCluster(conn -> conn.sync().pfcount(hllKey));
|
||||
}
|
||||
|
||||
void recordMetrics(Account account, boolean enforced, String counterKey, String hllEnforcedKey, String hllTotalKey,
|
||||
long hllTtl) {
|
||||
|
||||
Counter.builder(counterKey)
|
||||
.tag("enforced", String.valueOf(enforced))
|
||||
.register(meterRegistry)
|
||||
.increment();
|
||||
|
||||
metricsCluster.useCluster(connection -> {
|
||||
connection.sync().pfadd(hllEnforcedKey, account.getUuid().toString());
|
||||
connection.sync().expire(hllEnforcedKey, hllTtl);
|
||||
connection.sync().pfadd(hllTotalKey, account.getUuid().toString());
|
||||
connection.sync().expire(hllTotalKey, hllTtl);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,8 @@
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.CardinalityRateLimitConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.RateLimitConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
|
||||
public class RateLimiters {
|
||||
|
||||
@@ -39,22 +33,9 @@ public class RateLimiters {
|
||||
private final RateLimiter usernameLookupLimiter;
|
||||
private final RateLimiter usernameSetLimiter;
|
||||
|
||||
private final AtomicReference<CardinalityRateLimiter> unsealedSenderCardinalityLimiter;
|
||||
private final AtomicReference<RateLimiter> unsealedIpLimiter;
|
||||
private final AtomicReference<RateLimiter> rateLimitResetLimiter;
|
||||
private final AtomicReference<RateLimiter> recaptchaChallengeAttemptLimiter;
|
||||
private final AtomicReference<RateLimiter> recaptchaChallengeSuccessLimiter;
|
||||
private final AtomicReference<RateLimiter> pushChallengeAttemptLimiter;
|
||||
private final AtomicReference<RateLimiter> pushChallengeSuccessLimiter;
|
||||
private final AtomicReference<RateLimiter> dailyPreKeysLimiter;
|
||||
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfig;
|
||||
|
||||
public RateLimiters(RateLimitsConfiguration config, DynamicConfigurationManager<DynamicConfiguration> dynamicConfig, FaultTolerantRedisCluster cacheCluster) {
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.dynamicConfig = dynamicConfig;
|
||||
private final RateLimiter checkAccountExistenceLimiter;
|
||||
|
||||
public RateLimiters(RateLimitsConfiguration config, FaultTolerantRedisCluster cacheCluster) {
|
||||
this.smsDestinationLimiter = new RateLimiter(cacheCluster, "smsDestination",
|
||||
config.getSmsDestination().getBucketSize(),
|
||||
config.getSmsDestination().getLeakRatePerMinute());
|
||||
@@ -127,92 +108,9 @@ public class RateLimiters {
|
||||
config.getUsernameSet().getBucketSize(),
|
||||
config.getUsernameSet().getLeakRatePerMinute());
|
||||
|
||||
this.dailyPreKeysLimiter = new AtomicReference<>(createDailyPreKeysLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getDailyPreKeys()));
|
||||
|
||||
this.unsealedSenderCardinalityLimiter = new AtomicReference<>(createUnsealedSenderCardinalityLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber()));
|
||||
this.unsealedIpLimiter = new AtomicReference<>(createUnsealedIpLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getUnsealedSenderIp()));
|
||||
|
||||
this.rateLimitResetLimiter = new AtomicReference<>(
|
||||
createRateLimitResetLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getRateLimitReset()));
|
||||
|
||||
this.recaptchaChallengeAttemptLimiter = new AtomicReference<>(createRecaptchaChallengeAttemptLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getRecaptchaChallengeAttempt()));
|
||||
this.recaptchaChallengeSuccessLimiter = new AtomicReference<>(createRecaptchaChallengeSuccessLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getRecaptchaChallengeSuccess()));
|
||||
this.pushChallengeAttemptLimiter = new AtomicReference<>(createPushChallengeAttemptLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getPushChallengeAttempt()));
|
||||
this.pushChallengeSuccessLimiter = new AtomicReference<>(createPushChallengeSuccessLimiter(cacheCluster, dynamicConfig.getConfiguration().getLimits().getPushChallengeSuccess()));
|
||||
}
|
||||
|
||||
public CardinalityRateLimiter getUnsealedSenderCardinalityLimiter() {
|
||||
CardinalityRateLimitConfiguration currentConfiguration = dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber();
|
||||
|
||||
return this.unsealedSenderCardinalityLimiter.updateAndGet(rateLimiter -> {
|
||||
if (rateLimiter.hasConfiguration(currentConfiguration)) {
|
||||
return rateLimiter;
|
||||
} else {
|
||||
return createUnsealedSenderCardinalityLimiter(cacheCluster, currentConfiguration);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public RateLimiter getUnsealedIpLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
unsealedIpLimiter,
|
||||
dynamicConfig.getConfiguration().getLimits().getUnsealedSenderIp(),
|
||||
this::createUnsealedIpLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getRateLimitResetLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
rateLimitResetLimiter,
|
||||
dynamicConfig.getConfiguration().getLimits().getRateLimitReset(),
|
||||
this::createRateLimitResetLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getRecaptchaChallengeAttemptLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
recaptchaChallengeAttemptLimiter,
|
||||
dynamicConfig.getConfiguration().getLimits().getRecaptchaChallengeAttempt(),
|
||||
this::createRecaptchaChallengeAttemptLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getRecaptchaChallengeSuccessLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
recaptchaChallengeSuccessLimiter,
|
||||
dynamicConfig.getConfiguration().getLimits().getRecaptchaChallengeSuccess(),
|
||||
this::createRecaptchaChallengeSuccessLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getPushChallengeAttemptLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
pushChallengeAttemptLimiter,
|
||||
dynamicConfig.getConfiguration().getLimits().getPushChallengeAttempt(),
|
||||
this::createPushChallengeAttemptLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getPushChallengeSuccessLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
pushChallengeSuccessLimiter,
|
||||
dynamicConfig.getConfiguration().getLimits().getPushChallengeSuccess(),
|
||||
this::createPushChallengeSuccessLimiter);
|
||||
}
|
||||
|
||||
public RateLimiter getDailyPreKeysLimiter() {
|
||||
return updateAndGetRateLimiter(
|
||||
dailyPreKeysLimiter,
|
||||
dynamicConfig.getConfiguration().getLimits().getDailyPreKeys(),
|
||||
this::createDailyPreKeysLimiter);
|
||||
}
|
||||
|
||||
private RateLimiter updateAndGetRateLimiter(final AtomicReference<RateLimiter> rateLimiter,
|
||||
RateLimitConfiguration currentConfiguration,
|
||||
BiFunction<FaultTolerantRedisCluster, RateLimitConfiguration, RateLimiter> rateLimitFactory) {
|
||||
|
||||
return rateLimiter.updateAndGet(limiter -> {
|
||||
if (limiter.hasConfiguration(currentConfiguration)) {
|
||||
return limiter;
|
||||
} else {
|
||||
return rateLimitFactory.apply(cacheCluster, currentConfiguration);
|
||||
}
|
||||
});
|
||||
this.checkAccountExistenceLimiter = new RateLimiter(cacheCluster, "checkAccountExistence",
|
||||
config.getCheckAccountExistence().getBucketSize(),
|
||||
config.getCheckAccountExistence().getLeakRatePerMinute());
|
||||
}
|
||||
|
||||
public RateLimiter getAllocateDeviceLimiter() {
|
||||
@@ -287,42 +185,7 @@ public class RateLimiters {
|
||||
return usernameSetLimiter;
|
||||
}
|
||||
|
||||
private CardinalityRateLimiter createUnsealedSenderCardinalityLimiter(FaultTolerantRedisCluster cacheCluster, CardinalityRateLimitConfiguration configuration) {
|
||||
return new CardinalityRateLimiter(cacheCluster, "unsealedSender", configuration.getTtl(), configuration.getMaxCardinality());
|
||||
}
|
||||
|
||||
private RateLimiter createUnsealedIpLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration)
|
||||
{
|
||||
return createLimiter(cacheCluster, configuration, "unsealedIp");
|
||||
}
|
||||
|
||||
public RateLimiter createRateLimitResetLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "rateLimitReset");
|
||||
}
|
||||
|
||||
public RateLimiter createRecaptchaChallengeAttemptLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "recaptchaChallengeAttempt");
|
||||
}
|
||||
|
||||
public RateLimiter createRecaptchaChallengeSuccessLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "recaptchaChallengeSuccess");
|
||||
}
|
||||
|
||||
public RateLimiter createPushChallengeAttemptLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "pushChallengeAttempt");
|
||||
}
|
||||
|
||||
public RateLimiter createPushChallengeSuccessLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "pushChallengeSuccess");
|
||||
}
|
||||
|
||||
public RateLimiter createDailyPreKeysLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration) {
|
||||
return createLimiter(cacheCluster, configuration, "dailyPreKeys");
|
||||
}
|
||||
|
||||
private RateLimiter createLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration, String name) {
|
||||
return new RateLimiter(cacheCluster, name,
|
||||
configuration.getBucketSize(),
|
||||
configuration.getLeakRatePerMinute());
|
||||
public RateLimiter getCheckAccountExistenceLimiter() {
|
||||
return checkAccountExistenceLimiter;
|
||||
}
|
||||
}
|
||||
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import io.dropwizard.util.Duration;
|
||||
import io.lettuce.core.SetArgs;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class UnsealedSenderRateLimiter {
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final FaultTolerantRedisCluster rateLimitCluster;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final RateLimitResetMetricsManager metricsManager;
|
||||
|
||||
private static final String RATE_LIMIT_RESET_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "reset");
|
||||
private static final String RATE_LIMITED_UNSEALED_SENDER_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimited");
|
||||
private static final String RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_TOTAL_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimitedAccountsTotal");
|
||||
private static final String RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_ENFORCED_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimitedAccountsEnforced");
|
||||
private static final String RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_UNENFORCED_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimitedAccountsUnenforced");
|
||||
|
||||
private static final String RATE_LIMITED_ACCOUNTS_HLL_KEY = "UnsealedSenderRateLimiter::rateLimitedAccounts::total";
|
||||
private static final String RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY = "UnsealedSenderRateLimiter::rateLimitedAccounts::enforced";
|
||||
private static final String RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY = "UnsealedSenderRateLimiter::rateLimitedAccounts::unenforced";
|
||||
private static final long RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS = Duration.days(1).toSeconds();
|
||||
|
||||
|
||||
public UnsealedSenderRateLimiter(final RateLimiters rateLimiters,
|
||||
final FaultTolerantRedisCluster rateLimitCluster,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final RateLimitResetMetricsManager metricsManager) {
|
||||
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.rateLimitCluster = rateLimitCluster;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.metricsManager = metricsManager;
|
||||
|
||||
metricsManager.initializeFunctionCounters(RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_TOTAL_COUNTER_NAME,
|
||||
RATE_LIMITED_ACCOUNTS_HLL_KEY);
|
||||
metricsManager.initializeFunctionCounters(RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_ENFORCED_COUNTER_NAME,
|
||||
RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY);
|
||||
metricsManager.initializeFunctionCounters(RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_UNENFORCED_COUNTER_NAME,
|
||||
RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY);
|
||||
}
|
||||
|
||||
public void validate(final Account sender, final Account destination) throws RateLimitExceededException {
|
||||
final int maxCardinality = rateLimitCluster.withCluster(connection -> {
|
||||
final String cardinalityString = connection.sync().get(getMaxCardinalityKey(sender));
|
||||
|
||||
return cardinalityString != null
|
||||
? Integer.parseInt(cardinalityString)
|
||||
: dynamicConfigurationManager.getConfiguration().getLimits().getUnsealedSenderDefaultCardinalityLimit();
|
||||
});
|
||||
|
||||
try {
|
||||
rateLimiters.getUnsealedSenderCardinalityLimiter()
|
||||
.validate(sender.getUuid().toString(), destination.getUuid().toString(), maxCardinality);
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final boolean enforceLimit = dynamicConfigurationManager.getConfiguration()
|
||||
.getRateLimitChallengeConfiguration().isUnsealedSenderLimitEnforced();
|
||||
|
||||
metricsManager.recordMetrics(sender, enforceLimit, RATE_LIMITED_UNSEALED_SENDER_COUNTER_NAME,
|
||||
enforceLimit ? RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY : RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY,
|
||||
RATE_LIMITED_ACCOUNTS_HLL_KEY,
|
||||
RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS
|
||||
);
|
||||
|
||||
if (enforceLimit) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRateLimitReset(final Account account) {
|
||||
rateLimitCluster.useCluster(connection -> {
|
||||
final CardinalityRateLimiter unsealedSenderCardinalityLimiter = rateLimiters.getUnsealedSenderCardinalityLimiter();
|
||||
final DynamicRateLimitsConfiguration rateLimitsConfiguration =
|
||||
dynamicConfigurationManager.getConfiguration().getLimits();
|
||||
|
||||
final long ttl;
|
||||
{
|
||||
final long remainingTtl = unsealedSenderCardinalityLimiter.getRemainingTtl(account.getUuid().toString());
|
||||
ttl = remainingTtl > 0 ? remainingTtl : unsealedSenderCardinalityLimiter.getInitialTtl().toSeconds();
|
||||
}
|
||||
|
||||
final String key = getMaxCardinalityKey(account);
|
||||
|
||||
connection.sync().set(key,
|
||||
String.valueOf(rateLimitsConfiguration.getUnsealedSenderDefaultCardinalityLimit()),
|
||||
SetArgs.Builder.nx().ex(ttl));
|
||||
|
||||
connection.sync().incrby(key, rateLimitsConfiguration.getUnsealedSenderPermitIncrement());
|
||||
});
|
||||
|
||||
Metrics.counter(RATE_LIMIT_RESET_COUNTER_NAME,
|
||||
"countryCode", Util.getCountryCode(account.getNumber())).increment();
|
||||
}
|
||||
|
||||
private static String getMaxCardinalityKey(final Account account) {
|
||||
return "max_unsealed_sender_cardinality::" + account.getUuid();
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.mappers;
|
||||
|
||||
import java.util.concurrent.CompletionException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
import javax.ws.rs.ext.Providers;
|
||||
|
||||
@Provider
|
||||
public class CompletionExceptionMapper implements ExceptionMapper<CompletionException> {
|
||||
|
||||
@Context
|
||||
private Providers providers;
|
||||
|
||||
@Override
|
||||
public Response toResponse(final CompletionException exception) {
|
||||
final Throwable cause = exception.getCause();
|
||||
|
||||
if (cause != null) {
|
||||
final Class type = cause.getClass();
|
||||
return providers.getExceptionMapper(type).toResponse(cause);
|
||||
}
|
||||
|
||||
return Response.serverError().build();
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -10,21 +10,21 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.entities.RateLimitChallenge;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeOptionManager;
|
||||
|
||||
public class RateLimitChallengeExceptionMapper implements ExceptionMapper<RateLimitChallengeException> {
|
||||
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
private final RateLimitChallengeOptionManager rateLimitChallengeOptionManager;
|
||||
|
||||
public RateLimitChallengeExceptionMapper(final RateLimitChallengeManager rateLimitChallengeManager) {
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
public RateLimitChallengeExceptionMapper(final RateLimitChallengeOptionManager rateLimitChallengeOptionManager) {
|
||||
this.rateLimitChallengeOptionManager = rateLimitChallengeOptionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response toResponse(final RateLimitChallengeException exception) {
|
||||
return Response.status(428)
|
||||
.entity(new RateLimitChallenge(UUID.randomUUID().toString(),
|
||||
rateLimitChallengeManager.getChallengeOptions(exception.getAccount())))
|
||||
rateLimitChallengeOptionManager.getChallengeOptions(exception.getAccount())))
|
||||
.header("Retry-After", exception.getRetryAfter().toSeconds())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ public class CpuUsageGauge extends CachedGauge<Integer> {
|
||||
|
||||
@Override
|
||||
protected Integer loadValue() {
|
||||
return (int) Math.ceil(operatingSystemMXBean.getSystemCpuLoad() * 100);
|
||||
return (int) Math.ceil(operatingSystemMXBean.getCpuLoad() * 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
|
||||
public class FreeMemoryGauge implements Gauge<Long> {
|
||||
@@ -21,6 +20,6 @@ public class FreeMemoryGauge implements Gauge<Long> {
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
return operatingSystemMXBean.getFreePhysicalMemorySize();
|
||||
return operatingSystemMXBean.getFreeMemorySize();
|
||||
}
|
||||
}
|
||||
|
||||
+145
-37
@@ -6,17 +6,34 @@
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import io.lettuce.core.SetArgs;
|
||||
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
/**
|
||||
* Measures and records the latency between sending a push notification to a device and that device draining its queue
|
||||
@@ -28,48 +45,139 @@ import java.util.concurrent.TimeUnit;
|
||||
* device and records the time elapsed since the push notification timestamp as a latency observation.
|
||||
*/
|
||||
public class PushLatencyManager {
|
||||
private static final String TIMER_NAME = MetricRegistry.name(PushLatencyManager.class, "latency");
|
||||
private static final int TTL = (int)Duration.ofDays(1).toSeconds();
|
||||
|
||||
private final FaultTolerantRedisCluster redisCluster;
|
||||
private final FaultTolerantRedisCluster redisCluster;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
public PushLatencyManager(final FaultTolerantRedisCluster redisCluster) {
|
||||
this.redisCluster = redisCluster;
|
||||
private final Clock clock;
|
||||
|
||||
private static final String TIMER_NAME = MetricRegistry.name(PushLatencyManager.class, "latency");
|
||||
private static final int TTL = (int) Duration.ofDays(1).toSeconds();
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PushLatencyManager.class);
|
||||
|
||||
@VisibleForTesting
|
||||
enum PushType {
|
||||
STANDARD,
|
||||
VOIP
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class PushRecord {
|
||||
private final Instant timestamp;
|
||||
|
||||
@Nullable
|
||||
private final PushType pushType;
|
||||
|
||||
@JsonCreator
|
||||
PushRecord(@JsonProperty("timestamp") final Instant timestamp,
|
||||
@JsonProperty("pushType") @Nullable final PushType pushType) {
|
||||
|
||||
this.timestamp = timestamp;
|
||||
this.pushType = pushType;
|
||||
}
|
||||
|
||||
public void recordPushSent(final UUID accountUuid, final long deviceId) {
|
||||
recordPushSent(accountUuid, deviceId, System.currentTimeMillis());
|
||||
public Instant getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void recordPushSent(final UUID accountUuid, final long deviceId, final long currentTime) {
|
||||
redisCluster.useCluster(connection ->
|
||||
connection.async().set(getFirstUnacknowledgedPushKey(accountUuid, deviceId), String.valueOf(currentTime), SetArgs.Builder.nx().ex(TTL)));
|
||||
@Nullable
|
||||
public PushType getPushType() {
|
||||
return pushType;
|
||||
}
|
||||
}
|
||||
|
||||
public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgent) {
|
||||
getLatencyAndClearTimestamp(accountUuid, deviceId, System.currentTimeMillis()).thenAccept(latency -> {
|
||||
if (latency != null) {
|
||||
Metrics.timer(TIMER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).record(latency, TimeUnit.MILLISECONDS);
|
||||
public PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
|
||||
this(redisCluster, dynamicConfigurationManager, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
PushLatencyManager(final FaultTolerantRedisCluster redisCluster,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final Clock clock) {
|
||||
|
||||
this.redisCluster = redisCluster;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public void recordPushSent(final UUID accountUuid, final long deviceId, final boolean isVoip) {
|
||||
try {
|
||||
final String recordJson = SystemMapper.getMapper().writeValueAsString(
|
||||
new PushRecord(Instant.now(clock), isVoip ? PushType.VOIP : PushType.STANDARD));
|
||||
|
||||
redisCluster.useCluster(connection ->
|
||||
connection.async().set(getFirstUnacknowledgedPushKey(accountUuid, deviceId),
|
||||
recordJson,
|
||||
SetArgs.Builder.nx().ex(TTL)));
|
||||
} catch (final JsonProcessingException e) {
|
||||
// This should never happen
|
||||
log.error("Failed to write push latency record JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void recordQueueRead(final UUID accountUuid, final long deviceId, final String userAgentString) {
|
||||
takePushRecord(accountUuid, deviceId).thenAccept(pushRecord -> {
|
||||
if (pushRecord != null) {
|
||||
final Duration latency = Duration.between(pushRecord.getTimestamp(), Instant.now());
|
||||
|
||||
final List<Tag> tags = new ArrayList<>(2);
|
||||
|
||||
tags.add(UserAgentTagUtil.getPlatformTag(userAgentString));
|
||||
|
||||
if (pushRecord.getPushType() != null) {
|
||||
tags.add(Tag.of("pushType", pushRecord.getPushType().name().toLowerCase()));
|
||||
}
|
||||
|
||||
try {
|
||||
final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString);
|
||||
|
||||
final Set<Semver> instrumentedVersions =
|
||||
dynamicConfigurationManager.getConfiguration().getPushLatencyConfiguration().getInstrumentedVersions()
|
||||
.getOrDefault(userAgent.getPlatform(), Collections.emptySet());
|
||||
|
||||
if (instrumentedVersions.contains(userAgent.getVersion())) {
|
||||
tags.add(Tag.of("clientVersion", userAgent.getVersion().toString()));
|
||||
}
|
||||
} catch (UnrecognizedUserAgentException ignored) {
|
||||
}
|
||||
|
||||
Metrics.timer(TIMER_NAME, tags).record(latency);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CompletableFuture<PushRecord> takePushRecord(final UUID accountUuid, final long deviceId) {
|
||||
final String key = getFirstUnacknowledgedPushKey(accountUuid, deviceId);
|
||||
|
||||
return redisCluster.withCluster(connection -> {
|
||||
final CompletableFuture<PushRecord> getFuture = connection.async().get(key).toCompletableFuture()
|
||||
.thenApply(recordJson -> {
|
||||
if (StringUtils.isNotEmpty(recordJson)) {
|
||||
try {
|
||||
return SystemMapper.getMapper().readValue(recordJson, PushRecord.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@VisibleForTesting
|
||||
CompletableFuture<Long> getLatencyAndClearTimestamp(final UUID accountUuid, final long deviceId, final long currentTimeMillis) {
|
||||
final String key = getFirstUnacknowledgedPushKey(accountUuid, deviceId);
|
||||
getFuture.whenComplete((record, cause) -> {
|
||||
if (cause == null) {
|
||||
connection.async().del(key);
|
||||
}
|
||||
});
|
||||
|
||||
return redisCluster.withCluster(connection -> {
|
||||
final RedisAdvancedClusterAsyncCommands<String, String> commands = connection.async();
|
||||
return getFuture;
|
||||
});
|
||||
}
|
||||
|
||||
final CompletableFuture<String> getFuture = commands.get(key).toCompletableFuture();
|
||||
commands.del(key);
|
||||
|
||||
return getFuture.thenApply(timestampString -> timestampString != null ? currentTimeMillis - Long.parseLong(timestampString, 10) : null);
|
||||
});
|
||||
}
|
||||
|
||||
private static String getFirstUnacknowledgedPushKey(final UUID accountUuid, final long deviceId) {
|
||||
return "push_latency::" + accountUuid.toString() + "::" + deviceId;
|
||||
}
|
||||
private static String getFirstUnacknowledgedPushKey(final UUID accountUuid, final long deviceId) {
|
||||
return "push_latency::v2::" + accountUuid.toString() + "::" + deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public class APNSender implements Managed {
|
||||
private void handleUnregisteredUser(String registrationId, UUID uuid, long deviceId) {
|
||||
// logger.info("Got APN Unregistered: " + number + "," + deviceId);
|
||||
|
||||
Optional<Account> account = accountsManager.get(uuid);
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
logger.info("No account found: {}", uuid);
|
||||
|
||||
@@ -99,7 +99,7 @@ public class ApnFallbackManager implements Managed {
|
||||
|
||||
final Optional<Account> maybeAccount = separated.map(Pair::first)
|
||||
.map(UUID::fromString)
|
||||
.flatMap(accountsManager::get);
|
||||
.flatMap(accountsManager::getByAccountIdentifier);
|
||||
|
||||
final Optional<Device> maybeDevice = separated.map(Pair::second)
|
||||
.flatMap(deviceId -> maybeAccount.flatMap(account -> account.getDevice(deviceId)));
|
||||
|
||||
+26
-14
@@ -171,8 +171,16 @@ public class ClientPresenceManager extends RedisClusterPubSubAdapter<String, Str
|
||||
}
|
||||
}
|
||||
|
||||
public void displacePresence(final UUID accountUuid, final long deviceId) {
|
||||
displacePresence(getPresenceKey(accountUuid, deviceId));
|
||||
public void disconnectPresence(final UUID accountUuid, final long deviceId) {
|
||||
final String presenceKey = getPresenceKey(accountUuid, deviceId);
|
||||
|
||||
if (isLocallyPresent(accountUuid, deviceId)) {
|
||||
displacePresence(presenceKey);
|
||||
}
|
||||
|
||||
// If connected locally, we still need to clean up the presence key.
|
||||
// If connected remotely, the other server will get a keyspace message and handle the disconnect
|
||||
presenceCluster.useCluster(connection -> connection.sync().del(presenceKey));
|
||||
}
|
||||
|
||||
private void displacePresence(final String presenceKey) {
|
||||
@@ -268,18 +276,22 @@ public class ClientPresenceManager extends RedisClusterPubSubAdapter<String, Str
|
||||
public void message(final RedisClusterNode node, final String channel, final String message) {
|
||||
pubSubMessageMeter.mark();
|
||||
|
||||
if ("set".equals(message) && channel.startsWith("__keyspace@0__:presence::{")) {
|
||||
// Another process has overwritten this presence key, which means the client has connected to another host.
|
||||
// At this point, we're on a Lettuce IO thread and need to dispatch to a separate thread before making
|
||||
// synchronous Lettuce calls to avoid deadlocking.
|
||||
keyspaceNotificationExecutorService.execute(() -> {
|
||||
try {
|
||||
displacePresence(channel.substring("__keyspace@0__:".length()));
|
||||
remoteDisplacementMeter.mark();
|
||||
} catch (final Exception e) {
|
||||
log.warn("Error displacing presence", e);
|
||||
}
|
||||
});
|
||||
if (channel.startsWith("__keyspace@0__:presence::{")) {
|
||||
if ("set".equals(message) || "del".equals(message)) {
|
||||
// for "set", another process has overwritten this presence key, which means the client has connected to another host.
|
||||
// for "del", another process has indicated the client should be disconnected
|
||||
|
||||
// At this point, we're on a Lettuce IO thread and need to dispatch to a separate thread before making
|
||||
// synchronous Lettuce calls to avoid deadlocking.
|
||||
keyspaceNotificationExecutorService.execute(() -> {
|
||||
try {
|
||||
displacePresence(channel.substring("__keyspace@0__:".length()));
|
||||
remoteDisplacementMeter.mark();
|
||||
} catch (final Exception e) {
|
||||
log.warn("Error displacing presence", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ public class GCMSender {
|
||||
}
|
||||
|
||||
private Optional<Account> getAccountForEvent(GcmMessage message) {
|
||||
Optional<Account> account = message.getUuid().flatMap(accountsManager::get);
|
||||
Optional<Account> account = message.getUuid().flatMap(accountsManager::getByAccountIdentifier);
|
||||
|
||||
if (account.isPresent()) {
|
||||
Optional<Device> device = account.get().getDevice(message.getDeviceId());
|
||||
|
||||
@@ -124,22 +124,24 @@ public class MessageSender implements Managed {
|
||||
|
||||
gcmSender.sendMessage(gcmMessage);
|
||||
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId()));
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), false));
|
||||
}
|
||||
|
||||
private void sendApnNotification(Account account, Device device) {
|
||||
ApnMessage apnMessage;
|
||||
|
||||
if (!Util.isEmpty(device.getVoipApnId())) {
|
||||
apnMessage = new ApnMessage(device.getVoipApnId(), account.getUuid(), device.getId(), true, Type.NOTIFICATION, Optional.empty());
|
||||
final boolean useVoip = !Util.isEmpty(device.getVoipApnId());
|
||||
|
||||
if (useVoip) {
|
||||
apnMessage = new ApnMessage(device.getVoipApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
|
||||
RedisOperation.unchecked(() -> apnFallbackManager.schedule(account, device));
|
||||
} else {
|
||||
apnMessage = new ApnMessage(device.getApnId(), account.getUuid(), device.getId(), false, Type.NOTIFICATION, Optional.empty());
|
||||
apnMessage = new ApnMessage(device.getApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
|
||||
}
|
||||
|
||||
apnSender.sendMessage(apnMessage);
|
||||
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId()));
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -33,7 +33,7 @@ public class ReceiptSender {
|
||||
return;
|
||||
}
|
||||
|
||||
final Account destinationAccount = accountManager.get(destinationUuid)
|
||||
final Account destinationAccount = accountManager.getByAccountIdentifier(destinationUuid)
|
||||
.orElseThrow(() -> new NoSuchUserException(destinationUuid));
|
||||
|
||||
final Envelope.Builder message = Envelope.newBuilder()
|
||||
@@ -41,6 +41,7 @@ public class ReceiptSender {
|
||||
.setSource(sourceAccount.getNumber())
|
||||
.setSourceUuid(sourceAccount.getUuid().toString())
|
||||
.setSourceDevice((int) source.getAuthenticatedDevice().getId())
|
||||
.setDestinationUuid(destinationUuid.toString())
|
||||
.setTimestamp(messageId)
|
||||
.setType(Envelope.Type.SERVER_DELIVERY_RECEIPT);
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ public class LegacyRecaptchaClient implements RecaptchaClient {
|
||||
.post(Entity.form(formData), VerifyResponse.class);
|
||||
|
||||
if (response.success) {
|
||||
logger.info("Got successful captcha time: " + response.challenge_ts + ", current time: " + System.currentTimeMillis());
|
||||
logger.debug("Got successful captcha time: " + response.challenge_ts + ", current time: " + System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return response.success;
|
||||
|
||||
+1
-22
@@ -7,27 +7,6 @@ package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AbusiveHostRule {
|
||||
public record AbusiveHostRule(String host, boolean blocked, List<String> regions) {
|
||||
|
||||
private final String host;
|
||||
private final boolean blocked;
|
||||
private final List<String> regions;
|
||||
|
||||
public AbusiveHostRule(String host, boolean blocked, List<String> regions) {
|
||||
this.host = host;
|
||||
this.blocked = blocked;
|
||||
this.regions = regions;
|
||||
}
|
||||
|
||||
public List<String> getRegions() {
|
||||
return regions;
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return blocked;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-17
@@ -5,31 +5,35 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.storage.mappers.AbusiveHostRuleRowMapper;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
public class AbusiveHostRules {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String HOST = "host";
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbusiveHostRules.class);
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String HOST = "host";
|
||||
public static final String BLOCKED = "blocked";
|
||||
public static final String REGIONS = "regions";
|
||||
public static final String NOTES = "notes";
|
||||
public static final String NOTES = "notes";
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Timer getTimer = metricRegistry.timer(name(AbusiveHostRules.class, "get"));
|
||||
private final Timer insertTimer = metricRegistry.timer(name(AbusiveHostRules.class, "setBlockedHost"));
|
||||
private final Timer getTimer = metricRegistry.timer(name(AbusiveHostRules.class, "get"));
|
||||
private final Timer insertTimer = metricRegistry.timer(name(AbusiveHostRules.class, "setBlockedHost"));
|
||||
|
||||
private final FaultTolerantDatabase database;
|
||||
|
||||
public AbusiveHostRules(FaultTolerantDatabase database) {
|
||||
|
||||
this.database = database;
|
||||
this.database.getDatabase().registerRowMapper(new AbusiveHostRuleRowMapper());
|
||||
}
|
||||
@@ -38,9 +42,9 @@ public class AbusiveHostRules {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context timer = getTimer.time()) {
|
||||
return handle.createQuery("SELECT * FROM abusive_host_rules WHERE :host::inet <<= " + HOST)
|
||||
.bind("host", host)
|
||||
.mapTo(AbusiveHostRule.class)
|
||||
.list();
|
||||
.bind("host", host)
|
||||
.mapTo(AbusiveHostRule.class)
|
||||
.list();
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -48,11 +52,12 @@ public class AbusiveHostRules {
|
||||
public void setBlockedHost(String host, String notes) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context timer = insertTimer.time()) {
|
||||
handle.createUpdate("INSERT INTO abusive_host_rules(host, blocked, notes) VALUES(:host::inet, :blocked, :notes) ON CONFLICT DO NOTHING")
|
||||
.bind("host", host)
|
||||
.bind("blocked", 1)
|
||||
.bind("notes", notes)
|
||||
.execute();
|
||||
handle.createUpdate(
|
||||
"INSERT INTO abusive_host_rules(host, blocked, notes) VALUES(:host::inet, :blocked, :notes) ON CONFLICT DO NOTHING")
|
||||
.bind("host", host)
|
||||
.bind("blocked", 1)
|
||||
.bind("notes", notes)
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class Account {
|
||||
|
||||
@@ -32,24 +33,28 @@ public class Account {
|
||||
@JsonIgnore
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty("pni")
|
||||
private UUID phoneNumberIdentifier;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
@JsonProperty
|
||||
@Nullable
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private Set<Device> devices = new HashSet<>();
|
||||
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty("pniIdentityKey")
|
||||
private String phoneNumberIdentityKey;
|
||||
|
||||
@JsonProperty("cpv")
|
||||
private String currentProfileVersion;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
@JsonProperty
|
||||
private List<AccountBadge> badges = new ArrayList<>();
|
||||
|
||||
@@ -80,9 +85,10 @@ public class Account {
|
||||
public Account() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account(String number, UUID uuid, Set<Device> devices, byte[] unidentifiedAccessKey) {
|
||||
public Account(String number, UUID uuid, final UUID phoneNumberIdentifier, Set<Device> devices, byte[] unidentifiedAccessKey) {
|
||||
this.number = number;
|
||||
this.uuid = uuid;
|
||||
this.phoneNumberIdentifier = phoneNumberIdentifier;
|
||||
this.devices = devices;
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
}
|
||||
@@ -98,16 +104,20 @@ public class Account {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public void setNumber(String number) {
|
||||
public UUID getPhoneNumberIdentifier() {
|
||||
requireNotStale();
|
||||
|
||||
this.number = number;
|
||||
return phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
|
||||
requireNotStale();
|
||||
|
||||
this.canonicallyDiscoverable = canonicallyDiscoverable;
|
||||
/**
|
||||
* Tests whether this account's account identifier or phone number identifier matches the given UUID.
|
||||
*
|
||||
* @param identifier the identifier to test
|
||||
* @return {@code true} if this account's identifier or phone number identifier matches
|
||||
*/
|
||||
public boolean isIdentifiedBy(final UUID identifier) {
|
||||
return uuid.equals(identifier) || (phoneNumberIdentifier != null && phoneNumberIdentifier.equals(identifier));
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
@@ -116,6 +126,25 @@ public class Account {
|
||||
return number;
|
||||
}
|
||||
|
||||
public void setNumber(String number, UUID phoneNumberIdentifier) {
|
||||
requireNotStale();
|
||||
|
||||
this.number = number;
|
||||
this.phoneNumberIdentifier = phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
public Optional<String> getUsername() {
|
||||
requireNotStale();
|
||||
|
||||
return Optional.ofNullable(username);
|
||||
}
|
||||
|
||||
public void setUsername(final String username) {
|
||||
requireNotStale();
|
||||
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void addDevice(Device device) {
|
||||
requireNotStale();
|
||||
|
||||
@@ -247,6 +276,12 @@ public class Account {
|
||||
return canonicallyDiscoverable;
|
||||
}
|
||||
|
||||
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
|
||||
requireNotStale();
|
||||
|
||||
this.canonicallyDiscoverable = canonicallyDiscoverable;
|
||||
}
|
||||
|
||||
public Optional<String> getRelay() {
|
||||
requireNotStale();
|
||||
|
||||
@@ -265,6 +300,14 @@ public class Account {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public String getPhoneNumberIdentityKey() {
|
||||
return phoneNumberIdentityKey;
|
||||
}
|
||||
|
||||
public void setPhoneNumberIdentityKey(final String phoneNumberIdentityKey) {
|
||||
this.phoneNumberIdentityKey = phoneNumberIdentityKey;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
requireNotStale();
|
||||
|
||||
@@ -291,30 +334,6 @@ public class Account {
|
||||
this.currentProfileVersion = currentProfileVersion;
|
||||
}
|
||||
|
||||
public String getProfileName() {
|
||||
requireNotStale();
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setProfileName(String name) {
|
||||
requireNotStale();
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
requireNotStale();
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public void setAvatar(String avatar) {
|
||||
requireNotStale();
|
||||
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public List<AccountBadge> getBadges() {
|
||||
requireNotStale();
|
||||
|
||||
@@ -481,5 +500,4 @@ public class Account {
|
||||
logger.error("Accessor called on stale account", new RuntimeException());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-6
@@ -34,6 +34,7 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
|
||||
private static final long WORKER_TTL_MS = 120_000L;
|
||||
private static final long ACCELERATED_CHUNK_INTERVAL = 10L;
|
||||
|
||||
private final String name;
|
||||
private final AccountsManager accounts;
|
||||
private final int chunkSize;
|
||||
private final long chunkIntervalMs;
|
||||
@@ -44,11 +45,13 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
|
||||
private AtomicBoolean running = new AtomicBoolean(false);
|
||||
private boolean finished;
|
||||
|
||||
public AccountDatabaseCrawler(AccountsManager accounts,
|
||||
public AccountDatabaseCrawler(final String name,
|
||||
AccountsManager accounts,
|
||||
AccountDatabaseCrawlerCache cache,
|
||||
List<AccountDatabaseCrawlerListener> listeners,
|
||||
int chunkSize,
|
||||
long chunkIntervalMs) {
|
||||
this.name = name;
|
||||
this.accounts = accounts;
|
||||
this.chunkSize = chunkSize;
|
||||
this.chunkIntervalMs = chunkIntervalMs;
|
||||
@@ -82,7 +85,7 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
|
||||
accelerated = doPeriodicWork();
|
||||
sleepWhileRunning(accelerated ? ACCELERATED_CHUNK_INTERVAL : chunkIntervalMs);
|
||||
} catch (Throwable t) {
|
||||
logger.warn("error in database crawl: {}: {}", t.getClass().getSimpleName(), t.getMessage(), t);
|
||||
logger.warn("{}: error in database crawl: {}: {}", name, t.getClass().getSimpleName(), t.getMessage(), t);
|
||||
Util.sleep(10000);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +109,7 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
|
||||
final long endTimeMs = System.currentTimeMillis();
|
||||
final long sleepIntervalMs = chunkIntervalMs - (endTimeMs - startTimeMs);
|
||||
if (sleepIntervalMs > 0) {
|
||||
logger.debug("Sleeping {}ms", sleepIntervalMs);
|
||||
logger.debug("{}: Sleeping {}ms", name, sleepIntervalMs);
|
||||
sleepWhileRunning(sleepIntervalMs);
|
||||
}
|
||||
} finally {
|
||||
@@ -123,19 +126,19 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
|
||||
final Optional<UUID> fromUuid = getLastUuid();
|
||||
|
||||
if (fromUuid.isEmpty()) {
|
||||
logger.info("Started crawl");
|
||||
logger.info("{}: Started crawl", name);
|
||||
listeners.forEach(AccountDatabaseCrawlerListener::onCrawlStart);
|
||||
}
|
||||
|
||||
final AccountCrawlChunk chunkAccounts = readChunk(fromUuid, chunkSize);
|
||||
|
||||
if (chunkAccounts.getAccounts().isEmpty()) {
|
||||
logger.info("Finished crawl");
|
||||
logger.info("{}: Finished crawl", name);
|
||||
listeners.forEach(listener -> listener.onCrawlEnd(fromUuid));
|
||||
cacheLastUuid(Optional.empty());
|
||||
cache.setAccelerated(false);
|
||||
} else {
|
||||
logger.debug("Processing chunk");
|
||||
logger.debug("{}: Processing chunk", name);
|
||||
try {
|
||||
for (AccountDatabaseCrawlerListener listener : listeners) {
|
||||
listener.timeAndProcessCrawlChunk(fromUuid, chunkAccounts.getAccounts());
|
||||
|
||||
+24
-12
@@ -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.storage;
|
||||
@@ -16,6 +16,9 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class AccountDatabaseCrawlerCache {
|
||||
|
||||
public static final String GENERAL_PURPOSE_PREFIX = "";
|
||||
public static final String DIRECTORY_RECONCILER_PREFIX = "directory-reconciler";
|
||||
|
||||
private static final String ACTIVE_WORKER_KEY = "account_database_crawler_cache_active_worker";
|
||||
private static final String LAST_UUID_KEY = "account_database_crawler_cache_last_uuid";
|
||||
private static final String ACCELERATE_KEY = "account_database_crawler_cache_accelerate";
|
||||
@@ -27,17 +30,21 @@ public class AccountDatabaseCrawlerCache {
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final ClusterLuaScript unlockClusterScript;
|
||||
|
||||
public AccountDatabaseCrawlerCache(FaultTolerantRedisCluster cacheCluster) throws IOException {
|
||||
private final String prefix;
|
||||
|
||||
public AccountDatabaseCrawlerCache(FaultTolerantRedisCluster cacheCluster, String prefix) throws IOException {
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.unlockClusterScript = ClusterLuaScript.fromResource(cacheCluster, "lua/account_database_crawler/unlock.lua",
|
||||
ScriptOutputType.INTEGER);
|
||||
|
||||
this.prefix = prefix + "::";
|
||||
}
|
||||
|
||||
public void setAccelerated(final boolean accelerated) {
|
||||
if (accelerated) {
|
||||
cacheCluster.useCluster(connection -> connection.sync().set(ACCELERATE_KEY, "1"));
|
||||
cacheCluster.useCluster(connection -> connection.sync().set(getPrefixedKey(ACCELERATE_KEY), "1"));
|
||||
} else {
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(ACCELERATE_KEY));
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(ACCELERATE_KEY)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,16 +54,16 @@ public class AccountDatabaseCrawlerCache {
|
||||
|
||||
public boolean claimActiveWork(String workerId, long ttlMs) {
|
||||
return "OK".equals(cacheCluster.withCluster(connection -> connection.sync()
|
||||
.set(ACTIVE_WORKER_KEY, workerId, SetArgs.Builder.nx().px(ttlMs))));
|
||||
.set(getPrefixedKey(ACTIVE_WORKER_KEY), workerId, SetArgs.Builder.nx().px(ttlMs))));
|
||||
}
|
||||
|
||||
public void releaseActiveWork(String workerId) {
|
||||
unlockClusterScript.execute(List.of(ACTIVE_WORKER_KEY), List.of(workerId));
|
||||
unlockClusterScript.execute(List.of(getPrefixedKey(ACTIVE_WORKER_KEY)), List.of(workerId));
|
||||
}
|
||||
|
||||
public Optional<UUID> getLastUuid() {
|
||||
final String lastUuidString = cacheCluster.withCluster(
|
||||
connection -> connection.sync().get(LAST_UUID_KEY));
|
||||
connection -> connection.sync().get(getPrefixedKey(LAST_UUID_KEY)));
|
||||
|
||||
if (lastUuidString == null) {
|
||||
return Optional.empty();
|
||||
@@ -68,15 +75,15 @@ public class AccountDatabaseCrawlerCache {
|
||||
public void setLastUuid(Optional<UUID> lastUuid) {
|
||||
if (lastUuid.isPresent()) {
|
||||
cacheCluster.useCluster(connection -> connection.sync()
|
||||
.psetex(LAST_UUID_KEY, LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
.psetex(getPrefixedKey(LAST_UUID_KEY), LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
} else {
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(LAST_UUID_KEY));
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(LAST_UUID_KEY)));
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<UUID> getLastUuidDynamo() {
|
||||
final String lastUuidString = cacheCluster.withCluster(
|
||||
connection -> connection.sync().get(LAST_UUID_DYNAMO_KEY));
|
||||
connection -> connection.sync().get(getPrefixedKey(LAST_UUID_DYNAMO_KEY)));
|
||||
|
||||
if (lastUuidString == null) {
|
||||
return Optional.empty();
|
||||
@@ -89,9 +96,14 @@ public class AccountDatabaseCrawlerCache {
|
||||
if (lastUuid.isPresent()) {
|
||||
cacheCluster.useCluster(
|
||||
connection -> connection.sync()
|
||||
.psetex(LAST_UUID_DYNAMO_KEY, LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
.psetex(getPrefixedKey(LAST_UUID_DYNAMO_KEY), LAST_NUMBER_TTL_MS, lastUuid.get().toString()));
|
||||
} else {
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(LAST_UUID_DYNAMO_KEY));
|
||||
cacheCluster.useCluster(connection -> connection.sync().del(getPrefixedKey(LAST_UUID_DYNAMO_KEY)));
|
||||
}
|
||||
}
|
||||
|
||||
private String getPrefixedKey(final String key) {
|
||||
return prefix + key;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,11 +13,16 @@ import io.micrometer.core.instrument.Timer;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
@@ -29,7 +34,6 @@ import software.amazon.awssdk.services.dynamodb.model.Delete;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Put;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
|
||||
@@ -44,6 +48,8 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
// uuid, primary key
|
||||
static final String KEY_ACCOUNT_UUID = "U";
|
||||
// uuid, attribute on account table, primary key for PNI table
|
||||
static final String ATTR_PNI_UUID = "PNI";
|
||||
// phone number
|
||||
static final String ATTR_ACCOUNT_E164 = "P";
|
||||
// account, serialized to JSON
|
||||
@@ -52,32 +58,44 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
static final String ATTR_VERSION = "V";
|
||||
// canonically discoverable
|
||||
static final String ATTR_CANONICALLY_DISCOVERABLE = "C";
|
||||
// username; string
|
||||
static final String ATTR_USERNAME = "N";
|
||||
|
||||
private final DynamoDbClient client;
|
||||
|
||||
private final String phoneNumbersTableName;
|
||||
private final String phoneNumberConstraintTableName;
|
||||
private final String phoneNumberIdentifierConstraintTableName;
|
||||
private final String usernamesConstraintTableName;
|
||||
private final String accountsTableName;
|
||||
|
||||
private final int scanPageSize;
|
||||
|
||||
private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create"));
|
||||
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
|
||||
private static final Timer SET_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "setUsername"));
|
||||
private static final Timer CLEAR_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "clearUsername"));
|
||||
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
|
||||
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
|
||||
private static final Timer GET_BY_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "getByUsername"));
|
||||
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
|
||||
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
|
||||
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
|
||||
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
|
||||
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Accounts.class);
|
||||
|
||||
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumbersTableName,
|
||||
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumberConstraintTableName,
|
||||
String phoneNumberIdentifierConstraintTableName, final String usernamesConstraintTableName,
|
||||
final int scanPageSize) {
|
||||
|
||||
super(client);
|
||||
|
||||
this.client = client;
|
||||
this.phoneNumbersTableName = phoneNumbersTableName;
|
||||
this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
|
||||
this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
|
||||
this.accountsTableName = accountsTableName;
|
||||
this.usernamesConstraintTableName = usernamesConstraintTableName;
|
||||
this.scanPageSize = scanPageSize;
|
||||
}
|
||||
|
||||
@@ -85,35 +103,91 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
return CREATE_TIMER.record(() -> {
|
||||
|
||||
try {
|
||||
TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid());
|
||||
TransactWriteItem accountPut = buildPutWriteItemForAccount(account, account.getUuid(), Put.builder()
|
||||
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber()))));
|
||||
TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.item(Map.of(
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
TransactWriteItem phoneNumberIdentifierConstraintPut = TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.item(Map.of(
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#pni", ATTR_PNI_UUID))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
|
||||
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
|
||||
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
|
||||
|
||||
TransactWriteItem accountPut = TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
|
||||
.tableName(accountsTableName)
|
||||
.item(item)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(phoneNumberConstraintPut, accountPut)
|
||||
.transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut)
|
||||
.build();
|
||||
|
||||
try {
|
||||
client.transactWriteItems(request);
|
||||
} catch (TransactionCanceledException e) {
|
||||
|
||||
final CancellationReason accountCancellationReason = e.cancellationReasons().get(1);
|
||||
final CancellationReason accountCancellationReason = e.cancellationReasons().get(2);
|
||||
|
||||
if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
|
||||
throw new IllegalArgumentException("uuid present with different phone number");
|
||||
throw new IllegalArgumentException("account identifier present with different phone number");
|
||||
}
|
||||
|
||||
final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
|
||||
final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1);
|
||||
|
||||
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code())) {
|
||||
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ||
|
||||
"ConditionalCheckFailed".equals(phoneNumberIdentifierConstraintCancellationReason.code())) {
|
||||
|
||||
ByteBuffer actualAccountUuid = phoneNumberConstraintCancellationReason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
|
||||
// In theory, both reasons should trip in tandem and either should give us the information we need. Even so,
|
||||
// we'll be cautious here and make sure we're choosing a condition check that really failed.
|
||||
final CancellationReason reason = "ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ?
|
||||
phoneNumberConstraintCancellationReason : phoneNumberIdentifierConstraintCancellationReason;
|
||||
|
||||
ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
|
||||
account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
|
||||
|
||||
final int version = get(account.getUuid()).get().getVersion();
|
||||
account.setVersion(version);
|
||||
final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow();
|
||||
account.setNumber(existingAccount.getNumber(), existingAccount.getPhoneNumberIdentifier());
|
||||
account.setVersion(existingAccount.getVersion());
|
||||
|
||||
update(account);
|
||||
|
||||
@@ -125,7 +199,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
throw new ContestedOptimisticLockException();
|
||||
}
|
||||
|
||||
// this shouldn’t happen
|
||||
// this shouldn't happen
|
||||
throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
@@ -136,44 +210,11 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
private TransactWriteItem buildPutWriteItemForAccount(Account account, UUID uuid, Put.Builder putBuilder) throws JsonProcessingException {
|
||||
return TransactWriteItem.builder()
|
||||
.put(putBuilder
|
||||
.tableName(accountsTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
|
||||
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private TransactWriteItem buildPutWriteItemForPhoneNumberConstraint(Account account, UUID uuid) {
|
||||
return TransactWriteItem.builder()
|
||||
.put(
|
||||
Put.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.item(Map.of(
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
|
||||
.conditionExpression(
|
||||
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
|
||||
.expressionAttributeNames(
|
||||
Map.of("#uuid", KEY_ACCOUNT_UUID,
|
||||
"#number", ATTR_ACCOUNT_E164))
|
||||
.expressionAttributeValues(
|
||||
Map.of(":uuid", AttributeValues.fromUUID(uuid)))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the phone number for the given account. The given account's number should be its current, pre-change
|
||||
* number. If this method succeeds, the account's number will be changed to the new number. If the update fails for
|
||||
* any reason, the account's number will be unchanged.
|
||||
* number. If this method succeeds, the account's number will be changed to the new number and its phone number
|
||||
* identifier will be changed to the given phone number identifier. If the update fails for any reason, the account's
|
||||
* number and PNI will be unchanged.
|
||||
* <p/>
|
||||
* This method expects that any accounts with conflicting numbers will have been removed by the time this method is
|
||||
* called. This method may fail with an unspecified {@link RuntimeException} if another account with the same number
|
||||
@@ -182,26 +223,28 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
* @param account the account for which to change the phone number
|
||||
* @param number the new phone number
|
||||
*/
|
||||
public void changeNumber(final Account account, final String number) {
|
||||
public void changeNumber(final Account account, final String number, final UUID phoneNumberIdentifier) {
|
||||
CHANGE_NUMBER_TIMER.record(() -> {
|
||||
final String originalNumber = account.getNumber();
|
||||
final UUID originalPni = account.getPhoneNumberIdentifier();
|
||||
|
||||
boolean succeeded = false;
|
||||
|
||||
account.setNumber(number);
|
||||
account.setNumber(number, phoneNumberIdentifier);
|
||||
|
||||
try {
|
||||
final List<TransactWriteItem> writeItems = new ArrayList<>();
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
@@ -211,20 +254,41 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(originalPni)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.conditionExpression("attribute_not_exists(#pni)")
|
||||
.expressionAttributeNames(Map.of("#pni", ATTR_PNI_UUID))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(
|
||||
TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #number = :number, #cds = :cds ADD #version :version_increment")
|
||||
.updateExpression("SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#pni", ATTR_PNI_UUID,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":number", AttributeValues.fromString(number),
|
||||
":pni", AttributeValues.fromUUID(phoneNumberIdentifier),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
@@ -243,66 +307,228 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
throw new IllegalArgumentException(e);
|
||||
} finally {
|
||||
if (!succeeded) {
|
||||
account.setNumber(originalNumber);
|
||||
account.setNumber(originalNumber, originalPni);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setUsername(final Account account, final String username)
|
||||
throws ContestedOptimisticLockException, UsernameNotAvailableException {
|
||||
final long startNanos = System.nanoTime();
|
||||
|
||||
final Optional<String> maybeOriginalUsername = account.getUsername();
|
||||
account.setUsername(username);
|
||||
|
||||
boolean succeeded = false;
|
||||
|
||||
try {
|
||||
final List<TransactWriteItem> writeItems = new ArrayList<>();
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
|
||||
ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.conditionExpression("attribute_not_exists(#username)")
|
||||
.expressionAttributeNames(Map.of("#username", ATTR_USERNAME))
|
||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(
|
||||
TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #username = :username ADD #version :version_increment")
|
||||
.conditionExpression("#version = :version")
|
||||
.expressionAttributeNames(Map.of("#data", ATTR_ACCOUNT_DATA,
|
||||
"#username", ATTR_USERNAME,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":username", AttributeValues.fromString(username),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
maybeOriginalUsername.ifPresent(originalUsername -> writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(originalUsername)))
|
||||
.build())
|
||||
.build()));
|
||||
|
||||
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItems)
|
||||
.build();
|
||||
|
||||
client.transactWriteItems(request);
|
||||
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
succeeded = true;
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (final TransactionCanceledException e) {
|
||||
if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(0).code())) {
|
||||
throw new UsernameNotAvailableException();
|
||||
} else if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(1).code())) {
|
||||
throw new ContestedOptimisticLockException();
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
if (!succeeded) {
|
||||
account.setUsername(maybeOriginalUsername.orElse(null));
|
||||
}
|
||||
|
||||
SET_USERNAME_TIMER.record(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearUsername(Account account) {
|
||||
account.getUsername().ifPresent(username -> {
|
||||
CLEAR_USERNAME_TIMER.record(() -> {
|
||||
account.setUsername(null);
|
||||
|
||||
boolean succeeded = false;
|
||||
|
||||
try {
|
||||
final List<TransactWriteItem> writeItems = new ArrayList<>();
|
||||
|
||||
writeItems.add(
|
||||
TransactWriteItem.builder()
|
||||
.update(Update.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data REMOVE #username ADD #version :version_increment")
|
||||
.conditionExpression("#version = :version")
|
||||
.expressionAttributeNames(Map.of("#data", ATTR_ACCOUNT_DATA,
|
||||
"#username", ATTR_USERNAME,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
writeItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(writeItems)
|
||||
.build();
|
||||
|
||||
client.transactWriteItems(request);
|
||||
|
||||
account.setVersion(account.getVersion() + 1);
|
||||
succeeded = true;
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (final TransactionCanceledException e) {
|
||||
if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(0).code())) {
|
||||
throw new ContestedOptimisticLockException();
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
if (!succeeded) {
|
||||
account.setUsername(username);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void update(Account account) throws ContestedOptimisticLockException {
|
||||
UPDATE_TIMER.record(() -> {
|
||||
UpdateItemRequest updateItemRequest;
|
||||
final UpdateItemRequest updateItemRequest;
|
||||
|
||||
try {
|
||||
updateItemRequest = UpdateItemRequest.builder()
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.returnValues(ReturnValue.UPDATED_NEW)
|
||||
.build();
|
||||
|
||||
.tableName(accountsTableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
|
||||
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
|
||||
.conditionExpression("attribute_exists(#number) AND #version = :version")
|
||||
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
|
||||
"#data", ATTR_ACCOUNT_DATA,
|
||||
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
|
||||
"#version", ATTR_VERSION))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
|
||||
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
|
||||
":version", AttributeValues.fromInt(account.getVersion()),
|
||||
":version_increment", AttributeValues.fromInt(1)))
|
||||
.build();
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
UpdateItemResponse response = client.updateItem(updateItemRequest);
|
||||
|
||||
final UpdateItemResponse response = client.updateItem(updateItemRequest);
|
||||
account.setVersion(AttributeValues.getInt(response.attributes(), "V", account.getVersion() + 1));
|
||||
} catch (final TransactionConflictException e) {
|
||||
|
||||
throw new ContestedOptimisticLockException();
|
||||
|
||||
} catch (final ConditionalCheckFailedException e) {
|
||||
|
||||
// the exception doesn’t give details about which condition failed,
|
||||
// the exception doesn't give details about which condition failed,
|
||||
// but we can infer it was an optimistic locking failure if the UUID is known
|
||||
throw get(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
|
||||
throw getByAccountIdentifier(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> get(String number) {
|
||||
public Optional<Account> getByE164(String number) {
|
||||
return GET_BY_NUMBER_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(uuid -> accountByUuid(uuid))
|
||||
.map(Accounts::fromItem);
|
||||
.map(this::accountByUuid)
|
||||
.map(this::fromItem);
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> getByUsername(final String username) {
|
||||
return GET_BY_USERNAME_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(this::accountByUuid)
|
||||
.map(this::fromItem);
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
|
||||
return GET_BY_PNI_TIMER.record(() -> {
|
||||
|
||||
final GetItemResponse response = client.getItem(GetItemRequest.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
|
||||
.build());
|
||||
|
||||
return Optional.ofNullable(response.item())
|
||||
.map(item -> item.get(KEY_ACCOUNT_UUID))
|
||||
.map(this::accountByUuid)
|
||||
.map(this::fromItem);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,22 +541,20 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
return r.item().isEmpty() ? null : r.item();
|
||||
}
|
||||
|
||||
public Optional<Account> get(UUID uuid) {
|
||||
public Optional<Account> getByAccountIdentifier(UUID uuid) {
|
||||
return GET_BY_UUID_TIMER.record(() ->
|
||||
Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
|
||||
.map(Accounts::fromItem));
|
||||
.map(this::fromItem));
|
||||
}
|
||||
|
||||
public void delete(UUID uuid) {
|
||||
DELETE_TIMER.record(() -> {
|
||||
|
||||
Optional<Account> maybeAccount = get(uuid);
|
||||
|
||||
maybeAccount.ifPresent(account -> {
|
||||
getByAccountIdentifier(uuid).ifPresent(account -> {
|
||||
|
||||
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumbersTableName)
|
||||
.tableName(phoneNumberConstraintTableName)
|
||||
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
|
||||
.build())
|
||||
.build();
|
||||
@@ -342,8 +566,24 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.build())
|
||||
.build();
|
||||
|
||||
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(phoneNumberDelete, accountDelete));
|
||||
|
||||
transactWriteItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(phoneNumberIdentifierConstraintTableName)
|
||||
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier())))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
account.getUsername().ifPresent(username -> transactWriteItems.add(TransactWriteItem.builder()
|
||||
.delete(Delete.builder()
|
||||
.tableName(usernamesConstraintTableName)
|
||||
.key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
|
||||
.build())
|
||||
.build()));
|
||||
|
||||
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
|
||||
.transactItems(phoneNumberDelete, accountDelete).build();
|
||||
.transactItems(transactWriteItems).build();
|
||||
|
||||
client.transactWriteItems(request);
|
||||
});
|
||||
@@ -371,7 +611,7 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
|
||||
final List<Account> accounts = timer.record(() -> scan(scanRequestBuilder.build(), maxCount)
|
||||
.stream()
|
||||
.map(Accounts::fromItem)
|
||||
.map(this::fromItem)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
return new AccountCrawlChunk(accounts, accounts.size() > 0 ? accounts.get(accounts.size() - 1).getUuid() : null);
|
||||
@@ -383,8 +623,9 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
// TODO Make this static once PNI repairs are complete and the call to #update(Account) is no longer needed
|
||||
@VisibleForTesting
|
||||
static Account fromItem(Map<String, AttributeValue> item) {
|
||||
Account fromItem(Map<String, AttributeValue> item) {
|
||||
if (!item.containsKey(ATTR_ACCOUNT_DATA) ||
|
||||
!item.containsKey(ATTR_ACCOUNT_E164) ||
|
||||
// TODO: eventually require ATTR_CANONICALLY_DISCOVERABLE
|
||||
@@ -393,11 +634,38 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||
}
|
||||
try {
|
||||
Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
|
||||
account.setNumber(item.get(ATTR_ACCOUNT_E164).s());
|
||||
account.setUuid(UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer()));
|
||||
|
||||
final UUID accountIdentifier = UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer());
|
||||
final UUID phoneNumberIdentifierFromAttribute = AttributeValues.getUUID(item, ATTR_PNI_UUID, null);
|
||||
|
||||
if (account.getPhoneNumberIdentifier() == null || phoneNumberIdentifierFromAttribute == null ||
|
||||
!Objects.equals(account.getPhoneNumberIdentifier(), phoneNumberIdentifierFromAttribute)) {
|
||||
|
||||
log.warn("Missing or mismatched PNIs for account {}. From JSON: {}; from attribute: {}",
|
||||
accountIdentifier, account.getPhoneNumberIdentifier(), phoneNumberIdentifierFromAttribute);
|
||||
}
|
||||
|
||||
boolean shouldResetPni = false;
|
||||
|
||||
if (account.getPhoneNumberIdentifier() == null && phoneNumberIdentifierFromAttribute != null) {
|
||||
log.info("Account {} has a PNI in its attributes, but not JSON blob; will repair", accountIdentifier);
|
||||
shouldResetPni = true;
|
||||
}
|
||||
|
||||
account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), phoneNumberIdentifierFromAttribute);
|
||||
account.setUuid(accountIdentifier);
|
||||
account.setUsername(AttributeValues.getString(item, ATTR_USERNAME, null));
|
||||
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
|
||||
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));
|
||||
|
||||
if (shouldResetPni) {
|
||||
try {
|
||||
update(account);
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to reset PNI for account {}", accountIdentifier, e);
|
||||
}
|
||||
}
|
||||
|
||||
return account;
|
||||
|
||||
} catch (IOException e) {
|
||||
|
||||
+240
-103
@@ -17,6 +17,10 @@ import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -36,21 +40,25 @@ import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import org.whispersystems.textsecuregcm.util.UsernameValidator;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class AccountsManager {
|
||||
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Timer createTimer = metricRegistry.timer(name(AccountsManager.class, "create" ));
|
||||
private static final Timer updateTimer = metricRegistry.timer(name(AccountsManager.class, "update" ));
|
||||
private static final Timer getByNumberTimer = metricRegistry.timer(name(AccountsManager.class, "getByNumber"));
|
||||
private static final Timer getByUuidTimer = metricRegistry.timer(name(AccountsManager.class, "getByUuid" ));
|
||||
private static final Timer deleteTimer = metricRegistry.timer(name(AccountsManager.class, "delete"));
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private static final Timer createTimer = metricRegistry.timer(name(AccountsManager.class, "create"));
|
||||
private static final Timer updateTimer = metricRegistry.timer(name(AccountsManager.class, "update"));
|
||||
private static final Timer getByNumberTimer = metricRegistry.timer(name(AccountsManager.class, "getByNumber"));
|
||||
private static final Timer getByUsernameTimer = metricRegistry.timer(name(AccountsManager.class, "getByUsername"));
|
||||
private static final Timer getByUuidTimer = metricRegistry.timer(name(AccountsManager.class, "getByUuid"));
|
||||
private static final Timer deleteTimer = metricRegistry.timer(name(AccountsManager.class, "delete"));
|
||||
|
||||
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet" ));
|
||||
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet"));
|
||||
private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet"));
|
||||
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet" ));
|
||||
private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete" ));
|
||||
private static final Timer redisUsernameGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUsernameGet"));
|
||||
private static final Timer redisPniGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisPniGet"));
|
||||
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet"));
|
||||
private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete"));
|
||||
|
||||
private static final String CREATE_COUNTER_NAME = name(AccountsManager.class, "createCounter");
|
||||
private static final String DELETE_COUNTER_NAME = name(AccountsManager.class, "deleteCounter");
|
||||
@@ -60,18 +68,31 @@ public class AccountsManager {
|
||||
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
|
||||
|
||||
private final Accounts accounts;
|
||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final DeletedAccountsManager deletedAccountsManager;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final KeysDynamoDb keysDynamoDb;
|
||||
private final DirectoryQueue directoryQueue;
|
||||
private final Keys keys;
|
||||
private final MessagesManager messagesManager;
|
||||
private final UsernamesManager usernamesManager;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final ReservedUsernames reservedUsernames;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final StoredVerificationCodeManager pendingAccounts;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
private final SecureBackupClient secureBackupClient;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
private final SecureBackupClient secureBackupClient;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final ObjectMapper mapper;
|
||||
private final ObjectMapper mapper;
|
||||
private final Clock clock;
|
||||
|
||||
// An account that's used at least daily will get reset in the cache at least once per day when its "last seen"
|
||||
// timestamp updates; expiring entries after two days will help clear out "zombie" cache entries that are read
|
||||
// frequently (e.g. the account is in an active group and receives messages frequently), but aren't actively used by
|
||||
// the owner.
|
||||
private static final long CACHE_TTL_SECONDS = Duration.ofDays(2).toSeconds();
|
||||
|
||||
@FunctionalInterface
|
||||
private interface AccountPersister {
|
||||
void persistAccount(Account account) throws UsernameNotAvailableException;
|
||||
}
|
||||
|
||||
public enum DeletionReason {
|
||||
ADMIN_DELETED("admin"),
|
||||
@@ -85,35 +106,42 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public AccountsManager(Accounts accounts, FaultTolerantRedisCluster cacheCluster,
|
||||
public AccountsManager(final Accounts accounts,
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers,
|
||||
final FaultTolerantRedisCluster cacheCluster,
|
||||
final DeletedAccountsManager deletedAccountsManager,
|
||||
final DirectoryQueue directoryQueue,
|
||||
final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager,
|
||||
final UsernamesManager usernamesManager,
|
||||
final Keys keys,
|
||||
final MessagesManager messagesManager,
|
||||
final ReservedUsernames reservedUsernames,
|
||||
final ProfilesManager profilesManager,
|
||||
final StoredVerificationCodeManager pendingAccounts,
|
||||
final SecureStorageClient secureStorageClient,
|
||||
final SecureBackupClient secureBackupClient,
|
||||
final ClientPresenceManager clientPresenceManager) {
|
||||
final ClientPresenceManager clientPresenceManager,
|
||||
final Clock clock) {
|
||||
this.accounts = accounts;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.deletedAccountsManager = deletedAccountsManager;
|
||||
this.directoryQueue = directoryQueue;
|
||||
this.keysDynamoDb = keysDynamoDb;
|
||||
this.keys = keys;
|
||||
this.messagesManager = messagesManager;
|
||||
this.usernamesManager = usernamesManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.pendingAccounts = pendingAccounts;
|
||||
this.secureStorageClient = secureStorageClient;
|
||||
this.secureBackupClient = secureBackupClient;
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.reservedUsernames = reservedUsernames;
|
||||
this.mapper = SystemMapper.getMapper();
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
}
|
||||
|
||||
public Account create(final String number,
|
||||
final String password,
|
||||
final String signalAgent,
|
||||
final AccountAttributes accountAttributes) throws InterruptedException {
|
||||
final AccountAttributes accountAttributes,
|
||||
final List<AccountBadge> accountBadges) throws InterruptedException {
|
||||
|
||||
try (Timer.Context ignored = createTimer.time()) {
|
||||
final Account account = new Account();
|
||||
@@ -130,17 +158,18 @@ public class AccountsManager {
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setUserAgent(signalAgent);
|
||||
|
||||
account.setNumber(number);
|
||||
account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
|
||||
account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID));
|
||||
account.addDevice(device);
|
||||
account.setRegistrationLockFromAttributes(accountAttributes);
|
||||
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
|
||||
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
|
||||
account.setDiscoverableByPhoneNumber(accountAttributes.isDiscoverableByPhoneNumber());
|
||||
account.setBadges(clock, accountBadges);
|
||||
|
||||
final UUID originalUuid = account.getUuid();
|
||||
|
||||
boolean freshUser = dynamoCreate(account);
|
||||
boolean freshUser = accounts.create(account);
|
||||
|
||||
// create() sometimes updates the UUID, if there was a number conflict.
|
||||
// for metrics, we want secondary to run with the same original UUID
|
||||
@@ -164,7 +193,7 @@ public class AccountsManager {
|
||||
// account and need to clear out messages and keys that may have been stored for the old account.
|
||||
if (!originalUuid.equals(actualUuid)) {
|
||||
messagesManager.clear(actualUuid);
|
||||
keysDynamoDb.delete(actualUuid);
|
||||
keys.delete(actualUuid);
|
||||
profilesManager.deleteAll(actualUuid);
|
||||
}
|
||||
|
||||
@@ -202,7 +231,7 @@ public class AccountsManager {
|
||||
deletedAccountsManager.lockAndPut(account.getNumber(), number, () -> {
|
||||
redisDelete(account);
|
||||
|
||||
final Optional<Account> maybeExistingAccount = get(number);
|
||||
final Optional<Account> maybeExistingAccount = getByE164(number);
|
||||
final Optional<UUID> displacedUuid;
|
||||
|
||||
if (maybeExistingAccount.isPresent()) {
|
||||
@@ -213,12 +242,20 @@ public class AccountsManager {
|
||||
}
|
||||
|
||||
final UUID uuid = account.getUuid();
|
||||
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
||||
|
||||
final Account numberChangedAccount = updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> dynamoChangeNumber(a, number),
|
||||
() -> dynamoGet(uuid).orElseThrow());
|
||||
final Account numberChangedAccount;
|
||||
|
||||
try {
|
||||
numberChangedAccount = updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen when changing numbers
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
updatedAccount.set(numberChangedAccount);
|
||||
directoryQueue.changePhoneNumber(numberChangedAccount, originalNumber, number);
|
||||
@@ -229,13 +266,53 @@ public class AccountsManager {
|
||||
return updatedAccount.get();
|
||||
}
|
||||
|
||||
public Account setUsername(final Account account, final String username) throws UsernameNotAvailableException {
|
||||
final String canonicalUsername = UsernameValidator.getCanonicalUsername(username);
|
||||
|
||||
if (account.getUsername().map(canonicalUsername::equals).orElse(false)) {
|
||||
return account;
|
||||
}
|
||||
|
||||
if (reservedUsernames.isReserved(canonicalUsername, account.getUuid())) {
|
||||
throw new UsernameNotAvailableException();
|
||||
}
|
||||
|
||||
redisDelete(account);
|
||||
|
||||
return updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
a -> accounts.setUsername(a, canonicalUsername),
|
||||
() -> accounts.getByAccountIdentifier(account.getUuid()).orElseThrow());
|
||||
}
|
||||
|
||||
public Account clearUsername(final Account account) {
|
||||
redisDelete(account);
|
||||
|
||||
try {
|
||||
return updateWithRetries(
|
||||
account,
|
||||
a -> true,
|
||||
accounts::clearUsername,
|
||||
() -> accounts.getByAccountIdentifier(account.getUuid()).orElseThrow());
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Account update(Account account, Consumer<Account> updater) {
|
||||
|
||||
return update(account, a -> {
|
||||
updater.accept(a);
|
||||
// assume that all updaters passed to the public method actually modify the account
|
||||
return true;
|
||||
});
|
||||
try {
|
||||
return update(account, a -> {
|
||||
updater.accept(a);
|
||||
// assume that all updaters passed to the public method actually modify the account
|
||||
return true;
|
||||
});
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen for general-purpose, public account updates
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,28 +321,33 @@ public class AccountsManager {
|
||||
*/
|
||||
public Account updateDeviceLastSeen(Account account, Device device, final long lastSeen) {
|
||||
|
||||
return update(account, a -> {
|
||||
try {
|
||||
return update(account, a -> {
|
||||
|
||||
final Optional<Device> maybeDevice = a.getDevice(device.getId());
|
||||
final Optional<Device> maybeDevice = a.getDevice(device.getId());
|
||||
|
||||
return maybeDevice.map(d -> {
|
||||
if (d.getLastSeen() >= lastSeen) {
|
||||
return false;
|
||||
}
|
||||
return maybeDevice.map(d -> {
|
||||
if (d.getLastSeen() >= lastSeen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
d.setLastSeen(lastSeen);
|
||||
d.setLastSeen(lastSeen);
|
||||
|
||||
return true;
|
||||
return true;
|
||||
|
||||
}).orElse(false);
|
||||
});
|
||||
}).orElse(false);
|
||||
});
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen when updating last-seen timestamps
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param account account to update
|
||||
* @param updater must return {@code true} if the account was actually updated
|
||||
*/
|
||||
private Account update(Account account, Function<Account, Boolean> updater) {
|
||||
private Account update(Account account, Function<Account, Boolean> updater) throws UsernameNotAvailableException {
|
||||
|
||||
final boolean wasVisibleBeforeUpdate = account.shouldBeVisibleInDirectory();
|
||||
|
||||
@@ -277,8 +359,13 @@ public class AccountsManager {
|
||||
|
||||
final UUID uuid = account.getUuid();
|
||||
final String originalNumber = account.getNumber();
|
||||
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
|
||||
final Optional<String> originalUsername = account.getUsername();
|
||||
|
||||
updatedAccount = updateWithRetries(account, updater, this::dynamoUpdate, () -> dynamoGet(uuid).get());
|
||||
updatedAccount = updateWithRetries(account,
|
||||
updater,
|
||||
accounts::update,
|
||||
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
|
||||
|
||||
assert updatedAccount.getNumber().equals(originalNumber);
|
||||
|
||||
@@ -287,6 +374,20 @@ public class AccountsManager {
|
||||
new RuntimeException());
|
||||
}
|
||||
|
||||
assert updatedAccount.getPhoneNumberIdentifier().equals(originalPhoneNumberIdentifier);
|
||||
|
||||
if (!updatedAccount.getPhoneNumberIdentifier().equals(originalPhoneNumberIdentifier)) {
|
||||
logger.error("Phone number identifier changed via \"normal\" update; PNIs must be changed via changeNumber method",
|
||||
new RuntimeException());
|
||||
}
|
||||
|
||||
assert updatedAccount.getUsername().equals(originalUsername);
|
||||
|
||||
if (!updatedAccount.getUsername().equals(originalUsername)) {
|
||||
logger.error("Username changed via \"normal\" update; usernames must be changed via setUsername method",
|
||||
new RuntimeException());
|
||||
}
|
||||
|
||||
redisSet(updatedAccount);
|
||||
}
|
||||
|
||||
@@ -299,8 +400,8 @@ public class AccountsManager {
|
||||
return updatedAccount;
|
||||
}
|
||||
|
||||
private Account updateWithRetries(Account account, Function<Account, Boolean> updater, Consumer<Account> persister,
|
||||
Supplier<Account> retriever) {
|
||||
private Account updateWithRetries(Account account, Function<Account, Boolean> updater, AccountPersister persister,
|
||||
Supplier<Account> retriever) throws UsernameNotAvailableException {
|
||||
|
||||
if (!updater.apply(account)) {
|
||||
return account;
|
||||
@@ -312,7 +413,7 @@ public class AccountsManager {
|
||||
while (tries < maxTries) {
|
||||
|
||||
try {
|
||||
persister.accept(account);
|
||||
persister.persistAccount(account);
|
||||
|
||||
final Account updatedAccount;
|
||||
try {
|
||||
@@ -340,19 +441,24 @@ public class AccountsManager {
|
||||
}
|
||||
|
||||
public Account updateDevice(Account account, long deviceId, Consumer<Device> deviceUpdater) {
|
||||
return update(account, a -> {
|
||||
a.getDevice(deviceId).ifPresent(deviceUpdater);
|
||||
// assume that all updaters passed to the public method actually modify the device
|
||||
return true;
|
||||
});
|
||||
try {
|
||||
return update(account, a -> {
|
||||
a.getDevice(deviceId).ifPresent(deviceUpdater);
|
||||
// assume that all updaters passed to the public method actually modify the device
|
||||
return true;
|
||||
});
|
||||
} catch (UsernameNotAvailableException e) {
|
||||
// This should never happen when updating devices
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> get(String number) {
|
||||
public Optional<Account> getByE164(String number) {
|
||||
try (Timer.Context ignored = getByNumberTimer.time()) {
|
||||
Optional<Account> account = redisGet(number);
|
||||
Optional<Account> account = redisGetByE164(number);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = dynamoGet(number);
|
||||
account = accounts.getByE164(number);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
@@ -360,12 +466,40 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> get(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<Account> account = redisGet(uuid);
|
||||
public Optional<Account> getByPhoneNumberIdentifier(UUID pni) {
|
||||
try (Timer.Context ignored = getByNumberTimer.time()) {
|
||||
Optional<Account> account = redisGetByPhoneNumberIdentifier(pni);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = dynamoGet(uuid);
|
||||
account = accounts.getByPhoneNumberIdentifier(pni);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> getByUsername(final String username) {
|
||||
try (final Timer.Context ignored = getByUsernameTimer.time()) {
|
||||
final String canonicalUsername = UsernameValidator.getCanonicalUsername(username);
|
||||
|
||||
Optional<Account> account = redisGetByUsername(canonicalUsername);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = accounts.getByUsername(canonicalUsername);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Account> getByAccountIdentifier(UUID uuid) {
|
||||
try (Timer.Context ignored = getByUuidTimer.time()) {
|
||||
Optional<Account> account = redisGetByAccountIdentifier(uuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
account = accounts.getByAccountIdentifier(uuid);
|
||||
account.ifPresent(this::redisSet);
|
||||
}
|
||||
|
||||
@@ -404,24 +538,25 @@ public class AccountsManager {
|
||||
final CompletableFuture<Void> deleteStorageServiceDataFuture = secureStorageClient.deleteStoredData(account.getUuid());
|
||||
final CompletableFuture<Void> deleteBackupServiceDataFuture = secureBackupClient.deleteBackups(account.getUuid());
|
||||
|
||||
usernamesManager.delete(account.getUuid());
|
||||
profilesManager.deleteAll(account.getUuid());
|
||||
keysDynamoDb.delete(account.getUuid());
|
||||
keys.delete(account.getUuid());
|
||||
keys.delete(account.getPhoneNumberIdentifier());
|
||||
messagesManager.clear(account.getUuid());
|
||||
messagesManager.clear(account.getPhoneNumberIdentifier());
|
||||
|
||||
deleteStorageServiceDataFuture.join();
|
||||
deleteBackupServiceDataFuture.join();
|
||||
|
||||
accounts.delete(account.getUuid());
|
||||
redisDelete(account);
|
||||
dynamoDelete(account);
|
||||
|
||||
RedisOperation.unchecked(() ->
|
||||
account.getDevices().forEach(device ->
|
||||
clientPresenceManager.displacePresence(account.getUuid(), device.getId())));
|
||||
clientPresenceManager.disconnectPresence(account.getUuid(), device.getId())));
|
||||
}
|
||||
|
||||
private String getAccountMapKey(String number) {
|
||||
return "AccountMap::" + number;
|
||||
private String getAccountMapKey(String key) {
|
||||
return "AccountMap::" + key;
|
||||
}
|
||||
|
||||
private String getAccountEntityKey(UUID uuid) {
|
||||
@@ -435,19 +570,35 @@ public class AccountsManager {
|
||||
cacheCluster.useCluster(connection -> {
|
||||
final RedisAdvancedClusterCommands<String, String> commands = connection.sync();
|
||||
|
||||
commands.set(getAccountMapKey(account.getNumber()), account.getUuid().toString());
|
||||
commands.set(getAccountEntityKey(account.getUuid()), accountJson);
|
||||
commands.setex(getAccountMapKey(account.getPhoneNumberIdentifier().toString()), CACHE_TTL_SECONDS, account.getUuid().toString());
|
||||
commands.setex(getAccountMapKey(account.getNumber()), CACHE_TTL_SECONDS, account.getUuid().toString());
|
||||
commands.setex(getAccountEntityKey(account.getUuid()), CACHE_TTL_SECONDS, accountJson);
|
||||
|
||||
account.getUsername().ifPresent(username ->
|
||||
commands.setex(getAccountMapKey(username), CACHE_TTL_SECONDS, account.getUuid().toString()));
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> redisGet(String number) {
|
||||
try (Timer.Context ignored = redisNumberGetTimer.time()) {
|
||||
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(number)));
|
||||
private Optional<Account> redisGetByPhoneNumberIdentifier(UUID uuid) {
|
||||
return redisGetBySecondaryKey(uuid.toString(), redisPniGetTimer);
|
||||
}
|
||||
|
||||
if (uuid != null) return redisGet(UUID.fromString(uuid));
|
||||
private Optional<Account> redisGetByE164(String e164) {
|
||||
return redisGetBySecondaryKey(e164, redisNumberGetTimer);
|
||||
}
|
||||
|
||||
private Optional<Account> redisGetByUsername(String username) {
|
||||
return redisGetBySecondaryKey(username, redisUsernameGetTimer);
|
||||
}
|
||||
|
||||
private Optional<Account> redisGetBySecondaryKey(String secondaryKey, Timer timer) {
|
||||
try (Timer.Context ignored = timer.time()) {
|
||||
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(secondaryKey)));
|
||||
|
||||
if (uuid != null) return redisGetByAccountIdentifier(UUID.fromString(uuid));
|
||||
else return Optional.empty();
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Deserialization error", e);
|
||||
@@ -458,7 +609,7 @@ public class AccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> redisGet(UUID uuid) {
|
||||
private Optional<Account> redisGetByAccountIdentifier(UUID uuid) {
|
||||
try (Timer.Context ignored = redisUuidGetTimer.time()) {
|
||||
final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid)));
|
||||
|
||||
@@ -466,6 +617,10 @@ public class AccountsManager {
|
||||
Account account = mapper.readValue(json, Account.class);
|
||||
account.setUuid(uuid);
|
||||
|
||||
if (account.getPhoneNumberIdentifier() == null) {
|
||||
logger.warn("Account {} loaded from Redis is missing a PNI", uuid);
|
||||
}
|
||||
|
||||
return Optional.of(account);
|
||||
}
|
||||
|
||||
@@ -481,32 +636,14 @@ public class AccountsManager {
|
||||
|
||||
private void redisDelete(final Account account) {
|
||||
try (final Timer.Context ignored = redisDeleteTimer.time()) {
|
||||
cacheCluster.useCluster(connection -> connection.sync()
|
||||
.del(getAccountMapKey(account.getNumber()), getAccountEntityKey(account.getUuid())));
|
||||
cacheCluster.useCluster(connection -> {
|
||||
connection.sync().del(
|
||||
getAccountMapKey(account.getNumber()),
|
||||
getAccountMapKey(account.getPhoneNumberIdentifier().toString()),
|
||||
getAccountEntityKey(account.getUuid()));
|
||||
|
||||
account.getUsername().ifPresent(username -> connection.sync().del(getAccountMapKey(username)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Account> dynamoGet(String number) {
|
||||
return accounts.get(number);
|
||||
}
|
||||
|
||||
private Optional<Account> dynamoGet(UUID uuid) {
|
||||
return accounts.get(uuid);
|
||||
}
|
||||
|
||||
private boolean dynamoCreate(Account account) {
|
||||
return accounts.create(account);
|
||||
}
|
||||
|
||||
private void dynamoUpdate(Account account) {
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
private void dynamoDelete(final Account account) {
|
||||
accounts.delete(account.getUuid());
|
||||
}
|
||||
|
||||
private void dynamoChangeNumber(final Account account, final String number) {
|
||||
accounts.changeNumber(account, number);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ public class ContactDiscoveryWriter extends AccountDatabaseCrawlerListener {
|
||||
// It’s less than ideal, but crawler listeners currently must not call update()
|
||||
// with the accounts from the chunk, because updates cause the account instance to become stale. Instead, they
|
||||
// must get a new copy, which they are free to update.
|
||||
accounts.get(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
|
||||
accounts.getByAccountIdentifier(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ public class Device {
|
||||
@JsonProperty
|
||||
private SignedPreKey signedPreKey;
|
||||
|
||||
@JsonProperty("pniSignedPreKey")
|
||||
private SignedPreKey phoneNumberIdentitySignedPreKey;
|
||||
|
||||
@JsonProperty
|
||||
private long lastSeen;
|
||||
|
||||
@@ -215,6 +218,14 @@ public class Device {
|
||||
this.signedPreKey = signedPreKey;
|
||||
}
|
||||
|
||||
public SignedPreKey getPhoneNumberIdentitySignedPreKey() {
|
||||
return phoneNumberIdentitySignedPreKey;
|
||||
}
|
||||
|
||||
public void setPhoneNumberIdentitySignedPreKey(final SignedPreKey phoneNumberIdentitySignedPreKey) {
|
||||
this.phoneNumberIdentitySignedPreKey = phoneNumberIdentitySignedPreKey;
|
||||
}
|
||||
|
||||
public long getPushTimestamp() {
|
||||
return pushTimestamp;
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class Key {
|
||||
|
||||
private final byte[] userMessageQueue;
|
||||
private final byte[] userMessageQueueMetadata;
|
||||
private final byte[] userMessageQueuePersistInProgress;
|
||||
|
||||
private final String address;
|
||||
private final long deviceId;
|
||||
|
||||
Key(String address, long deviceId) {
|
||||
this.address = address;
|
||||
this.deviceId = deviceId;
|
||||
this.userMessageQueue = ("user_queue::" + address + "::" + deviceId).getBytes();
|
||||
this.userMessageQueueMetadata = ("user_queue_metadata::" + address + "::" + deviceId).getBytes();
|
||||
this.userMessageQueuePersistInProgress = ("user_queue_persisting::" + address + "::" + deviceId).getBytes();
|
||||
}
|
||||
|
||||
String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
byte[] getUserMessageQueue() {
|
||||
return userMessageQueue;
|
||||
}
|
||||
|
||||
byte[] getUserMessageQueueMetadata() {
|
||||
return userMessageQueueMetadata;
|
||||
}
|
||||
|
||||
byte[] getUserMessageQueuePersistInProgress() {
|
||||
return userMessageQueuePersistInProgress;
|
||||
}
|
||||
|
||||
static byte[] getUserMessageQueueIndex() {
|
||||
return "user_queue_index".getBytes();
|
||||
}
|
||||
|
||||
static Key fromUserMessageQueue(byte[] userMessageQueue) throws IOException {
|
||||
try {
|
||||
String[] parts = new String(userMessageQueue).split("::");
|
||||
|
||||
if (parts.length != 3) {
|
||||
throw new IOException("Malformed key: " + new String(userMessageQueue));
|
||||
}
|
||||
|
||||
return new Key(parts[1], Long.parseLong(parts[2]));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-32
@@ -5,15 +5,12 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -32,7 +29,9 @@ import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Select;
|
||||
import software.amazon.awssdk.services.dynamodb.model.WriteRequest;
|
||||
|
||||
public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
public class Keys extends AbstractDynamoDbStore {
|
||||
|
||||
private final String tableName;
|
||||
|
||||
@@ -40,30 +39,29 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
static final String KEY_DEVICE_ID_KEY_ID = "DK";
|
||||
static final String KEY_PUBLIC_KEY = "P";
|
||||
|
||||
private static final Timer STORE_KEYS_TIMER = Metrics.timer(name(KeysDynamoDb.class, "storeKeys"));
|
||||
private static final Timer TAKE_KEY_FOR_DEVICE_TIMER = Metrics.timer(name(KeysDynamoDb.class, "takeKeyForDevice"));
|
||||
private static final Timer TAKE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "takeKeyForAccount"));
|
||||
private static final Timer GET_KEY_COUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "getKeyCount"));
|
||||
private static final Timer DELETE_KEYS_FOR_DEVICE_TIMER = Metrics.timer(name(KeysDynamoDb.class, "deleteKeysForDevice"));
|
||||
private static final Timer DELETE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(KeysDynamoDb.class, "deleteKeysForAccount"));
|
||||
private static final DistributionSummary CONTESTED_KEY_DISTRIBUTION = Metrics.summary(name(KeysDynamoDb.class, "contestedKeys"));
|
||||
private static final DistributionSummary KEY_COUNT_DISTRIBUTION = Metrics.summary(name(KeysDynamoDb.class, "keyCount"));
|
||||
private static final Timer STORE_KEYS_TIMER = Metrics.timer(name(Keys.class, "storeKeys"));
|
||||
private static final Timer TAKE_KEY_FOR_DEVICE_TIMER = Metrics.timer(name(Keys.class, "takeKeyForDevice"));
|
||||
private static final Timer GET_KEY_COUNT_TIMER = Metrics.timer(name(Keys.class, "getKeyCount"));
|
||||
private static final Timer DELETE_KEYS_FOR_DEVICE_TIMER = Metrics.timer(name(Keys.class, "deleteKeysForDevice"));
|
||||
private static final Timer DELETE_KEYS_FOR_ACCOUNT_TIMER = Metrics.timer(name(Keys.class, "deleteKeysForAccount"));
|
||||
private static final DistributionSummary CONTESTED_KEY_DISTRIBUTION = Metrics.summary(name(Keys.class, "contestedKeys"));
|
||||
private static final DistributionSummary KEY_COUNT_DISTRIBUTION = Metrics.summary(name(Keys.class, "keyCount"));
|
||||
|
||||
public KeysDynamoDb(final DynamoDbClient dynamoDB, final String tableName) {
|
||||
public Keys(final DynamoDbClient dynamoDB, final String tableName) {
|
||||
super(dynamoDB);
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
public void store(final Account account, final long deviceId, final List<PreKey> keys) {
|
||||
public void store(final UUID identifier, final long deviceId, final List<PreKey> keys) {
|
||||
STORE_KEYS_TIMER.record(() -> {
|
||||
delete(account.getUuid(), deviceId);
|
||||
delete(identifier, deviceId);
|
||||
|
||||
writeInBatches(keys, batch -> {
|
||||
List<WriteRequest> items = new ArrayList<>();
|
||||
for (final PreKey preKey : batch) {
|
||||
items.add(WriteRequest.builder()
|
||||
.putRequest(PutRequest.builder()
|
||||
.item(getItemFromPreKey(account.getUuid(), deviceId, preKey))
|
||||
.item(getItemFromPreKey(identifier, deviceId, preKey))
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
@@ -72,9 +70,9 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<PreKey> take(final Account account, final long deviceId) {
|
||||
public Optional<PreKey> take(final UUID identifier, final long deviceId) {
|
||||
return TAKE_KEY_FOR_DEVICE_TIMER.record(() -> {
|
||||
final AttributeValue partitionKey = getPartitionKey(account.getUuid());
|
||||
final AttributeValue partitionKey = getPartitionKey(identifier);
|
||||
QueryRequest queryRequest = QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)")
|
||||
@@ -113,26 +111,14 @@ public class KeysDynamoDb extends AbstractDynamoDbStore {
|
||||
});
|
||||
}
|
||||
|
||||
public Map<Long, PreKey> take(final Account account) {
|
||||
return TAKE_KEYS_FOR_ACCOUNT_TIMER.record(() -> {
|
||||
final Map<Long, PreKey> preKeysByDeviceId = new HashMap<>();
|
||||
|
||||
for (final Device device : account.getDevices()) {
|
||||
take(account, device.getId()).ifPresent(preKey -> preKeysByDeviceId.put(device.getId(), preKey));
|
||||
}
|
||||
|
||||
return preKeysByDeviceId;
|
||||
});
|
||||
}
|
||||
|
||||
public int getCount(final Account account, final long deviceId) {
|
||||
public int getCount(final UUID identifier, final long deviceId) {
|
||||
return GET_KEY_COUNT_TIMER.record(() -> {
|
||||
QueryRequest queryRequest = QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.keyConditionExpression("#uuid = :uuid AND begins_with (#sort, :sortprefix)")
|
||||
.expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#sort", KEY_DEVICE_ID_KEY_ID))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":uuid", getPartitionKey(account.getUuid()),
|
||||
":uuid", getPartitionKey(identifier),
|
||||
":sortprefix", getSortKeyPrefix(deviceId)))
|
||||
.select(Select.COUNT)
|
||||
.consistentRead(false)
|
||||
+1
-1
@@ -146,7 +146,7 @@ public class MessagePersister implements Managed {
|
||||
|
||||
@VisibleForTesting
|
||||
void persistQueue(final UUID accountUuid, final long deviceId) {
|
||||
final Optional<Account> maybeAccount = accountsManager.get(accountUuid);
|
||||
final Optional<Account> maybeAccount = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (maybeAccount.isEmpty()) {
|
||||
logger.error("No account record found for account {}", accountUuid);
|
||||
|
||||
@@ -167,7 +167,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
|
||||
for (final byte[] bytes : serialized) {
|
||||
try {
|
||||
removedMessages.add(constructEntityFromEnvelope(0, MessageProtos.Envelope.parseFrom(bytes)));
|
||||
removedMessages.add(constructEntityFromEnvelope(MessageProtos.Envelope.parseFrom(bytes)));
|
||||
} catch (final InvalidProtocolBufferException e) {
|
||||
logger.warn("Failed to parse envelope", e);
|
||||
}
|
||||
@@ -208,7 +208,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
|
||||
final long id = Long.parseLong(new String(queueItems.get(i + 1), StandardCharsets.UTF_8));
|
||||
|
||||
messageEntities.add(constructEntityFromEnvelope(id, message));
|
||||
messageEntities.add(constructEntityFromEnvelope(message));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
logger.warn("Failed to parse envelope", e);
|
||||
}
|
||||
@@ -376,8 +376,8 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static OutgoingMessageEntity constructEntityFromEnvelope(long id, MessageProtos.Envelope envelope) {
|
||||
return new OutgoingMessageEntity(id, true,
|
||||
static OutgoingMessageEntity constructEntityFromEnvelope(MessageProtos.Envelope envelope) {
|
||||
return new OutgoingMessageEntity(
|
||||
envelope.hasServerGuid() ? UUID.fromString(envelope.getServerGuid()) : null,
|
||||
envelope.getType().getNumber(),
|
||||
envelope.getRelay(),
|
||||
@@ -385,6 +385,7 @@ public class MessagesCache extends RedisClusterPubSubAdapter<String, String> imp
|
||||
envelope.getSource(),
|
||||
envelope.hasSourceUuid() ? UUID.fromString(envelope.getSourceUuid()) : null,
|
||||
envelope.getSourceDevice(),
|
||||
envelope.hasDestinationUuid() ? UUID.fromString(envelope.getDestinationUuid()) : null,
|
||||
envelope.hasLegacyMessage() ? envelope.getLegacyMessage().toByteArray() : null,
|
||||
envelope.hasContent() ? envelope.getContent().toByteArray() : null,
|
||||
envelope.hasServerTimestamp() ? envelope.getServerTimestamp() : 0);
|
||||
|
||||
+9
-1
@@ -46,6 +46,7 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
private static final String KEY_SOURCE = "SN";
|
||||
private static final String KEY_SOURCE_UUID = "SU";
|
||||
private static final String KEY_SOURCE_DEVICE = "SD";
|
||||
private static final String KEY_DESTINATION_UUID = "DU";
|
||||
private static final String KEY_MESSAGE = "M";
|
||||
private static final String KEY_CONTENT = "C";
|
||||
private static final String KEY_TTL = "E";
|
||||
@@ -86,6 +87,12 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
.put(KEY_TYPE, AttributeValues.fromInt(message.getType().getNumber()))
|
||||
.put(KEY_TIMESTAMP, AttributeValues.fromLong(message.getTimestamp()))
|
||||
.put(KEY_TTL, AttributeValues.fromLong(getTtlForMessage(message)));
|
||||
|
||||
// TODO All messages should have a destination UUID by 2021-12-03, and this can be set unconditionally
|
||||
if (message.hasDestinationUuid()) {
|
||||
item.put(KEY_DESTINATION_UUID, AttributeValues.fromUUID(UUID.fromString(message.getDestinationUuid())));
|
||||
}
|
||||
|
||||
if (message.hasRelay() && message.getRelay().length() > 0) {
|
||||
item.put(KEY_RELAY, AttributeValues.fromString(message.getRelay()));
|
||||
}
|
||||
@@ -224,9 +231,10 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
|
||||
final String source = AttributeValues.getString(message, KEY_SOURCE, null);
|
||||
final UUID sourceUuid = AttributeValues.getUUID(message, KEY_SOURCE_UUID, null);
|
||||
final int sourceDevice = AttributeValues.getInt(message, KEY_SOURCE_DEVICE, 0);
|
||||
final UUID destinationUuid = AttributeValues.getUUID(message, KEY_DESTINATION_UUID, null);
|
||||
final byte[] messageBytes = AttributeValues.getByteArray(message, KEY_MESSAGE, null);
|
||||
final byte[] content = AttributeValues.getByteArray(message, KEY_CONTENT, null);
|
||||
return new OutgoingMessageEntity(-1L, false, messageUuid, type, relay, timestamp, source, sourceUuid, sourceDevice, messageBytes, content, sortKey.getServerTimestamp());
|
||||
return new OutgoingMessageEntity(messageUuid, type, relay, timestamp, source, sourceUuid, sourceDevice, destinationUuid, messageBytes, content, sortKey.getServerTimestamp());
|
||||
}
|
||||
|
||||
private void deleteRowsMatchingQuery(AttributeValue partitionKey, QueryRequest querySpec) {
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public class NonNormalizedAccountCrawlerListener extends AccountDatabaseCrawlerL
|
||||
workingNonNormalizedNumbers++;
|
||||
|
||||
try {
|
||||
final Optional<Account> maybeConflictingAccount = accountsManager.get(getNormalizedNumber(account));
|
||||
final Optional<Account> maybeConflictingAccount = accountsManager.getByE164(getNormalizedNumber(account));
|
||||
|
||||
if (maybeConflictingAccount.isPresent()) {
|
||||
workingConflictingNumbers++;
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
/**
|
||||
* Manages a global, persistent mapping of phone numbers to phone number identifiers regardless of whether those
|
||||
* numbers/identifiers are actually associated with an account.
|
||||
*/
|
||||
public class PhoneNumberIdentifiers {
|
||||
|
||||
private final DynamoDbClient dynamoDbClient;
|
||||
private final String tableName;
|
||||
|
||||
@VisibleForTesting
|
||||
static final String KEY_E164 = "P";
|
||||
private static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
|
||||
|
||||
private static final Timer GET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "get"));
|
||||
private static final Timer SET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "set"));
|
||||
|
||||
public PhoneNumberIdentifiers(final DynamoDbClient dynamoDbClient, final String tableName) {
|
||||
this.dynamoDbClient = dynamoDbClient;
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the phone number identifier (PNI) associated with the given phone number.
|
||||
*
|
||||
* @param phoneNumber the phone number for which to retrieve a phone number identifier
|
||||
* @return the phone number identifier associated with the given phone number
|
||||
*/
|
||||
public UUID getPhoneNumberIdentifier(final String phoneNumber) {
|
||||
final GetItemResponse response = GET_PNI_TIMER.record(() -> dynamoDbClient.getItem(GetItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
|
||||
.projectionExpression(ATTR_PHONE_NUMBER_IDENTIFIER)
|
||||
.build()));
|
||||
|
||||
final UUID phoneNumberIdentifier;
|
||||
|
||||
if (response.hasItem()) {
|
||||
phoneNumberIdentifier = AttributeValues.getUUID(response.item(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
|
||||
} else {
|
||||
phoneNumberIdentifier = generatePhoneNumberIdentifierIfNotExists(phoneNumber);
|
||||
}
|
||||
|
||||
if (phoneNumberIdentifier == null) {
|
||||
throw new RuntimeException("Could not retrieve phone number identifier from stored item");
|
||||
}
|
||||
|
||||
return phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) {
|
||||
final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
|
||||
.updateExpression("SET #pni = if_not_exists(#pni, :pni)")
|
||||
.expressionAttributeNames(Map.of("#pni", ATTR_PHONE_NUMBER_IDENTIFIER))
|
||||
.expressionAttributeValues(Map.of(":pni", AttributeValues.fromUUID(UUID.randomUUID())))
|
||||
.returnValues(ReturnValue.ALL_NEW)
|
||||
.build()));
|
||||
|
||||
return AttributeValues.getUUID(response.attributes(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,235 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.whispersystems.textsecuregcm.storage.mappers.VersionedProfileMapper;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.paginators.QueryIterable;
|
||||
|
||||
public class Profiles {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String UID = "uuid";
|
||||
public static final String VERSION = "version";
|
||||
public static final String NAME = "name";
|
||||
public static final String AVATAR = "avatar";
|
||||
public static final String ABOUT_EMOJI = "about_emoji";
|
||||
public static final String ABOUT = "about";
|
||||
public static final String PAYMENT_ADDRESS = "payment_address";
|
||||
public static final String COMMITMENT = "commitment";
|
||||
private final DynamoDbClient dynamoDbClient;
|
||||
private final DynamoDbAsyncClient dynamoDbAsyncClient;
|
||||
private final String tableName;
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
// UUID of the account that owns this profile; byte array
|
||||
@VisibleForTesting
|
||||
static final String KEY_ACCOUNT_UUID = "U";
|
||||
|
||||
private final Timer setTimer = metricRegistry.timer(name(Profiles.class, "set" ));
|
||||
private final Timer getTimer = metricRegistry.timer(name(Profiles.class, "get" ));
|
||||
private final Timer deleteTimer = metricRegistry.timer(name(Profiles.class, "delete"));
|
||||
// Version of this profile; string
|
||||
@VisibleForTesting
|
||||
static final String ATTR_VERSION = "V";
|
||||
|
||||
private final FaultTolerantDatabase database;
|
||||
// User's name; string
|
||||
private static final String ATTR_NAME = "N";
|
||||
|
||||
public Profiles(FaultTolerantDatabase database) {
|
||||
this.database = database;
|
||||
this.database.getDatabase().registerRowMapper(new VersionedProfileMapper());
|
||||
// Avatar path/filename; string
|
||||
private static final String ATTR_AVATAR = "A";
|
||||
|
||||
// Bio/about text; string
|
||||
private static final String ATTR_ABOUT = "B";
|
||||
|
||||
// Bio/about emoji; string
|
||||
private static final String ATTR_EMOJI = "E";
|
||||
|
||||
// Payment address; string
|
||||
private static final String ATTR_PAYMENT_ADDRESS = "P";
|
||||
|
||||
// Commitment; byte array
|
||||
private static final String ATTR_COMMITMENT = "C";
|
||||
|
||||
private static final Map<String, String> UPDATE_EXPRESSION_ATTRIBUTE_NAMES = Map.of(
|
||||
"#commitment", ATTR_COMMITMENT,
|
||||
"#name", ATTR_NAME,
|
||||
"#avatar", ATTR_AVATAR,
|
||||
"#about", ATTR_ABOUT,
|
||||
"#aboutEmoji", ATTR_EMOJI,
|
||||
"#paymentAddress", ATTR_PAYMENT_ADDRESS);
|
||||
|
||||
private static final Timer SET_PROFILES_TIMER = Metrics.timer(name(Profiles.class, "set"));
|
||||
private static final Timer GET_PROFILE_TIMER = Metrics.timer(name(Profiles.class, "get"));
|
||||
private static final Timer DELETE_PROFILES_TIMER = Metrics.timer(name(Profiles.class, "delete"));
|
||||
|
||||
public Profiles(final DynamoDbClient dynamoDbClient,
|
||||
final DynamoDbAsyncClient dynamoDbAsyncClient,
|
||||
final String tableName) {
|
||||
|
||||
this.dynamoDbClient = dynamoDbClient;
|
||||
this.dynamoDbAsyncClient = dynamoDbAsyncClient;
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
public void set(UUID uuid, VersionedProfile profile) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context ignored = setTimer.time()) {
|
||||
handle.createUpdate(
|
||||
"INSERT INTO profiles ("
|
||||
+ UID + ", "
|
||||
+ VERSION + ", "
|
||||
+ NAME + ", "
|
||||
+ AVATAR + ", "
|
||||
+ ABOUT_EMOJI + ", "
|
||||
+ ABOUT + ", "
|
||||
+ PAYMENT_ADDRESS + ", "
|
||||
+ COMMITMENT + ") "
|
||||
+ "VALUES (:uuid, :version, :name, :avatar, :about_emoji, :about, :payment_address, :commitment) "
|
||||
+ "ON CONFLICT (" + UID + ", " + VERSION + ") "
|
||||
+ "DO UPDATE SET "
|
||||
+ NAME + " = EXCLUDED." + NAME + ", "
|
||||
+ AVATAR + " = EXCLUDED." + AVATAR + ", "
|
||||
+ ABOUT + " = EXCLUDED." + ABOUT + ", "
|
||||
+ ABOUT_EMOJI + " = EXCLUDED." + ABOUT_EMOJI + ", "
|
||||
+ PAYMENT_ADDRESS + " = EXCLUDED." + PAYMENT_ADDRESS)
|
||||
.bind("uuid", uuid)
|
||||
.bind("version", profile.getVersion())
|
||||
.bind("name", profile.getName())
|
||||
.bind("avatar", profile.getAvatar())
|
||||
.bind("about_emoji", profile.getAboutEmoji())
|
||||
.bind("about", profile.getAbout())
|
||||
.bind("payment_address", profile.getPaymentAddress())
|
||||
.bind("commitment", profile.getCommitment())
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
public void set(final UUID uuid, final VersionedProfile profile) {
|
||||
SET_PROFILES_TIMER.record(() -> {
|
||||
dynamoDbClient.updateItem(UpdateItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(buildPrimaryKey(uuid, profile.getVersion()))
|
||||
.updateExpression(buildUpdateExpression(profile))
|
||||
.expressionAttributeNames(UPDATE_EXPRESSION_ATTRIBUTE_NAMES)
|
||||
.expressionAttributeValues(buildUpdateExpressionAttributeValues(profile))
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<VersionedProfile> get(UUID uuid, String version) {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context ignored = getTimer.time()) {
|
||||
return handle.createQuery("SELECT * FROM profiles WHERE " + UID + " = :uuid AND " + VERSION + " = :version")
|
||||
.bind("uuid", uuid)
|
||||
.bind("version", version)
|
||||
.mapTo(VersionedProfile.class)
|
||||
.findFirst();
|
||||
}
|
||||
}));
|
||||
private static Map<String, AttributeValue> buildPrimaryKey(final UUID uuid, final String version) {
|
||||
return Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
|
||||
ATTR_VERSION, AttributeValues.fromString(version));
|
||||
}
|
||||
|
||||
public void deleteAll(UUID uuid) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context ignored = deleteTimer.time()) {
|
||||
handle.createUpdate("DELETE FROM profiles WHERE " + UID + " = :uuid")
|
||||
.bind("uuid", uuid)
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
@VisibleForTesting
|
||||
static String buildUpdateExpression(final VersionedProfile profile) {
|
||||
final List<String> updatedAttributes = new ArrayList<>(5);
|
||||
final List<String> deletedAttributes = new ArrayList<>(5);
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getName())) {
|
||||
updatedAttributes.add("name");
|
||||
} else {
|
||||
deletedAttributes.add("name");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getAvatar())) {
|
||||
updatedAttributes.add("avatar");
|
||||
} else {
|
||||
deletedAttributes.add("avatar");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getAbout())) {
|
||||
updatedAttributes.add("about");
|
||||
} else {
|
||||
deletedAttributes.add("about");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getAboutEmoji())) {
|
||||
updatedAttributes.add("aboutEmoji");
|
||||
} else {
|
||||
deletedAttributes.add("aboutEmoji");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getPaymentAddress())) {
|
||||
updatedAttributes.add("paymentAddress");
|
||||
} else {
|
||||
deletedAttributes.add("paymentAddress");
|
||||
}
|
||||
|
||||
final StringBuilder updateExpressionBuilder = new StringBuilder(
|
||||
"SET #commitment = if_not_exists(#commitment, :commitment)");
|
||||
|
||||
if (!updatedAttributes.isEmpty()) {
|
||||
updatedAttributes.forEach(token -> updateExpressionBuilder
|
||||
.append(", #")
|
||||
.append(token)
|
||||
.append(" = :")
|
||||
.append(token));
|
||||
}
|
||||
|
||||
if (!deletedAttributes.isEmpty()) {
|
||||
updateExpressionBuilder.append(" REMOVE ");
|
||||
updateExpressionBuilder.append(deletedAttributes.stream()
|
||||
.map(token -> "#" + token)
|
||||
.collect(Collectors.joining(", ")));
|
||||
}
|
||||
|
||||
return updateExpressionBuilder.toString();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Map<String, AttributeValue> buildUpdateExpressionAttributeValues(final VersionedProfile profile) {
|
||||
final Map<String, AttributeValue> expressionValues = new HashMap<>();
|
||||
|
||||
expressionValues.put(":commitment", AttributeValues.fromByteArray(profile.getCommitment()));
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getName())) {
|
||||
expressionValues.put(":name", AttributeValues.fromString(profile.getName()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getAvatar())) {
|
||||
expressionValues.put(":avatar", AttributeValues.fromString(profile.getAvatar()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getAbout())) {
|
||||
expressionValues.put(":about", AttributeValues.fromString(profile.getAbout()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getAboutEmoji())) {
|
||||
expressionValues.put(":aboutEmoji", AttributeValues.fromString(profile.getAboutEmoji()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(profile.getPaymentAddress())) {
|
||||
expressionValues.put(":paymentAddress", AttributeValues.fromString(profile.getPaymentAddress()));
|
||||
}
|
||||
|
||||
return expressionValues;
|
||||
}
|
||||
|
||||
public Optional<VersionedProfile> get(final UUID uuid, final String version) {
|
||||
return GET_PROFILE_TIMER.record(() -> {
|
||||
final GetItemResponse response = dynamoDbClient.getItem(GetItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(buildPrimaryKey(uuid, version))
|
||||
.consistentRead(true)
|
||||
.build());
|
||||
|
||||
return response.hasItem() ? Optional.of(fromItem(response.item())) : Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
private static VersionedProfile fromItem(final Map<String, AttributeValue> item) {
|
||||
return new VersionedProfile(
|
||||
AttributeValues.getString(item, ATTR_VERSION, null),
|
||||
AttributeValues.getString(item, ATTR_NAME, null),
|
||||
AttributeValues.getString(item, ATTR_AVATAR, null),
|
||||
AttributeValues.getString(item, ATTR_EMOJI, null),
|
||||
AttributeValues.getString(item, ATTR_ABOUT, null),
|
||||
AttributeValues.getString(item, ATTR_PAYMENT_ADDRESS, null),
|
||||
AttributeValues.getByteArray(item, ATTR_COMMITMENT, null));
|
||||
}
|
||||
|
||||
public void deleteAll(final UUID uuid) {
|
||||
DELETE_PROFILES_TIMER.record(() -> {
|
||||
final AttributeValue uuidAttributeValue = AttributeValues.fromUUID(uuid);
|
||||
|
||||
final QueryIterable queryIterable = dynamoDbClient.queryPaginator(QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.keyConditionExpression("#uuid = :uuid")
|
||||
.expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID))
|
||||
.expressionAttributeValues(Map.of(":uuid", uuidAttributeValue))
|
||||
.projectionExpression(ATTR_VERSION)
|
||||
.consistentRead(true)
|
||||
.build());
|
||||
|
||||
CompletableFuture.allOf(queryIterable.items().stream()
|
||||
.map(item -> dynamoDbAsyncClient.deleteItem(DeleteItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(
|
||||
KEY_ACCOUNT_UUID, uuidAttributeValue,
|
||||
ATTR_VERSION, item.get(ATTR_VERSION)))
|
||||
.build()))
|
||||
.toArray(CompletableFuture[]::new)).join();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -8,29 +8,29 @@ package org.whispersystems.textsecuregcm.storage;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.lettuce.core.RedisException;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ProfilesManager {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ProfilesManager.class);
|
||||
|
||||
private static final String CACHE_PREFIX = "profiles::";
|
||||
|
||||
private final Profiles profiles;
|
||||
private final Profiles profiles;
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final ObjectMapper mapper;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public ProfilesManager(Profiles profiles, FaultTolerantRedisCluster cacheCluster) {
|
||||
this.profiles = profiles;
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.mapper = SystemMapper.getMapper();
|
||||
public ProfilesManager(final Profiles profiles,
|
||||
final FaultTolerantRedisCluster cacheCluster) {
|
||||
this.profiles = profiles;
|
||||
this.cacheCluster = cacheCluster;
|
||||
this.mapper = SystemMapper.getMapper();
|
||||
}
|
||||
|
||||
public void set(UUID uuid, VersionedProfile versionedProfile) {
|
||||
@@ -46,7 +46,7 @@ public class ProfilesManager {
|
||||
public Optional<VersionedProfile> get(UUID uuid, String version) {
|
||||
Optional<VersionedProfile> profile = memcacheGet(uuid, version);
|
||||
|
||||
if (!profile.isPresent()) {
|
||||
if (profile.isEmpty()) {
|
||||
profile = profiles.get(uuid, version);
|
||||
profile.ifPresent(versionedProfile -> memcacheSet(uuid, versionedProfile));
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
|
||||
|
||||
if (update) {
|
||||
// fetch a new version, since the chunk is shared and implicitly read-only
|
||||
accountsManager.get(account.getUuid()).ifPresent(accountToUpdate -> {
|
||||
accountsManager.getByAccountIdentifier(account.getUuid()).ifPresent(accountToUpdate -> {
|
||||
accountsManager.update(accountToUpdate, a -> {
|
||||
for (Device device : a.getDevices()) {
|
||||
if (deviceNeedsUpdate(device)) {
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ public class RefreshingAccountAndDeviceSupplier implements Supplier<Pair<Account
|
||||
@Override
|
||||
public Pair<Account, Device> get() {
|
||||
if (account.isStale()) {
|
||||
account = accountsManager.get(account.getUuid())
|
||||
account = accountsManager.getByAccountIdentifier(account.getUuid())
|
||||
.orElseThrow(() -> new RuntimeException("Could not find account"));
|
||||
device = account.getDevice(device.getId())
|
||||
.orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device"));
|
||||
|
||||
@@ -1,75 +1,114 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import com.codahale.metrics.Timer;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
import software.amazon.awssdk.core.SdkBytes;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.whispersystems.textsecuregcm.storage.mappers.RemoteConfigRowMapper;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class RemoteConfigs {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String NAME = "name";
|
||||
public static final String PERCENTAGE = "percentage";
|
||||
public static final String UUIDS = "uuids";
|
||||
public static final String DEFAULT_VALUE = "default_value";
|
||||
public static final String VALUE = "value";
|
||||
public static final String HASH_KEY = "hash_key";
|
||||
private final DynamoDbClient dynamoDbClient;
|
||||
private final String tableName;
|
||||
|
||||
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
private final Timer setTimer = metricRegistry.timer(name(RemoteConfigs.class, "set" ));
|
||||
private final Timer getAllTimer = metricRegistry.timer(name(RemoteConfigs.class, "getAll"));
|
||||
private final Timer deleteTimer = metricRegistry.timer(name(RemoteConfigs.class, "delete"));
|
||||
// Config name; string
|
||||
static final String KEY_NAME = "N";
|
||||
// Rollout percentage; integer
|
||||
private static final String ATTR_PERCENTAGE = "P";
|
||||
// Enrolled UUIDs (ACIs); list of byte arrays
|
||||
private static final String ATTR_UUIDS = "U";
|
||||
// Default value; string
|
||||
private static final String ATTR_DEFAULT_VALUE = "D";
|
||||
// Value when enrolled; string
|
||||
private static final String ATTR_VALUE = "V";
|
||||
// Hash key; string
|
||||
private static final String ATTR_HASH_KEY = "H";
|
||||
|
||||
private final FaultTolerantDatabase database;
|
||||
|
||||
public RemoteConfigs(FaultTolerantDatabase database) {
|
||||
this.database = database;
|
||||
this.database.getDatabase().registerRowMapper(new RemoteConfigRowMapper());
|
||||
this.database.getDatabase().registerArrayType(UUID.class, "uuid");
|
||||
public RemoteConfigs(final DynamoDbClient dynamoDbClient, final String tableName) {
|
||||
this.dynamoDbClient = dynamoDbClient;
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
public void set(RemoteConfig remoteConfig) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context ignored = setTimer.time()) {
|
||||
handle.createUpdate("INSERT INTO remote_config (" + NAME + ", " + PERCENTAGE + ", " + UUIDS + ", " + DEFAULT_VALUE + ", " + VALUE + ", " + HASH_KEY + ") VALUES (:name, :percentage, :uuids, :default_value, :value, :hash_key) ON CONFLICT(" + NAME + ") DO UPDATE SET " + PERCENTAGE + " = EXCLUDED." + PERCENTAGE + ", " + UUIDS + " = EXCLUDED." + UUIDS + ", " + DEFAULT_VALUE + " = EXCLUDED." + DEFAULT_VALUE + ", " + VALUE + " = EXCLUDED." + VALUE + ", " + HASH_KEY + " = EXCLUDED." + HASH_KEY)
|
||||
.bind("name", remoteConfig.getName())
|
||||
.bind("percentage", remoteConfig.getPercentage())
|
||||
.bind("uuids", remoteConfig.getUuids().toArray(new UUID[0]))
|
||||
.bind("default_value", remoteConfig.getDefaultValue())
|
||||
.bind("value", remoteConfig.getValue())
|
||||
.bind("hash_key", remoteConfig.getHashKey())
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
public void set(final RemoteConfig remoteConfig) {
|
||||
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
|
||||
KEY_NAME, AttributeValues.fromString(remoteConfig.getName()),
|
||||
ATTR_PERCENTAGE, AttributeValues.fromInt(remoteConfig.getPercentage())));
|
||||
|
||||
if (remoteConfig.getUuids() != null && !remoteConfig.getUuids().isEmpty()) {
|
||||
final List<SdkBytes> uuidByteSets = remoteConfig.getUuids().stream()
|
||||
.map(UUIDUtil::toByteBuffer)
|
||||
.map(SdkBytes::fromByteBuffer)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
item.put(ATTR_UUIDS, AttributeValue.builder().bs(uuidByteSets).build());
|
||||
}
|
||||
|
||||
if (remoteConfig.getDefaultValue() != null) {
|
||||
item.put(ATTR_DEFAULT_VALUE, AttributeValues.fromString(remoteConfig.getDefaultValue()));
|
||||
}
|
||||
|
||||
if (remoteConfig.getValue() != null) {
|
||||
item.put(ATTR_VALUE, AttributeValues.fromString(remoteConfig.getValue()));
|
||||
}
|
||||
|
||||
if (remoteConfig.getHashKey() != null) {
|
||||
item.put(ATTR_HASH_KEY, AttributeValues.fromString(remoteConfig.getHashKey()));
|
||||
}
|
||||
|
||||
dynamoDbClient.putItem(PutItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.item(item)
|
||||
.build());
|
||||
}
|
||||
|
||||
public List<RemoteConfig> getAll() {
|
||||
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||
try (Timer.Context ignored = getAllTimer.time()) {
|
||||
return handle.createQuery("SELECT * FROM remote_config")
|
||||
.mapTo(RemoteConfig.class)
|
||||
.list();
|
||||
}
|
||||
}));
|
||||
return dynamoDbClient.scanPaginator(ScanRequest.builder()
|
||||
.tableName(tableName)
|
||||
.consistentRead(true)
|
||||
.build())
|
||||
.items()
|
||||
.stream()
|
||||
.map(item -> {
|
||||
final String name = AttributeValues.getString(item, KEY_NAME, null);
|
||||
final int percentage = AttributeValues.getInt(item, ATTR_PERCENTAGE, 0);
|
||||
final String defaultValue = AttributeValues.getString(item, ATTR_DEFAULT_VALUE, null);
|
||||
final String value = AttributeValues.getString(item, ATTR_VALUE, null);
|
||||
final String hashKey = AttributeValues.getString(item, ATTR_HASH_KEY, null);
|
||||
|
||||
final Set<UUID> uuids;
|
||||
|
||||
if (item.containsKey(ATTR_UUIDS)) {
|
||||
uuids = item.get(ATTR_UUIDS).bs().stream()
|
||||
.map(sdkBytes -> UUIDUtil.fromByteBuffer(sdkBytes.asByteBuffer()))
|
||||
.collect(Collectors.toSet());
|
||||
} else {
|
||||
uuids = Collections.emptySet();
|
||||
}
|
||||
|
||||
return new RemoteConfig(name, percentage, uuids, defaultValue, value, hashKey);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void delete(String name) {
|
||||
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||
try (Timer.Context ignored = deleteTimer.time()) {
|
||||
handle.createUpdate("DELETE FROM remote_config WHERE " + NAME + " = :name")
|
||||
.bind("name", name)
|
||||
.execute();
|
||||
}
|
||||
}));
|
||||
public void delete(final String name) {
|
||||
dynamoDbClient.deleteItem(DeleteItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_NAME, AttributeValues.fromString(name)))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
+7
-56
@@ -5,70 +5,26 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import com.google.common.base.Suppliers;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class RemoteConfigsManager implements Managed {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RemoteConfigsManager.class);
|
||||
public class RemoteConfigsManager {
|
||||
|
||||
private final RemoteConfigs remoteConfigs;
|
||||
private final long sleepInterval;
|
||||
|
||||
private final AtomicReference<List<RemoteConfig>> cachedConfigs = new AtomicReference<>(new LinkedList<>());
|
||||
private final Supplier<List<RemoteConfig>> remoteConfigSupplier;
|
||||
|
||||
public RemoteConfigsManager(RemoteConfigs remoteConfigs) {
|
||||
this(remoteConfigs, TimeUnit.SECONDS.toMillis(10));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public RemoteConfigsManager(RemoteConfigs remoteConfigs, long sleepInterval) {
|
||||
this.remoteConfigs = remoteConfigs;
|
||||
this.sleepInterval = sleepInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
refreshCache();
|
||||
|
||||
new Thread(() -> {
|
||||
while (true) {
|
||||
try {
|
||||
refreshCache();
|
||||
} catch (Throwable t) {
|
||||
logger.warn("Error updating remote configs cache", t);
|
||||
}
|
||||
|
||||
Util.sleep(sleepInterval);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void refreshCache() {
|
||||
this.cachedConfigs.set(remoteConfigs.getAll());
|
||||
|
||||
synchronized (this.cachedConfigs) {
|
||||
this.cachedConfigs.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void waitForCacheRefresh() throws InterruptedException {
|
||||
synchronized (this.cachedConfigs) {
|
||||
this.cachedConfigs.wait();
|
||||
}
|
||||
remoteConfigSupplier =
|
||||
Suppliers.memoizeWithExpiration(remoteConfigs::getAll, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public List<RemoteConfig> getAll() {
|
||||
return cachedConfigs.get();
|
||||
return remoteConfigSupplier.get();
|
||||
}
|
||||
|
||||
public void set(RemoteConfig config) {
|
||||
@@ -78,9 +34,4 @@ public class RemoteConfigsManager implements Managed {
|
||||
public void delete(String name) {
|
||||
remoteConfigs.delete(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ public class ReportMessageManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void report(String sourceNumber, UUID messageGuid) {
|
||||
public void report(String sourceNumber, UUID messageGuid, UUID reporterUuid) {
|
||||
|
||||
final boolean found = reportMessageDynamoDb.remove(hash(messageGuid, sourceNumber));
|
||||
|
||||
@@ -62,7 +62,7 @@ public class ReportMessageManager {
|
||||
rateLimitCluster.useCluster(connection -> {
|
||||
final String reportedSenderKey = getReportedSenderKey(sourceNumber);
|
||||
|
||||
connection.sync().pfadd(reportedSenderKey, sourceNumber);
|
||||
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
|
||||
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user